Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.41% covered (danger)
49.41%
42 / 85
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
DOMProcessorPipeline
49.41% covered (danger)
49.41%
42 / 85
33.33% covered (danger)
33.33%
3 / 9
155.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTimeProfile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 registerProcessors
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 setSrcOffsets
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doPostProcess
46.77% covered (danger)
46.77%
29 / 62
0.00% covered (danger)
0.00%
0 / 1
66.86
 process
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 processChunkily
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 resetState
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 finalize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html;
5
6use Generator;
7use stdClass;
8use Wikimedia\Parsoid\Config\Env;
9use Wikimedia\Parsoid\Core\SelectiveUpdateData;
10use Wikimedia\Parsoid\Core\SourceRange;
11use Wikimedia\Parsoid\DOM\DocumentFragment;
12use Wikimedia\Parsoid\DOM\Element;
13use Wikimedia\Parsoid\DOM\Node;
14use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
15use Wikimedia\Parsoid\Utils\ContentUtils;
16use Wikimedia\Parsoid\Wt2Html\DOM\Processors\DOMPPTraverser;
17
18/**
19 * Perform post-processing steps on an already-built HTML DOM.
20 */
21class DOMProcessorPipeline extends PipelineStage {
22    private array $options;
23    /** @var array[] */
24    private array $processors = [];
25    private ParsoidExtensionAPI $extApi; // Provides post-processing support to extensions
26    private string $timeProfile = '';
27    private ?SelectiveUpdateData $selparData = null;
28    private ?stdClass $tplInfo = null;
29
30    public function __construct( Env $env, array $options = [], string $stageId = "" ) {
31        parent::__construct( $env );
32        $this->options = $options;
33        $this->extApi = new ParsoidExtensionAPI( $env );
34    }
35
36    public function getTimeProfile(): string {
37        return $this->timeProfile;
38    }
39
40    public function registerProcessors( array $processors ): void {
41        foreach ( $processors as $p ) {
42            if ( isset( $p['Processor'] ) ) {
43                // Internal processor w/ ::run() method, class name given
44                $p['proc'] = new $p['Processor']( $this );
45            } else {
46                $t = new DOMPPTraverser( $this, $p['tplInfo'] ?? false );
47                foreach ( $p['handlers'] as $h ) {
48                    $t->addHandler( $h['nodeName'], $h['action'] );
49                }
50                $p['proc'] = $t;
51            }
52            $this->processors[] = $p;
53        }
54    }
55
56    /**
57     * @inheritDoc
58     */
59    public function setSrcOffsets( SourceRange $srcOffsets ): void {
60        $this->options['srcOffsets'] = $srcOffsets;
61    }
62
63    public function doPostProcess( Node $node ): void {
64        $env = $this->env;
65
66        $hasDumpFlags = $env->hasDumpFlags();
67
68        // FIXME: This works right now, but may not always be the right place to dump
69        // if custom DOM pipelines start getting more specialized and we enter this
70        // pipeline immediate after tree building.
71        if ( $hasDumpFlags && $env->hasDumpFlag( 'dom:post-builder' ) ) {
72            $opts = [];
73            $env->writeDump( ContentUtils::dumpDOM( $node, 'DOM: after tree builder', $opts ) );
74        }
75
76        $prefix = null;
77        $resourceCategory = null;
78
79        $profile = null;
80        if ( $env->profiling() ) {
81            $profile = $env->getCurrentProfile();
82            if ( $this->atTopLevel ) {
83                $this->timeProfile = str_repeat( "-", 85 ) . "\n";
84                $prefix = 'TOP';
85                // Turn off DOM pass timing tracing on non-top-level documents
86                $resourceCategory = 'DOMPasses:TOP';
87            } else {
88                $prefix = '---';
89                $resourceCategory = 'DOMPasses:NESTED';
90            }
91        }
92
93        foreach ( $this->processors as $pp ) {
94            // This is an optimization for the 'AddAnnotationIds' handler
95            // which is embedded in a DOMTraverser where we cannot check this flag.
96            if ( !empty( $pp['withAnnotations'] ) && !$this->env->hasAnnotations ) {
97                continue;
98            }
99
100            $ppName = null;
101            $ppStart = null;
102
103            // Trace
104            if ( $profile ) {
105                $ppName = $pp['name'] . str_repeat(
106                    " ",
107                    ( strlen( $pp['name'] ) < 30 ) ? 30 - strlen( $pp['name'] ) : 0
108                );
109                $ppStart = hrtime( true );
110            }
111
112            $opts = null;
113            if ( $hasDumpFlags ) {
114                $opts = [
115                    'env' => $env,
116                    'dumpFragmentMap' => $this->atTopLevel,
117                    'keepTmp' => true
118                ];
119
120                if ( $env->hasDumpFlag( 'dom:pre-' . $pp['shortcut'] )
121                    || $env->hasDumpFlag( 'dom:pre-*' )
122                ) {
123                    $env->writeDump(
124                        ContentUtils::dumpDOM( $node, 'DOM: pre-' . $pp['shortcut'], $opts )
125                    );
126                }
127            }
128
129            // FIXME: env, extApi, frame, selparData, options, atTopLevel can all be
130            // put into a stdclass or a real class (DOMProcConfig?) and passed around.
131            $pp['proc']->run(
132                $this->env,
133                $node,
134                [
135                    'extApi' => $this->extApi,
136                    'frame' => $this->frame,
137                    'selparData' => $this->selparData,
138                    // For linting embedded docs
139                    'tplInfo' => $this->tplInfo,
140                ] + $this->options,
141                $this->atTopLevel
142            );
143
144            if ( $hasDumpFlags && ( $env->hasDumpFlag( 'dom:post-' . $pp['shortcut'] )
145                || $env->hasDumpFlag( 'dom:post-*' ) )
146            ) {
147                $env->writeDump(
148                    ContentUtils::dumpDOM( $node, 'DOM: post-' . $pp['shortcut'], $opts )
149                );
150            }
151
152            if ( $profile ) {
153                $ppElapsed = hrtime( true ) - $ppStart;
154                if ( $this->atTopLevel ) {
155                    $this->timeProfile .= str_pad( $prefix . '; ' . $ppName, 65 ) .
156                        ' time = ' .
157                        str_pad( number_format( $ppElapsed / 1000000, 2 ), 10, ' ', STR_PAD_LEFT ) . "\n";
158                }
159                $profile->bumpTimeUse( $resourceCategory, $ppElapsed, 'DOM' );
160            }
161        }
162    }
163
164    /**
165     * @inheritDoc
166     */
167    public function process(
168        string|array|DocumentFragment|Element $input,
169        array $options
170    ): array|Element|DocumentFragment {
171        if ( isset( $options['selparData'] ) ) {
172            $this->selparData = $options['selparData'];
173        }
174        '@phan-var Node $input'; // @var Node $input
175        $this->doPostProcess( $input );
176        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
177        return $input;
178    }
179
180    /**
181     * @inheritDoc
182     */
183    public function processChunkily(
184        string|array|DocumentFragment|Element $input,
185        array $options
186    ): Generator {
187        if ( $input !== [] ) {
188            $this->process( $input, $options );
189            yield $input;
190        }
191    }
192
193    /**
194     * @inheritDoc
195     */
196    public function resetState( array $options ): void {
197        parent::resetState( $options );
198        $this->tplInfo = $options['tplInfo'] ?? null;
199    }
200
201    /**
202     * @inheritDoc
203     */
204    public function finalize(): Generator {
205        yield [];
206    }
207}