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    '"': '&quot;',
136    "'": '&apos;',
137    '<': '&lt;',
138    '>': '&gt;',
139    '&': '&amp;',
140    '\n': '&#xA;',
141    '\r': '&#xD;',
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