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