Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MarkFosteredContent
0.00% covered (danger)
0.00%
0 / 116
0.00% covered (danger)
0.00%
0 / 7
1980
0.00% covered (danger)
0.00%
0 / 1
 createNodeWithAttributes
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 removeTransclusionShadows
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 insertTransclusionMetas
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
90
 moveFosteredAnnotations
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
56
 getFosterContentHolder
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 processRecursively
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
342
 run
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\DOM\Processors;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\Parsoid\Config\Env;
8use Wikimedia\Parsoid\DOM\Comment;
9use Wikimedia\Parsoid\DOM\Document;
10use Wikimedia\Parsoid\DOM\DocumentFragment;
11use Wikimedia\Parsoid\DOM\Element;
12use Wikimedia\Parsoid\DOM\Node;
13use Wikimedia\Parsoid\DOM\Text;
14use Wikimedia\Parsoid\NodeData\DataParsoid;
15use Wikimedia\Parsoid\NodeData\TempData;
16use Wikimedia\Parsoid\Utils\DOMCompat;
17use Wikimedia\Parsoid\Utils\DOMDataUtils;
18use Wikimedia\Parsoid\Utils\DOMUtils;
19use Wikimedia\Parsoid\Utils\WTUtils;
20use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor;
21
22/**
23 * Non-IEW (inter-element-whitespace) can only be found in <td> <th> and
24 * <caption> tags in a table.  If found elsewhere within a table, such
25 * content will be moved out of the table and be "adopted" by the table's
26 * sibling ("foster parent"). The content that gets adopted is "fostered
27 * content".
28 *
29 * http://www.w3.org/TR/html5/syntax.html#foster-parent
30 * @module
31 */
32class MarkFosteredContent implements Wt2HtmlDOMProcessor {
33    /**
34     * Create a new DOM node with attributes.
35     *
36     * @param Document $document
37     * @param string $type
38     * @param array $attrs
39     * @return Element
40     */
41    private static function createNodeWithAttributes(
42        Document $document, string $type, array $attrs
43    ): Element {
44        $node = $document->createElement( $type );
45        DOMUtils::addAttributes( $node, $attrs );
46        return $node;
47    }
48
49    /**
50     * Cleans up transclusion shadows, keeping track of fostered transclusions
51     *
52     * @param Node $node
53     * @return bool
54     */
55    private static function removeTransclusionShadows( Node $node ): bool {
56        $sibling = null;
57        $fosteredTransclusions = false;
58        if ( $node instanceof Element ) {
59            if ( DOMUtils::isMarkerMeta( $node, 'mw:TransclusionShadow' ) ) {
60                $node->parentNode->removeChild( $node );
61                return true;
62            } elseif ( DOMDataUtils::getDataParsoid( $node )->getTempFlag( TempData::IN_TRANSCLUSION ) ) {
63                $fosteredTransclusions = true;
64            }
65            $node = $node->firstChild;
66            while ( $node ) {
67                $sibling = $node->nextSibling;
68                if ( self::removeTransclusionShadows( $node ) ) {
69                    $fosteredTransclusions = true;
70                }
71                $node = $sibling;
72            }
73        }
74        return $fosteredTransclusions;
75    }
76
77    /**
78     * Inserts metas around the fosterbox and table
79     *
80     * @param Env $env
81     * @param Node $fosterBox
82     * @param Element $table
83     */
84    private static function insertTransclusionMetas(
85        Env $env, Node $fosterBox, Element $table
86    ): void {
87        $aboutId = $env->newAboutId();
88
89        // Ensure we have depth entries for 'aboutId'.
90        $docDataBag = DOMDataUtils::getBag( $table->ownerDocument );
91        $docDataBag->transclusionMetaTagDepthMap[$aboutId]['start'] =
92            $docDataBag->transclusionMetaTagDepthMap[$aboutId]['end'] =
93            DOMUtils::nodeDepth( $table );
94
95        // You might be asking yourself, why is $table->dataParsoid->tsr->end always
96        // present? The earlier implementation searched the table's siblings for
97        // their tsr->start. However, encapsulation doesn't happen when the foster box,
98        // and thus the table, are in the transclusion.
99        $s = self::createNodeWithAttributes( $fosterBox->ownerDocument, 'meta', [
100                'about' => $aboutId,
101                'id' => substr( $aboutId, 1 ),
102                'typeof' => 'mw:Transclusion',
103            ]
104        );
105        $dp = new DataParsoid;
106        $dp->tsr = clone DOMDataUtils::getDataParsoid( $table )->tsr;
107        $dp->setTempFlag( TempData::FROM_FOSTER );
108        DOMDataUtils::setDataParsoid( $s, $dp );
109        $fosterBox->parentNode->insertBefore( $s, $fosterBox );
110
111        $e = self::createNodeWithAttributes( $table->ownerDocument, 'meta', [
112                'about' => $aboutId,
113                'typeof' => 'mw:Transclusion/End',
114            ]
115        );
116
117        $sibling = $table->nextSibling;
118        $beforeText = null;
119
120        // Skip past the table end, mw:shadow and any transclusions that
121        // start inside the table. There may be newlines and comments in
122        // between so keep track of that, and backtrack when necessary.
123        while ( $sibling ) {
124            if ( !WTUtils::isTplStartMarkerMeta( $sibling ) &&
125                ( WTUtils::isEncapsulatedDOMForestRoot( $sibling ) ||
126                    DOMUtils::isMarkerMeta( $sibling, 'mw:TransclusionShadow' )
127                )
128            ) {
129                $sibling = $sibling->nextSibling;
130                $beforeText = null;
131            } elseif ( $sibling instanceof Comment || $sibling instanceof Text ) {
132                if ( !$beforeText ) {
133                    $beforeText = $sibling;
134                }
135                $sibling = $sibling->nextSibling;
136            } else {
137                break;
138            }
139        }
140
141        $table->parentNode->insertBefore( $e, $beforeText ?: $sibling );
142    }
143
144    /**
145     * @param Node $e
146     * @param Node $firstFosteredNode
147     * @param Element|DocumentFragment $tableParent
148     * @param ?Node $tableNextSibling
149     */
150    private static function moveFosteredAnnotations(
151        Node $e, Node $firstFosteredNode, $tableParent, ?Node $tableNextSibling
152    ): void {
153        if ( WTUtils::isAnnotationStartMarkerMeta( $e ) && $e !== $firstFosteredNode ) {
154            '@phan-var Element $e';
155            DOMDataUtils::getDataParsoid( $e )->wasMoved = true;
156            $firstFosteredNode->parentNode->insertBefore( $e, $firstFosteredNode );
157        } elseif ( WTUtils::isAnnotationEndMarkerMeta( $e ) ) {
158            '@phan-var Element $e';
159            DOMDataUtils::getDataParsoid( $e )->wasMoved = true;
160            $tableParent->insertBefore( $e, $tableNextSibling );
161        } elseif ( $e instanceof Element && $e->hasChildNodes() ) {
162            // avoid iterating over a mutated DOMNodeList
163            $childNodeList = iterator_to_array( $e->childNodes );
164            foreach ( $childNodeList as $child ) {
165                self::moveFosteredAnnotations( $child, $firstFosteredNode, $tableParent, $tableNextSibling );
166            }
167        }
168    }
169
170    private static function getFosterContentHolder( Document $doc, bool $inPTag ): Element {
171        $fosterContentHolder = $doc->createElement( $inPTag ? 'span' : 'p' );
172        $dp = new DataParsoid;
173        $dp->fostered = true;
174        // Set autoInsertedStart for bug-compatibility with the old ProcessTreeBuilderFixups code
175        $dp->autoInsertedStart = true;
176
177        DOMDataUtils::setDataParsoid( $fosterContentHolder, $dp );
178        return $fosterContentHolder;
179    }
180
181    /**
182     * Searches for FosterBoxes and does two things when it hits one:
183     * - Marks all nextSiblings as fostered until the accompanying table.
184     * - Wraps the whole thing (table + fosterbox) with transclusion metas if
185     *   there is any fostered transclusion content.
186     *
187     * @param Node $node
188     * @param Env $env
189     */
190    private static function processRecursively( Node $node, Env $env ): void {
191        $c = $node->firstChild;
192
193        while ( $c ) {
194            $sibling = $c->nextSibling;
195            $fosteredTransclusions = false;
196
197            if ( DOMUtils::hasNameAndTypeOf( $c, 'table', 'mw:FosterBox' ) ) {
198                $inPTag = DOMUtils::hasNameOrHasAncestorOfName( $c->parentNode, 'p' );
199                $fosterContentHolder = self::getFosterContentHolder( $c->ownerDocument, $inPTag );
200
201                $fosteredElements = [];
202                // mark as fostered until we hit the table
203                while ( $sibling &&
204                    ( !( $sibling instanceof Element ) || DOMCompat::nodeName( $sibling ) !== 'table' )
205                ) {
206                    $fosteredElements[] = $sibling;
207                    $next = $sibling->nextSibling;
208                    if ( $sibling instanceof Element ) {
209                        // TODO: Note the similarity here with the p-wrapping pass.
210                        // This can likely be combined in some more maintainable way.
211                        if (
212                            DOMUtils::isRemexBlockNode( $sibling ) ||
213                            PWrap::pWrapOptional( $sibling )
214                        ) {
215                            // Block nodes don't need to be wrapped in a p-tag either.
216                            // Links, includeonly directives, and other rendering-transparent
217                            // nodes dont need wrappers. sol-transparent wikitext generate
218                            // rendering-transparent nodes and we use that helper as a proxy here.
219                            DOMDataUtils::getDataParsoid( $sibling )->fostered = true;
220                            // If the foster content holder is not empty,
221                            // close it and get a new content holder.
222                            if ( $fosterContentHolder->hasChildNodes() ) {
223                                $sibling->parentNode->insertBefore( $fosterContentHolder, $sibling );
224                                $fosterContentHolder = self::getFosterContentHolder( $sibling->ownerDocument, $inPTag );
225                            }
226                        } else {
227                            $fosterContentHolder->appendChild( $sibling );
228                        }
229
230                        if ( self::removeTransclusionShadows( $sibling ) ) {
231                            $fosteredTransclusions = true;
232                        }
233                    } else {
234                        $fosterContentHolder->appendChild( $sibling );
235                    }
236                    $sibling = $next;
237                }
238
239                $table = $sibling;
240
241                // we should be able to reach the table from the fosterbox
242                Assert::invariant(
243                    $table instanceof Element && DOMCompat::nodeName( $table ) === 'table',
244                    "Table isn't a sibling. Something's amiss!"
245                );
246
247                if ( $fosterContentHolder->hasChildNodes() ) {
248                    $table->parentNode->insertBefore( $fosterContentHolder, $table );
249                }
250
251                // we have fostered transclusions
252                // wrap the whole thing in a transclusion
253                if ( $fosteredTransclusions ) {
254                    self::insertTransclusionMetas( $env, $c, $table );
255                }
256
257                // We have two possibilities here for the insertion of more than one meta tag after the table.
258                // We can either keep them in the order of traversal (by keeping a reference to the initial
259                // $table->nextSibling), or in reverse order of traversal (by updating $table->nextSibling to
260                // the inserted meta.
261                // This has different consequences depending on whether multiple ranges are nested or not.
262                // If the fosterbox initially contains <ann1><ann2></ann2></ann1>, the end result for the first
263                // possibility becomes <ann1><ann2>TABLE</ann2></ann1>. If the fosterbox initially contains
264                // <ann1></ann1><ann2></ann2>, the end result becomes <ann1><ann2>TABLE</ann1></ann2>. The
265                // consequences are inverted if we insert in reverse order of traversal.
266                // Note that this is only relevant if the annotations are of different types and that, right
267                // now, we only have two types of annotation (namely <translate> and <tvar>), and <tvar> can
268                // only exist nested in <translate>. Hence, we choose to insert in traversal order so that we can
269                // preserve existing nesting order.
270                // (The last option would be to keep a stack of opening metas in the foster table and to re-add
271                // them in inverse order at the end of the table. This would add significant code complexity for
272                // what seems like marginal benefits at best as long as we do not have more annotation types.)
273                $tableNextSibling = $table->nextSibling;
274                $tableParent = $table->parentNode;
275                // this needs to happen after inserting the transclusion meta so that they get
276                // included in the transclusion
277                foreach ( $fosteredElements as $elem ) {
278                    '@phan-var Element $elem';
279                    self::moveFosteredAnnotations(
280                        $elem, $fosteredElements[0], $tableParent, $tableNextSibling
281                    );
282                }
283
284                // remove the foster box
285                $c->parentNode->removeChild( $c );
286
287            } elseif ( DOMUtils::isMarkerMeta( $c, 'mw:TransclusionShadow' ) ) {
288                $c->parentNode->removeChild( $c );
289            } elseif ( $c instanceof Element ) {
290                if ( $c->hasChildNodes() ) {
291                    self::processRecursively( $c, $env );
292                }
293            }
294
295            $c = $sibling;
296        }
297    }
298
299    /**
300     * @inheritDoc
301     */
302    public function run(
303        Env $env, Node $root, array $options = [], bool $atTopLevel = false
304    ): void {
305        self::processRecursively( $root, $env );
306    }
307}