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

Side by Side 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, 2 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 unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « grit/node/__init__.py ('k') | grit/node/base_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 #!/usr/bin/python2.4
2 # Copyright (c) 2011 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 os
10 import sys
11 import types
12 from xml.sax import saxutils
13
14 from grit import exception
15 from grit import util
16 from grit import clique
17 import grit.format.interface
18
19
20 class Node(grit.format.interface.ItemFormatter):
21 '''An item in the tree that has children. Also implements the
22 ItemFormatter interface to allow formatting a node as a GRD document.'''
23
24 # Valid content types that can be returned by _ContentType()
25 _CONTENT_TYPE_NONE = 0 # No CDATA content but may have children
26 _CONTENT_TYPE_CDATA = 1 # Only CDATA, no children.
27 _CONTENT_TYPE_MIXED = 2 # CDATA and children, possibly intermingled
28
29 # Default nodes to not whitelist skipped
30 _whitelist_marked_as_skip = False
31
32 def __init__(self):
33 self.children = [] # A list of child elements
34 self.mixed_content = [] # A list of u'' and/or child elements (this
35 # duplicates 'children' but
36 # is needed to preserve markup-type content).
37 self.name = u'' # The name of this element
38 self.attrs = {} # The set of attributes (keys to values)
39 self.parent = None # Our parent unless we are the root element.
40 self.uberclique = None # Allows overriding uberclique for parts of tree
41
42 def __iter__(self):
43 '''An in-order iteration through the tree that this node is the
44 root of.'''
45 return self.inorder()
46
47 def inorder(self):
48 '''Generator that generates first this node, then the same generator for
49 any child nodes.'''
50 yield self
51 for child in self.children:
52 for iterchild in child.inorder():
53 yield iterchild
54
55 def GetRoot(self):
56 '''Returns the root Node in the tree this Node belongs to.'''
57 curr = self
58 while curr.parent:
59 curr = curr.parent
60 return curr
61
62 # TODO(joi) Use this (currently untested) optimization?:
63 #if hasattr(self, '_root'):
64 # return self._root
65 #curr = self
66 #while curr.parent and not hasattr(curr, '_root'):
67 # curr = curr.parent
68 #if curr.parent:
69 # self._root = curr._root
70 #else:
71 # self._root = curr
72 #return self._root
73
74 def StartParsing(self, name, parent):
75 '''Called at the start of parsing.
76
77 Args:
78 name: u'elementname'
79 parent: grit.node.base.Node or subclass or None
80 '''
81 assert isinstance(name, types.StringTypes)
82 assert not parent or isinstance(parent, Node)
83 self.name = name
84 self.parent = parent
85
86 def AddChild(self, child):
87 '''Adds a child to the list of children of this node, if it is a valid
88 child for the node.'''
89 assert isinstance(child, Node)
90 if (not self._IsValidChild(child) or
91 self._ContentType() == self._CONTENT_TYPE_CDATA):
92 if child.parent:
93 explanation = 'child %s of parent %s' % (child.name, child.parent.name)
94 else:
95 explanation = 'node %s with no parent' % child.name
96 raise exception.UnexpectedChild(explanation)
97 self.children.append(child)
98 self.mixed_content.append(child)
99
100 def RemoveChild(self, child_id):
101 '''Removes the first node that has a "name" attribute which
102 matches "child_id" in the list of immediate children of
103 this node.
104
105 Args:
106 child_id: String identifying the child to be removed
107 '''
108 index = 0
109 # Safe not to copy since we only remove the first element found
110 for child in self.children:
111 name_attr = child.attrs['name']
112 if name_attr == child_id:
113 self.children.pop(index)
114 self.mixed_content.pop(index)
115 break
116 index += 1
117
118 def AppendContent(self, content):
119 '''Appends a chunk of text as content of this node.
120
121 Args:
122 content: u'hello'
123
124 Return:
125 None
126 '''
127 assert isinstance(content, types.StringTypes)
128 if self._ContentType() != self._CONTENT_TYPE_NONE:
129 self.mixed_content.append(content)
130 elif content.strip() != '':
131 raise exception.UnexpectedContent()
132
133 def HandleAttribute(self, attrib, value):
134 '''Informs the node of an attribute that was parsed out of the GRD file
135 for it.
136
137 Args:
138 attrib: 'name'
139 value: 'fooblat'
140
141 Return:
142 None
143 '''
144 assert isinstance(attrib, types.StringTypes)
145 assert isinstance(value, types.StringTypes)
146 if self._IsValidAttribute(attrib, value):
147 self.attrs[attrib] = value
148 else:
149 raise exception.UnexpectedAttribute(attrib)
150
151 def EndParsing(self):
152 '''Called at the end of parsing.'''
153
154 # TODO(joi) Rewrite this, it's extremely ugly!
155 if len(self.mixed_content):
156 if isinstance(self.mixed_content[0], types.StringTypes):
157 # Remove leading and trailing chunks of pure whitespace.
158 while (len(self.mixed_content) and
159 isinstance(self.mixed_content[0], types.StringTypes) and
160 self.mixed_content[0].strip() == ''):
161 self.mixed_content = self.mixed_content[1:]
162 # Strip leading and trailing whitespace from mixed content chunks
163 # at front and back.
164 if (len(self.mixed_content) and
165 isinstance(self.mixed_content[0], types.StringTypes)):
166 self.mixed_content[0] = self.mixed_content[0].lstrip()
167 # Remove leading and trailing ''' (used to demarcate whitespace)
168 if (len(self.mixed_content) and
169 isinstance(self.mixed_content[0], types.StringTypes)):
170 if self.mixed_content[0].startswith("'''"):
171 self.mixed_content[0] = self.mixed_content[0][3:]
172 if len(self.mixed_content):
173 if isinstance(self.mixed_content[-1], types.StringTypes):
174 # Same stuff all over again for the tail end.
175 while (len(self.mixed_content) and
176 isinstance(self.mixed_content[-1], types.StringTypes) and
177 self.mixed_content[-1].strip() == ''):
178 self.mixed_content = self.mixed_content[:-1]
179 if (len(self.mixed_content) and
180 isinstance(self.mixed_content[-1], types.StringTypes)):
181 self.mixed_content[-1] = self.mixed_content[-1].rstrip()
182 if (len(self.mixed_content) and
183 isinstance(self.mixed_content[-1], types.StringTypes)):
184 if self.mixed_content[-1].endswith("'''"):
185 self.mixed_content[-1] = self.mixed_content[-1][:-3]
186
187 # Check that all mandatory attributes are there.
188 for node_mandatt in self.MandatoryAttributes():
189 mandatt_list = []
190 if node_mandatt.find('|') >= 0:
191 mandatt_list = node_mandatt.split('|')
192 else:
193 mandatt_list.append(node_mandatt)
194
195 mandatt_option_found = False
196 for mandatt in mandatt_list:
197 assert mandatt not in self.DefaultAttributes().keys()
198 if mandatt in self.attrs:
199 if not mandatt_option_found:
200 mandatt_option_found = True
201 else:
202 raise exception.MutuallyExclusiveMandatoryAttribute(mandatt)
203
204 if not mandatt_option_found:
205 raise exception.MissingMandatoryAttribute(mandatt)
206
207 # Add default attributes if not specified in input file.
208 for defattr in self.DefaultAttributes():
209 if not defattr in self.attrs:
210 self.attrs[defattr] = self.DefaultAttributes()[defattr]
211
212 def GetCdata(self):
213 '''Returns all CDATA of this element, concatenated into a single
214 string. Note that this ignores any elements embedded in CDATA.'''
215 return ''.join(filter(lambda c: isinstance(c, types.StringTypes),
216 self.mixed_content))
217
218 def __unicode__(self):
219 '''Returns this node and all nodes below it as an XML document in a Unicode
220 string.'''
221 header = u'<?xml version="1.0" encoding="UTF-8"?>\n'
222 return header + self.FormatXml()
223
224 # Compliance with ItemFormatter interface.
225 def Format(self, item, lang_re = None, begin_item=True):
226 if not begin_item:
227 return ''
228 else:
229 return item.FormatXml()
230
231 def FormatXml(self, indent = u'', one_line = False):
232 '''Returns this node and all nodes below it as an XML
233 element in a Unicode string. This differs from __unicode__ in that it does
234 not include the <?xml> stuff at the top of the string. If one_line is true,
235 children and CDATA are layed out in a way that preserves internal
236 whitespace.
237 '''
238 assert isinstance(indent, types.StringTypes)
239
240 content_one_line = (one_line or
241 self._ContentType() == self._CONTENT_TYPE_MIXED)
242 inside_content = self.ContentsAsXml(indent, content_one_line)
243
244 # Then the attributes for this node.
245 attribs = u' '
246 for (attrib, value) in self.attrs.iteritems():
247 # Only print an attribute if it is other than the default value.
248 if (not self.DefaultAttributes().has_key(attrib) or
249 value != self.DefaultAttributes()[attrib]):
250 attribs += u'%s=%s ' % (attrib, saxutils.quoteattr(value))
251 attribs = attribs.rstrip() # if no attribs, we end up with '', otherwise
252 # we end up with a space-prefixed string
253
254 # Finally build the XML for our node and return it
255 if len(inside_content) > 0:
256 if one_line:
257 return u'<%s%s>%s</%s>' % (self.name, attribs, inside_content, self.name )
258 elif content_one_line:
259 return u'%s<%s%s>\n%s %s\n%s</%s>' % (
260 indent, self.name, attribs,
261 indent, inside_content,
262 indent, self.name)
263 else:
264 return u'%s<%s%s>\n%s\n%s</%s>' % (
265 indent, self.name, attribs,
266 inside_content,
267 indent, self.name)
268 else:
269 return u'%s<%s%s />' % (indent, self.name, attribs)
270
271 def ContentsAsXml(self, indent, one_line):
272 '''Returns the contents of this node (CDATA and child elements) in XML
273 format. If 'one_line' is true, the content will be laid out on one line.'''
274 assert isinstance(indent, types.StringTypes)
275
276 # Build the contents of the element.
277 inside_parts = []
278 last_item = None
279 for mixed_item in self.mixed_content:
280 if isinstance(mixed_item, Node):
281 inside_parts.append(mixed_item.FormatXml(indent + u' ', one_line))
282 if not one_line:
283 inside_parts.append(u'\n')
284 else:
285 message = mixed_item
286 # If this is the first item and it starts with whitespace, we add
287 # the ''' delimiter.
288 if not last_item and message.lstrip() != message:
289 message = u"'''" + message
290 inside_parts.append(util.EncodeCdata(message))
291 last_item = mixed_item
292
293 # If there are only child nodes and no cdata, there will be a spurious
294 # trailing \n
295 if len(inside_parts) and inside_parts[-1] == '\n':
296 inside_parts = inside_parts[:-1]
297
298 # If the last item is a string (not a node) and ends with whitespace,
299 # we need to add the ''' delimiter.
300 if (isinstance(last_item, types.StringTypes) and
301 last_item.rstrip() != last_item):
302 inside_parts[-1] = inside_parts[-1] + u"'''"
303
304 return u''.join(inside_parts)
305
306 def RunGatherers(self, recursive=0, debug=False):
307 '''Runs all gatherers on this object, which may add to the data stored
308 by the object. If 'recursive' is true, will call RunGatherers() recursively
309 on all child nodes first. If 'debug' is True, will print out information
310 as it is running each nodes' gatherers.
311
312 Gatherers for <translations> child nodes will always be run after all other
313 child nodes have been gathered.
314 '''
315 if recursive:
316 process_last = []
317 for child in self.children:
318 if child.name == 'translations':
319 process_last.append(child)
320 else:
321 child.RunGatherers(recursive=recursive, debug=debug)
322 for child in process_last:
323 child.RunGatherers(recursive=recursive, debug=debug)
324
325 def ItemFormatter(self, type):
326 '''Returns an instance of the item formatter for this object of the
327 specified type, or None if not supported.
328
329 Args:
330 type: 'rc-header'
331
332 Return:
333 (object RcHeaderItemFormatter)
334 '''
335 if type == 'xml':
336 return self
337 else:
338 return None
339
340 def SatisfiesOutputCondition(self):
341 '''Returns true if this node is either not a child of an <if> element
342 or if it is a child of an <if> element and the conditions for it being
343 output are satisfied.
344
345 Used to determine whether to return item formatters for formats that
346 obey conditional output of resources (e.g. the RC formatters).
347 '''
348 from grit.node import misc
349 if not self.parent or not isinstance(self.parent, misc.IfNode):
350 return True
351 else:
352 return self.parent.IsConditionSatisfied()
353
354 def _IsValidChild(self, child):
355 '''Returns true if 'child' is a valid child of this node.
356 Overridden by subclasses.'''
357 return False
358
359 def _IsValidAttribute(self, name, value):
360 '''Returns true if 'name' is the name of a valid attribute of this element
361 and 'value' is a valid value for that attribute. Overriden by
362 subclasses unless they have only mandatory attributes.'''
363 return (name in self.MandatoryAttributes() or
364 name in self.DefaultAttributes())
365
366 def _ContentType(self):
367 '''Returns the type of content this element can have. Overridden by
368 subclasses. The content type can be one of the _CONTENT_TYPE_XXX constants
369 above.'''
370 return self._CONTENT_TYPE_NONE
371
372 def MandatoryAttributes(self):
373 '''Returns a list of attribute names that are mandatory (non-optional)
374 on the current element. One can specify a list of
375 "mutually exclusive mandatory" attributes by specifying them as one
376 element in the list, separated by a "|" character.
377 '''
378 return []
379
380 def DefaultAttributes(self):
381 '''Returns a dictionary of attribute names that have defaults, mapped to
382 the default value. Overridden by subclasses.'''
383 return {}
384
385 def GetCliques(self):
386 '''Returns all MessageClique objects belonging to this node. Overridden
387 by subclasses.
388
389 Return:
390 [clique1, clique2] or []
391 '''
392 return []
393
394 def ToRealPath(self, path_from_basedir):
395 '''Returns a real path (which can be absolute or relative to the current
396 working directory), given a path that is relative to the base directory
397 set for the GRIT input file.
398
399 Args:
400 path_from_basedir: '..'
401
402 Return:
403 'resource'
404 '''
405 return util.normpath(os.path.join(self.GetRoot().GetBaseDir(),
406 path_from_basedir))
407
408 def FilenameToOpen(self):
409 '''Returns a path, either absolute or relative to the current working
410 directory, that points to the file the node refers to. This is only valid
411 for nodes that have a 'file' or 'path' attribute. Note that the attribute
412 is a path to the file relative to the 'base-dir' of the .grd file, whereas
413 this function returns a path that can be used to open the file.'''
414 file_attribute = 'file'
415 if not file_attribute in self.attrs:
416 file_attribute = 'path'
417 return self.ToRealPath(self.attrs[file_attribute])
418
419 def UberClique(self):
420 '''Returns the uberclique that should be used for messages originating in
421 a given node. If the node itself has its uberclique set, that is what we
422 use, otherwise we search upwards until we find one. If we do not find one
423 even at the root node, we set the root node's uberclique to a new
424 uberclique instance.
425 '''
426 node = self
427 while not node.uberclique and node.parent:
428 node = node.parent
429 if not node.uberclique:
430 node.uberclique = clique.UberClique()
431 return node.uberclique
432
433 def IsTranslateable(self):
434 '''Returns false if the node has contents that should not be translated,
435 otherwise returns false (even if the node has no contents).
436 '''
437 if not 'translateable' in self.attrs:
438 return True
439 else:
440 return self.attrs['translateable'] == 'true'
441
442 def GetNodeById(self, id):
443 '''Returns the node in the subtree parented by this node that has a 'name'
444 attribute matching 'id'. Returns None if no such node is found.
445 '''
446 for node in self:
447 if 'name' in node.attrs and node.attrs['name'] == id:
448 return node
449 return None
450
451 def GetTextualIds(self):
452 '''Returns the textual ids of this node, if it has some.
453 Otherwise it just returns None.
454 '''
455 if 'name' in self.attrs:
456 return [self.attrs['name']]
457 return None
458
459 def EvaluateCondition(self, expr):
460 '''Returns true if and only if the Python expression 'expr' evaluates
461 to true.
462
463 The expression is given a few local variables:
464 - 'lang' is the language currently being output
465 - 'defs' is a map of C preprocessor-style define names to their values
466 - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin').
467 - 'pp_ifdef(define)' which behaves just like the C preprocessors #ifdef,
468 i.e. it is shorthand for "define in defs"
469 - 'pp_if(define)' which behaves just like the C preprocessor's #if, i.e.
470 it is shorthand for "define in defs and defs[define]".
471 '''
472 root = self.GetRoot()
473 lang = ''
474 defs = {}
475 def pp_ifdef(define):
476 return define in defs
477 def pp_if(define):
478 return define in defs and defs[define]
479 if hasattr(root, 'output_language'):
480 lang = root.output_language
481 if hasattr(root, 'defines'):
482 defs = root.defines
483 variable_map = {
484 'lang' : lang,
485 'defs' : defs,
486 'os': sys.platform,
487 'is_linux': sys.platform.startswith('linux'),
488 'is_macosx': sys.platform == 'darwin',
489 'is_win': sys.platform in ('cygwin', 'win32'),
490 'is_posix': (sys.platform in ('darwin', 'linux2', 'linux3', 'sunos5')
491 or sys.platform.find('bsd') != -1),
492 'pp_ifdef' : pp_ifdef,
493 'pp_if' : pp_if,
494 }
495 return eval(expr, {}, variable_map)
496
497 def OnlyTheseTranslations(self, languages):
498 '''Turns off loading of translations for languages not in the provided list.
499
500 Attrs:
501 languages: ['fr', 'zh_cn']
502 '''
503 for node in self:
504 if (hasattr(node, 'IsTranslation') and
505 node.IsTranslation() and
506 node.GetLang() not in languages):
507 node.DisableLoading()
508
509 def PseudoIsAllowed(self):
510 '''Returns true if this node is allowed to use pseudo-translations. This
511 is true by default, unless this node is within a <release> node that has
512 the allow_pseudo attribute set to false.
513 '''
514 p = self.parent
515 while p:
516 if 'allow_pseudo' in p.attrs:
517 return (p.attrs['allow_pseudo'].lower() == 'true')
518 p = p.parent
519 return True
520
521 def ShouldFallbackToEnglish(self):
522 '''Returns true iff this node should fall back to English when
523 pseudotranslations are disabled and no translation is available for a
524 given message.
525 '''
526 p = self.parent
527 while p:
528 if 'fallback_to_english' in p.attrs:
529 return (p.attrs['fallback_to_english'].lower() == 'true')
530 p = p.parent
531 return False
532
533 def WhitelistMarkedAsSkip(self):
534 '''Returns true if the node is marked to be skipped in the output by a
535 whitelist.
536 '''
537 return self._whitelist_marked_as_skip
538
539 def SetWhitelistMarkedAsSkip(self, mark_skipped):
540 '''Sets WhitelistMarkedAsSkip.
541 '''
542 self._whitelist_marked_as_skip = mark_skipped
543
544
545 class ContentNode(Node):
546 '''Convenience baseclass for nodes that can have content.'''
547 def _ContentType(self):
548 return self._CONTENT_TYPE_MIXED
549
OLDNEW
« 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