OLD | NEW |
(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 |
OLD | NEW |