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