Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
10.69% |
14 / 131 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
MetaHandler | |
10.69% |
14 / 131 |
|
0.00% |
0 / 7 |
3560.92 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
handle | |
24.56% |
14 / 57 |
|
0.00% |
0 / 1 |
229.79 | |||
needToWriteStartMeta | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
110 | |||
needToWriteEndMeta | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
90 | |||
needNewLineSepBeforeMeta | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
before | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
156 | |||
after | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
132 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers; |
5 | |
6 | use Wikimedia\Parsoid\DOM\Element; |
7 | use Wikimedia\Parsoid\DOM\Node; |
8 | use Wikimedia\Parsoid\DOM\Text; |
9 | use Wikimedia\Parsoid\Html2Wt\DiffUtils; |
10 | use Wikimedia\Parsoid\Html2Wt\SerializerState; |
11 | use Wikimedia\Parsoid\Html2Wt\WTSUtils; |
12 | use Wikimedia\Parsoid\Utils\DiffDOMUtils; |
13 | use Wikimedia\Parsoid\Utils\DOMCompat; |
14 | use Wikimedia\Parsoid\Utils\DOMDataUtils; |
15 | use Wikimedia\Parsoid\Utils\DOMUtils; |
16 | use Wikimedia\Parsoid\Utils\WTUtils; |
17 | |
18 | class MetaHandler extends DOMHandler { |
19 | |
20 | public function __construct() { |
21 | parent::__construct( false ); |
22 | } |
23 | |
24 | /** @inheritDoc */ |
25 | public function handle( |
26 | Element $node, SerializerState $state, bool $wrapperUnmodified = false |
27 | ): ?Node { |
28 | $property = DOMCompat::getAttribute( $node, 'property' ) ?? ''; |
29 | $dp = DOMDataUtils::getDataParsoid( $node ); |
30 | $dmw = DOMDataUtils::getDataMw( $node ); |
31 | |
32 | if ( isset( $dp->src ) && |
33 | DOMUtils::matchTypeOf( $node, '#^mw:Placeholder(/|$)#' ) |
34 | ) { |
35 | $this->emitPlaceholderSrc( $node, $state ); |
36 | return $node->nextSibling; |
37 | } |
38 | |
39 | // Check for property before type so that page properties with |
40 | // templated attrs roundtrip properly. |
41 | // Ex: {{DEFAULTSORT:{{1x|foo}} }} |
42 | if ( $property ) { |
43 | preg_match( '#^mw\:PageProp/(.*)$#D', $property, $switchType ); |
44 | if ( $switchType ) { |
45 | $out = $state->getEnv()->getSiteConfig()->getMagicWordWT( |
46 | $switchType[1], $dp->magicSrc ?? '' |
47 | ); |
48 | $state->emitChunk( $out, $node ); |
49 | } else { |
50 | ( new FallbackHTMLHandler )->handle( $node, $state ); |
51 | } |
52 | } elseif ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { |
53 | $annType = WTUtils::extractAnnotationType( $node ); |
54 | if ( $this->needToWriteStartMeta( $state, $node ) ) { |
55 | $datamw = DOMDataUtils::getDataMw( $node ); |
56 | $attrs = ""; |
57 | foreach ( ( $datamw->getExtAttribs() ?? [] ) as $k => $v ) { |
58 | // numeric $k will get converted to int by PHP when |
59 | // they are used as array keys |
60 | $k = (string)$k; |
61 | if ( $v === "" ) { |
62 | $attrs .= ' ' . $k; |
63 | } else { |
64 | $attrs .= ' ' . $k . '="' . $v . '"'; |
65 | } |
66 | } |
67 | // Follow-up on attributes sanitation to happen in T295168 |
68 | $state->emitChunk( '<' . $annType . $attrs . '>', $node ); |
69 | $state->openAnnotationRange( $annType, $datamw->extendedRange ?? false ); |
70 | } |
71 | } elseif ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { |
72 | if ( $this->needToWriteEndMeta( $state, $node ) ) { |
73 | $annType = WTUtils::extractAnnotationType( $node ); |
74 | $state->emitChunk( '</' . $annType . '>', $node ); |
75 | $state->closeAnnotationRange( $annType ); |
76 | } |
77 | } else { |
78 | switch ( DOMCompat::getAttribute( $node, 'typeof' ) ) { |
79 | case 'mw:Includes/IncludeOnly': |
80 | // Remove the dp.src when older revisions of HTML expire in RESTBase |
81 | $state->emitChunk( $dmw->src ?? $dp->src ?? '', $node ); |
82 | break; |
83 | case 'mw:Includes/IncludeOnly/End': |
84 | // Just ignore. |
85 | break; |
86 | case 'mw:Includes/NoInclude': |
87 | $state->emitChunk( $dp->src ?? '<noinclude>', $node ); |
88 | break; |
89 | case 'mw:Includes/NoInclude/End': |
90 | $state->emitChunk( $dp->src ?? '</noinclude>', $node ); |
91 | break; |
92 | case 'mw:Includes/OnlyInclude': |
93 | $state->emitChunk( $dp->src ?? '<onlyinclude>', $node ); |
94 | break; |
95 | case 'mw:Includes/OnlyInclude/End': |
96 | $state->emitChunk( $dp->src ?? '</onlyinclude>', $node ); |
97 | break; |
98 | case 'mw:DiffMarker/inserted': |
99 | case 'mw:DiffMarker/deleted': |
100 | case 'mw:DiffMarker/moved': |
101 | case 'mw:Separator': |
102 | // just ignore it |
103 | break; |
104 | default: |
105 | ( new FallbackHTMLHandler() )->handle( $node, $state ); |
106 | } |
107 | } |
108 | return $node->nextSibling; |
109 | } |
110 | |
111 | /** |
112 | * Decides if we need to write an annotation start meta at the place we encounter it |
113 | * @param SerializerState $state |
114 | * @param Element $node |
115 | * @return bool |
116 | */ |
117 | private function needToWriteStartMeta( SerializerState $state, Element $node ): bool { |
118 | if ( !$state->selserMode ) { |
119 | return true; |
120 | } |
121 | if ( WTUtils::isMovedMetaTag( $node ) ) { |
122 | $nextContentSibling = DOMCompat::getNextElementSibling( $node ); |
123 | // If the meta tag has been moved, it comes from its next element.... "almost". |
124 | // First exception is if we have several marker annotations in a row - then we need |
125 | // to pass them all. Second exception is if we have fostered content: then we're |
126 | // interested in what happens in the table, which happens _after_ the fostered content. |
127 | while ( $nextContentSibling !== null && |
128 | ( WTUtils::isMarkerAnnotation( $nextContentSibling ) || |
129 | !empty( DOMDataUtils::getDataParsoid( $nextContentSibling )->fostered ) |
130 | ) |
131 | ) { |
132 | $nextContentSibling = DOMCompat::getNextElementSibling( $nextContentSibling ); |
133 | } |
134 | |
135 | if ( $nextContentSibling !== null ) { |
136 | // When the content from which the meta tag comes gets |
137 | // deleted or modified, we emit _now_ so that we don't risk losing it. The range |
138 | // stays extended in the round-tripped version of the wikitext. |
139 | $nextdiffdata = DOMDataUtils::getDataParsoidDiff( $nextContentSibling ); |
140 | if ( |
141 | DiffUtils::isDiffMarker( $nextContentSibling ) || |
142 | ( $nextdiffdata && !$nextdiffdata->isEmpty() ) |
143 | ) { |
144 | return true; |
145 | } |
146 | |
147 | return !WTSUtils::origSrcValidInEditedContext( $state, $nextContentSibling ); |
148 | } |
149 | } |
150 | return true; |
151 | } |
152 | |
153 | /** |
154 | * Decides if we need to write an annotation end meta at the place we encounter it |
155 | * @param SerializerState $state |
156 | * @param Element $node |
157 | * @return bool |
158 | */ |
159 | private function needToWriteEndMeta( SerializerState $state, Element $node ): bool { |
160 | if ( !$state->selserMode ) { |
161 | return true; |
162 | } |
163 | if ( WTUtils::isMovedMetaTag( $node ) ) { |
164 | $prevElementSibling = DOMCompat::getPreviousElementSibling( $node ); |
165 | while ( $prevElementSibling !== null && |
166 | WTUtils::isMarkerAnnotation( $prevElementSibling ) |
167 | ) { |
168 | $prevElementSibling = DOMCompat::getPreviousElementSibling( $prevElementSibling ); |
169 | } |
170 | if ( $prevElementSibling ) { |
171 | $prevdiffdata = DOMDataUtils::getDataParsoidDiff( $prevElementSibling ); |
172 | |
173 | if ( |
174 | DiffUtils::isDiffMarker( $prevElementSibling ) || |
175 | ( $prevdiffdata && !$prevdiffdata->isEmpty() ) |
176 | ) { |
177 | return true; |
178 | } |
179 | return !WTSUtils::origSrcValidInEditedContext( $state, $prevElementSibling ); |
180 | } |
181 | } |
182 | return true; |
183 | } |
184 | |
185 | /** |
186 | * We create a newline (or two) if: |
187 | * * the previous element is a block element |
188 | * * the previous element is text, AND we're not in an inline-text situation: this |
189 | * corresponds to text having been added in VE without creating a paragraph, which happens |
190 | * when inserting a new line before the <meta> tag in VE. The "we're not in an inline text" |
191 | * is a heuristic and doesn't work for the ends of line for instance, but it shouldn't add |
192 | * semantic whitespace either. |
193 | * @param Node $meta |
194 | * @param Node $otherNode |
195 | * @return bool |
196 | */ |
197 | private function needNewLineSepBeforeMeta( Node $meta, Node $otherNode ) { |
198 | return ( $otherNode !== $meta->parentNode |
199 | && ( |
200 | ( $otherNode instanceof Element && DOMUtils::isWikitextBlockNode( $otherNode ) ) || |
201 | ( $otherNode instanceof Text && |
202 | DOMUtils::isWikitextBlockNode( DiffDOMUtils::nextNonSepSibling( $meta ) ) |
203 | ) |
204 | ) ); |
205 | } |
206 | |
207 | /** @inheritDoc */ |
208 | public function before( Element $node, Node $otherNode, SerializerState $state ): array { |
209 | if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { |
210 | if ( $this->needNewLineSepBeforeMeta( $node, $otherNode ) ) { |
211 | return [ 'min' => 2 ]; |
212 | } else { |
213 | return []; |
214 | } |
215 | } |
216 | if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { |
217 | if ( $this->needNewLineSepBeforeMeta( $node, $otherNode ) ) { |
218 | return [ |
219 | 'min' => 1 |
220 | ]; |
221 | } else { |
222 | return []; |
223 | } |
224 | } |
225 | |
226 | $type = DOMCompat::getAttribute( $node, 'typeof' ) ?? |
227 | DOMCompat::getAttribute( $node, 'property' ); |
228 | if ( $type && str_contains( $type, 'mw:PageProp/categorydefaultsort' ) ) { |
229 | if ( $otherNode instanceof Element |
230 | && DOMCompat::nodeName( $otherNode ) === 'p' |
231 | && ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html' |
232 | ) { |
233 | // Since defaultsort is outside the p-tag, we need 2 newlines |
234 | // to ensure that it go back into the p-tag when parsed. |
235 | return [ 'min' => 2 ]; |
236 | } else { |
237 | return [ 'min' => 1 ]; |
238 | } |
239 | } elseif ( WTUtils::isNewElt( $node ) && |
240 | // Placeholder and annotation metas or <*include*> tags don't need to be serialized on |
241 | // their own line |
242 | !DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' ) |
243 | ) { |
244 | return [ 'min' => 1 ]; |
245 | } else { |
246 | return []; |
247 | } |
248 | } |
249 | |
250 | /** @inheritDoc */ |
251 | public function after( Element $node, Node $otherNode, SerializerState $state ): array { |
252 | if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { |
253 | if ( $otherNode !== $node->parentNode && $otherNode instanceof Element && |
254 | DOMUtils::isWikitextBlockNode( $otherNode ) ) { |
255 | return [ 'min' => 2 ]; |
256 | } else { |
257 | return []; |
258 | } |
259 | } |
260 | if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { |
261 | if ( $otherNode !== $node->parentNode && $otherNode instanceof Element && |
262 | DOMUtils::isWikitextBlockNode( $otherNode ) ) { |
263 | return [ 'min' => 1 ]; |
264 | } else { |
265 | return []; |
266 | } |
267 | } |
268 | |
269 | // No diffs |
270 | if ( WTUtils::isNewElt( $node ) && |
271 | // Placeholder and annotation metas or <*include*> tags don't need to be serialized on |
272 | // their own line |
273 | !DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' ) |
274 | ) { |
275 | return [ 'min' => 1 ]; |
276 | } else { |
277 | return []; |
278 | } |
279 | } |
280 | } |