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