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
1056
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 / 37
0.00% covered (danger)
0.00%
0 / 1
462
 run
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
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        // Check if $node is a fostered node
116        $fostered = !empty( DOMDataUtils::getDataParsoid( $node )->fostered );
117
118        $firstChild = DiffDOMUtils::firstNonSepChild( $node );
119        if ( $firstChild && $this->migrateFirstChild( $firstChild ) ) {
120            // We can migrate the meta-tag across this node's start-tag barrier only
121            // if that start-tag is zero-width, or auto-inserted.
122            $tagWidth = Consts::$WtTagWidths[DOMCompat::nodeName( $node )] ?? null;
123            DOMUtils::assertElt( $node );
124            if ( ( $tagWidth && $tagWidth[0] === 0 && !WTUtils::isLiteralHTMLNode( $node ) ) ||
125                !empty( DOMDataUtils::getDataParsoid( $node )->autoInsertedStart )
126            ) {
127                $sentinel = $firstChild;
128                do {
129                    $firstChild = $node->firstChild;
130                    $node->parentNode->insertBefore( $firstChild, $node );
131                    if ( $fostered && $firstChild instanceof Element ) {
132                        // $firstChild is being migrated out of a fostered node
133                        // So, mark $lastChild itself fostered!
134                        DOMDataUtils::getDataParsoid( $firstChild )->fostered = true;
135                    }
136                } while ( $sentinel !== $firstChild );
137
138                $this->updateDepths( $firstChild );
139            }
140        }
141
142        $lastChild = DiffDOMUtils::lastNonSepChild( $node );
143        if ( $lastChild && $this->migrateLastChild( $lastChild ) ) {
144            // We can migrate the meta-tag across this node's end-tag barrier only
145            // if that end-tag is zero-width, or auto-inserted.
146            $tagWidth = Consts::$WtTagWidths[DOMCompat::nodeName( $node )] ?? null;
147            DOMUtils::assertElt( $node );
148            if ( ( $tagWidth && $tagWidth[1] === 0 &&
149                !WTUtils::isLiteralHTMLNode( $node ) ) ||
150                ( !empty( DOMDataUtils::getDataParsoid( $node )->autoInsertedEnd ) &&
151                // Except, don't migrate out of a table since the end meta
152                // marker may have been fostered and this is more likely to
153                // result in a flipped range that isn't enclosed.
154                DOMCompat::nodeName( $node ) !== 'table' )
155            ) {
156                $sentinel = $lastChild;
157                do {
158                    $lastChild = $node->lastChild;
159                    $node->parentNode->insertBefore( $lastChild, $node->nextSibling );
160                    if ( $fostered && $lastChild instanceof Element ) {
161                        // $lastChild is being migrated out of a fostered node
162                        // So, mark $lastChild itself fostered!
163                        DOMDataUtils::getDataParsoid( $lastChild )->fostered = true;
164                    }
165                } while ( $sentinel !== $lastChild );
166
167                $this->updateDepths( $lastChild );
168            }
169        }
170    }
171
172    /**
173     * @inheritDoc
174     */
175    public function run(
176        Env $env, Node $root, array $options = [], bool $atTopLevel = false
177    ): void {
178        if ( $root instanceof Element || $root instanceof DocumentFragment ) {
179            $this->doMigrate( $root, $env );
180        }
181    }
182}