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.
6 '''Base types for nodes in a GRIT resource tree.
12 from xml.sax import saxutils
14 from grit import clique
15 from grit import exception
20 '''An item in the tree that has children.'''
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
27 # Default nodes to not whitelist skipped
28 _whitelist_marked_as_skip = False
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']).
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
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.
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)
58 '''A preorder iteration through the tree that this node is the root of.'''
59 return self.Preorder()
62 '''Generator that generates first this node, then the same generator for
65 for child in self.children:
66 for iterchild in child.Preorder():
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()]
74 def ActiveDescendants(self):
75 '''Yields the current node and all descendants that should be included in
76 the current configuration, in preorder.'''
78 for child in self.ActiveChildren():
79 for descendant in child.ActiveDescendants():
83 '''Returns the root Node in the tree this Node belongs to.'''
89 # TODO(joi) Use this (currently untested) optimization?:
90 #if hasattr(self, '_root'):
93 #while curr.parent and not hasattr(curr, '_root'):
96 # self._root = curr._root
101 def StartParsing(self, name, parent):
102 '''Called at the start of parsing.
106 parent: grit.node.base.Node or subclass or None
108 assert isinstance(name, types.StringTypes)
109 assert not parent or isinstance(parent, Node)
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)
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
130 child_id: String identifying the child to be removed
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)
142 def AppendContent(self, content):
143 '''Appends a chunk of text as content of this node.
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()
157 def HandleAttribute(self, attrib, value):
158 '''Informs the node of an attribute that was parsed out of the GRD file
168 assert isinstance(attrib, types.StringTypes)
169 assert isinstance(value, types.StringTypes)
170 if self._IsValidAttribute(attrib, value):
171 self.attrs[attrib] = value
173 raise exception.UnexpectedAttribute(attrib)
175 def EndParsing(self):
176 '''Called at the end of parsing.'''
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
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]
211 # Check that all mandatory attributes are there.
212 for node_mandatt in self.MandatoryAttributes():
214 if node_mandatt.find('|') >= 0:
215 mandatt_list = node_mandatt.split('|')
217 mandatt_list.append(node_mandatt)
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
226 raise exception.MutuallyExclusiveMandatoryAttribute(mandatt)
228 if not mandatt_option_found:
229 raise exception.MissingMandatoryAttribute(mandatt)
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]
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)])
242 def __unicode__(self):
243 '''Returns this node and all nodes below it as an XML document in a Unicode
245 header = u'<?xml version="1.0" encoding="UTF-8"?>\n'
246 return header + self.FormatXml()
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
255 assert isinstance(indent, types.StringTypes)
257 content_one_line = (one_line or
258 self._ContentType() == self._CONTENT_TYPE_MIXED)
259 inside_content = self.ContentsAsXml(indent, content_one_line)
261 # Then the attributes for this node.
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))
269 # Finally build the XML for our node and return it
270 if len(inside_content) > 0:
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,
279 return u'%s<%s%s>\n%s\n%s</%s>' % (
280 indent, self.name, attribs,
284 return u'%s<%s%s />' % (indent, self.name, attribs)
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)
291 # Build the contents of the element.
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))
298 inside_parts.append(u'\n')
301 # If this is the first item and it starts with whitespace, we add
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
308 # If there are only child nodes and no cdata, there will be a spurious
310 if len(inside_parts) and inside_parts[-1] == '\n':
311 inside_parts = inside_parts[:-1]
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"'''"
319 return u''.join(inside_parts)
321 def SubstituteMessages(self, substituter):
322 '''Applies substitutions to all messages in the tree.
324 Called as a final step of RunGatherers.
327 substituter: a grit.util.Substituter object.
329 for child in self.children:
330 child.SubstituteMessages(substituter)
332 def _IsValidChild(self, child):
333 '''Returns true if 'child' is a valid child of this node.
334 Overridden by subclasses.'''
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())
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
348 return self._CONTENT_TYPE_NONE
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.
358 def DefaultAttributes(self):
359 '''Returns a dictionary of attribute names that have defaults, mapped to
360 the default value. Overridden by subclasses.'''
363 def GetCliques(self):
364 '''Returns all MessageClique objects belonging to this node. Overridden
368 [clique1, clique2] or []
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.
378 path_from_basedir: '..'
383 return util.normpath(os.path.join(self.GetRoot().GetBaseDir(),
384 os.path.expandvars(path_from_basedir)))
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.
390 # This implementation works for most nodes that have an input file.
391 return self.attrs['file']
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
401 while not node.uberclique and node.parent:
403 if not node.uberclique:
404 node.uberclique = clique.UberClique()
405 return node.uberclique
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).
411 if not 'translateable' in self.attrs:
414 return self.attrs['translateable'] == 'true'
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.
421 if 'name' in node.attrs and node.attrs['name'] == id:
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).
430 type: A type you could use with isinstance().
433 A list, possibly empty.
435 return [child for child in self if isinstance(child, type)]
437 def GetTextualIds(self):
438 '''Returns a list of the textual ids of this node.
440 if 'name' in self.attrs:
441 return [self.attrs['name']]
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]
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
457 # Set values only for variables that are needed to eval the expression.
459 for name in variables_in_expr:
461 value = target_platform
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',
480 or 'bsd' in target_platform)
482 elif name == 'pp_ifdef':
483 def pp_ifdef(symbol):
484 return symbol in defs
486 elif name == 'pp_if':
488 return defs.get(symbol, False)
493 elif name in extra_variables:
494 value = extra_variables[name]
496 # Undefined variables default to False.
499 variable_map[name] = value
501 eval_result = eval(code, {}, variable_map)
502 assert isinstance(eval_result, bool)
505 def EvaluateCondition(self, expr):
506 '''Returns true if and only if the Python expression 'expr' evaluates
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.
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', '')
530 return Node.EvaluateExpression(
531 expr, defs, target_platform, extra_variables)
533 def OnlyTheseTranslations(self, languages):
534 '''Turns off loading of translations for languages not in the provided list.
537 languages: ['fr', 'zh_cn']
540 if (hasattr(node, 'IsTranslation') and
541 node.IsTranslation() and
542 node.GetLang() not in languages):
543 node.DisableLoading()
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.
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.
554 p = self.parent if skip_self else self
556 value = p.attrs.get(attr, 'default').lower()
557 if value != 'default':
558 return (value == 'true')
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.
567 return self.FindBooleanAttribute('allow_pseudo',
568 default=True, skip_self=True)
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
575 return self.FindBooleanAttribute('fallback_to_english',
576 default=False, skip_self=True)
578 def WhitelistMarkedAsSkip(self):
579 '''Returns true if the node is marked to be skipped in the output by a
582 return self._whitelist_marked_as_skip
584 def SetWhitelistMarkedAsSkip(self, mark_skipped):
585 '''Sets WhitelistMarkedAsSkip.
587 self._whitelist_marked_as_skip = mark_skipped
589 def ExpandVariables(self):
590 '''Whether we need to expand variables on a given node.'''
594 class ContentNode(Node):
595 '''Convenience baseclass for nodes that can have content.'''
596 def _ContentType(self):
597 return self._CONTENT_TYPE_MIXED