• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2011 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import sys
6import re
7import os
8import locale
9from functools import reduce
10
11
12def XmlToString(content, encoding="utf-8", pretty=False):
13    """ Writes the XML content to disk, touching the file only if it has changed.
14
15  Visual Studio files have a lot of pre-defined structures.  This function makes
16  it easy to represent these structures as Python data structures, instead of
17  having to create a lot of function calls.
18
19  Each XML element of the content is represented as a list composed of:
20  1. The name of the element, a string,
21  2. The attributes of the element, a dictionary (optional), and
22  3+. The content of the element, if any.  Strings are simple text nodes and
23      lists are child elements.
24
25  Example 1:
26      <test/>
27  becomes
28      ['test']
29
30  Example 2:
31      <myelement a='value1' b='value2'>
32         <childtype>This is</childtype>
33         <childtype>it!</childtype>
34      </myelement>
35
36  becomes
37      ['myelement', {'a':'value1', 'b':'value2'},
38         ['childtype', 'This is'],
39         ['childtype', 'it!'],
40      ]
41
42  Args:
43    content:  The structured content to be converted.
44    encoding: The encoding to report on the first XML line.
45    pretty: True if we want pretty printing with indents and new lines.
46
47  Returns:
48    The XML content as a string.
49  """
50    # We create a huge list of all the elements of the file.
51    xml_parts = ['<?xml version="1.0" encoding="%s"?>' % encoding]
52    if pretty:
53        xml_parts.append("\n")
54    _ConstructContentList(xml_parts, content, pretty)
55
56    # Convert it to a string
57    return "".join(xml_parts)
58
59
60def _ConstructContentList(xml_parts, specification, pretty, level=0):
61    """ Appends the XML parts corresponding to the specification.
62
63  Args:
64    xml_parts: A list of XML parts to be appended to.
65    specification:  The specification of the element.  See EasyXml docs.
66    pretty: True if we want pretty printing with indents and new lines.
67    level: Indentation level.
68  """
69    # The first item in a specification is the name of the element.
70    if pretty:
71        indentation = "  " * level
72        new_line = "\n"
73    else:
74        indentation = ""
75        new_line = ""
76    name = specification[0]
77    if not isinstance(name, str):
78        raise Exception(
79            "The first item of an EasyXml specification should be "
80            "a string.  Specification was " + str(specification)
81        )
82    xml_parts.append(indentation + "<" + name)
83
84    # Optionally in second position is a dictionary of the attributes.
85    rest = specification[1:]
86    if rest and isinstance(rest[0], dict):
87        for at, val in sorted(rest[0].items()):
88            xml_parts.append(f' {at}="{_XmlEscape(val, attr=True)}"')
89        rest = rest[1:]
90    if rest:
91        xml_parts.append(">")
92        all_strings = reduce(lambda x, y: x and isinstance(y, str), rest, True)
93        multi_line = not all_strings
94        if multi_line and new_line:
95            xml_parts.append(new_line)
96        for child_spec in rest:
97            # If it's a string, append a text node.
98            # Otherwise recurse over that child definition
99            if isinstance(child_spec, str):
100                xml_parts.append(_XmlEscape(child_spec))
101            else:
102                _ConstructContentList(xml_parts, child_spec, pretty, level + 1)
103        if multi_line and indentation:
104            xml_parts.append(indentation)
105        xml_parts.append(f"</{name}>{new_line}")
106    else:
107        xml_parts.append("/>%s" % new_line)
108
109
110def WriteXmlIfChanged(content, path, encoding="utf-8", pretty=False,
111                      win32=(sys.platform == "win32")):
112    """ Writes the XML content to disk, touching the file only if it has changed.
113
114  Args:
115    content:  The structured content to be written.
116    path: Location of the file.
117    encoding: The encoding to report on the first line of the XML file.
118    pretty: True if we want pretty printing with indents and new lines.
119  """
120    xml_string = XmlToString(content, encoding, pretty)
121    if win32 and os.linesep != "\r\n":
122        xml_string = xml_string.replace("\n", "\r\n")
123
124    default_encoding = locale.getdefaultlocale()[1]
125    if default_encoding and default_encoding.upper() != encoding.upper():
126        xml_string = xml_string.encode(encoding)
127
128    # Get the old content
129    try:
130        with open(path) as file:
131            existing = file.read()
132    except OSError:
133        existing = None
134
135    # It has changed, write it
136    if existing != xml_string:
137        with open(path, "wb") as file:
138            file.write(xml_string)
139
140
141_xml_escape_map = {
142    '"': "&quot;",
143    "'": "&apos;",
144    "<": "&lt;",
145    ">": "&gt;",
146    "&": "&amp;",
147    "\n": "&#xA;",
148    "\r": "&#xD;",
149}
150
151
152_xml_escape_re = re.compile("(%s)" % "|".join(map(re.escape, _xml_escape_map.keys())))
153
154
155def _XmlEscape(value, attr=False):
156    """ Escape a string for inclusion in XML."""
157
158    def replace(match):
159        m = match.string[match.start() : match.end()]
160        # don't replace single quotes in attrs
161        if attr and m == "'":
162            return m
163        return _xml_escape_map[m]
164
165    return _xml_escape_re.sub(replace, value)
166