Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
15.97% |
23 / 144 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
MetaHandler | |
15.97% |
23 / 144 |
|
0.00% |
0 / 7 |
3781.74 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
handle | |
32.39% |
23 / 71 |
|
0.00% |
0 / 1 |
308.09 | |||
needToWriteStartMeta | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
90 | |||
needToWriteEndMeta | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
90 | |||
needNewLineSepBeforeMeta | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
before | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
210 | |||
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 = $node->getAttribute( '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 = $switchType[1]; |
46 | $cat = preg_match( '/^(?:category)?(.*)/', $out, $catMatch ); |
47 | if ( $cat && ( |
48 | // Need this b/c support while RESTBase has Parsoid HTML |
49 | // in storage with meta tags for these. |
50 | // Can be removed as part of T335843 |
51 | $catMatch[1] === 'defaultsort' || $catMatch[1] === 'displaytitle' |
52 | ) ) { |
53 | $contentInfo = $state->serializer->serializedAttrVal( $node, 'content' ); |
54 | if ( WTUtils::hasExpandedAttrsType( $node ) ) { |
55 | $out = '{{' . $contentInfo['value'] . '}}'; |
56 | } elseif ( isset( $dp->src ) ) { |
57 | $colon = strpos( $dp->src, ':', 2 ); |
58 | $out = preg_replace( '/^([^:}]+).*$/D', "$1", $dp->src, 1 ); |
59 | if ( ( $colon === false ) && ( $contentInfo['value'] === '' ) ) { |
60 | $out .= '}}'; |
61 | } else { |
62 | $out .= ':' . $contentInfo['value'] . '}}'; |
63 | } |
64 | } else { |
65 | $magicWord = mb_strtoupper( $catMatch[1] ); |
66 | $out = '{{' . $magicWord . ':' . $contentInfo['value'] . '}}'; |
67 | } |
68 | } else { |
69 | $out = $state->getEnv()->getSiteConfig()->getMagicWordWT( |
70 | $switchType[1], $dp->magicSrc ?? '' ); |
71 | } |
72 | $state->emitChunk( $out, $node ); |
73 | } else { |
74 | ( new FallbackHTMLHandler )->handle( $node, $state ); |
75 | } |
76 | } elseif ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { |
77 | $annType = WTUtils::extractAnnotationType( $node ); |
78 | if ( $this->needToWriteStartMeta( $state, $node ) ) { |
79 | $datamw = DOMDataUtils::getDataMw( $node ); |
80 | $attrs = ""; |
81 | if ( isset( $datamw->attrs ) ) { |
82 | foreach ( get_object_vars( $datamw->attrs ) as $k => $v ) { |
83 | if ( $v === "" ) { |
84 | $attrs .= ' ' . $k; |
85 | } else { |
86 | $attrs .= ' ' . $k . '="' . $v . '"'; |
87 | } |
88 | } |
89 | } |
90 | // Follow-up on attributes sanitation to happen in T295168 |
91 | $state->emitChunk( '<' . $annType . $attrs . '>', $node ); |
92 | $state->openAnnotationRange( $annType, $datamw->extendedRange ?? false ); |
93 | } |
94 | } elseif ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { |
95 | if ( $this->needToWriteEndMeta( $state, $node ) ) { |
96 | $annType = WTUtils::extractAnnotationType( $node ); |
97 | $state->emitChunk( '</' . $annType . '>', $node ); |
98 | $state->closeAnnotationRange( $annType ); |
99 | } |
100 | } else { |
101 | switch ( $node->getAttribute( 'typeof' ) ?? '' ) { |
102 | case 'mw:Includes/IncludeOnly': |
103 | // Remove the dp.src when older revisions of HTML expire in RESTBase |
104 | $state->emitChunk( $dmw->src ?? $dp->src ?? '', $node ); |
105 | break; |
106 | case 'mw:Includes/IncludeOnly/End': |
107 | // Just ignore. |
108 | break; |
109 | case 'mw:Includes/NoInclude': |
110 | $state->emitChunk( $dp->src ?? '<noinclude>', $node ); |
111 | break; |
112 | case 'mw:Includes/NoInclude/End': |
113 | $state->emitChunk( $dp->src ?? '</noinclude>', $node ); |
114 | break; |
115 | case 'mw:Includes/OnlyInclude': |
116 | $state->emitChunk( $dp->src ?? '<onlyinclude>', $node ); |
117 | break; |
118 | case 'mw:Includes/OnlyInclude/End': |
119 | $state->emitChunk( $dp->src ?? '</onlyinclude>', $node ); |
120 | break; |
121 | case 'mw:DiffMarker/inserted': |
122 | case 'mw:DiffMarker/deleted': |
123 | case 'mw:DiffMarker/moved': |
124 | case 'mw:Separator': |
125 | // just ignore it |
126 | break; |
127 | default: |
128 | ( new FallbackHTMLHandler() )->handle( $node, $state ); |
129 | } |
130 | } |
131 | return $node->nextSibling; |
132 | } |
133 | |
134 | /** |
135 | * Decides if we need to write an annotation start meta at the place we encounter it |
136 | * @param SerializerState $state |
137 | * @param Element $node |
138 | * @return bool |
139 | */ |
140 | private function needToWriteStartMeta( SerializerState $state, Element $node ): bool { |
141 | if ( !$state->selserMode ) { |
142 | return true; |
143 | } |
144 | if ( WTUtils::isMovedMetaTag( $node ) ) { |
145 | $nextContentSibling = DOMCompat::getNextElementSibling( $node ); |
146 | // If the meta tag has been moved, it comes from its next element.... "almost". |
147 | // First exception is if we have several marker annotations in a row - then we need |
148 | // to pass them all. Second exception is if we have fostered content: then we're |
149 | // interested in what happens in the table, which happens _after_ the fostered content. |
150 | while ( $nextContentSibling !== null && |
151 | ( WTUtils::isMarkerAnnotation( $nextContentSibling ) || |
152 | !empty( DOMDataUtils::getDataParsoid( $nextContentSibling )->fostered ) |
153 | ) |
154 | ) { |
155 | $nextContentSibling = DOMCompat::getNextElementSibling( $nextContentSibling ); |
156 | } |
157 | |
158 | if ( $nextContentSibling !== null ) { |
159 | // When the content from which the meta tag comes gets |
160 | // deleted or modified, we emit _now_ so that we don't risk losing it. The range |
161 | // stays extended in the round-tripped version of the wikitext. |
162 | $nextdiffdata = DOMDataUtils::getDataParsoidDiff( $nextContentSibling ); |
163 | if ( DiffUtils::isDiffMarker( $nextContentSibling ) || |
164 | ( $nextdiffdata->diff ?? null ) ) { |
165 | return true; |
166 | } |
167 | |
168 | return !WTSUtils::origSrcValidInEditedContext( $state, $nextContentSibling ); |
169 | } |
170 | } |
171 | return true; |
172 | } |
173 | |
174 | /** |
175 | * Decides if we need to write an annotation end meta at the place we encounter it |
176 | * @param SerializerState $state |
177 | * @param Element $node |
178 | * @return bool |
179 | */ |
180 | private function needToWriteEndMeta( SerializerState $state, Element $node ): bool { |
181 | if ( !$state->selserMode ) { |
182 | return true; |
183 | } |
184 | if ( WTUtils::isMovedMetaTag( $node ) ) { |
185 | $prevElementSibling = DOMCompat::getPreviousElementSibling( $node ); |
186 | while ( $prevElementSibling !== null && |
187 | WTUtils::isMarkerAnnotation( $prevElementSibling ) |
188 | ) { |
189 | $prevElementSibling = DOMCompat::getPreviousElementSibling( $prevElementSibling ); |
190 | } |
191 | if ( $prevElementSibling ) { |
192 | $prevdiffdata = DOMDataUtils::getDataParsoidDiff( $prevElementSibling ); |
193 | |
194 | if ( |
195 | DiffUtils::isDiffMarker( $prevElementSibling ) || |
196 | $prevdiffdata !== null && $prevdiffdata->diff !== null |
197 | ) { |
198 | return true; |
199 | } |
200 | return !WTSUtils::origSrcValidInEditedContext( $state, $prevElementSibling ); |
201 | } |
202 | } |
203 | return true; |
204 | } |
205 | |
206 | /** |
207 | * We create a newline (or two) if: |
208 | * * the previous element is a block element |
209 | * * the previous element is text, AND we're not in an inline-text situation: this |
210 | * corresponds to text having been added in VE without creating a paragraph, which happens |
211 | * when inserting a new line before the <meta> tag in VE. The "we're not in an inline text" |
212 | * is a heuristic and doesn't work for the ends of line for instance, but it shouldn't add |
213 | * semantic whitespace either. |
214 | * @param Node $meta |
215 | * @param Node $otherNode |
216 | * @return bool |
217 | */ |
218 | private function needNewLineSepBeforeMeta( Node $meta, Node $otherNode ) { |
219 | return ( $otherNode !== $meta->parentNode |
220 | && ( $otherNode instanceof Element && |
221 | DOMUtils::isWikitextBlockNode( $otherNode ) || |
222 | ( $otherNode instanceof Text && |
223 | DOMUtils::isWikitextBlockNode( DiffDOMUtils::nextNonSepSibling( $meta ) ) |
224 | ) |
225 | ) ); |
226 | } |
227 | |
228 | /** @inheritDoc */ |
229 | public function before( Element $node, Node $otherNode, SerializerState $state ): array { |
230 | if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { |
231 | if ( $this->needNewLineSepBeforeMeta( $node, $otherNode ) ) { |
232 | return [ 'min' => 2 ]; |
233 | } else { |
234 | return []; |
235 | } |
236 | } |
237 | if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { |
238 | if ( $this->needNewLineSepBeforeMeta( $node, $otherNode ) ) { |
239 | return [ |
240 | 'min' => 1 |
241 | ]; |
242 | } else { |
243 | return []; |
244 | } |
245 | } |
246 | |
247 | $type = $node->getAttribute( 'typeof' ) ?: $node->getAttribute( 'property' ) ?: null; |
248 | if ( $type && str_contains( $type, 'mw:PageProp/categorydefaultsort' ) ) { |
249 | if ( $otherNode instanceof Element |
250 | && DOMCompat::nodeName( $otherNode ) === 'p' |
251 | && ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html' |
252 | ) { |
253 | // Since defaultsort is outside the p-tag, we need 2 newlines |
254 | // to ensure that it go back into the p-tag when parsed. |
255 | return [ 'min' => 2 ]; |
256 | } else { |
257 | return [ 'min' => 1 ]; |
258 | } |
259 | } elseif ( WTUtils::isNewElt( $node ) && |
260 | // Placeholder and annotation metas or <*include*> tags don't need to be serialized on |
261 | // their own line |
262 | !DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' ) |
263 | ) { |
264 | return [ 'min' => 1 ]; |
265 | } else { |
266 | return []; |
267 | } |
268 | } |
269 | |
270 | /** @inheritDoc */ |
271 | public function after( Element $node, Node $otherNode, SerializerState $state ): array { |
272 | if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) { |
273 | if ( $otherNode !== $node->parentNode && $otherNode instanceof Element && |
274 | DOMUtils::isWikitextBlockNode( $otherNode ) ) { |
275 | return [ 'min' => 2 ]; |
276 | } else { |
277 | return []; |
278 | } |
279 | } |
280 | if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) { |
281 | if ( $otherNode !== $node->parentNode && $otherNode instanceof Element && |
282 | DOMUtils::isWikitextBlockNode( $otherNode ) ) { |
283 | return [ 'min' => 1 ]; |
284 | } else { |
285 | return []; |
286 | } |
287 | } |
288 | |
289 | // No diffs |
290 | if ( WTUtils::isNewElt( $node ) && |
291 | // Placeholder and annotation metas or <*include*> tags don't need to be serialized on |
292 | // their own line |
293 | !DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' ) |
294 | ) { |
295 | return [ 'min' => 1 ]; |
296 | } else { |
297 | return []; |
298 | } |
299 | } |
300 | } |