Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
TreeMutationRelay
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 9
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 matchStartTag
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 matchEndTag
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 resetMatch
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getMatchedElement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isMarkable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 insertElement
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
 endTag
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 reparentChildren
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Wt2Html\TreeBuilder;
5
6use Wikimedia\Parsoid\DOM\Element as DOMElement;
7use Wikimedia\Parsoid\Utils\DOMDataUtils;
8use Wikimedia\Parsoid\Utils\DOMUtils;
9use Wikimedia\Parsoid\Utils\WTUtils;
10use Wikimedia\RemexHtml\TreeBuilder\Element;
11use Wikimedia\RemexHtml\TreeBuilder\RelayTreeHandler;
12
13/**
14 * This is a stage inserted between RemexHtml's TreeBuilder and our DOMBuilder
15 * subclass. Any code that needs to modify the tree mutation event stream
16 * should go here. It's currently used for auto-insert detection.
17 */
18class TreeMutationRelay extends RelayTreeHandler {
19    /** @var Attributes|null */
20    private $matchAttribs;
21
22    /** @var int|null */
23    private $matchEndLength;
24
25    /** @var bool|null */
26    private $matchEndIsHtml;
27
28    /** @var DOMElement|null */
29    private $matchedElement;
30
31    /**
32     * @param DOMBuilder $nextHandler
33     */
34    public function __construct( DOMBuilder $nextHandler ) {
35        parent::__construct( $nextHandler );
36    }
37
38    /**
39     * Start watching for a start tag with the given Attributes object.
40     *
41     * @param Attributes $attribs
42     */
43    public function matchStartTag( Attributes $attribs ): void {
44        $this->matchAttribs = $attribs;
45        $this->matchEndLength = null;
46        $this->matchEndIsHtml = null;
47        $this->matchedElement = null;
48    }
49
50    /**
51     * Start watching for an end tag with the given fake source length.
52     *
53     * @param int $sourceLength
54     * @param bool $isHTML $dp->stx=='html', which helps us decide whether to
55     *   set autoInsertedEnd
56     */
57    public function matchEndTag( int $sourceLength, bool $isHTML ): void {
58        $this->matchAttribs = null;
59        $this->matchEndLength = $sourceLength;
60        $this->matchEndIsHtml = $isHTML;
61        $this->matchedElement = null;
62    }
63
64    /**
65     * Stop looking for a matching element
66     */
67    public function resetMatch(): void {
68        $this->matchAttribs = null;
69        $this->matchEndLength = null;
70        $this->matchEndIsHtml = null;
71        $this->matchedElement = null;
72    }
73
74    /**
75     * If an element was matched, return the element object from the DOM.
76     *
77     * @return DOMElement|null A local alias since there are two classes called
78     *   Element here.
79     */
80    public function getMatchedElement(): ?DOMElement {
81        return $this->matchedElement;
82    }
83
84    /**
85     * Tags that are always auto-generated conventionally do not get
86     * autoInsertedStart or autoInsertedEnd. In the case of html and body, the
87     * DataBag is not set up when they are created, so trying to mark them
88     * would cause a fatal error.
89     *
90     * @param Element $element
91     * @return bool
92     */
93    private function isMarkable( Element $element ) {
94        return !in_array( $element->htmlName, [
95            'html',
96            'head',
97            'body',
98            'tbody',
99            'meta'
100        ], true );
101    }
102
103    /**
104     * Set autoInsertedStart on auto-inserted nodes and forward the event to
105     * DOMBuilder.
106     *
107     * @param int $preposition
108     * @param Element|null $ref
109     * @param Element $element
110     * @param bool $void
111     * @param int $sourceStart
112     * @param int $sourceLength
113     */
114    public function insertElement(
115        $preposition, $ref, Element $element, $void, $sourceStart, $sourceLength
116    ) {
117        // Elements can be inserted twice due to reparenting by the adoption
118        // agency algorithm. If this is a reparenting, we don't want to
119        // override autoInsertedStart flag set the first time around.
120        $isMove = (bool)$element->userData;
121
122        $this->nextHandler->insertElement( $preposition, $ref, $element, $void,
123            $sourceStart, $sourceLength );
124
125        // Compute nesting depth of mw:Transclusion meta tags
126        if ( WTUtils::isTplMarkerMeta( $element->userData ) ) {
127            $meta = $element->userData;
128
129            $about = $meta->getAttribute( 'about' );
130            $isEnd = WTUtils::isTplEndMarkerMeta( $meta );
131            $docDataBag = DOMDataUtils::getBag( $meta->ownerDocument );
132            $docDataBag->transclusionMetaTagDepthMap[$about][$isEnd ? 'end' : 'start'] =
133                DOMUtils::nodeDepth( $meta );
134        }
135
136        if ( $element->attrs === $this->matchAttribs ) {
137            $this->matchedElement = $element->userData;
138        } elseif ( !$isMove && $this->isMarkable( $element ) ) {
139            DOMDataUtils::getDataParsoid( $element->userData )->autoInsertedStart = true;
140        }
141    }
142
143    /**
144     * Set autoInsertedEnd on elements that were not closed by an explicit
145     * EndTagTk in the source stream.
146     *
147     * @param Element $element
148     * @param int $sourceStart
149     * @param int $sourceLength
150     */
151    public function endTag( Element $element, $sourceStart, $sourceLength ) {
152        $this->nextHandler->endTag( $element, $sourceStart, $sourceLength );
153
154        if ( $sourceLength === $this->matchEndLength ) {
155            $this->matchedElement = $element->userData;
156            $isMatch = true;
157        } else {
158            $isMatch = false;
159        }
160        if ( $this->isMarkable( $element ) ) {
161            /** @var DOMElement $node */
162            $node = $element->userData;
163            $dp = DOMDataUtils::getDataParsoid( $node );
164            if ( !$isMatch ) {
165                // An end tag auto-inserted by TreeBuilder
166                $dp->autoInsertedEnd = true;
167                unset( $dp->tmp->endTSR );
168            } elseif ( $this->matchEndIsHtml ) {
169                // We found a matching HTML end-tag - unset any AI flags.
170                // This can happen because of wikitext like this:
171                // '''X</b> where the quote-transformer inserts a
172                // new autoInsertedEnd tag because it doesn't track
173                // HTML quote tags.
174                unset( $dp->autoInsertedEndToken );
175                unset( $dp->autoInsertedEnd );
176            } else {
177                // If the node (start tag) was literal html, the end tag will be as well.
178                // However, the converse isn't true.
179                //
180                // 1. A node for an auto-inserted start tag wouldn't have stx=html.
181                //    See "Table with missing opening <tr> tag" test as an example.
182                // 2. In "{|\n|foo\n</table>" (yes, found on wikis), start tag isn't HTML.
183                //
184                // We get to this branch if matched tag is not a html end-tag.
185                // Check if start tag is html. If so, mark autoInsertedEnd.
186                $startIsHtml = ( $dp->stx ?? '' ) === 'html';
187                if ( $startIsHtml ) {
188                    $dp->autoInsertedEnd = true;
189                    unset( $dp->tmp->endTSR );
190                }
191            }
192        }
193    }
194
195    /**
196     * A reparentChildren() operation includes insertion of the new parent.
197     * This is always automatic, so set autoInsertedStart on the new parent.
198     *
199     * @param Element $element
200     * @param Element $newParent
201     * @param int $sourceStart
202     */
203    public function reparentChildren( Element $element, Element $newParent, $sourceStart ) {
204        $this->nextHandler->reparentChildren( $element, $newParent, $sourceStart );
205        if ( $this->isMarkable( $newParent ) ) {
206            /** @var DOMElement $node */
207            $node = $newParent->userData;
208            DOMDataUtils::getDataParsoid( $node )->autoInsertedStart = true;
209        }
210    }
211}