Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
47.62% |
40 / 84 |
|
28.57% |
2 / 7 |
CRAP | |
0.00% |
0 / 1 |
DOMProcessorPipeline | |
47.62% |
40 / 84 |
|
28.57% |
2 / 7 |
149.87 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTimeProfile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
registerProcessors | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
setSourceOffsets | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doPostProcess | |
46.77% |
29 / 62 |
|
0.00% |
0 / 1 |
66.86 | |||
process | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
processChunkily | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Wt2Html; |
5 | |
6 | use Generator; |
7 | use Wikimedia\Parsoid\Config\Env; |
8 | use Wikimedia\Parsoid\Core\SelectiveUpdateData; |
9 | use Wikimedia\Parsoid\DOM\Node; |
10 | use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI; |
11 | use Wikimedia\Parsoid\Tokens\SourceRange; |
12 | use Wikimedia\Parsoid\Utils\ContentUtils; |
13 | use Wikimedia\Parsoid\Wt2Html\DOM\Processors\DOMPPTraverser; |
14 | |
15 | /** |
16 | * Perform post-processing steps on an already-built HTML DOM. |
17 | */ |
18 | class 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 | } |