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