[6a3a178] | 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)
|
---|