Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.86% covered (danger)
15.86%
23 / 145
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetaHandler
15.86% covered (danger)
15.86%
23 / 145
0.00% covered (danger)
0.00%
0 / 7
3608.48
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
32.39% covered (danger)
32.39%
23 / 71
0.00% covered (danger)
0.00%
0 / 1
308.09
 needToWriteStartMeta
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
90
 needToWriteEndMeta
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
90
 needNewLineSepBeforeMeta
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 before
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
156
 after
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Html2Wt\DOMHandlers;
5
6use Wikimedia\Parsoid\DOM\Element;
7use Wikimedia\Parsoid\DOM\Node;
8use Wikimedia\Parsoid\DOM\Text;
9use Wikimedia\Parsoid\Html2Wt\DiffUtils;
10use Wikimedia\Parsoid\Html2Wt\SerializerState;
11use Wikimedia\Parsoid\Html2Wt\WTSUtils;
12use Wikimedia\Parsoid\Utils\DiffDOMUtils;
13use Wikimedia\Parsoid\Utils\DOMCompat;
14use Wikimedia\Parsoid\Utils\DOMDataUtils;
15use Wikimedia\Parsoid\Utils\DOMUtils;
16use Wikimedia\Parsoid\Utils\WTUtils;
17
18class 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 = $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 ( DOMCompat::getAttribute( $node, '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 = DOMCompat::getAttribute( $node, 'typeof' ) ??
248            DOMCompat::getAttribute( $node, 'property' );
249        if ( $type && str_contains( $type, 'mw:PageProp/categorydefaultsort' ) ) {
250            if ( $otherNode instanceof Element
251                && DOMCompat::nodeName( $otherNode ) === 'p'
252                && ( DOMDataUtils::getDataParsoid( $otherNode )->stx ?? null ) !== 'html'
253            ) {
254                // Since defaultsort is outside the p-tag, we need 2 newlines
255                // to ensure that it go back into the p-tag when parsed.
256                return [ 'min' => 2 ];
257            } else {
258                return [ 'min' => 1 ];
259            }
260        } elseif ( WTUtils::isNewElt( $node ) &&
261            // Placeholder and annotation metas or <*include*> tags don't need to be serialized on
262            // their own line
263            !DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' )
264        ) {
265            return [ 'min' => 1 ];
266        } else {
267            return [];
268        }
269    }
270
271    /** @inheritDoc */
272    public function after( Element $node, Node $otherNode, SerializerState $state ): array {
273        if ( WTUtils::isAnnotationEndMarkerMeta( $node ) ) {
274            if ( $otherNode !== $node->parentNode && $otherNode instanceof Element &&
275                DOMUtils::isWikitextBlockNode( $otherNode ) ) {
276                return [ 'min' => 2 ];
277            } else {
278                return [];
279            }
280        }
281        if ( WTUtils::isAnnotationStartMarkerMeta( $node ) ) {
282            if ( $otherNode !== $node->parentNode && $otherNode instanceof Element &&
283                DOMUtils::isWikitextBlockNode( $otherNode ) ) {
284                return [ 'min' => 1 ];
285            } else {
286                return [];
287            }
288        }
289
290        // No diffs
291        if ( WTUtils::isNewElt( $node ) &&
292            // Placeholder and annotation metas or <*include*> tags don't need to be serialized on
293            // their own line
294            !DOMUtils::matchTypeOf( $node, '#^mw:(Placeholder|Includes|Annotation)(/|$)#' )
295        ) {
296            return [ 'min' => 1 ];
297        } else {
298            return [];
299        }
300    }
301}