aboutsummaryrefslogtreecommitdiff
path: root/grit/node/base.py
blob: 375524050878f1c44a1fc31e7d3f154018e763cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
#!/usr/bin/env python
# Copyright (c) 2012 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 collections
import os
import sys
import types
from xml.sax import saxutils

from grit import clique
from grit import exception
from grit import util


class Node(object):
  '''An item in the tree that has children.'''

  # 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

  # A class-static cache to memoize EvaluateExpression().
  # It has a 2 level nested dict structure.  The outer dict has keys
  # of tuples which define the environment in which the expression
  # will be evaluated. The inner dict is map of expr->result.
  eval_expr_cache = collections.defaultdict(dict)

  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

  # This context handler allows you to write "with node:" and get a
  # line identifying the offending node if an exception escapes from the body
  # of the with statement.
  def __enter__(self):
    return self

  def __exit__(self, exc_type, exc_value, traceback):
    if exc_type is not None:
      print u'Error processing node %s' % unicode(self)

  def __iter__(self):
    '''A preorder iteration through the tree that this node is the root of.'''
    return self.Preorder()

  def Preorder(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.Preorder():
        yield iterchild

  def ActiveChildren(self):
    '''Returns the children of this node that should be included in the current
    configuration. Overridden by <if>.'''
    return [node for node in self.children if not node.WhitelistMarkedAsSkip()]

  def ActiveDescendants(self):
    '''Yields the current node and all descendants that should be included in
    the current configuration, in preorder.'''
    yield self
    for child in self.ActiveChildren():
      for descendant in child.ActiveDescendants():
        yield descendant

  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):
      explanation = 'invalid child %s for parent %s' % (str(child), self.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([c for c in self.mixed_content
                    if isinstance(c, types.StringTypes)])

  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()

  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''
    default_attribs = self.DefaultAttributes()
    for attrib, value in sorted(self.attrs.items()):
      # Only print an attribute if it is other than the default value.
      if attrib not in default_attribs or value != default_attribs[attrib]:
        attribs += u' %s=%s' % (attrib, saxutils.quoteattr(value))

    # 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 SubstituteMessages(self, substituter):
    '''Applies substitutions to all messages in the tree.

    Called as a final step of RunGatherers.

    Args:
      substituter: a grit.util.Substituter object.
    '''
    for child in self.children:
      child.SubstituteMessages(substituter)

  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(),
                                      os.path.expandvars(path_from_basedir)))

  def GetInputPath(self):
    '''Returns a path, relative to the base directory set for the grd file,
    that points to the file the node refers to.
    '''
    # This implementation works for most nodes that have an input file.
    return self.attrs['file']

  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 GetChildrenOfType(self, type):
    '''Returns a list of all subnodes (recursing to all leaves) of this node
    that are of the indicated type (or tuple of types).

    Args:
      type: A type you could use with isinstance().

    Return:
      A list, possibly empty.
    '''
    return [child for child in self if isinstance(child, type)]

  def GetTextualIds(self):
    '''Returns a list of the textual ids of this node.
    '''
    if 'name' in self.attrs:
      return [self.attrs['name']]
    return []

  @classmethod
  def GetPlatformAssertion(cls, target_platform):
    '''If the platform is a specific well-known platform, this returns
    the is_xyz string representing that platform (e.g. is_linux),
    otherwise the empty string.
    '''
    platform = ''
    if target_platform == 'darwin':
      platform = 'is_macosx'
    elif target_platform.startswith('linux'):
      platform = 'is_linux'
    elif target_platform in ('cygwin', 'win32'):
      platform = 'is_win'
    elif target_platform in ('android', 'ios'):
      platform = 'is_%s' % target_platform
    return platform

  @classmethod
  def EvaluateExpression(cls, expr, defs, target_platform, extra_variables=None):
    '''Worker for EvaluateCondition (below) and conditions in XTB files.'''
    cache_dict = cls.eval_expr_cache[
        (tuple(defs.iteritems()), target_platform, extra_variables)]
    if expr in cache_dict:
      return cache_dict[expr]
    def pp_ifdef(symbol):
      return symbol in defs
    def pp_if(symbol):
      return defs.get(symbol, False)
    variable_map = {
        'defs' : defs,
        'os': target_platform,

        # One of these is_xyz assertions gets set to True in the line
        # following this initializer block.
        'is_linux': False,
        'is_macosx': False,
        'is_win': False,
        'is_android': False,
        'is_ios': False,

        # is_posix is not mutually exclusive of the others and gets
        # set here, not below.
        'is_posix': (target_platform in ('darwin', 'linux2', 'linux3', 'sunos5')
                     or 'bsd' in sys.platform),

        'pp_ifdef' : pp_ifdef,
        'pp_if' : pp_if,
    }
    variable_map[Node.GetPlatformAssertion(target_platform)] = True

    if extra_variables:
      variable_map.update(extra_variables)
    eval_result = cache_dict[expr] = eval(expr, {}, variable_map)
    return eval_result

  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
           (the 'lang' attribute of the <output> element).
      - 'context' is the current output context
           (the 'context' attribute of the <output> element).
      - 'defs' is a map of C preprocessor-style symbol names to their values.
      - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin').
      - 'pp_ifdef(symbol)' is a shorthand for "symbol in defs".
      - 'pp_if(symbol)' is a shorthand for "symbol in defs and defs[symbol]".
      - 'is_linux', 'is_macosx', 'is_win', 'is_posix' are true if 'os'
           matches the given platform.
    '''
    root = self.GetRoot()
    lang = getattr(root, 'output_language', '')
    context = getattr(root, 'output_context', '')
    defs = getattr(root, 'defines', {})
    target_platform = getattr(root, 'target_platform', '')
    extra_variables = (
        ('lang', lang),
        ('context', context),
    )
    return Node.EvaluateExpression(
        expr, defs, target_platform, extra_variables)

  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 FindBooleanAttribute(self, attr, default, skip_self):
    '''Searches all ancestors of the current node for the nearest enclosing
    definition of the given boolean attribute.

    Args:
      attr: 'fallback_to_english'
      default: What to return if no node defines the attribute.
      skip_self: Don't check the current node, only its parents.
    '''
    p = self.parent if skip_self else self
    while p:
      value = p.attrs.get(attr, 'default').lower()
      if value != 'default':
        return (value == 'true')
      p = p.parent
    return default

  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.
    '''
    return self.FindBooleanAttribute('allow_pseudo',
                                     default=True, skip_self=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.
    '''
    return self.FindBooleanAttribute('fallback_to_english',
                                     default=False, skip_self=True)

  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

  def ExpandVariables(self):
    '''Whether we need to expand variables on a given node.'''
    return False


class ContentNode(Node):
  '''Convenience baseclass for nodes that can have content.'''
  def _ContentType(self):
    return self._CONTENT_TYPE_MIXED