Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
PHandler
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 8
4556
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
 handle
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 before
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
462
 after
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
156
 currWikitextLineHasBlockNode
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
132
 newWikitextLineMightHaveBlockNode
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 treatAsPPTransition
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
72
 isPPTransition
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers;
5
6use stdClass;
7use Wikimedia\Parsoid\DOM\Element;
8use Wikimedia\Parsoid\DOM\Node;
9use Wikimedia\Parsoid\DOM\Text;
10use Wikimedia\Parsoid\Html2Wt\SerializerState;
11use Wikimedia\Parsoid\Utils\DiffDOMUtils;
12use Wikimedia\Parsoid\Utils\DOMCompat;
13use Wikimedia\Parsoid\Utils\DOMDataUtils;
14use Wikimedia\Parsoid\Utils\DOMUtils;
15use Wikimedia\Parsoid\Utils\WTUtils;
16use Wikimedia\Parsoid\Wikitext\Consts;
17
18class PHandler extends DOMHandler {
19
20    public function __construct() {
21        // Counterintuitive but seems right.
22        // Otherwise the generated wikitext will parse as an indent-pre
23        // escapeWikitext nowiking will deal with leading space for content
24        // inside the p-tag, but forceSOL suppresses whitespace before the p-tag.
25        parent::__construct( true );
26    }
27
28    /** @inheritDoc */
29    public function handle(
30        Element $node, SerializerState $state, bool $wrapperUnmodified = false
31    ): ?Node {
32        // XXX: Handle single-line mode by switching to HTML handler!
33        $state->serializeChildren( $node );
34        return $node->nextSibling;
35    }
36
37    /** @inheritDoc */
38    public function before( Element $node, Node $otherNode, SerializerState $state ): array {
39        $otherNodeName = DOMCompat::nodeName( $otherNode );
40        $tableCellOrBody = [ 'td', 'th', 'body' ];
41        if ( $node->parentNode === $otherNode
42            && ( DOMUtils::isListItem( $otherNode ) || in_array( $otherNodeName, $tableCellOrBody, true ) )
43        ) {
44            if ( in_array( $otherNodeName, $tableCellOrBody, true ) ) {
45                return [ 'min' => 0, 'max' => 1 ];
46            } else {
47                return [ 'min' => 0, 'max' => 0 ];
48            }
49        } elseif ( ( $otherNode === DiffDOMUtils::previousNonDeletedSibling( $node )
50                // p-p transition
51                && $otherNode instanceof Element // for static analyzers
52                && $otherNodeName === 'p'
53                && ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html' )
54            || ( self::treatAsPPTransition( $otherNode )
55                && $otherNode === DiffDOMUtils::previousNonSepSibling( $node )
56                // A new wikitext line could start at this P-tag. We have to figure out
57                // if 'node' needs a separation of 2 newlines from that P-tag. Examine
58                // previous siblings of 'node' to see if we emitted a block tag
59                // there => we can make do with 1 newline separator instead of 2
60                // before the P-tag.
61                && !$this->currWikitextLineHasBlockNode( $state->currLine, $otherNode ) )
62            || ( WTUtils::isMarkerAnnotation( DiffDOMUtils::nextNonSepSibling( $otherNode ) )
63                && DiffDOMUtils::nextNonSepSibling( DiffDOMUtils::nextNonSepSibling( $otherNode ) ) === $node )
64        ) {
65            return [ 'min' => 2, 'max' => 2 ];
66        } elseif ( self::treatAsPPTransition( $otherNode )
67            || ( DOMUtils::isWikitextBlockNode( $otherNode )
68                && DOMCompat::nodeName( $otherNode ) !== 'blockquote'
69                && $node->parentNode === $otherNode )
70            // new p-node added after sol-transparent wikitext should always
71            // get serialized onto a new wikitext line.
72            || ( WTUtils::emitsSolTransparentSingleLineWT( $otherNode )
73                && WTUtils::isNewElt( $node ) )
74        ) {
75            if ( !DOMUtils::hasNameOrHasAncestorOfName( $otherNode, 'figcaption' ) ) {
76                return [ 'min' => 1, 'max' => 2 ];
77            } else {
78                return [ 'min' => 0, 'max' => 2 ];
79            }
80        } else {
81            return [ 'min' => 0, 'max' => 2 ];
82        }
83    }
84
85    /** @inheritDoc */
86    public function after( Element $node, Node $otherNode, SerializerState $state ): array {
87        if ( !( $node->lastChild && DOMCompat::nodeName( $node->lastChild ) === 'br' )
88            && self::isPPTransition( $otherNode )
89            // A new wikitext line could start at this P-tag. We have to figure out
90            // if 'node' needs a separation of 2 newlines from that P-tag. Examine
91            // previous siblings of 'node' to see if we emitted a block tag
92            // there => we can make do with 1 newline separator instead of 2
93            // before the P-tag.
94             && !$this->currWikitextLineHasBlockNode( $state->currLine, $node, true )
95            // Since we are going to emit newlines before the other P-tag, we know it
96            // is going to start a new wikitext line. We have to figure out if 'node'
97            // needs a separation of 2 newlines from that P-tag. Examine following
98            // siblings of 'node' to see if we might emit a block tag there => we can
99            // make do with 1 newline separator instead of 2 before the P-tag.
100             && !$this->newWikitextLineMightHaveBlockNode( $otherNode )
101        ) {
102            return [ 'min' => 2, 'max' => 2 ];
103        } elseif ( DOMUtils::atTheTop( $otherNode ) ) {
104            return [ 'min' => 0, 'max' => 2 ];
105        } elseif ( self::treatAsPPTransition( $otherNode )
106            || ( DOMUtils::isWikitextBlockNode( $otherNode )
107                && DOMCompat::nodeName( $otherNode ) !== 'blockquote'
108                && $node->parentNode === $otherNode )
109        ) {
110            if ( !DOMUtils::hasNameOrHasAncestorOfName( $otherNode, 'figcaption' ) ) {
111                return [ 'min' => 1, 'max' => 2 ];
112            } else {
113                return [ 'min' => 0, 'max' => 2 ];
114            }
115        } else {
116            return [ 'min' => 0, 'max' => 2 ];
117        }
118    }
119
120    // IMPORTANT: Do not start walking from line.firstNode forward. Always
121    // walk backward from node. This is because in selser mode, it looks like
122    // line.firstNode doesn't always correspond to the wikitext line that is
123    // being processed since the previous emitted node might have been an unmodified
124    // DOM node that generated multiple wikitext lines.
125
126    /**
127     * @param ?stdClass $line See SerializerState::$currLine
128     * @param Node $node
129     * @param bool $skipNode
130     * @return bool
131     */
132    private function currWikitextLineHasBlockNode(
133        ?stdClass $line, Node $node, bool $skipNode = false
134    ): bool {
135        $parentNode = $node->parentNode;
136        if ( !$skipNode ) {
137            // If this node could break this wikitext line and emit
138            // non-ws content on a new line, the P-tag will be on that new line
139            // with text content that needs P-wrapping.
140            if ( preg_match( '/\n\S/', $node->textContent ) ) {
141                return false;
142            }
143        }
144        $node = DiffDOMUtils::previousNonDeletedSibling( $node );
145        while ( !$node || !DOMUtils::atTheTop( $node ) ) {
146            while ( $node ) {
147                // If we hit a block node that will render on the same line, we are done!
148                if ( WTUtils::isBlockNodeWithVisibleWT( $node ) ) {
149                    return true;
150                }
151
152                // If this node could break this wikitext line, we are done.
153                // This is conservative because textContent could be looking at descendents
154                // of 'node' that may not have been serialized yet. But this is safe.
155                if ( str_contains( $node->textContent, "\n" ) ) {
156                    return false;
157                }
158
159                $node = DiffDOMUtils::previousNonDeletedSibling( $node );
160
161                // Don't go past the current line in any case.
162                if ( !empty( $line->firstNode ) && $node &&
163                    DOMUtils::isAncestorOf( $node, $line->firstNode )
164                ) {
165                    return false;
166                }
167            }
168            $node = $parentNode;
169            $parentNode = $node->parentNode;
170        }
171
172        return false;
173    }
174
175    private function newWikitextLineMightHaveBlockNode( Node $node ): bool {
176        $node = DiffDOMUtils::nextNonDeletedSibling( $node );
177        while ( $node ) {
178            if ( $node instanceof Text ) {
179                // If this node will break this wikitext line, we are done!
180                if ( preg_match( '/\n/', $node->nodeValue ) ) {
181                    return false;
182                }
183            } elseif ( $node instanceof Element ) {
184                // These tags will always serialize onto a new line
185                if (
186                    isset( Consts::$HTMLTagsRequiringSOLContext[DOMCompat::nodeName( $node )] ) &&
187                    !WTUtils::isLiteralHTMLNode( $node )
188                ) {
189                    return false;
190                }
191
192                // We hit a block node that will render on the same line
193                if ( WTUtils::isBlockNodeWithVisibleWT( $node ) ) {
194                    return true;
195                }
196
197                // Go conservative
198                return false;
199            }
200
201            $node = DiffDOMUtils::nextNonDeletedSibling( $node );
202        }
203        return false;
204    }
205
206    /**
207     * Node is being serialized before/after a P-tag.
208     * While computing newline constraints, this function tests
209     * if node should be treated as a P-wrapped node.
210     * @param Node $node
211     * @return bool
212     */
213    private static function treatAsPPTransition( Node $node ): bool {
214        // Treat text/p similar to p/p transition
215        // If an element, it should not be a:
216        // * block node or literal HTML node
217        // * template wrapper
218        // * mw:Includes or Annotation meta or a SOL-transparent link
219        return $node instanceof Text
220            || ( !DOMUtils::atTheTop( $node )
221                && !DOMUtils::isWikitextBlockNode( $node )
222                && !WTUtils::isLiteralHTMLNode( $node )
223                && !WTUtils::isEncapsulationWrapper( $node )
224                && !WTUtils::isSolTransparentLink( $node )
225                && !DOMUtils::matchTypeOf( $node, '#^mw:Includes/#' )
226                && !DOMUtils::matchTypeOf( $node, '#^mw:Annotation/#' ) );
227    }
228
229    /**
230     * Test if $node is a P-wrapped node or should be treated as one.
231     *
232     * @param ?Node $node
233     * @return bool
234     */
235    public static function isPPTransition( ?Node $node ): bool {
236        if ( !$node ) {
237            return false;
238        }
239        return ( $node instanceof Element // for static analyzers
240                && DOMCompat::nodeName( $node ) === 'p'
241                && ( DOMDataUtils::getDataParsoid( $node )->stx ?? '' ) !== 'html' )
242            || self::treatAsPPTransition( $node );
243    }
244
245}