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