summaryrefslogtreecommitdiff
path: root/tools/gyp/pylib/gyp/easy_xml.py
blob: acccb47b75ad31f90caf7e4397fc79e1f5dd004d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# Copyright (c) 2011 Google Inc. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import re
import os
import locale
from functools import reduce


def XmlToString(content, encoding='utf-8', pretty=False):
  """ Writes the XML content to disk, touching the file only if it has changed.

  Visual Studio files have a lot of pre-defined structures.  This function makes
  it easy to represent these structures as Python data structures, instead of
  having to create a lot of function calls.

  Each XML element of the content is represented as a list composed of:
  1. The name of the element, a string,
  2. The attributes of the element, a dictionary (optional), and
  3+. The content of the element, if any.  Strings are simple text nodes and
      lists are child elements.

  Example 1:
      <test/>
  becomes
      ['test']

  Example 2:
      <myelement a='value1' b='value2'>
         <childtype>This is</childtype>
         <childtype>it!</childtype>
      </myelement>

  becomes
      ['myelement', {'a':'value1', 'b':'value2'},
         ['childtype', 'This is'],
         ['childtype', 'it!'],
      ]

  Args:
    content:  The structured content to be converted.
    encoding: The encoding to report on the first XML line.
    pretty: True if we want pretty printing with indents and new lines.

  Returns:
    The XML content as a string.
  """
  # We create a huge list of all the elements of the file.
  xml_parts = ['<?xml version="1.0" encoding="%s"?>' % encoding]
  if pretty:
    xml_parts.append('\n')
  _ConstructContentList(xml_parts, content, pretty)

  # Convert it to a string
  return ''.join(xml_parts)


def _ConstructContentList(xml_parts, specification, pretty, level=0):
  """ Appends the XML parts corresponding to the specification.

  Args:
    xml_parts: A list of XML parts to be appended to.
    specification:  The specification of the element.  See EasyXml docs.
    pretty: True if we want pretty printing with indents and new lines.
    level: Indentation level.
  """
  # The first item in a specification is the name of the element.
  if pretty:
    indentation = '  ' * level
    new_line = '\n'
  else:
    indentation = ''
    new_line = ''
  name = specification[0]
  if not isinstance(name, str):
    raise Exception('The first item of an EasyXml specification should be '
                    'a string.  Specification was ' + str(specification))
  xml_parts.append(indentation + '<' + name)

  # Optionally in second position is a dictionary of the attributes.
  rest = specification[1:]
  if rest and isinstance(rest[0], dict):
    for at, val in sorted(rest[0].items()):
      xml_parts.append(' %s="%s"' % (at, _XmlEscape(val, attr=True)))
    rest = rest[1:]
  if rest:
    xml_parts.append('>')
    all_strings = reduce(lambda x, y: x and isinstance(y, str), rest, True)
    multi_line = not all_strings
    if multi_line and new_line:
      xml_parts.append(new_line)
    for child_spec in rest:
      # If it's a string, append a text node.
      # Otherwise recurse over that child definition
      if isinstance(child_spec, str):
       xml_parts.append(_XmlEscape(child_spec))
      else:
        _ConstructContentList(xml_parts, child_spec, pretty, level + 1)
    if multi_line and indentation:
      xml_parts.append(indentation)
    xml_parts.append('</%s>%s' % (name, new_line))
  else:
    xml_parts.append('/>%s' % new_line)


def WriteXmlIfChanged(content, path, encoding='utf-8', pretty=False,
                      win32=False):
  """ Writes the XML content to disk, touching the file only if it has changed.

  Args:
    content:  The structured content to be written.
    path: Location of the file.
    encoding: The encoding to report on the first line of the XML file.
    pretty: True if we want pretty printing with indents and new lines.
  """
  xml_string = XmlToString(content, encoding, pretty)
  if win32 and os.linesep != '\r\n':
    xml_string = xml_string.replace('\n', '\r\n')

  default_encoding = locale.getdefaultlocale()[1]
  if default_encoding and default_encoding.upper() != encoding.upper():
    xml_string = xml_string.decode(default_encoding).encode(encoding)

  # Get the old content
  try:
    f = open(path, 'r')
    existing = f.read()
    f.close()
  except:
    existing = None

  # It has changed, write it
  if existing != xml_string:
    f = open(path, 'w')
    f.write(xml_string)
    f.close()


_xml_escape_map = {
    '"': '&quot;',
    "'": '&apos;',
    '<': '&lt;',
    '>': '&gt;',
    '&': '&amp;',
    '\n': '&#xA;',
    '\r': '&#xD;',
}


_xml_escape_re = re.compile(
    "(%s)" % "|".join(map(re.escape, _xml_escape_map.keys())))


def _XmlEscape(value, attr=False):
  """ Escape a string for inclusion in XML."""
  def replace(match):
    m = match.string[match.start() : match.end()]
    # don't replace single quotes in attrs
    if attr and m == "'":
      return m
    return _xml_escape_map[m]
  return _xml_escape_re.sub(replace, value)