easy_xml.py 4.9 KB

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