1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6'''Class for reading GRD files into memory, without processing them.
7'''
8
9import os.path
10import types
11import xml.sax
12import xml.sax.handler
13
14from grit import exception
15from grit import util
16from grit.node import base
17from grit.node import mapping
18from grit.node import misc
19
20
21class StopParsingException(Exception):
22  '''An exception used to stop parsing.'''
23  pass
24
25
26class GrdContentHandler(xml.sax.handler.ContentHandler):
27  def __init__(self, stop_after, debug, dir, defines, tags_to_ignore,
28               target_platform):
29    # Invariant of data:
30    # 'root' is the root of the parse tree being created, or None if we haven't
31    # parsed out any elements.
32    # 'stack' is the a stack of elements that we push new nodes onto and
33    # pop from when they finish parsing, or [] if we are not currently parsing.
34    # 'stack[-1]' is the top of the stack.
35    self.root = None
36    self.stack = []
37    self.stop_after = stop_after
38    self.debug = debug
39    self.dir = dir
40    self.defines = defines
41    self.tags_to_ignore = tags_to_ignore or set()
42    self.ignore_depth = 0
43    self.target_platform = target_platform
44
45  def startElement(self, name, attrs):
46    if self.ignore_depth or name in self.tags_to_ignore:
47      if self.debug and self.ignore_depth == 0:
48        print "Ignoring element %s and its children" % name
49      self.ignore_depth += 1
50      return
51
52    if self.debug:
53      attr_list = ' '.join('%s="%s"' % kv for kv in attrs.items())
54      print ("Starting parsing of element %s with attributes %r" %
55          (name, attr_list or '(none)'))
56
57    typeattr = attrs.get('type')
58    node = mapping.ElementToClass(name, typeattr)()
59
60    if self.stack:
61      self.stack[-1].AddChild(node)
62      node.StartParsing(name, self.stack[-1])
63    else:
64      assert self.root is None
65      self.root = node
66      if isinstance(self.root, misc.GritNode):
67        if self.target_platform:
68          self.root.SetTargetPlatform(self.target_platform)
69      node.StartParsing(name, None)
70      if self.defines:
71        node.SetDefines(self.defines)
72    self.stack.append(node)
73
74    for attr, attrval in attrs.items():
75      node.HandleAttribute(attr, attrval)
76
77  def endElement(self, name):
78    if self.ignore_depth:
79      self.ignore_depth -= 1
80      return
81
82    if name == 'part':
83      partnode = self.stack[-1]
84      partnode.started_inclusion = True
85      # Add the contents of the sub-grd file as children of the <part> node.
86      partname = partnode.GetInputPath()
87      if os.path.dirname(partname):
88        # TODO(benrg): Remove this limitation. (The problem is that GRIT
89        # assumes that files referenced from the GRD file are relative to
90        # a path stored in the root <grit> node.)
91        raise exception.GotPathExpectedFilenameOnly()
92      partname = os.path.join(self.dir, partname)
93      # Exceptions propagate to the handler in grd_reader.Parse().
94      xml.sax.parse(partname, GrdPartContentHandler(self))
95
96    if self.debug:
97      print "End parsing of element %s" % name
98    self.stack.pop().EndParsing()
99
100    if name == self.stop_after:
101      raise StopParsingException()
102
103  def characters(self, content):
104    if self.ignore_depth == 0:
105      if self.stack[-1]:
106        self.stack[-1].AppendContent(content)
107
108  def ignorableWhitespace(self, whitespace):
109    # TODO(joi)  This is not supported by expat.  Should use a different XML parser?
110    pass
111
112
113class GrdPartContentHandler(xml.sax.handler.ContentHandler):
114  def __init__(self, parent):
115    self.parent = parent
116    self.depth = 0
117
118  def startElement(self, name, attrs):
119    if self.depth:
120      self.parent.startElement(name, attrs)
121    else:
122      if name != 'grit-part':
123        raise exception.MissingElement("root tag must be <grit-part>")
124      if attrs:
125        raise exception.UnexpectedAttribute(
126            "<grit-part> tag must not have attributes")
127    self.depth += 1
128
129  def endElement(self, name):
130    self.depth -= 1
131    if self.depth:
132      self.parent.endElement(name)
133
134  def characters(self, content):
135    self.parent.characters(content)
136
137  def ignorableWhitespace(self, whitespace):
138    self.parent.ignorableWhitespace(whitespace)
139
140
141def Parse(filename_or_stream, dir=None, stop_after=None, first_ids_file=None,
142          debug=False, defines=None, tags_to_ignore=None, target_platform=None):
143  '''Parses a GRD file into a tree of nodes (from grit.node).
144
145  If filename_or_stream is a stream, 'dir' should point to the directory
146  notionally containing the stream (this feature is only used in unit tests).
147
148  If 'stop_after' is provided, the parsing will stop once the first node
149  with this name has been fully parsed (including all its contents).
150
151  If 'debug' is true, lots of information about the parsing events will be
152  printed out during parsing of the file.
153
154  If 'first_ids_file' is non-empty, it is used to override the setting for the
155  first_ids_file attribute of the <grit> root node. Note that the first_ids_file
156  parameter should be relative to the cwd, even though the first_ids_file
157  attribute of the <grit> node is relative to the grd file.
158
159  If 'target_platform' is set, this is used to determine the target
160  platform of builds, instead of using |sys.platform|.
161
162  Args:
163    filename_or_stream: './bla.xml'
164    dir: None (if filename_or_stream is a filename) or '.'
165    stop_after: 'inputs'
166    first_ids_file: 'GRIT_DIR/../gritsettings/resource_ids'
167    debug: False
168    defines: dictionary of defines, like {'chromeos': '1'}
169    target_platform: None or the value that would be returned by sys.platform
170        on your target platform.
171
172  Return:
173    Subclass of grit.node.base.Node
174
175  Throws:
176    grit.exception.Parsing
177  '''
178
179  if dir is None and isinstance(filename_or_stream, types.StringType):
180    dir = util.dirname(filename_or_stream)
181
182  handler = GrdContentHandler(stop_after=stop_after, debug=debug, dir=dir,
183                              defines=defines, tags_to_ignore=tags_to_ignore,
184                              target_platform=target_platform)
185  try:
186    xml.sax.parse(filename_or_stream, handler)
187  except StopParsingException:
188    assert stop_after
189    pass
190  except:
191    if not debug:
192      print "parse exception: run GRIT with the -x flag to debug .grd problems"
193    raise
194
195  if handler.root.name != 'grit':
196    raise exception.MissingElement("root tag must be <grit>")
197
198  if hasattr(handler.root, 'SetOwnDir'):
199    # Fix up the base_dir so it is relative to the input file.
200    assert dir is not None
201    handler.root.SetOwnDir(dir)
202
203  if isinstance(handler.root, misc.GritNode):
204    if first_ids_file:
205      # Make the path to the first_ids_file relative to the grd file,
206      # unless it begins with GRIT_DIR.
207      GRIT_DIR_PREFIX = 'GRIT_DIR'
208      if not (first_ids_file.startswith(GRIT_DIR_PREFIX)
209          and first_ids_file[len(GRIT_DIR_PREFIX)] in ['/', '\\']):
210        rel_dir = os.path.relpath(os.getcwd(), dir)
211        first_ids_file = util.normpath(os.path.join(rel_dir, first_ids_file))
212      handler.root.attrs['first_ids_file'] = first_ids_file
213    # Assign first ids to the nodes that don't have them.
214    handler.root.AssignFirstIds(filename_or_stream, defines)
215
216  return handler.root
217
218
219if __name__ == '__main__':
220  util.ChangeStdoutEncoding()
221  print unicode(Parse(sys.argv[1]))
222