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