Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
WTSUtils
0.00% covered (danger)
0.00%
0 / 145
0.00% covered (danger)
0.00%
0 / 15
5700
0.00% covered (danger)
0.00%
0 / 1
 isValidSep
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasValidTagWidths
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getAttributeKVArray
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 mkTagTk
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 mkEndTagTk
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getShadowInfo
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 getAttributeShadowInfo
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 commentWT
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 nextToDeletedBlockNodeInWT
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
90
 precedingSpaceSuppressesIndentPre
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 traceNodeName
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 origSrcValidInEditedContext
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
552
 dsrContainsOpenExtendedRangeAnnotationTag
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 getAttrFromDataMw
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 escapeNowikiTags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Html2Wt;
5
6use Wikimedia\Assert\UnreachableException;
7use Wikimedia\Parsoid\Core\DomSourceRange;
8use Wikimedia\Parsoid\DOM\Element;
9use Wikimedia\Parsoid\DOM\Node;
10use Wikimedia\Parsoid\DOM\Text;
11use Wikimedia\Parsoid\NodeData\DataMw;
12use Wikimedia\Parsoid\NodeData\DataMwAttrib;
13use Wikimedia\Parsoid\Tokens\EndTagTk;
14use Wikimedia\Parsoid\Tokens\KV;
15use Wikimedia\Parsoid\Tokens\TagTk;
16use Wikimedia\Parsoid\Utils\DiffDOMUtils;
17use Wikimedia\Parsoid\Utils\DOMCompat;
18use Wikimedia\Parsoid\Utils\DOMDataUtils;
19use Wikimedia\Parsoid\Utils\DOMUtils;
20use Wikimedia\Parsoid\Utils\PHPUtils;
21use Wikimedia\Parsoid\Utils\WTUtils;
22
23class WTSUtils {
24
25    public static function isValidSep( string $sep ): bool {
26        /* TODO (Anomie)
27        You might be able to simplify the regex a bit using a no-backtracking group:
28        '/^(?>\s*<!--.*?-->)*\s*$/s'
29        Although I'm not sure that'll actually run faster.*/
30        return (bool)preg_match( '/^(\s|<!--([^\-]|-(?!->))*-->)*$/uD', $sep );
31    }
32
33    public static function hasValidTagWidths( ?DomSourceRange $dsr ): bool {
34        return $dsr !== null && $dsr->hasValidTagWidths();
35    }
36
37    /**
38     * Get the attributes on a node in an array of KV objects.
39     *
40     * @param Element $node
41     * @return KV[]
42     */
43    public static function getAttributeKVArray( Element $node ): array {
44        $kvs = [];
45        foreach ( DOMUtils::attributes( $node ) as $name => $value ) {
46            $kvs[] = new KV( $name, $value );
47        }
48        return $kvs;
49    }
50
51    /**
52     * Create a `TagTk` corresponding to a DOM node.
53     *
54     * @param Element $node
55     * @return TagTk
56     */
57    public static function mkTagTk( Element $node ): TagTk {
58        $attribKVs = self::getAttributeKVArray( $node );
59        return new TagTk(
60            DOMCompat::nodeName( $node ),
61            $attribKVs,
62            DOMDataUtils::getDataParsoid( $node )
63        );
64    }
65
66    /**
67     * Create a `EndTagTk` corresponding to a DOM node.
68     *
69     * @param Element $node
70     * @return EndTagTk
71     */
72    public static function mkEndTagTk( Element $node ): EndTagTk {
73        $attribKVs = self::getAttributeKVArray( $node );
74        return new EndTagTk(
75            DOMCompat::nodeName( $node ),
76            $attribKVs,
77            DOMDataUtils::getDataParsoid( $node )
78        );
79    }
80
81    /**
82     * For new elements, attrs are always considered modified.  However, For
83     * old elements, we only consider an attribute modified if we have shadow
84     * info for it and it doesn't match the current value.
85     * Returns array with data:
86     * [
87     * value => mixed,
88     * modified => bool (If the value of the attribute changed since we parsed the wikitext),
89     * fromsrc => bool (Whether we got the value from source-based roundtripping)
90     * ]
91     *
92     * @param Element $node
93     * @param string $name
94     * @param ?string $curVal
95     * @return array
96     */
97    public static function getShadowInfo( Element $node, string $name, ?string $curVal ): array {
98        $dp = DOMDataUtils::getDataParsoid( $node );
99
100        // Not the case, continue regular round-trip information.
101        if ( !isset( $dp->a ) || !array_key_exists( $name, $dp->a ) ) {
102            return [
103                'value' => $curVal,
104                // Mark as modified if a new element
105                'modified' => WTUtils::isNewElt( $node ),
106                'fromsrc' => false
107            ];
108        } elseif ( $dp->a[$name] !== $curVal ) {
109            return [
110                'value' => $curVal,
111                'modified' => true,
112                'fromsrc' => false
113            ];
114        } elseif ( !isset( $dp->sa ) || !array_key_exists( $name, $dp->sa ) ) {
115            return [
116                'value' => $curVal,
117                'modified' => false,
118                'fromsrc' => false
119            ];
120        } else {
121            return [
122                'value' => $dp->sa[$name],
123                'modified' => false,
124                'fromsrc' => true
125            ];
126        }
127    }
128
129    /**
130     * Get shadowed information about an attribute on a node.
131     * Returns array with data:
132     * [
133     * value => mixed,
134     * modified => bool (If the value of the attribute changed since we parsed the wikitext),
135     * fromsrc => bool (Whether we got the value from source-based roundtripping)
136     * ]
137     *
138     * @param Element $node
139     * @param string $name
140     * @return array
141     */
142    public static function getAttributeShadowInfo( Element $node, string $name ): array {
143        return self::getShadowInfo(
144            $node,
145            $name,
146            DOMCompat::getAttribute( $node, $name )
147        );
148    }
149
150    public static function commentWT( string $comment ): string {
151        return '<!--' . WTUtils::decodeComment( $comment ) . '-->';
152    }
153
154    /**
155     * In wikitext, did origNode occur next to a block node which has been
156     * deleted? While looking for next, we look past DOM nodes that are
157     * transparent in rendering. (See emitsSolTransparentSingleLineWT for
158     * which nodes.)
159     *
160     * @param ?Node $origNode
161     * @param bool $before
162     * @return bool
163     */
164    public static function nextToDeletedBlockNodeInWT(
165        ?Node $origNode, bool $before
166    ): bool {
167        if ( !$origNode || DOMUtils::atTheTop( $origNode ) ) {
168            return false;
169        }
170
171        while ( true ) {
172            // Find the nearest node that shows up in HTML (ignore nodes that show up
173            // in wikitext but don't affect sol-state or HTML rendering -- note that
174            // whitespace is being ignored, but that whitespace occurs between block nodes).
175            $node = $origNode;
176            do {
177                $node = $before ? $node->previousSibling : $node->nextSibling;
178                if ( DiffUtils::maybeDeletedNode( $node ) ) {
179                    return DiffUtils::isDeletedBlockNode( $node );
180                }
181            } while ( $node && WTUtils::emitsSolTransparentSingleLineWT( $node ) );
182
183            if ( $node ) {
184                return false;
185            } else {
186                // Walk up past zero-width wikitext parents
187                $node = $origNode->parentNode;
188                if ( !WTUtils::isZeroWidthWikitextElt( $node ) ) {
189                    // If the parent occupies space in wikitext,
190                    // clearly, we are not next to a deleted block node!
191                    // We'll eventually hit BODY here and return.
192                    return false;
193                }
194                $origNode = $node;
195            }
196        }
197    }
198
199    /**
200     * Check if whitespace preceding this node would NOT trigger an indent-pre.
201     *
202     * @param Node $node
203     * @param Node $sepNode
204     * @return bool
205     */
206    public static function precedingSpaceSuppressesIndentPre( Node $node, Node $sepNode ): bool {
207        if ( $node !== $sepNode && $node instanceof Text ) {
208            // if node is the same as sepNode, then the separator text
209            // at the beginning of it has been stripped out already, and
210            // we cannot use it to test it for indent-pre safety
211            return (bool)preg_match( '/^[ \t]*\n/', $node->nodeValue );
212        } elseif ( DOMCompat::nodeName( $node ) === 'br' ) {
213            return true;
214        } elseif ( WTUtils::isFirstEncapsulationWrapperNode( $node ) ) {
215            DOMUtils::assertElt( $node );
216            // Dont try any harder than this
217            return !$node->hasChildNodes() || DOMCompat::getInnerHTML( $node )[0] === "\n";
218        } else {
219            return WTUtils::isBlockNodeWithVisibleWT( $node );
220        }
221    }
222
223    public static function traceNodeName( Node $node ): string {
224        switch ( $node->nodeType ) {
225            case XML_ELEMENT_NODE:
226                return ( DiffUtils::isDiffMarker( $node ) ) ? 'DIFF_MARK' : 'NODE: ' . DOMCompat::nodeName( $node );
227            case XML_TEXT_NODE:
228                return 'TEXT: ' . PHPUtils::jsonEncode( $node->nodeValue );
229            case XML_COMMENT_NODE:
230                return 'CMT : ' . PHPUtils::jsonEncode( self::commentWT( $node->nodeValue ) );
231            default:
232                return DOMCompat::nodeName( $node );
233        }
234    }
235
236    /**
237     * In selser mode, check if an unedited node's wikitext from source wikitext
238     * is reusable as is.
239     *
240     * @param SerializerState $state
241     * @param Node $node
242     * @return bool
243     */
244    public static function origSrcValidInEditedContext( SerializerState $state, Node $node ): bool {
245        $env = $state->getEnv();
246        $prev = null;
247
248        if ( WTUtils::isRedirectLink( $node ) ) {
249            return DOMUtils::atTheTop( $node->parentNode ) && !$node->previousSibling;
250        } elseif ( self::dsrContainsOpenExtendedRangeAnnotationTag( $node, $state ) ) {
251            return false;
252        } elseif ( DOMCompat::nodeName( $node ) === 'th' || DOMCompat::nodeName( $node ) === 'td' ) {
253            DOMUtils::assertElt( $node );
254            // The wikitext representation for them is dependent
255            // on cell position (first cell is always single char).
256
257            // If there is no previous sibling, nothing to worry about.
258            $prev = $node->previousSibling;
259            if ( !$prev ) {
260                return true;
261            }
262
263            if (
264                DiffUtils::hasInsertedDiffMark( $prev ) ||
265                DiffUtils::hasInsertedDiffMark( $node )
266            ) {
267                return false;
268            }
269
270            // If previous sibling is unmodified, nothing to worry about.
271            if (
272                !DiffUtils::isDiffMarker( $prev ) &&
273                !DiffUtils::directChildrenChanged( $prev )
274            ) {
275                return true;
276            }
277
278            // If it didn't have a stx marker that indicated that the cell
279            // showed up on the same line via the "||" or "!!" syntax, nothing
280            // to worry about.
281            return ( DOMDataUtils::getDataParsoid( $node )->stx ?? '' ) !== 'row';
282        } elseif ( $node instanceof Element && DOMCompat::nodeName( $node ) === 'tr' &&
283            empty( DOMDataUtils::getDataParsoid( $node )->startTagSrc )
284        ) {
285            // If this <tr> didn't have a startTagSrc, it would have been
286            // the first row of a table in original wikitext. So, it is safe
287            // to reuse the original source for the row (without a "|-") as long as
288            // it continues to be the first row of the table.  If not, since we need to
289            // insert a "|-" to separate it from the newly added row (in an edit),
290            // we cannot simply reuse orig. wikitext for this <tr>.
291            return !DiffDOMUtils::previousNonSepSibling( $node );
292        } elseif ( DOMUtils::isNestedListOrListItem( $node ) ) {
293            if ( DOMUtils::isList( $node ) ) {
294                // Lists never get bullets assigned to them. So, unless they
295                // start a fresh list ( => they have a previous sibling ),
296                // we cannot reuse source for nested lists.
297                if ( !$node->previousSibling ) {
298                    return false;
299                }
300            } else {
301                // Consider this wikitext snippet and its output below:
302                //
303                //   ** a
304                //   *** b
305                //
306                //   <ul><li-*>
307                //   <ul><li-*> a              <-- cannot reuse source of this <li>
308                //   <ul><li-***> b</li></ul>  <-- can reuse source of this <li>
309                //   </li></ul>
310                //   </li></ul>
311                //
312                // If we reuse the src for the inner li with the a, we'd be missing
313                // one bullet because the tag handler for lists in the serializer only
314                // emits start tag src when it hits a first child that isn't a list
315                // element. We need to walk up and get the other bullet(s).
316                //
317                // The above logic can be condensed into this observation.
318                // Reusable nested <li> nodes will always have multiple bullets.
319                // Don't reuse source from any nested list
320                $dp = DOMDataUtils::getDataParsoid( $node );
321                if ( !isset( $dp->dsr ) || $dp->dsr->openWidth < 2 ) {
322                    return false;
323                }
324            }
325
326            // If a previous sibling was modified, we can't reuse the start dsr.
327            $prev = $node->previousSibling;
328            while ( $prev ) {
329                if ( DiffUtils::isDiffMarker( $prev ) || DiffUtils::hasInsertedDiffMark( $prev ) ) {
330                    return false;
331                }
332                $prev = $prev->previousSibling;
333            }
334
335            return true;
336        } elseif ( WTUtils::isMovedMetaTag( $node ) ) {
337            return false;
338        } else {
339            return true;
340        }
341    }
342
343    /**
344     * We keep track in $state of all extended ranges that are currently open by a <meta> tag.
345     * This method checks whether the wikitext source pointed by the dsr of the node contains either
346     * an opening or closing tag matching that annotation (<translate> or </translate> for example.)
347     * @param Node $node
348     * @param SerializerState $state
349     * @return bool
350     */
351    private static function dsrContainsOpenExtendedRangeAnnotationTag( Node $node,
352        SerializerState $state
353    ): bool {
354        if ( empty( $state->openAnnotations ) || !$node instanceof Element ) {
355            return false;
356        }
357
358        $dsr = DOMDataUtils::getDataParsoid( $node )->dsr ?? null;
359        if ( !$dsr ) {
360            return false;
361        }
362        $src = $state->getOrigSrc( $dsr->innerRange() );
363        foreach ( $state->openAnnotations as $ann => $extended ) {
364            if ( $extended ) {
365                if ( preg_match( '</?' . $ann . '.*>', $src ) ) {
366                    return true;
367                }
368            }
369        }
370        return false;
371    }
372
373    /**
374     * FIXME: This method should probably be moved to DOMDataUtils class since
375     * it is used by both html2wt and wt2html code
376     *
377     * @param DataMw $dataMw
378     * @param string $key
379     * @param bool $keep
380     * @return ?DataMwAttrib
381     */
382    public static function getAttrFromDataMw(
383        DataMw $dataMw, string $key, bool $keep
384    ): ?DataMwAttrib {
385        $arr = $dataMw->attribs ?? [];
386        $i = false;
387        foreach ( $arr as $k => $a ) {
388            if ( is_string( $a->key ) ) {
389                $txt = $a->key;
390            } elseif ( is_array( $a->key ) ) {
391                $txt = $a->key['txt'] ?? null;
392            } else {
393                throw new UnreachableException( 'Control should never get here!' );
394            }
395            if ( $txt === $key ) {
396                $i = $k;
397                break;
398            }
399        }
400        if ( $i === false ) {
401            return null;
402        }
403
404        $ret = $arr[$i];
405        if ( !$keep && !isset( $ret->value['html'] ) ) {
406            array_splice( $arr, $i, 1 );
407            $dataMw->attribs = $arr;
408        }
409        return $ret;
410    }
411
412    /**
413     * Escape `<nowiki>` tags.
414     *
415     * @param string $text
416     * @return string
417     */
418    public static function escapeNowikiTags( string $text ): string {
419        return preg_replace( '#<(/?nowiki\s*/?\s*)>#i', '&lt;$1&gt;', $text );
420    }
421
422}