Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
6.33% covered (danger)
6.33%
5 / 79
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
DiffUtils
6.33% covered (danger)
6.33%
5 / 79
0.00% covered (danger)
0.00%
0 / 15
2188.74
0.00% covered (danger)
0.00%
0 / 1
 getDiffMark
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 hasDiffMarkers
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 hasDiffMark
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
30
 hasInsertedDiffMark
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 maybeDeletedNode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isDeletedBlockNode
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 directChildrenChanged
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onlySubtreeChanged
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 subtreeUnchanged
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addDiffMark
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 setDiffMark
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 prependTypedMeta
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getAttributes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 attribsEquals
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
132
 isDiffMarker
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Html2Wt;
5
6use Wikimedia\Parsoid\Config\Env;
7use Wikimedia\Parsoid\DOM\Comment;
8use Wikimedia\Parsoid\DOM\Element;
9use Wikimedia\Parsoid\DOM\Node;
10use Wikimedia\Parsoid\DOM\Text;
11use Wikimedia\Parsoid\NodeData\DataParsoidDiff;
12use Wikimedia\Parsoid\Utils\DOMCompat;
13use Wikimedia\Parsoid\Utils\DOMDataUtils;
14use Wikimedia\Parsoid\Utils\DOMUtils;
15
16class DiffUtils {
17    /**
18     * Get a node's diff marker.
19     *
20     * @param Node $node
21     * @return ?DataParsoidDiff
22     */
23    public static function getDiffMark( Node $node ): ?DataParsoidDiff {
24        if ( !( $node instanceof Element ) ) {
25            return null;
26        }
27        return DOMDataUtils::getDataParsoidDiff( $node );
28    }
29
30    /**
31     * Check that the diff markers on the node exist.
32     *
33     * @param Node $node
34     * @return bool
35     */
36    public static function hasDiffMarkers( Node $node ): bool {
37        return self::getDiffMark( $node ) !== null || self::isDiffMarker( $node );
38    }
39
40    public static function hasDiffMark( Node $node, DiffMarkers $mark ): bool {
41        // For 'deletion' and 'insertion' markers on non-element nodes,
42        // a mw:DiffMarker meta is added
43        if ( $mark === DiffMarkers::DELETED || ( $mark === DiffMarkers::INSERTED && !( $node instanceof Element ) ) ) {
44            return self::isDiffMarker( $node->previousSibling, $mark );
45        } else {
46            $diffMarks = self::getDiffMark( $node );
47            return $diffMarks && $diffMarks->hasDiffMarker( $mark );
48        }
49    }
50
51    public static function hasInsertedDiffMark( Node $node ): bool {
52        return self::hasDiffMark( $node, DiffMarkers::INSERTED );
53    }
54
55    public static function maybeDeletedNode( ?Node $node ): bool {
56        return $node instanceof Element && self::isDiffMarker( $node, DiffMarkers::DELETED );
57    }
58
59    /**
60     * Is node a mw:DiffMarker node that represents a deleted block node?
61     * This annotation is added by the DOMDiff pass.
62     *
63     * @param ?Node $node
64     * @return bool
65     */
66    public static function isDeletedBlockNode( ?Node $node ): bool {
67        return $node instanceof Element && self::maybeDeletedNode( $node ) &&
68            $node->hasAttribute( 'data-is-block' );
69    }
70
71    public static function directChildrenChanged( Node $node ): bool {
72        return self::hasDiffMark( $node, DiffMarkers::CHILDREN_CHANGED );
73    }
74
75    public static function onlySubtreeChanged( Element $node ): bool {
76        $dmark = self::getDiffMark( $node );
77        if ( !$dmark ) {
78            return false;
79        }
80        return $dmark->hasOnlyDiffMarkers(
81            DiffMarkers::SUBTREE_CHANGED, DiffMarkers::CHILDREN_CHANGED
82        );
83    }
84
85    public static function subtreeUnchanged( Element $node ): bool {
86        $dmark = self::getDiffMark( $node );
87        if ( !$dmark ) {
88            return true;
89        }
90        return $dmark->hasOnlyDiffMarkers( DiffMarkers::MODIFIED_WRAPPER );
91    }
92
93    public static function addDiffMark( Node $node, Env $env, DiffMarkers $mark ): ?Element {
94        static $ignoreableNodeTypes = [ XML_DOCUMENT_NODE, XML_DOCUMENT_TYPE_NODE, XML_DOCUMENT_FRAG_NODE ];
95
96        if ( $mark === DiffMarkers::DELETED || $mark === DiffMarkers::MOVED ) {
97            return self::prependTypedMeta( $node, "mw:DiffMarker/{$mark->value}" );
98        } elseif ( $node instanceof Text || $node instanceof Comment ) {
99            if ( $mark !== DiffMarkers::INSERTED ) {
100                $env->log( 'error', 'BUG! CHANGE-marker for ', $node->nodeType, ' node is: ', $mark->value );
101            }
102            return self::prependTypedMeta( $node, "mw:DiffMarker/{$mark->value}" );
103        } elseif ( $node instanceof Element ) {
104            self::setDiffMark( $node, $mark );
105        } elseif ( !in_array( $node->nodeType, $ignoreableNodeTypes, true ) ) {
106            $env->log( 'error', 'Unhandled node type', $node->nodeType, 'in addDiffMark!' );
107        }
108
109        return null;
110    }
111
112    /**
113     * Set a diff marker on a node.
114     */
115    private static function setDiffMark( Node $node, DiffMarkers $change ): void {
116        if ( !( $node instanceof Element ) ) {
117            return;
118        }
119        $dpd = DOMDataUtils::getDataParsoidDiffDefault( $node );
120        $dpd->addDiffMarker( $change );
121    }
122
123    /**
124     * Insert a meta element with the passed-in typeof attribute before a node.
125     *
126     * @param Node $node
127     * @param string $type
128     * @return Element
129     */
130    private static function prependTypedMeta( Node $node, string $type ): Element {
131        $meta = $node->ownerDocument->createElement( 'meta' );
132        DOMUtils::addTypeOf( $meta, $type );
133        $node->parentNode->insertBefore( $meta, $node );
134        return $meta;
135    }
136
137    /**
138     * @param Element $node
139     * @param string[] $ignoreableAttribs
140     * @return array<string,mixed>
141     */
142    private static function getAttributes( Element $node, array $ignoreableAttribs ): array {
143        $h = DOMCompat::attributes( $node );
144        foreach ( $h as $name => $_value ) {
145            if ( in_array( $name, $ignoreableAttribs, true ) ) {
146                unset( $h[$name] );
147            }
148        }
149        // If there's no special attribute handler, we want a straight
150        // comparison of these.
151        // XXX This has the side-effect of allocating empty DataParsoid/DataMw
152        // on each node; also we should ideally treat all rich attributes
153        // consistently.
154        if ( !in_array( 'data-parsoid', $ignoreableAttribs, true ) ) {
155            $h['data-parsoid'] = DOMDataUtils::getDataParsoid( $node );
156        }
157        if ( !in_array( 'data-mw', $ignoreableAttribs, true ) ) {
158            $h['data-mw'] = DOMDataUtils::getDataMw( $node );
159        }
160        return $h;
161    }
162
163    /**
164     * Attribute equality test.
165     *
166     * @param Element $nodeA
167     * @param Element $nodeB
168     * @param string[] $ignoreableAttribs
169     * @param array<string,callable(Element,mixed,Element,mixed):bool> $specializedAttribHandlers
170     * @return bool
171     */
172    public static function attribsEquals(
173        Element $nodeA, Element $nodeB, array $ignoreableAttribs, array $specializedAttribHandlers
174    ): bool {
175        $hA = self::getAttributes( $nodeA, $ignoreableAttribs );
176        $hB = self::getAttributes( $nodeB, $ignoreableAttribs );
177
178        if ( count( $hA ) !== count( $hB ) ) {
179            return false;
180        }
181
182        $keysA = array_keys( $hA );
183        sort( $keysA );
184        $keysB = array_keys( $hB );
185        sort( $keysB );
186
187        foreach ( $keysA as $i => $k ) {
188            if ( $k !== $keysB[$i] ) {
189                return false;
190            }
191
192            // Use a specialized compare function, if provided
193            $attribEquals = $specializedAttribHandlers[$k] ?? null;
194            if ( $attribEquals ) {
195                if ( $hA[$k] === null && $hB[$k] === null ) {
196                    /* two nulls count as equal */
197                } elseif ( $hA[$k] === null || $hB[$k] === null ) {
198                    /* only one null => not equal */
199                    return false;
200                // only invoke attribute comparator when both are non-null
201                } elseif ( !$attribEquals( $nodeA, $hA[$k], $nodeB, $hB[$k] ) ) {
202                    return false;
203                }
204            } elseif ( $hA[$k] !== $hB[$k] ) {
205                return false;
206            }
207        }
208
209        return true;
210    }
211
212    /**
213     * Check a node to see whether it's a diff marker.
214     */
215    public static function isDiffMarker(
216        ?Node $node, ?DiffMarkers $mark = null
217    ): bool {
218        if ( !$node ) {
219            return false;
220        }
221
222        if ( $mark !== null ) {
223            return DOMUtils::isMarkerMeta( $node, "mw:DiffMarker/{$mark->value}" );
224        } else {
225            return DOMUtils::nodeName( $node ) === 'meta' &&
226                DOMUtils::matchTypeOf( $node, '#^mw:DiffMarker/#' );
227        }
228    }
229}