Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / tools / grit / grit / node / base.py
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 '''Base types for nodes in a GRIT resource tree.
7 '''
8
9 import ast
10 import os
11 import types
12 from xml.sax import saxutils
13
14 from grit import clique
15 from grit import exception
16 from grit import util
17
18
19 class Node(object):
20   '''An item in the tree that has children.'''
21
22   # Valid content types that can be returned by _ContentType()
23   _CONTENT_TYPE_NONE = 0   # No CDATA content but may have children
24   _CONTENT_TYPE_CDATA = 1  # Only CDATA, no children.
25   _CONTENT_TYPE_MIXED = 2  # CDATA and children, possibly intermingled
26
27   # Default nodes to not whitelist skipped
28   _whitelist_marked_as_skip = False
29
30   # A class-static cache to speed up EvaluateExpression().
31   # Keys are expressions (e.g. 'is_ios and lang == "fr"'). Values are tuples
32   # (code, variables_in_expr) where code is the compiled expression and can be
33   # directly eval'd, and variables_in_expr is the list of variable and method
34   # names used in the expression (e.g. ['is_ios', 'lang']).
35   eval_expr_cache = {}
36
37   def __init__(self):
38     self.children = []        # A list of child elements
39     self.mixed_content = []   # A list of u'' and/or child elements (this
40                               # duplicates 'children' but
41                               # is needed to preserve markup-type content).
42     self.name = u''           # The name of this element
43     self.attrs = {}           # The set of attributes (keys to values)
44     self.parent = None        # Our parent unless we are the root element.
45     self.uberclique = None    # Allows overriding uberclique for parts of tree
46
47   # This context handler allows you to write "with node:" and get a
48   # line identifying the offending node if an exception escapes from the body
49   # of the with statement.
50   def __enter__(self):
51     return self
52
53   def __exit__(self, exc_type, exc_value, traceback):
54     if exc_type is not None:
55       print u'Error processing node %s' % unicode(self)
56
57   def __iter__(self):
58     '''A preorder iteration through the tree that this node is the root of.'''
59     return self.Preorder()
60
61   def Preorder(self):
62     '''Generator that generates first this node, then the same generator for
63     any child nodes.'''
64     yield self
65     for child in self.children:
66       for iterchild in child.Preorder():
67         yield iterchild
68
69   def ActiveChildren(self):
70     '''Returns the children of this node that should be included in the current
71     configuration. Overridden by <if>.'''
72     return [node for node in self.children if not node.WhitelistMarkedAsSkip()]
73
74   def ActiveDescendants(self):
75     '''Yields the current node and all descendants that should be included in
76     the current configuration, in preorder.'''
77     yield self
78     for child in self.ActiveChildren():
79       for descendant in child.ActiveDescendants():
80         yield descendant
81
82   def GetRoot(self):
83     '''Returns the root Node in the tree this Node belongs to.'''
84     curr = self
85     while curr.parent:
86       curr = curr.parent
87     return curr
88
89     # TODO(joi) Use this (currently untested) optimization?:
90     #if hasattr(self, '_root'):
91     #  return self._root
92     #curr = self
93     #while curr.parent and not hasattr(curr, '_root'):
94     #  curr = curr.parent
95     #if curr.parent:
96     #  self._root = curr._root
97     #else:
98     #  self._root = curr
99     #return self._root
100
101   def StartParsing(self, name, parent):
102     '''Called at the start of parsing.
103
104     Args:
105       name: u'elementname'
106       parent: grit.node.base.Node or subclass or None
107     '''
108     assert isinstance(name, types.StringTypes)
109     assert not parent or isinstance(parent, Node)
110     self.name = name
111     self.parent = parent
112
113   def AddChild(self, child):
114     '''Adds a child to the list of children of this node, if it is a valid
115     child for the node.'''
116     assert isinstance(child, Node)
117     if (not self._IsValidChild(child) or
118         self._ContentType() == self._CONTENT_TYPE_CDATA):
119       explanation = 'invalid child %s for parent %s' % (str(child), self.name)
120       raise exception.UnexpectedChild(explanation)
121     self.children.append(child)
122     self.mixed_content.append(child)
123
124   def RemoveChild(self, child_id):
125     '''Removes the first node that has a "name" attribute which
126     matches "child_id" in the list of immediate children of
127     this node.
128
129     Args:
130       child_id: String identifying the child to be removed
131     '''
132     index = 0
133     # Safe not to copy since we only remove the first element found
134     for child in self.children:
135       name_attr = child.attrs['name']
136       if name_attr == child_id:
137         self.children.pop(index)
138         self.mixed_content.pop(index)
139         break
140       index += 1
141
142   def AppendContent(self, content):
143     '''Appends a chunk of text as content of this node.
144
145     Args:
146       content: u'hello'
147
148     Return:
149       None
150     '''
151     assert isinstance(content, types.StringTypes)
152     if self._ContentType() != self._CONTENT_TYPE_NONE:
153       self.mixed_content.append(content)
154     elif content.strip() != '':
155       raise exception.UnexpectedContent()
156
157   def HandleAttribute(self, attrib, value):
158     '''Informs the node of an attribute that was parsed out of the GRD file
159     for it.
160
161     Args:
162       attrib: 'name'
163       value: 'fooblat'
164
165     Return:
166       None
167     '''
168     assert isinstance(attrib, types.StringTypes)
169     assert isinstance(value, types.StringTypes)
170     if self._IsValidAttribute(attrib, value):
171       self.attrs[attrib] = value
172     else:
173       raise exception.UnexpectedAttribute(attrib)
174
175   def EndParsing(self):
176     '''Called at the end of parsing.'''
177
178     # TODO(joi) Rewrite this, it's extremely ugly!
179     if len(self.mixed_content):
180       if isinstance(self.mixed_content[0], types.StringTypes):
181         # Remove leading and trailing chunks of pure whitespace.
182         while (len(self.mixed_content) and
183                isinstance(self.mixed_content[0], types.StringTypes) and
184                self.mixed_content[0].strip() == ''):
185           self.mixed_content = self.mixed_content[1:]
186         # Strip leading and trailing whitespace from mixed content chunks
187         # at front and back.
188         if (len(self.mixed_content) and
189             isinstance(self.mixed_content[0], types.StringTypes)):
190           self.mixed_content[0] = self.mixed_content[0].lstrip()
191         # Remove leading and trailing ''' (used to demarcate whitespace)
192         if (len(self.mixed_content) and
193             isinstance(self.mixed_content[0], types.StringTypes)):
194           if self.mixed_content[0].startswith("'''"):
195             self.mixed_content[0] = self.mixed_content[0][3:]
196     if len(self.mixed_content):
197       if isinstance(self.mixed_content[-1], types.StringTypes):
198         # Same stuff all over again for the tail end.
199         while (len(self.mixed_content) and
200                isinstance(self.mixed_content[-1], types.StringTypes) and
201                self.mixed_content[-1].strip() == ''):
202           self.mixed_content = self.mixed_content[:-1]
203         if (len(self.mixed_content) and
204             isinstance(self.mixed_content[-1], types.StringTypes)):
205           self.mixed_content[-1] = self.mixed_content[-1].rstrip()
206         if (len(self.mixed_content) and
207             isinstance(self.mixed_content[-1], types.StringTypes)):
208           if self.mixed_content[-1].endswith("'''"):
209             self.mixed_content[-1] = self.mixed_content[-1][:-3]
210
211     # Check that all mandatory attributes are there.
212     for node_mandatt in self.MandatoryAttributes():
213       mandatt_list = []
214       if node_mandatt.find('|') >= 0:
215         mandatt_list = node_mandatt.split('|')
216       else:
217         mandatt_list.append(node_mandatt)
218
219       mandatt_option_found = False
220       for mandatt in mandatt_list:
221         assert mandatt not in self.DefaultAttributes().keys()
222         if mandatt in self.attrs:
223           if not mandatt_option_found:
224             mandatt_option_found = True
225           else:
226             raise exception.MutuallyExclusiveMandatoryAttribute(mandatt)
227
228       if not mandatt_option_found:
229         raise exception.MissingMandatoryAttribute(mandatt)
230
231     # Add default attributes if not specified in input file.
232     for defattr in self.DefaultAttributes():
233       if not defattr in self.attrs:
234         self.attrs[defattr] = self.DefaultAttributes()[defattr]
235
236   def GetCdata(self):
237     '''Returns all CDATA of this element, concatenated into a single
238     string.  Note that this ignores any elements embedded in CDATA.'''
239     return ''.join([c for c in self.mixed_content
240                     if isinstance(c, types.StringTypes)])
241
242   def __unicode__(self):
243     '''Returns this node and all nodes below it as an XML document in a Unicode
244     string.'''
245     header = u'<?xml version="1.0" encoding="UTF-8"?>\n'
246     return header + self.FormatXml()
247
248   def FormatXml(self, indent = u'', one_line = False):
249     '''Returns this node and all nodes below it as an XML
250     element in a Unicode string.  This differs from __unicode__ in that it does
251     not include the <?xml> stuff at the top of the string.  If one_line is true,
252     children and CDATA are layed out in a way that preserves internal
253     whitespace.
254     '''
255     assert isinstance(indent, types.StringTypes)
256
257     content_one_line = (one_line or
258                         self._ContentType() == self._CONTENT_TYPE_MIXED)
259     inside_content = self.ContentsAsXml(indent, content_one_line)
260
261     # Then the attributes for this node.
262     attribs = u''
263     default_attribs = self.DefaultAttributes()
264     for attrib, value in sorted(self.attrs.items()):
265       # Only print an attribute if it is other than the default value.
266       if attrib not in default_attribs or value != default_attribs[attrib]:
267         attribs += u' %s=%s' % (attrib, saxutils.quoteattr(value))
268
269     # Finally build the XML for our node and return it
270     if len(inside_content) > 0:
271       if one_line:
272         return u'<%s%s>%s</%s>' % (self.name, attribs, inside_content, self.name)
273       elif content_one_line:
274         return u'%s<%s%s>\n%s  %s\n%s</%s>' % (
275           indent, self.name, attribs,
276           indent, inside_content,
277           indent, self.name)
278       else:
279         return u'%s<%s%s>\n%s\n%s</%s>' % (
280           indent, self.name, attribs,
281           inside_content,
282           indent, self.name)
283     else:
284       return u'%s<%s%s />' % (indent, self.name, attribs)
285
286   def ContentsAsXml(self, indent, one_line):
287     '''Returns the contents of this node (CDATA and child elements) in XML
288     format.  If 'one_line' is true, the content will be laid out on one line.'''
289     assert isinstance(indent, types.StringTypes)
290
291     # Build the contents of the element.
292     inside_parts = []
293     last_item = None
294     for mixed_item in self.mixed_content:
295       if isinstance(mixed_item, Node):
296         inside_parts.append(mixed_item.FormatXml(indent + u'  ', one_line))
297         if not one_line:
298           inside_parts.append(u'\n')
299       else:
300         message = mixed_item
301         # If this is the first item and it starts with whitespace, we add
302         # the ''' delimiter.
303         if not last_item and message.lstrip() != message:
304           message = u"'''" + message
305         inside_parts.append(util.EncodeCdata(message))
306       last_item = mixed_item
307
308     # If there are only child nodes and no cdata, there will be a spurious
309     # trailing \n
310     if len(inside_parts) and inside_parts[-1] == '\n':
311       inside_parts = inside_parts[:-1]
312
313     # If the last item is a string (not a node) and ends with whitespace,
314     # we need to add the ''' delimiter.
315     if (isinstance(last_item, types.StringTypes) and
316         last_item.rstrip() != last_item):
317       inside_parts[-1] = inside_parts[-1] + u"'''"
318
319     return u''.join(inside_parts)
320
321   def SubstituteMessages(self, substituter):
322     '''Applies substitutions to all messages in the tree.
323
324     Called as a final step of RunGatherers.
325
326     Args:
327       substituter: a grit.util.Substituter object.
328     '''
329     for child in self.children:
330       child.SubstituteMessages(substituter)
331
332   def _IsValidChild(self, child):
333     '''Returns true if 'child' is a valid child of this node.
334     Overridden by subclasses.'''
335     return False
336
337   def _IsValidAttribute(self, name, value):
338     '''Returns true if 'name' is the name of a valid attribute of this element
339     and 'value' is a valid value for that attribute.  Overriden by
340     subclasses unless they have only mandatory attributes.'''
341     return (name in self.MandatoryAttributes() or
342             name in self.DefaultAttributes())
343
344   def _ContentType(self):
345     '''Returns the type of content this element can have.  Overridden by
346     subclasses.  The content type can be one of the _CONTENT_TYPE_XXX constants
347     above.'''
348     return self._CONTENT_TYPE_NONE
349
350   def MandatoryAttributes(self):
351     '''Returns a list of attribute names that are mandatory (non-optional)
352     on the current element. One can specify a list of
353     "mutually exclusive mandatory" attributes by specifying them as one
354     element in the list, separated by a "|" character.
355     '''
356     return []
357
358   def DefaultAttributes(self):
359     '''Returns a dictionary of attribute names that have defaults, mapped to
360     the default value.  Overridden by subclasses.'''
361     return {}
362
363   def GetCliques(self):
364     '''Returns all MessageClique objects belonging to this node.  Overridden
365     by subclasses.
366
367     Return:
368       [clique1, clique2] or []
369     '''
370     return []
371
372   def ToRealPath(self, path_from_basedir):
373     '''Returns a real path (which can be absolute or relative to the current
374     working directory), given a path that is relative to the base directory
375     set for the GRIT input file.
376
377     Args:
378       path_from_basedir: '..'
379
380     Return:
381       'resource'
382     '''
383     return util.normpath(os.path.join(self.GetRoot().GetBaseDir(),
384                                       os.path.expandvars(path_from_basedir)))
385
386   def GetInputPath(self):
387     '''Returns a path, relative to the base directory set for the grd file,
388     that points to the file the node refers to.
389     '''
390     # This implementation works for most nodes that have an input file.
391     return self.attrs['file']
392
393   def UberClique(self):
394     '''Returns the uberclique that should be used for messages originating in
395     a given node.  If the node itself has its uberclique set, that is what we
396     use, otherwise we search upwards until we find one.  If we do not find one
397     even at the root node, we set the root node's uberclique to a new
398     uberclique instance.
399     '''
400     node = self
401     while not node.uberclique and node.parent:
402       node = node.parent
403     if not node.uberclique:
404       node.uberclique = clique.UberClique()
405     return node.uberclique
406
407   def IsTranslateable(self):
408     '''Returns false if the node has contents that should not be translated,
409     otherwise returns false (even if the node has no contents).
410     '''
411     if not 'translateable' in self.attrs:
412       return True
413     else:
414       return self.attrs['translateable'] == 'true'
415
416   def GetNodeById(self, id):
417     '''Returns the node in the subtree parented by this node that has a 'name'
418     attribute matching 'id'.  Returns None if no such node is found.
419     '''
420     for node in self:
421       if 'name' in node.attrs and node.attrs['name'] == id:
422         return node
423     return None
424
425   def GetChildrenOfType(self, type):
426     '''Returns a list of all subnodes (recursing to all leaves) of this node
427     that are of the indicated type (or tuple of types).
428
429     Args:
430       type: A type you could use with isinstance().
431
432     Return:
433       A list, possibly empty.
434     '''
435     return [child for child in self if isinstance(child, type)]
436
437   def GetTextualIds(self):
438     '''Returns a list of the textual ids of this node.
439     '''
440     if 'name' in self.attrs:
441       return [self.attrs['name']]
442     return []
443
444   @classmethod
445   def EvaluateExpression(cls, expr, defs, target_platform, extra_variables={}):
446     '''Worker for EvaluateCondition (below) and conditions in XTB files.'''
447     if expr in cls.eval_expr_cache:
448       code, variables_in_expr = cls.eval_expr_cache[expr]
449     else:
450       # Get a list of all variable and method names used in the expression.
451       syntax_tree = ast.parse(expr, mode='eval')
452       variables_in_expr = [node.id for node in ast.walk(syntax_tree) if
453           isinstance(node, ast.Name) and node.id not in ('True', 'False')]
454       code = compile(syntax_tree, filename='<string>', mode='eval')
455       cls.eval_expr_cache[expr] = code, variables_in_expr
456
457     # Set values only for variables that are needed to eval the expression.
458     variable_map = {}
459     for name in variables_in_expr:
460       if name == 'os':
461         value = target_platform
462       elif name == 'defs':
463         value = defs
464
465       elif name == 'is_linux':
466         value = target_platform.startswith('linux')
467       elif name == 'is_macosx':
468         value = target_platform == 'darwin'
469       elif name == 'is_win':
470         value = target_platform in ('cygwin', 'win32')
471       elif name == 'is_android':
472         value = target_platform == 'android'
473       elif name == 'is_ios':
474         value = target_platform == 'ios'
475       elif name == 'is_bsd':
476         value = 'bsd' in target_platform
477       elif name == 'is_posix':
478         value = (target_platform in ('darwin', 'linux2', 'linux3', 'sunos5',
479                                      'android', 'ios')
480                  or 'bsd' in target_platform)
481
482       elif name == 'pp_ifdef':
483         def pp_ifdef(symbol):
484           return symbol in defs
485         value = pp_ifdef
486       elif name == 'pp_if':
487         def pp_if(symbol):
488           return defs.get(symbol, False)
489         value = pp_if
490
491       elif name in defs:
492         value = defs[name]
493       elif name in extra_variables:
494         value = extra_variables[name]
495       else:
496         # Undefined variables default to False.
497         value = False
498
499       variable_map[name] = value
500
501     eval_result = eval(code, {}, variable_map)
502     assert isinstance(eval_result, bool)
503     return eval_result
504
505   def EvaluateCondition(self, expr):
506     '''Returns true if and only if the Python expression 'expr' evaluates
507     to true.
508
509     The expression is given a few local variables:
510       - 'lang' is the language currently being output
511            (the 'lang' attribute of the <output> element).
512       - 'context' is the current output context
513            (the 'context' attribute of the <output> element).
514       - 'defs' is a map of C preprocessor-style symbol names to their values.
515       - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin').
516       - 'pp_ifdef(symbol)' is a shorthand for "symbol in defs".
517       - 'pp_if(symbol)' is a shorthand for "symbol in defs and defs[symbol]".
518       - 'is_linux', 'is_macosx', 'is_win', 'is_posix' are true if 'os'
519            matches the given platform.
520     '''
521     root = self.GetRoot()
522     lang = getattr(root, 'output_language', '')
523     context = getattr(root, 'output_context', '')
524     defs = getattr(root, 'defines', {})
525     target_platform = getattr(root, 'target_platform', '')
526     extra_variables = {
527         'lang': lang,
528         'context': context,
529     }
530     return Node.EvaluateExpression(
531         expr, defs, target_platform, extra_variables)
532
533   def OnlyTheseTranslations(self, languages):
534     '''Turns off loading of translations for languages not in the provided list.
535
536     Attrs:
537       languages: ['fr', 'zh_cn']
538     '''
539     for node in self:
540       if (hasattr(node, 'IsTranslation') and
541           node.IsTranslation() and
542           node.GetLang() not in languages):
543         node.DisableLoading()
544
545   def FindBooleanAttribute(self, attr, default, skip_self):
546     '''Searches all ancestors of the current node for the nearest enclosing
547     definition of the given boolean attribute.
548
549     Args:
550       attr: 'fallback_to_english'
551       default: What to return if no node defines the attribute.
552       skip_self: Don't check the current node, only its parents.
553     '''
554     p = self.parent if skip_self else self
555     while p:
556       value = p.attrs.get(attr, 'default').lower()
557       if value != 'default':
558         return (value == 'true')
559       p = p.parent
560     return default
561
562   def PseudoIsAllowed(self):
563     '''Returns true if this node is allowed to use pseudo-translations.  This
564     is true by default, unless this node is within a <release> node that has
565     the allow_pseudo attribute set to false.
566     '''
567     return self.FindBooleanAttribute('allow_pseudo',
568                                      default=True, skip_self=True)
569
570   def ShouldFallbackToEnglish(self):
571     '''Returns true iff this node should fall back to English when
572     pseudotranslations are disabled and no translation is available for a
573     given message.
574     '''
575     return self.FindBooleanAttribute('fallback_to_english',
576                                      default=False, skip_self=True)
577
578   def WhitelistMarkedAsSkip(self):
579     '''Returns true if the node is marked to be skipped in the output by a
580     whitelist.
581     '''
582     return self._whitelist_marked_as_skip
583
584   def SetWhitelistMarkedAsSkip(self, mark_skipped):
585     '''Sets WhitelistMarkedAsSkip.
586     '''
587     self._whitelist_marked_as_skip = mark_skipped
588
589   def ExpandVariables(self):
590     '''Whether we need to expand variables on a given node.'''
591     return False
592
593
594 class ContentNode(Node):
595   '''Convenience baseclass for nodes that can have content.'''
596   def _ContentType(self):
597     return self._CONTENT_TYPE_MIXED
598