Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.00% |
192 / 200 |
|
61.11% |
11 / 18 |
CRAP | |
0.00% |
0 / 1 |
RemexCompatMunger | |
96.00% |
192 / 200 |
|
61.11% |
11 / 18 |
71 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
startDocument | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
endDocument | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParentForInsert | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
6 | |||
insertPWrapper | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
characters | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
7 | |||
trace | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 | |||
insertElement | |
100.00% |
51 / 51 |
|
100.00% |
1 / 1 |
26 | |||
splitTagStack | |
97.96% |
48 / 49 |
|
0.00% |
0 / 1 |
10 | |||
disablePWrapper | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
2.00 | |||
endTag | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
doctype | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
comment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
error | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
mergeAttributes | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
removeNode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
reparentChildren | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
4 | |||
isTableOfContentsMarker | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Tidy; |
4 | |
5 | use InvalidArgumentException; |
6 | use Wikimedia\RemexHtml\HTMLData; |
7 | use Wikimedia\RemexHtml\Serializer\Serializer; |
8 | use Wikimedia\RemexHtml\Serializer\SerializerNode; |
9 | use Wikimedia\RemexHtml\Tokenizer\Attributes; |
10 | use Wikimedia\RemexHtml\Tokenizer\PlainAttributes; |
11 | use Wikimedia\RemexHtml\TreeBuilder\Element; |
12 | use Wikimedia\RemexHtml\TreeBuilder\TreeBuilder; |
13 | use Wikimedia\RemexHtml\TreeBuilder\TreeHandler; |
14 | |
15 | /** |
16 | * @internal |
17 | */ |
18 | class RemexCompatMunger implements TreeHandler { |
19 | private static $onlyInlineElements = [ |
20 | "a" => true, |
21 | "abbr" => true, |
22 | "acronym" => true, |
23 | "applet" => true, |
24 | "b" => true, |
25 | "basefont" => true, |
26 | "bdo" => true, |
27 | "big" => true, |
28 | "br" => true, |
29 | "button" => true, |
30 | "cite" => true, |
31 | "code" => true, |
32 | "del" => true, |
33 | "dfn" => true, |
34 | "em" => true, |
35 | "font" => true, |
36 | "i" => true, |
37 | "iframe" => true, |
38 | "img" => true, |
39 | "input" => true, |
40 | "ins" => true, |
41 | "kbd" => true, |
42 | "label" => true, |
43 | "legend" => true, |
44 | "map" => true, |
45 | "object" => true, |
46 | "param" => true, |
47 | "q" => true, |
48 | "rb" => true, |
49 | "rbc" => true, |
50 | "rp" => true, |
51 | "rt" => true, |
52 | "rtc" => true, |
53 | "ruby" => true, |
54 | "s" => true, |
55 | "samp" => true, |
56 | "select" => true, |
57 | "small" => true, |
58 | "span" => true, |
59 | "strike" => true, |
60 | "strong" => true, |
61 | "sub" => true, |
62 | "sup" => true, |
63 | "textarea" => true, |
64 | "tt" => true, |
65 | "u" => true, |
66 | "var" => true, |
67 | // Those defined in tidy.conf |
68 | "video" => true, |
69 | "audio" => true, |
70 | "bdi" => true, |
71 | "data" => true, |
72 | "time" => true, |
73 | "mark" => true, |
74 | ]; |
75 | |
76 | /** |
77 | * For the purposes of this class, "metadata" elements are those that |
78 | * should neither trigger p-wrapping nor stop an outer p-wrapping, |
79 | * typically those that are themselves invisible in a browser's rendering. |
80 | * This isn't a complete list, it's just the tags that we're likely to |
81 | * encounter in practice. |
82 | * @var array |
83 | */ |
84 | private static $metadataElements = [ |
85 | 'style' => true, |
86 | 'script' => true, |
87 | 'link' => true, |
88 | // Except for the TableOfContentsMarker (see ::isTableOfContentsMarker() |
89 | // and Parser::TOC_PLACEHOLDER) which should break a paragraph. |
90 | 'meta' => true, |
91 | ]; |
92 | |
93 | private static $formattingElements = [ |
94 | 'a' => true, |
95 | 'b' => true, |
96 | 'big' => true, |
97 | 'code' => true, |
98 | 'em' => true, |
99 | 'font' => true, |
100 | 'i' => true, |
101 | 'nobr' => true, |
102 | 's' => true, |
103 | 'small' => true, |
104 | 'strike' => true, |
105 | 'strong' => true, |
106 | 'tt' => true, |
107 | 'u' => true, |
108 | ]; |
109 | |
110 | /** @var Serializer */ |
111 | private $serializer; |
112 | |
113 | /** @var bool */ |
114 | private $trace; |
115 | |
116 | /** |
117 | * @param Serializer $serializer |
118 | * @param bool $trace |
119 | */ |
120 | public function __construct( Serializer $serializer, $trace = false ) { |
121 | $this->serializer = $serializer; |
122 | $this->trace = $trace; |
123 | } |
124 | |
125 | public function startDocument( $fragmentNamespace, $fragmentName ) { |
126 | $this->serializer->startDocument( $fragmentNamespace, $fragmentName ); |
127 | $root = $this->serializer->getRootNode(); |
128 | $root->snData = new RemexMungerData; |
129 | $root->snData->needsPWrapping = true; |
130 | } |
131 | |
132 | public function endDocument( $pos ) { |
133 | $this->serializer->endDocument( $pos ); |
134 | } |
135 | |
136 | private function getParentForInsert( $preposition, $refElement ) { |
137 | if ( $preposition === TreeBuilder::ROOT ) { |
138 | return [ $this->serializer->getRootNode(), null ]; |
139 | } elseif ( $preposition === TreeBuilder::BEFORE ) { |
140 | $refNode = $refElement->userData; |
141 | return [ $this->serializer->getParentNode( $refNode ), $refNode ]; |
142 | } else { |
143 | $refNode = $refElement->userData; |
144 | $refData = $refNode->snData; |
145 | if ( $refData->currentCloneElement ) { |
146 | // Follow a chain of clone links if necessary |
147 | $origRefData = $refData; |
148 | while ( $refData->currentCloneElement ) { |
149 | $refElement = $refData->currentCloneElement; |
150 | $refNode = $refElement->userData; |
151 | $refData = $refNode->snData; |
152 | } |
153 | // Cache the end of the chain in the requested element |
154 | $origRefData->currentCloneElement = $refElement; |
155 | } elseif ( $refData->childPElement ) { |
156 | $refElement = $refData->childPElement; |
157 | $refNode = $refElement->userData; |
158 | } |
159 | return [ $refNode, $refNode ]; |
160 | } |
161 | } |
162 | |
163 | /** |
164 | * Insert a p-wrapper |
165 | * |
166 | * @param SerializerNode $parent |
167 | * @param int $sourceStart |
168 | * @return SerializerNode |
169 | */ |
170 | private function insertPWrapper( SerializerNode $parent, $sourceStart ) { |
171 | $pWrap = new Element( HTMLData::NS_HTML, 'mw:p-wrap', new PlainAttributes ); |
172 | $this->serializer->insertElement( TreeBuilder::UNDER, $parent, $pWrap, false, |
173 | $sourceStart, 0 ); |
174 | $data = new RemexMungerData; |
175 | $data->isPWrapper = true; |
176 | $data->wrapBaseNode = $parent; |
177 | $pWrap->userData->snData = $data; |
178 | $parent->snData->childPElement = $pWrap; |
179 | return $pWrap->userData; |
180 | } |
181 | |
182 | public function characters( $preposition, $refElement, $text, $start, $length, |
183 | $sourceStart, $sourceLength |
184 | ) { |
185 | $isBlank = strspn( $text, "\t\n\f\r ", $start, $length ) === $length; |
186 | |
187 | [ $parent, $refNode ] = $this->getParentForInsert( $preposition, $refElement ); |
188 | $parentData = $parent->snData; |
189 | |
190 | if ( $preposition === TreeBuilder::UNDER ) { |
191 | if ( $parentData->needsPWrapping && !$isBlank ) { |
192 | // Add a p-wrapper for bare text under body/blockquote |
193 | $refNode = $this->insertPWrapper( $refNode, $sourceStart ); |
194 | $parent = $refNode; |
195 | $parentData = $parent->snData; |
196 | } elseif ( $parentData->isSplittable && !$parentData->ancestorPNode ) { |
197 | // The parent is splittable and in block mode, so split the tag stack |
198 | $refNode = $this->splitTagStack( $refNode, true, $sourceStart ); |
199 | $parent = $refNode; |
200 | $parentData = $parent->snData; |
201 | } |
202 | } |
203 | |
204 | if ( !$isBlank ) { |
205 | // Non-whitespace characters detected |
206 | $parentData->nonblankNodeCount++; |
207 | } |
208 | $this->serializer->characters( $preposition, $refNode, $text, $start, |
209 | $length, $sourceStart, $sourceLength ); |
210 | } |
211 | |
212 | private function trace( $msg ) { |
213 | if ( $this->trace ) { |
214 | wfDebug( "[RCM] $msg" ); |
215 | } |
216 | } |
217 | |
218 | /** |
219 | * Insert or reparent an element. Create p-wrappers or split the tag stack |
220 | * as necessary. |
221 | * |
222 | * Consider the following insertion locations. The parent may be: |
223 | * |
224 | * - A: A body or blockquote (!!needsPWrapping) |
225 | * - B: A p-wrapper (!!isPWrapper) |
226 | * - C: A descendant of a p-wrapper (!!ancestorPNode) |
227 | * - CS: With splittable formatting elements in the stack region up to |
228 | * the p-wrapper |
229 | * - CU: With one or more unsplittable elements in the stack region up |
230 | * to the p-wrapper |
231 | * - D: Not a descendant of a p-wrapper (!ancestorNode) |
232 | * - DS: With splittable formatting elements in the stack region up to |
233 | * the body or blockquote |
234 | * - DU: With one or more unsplittable elements in the stack region up |
235 | * to the body or blockquote |
236 | * |
237 | * And consider that we may insert two types of element: |
238 | * - b: block |
239 | * - i: inline |
240 | * |
241 | * We handle the insertion as follows: |
242 | * |
243 | * - A/i: Create a p-wrapper, insert under it |
244 | * - A/b: Insert as normal |
245 | * - B/i: Insert as normal |
246 | * - B/b: Close the p-wrapper, insert under the body/blockquote (wrap |
247 | * base) instead) |
248 | * - C/i: Insert as normal |
249 | * - CS/b: Split the tag stack, insert the block under cloned formatting |
250 | * elements which have the wrap base (the parent of the p-wrap) as |
251 | * their ultimate parent. |
252 | * - CU/b: Disable the p-wrap, by reparenting the currently open child |
253 | * of the p-wrap under the p-wrap's parent. Then insert the block as |
254 | * normal. |
255 | * - D/b: Insert as normal |
256 | * - DS/i: Split the tag stack, creating a new p-wrapper as the ultimate |
257 | * parent of the formatting elements thus cloned. The parent of the |
258 | * p-wrapper is the body or blockquote. |
259 | * - DU/i: Insert as normal |
260 | * |
261 | * FIXME: fostering ($preposition == BEFORE) is mostly done by inserting as |
262 | * normal, the full algorithm is not followed. |
263 | * |
264 | * @param int $preposition |
265 | * @param Element|SerializerNode|null $refElement |
266 | * @param Element $element |
267 | * @param bool $void |
268 | * @param int $sourceStart |
269 | * @param int $sourceLength |
270 | */ |
271 | public function insertElement( $preposition, $refElement, Element $element, $void, |
272 | $sourceStart, $sourceLength |
273 | ) { |
274 | [ $parent, $newRef ] = $this->getParentForInsert( $preposition, $refElement ); |
275 | $parentData = $parent->snData; |
276 | $elementName = $element->htmlName; |
277 | |
278 | $inline = isset( self::$onlyInlineElements[$elementName] ); |
279 | $under = $preposition === TreeBuilder::UNDER; |
280 | |
281 | if ( isset( self::$metadataElements[$elementName] ) |
282 | && !self::isTableOfContentsMarker( $element ) |
283 | ) { |
284 | // The element is a metadata element, that we allow to appear in |
285 | // both inline and block contexts. |
286 | $this->trace( 'insert metadata' ); |
287 | } elseif ( $under && $parentData->isPWrapper && !$inline ) { |
288 | // [B/b] The element is non-inline and the parent is a p-wrapper, |
289 | // close the parent and insert into its parent instead |
290 | $this->trace( 'insert B/b' ); |
291 | $newParent = $this->serializer->getParentNode( $parent ); |
292 | $parent = $newParent; |
293 | $parentData = $parent->snData; |
294 | $parentData->childPElement = null; |
295 | $newRef = $refElement->userData; |
296 | } elseif ( $under && $parentData->isSplittable |
297 | && (bool)$parentData->ancestorPNode !== $inline |
298 | ) { |
299 | // [CS/b, DS/i] The parent is splittable and the current element is |
300 | // inline in block context, or if the current element is a block |
301 | // under a p-wrapper, split the tag stack. |
302 | $this->trace( $inline ? 'insert DS/i' : 'insert CS/b' ); |
303 | $newRef = $this->splitTagStack( $newRef, $inline, $sourceStart ); |
304 | $parent = $newRef; |
305 | $parentData = $parent->snData; |
306 | } elseif ( $under && $parentData->needsPWrapping && $inline ) { |
307 | // [A/i] If the element is inline and we are in body/blockquote, |
308 | // we need to create a p-wrapper |
309 | $this->trace( 'insert A/i' ); |
310 | $newRef = $this->insertPWrapper( $newRef, $sourceStart ); |
311 | $parent = $newRef; |
312 | $parentData = $parent->snData; |
313 | } elseif ( $parentData->ancestorPNode && !$inline ) { |
314 | // [CU/b] If the element is non-inline and (despite attempting to |
315 | // split above) there is still an ancestor p-wrap, disable that |
316 | // p-wrap |
317 | $this->trace( 'insert CU/b' ); |
318 | $this->disablePWrapper( $parent, $sourceStart ); |
319 | } else { |
320 | // [A/b, B/i, C/i, D/b, DU/i] insert as normal |
321 | $this->trace( 'insert normal' ); |
322 | } |
323 | |
324 | // An element with element children is a non-blank element |
325 | $parentData->nonblankNodeCount++; |
326 | |
327 | // Insert the element downstream and so initialise its userData |
328 | $this->serializer->insertElement( $preposition, $newRef, |
329 | $element, $void, $sourceStart, $sourceLength ); |
330 | |
331 | // Initialise snData |
332 | if ( !$element->userData->snData ) { |
333 | $elementData = $element->userData->snData = new RemexMungerData; |
334 | } else { |
335 | $elementData = $element->userData->snData; |
336 | } |
337 | if ( ( $parentData->isPWrapper || $parentData->isSplittable ) |
338 | && isset( self::$formattingElements[$elementName] ) |
339 | ) { |
340 | $elementData->isSplittable = true; |
341 | } |
342 | if ( $parentData->isPWrapper ) { |
343 | $elementData->ancestorPNode = $parent; |
344 | } elseif ( $parentData->ancestorPNode ) { |
345 | $elementData->ancestorPNode = $parentData->ancestorPNode; |
346 | } |
347 | if ( $parentData->wrapBaseNode ) { |
348 | $elementData->wrapBaseNode = $parentData->wrapBaseNode; |
349 | } elseif ( $parentData->needsPWrapping ) { |
350 | $elementData->wrapBaseNode = $parent; |
351 | } |
352 | if ( $elementName === 'body' |
353 | || $elementName === 'blockquote' |
354 | || $elementName === 'html' |
355 | ) { |
356 | $elementData->needsPWrapping = true; |
357 | } |
358 | } |
359 | |
360 | /** |
361 | * Clone nodes in a stack range and return the new parent |
362 | * |
363 | * @param SerializerNode $parentNode |
364 | * @param bool $inline |
365 | * @param int $pos The source position |
366 | * @return SerializerNode |
367 | */ |
368 | private function splitTagStack( SerializerNode $parentNode, $inline, $pos ) { |
369 | $parentData = $parentNode->snData; |
370 | $wrapBase = $parentData->wrapBaseNode; |
371 | $pWrap = $parentData->ancestorPNode; |
372 | if ( !$pWrap ) { |
373 | $cloneEnd = $wrapBase; |
374 | } else { |
375 | $cloneEnd = $parentData->ancestorPNode; |
376 | } |
377 | |
378 | $serializer = $this->serializer; |
379 | $node = $parentNode; |
380 | $root = $serializer->getRootNode(); |
381 | $nodes = []; |
382 | $removableNodes = []; |
383 | while ( $node !== $cloneEnd ) { |
384 | $nextParent = $serializer->getParentNode( $node ); |
385 | if ( $nextParent === $root ) { |
386 | throw new InvalidArgumentException( 'Did not find end of clone range' ); |
387 | } |
388 | $nodes[] = $node; |
389 | if ( $node->snData->nonblankNodeCount === 0 ) { |
390 | $removableNodes[] = $node; |
391 | $nextParent->snData->nonblankNodeCount--; |
392 | } |
393 | $node = $nextParent; |
394 | } |
395 | |
396 | if ( $inline ) { |
397 | $pWrap = $this->insertPWrapper( $wrapBase, $pos ); |
398 | $node = $pWrap; |
399 | } else { |
400 | if ( $pWrap ) { |
401 | // End the p-wrap which was open, cancel the diversion |
402 | $wrapBase->snData->childPElement = null; |
403 | } |
404 | $pWrap = null; |
405 | $node = $wrapBase; |
406 | } |
407 | |
408 | for ( $i = count( $nodes ) - 1; $i >= 0; $i-- ) { |
409 | $oldNode = $nodes[$i]; |
410 | $oldData = $oldNode->snData; |
411 | $nodeParent = $node; |
412 | $element = new Element( $oldNode->namespace, $oldNode->name, $oldNode->attrs ); |
413 | $this->serializer->insertElement( TreeBuilder::UNDER, $nodeParent, |
414 | $element, false, $pos, 0 ); |
415 | $oldData->currentCloneElement = $element; |
416 | |
417 | $newNode = $element->userData; |
418 | $newData = $newNode->snData = new RemexMungerData; |
419 | if ( $pWrap ) { |
420 | $newData->ancestorPNode = $pWrap; |
421 | } |
422 | $newData->isSplittable = true; |
423 | $newData->wrapBaseNode = $wrapBase; |
424 | $newData->isPWrapper = $oldData->isPWrapper; |
425 | |
426 | $nodeParent->snData->nonblankNodeCount++; |
427 | |
428 | $node = $newNode; |
429 | } |
430 | foreach ( $removableNodes as $rNode ) { |
431 | $fakeElement = new Element( $rNode->namespace, $rNode->name, $rNode->attrs ); |
432 | $fakeElement->userData = $rNode; |
433 | $this->serializer->removeNode( $fakeElement, $pos ); |
434 | } |
435 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable False positive |
436 | return $node; |
437 | } |
438 | |
439 | /** |
440 | * Find the ancestor of $node which is a child of a p-wrapper, and |
441 | * reparent that node so that it is placed after the end of the p-wrapper |
442 | * @param SerializerNode $node |
443 | * @param int $sourceStart |
444 | */ |
445 | private function disablePWrapper( SerializerNode $node, $sourceStart ) { |
446 | $nodeData = $node->snData; |
447 | $pWrapNode = $nodeData->ancestorPNode; |
448 | $newParent = $this->serializer->getParentNode( $pWrapNode ); |
449 | if ( $pWrapNode !== $this->serializer->getLastChild( $newParent ) ) { |
450 | // Fostering or something? Abort! |
451 | return; |
452 | } |
453 | |
454 | $nextParent = $node; |
455 | do { |
456 | $victim = $nextParent; |
457 | $victim->snData->ancestorPNode = null; |
458 | $nextParent = $this->serializer->getParentNode( $victim ); |
459 | } while ( $nextParent !== $pWrapNode ); |
460 | |
461 | // Make a fake Element to use in a reparenting operation |
462 | $victimElement = new Element( $victim->namespace, $victim->name, $victim->attrs ); |
463 | $victimElement->userData = $victim; |
464 | |
465 | // Reparent |
466 | $this->serializer->insertElement( TreeBuilder::UNDER, $newParent, $victimElement, |
467 | false, $sourceStart, 0 ); |
468 | |
469 | // Decrement nonblank node count |
470 | $pWrapNode->snData->nonblankNodeCount--; |
471 | |
472 | // Cancel the diversion so that no more elements are inserted under this p-wrap |
473 | $newParent->snData->childPElement = null; |
474 | } |
475 | |
476 | public function endTag( Element $element, $sourceStart, $sourceLength ) { |
477 | $data = $element->userData->snData; |
478 | if ( $data->childPElement ) { |
479 | $this->endTag( $data->childPElement, $sourceStart, 0 ); |
480 | } |
481 | $this->serializer->endTag( $element, $sourceStart, $sourceLength ); |
482 | $element->userData->snData = null; |
483 | $element->userData = null; |
484 | } |
485 | |
486 | public function doctype( $name, $public, $system, $quirks, $sourceStart, $sourceLength ) { |
487 | $this->serializer->doctype( $name, $public, $system, $quirks, |
488 | $sourceStart, $sourceLength ); |
489 | } |
490 | |
491 | public function comment( $preposition, $refElement, $text, $sourceStart, $sourceLength ) { |
492 | [ , $refNode ] = $this->getParentForInsert( $preposition, $refElement ); |
493 | $this->serializer->comment( $preposition, $refNode, $text, $sourceStart, $sourceLength ); |
494 | } |
495 | |
496 | public function error( $text, $pos ) { |
497 | $this->serializer->error( $text, $pos ); |
498 | } |
499 | |
500 | public function mergeAttributes( Element $element, Attributes $attrs, $sourceStart ) { |
501 | $this->serializer->mergeAttributes( $element, $attrs, $sourceStart ); |
502 | } |
503 | |
504 | public function removeNode( Element $element, $sourceStart ) { |
505 | $this->serializer->removeNode( $element, $sourceStart ); |
506 | } |
507 | |
508 | public function reparentChildren( Element $element, Element $newParent, $sourceStart ) { |
509 | $self = $element->userData; |
510 | if ( $self->snData->childPElement ) { |
511 | // Reparent under the p-wrapper instead, so that e.g. |
512 | // <blockquote><mw:p-wrap>...</mw:p-wrap></blockquote> |
513 | // becomes |
514 | // <blockquote><mw:p-wrap><i>...</i></mw:p-wrap></blockquote> |
515 | |
516 | // The formatting element should not be the parent of the p-wrap. |
517 | // Without this special case, the insertElement() of the <i> below |
518 | // would be diverted into the p-wrapper, causing infinite recursion |
519 | // (T178632) |
520 | $this->reparentChildren( $self->snData->childPElement, $newParent, $sourceStart ); |
521 | return; |
522 | } |
523 | |
524 | $children = $self->children; |
525 | $self->children = []; |
526 | $this->insertElement( TreeBuilder::UNDER, $element, $newParent, false, $sourceStart, 0 ); |
527 | $newParentNode = $newParent->userData; |
528 | $newParentId = $newParentNode->id; |
529 | foreach ( $children as $child ) { |
530 | if ( is_object( $child ) ) { |
531 | $this->trace( "reparent <{$child->name}>" ); |
532 | $child->parentId = $newParentId; |
533 | } |
534 | } |
535 | $newParentNode->children = $children; |
536 | } |
537 | |
538 | /** |
539 | * Helper function to match the Parser::TOC_PLACEHOLDER. |
540 | * Note that Parsoid's version of this placeholder might |
541 | * include additional attributes. |
542 | * @param Element $element |
543 | * @return bool If the given element is a Parser::TOC_PLACEHOLDER |
544 | */ |
545 | private function isTableOfContentsMarker( Element $element ): bool { |
546 | // Keep this in sync with Parser::TOC_PLACEHOLDER |
547 | return ( |
548 | $element->htmlName === 'meta' && |
549 | isset( $element->attrs['property'] ) && |
550 | $element->attrs['property'] === 'mw:PageProp/toc' |
551 | ); |
552 | } |
553 | } |