Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
10.69% covered (danger)
10.69%
14 / 131
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
MetaHandler
10.69% covered (danger)
10.69%
14 / 131
0.00% covered (danger)
0.00%
0 / 7
3560.92
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
24.56% covered (danger)
24.56%
14 / 57
0.00% covered (danger)
0.00%
0 / 1
229.79
 needToWriteStartMeta
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
110
 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 = $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}