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 '"': """, 143 "'": "'", 144 "<": "<", 145 ">": ">", 146 "&": "&", 147 "\n": "
", 148 "\r": "
", 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