Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.62% covered (danger)
47.62%
40 / 84
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
DOMProcessorPipeline
47.62% covered (danger)
47.62%
40 / 84
28.57% covered (danger)
28.57%
2 / 7
149.87
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
 setSourceOffsets
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 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html;
5
6use Generator;
7use Wikimedia\Parsoid\Config\Env;
8use Wikimedia\Parsoid\Core\SelectiveUpdateData;
9use Wikimedia\Parsoid\DOM\Node;
10use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
11use Wikimedia\Parsoid\Tokens\SourceRange;
12use Wikimedia\Parsoid\Utils\ContentUtils;
13use Wikimedia\Parsoid\Wt2Html\DOM\Processors\DOMPPTraverser;
14
15/**
16 * Perform post-processing steps on an already-built HTML DOM.
17 */
18class DOMProcessorPipeline extends PipelineStage {
19    private array $options;
20    /** @var array[] */
21    private array $processors = [];
22    private ParsoidExtensionAPI $extApi; // Provides post-processing support to extensions
23    private string $timeProfile = '';
24    private ?SelectiveUpdateData $selparData = null;
25
26    public function __construct(
27        Env $env, array $options = [], string $stageId = "",
28        ?PipelineStage $prevStage = null
29    ) {
30        parent::__construct( $env, $prevStage );
31
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 setSourceOffsets( SourceRange $so ): void {
60        $this->options['sourceOffsets'] = $so;
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        $traceLevel = null;
78        $resourceCategory = null;
79
80        $profile = null;
81        if ( $env->profiling() ) {
82            $profile = $env->getCurrentProfile();
83            if ( $this->atTopLevel ) {
84                $this->timeProfile = str_repeat( "-", 85 ) . "\n";
85                $prefix = 'TOP';
86                // Turn off DOM pass timing tracing on non-top-level documents
87                $resourceCategory = 'DOMPasses:TOP';
88            } else {
89                $prefix = '---';
90                $resourceCategory = 'DOMPasses:NESTED';
91            }
92        }
93
94        foreach ( $this->processors as $pp ) {
95            // This is an optimization for the 'AddAnnotationIds' handler
96            // which is embedded in a DOMTraverser where we cannot check this flag.
97            if ( !empty( $pp['withAnnotations'] ) && !$this->env->hasAnnotations ) {
98                continue;
99            }
100
101            $ppName = null;
102            $ppStart = null;
103
104            // Trace
105            if ( $profile ) {
106                $ppName = $pp['name'] . str_repeat(
107                    " ",
108                    ( strlen( $pp['name'] ) < 30 ) ? 30 - strlen( $pp['name'] ) : 0
109                );
110                $ppStart = microtime( true );
111            }
112
113            $opts = null;
114            if ( $hasDumpFlags ) {
115                $opts = [
116                    'env' => $env,
117                    'dumpFragmentMap' => $this->atTopLevel,
118                    'keepTmp' => true
119                ];
120
121                if ( $env->hasDumpFlag( 'dom:pre-' . $pp['shortcut'] )
122                    || $env->hasDumpFlag( 'dom:pre-*' )
123                ) {
124                    $env->writeDump(
125                        ContentUtils::dumpDOM( $node, 'DOM: pre-' . $pp['shortcut'], $opts )
126                    );
127                }
128            }
129
130            // FIXME: env, extApi, frame, selparData, options, atTopLevel can all be
131            // put into a stdclass or a real class (DOMProcConfig?) and passed around.
132            $pp['proc']->run(
133                $this->env,
134                $node,
135                [
136                    'extApi' => $this->extApi,
137                    'frame' => $this->frame,
138                    'selparData' => $this->selparData,
139                ] + $this->options,
140                $this->atTopLevel
141            );
142
143            if ( $hasDumpFlags && ( $env->hasDumpFlag( 'dom:post-' . $pp['shortcut'] )
144                || $env->hasDumpFlag( 'dom:post-*' ) )
145            ) {
146                $env->writeDump(
147                    ContentUtils::dumpDOM( $node, 'DOM: post-' . $pp['shortcut'], $opts )
148                );
149            }
150
151            if ( $profile ) {
152                $ppElapsed = 1000 * ( microtime( true ) - $ppStart );
153                if ( $this->atTopLevel ) {
154                    $this->timeProfile .= str_pad( $prefix . '; ' . $ppName, 65 ) .
155                        ' time = ' .
156                        str_pad( number_format( $ppElapsed, 2 ), 10, ' ', STR_PAD_LEFT ) . "\n";
157                }
158                $profile->bumpTimeUse( $resourceCategory, $ppElapsed, 'DOM' );
159            }
160        }
161    }
162
163    /**
164     * @inheritDoc
165     */
166    public function process( $node, array $opts ) {
167        if ( isset( $opts['selparData'] ) ) {
168            $this->selparData = $opts['selparData'];
169        }
170        '@phan-var Node $node'; // @var Node $node
171        $this->doPostProcess( $node );
172        // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
173        return $node;
174    }
175
176    /**
177     * @inheritDoc
178     */
179    public function processChunkily( $input, array $options ): Generator {
180        if ( $this->prevStage ) {
181            // The previous stage will yield a DOM.
182            // FIXME: Should we change the signature of that to return a DOM
183            // If we do so, a pipeline stage returns either a generator or
184            // concrete output (in this case, a DOM).
185            $node = $this->prevStage->processChunkily( $input, $options )->current();
186        } else {
187            $node = $input;
188        }
189        $this->process( $node, $options );
190        yield $node;
191    }
192}