Index: grit/node/base.py |
=================================================================== |
--- grit/node/base.py (revision 202) |
+++ grit/node/base.py (working copy) |
@@ -1,613 +0,0 @@ |
-#!/usr/bin/env python |
-# Copyright (c) 2012 The Chromium Authors. All rights reserved. |
-# Use of this source code is governed by a BSD-style license that can be |
-# found in the LICENSE file. |
- |
-'''Base types for nodes in a GRIT resource tree. |
-''' |
- |
-import ast |
-import os |
-import types |
-from xml.sax import saxutils |
- |
-from grit import clique |
-from grit import exception |
-from grit import util |
- |
- |
-class Node(object): |
- '''An item in the tree that has children.''' |
- |
- # Valid content types that can be returned by _ContentType() |
- _CONTENT_TYPE_NONE = 0 # No CDATA content but may have children |
- _CONTENT_TYPE_CDATA = 1 # Only CDATA, no children. |
- _CONTENT_TYPE_MIXED = 2 # CDATA and children, possibly intermingled |
- |
- # Default nodes to not whitelist skipped |
- _whitelist_marked_as_skip = False |
- |
- # A class-static cache to speed up EvaluateExpression(). |
- # Keys are expressions (e.g. 'is_ios and lang == "fr"'). Values are tuples |
- # (code, variables_in_expr) where code is the compiled expression and can be |
- # directly eval'd, and variables_in_expr is the list of variable and method |
- # names used in the expression (e.g. ['is_ios', 'lang']). |
- eval_expr_cache = {} |
- |
- def __init__(self): |
- self.children = [] # A list of child elements |
- self.mixed_content = [] # A list of u'' and/or child elements (this |
- # duplicates 'children' but |
- # is needed to preserve markup-type content). |
- self.name = u'' # The name of this element |
- self.attrs = {} # The set of attributes (keys to values) |
- self.parent = None # Our parent unless we are the root element. |
- self.uberclique = None # Allows overriding uberclique for parts of tree |
- |
- # This context handler allows you to write "with node:" and get a |
- # line identifying the offending node if an exception escapes from the body |
- # of the with statement. |
- def __enter__(self): |
- return self |
- |
- def __exit__(self, exc_type, exc_value, traceback): |
- if exc_type is not None: |
- print u'Error processing node %s' % unicode(self) |
- |
- def __iter__(self): |
- '''A preorder iteration through the tree that this node is the root of.''' |
- return self.Preorder() |
- |
- def Preorder(self): |
- '''Generator that generates first this node, then the same generator for |
- any child nodes.''' |
- yield self |
- for child in self.children: |
- for iterchild in child.Preorder(): |
- yield iterchild |
- |
- def ActiveChildren(self): |
- '''Returns the children of this node that should be included in the current |
- configuration. Overridden by <if>.''' |
- return [node for node in self.children if not node.WhitelistMarkedAsSkip()] |
- |
- def ActiveDescendants(self): |
- '''Yields the current node and all descendants that should be included in |
- the current configuration, in preorder.''' |
- yield self |
- for child in self.ActiveChildren(): |
- for descendant in child.ActiveDescendants(): |
- yield descendant |
- |
- def GetRoot(self): |
- '''Returns the root Node in the tree this Node belongs to.''' |
- curr = self |
- while curr.parent: |
- curr = curr.parent |
- return curr |
- |
- # TODO(joi) Use this (currently untested) optimization?: |
- #if hasattr(self, '_root'): |
- # return self._root |
- #curr = self |
- #while curr.parent and not hasattr(curr, '_root'): |
- # curr = curr.parent |
- #if curr.parent: |
- # self._root = curr._root |
- #else: |
- # self._root = curr |
- #return self._root |
- |
- def StartParsing(self, name, parent): |
- '''Called at the start of parsing. |
- |
- Args: |
- name: u'elementname' |
- parent: grit.node.base.Node or subclass or None |
- ''' |
- assert isinstance(name, types.StringTypes) |
- assert not parent or isinstance(parent, Node) |
- self.name = name |
- self.parent = parent |
- |
- def AddChild(self, child): |
- '''Adds a child to the list of children of this node, if it is a valid |
- child for the node.''' |
- assert isinstance(child, Node) |
- if (not self._IsValidChild(child) or |
- self._ContentType() == self._CONTENT_TYPE_CDATA): |
- explanation = 'invalid child %s for parent %s' % (str(child), self.name) |
- raise exception.UnexpectedChild(explanation) |
- self.children.append(child) |
- self.mixed_content.append(child) |
- |
- def RemoveChild(self, child_id): |
- '''Removes the first node that has a "name" attribute which |
- matches "child_id" in the list of immediate children of |
- this node. |
- |
- Args: |
- child_id: String identifying the child to be removed |
- ''' |
- index = 0 |
- # Safe not to copy since we only remove the first element found |
- for child in self.children: |
- name_attr = child.attrs['name'] |
- if name_attr == child_id: |
- self.children.pop(index) |
- self.mixed_content.pop(index) |
- break |
- index += 1 |
- |
- def AppendContent(self, content): |
- '''Appends a chunk of text as content of this node. |
- |
- Args: |
- content: u'hello' |
- |
- Return: |
- None |
- ''' |
- assert isinstance(content, types.StringTypes) |
- if self._ContentType() != self._CONTENT_TYPE_NONE: |
- self.mixed_content.append(content) |
- elif content.strip() != '': |
- raise exception.UnexpectedContent() |
- |
- def HandleAttribute(self, attrib, value): |
- '''Informs the node of an attribute that was parsed out of the GRD file |
- for it. |
- |
- Args: |
- attrib: 'name' |
- value: 'fooblat' |
- |
- Return: |
- None |
- ''' |
- assert isinstance(attrib, types.StringTypes) |
- assert isinstance(value, types.StringTypes) |
- if self._IsValidAttribute(attrib, value): |
- self.attrs[attrib] = value |
- else: |
- raise exception.UnexpectedAttribute(attrib) |
- |
- def EndParsing(self): |
- '''Called at the end of parsing.''' |
- |
- # TODO(joi) Rewrite this, it's extremely ugly! |
- if len(self.mixed_content): |
- if isinstance(self.mixed_content[0], types.StringTypes): |
- # Remove leading and trailing chunks of pure whitespace. |
- while (len(self.mixed_content) and |
- isinstance(self.mixed_content[0], types.StringTypes) and |
- self.mixed_content[0].strip() == ''): |
- self.mixed_content = self.mixed_content[1:] |
- # Strip leading and trailing whitespace from mixed content chunks |
- # at front and back. |
- if (len(self.mixed_content) and |
- isinstance(self.mixed_content[0], types.StringTypes)): |
- self.mixed_content[0] = self.mixed_content[0].lstrip() |
- # Remove leading and trailing ''' (used to demarcate whitespace) |
- if (len(self.mixed_content) and |
- isinstance(self.mixed_content[0], types.StringTypes)): |
- if self.mixed_content[0].startswith("'''"): |
- self.mixed_content[0] = self.mixed_content[0][3:] |
- if len(self.mixed_content): |
- if isinstance(self.mixed_content[-1], types.StringTypes): |
- # Same stuff all over again for the tail end. |
- while (len(self.mixed_content) and |
- isinstance(self.mixed_content[-1], types.StringTypes) and |
- self.mixed_content[-1].strip() == ''): |
- self.mixed_content = self.mixed_content[:-1] |
- if (len(self.mixed_content) and |
- isinstance(self.mixed_content[-1], types.StringTypes)): |
- self.mixed_content[-1] = self.mixed_content[-1].rstrip() |
- if (len(self.mixed_content) and |
- isinstance(self.mixed_content[-1], types.StringTypes)): |
- if self.mixed_content[-1].endswith("'''"): |
- self.mixed_content[-1] = self.mixed_content[-1][:-3] |
- |
- # Check that all mandatory attributes are there. |
- for node_mandatt in self.MandatoryAttributes(): |
- mandatt_list = [] |
- if node_mandatt.find('|') >= 0: |
- mandatt_list = node_mandatt.split('|') |
- else: |
- mandatt_list.append(node_mandatt) |
- |
- mandatt_option_found = False |
- for mandatt in mandatt_list: |
- assert mandatt not in self.DefaultAttributes().keys() |
- if mandatt in self.attrs: |
- if not mandatt_option_found: |
- mandatt_option_found = True |
- else: |
- raise exception.MutuallyExclusiveMandatoryAttribute(mandatt) |
- |
- if not mandatt_option_found: |
- raise exception.MissingMandatoryAttribute(mandatt) |
- |
- # Add default attributes if not specified in input file. |
- for defattr in self.DefaultAttributes(): |
- if not defattr in self.attrs: |
- self.attrs[defattr] = self.DefaultAttributes()[defattr] |
- |
- def GetCdata(self): |
- '''Returns all CDATA of this element, concatenated into a single |
- string. Note that this ignores any elements embedded in CDATA.''' |
- return ''.join([c for c in self.mixed_content |
- if isinstance(c, types.StringTypes)]) |
- |
- def __unicode__(self): |
- '''Returns this node and all nodes below it as an XML document in a Unicode |
- string.''' |
- header = u'<?xml version="1.0" encoding="UTF-8"?>\n' |
- return header + self.FormatXml() |
- |
- def FormatXml(self, indent = u'', one_line = False): |
- '''Returns this node and all nodes below it as an XML |
- element in a Unicode string. This differs from __unicode__ in that it does |
- not include the <?xml> stuff at the top of the string. If one_line is true, |
- children and CDATA are layed out in a way that preserves internal |
- whitespace. |
- ''' |
- assert isinstance(indent, types.StringTypes) |
- |
- content_one_line = (one_line or |
- self._ContentType() == self._CONTENT_TYPE_MIXED) |
- inside_content = self.ContentsAsXml(indent, content_one_line) |
- |
- # Then the attributes for this node. |
- attribs = u'' |
- default_attribs = self.DefaultAttributes() |
- for attrib, value in sorted(self.attrs.items()): |
- # Only print an attribute if it is other than the default value. |
- if attrib not in default_attribs or value != default_attribs[attrib]: |
- attribs += u' %s=%s' % (attrib, saxutils.quoteattr(value)) |
- |
- # Finally build the XML for our node and return it |
- if len(inside_content) > 0: |
- if one_line: |
- return u'<%s%s>%s</%s>' % (self.name, attribs, inside_content, self.name) |
- elif content_one_line: |
- return u'%s<%s%s>\n%s %s\n%s</%s>' % ( |
- indent, self.name, attribs, |
- indent, inside_content, |
- indent, self.name) |
- else: |
- return u'%s<%s%s>\n%s\n%s</%s>' % ( |
- indent, self.name, attribs, |
- inside_content, |
- indent, self.name) |
- else: |
- return u'%s<%s%s />' % (indent, self.name, attribs) |
- |
- def ContentsAsXml(self, indent, one_line): |
- '''Returns the contents of this node (CDATA and child elements) in XML |
- format. If 'one_line' is true, the content will be laid out on one line.''' |
- assert isinstance(indent, types.StringTypes) |
- |
- # Build the contents of the element. |
- inside_parts = [] |
- last_item = None |
- for mixed_item in self.mixed_content: |
- if isinstance(mixed_item, Node): |
- inside_parts.append(mixed_item.FormatXml(indent + u' ', one_line)) |
- if not one_line: |
- inside_parts.append(u'\n') |
- else: |
- message = mixed_item |
- # If this is the first item and it starts with whitespace, we add |
- # the ''' delimiter. |
- if not last_item and message.lstrip() != message: |
- message = u"'''" + message |
- inside_parts.append(util.EncodeCdata(message)) |
- last_item = mixed_item |
- |
- # If there are only child nodes and no cdata, there will be a spurious |
- # trailing \n |
- if len(inside_parts) and inside_parts[-1] == '\n': |
- inside_parts = inside_parts[:-1] |
- |
- # If the last item is a string (not a node) and ends with whitespace, |
- # we need to add the ''' delimiter. |
- if (isinstance(last_item, types.StringTypes) and |
- last_item.rstrip() != last_item): |
- inside_parts[-1] = inside_parts[-1] + u"'''" |
- |
- return u''.join(inside_parts) |
- |
- def SubstituteMessages(self, substituter): |
- '''Applies substitutions to all messages in the tree. |
- |
- Called as a final step of RunGatherers. |
- |
- Args: |
- substituter: a grit.util.Substituter object. |
- ''' |
- for child in self.children: |
- child.SubstituteMessages(substituter) |
- |
- def _IsValidChild(self, child): |
- '''Returns true if 'child' is a valid child of this node. |
- Overridden by subclasses.''' |
- return False |
- |
- def _IsValidAttribute(self, name, value): |
- '''Returns true if 'name' is the name of a valid attribute of this element |
- and 'value' is a valid value for that attribute. Overriden by |
- subclasses unless they have only mandatory attributes.''' |
- return (name in self.MandatoryAttributes() or |
- name in self.DefaultAttributes()) |
- |
- def _ContentType(self): |
- '''Returns the type of content this element can have. Overridden by |
- subclasses. The content type can be one of the _CONTENT_TYPE_XXX constants |
- above.''' |
- return self._CONTENT_TYPE_NONE |
- |
- def MandatoryAttributes(self): |
- '''Returns a list of attribute names that are mandatory (non-optional) |
- on the current element. One can specify a list of |
- "mutually exclusive mandatory" attributes by specifying them as one |
- element in the list, separated by a "|" character. |
- ''' |
- return [] |
- |
- def DefaultAttributes(self): |
- '''Returns a dictionary of attribute names that have defaults, mapped to |
- the default value. Overridden by subclasses.''' |
- return {} |
- |
- def GetCliques(self): |
- '''Returns all MessageClique objects belonging to this node. Overridden |
- by subclasses. |
- |
- Return: |
- [clique1, clique2] or [] |
- ''' |
- return [] |
- |
- def ToRealPath(self, path_from_basedir): |
- '''Returns a real path (which can be absolute or relative to the current |
- working directory), given a path that is relative to the base directory |
- set for the GRIT input file. |
- |
- Args: |
- path_from_basedir: '..' |
- |
- Return: |
- 'resource' |
- ''' |
- return util.normpath(os.path.join(self.GetRoot().GetBaseDir(), |
- os.path.expandvars(path_from_basedir))) |
- |
- def GetInputPath(self): |
- '''Returns a path, relative to the base directory set for the grd file, |
- that points to the file the node refers to. |
- ''' |
- # This implementation works for most nodes that have an input file. |
- return self.attrs['file'] |
- |
- def UberClique(self): |
- '''Returns the uberclique that should be used for messages originating in |
- a given node. If the node itself has its uberclique set, that is what we |
- use, otherwise we search upwards until we find one. If we do not find one |
- even at the root node, we set the root node's uberclique to a new |
- uberclique instance. |
- ''' |
- node = self |
- while not node.uberclique and node.parent: |
- node = node.parent |
- if not node.uberclique: |
- node.uberclique = clique.UberClique() |
- return node.uberclique |
- |
- def IsTranslateable(self): |
- '''Returns false if the node has contents that should not be translated, |
- otherwise returns false (even if the node has no contents). |
- ''' |
- if not 'translateable' in self.attrs: |
- return True |
- else: |
- return self.attrs['translateable'] == 'true' |
- |
- def GetNodeById(self, id): |
- '''Returns the node in the subtree parented by this node that has a 'name' |
- attribute matching 'id'. Returns None if no such node is found. |
- ''' |
- for node in self: |
- if 'name' in node.attrs and node.attrs['name'] == id: |
- return node |
- return None |
- |
- def GetChildrenOfType(self, type): |
- '''Returns a list of all subnodes (recursing to all leaves) of this node |
- that are of the indicated type (or tuple of types). |
- |
- Args: |
- type: A type you could use with isinstance(). |
- |
- Return: |
- A list, possibly empty. |
- ''' |
- return [child for child in self if isinstance(child, type)] |
- |
- def GetTextualIds(self): |
- '''Returns a list of the textual ids of this node. |
- ''' |
- if 'name' in self.attrs: |
- return [self.attrs['name']] |
- return [] |
- |
- @classmethod |
- def EvaluateExpression(cls, expr, defs, target_platform, extra_variables={}): |
- '''Worker for EvaluateCondition (below) and conditions in XTB files.''' |
- if expr in cls.eval_expr_cache: |
- code, variables_in_expr = cls.eval_expr_cache[expr] |
- else: |
- # Get a list of all variable and method names used in the expression. |
- syntax_tree = ast.parse(expr, mode='eval') |
- variables_in_expr = [node.id for node in ast.walk(syntax_tree) if |
- isinstance(node, ast.Name) and node.id not in ('True', 'False')] |
- code = compile(syntax_tree, filename='<string>', mode='eval') |
- cls.eval_expr_cache[expr] = code, variables_in_expr |
- |
- # Set values only for variables that are needed to eval the expression. |
- variable_map = {} |
- for name in variables_in_expr: |
- if name == 'os': |
- value = target_platform |
- elif name == 'defs': |
- value = defs |
- |
- elif name == 'is_linux': |
- value = target_platform.startswith('linux') |
- elif name == 'is_macosx': |
- value = target_platform == 'darwin' |
- elif name == 'is_win': |
- value = target_platform in ('cygwin', 'win32') |
- elif name == 'is_android': |
- value = target_platform == 'android' |
- elif name == 'is_ios': |
- value = target_platform == 'ios' |
- elif name == 'is_bsd': |
- value = 'bsd' in target_platform |
- elif name == 'is_posix': |
- value = (target_platform in ('darwin', 'linux2', 'linux3', 'sunos5', |
- 'android', 'ios') |
- or 'bsd' in target_platform) |
- |
- elif name == 'pp_ifdef': |
- def pp_ifdef(symbol): |
- return symbol in defs |
- value = pp_ifdef |
- elif name == 'pp_if': |
- def pp_if(symbol): |
- return defs.get(symbol, False) |
- value = pp_if |
- |
- elif name in defs: |
- value = defs[name] |
- elif name in extra_variables: |
- value = extra_variables[name] |
- else: |
- # Undefined variables default to False. |
- value = False |
- |
- variable_map[name] = value |
- |
- eval_result = eval(code, {}, variable_map) |
- assert isinstance(eval_result, bool) |
- return eval_result |
- |
- def EvaluateCondition(self, expr): |
- '''Returns true if and only if the Python expression 'expr' evaluates |
- to true. |
- |
- The expression is given a few local variables: |
- - 'lang' is the language currently being output |
- (the 'lang' attribute of the <output> element). |
- - 'context' is the current output context |
- (the 'context' attribute of the <output> element). |
- - 'defs' is a map of C preprocessor-style symbol names to their values. |
- - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin'). |
- - 'pp_ifdef(symbol)' is a shorthand for "symbol in defs". |
- - 'pp_if(symbol)' is a shorthand for "symbol in defs and defs[symbol]". |
- - 'is_linux', 'is_macosx', 'is_win', 'is_posix' are true if 'os' |
- matches the given platform. |
- ''' |
- root = self.GetRoot() |
- lang = getattr(root, 'output_language', '') |
- context = getattr(root, 'output_context', '') |
- defs = getattr(root, 'defines', {}) |
- target_platform = getattr(root, 'target_platform', '') |
- extra_variables = { |
- 'lang': lang, |
- 'context': context, |
- } |
- return Node.EvaluateExpression( |
- expr, defs, target_platform, extra_variables) |
- |
- def OnlyTheseTranslations(self, languages): |
- '''Turns off loading of translations for languages not in the provided list. |
- |
- Attrs: |
- languages: ['fr', 'zh_cn'] |
- ''' |
- for node in self: |
- if (hasattr(node, 'IsTranslation') and |
- node.IsTranslation() and |
- node.GetLang() not in languages): |
- node.DisableLoading() |
- |
- def FindBooleanAttribute(self, attr, default, skip_self): |
- '''Searches all ancestors of the current node for the nearest enclosing |
- definition of the given boolean attribute. |
- |
- Args: |
- attr: 'fallback_to_english' |
- default: What to return if no node defines the attribute. |
- skip_self: Don't check the current node, only its parents. |
- ''' |
- p = self.parent if skip_self else self |
- while p: |
- value = p.attrs.get(attr, 'default').lower() |
- if value != 'default': |
- return (value == 'true') |
- p = p.parent |
- return default |
- |
- def PseudoIsAllowed(self): |
- '''Returns true if this node is allowed to use pseudo-translations. This |
- is true by default, unless this node is within a <release> node that has |
- the allow_pseudo attribute set to false. |
- ''' |
- return self.FindBooleanAttribute('allow_pseudo', |
- default=True, skip_self=True) |
- |
- def ShouldFallbackToEnglish(self): |
- '''Returns true iff this node should fall back to English when |
- pseudotranslations are disabled and no translation is available for a |
- given message. |
- ''' |
- return self.FindBooleanAttribute('fallback_to_english', |
- default=False, skip_self=True) |
- |
- def WhitelistMarkedAsSkip(self): |
- '''Returns true if the node is marked to be skipped in the output by a |
- whitelist. |
- ''' |
- return self._whitelist_marked_as_skip |
- |
- def SetWhitelistMarkedAsSkip(self, mark_skipped): |
- '''Sets WhitelistMarkedAsSkip. |
- ''' |
- self._whitelist_marked_as_skip = mark_skipped |
- |
- def ExpandVariables(self): |
- '''Whether we need to expand variables on a given node.''' |
- return False |
- |
- def IsResourceMapSource(self): |
- '''Whether this node is a resource map source.''' |
- return False |
- |
- def GeneratesResourceMapEntry(self, output_all_resource_defines, |
- is_active_descendant): |
- '''Whether this node should output a resource map entry. |
- |
- Args: |
- output_all_resource_defines: The value of output_all_resource_defines for |
- the root node. |
- is_active_descendant: Whether the current node is an active descendant |
- from the root node.''' |
- return False |
- |
- |
-class ContentNode(Node): |
- '''Convenience baseclass for nodes that can have content.''' |
- def _ContentType(self): |
- return self._CONTENT_TYPE_MIXED |
- |