Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 96 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
PHandler | |
0.00% |
0 / 96 |
|
0.00% |
0 / 8 |
4556 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
handle | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
before | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
462 | |||
after | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
156 | |||
currWikitextLineHasBlockNode | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
132 | |||
newWikitextLineMightHaveBlockNode | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 | |||
treatAsPPTransition | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
72 | |||
isPPTransition | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers; |
5 | |
6 | use stdClass; |
7 | use Wikimedia\Parsoid\DOM\Element; |
8 | use Wikimedia\Parsoid\DOM\Node; |
9 | use Wikimedia\Parsoid\DOM\Text; |
10 | use Wikimedia\Parsoid\Html2Wt\SerializerState; |
11 | use Wikimedia\Parsoid\Utils\DiffDOMUtils; |
12 | use Wikimedia\Parsoid\Utils\DOMCompat; |
13 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
14 | use Wikimedia\Parsoid\Utils\DOMUtils; |
15 | use Wikimedia\Parsoid\Utils\WTUtils; |
16 | use Wikimedia\Parsoid\Wikitext\Consts; |
17 | |
18 | class 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 | } |