Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(384)

Unified Diff: grit/node/base.py

Issue 7994004: Initial source commit to grit-i18n project. (Closed) Base URL: http://grit-i18n.googlecode.com/svn/trunk/
Patch Set: Created 9 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « grit/node/__init__.py ('k') | grit/node/base_unittest.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « grit/node/__init__.py ('k') | grit/node/base_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698