summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/tester/xmldiff.py
blob: 17f43a4f6fc82fd25839aa5d123653cb95dd72da (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
#
# 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

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 = "<{}.../>".format(tag_a)
        if tag_b:
            tag_b = "<{}.../>".format(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':
                    from inkex.paths import Path
                    return [attr] + Path(val).to_arrays()
                return (attr, val)
            return val
        self.append((_prep(value_a), _prep(value_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 "{} xml differences".format(len(self))

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 = '{}XXX{}'.format(xml1.tag, 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] = "{}XXX{}".format(xml1.attrib.get(name), 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 = "{}XXX{}".format(xml1.text, xml2.text)
    if not text_compare(xml1.tail, xml2.tail):
        delta.append_text(xml1.tail, xml2.tail)
        xml1.tail = "{}XXX{}".format(xml1.tail, xml2.tail)

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

    for child_a, child_b in zip(children_a, children_b):
        if child_a is None: # child_b exists
            child_c = child_b.clone()
            delta.append_tag(child_c.tag, None)
            child_c.tag = 'XXX' + child_c.tag
            xml1.append(child_c)
        elif child_b is None: # child_a exists
            delta.append_tag(None, child_a.tag)
            child_a.tag += 'XXX'
        else:
            _xmldiff(child_a, child_b, delta)