Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
MigrateTemplateMarkerMetas
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 5
1122
0.00% covered (danger)
0.00%
0 / 1
 migrateFirstChild
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 migrateLastChild
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 updateDepths
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 doMigrate
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
462
 run
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\DOM\Processors;
5
6use Wikimedia\Parsoid\Config\Env;
7use Wikimedia\Parsoid\DOM\DocumentFragment;
8use Wikimedia\Parsoid\DOM\Element;
9use Wikimedia\Parsoid\DOM\Node;
10use Wikimedia\Parsoid\Utils\DiffDOMUtils;
11use Wikimedia\Parsoid\Utils\DOMCompat;
12use Wikimedia\Parsoid\Utils\DOMDataUtils;
13use Wikimedia\Parsoid\Utils\DOMUtils;
14use Wikimedia\Parsoid\Utils\WTUtils;
15use Wikimedia\Parsoid\Wikitext\Consts;
16use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor;
17
18class MigrateTemplateMarkerMetas implements Wt2HtmlDOMProcessor {
19
20    private function migrateFirstChild( Node $firstChild ): bool {
21        if ( WTUtils::isTplEndMarkerMeta( $firstChild ) ) {
22            return true;
23        }
24
25        if ( WTUtils::isTplStartMarkerMeta( $firstChild ) ) {
26            '@phan-var Element $firstChild';  // @var Element $firstChild
27
28            $docDataBag = DOMDataUtils::getBag( $firstChild->ownerDocument );
29            $about = DOMCompat::getAttribute( $firstChild, 'about' );
30            $startDepth = $docDataBag->transclusionMetaTagDepthMap[$about]['start'];
31            $endDepth = $docDataBag->transclusionMetaTagDepthMap[$about]['end'];
32            return $startDepth > $endDepth;
33        }
34
35        return false;
36    }
37
38    private function migrateLastChild( Node $lastChild ): bool {
39        if ( WTUtils::isTplStartMarkerMeta( $lastChild ) ) {
40            return true;
41        }
42
43        if ( WTUtils::isTplEndMarkerMeta( $lastChild ) ) {
44            '@phan-var Element $lastChild';  // @var Element $lastChild
45            $docDataBag = DOMDataUtils::getBag( $lastChild->ownerDocument );
46            $about = DOMCompat::getAttribute( $lastChild, 'about' );
47            $startDepth = $docDataBag->transclusionMetaTagDepthMap[$about]['start'];
48            $endDepth = $docDataBag->transclusionMetaTagDepthMap[$about]['end'];
49            return $startDepth < $endDepth;
50        }
51
52        return false;
53    }
54
55    private function updateDepths( Element $elt ): void {
56        // Update depths
57        $docDataBag = DOMDataUtils::getBag( $elt->ownerDocument );
58        $about = DOMCompat::getAttribute( $elt, 'about' );
59        if ( WTUtils::isTplEndMarkerMeta( $elt ) ) {
60            // end depth
61            $docDataBag->transclusionMetaTagDepthMap[$about]['end']--;
62        } else {
63            // start depth
64            $docDataBag->transclusionMetaTagDepthMap[$about]['start']--;
65        }
66    }
67
68    /**
69     * The goal of this pass is to assist the WrapTemplates pass
70     * by using some simple heuristics to bring the DOM into a more
71     * canonical form. There is no correctness issue with WrapTemplates
72     * wrapping a wider range of content than what a template generated.
73     * These heuristics can be evolved as needed.
74     *
75     * Given the above considerations, we are going to consider migration
76     * possibilities only where the migration won't lead to additional
77     * untemplated content getting pulled into the template wrapper.
78     *
79     * The simplest heuristics that satisfy this constraint are:
80     * - Only examine first/last child of a node.
81     * - We relax the first/last child constraint by ignoring
82     *   separator nodes (comments, whitespace) but this is
83     *   something worth revisiting in the future.
84     * - Only migrate upwards if the node's start/end tag (barrier)
85     *   comes from zero-width-wikitext.
86     * - If the start meta is the last child OR if the end meta is
87     *   the first child, migrate up.
88     * - If the start meta is the first child OR if the end meta is
89     *   the last child, there is no benefit to migrating the meta tags
90     *   up if both the start and end metas are at the same tree depth.
91     * - In some special cases, it might be possible to migrate
92     *   metas downward rather than upward. Migrating downwards has
93     *   wt2wt corruption implications if done incorrectly. So, we
94     *   aren't considering this possibility right now.
95     *
96     * @param Element|DocumentFragment $node
97     * @param Env $env
98     */
99    private function doMigrate( Node $node, Env $env ): void {
100        $c = $node->firstChild;
101        while ( $c ) {
102            $sibling = $c->nextSibling;
103            if ( $c->hasChildNodes() ) {
104                '@phan-var Element $c';  // @var Element $c
105                $this->doMigrate( $c, $env );
106            }
107            $c = $sibling;
108        }
109
110        // No migration out of fragment
111        if ( DOMUtils::atTheTop( $node ) ) {
112            return;
113        }
114
115        '@phan-var Element $node'; // @var Element $node
116
117        // Check if $node is a fostered node
118        $fostered = !empty( DOMDataUtils::getDataParsoid( $node )->fostered );
119
120        $firstChild = DiffDOMUtils::firstNonSepChild( $node );
121        if ( $firstChild && $this->migrateFirstChild( $firstChild ) ) {
122            // We can migrate the meta-tag across this node's start-tag barrier only
123            // if that start-tag is zero-width, or auto-inserted.
124            $tagWidth = Consts::$WtTagWidths[DOMUtils::nodeName( $node )] ?? null;
125            if ( ( $tagWidth && $tagWidth[0] === 0 && !WTUtils::isLiteralHTMLNode( $node ) ) ||
126                !empty( DOMDataUtils::getDataParsoid( $node )->autoInsertedStart )
127            ) {
128                $sentinel = $firstChild;
129                do {
130                    $firstChild = $node->firstChild;
131                    $node->parentNode->insertBefore( $firstChild, $node );
132                    if ( $fostered && $firstChild instanceof Element ) {
133                        // $firstChild is being migrated out of a fostered node
134                        // So, mark $lastChild itself fostered!
135                        DOMDataUtils::getDataParsoid( $firstChild )->fostered = true;
136                    }
137                } while ( $sentinel !== $firstChild );
138
139                $this->updateDepths( $firstChild );
140            }
141        }
142
143        $lastChild = DiffDOMUtils::lastNonSepChild( $node );
144        if ( $lastChild && $this->migrateLastChild( $lastChild ) ) {
145            // We can migrate the meta-tag across this node's end-tag barrier only
146            // if that end-tag is zero-width, or auto-inserted.
147            $tagWidth = Consts::$WtTagWidths[DOMUtils::nodeName( $node )] ?? null;
148            '@phan-var Element $node'; // @var Element $node
149            if ( ( $tagWidth && $tagWidth[1] === 0 &&
150                !WTUtils::isLiteralHTMLNode( $node ) ) ||
151                ( !empty( DOMDataUtils::getDataParsoid( $node )->autoInsertedEnd ) &&
152                // Except, don't migrate out of a table since the end meta
153                // marker may have been fostered and this is more likely to
154                // result in a flipped range that isn't enclosed.
155                DOMUtils::nodeName( $node ) !== 'table' )
156            ) {
157                $sentinel = $lastChild;
158                do {
159                    $lastChild = $node->lastChild;
160                    $node->parentNode->insertBefore( $lastChild, $node->nextSibling );
161                    if ( $fostered && $lastChild instanceof Element ) {
162                        // $lastChild is being migrated out of a fostered node
163                        // So, mark $lastChild itself fostered!
164                        DOMDataUtils::getDataParsoid( $lastChild )->fostered = true;
165                    }
166                } while ( $sentinel !== $lastChild );
167
168                $this->updateDepths( $lastChild );
169            }
170        }
171    }
172
173    /**
174     * @inheritDoc
175     */
176    public function run(
177        Env $env, Node $root, array $options = [], bool $atTopLevel = false
178    ): void {
179        // Don't run this in template content
180        if ( $options['inTemplate'] ) {
181            return;
182        }
183        if ( $root instanceof Element || $root instanceof DocumentFragment ) {
184            $this->doMigrate( $root, $env );
185        }
186    }
187}