Index: grit/node/base.py |
=================================================================== |
--- grit/node/base.py (revision 0) |
+++ grit/node/base.py (revision 0) |
@@ -0,0 +1,549 @@ |
+#!/usr/bin/python2.4 |
+# Copyright (c) 2011 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 os |
+import sys |
+import types |
+from xml.sax import saxutils |
+ |
+from grit import exception |
+from grit import util |
+from grit import clique |
+import grit.format.interface |
+ |
+ |
+class Node(grit.format.interface.ItemFormatter): |
+ '''An item in the tree that has children. Also implements the |
+ ItemFormatter interface to allow formatting a node as a GRD document.''' |
+ |
+ # 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 |
+ |
+ 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 |
+ |
+ def __iter__(self): |
+ '''An in-order iteration through the tree that this node is the |
+ root of.''' |
+ return self.inorder() |
+ |
+ def inorder(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.inorder(): |
+ yield iterchild |
+ |
+ 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): |
+ if child.parent: |
+ explanation = 'child %s of parent %s' % (child.name, child.parent.name) |
+ else: |
+ explanation = 'node %s with no parent' % child.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(filter(lambda c: isinstance(c, types.StringTypes), |
+ self.mixed_content)) |
+ |
+ 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() |
+ |
+ # Compliance with ItemFormatter interface. |
+ def Format(self, item, lang_re = None, begin_item=True): |
+ if not begin_item: |
+ return '' |
+ else: |
+ return item.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' ' |
+ for (attrib, value) in self.attrs.iteritems(): |
+ # Only print an attribute if it is other than the default value. |
+ if (not self.DefaultAttributes().has_key(attrib) or |
+ value != self.DefaultAttributes()[attrib]): |
+ attribs += u'%s=%s ' % (attrib, saxutils.quoteattr(value)) |
+ attribs = attribs.rstrip() # if no attribs, we end up with '', otherwise |
+ # we end up with a space-prefixed string |
+ |
+ # 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 RunGatherers(self, recursive=0, debug=False): |
+ '''Runs all gatherers on this object, which may add to the data stored |
+ by the object. If 'recursive' is true, will call RunGatherers() recursively |
+ on all child nodes first. If 'debug' is True, will print out information |
+ as it is running each nodes' gatherers. |
+ |
+ Gatherers for <translations> child nodes will always be run after all other |
+ child nodes have been gathered. |
+ ''' |
+ if recursive: |
+ process_last = [] |
+ for child in self.children: |
+ if child.name == 'translations': |
+ process_last.append(child) |
+ else: |
+ child.RunGatherers(recursive=recursive, debug=debug) |
+ for child in process_last: |
+ child.RunGatherers(recursive=recursive, debug=debug) |
+ |
+ def ItemFormatter(self, type): |
+ '''Returns an instance of the item formatter for this object of the |
+ specified type, or None if not supported. |
+ |
+ Args: |
+ type: 'rc-header' |
+ |
+ Return: |
+ (object RcHeaderItemFormatter) |
+ ''' |
+ if type == 'xml': |
+ return self |
+ else: |
+ return None |
+ |
+ def SatisfiesOutputCondition(self): |
+ '''Returns true if this node is either not a child of an <if> element |
+ or if it is a child of an <if> element and the conditions for it being |
+ output are satisfied. |
+ |
+ Used to determine whether to return item formatters for formats that |
+ obey conditional output of resources (e.g. the RC formatters). |
+ ''' |
+ from grit.node import misc |
+ if not self.parent or not isinstance(self.parent, misc.IfNode): |
+ return True |
+ else: |
+ return self.parent.IsConditionSatisfied() |
+ |
+ 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(), |
+ path_from_basedir)) |
+ |
+ def FilenameToOpen(self): |
+ '''Returns a path, either absolute or relative to the current working |
+ directory, that points to the file the node refers to. This is only valid |
+ for nodes that have a 'file' or 'path' attribute. Note that the attribute |
+ is a path to the file relative to the 'base-dir' of the .grd file, whereas |
+ this function returns a path that can be used to open the file.''' |
+ file_attribute = 'file' |
+ if not file_attribute in self.attrs: |
+ file_attribute = 'path' |
+ return self.ToRealPath(self.attrs[file_attribute]) |
+ |
+ 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 GetTextualIds(self): |
+ '''Returns the textual ids of this node, if it has some. |
+ Otherwise it just returns None. |
+ ''' |
+ if 'name' in self.attrs: |
+ return [self.attrs['name']] |
+ return None |
+ |
+ 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 |
+ - 'defs' is a map of C preprocessor-style define names to their values |
+ - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin'). |
+ - 'pp_ifdef(define)' which behaves just like the C preprocessors #ifdef, |
+ i.e. it is shorthand for "define in defs" |
+ - 'pp_if(define)' which behaves just like the C preprocessor's #if, i.e. |
+ it is shorthand for "define in defs and defs[define]". |
+ ''' |
+ root = self.GetRoot() |
+ lang = '' |
+ defs = {} |
+ def pp_ifdef(define): |
+ return define in defs |
+ def pp_if(define): |
+ return define in defs and defs[define] |
+ if hasattr(root, 'output_language'): |
+ lang = root.output_language |
+ if hasattr(root, 'defines'): |
+ defs = root.defines |
+ variable_map = { |
+ 'lang' : lang, |
+ 'defs' : defs, |
+ 'os': sys.platform, |
+ 'is_linux': sys.platform.startswith('linux'), |
+ 'is_macosx': sys.platform == 'darwin', |
+ 'is_win': sys.platform in ('cygwin', 'win32'), |
+ 'is_posix': (sys.platform in ('darwin', 'linux2', 'linux3', 'sunos5') |
+ or sys.platform.find('bsd') != -1), |
+ 'pp_ifdef' : pp_ifdef, |
+ 'pp_if' : pp_if, |
+ } |
+ return eval(expr, {}, variable_map) |
+ |
+ 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 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. |
+ ''' |
+ p = self.parent |
+ while p: |
+ if 'allow_pseudo' in p.attrs: |
+ return (p.attrs['allow_pseudo'].lower() == 'true') |
+ p = p.parent |
+ return 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. |
+ ''' |
+ p = self.parent |
+ while p: |
+ if 'fallback_to_english' in p.attrs: |
+ return (p.attrs['fallback_to_english'].lower() == 'true') |
+ p = p.parent |
+ return False |
+ |
+ 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 |
+ |
+ |
+class ContentNode(Node): |
+ '''Convenience baseclass for nodes that can have content.''' |
+ def _ContentType(self): |
+ return self._CONTENT_TYPE_MIXED |
+ |
Property changes on: grit/node/base.py |
___________________________________________________________________ |
Added: svn:eol-style |
+ LF |