summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/tester/xmldiff.py
blob: 1864530ebc4f34fbd0f0fd6180b7eae139ebb4e2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#
# Copyright 2011 (c) Ian Bicking <ianb@colorstudy.com>
#           2019 (c) Martin Owens <doctormo@gmail.com>
#
# Taken from http://formencode.org under the GPL compatible PSF License.
# Modified to produce more output as a diff.
#
"""
Allow two xml files/lxml etrees to be compared, returning their differences.
"""
import xml.etree.ElementTree as xml
from io import BytesIO

from inkex.paths import Path


def text_compare(test1, test2):
    """
    Compare two text strings while allowing for '*' to match
    anything on either lhs or rhs.
    """
    if not test1 and not test2:
        return True
    if test1 == "*" or test2 == "*":
        return True
    return (test1 or "").strip() == (test2 or "").strip()


class DeltaLogger(list):
    """A record keeper of the delta between two svg files"""

    def append_tag(self, tag_a, tag_b):
        """Record a tag difference"""
        if tag_a:
            tag_a = f"<{tag_a}.../>"
        if tag_b:
            tag_b = f"<{tag_b}.../>"
        self.append((tag_a, tag_b))

    def append_attr(self, attr, value_a, value_b):
        """Record an attribute difference"""

        def _prep(val):
            if val:
                if attr == "d":
                    return [attr] + Path(val).to_arrays()
                return (attr, val)
            return val

        # Only append a difference if the preprocessed values are different.
        # This solves the issue that -0 != 0 in path data.
        prep_a = _prep(value_a)
        prep_b = _prep(value_b)
        if prep_a != prep_b:
            self.append((prep_a, prep_b))

    def append_text(self, text_a, text_b):
        """Record a text difference"""
        self.append((text_a, text_b))

    def __bool__(self):
        """Returns True if there's no log, i.e. the delta is clean"""
        return not self.__len__()

    __nonzero__ = __bool__

    def __repr__(self):
        if self:
            return "No differences detected"
        return f"{len(self)} xml differences"


def to_xml(data):
    """Convert string or bytes to xml parsed root node"""
    if isinstance(data, str):
        data = data.encode("utf8")
    if isinstance(data, bytes):
        return xml.parse(BytesIO(data)).getroot()
    return data


def xmldiff(data1, data2):
    """Create an xml difference, will modify the first xml structure with a diff"""
    xml1, xml2 = to_xml(data1), to_xml(data2)
    delta = DeltaLogger()
    _xmldiff(xml1, xml2, delta)
    return xml.tostring(xml1).decode("utf-8"), delta


def _xmldiff(xml1, xml2, delta):
    if xml1.tag != xml2.tag:
        xml1.tag = f"{xml1.tag}XXX{xml2.tag}"
        delta.append_tag(xml1.tag, xml2.tag)
    for name, value in xml1.attrib.items():
        if name not in xml2.attrib:
            delta.append_attr(name, xml1.attrib[name], None)
            xml1.attrib[name] += "XXX"
        elif xml2.attrib.get(name) != value:
            delta.append_attr(name, xml1.attrib.get(name), xml2.attrib.get(name))
            xml1.attrib[name] = f"{xml1.attrib.get(name)}XXX{xml2.attrib.get(name)}"
    for name, value in xml2.attrib.items():
        if name not in xml1.attrib:
            delta.append_attr(name, None, value)
            xml1.attrib[name] = "XXX" + value
    if not text_compare(xml1.text, xml2.text):
        delta.append_text(xml1.text, xml2.text)
        xml1.text = f"{xml1.text}XXX{xml2.text}"
    if not text_compare(xml1.tail, xml2.tail):
        delta.append_text(xml1.tail, xml2.tail)
        xml1.tail = f"{xml1.tail}XXX{xml2.tail}"

    # Get children and pad with nulls
    children_a = list(xml1)
    children_b = list(xml2)
    children_a += [None] * (len(children_b) - len(children_a))
    children_b += [None] * (len(children_a) - len(children_b))

    for child_a, child_b in zip(children_a, children_b):
        if child_a is None:  # child_b exists
            delta.append_tag(child_b.tag, None)
        elif child_b is None:  # child_a exists
            delta.append_tag(None, child_a.tag)
        else:
            _xmldiff(child_a, child_b, delta)