OLD | NEW |
| (Empty) |
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 def IsResourceMapSource(self): | |
594 '''Whether this node is a resource map source.''' | |
595 return False | |
596 | |
597 def GeneratesResourceMapEntry(self, output_all_resource_defines, | |
598 is_active_descendant): | |
599 '''Whether this node should output a resource map entry. | |
600 | |
601 Args: | |
602 output_all_resource_defines: The value of output_all_resource_defines for | |
603 the root node. | |
604 is_active_descendant: Whether the current node is an active descendant | |
605 from the root node.''' | |
606 return False | |
607 | |
608 | |
609 class ContentNode(Node): | |
610 '''Convenience baseclass for nodes that can have content.''' | |
611 def _ContentType(self): | |
612 return self._CONTENT_TYPE_MIXED | |
613 | |
OLD | NEW |