summaryrefslogtreecommitdiffstats
path: root/chromium/tools/grit/grit/node/misc.py
blob: 345081f5a269d8b1eeec17a559e66f6a4a10977a (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
#!/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.

"""Miscellaneous node types.
"""

import os.path
import re
import sys

from grit import constants
from grit import exception
from grit import util
import grit.format.rc_header
from grit.node import base
from grit.node import io
from grit.node import message


# RTL languages
# TODO(jennyz): remove this fixed set of RTL language array
# now that generic expand_variable code exists.
_RTL_LANGS = (
    'ar',  # Arabic
    'fa',  # Farsi
    'iw',  # Hebrew
    'ks',  # Kashmiri
    'ku',  # Kurdish
    'ps',  # Pashto
    'ur',  # Urdu
    'yi',  # Yiddish
)


def _ReadFirstIdsFromFile(filename, defines):
  """Read the starting resource id values from |filename|.  We also
  expand variables of the form <(FOO) based on defines passed in on
  the command line.

  Returns a tuple, the absolute path of SRCDIR followed by the
  first_ids dictionary.
  """
  first_ids_dict = eval(util.ReadFile(filename, util.RAW_TEXT))
  src_root_dir = os.path.abspath(os.path.join(os.path.dirname(filename),
                                              first_ids_dict['SRCDIR']))

  def ReplaceVariable(matchobj):
    for key, value in defines.iteritems():
      if matchobj.group(1) == key:
        return value
    return ''

  renames = []
  for grd_filename in first_ids_dict:
    new_grd_filename = re.sub(r'<\(([A-Za-z_]+)\)', ReplaceVariable,
                              grd_filename)
    if new_grd_filename != grd_filename:
      abs_grd_filename = os.path.abspath(new_grd_filename)
      if abs_grd_filename[:len(src_root_dir)] != src_root_dir:
        new_grd_filename = os.path.basename(abs_grd_filename)
      else:
        new_grd_filename = abs_grd_filename[len(src_root_dir) + 1:]
        new_grd_filename = new_grd_filename.replace('\\', '/')
      renames.append((grd_filename, new_grd_filename))

  for grd_filename, new_grd_filename in renames:
    first_ids_dict[new_grd_filename] = first_ids_dict[grd_filename]
    del(first_ids_dict[grd_filename])

  return (src_root_dir, first_ids_dict)


class SplicingNode(base.Node):
  """A node whose children should be considered to be at the same level as
  its siblings for most purposes. This includes <if> and <part> nodes.
  """

  def _IsValidChild(self, child):
    assert self.parent, '<%s> node should never be root.' % self.name
    if isinstance(child, SplicingNode):
      return True  # avoid O(n^2) behavior
    return self.parent._IsValidChild(child)


class IfNode(SplicingNode):
  """A node for conditional inclusion of resources.
  """

  def MandatoryAttributes(self):
    return ['expr']

  def _IsValidChild(self, child):
    return (isinstance(child, (ThenNode, ElseNode)) or
            super(IfNode, self)._IsValidChild(child))

  def EndParsing(self):
    children = self.children
    self.if_then_else = False
    if any(isinstance(node, (ThenNode, ElseNode)) for node in children):
      if (len(children) != 2 or not isinstance(children[0], ThenNode) or
                                not isinstance(children[1], ElseNode)):
        raise exception.UnexpectedChild(
            '<if> element must be <if><then>...</then><else>...</else></if>')
      self.if_then_else = True

  def ActiveChildren(self):
    cond = self.EvaluateCondition(self.attrs['expr'])
    if self.if_then_else:
      return self.children[0 if cond else 1].ActiveChildren()
    else:
      # Equivalent to having all children inside <then> with an empty <else>
      return super(IfNode, self).ActiveChildren() if cond else []


class ThenNode(SplicingNode):
  """A <then> node. Can only appear directly inside an <if> node."""
  pass


class ElseNode(SplicingNode):
  """An <else> node. Can only appear directly inside an <if> node."""
  pass


class PartNode(SplicingNode):
  """A node for inclusion of sub-grd (*.grp) files.
  """

  def __init__(self):
    super(PartNode, self).__init__()
    self.started_inclusion = False

  def MandatoryAttributes(self):
    return ['file']

  def _IsValidChild(self, child):
    return self.started_inclusion and super(PartNode, self)._IsValidChild(child)


class ReleaseNode(base.Node):
  """The <release> element."""

  def _IsValidChild(self, child):
    from grit.node import empty
    return isinstance(child, (empty.IncludesNode, empty.MessagesNode,
                              empty.StructuresNode, empty.IdentifiersNode))

  def _IsValidAttribute(self, name, value):
    return (
      (name == 'seq' and int(value) <= self.GetRoot().GetCurrentRelease()) or
      name == 'allow_pseudo'
    )

  def MandatoryAttributes(self):
    return ['seq']

  def DefaultAttributes(self):
    return { 'allow_pseudo' : 'true' }

  def GetReleaseNumber():
    """Returns the sequence number of this release."""
    return self.attribs['seq']

class GritNode(base.Node):
  """The <grit> root element."""

  def __init__(self):
    super(GritNode, self).__init__()
    self.output_language = ''
    self.defines = {}
    self.substituter = None
    self.target_platform = sys.platform

  def _IsValidChild(self, child):
    from grit.node import empty
    return isinstance(child, (ReleaseNode, empty.TranslationsNode,
                              empty.OutputsNode))

  def _IsValidAttribute(self, name, value):
    if name not in ['base_dir', 'first_ids_file', 'source_lang_id',
                    'latest_public_release', 'current_release',
                    'enc_check', 'tc_project', 'grit_version',
                    'output_all_resource_defines']:
      return False
    if name in ['latest_public_release', 'current_release'] and value.strip(
      '0123456789') != '':
      return False
    return True

  def MandatoryAttributes(self):
    return ['latest_public_release', 'current_release']

  def DefaultAttributes(self):
    return {
      'base_dir' : '.',
      'first_ids_file': '',
      'grit_version': 1,
      'source_lang_id' : 'en',
      'enc_check' : constants.ENCODING_CHECK,
      'tc_project' : 'NEED_TO_SET_tc_project_ATTRIBUTE',
      'output_all_resource_defines': 'true'
    }

  def EndParsing(self):
    super(GritNode, self).EndParsing()
    if (int(self.attrs['latest_public_release'])
        > int(self.attrs['current_release'])):
      raise exception.Parsing('latest_public_release cannot have a greater '
                              'value than current_release')

    self.ValidateUniqueIds()

    # Add the encoding check if it's not present (should ensure that it's always
    # present in all .grd files generated by GRIT). If it's present, assert if
    # it's not correct.
    if 'enc_check' not in self.attrs or self.attrs['enc_check'] == '':
      self.attrs['enc_check'] = constants.ENCODING_CHECK
    else:
      assert self.attrs['enc_check'] == constants.ENCODING_CHECK, (
        'Are you sure your .grd file is in the correct encoding (UTF-8)?')

  def ValidateUniqueIds(self):
    """Validate that 'name' attribute is unique in all nodes in this tree
    except for nodes that are children of <if> nodes.
    """
    unique_names = {}
    duplicate_names = []
    # To avoid false positives from mutually exclusive <if> clauses, check
    # against whatever the output condition happens to be right now.
    # TODO(benrg): do something better.
    for node in self.ActiveDescendants():
      if node.attrs.get('generateid', 'true') == 'false':
        continue  # Duplication not relevant in that case

      for node_id in node.GetTextualIds():
        if util.SYSTEM_IDENTIFIERS.match(node_id):
          continue  # predefined IDs are sometimes used more than once

        if node_id in unique_names and node_id not in duplicate_names:
          duplicate_names.append(node_id)
        unique_names[node_id] = 1

    if len(duplicate_names):
      raise exception.DuplicateKey(', '.join(duplicate_names))


  def GetCurrentRelease(self):
    """Returns the current release number."""
    return int(self.attrs['current_release'])

  def GetLatestPublicRelease(self):
    """Returns the latest public release number."""
    return int(self.attrs['latest_public_release'])

  def GetSourceLanguage(self):
    """Returns the language code of the source language."""
    return self.attrs['source_lang_id']

  def GetTcProject(self):
    """Returns the name of this project in the TranslationConsole, or
    'NEED_TO_SET_tc_project_ATTRIBUTE' if it is not defined."""
    return self.attrs['tc_project']

  def SetOwnDir(self, dir):
    """Informs the 'grit' element of the directory the file it is in resides.
    This allows it to calculate relative paths from the input file, which is
    what we desire (rather than from the current path).

    Args:
      dir: r'c:\bla'

    Return:
      None
    """
    assert dir
    self.base_dir = os.path.normpath(os.path.join(dir, self.attrs['base_dir']))

  def GetBaseDir(self):
    """Returns the base directory, relative to the working directory.  To get
    the base directory as set in the .grd file, use GetOriginalBaseDir()
    """
    if hasattr(self, 'base_dir'):
      return self.base_dir
    else:
      return self.GetOriginalBaseDir()

  def GetOriginalBaseDir(self):
    """Returns the base directory, as set in the .grd file.
    """
    return self.attrs['base_dir']

  def ShouldOutputAllResourceDefines(self):
    """Returns true if all resource defines should be output, false if
    defines for resources not emitted to resource files should be
    skipped.
    """
    return self.attrs['output_all_resource_defines'] == 'true'

  def GetInputFiles(self):
    """Returns the list of files that are read to produce the output."""

    # Importing this here avoids a circular dependency in the imports.
    # pylint: disable-msg=C6204
    from grit.node import include
    from grit.node import misc
    from grit.node import structure
    from grit.node import variant

    # Check if the input is required for any output configuration.
    input_files = set()
    old_output_language = self.output_language
    for lang, ctx in self.GetConfigurations():
      self.SetOutputLanguage(lang or self.GetSourceLanguage())
      self.SetOutputContext(ctx)
      for node in self.ActiveDescendants():
        if isinstance(node, (io.FileNode, include.IncludeNode, misc.PartNode,
                             structure.StructureNode, variant.SkeletonNode)):
          input_files.add(node.GetInputPath())
    self.SetOutputLanguage(old_output_language)
    return sorted(map(self.ToRealPath, input_files))

  def GetFirstIdsFile(self):
    """Returns a usable path to the first_ids file, if set, otherwise
    returns None.

    The first_ids_file attribute is by default relative to the
    base_dir of the .grd file, but may be prefixed by GRIT_DIR/,
    which makes it relative to the directory of grit.py
    (e.g. GRIT_DIR/../gritsettings/resource_ids).
    """
    if not self.attrs['first_ids_file']:
      return None

    path = self.attrs['first_ids_file']
    GRIT_DIR_PREFIX = 'GRIT_DIR'
    if (path.startswith(GRIT_DIR_PREFIX)
        and path[len(GRIT_DIR_PREFIX)] in ['/', '\\']):
      return util.PathFromRoot(path[len(GRIT_DIR_PREFIX) + 1:])
    else:
      return self.ToRealPath(path)

  def GetOutputFiles(self):
    """Returns the list of <output> nodes that are descendants of this node's
    <outputs> child and are not enclosed by unsatisfied <if> conditionals.
    """
    for child in self.children:
      if child.name == 'outputs':
        return [node for node in child.ActiveDescendants()
                     if node.name == 'output']
    raise exception.MissingElement()

  def GetConfigurations(self):
    """Returns the distinct (language, context) pairs from the output nodes.
    """
    return set((n.GetLanguage(), n.GetContext()) for n in self.GetOutputFiles())

  def GetSubstitutionMessages(self):
    """Returns the list of <message sub_variable="true"> nodes."""
    return [n for n in self.ActiveDescendants()
            if isinstance(n, message.MessageNode)
                and n.attrs['sub_variable'] == 'true']

  def SetOutputLanguage(self, output_language):
    """Set the output language. Prepares substitutions.

    The substitutions are reset every time the language is changed.
    They include messages designated as variables, and language codes for html
    and rc files.

    Args:
      output_language: a two-letter language code (eg: 'en', 'ar'...) or ''
    """
    if not output_language:
      # We do not specify the output language for .grh files,
      # so we get an empty string as the default.
      # The value should match grit.clique.MessageClique.source_language.
      output_language = self.GetSourceLanguage()
    if output_language != self.output_language:
      self.output_language = output_language
      self.substituter = None  # force recalculate

  def SetOutputContext(self, output_context):
    self.output_context = output_context
    self.substituter = None  # force recalculate

  def SetDefines(self, defines):
    self.defines = defines
    self.substituter = None  # force recalculate

  def SetTargetPlatform(self, target_platform):
    self.target_platform = target_platform

  def GetSubstituter(self):
    if self.substituter is None:
      self.substituter = util.Substituter()
      self.substituter.AddMessages(self.GetSubstitutionMessages(),
                                   self.output_language)
      if self.output_language in _RTL_LANGS:
        direction = 'dir="RTL"'
      else:
        direction = 'dir="LTR"'
      self.substituter.AddSubstitutions({
          'GRITLANGCODE': self.output_language,
          'GRITDIR': direction,
      })
      from grit.format import rc  # avoid circular dep
      rc.RcSubstitutions(self.substituter, self.output_language)
    return self.substituter

  def AssignFirstIds(self, filename_or_stream, defines):
    """Assign first ids to each grouping node based on values from the
    first_ids file (if specified on the <grit> node).
    """
    # If the input is a stream, then we're probably in a unit test and
    # should skip this step.
    if type(filename_or_stream) not in (str, unicode):
      return

    # Nothing to do if the first_ids_filename attribute isn't set.
    first_ids_filename = self.GetFirstIdsFile()
    if not first_ids_filename:
      return

    src_root_dir, first_ids = _ReadFirstIdsFromFile(first_ids_filename,
                                                    defines)
    from grit.node import empty
    for node in self.Preorder():
      if isinstance(node, empty.GroupingNode):
        abs_filename = os.path.abspath(filename_or_stream)
        if abs_filename[:len(src_root_dir)] != src_root_dir:
          filename = os.path.basename(filename_or_stream)
        else:
          filename = abs_filename[len(src_root_dir) + 1:]
          filename = filename.replace('\\', '/')

        if node.attrs['first_id'] != '':
          raise Exception(
              "Don't set the first_id attribute when using the first_ids_file "
              "attribute on the <grit> node, update %s instead." %
              first_ids_filename)

        try:
          id_list = first_ids[filename][node.name]
        except KeyError, e:
          print '-' * 78
          print 'Resource id not set for %s (%s)!' % (filename, node.name)
          print ('Please update %s to include an entry for %s.  See the '
                 'comments in resource_ids for information on why you need to '
                 'update that file.' % (first_ids_filename, filename))
          print '-' * 78
          raise e

        try:
          node.attrs['first_id'] = str(id_list.pop(0))
        except IndexError, e:
          raise Exception('Please update %s and add a first id for %s (%s).'
                          % (first_ids_filename, filename, node.name))

  def RunGatherers(self, debug=False):
    '''Call RunPreSubstitutionGatherer() on every node of the tree, then apply
    substitutions, then call RunPostSubstitutionGatherer() on every node.

    The substitutions step requires that the output language has been set.
    Locally, get the Substitution messages and add them to the substituter.
    Also add substitutions for language codes in the Rc.

    Args:
      debug: will print information while running gatherers.
    '''
    for node in self.ActiveDescendants():
      if hasattr(node, 'RunPreSubstitutionGatherer'):
        with node:
          node.RunPreSubstitutionGatherer(debug=debug)

    assert self.output_language
    self.SubstituteMessages(self.GetSubstituter())

    for node in self.ActiveDescendants():
      if hasattr(node, 'RunPostSubstitutionGatherer'):
        with node:
          node.RunPostSubstitutionGatherer(debug=debug)


class IdentifierNode(base.Node):
  """A node for specifying identifiers that should appear in the resource
  header file, and be unique amongst all other resource identifiers, but don't
  have any other attributes or reference any resources.
  """

  def MandatoryAttributes(self):
    return ['name']

  def DefaultAttributes(self):
    return { 'comment' : '', 'id' : '', 'systemid': 'false' }

  def GetId(self):
    """Returns the id of this identifier if it has one, None otherwise
    """
    if 'id' in self.attrs:
      return self.attrs['id']
    return None

  def EndParsing(self):
    """Handles system identifiers."""
    super(IdentifierNode, self).EndParsing()
    if self.attrs['systemid'] == 'true':
      util.SetupSystemIdentifiers((self.attrs['name'],))

  @staticmethod
  def Construct(parent, name, id, comment, systemid='false'):
    """Creates a new node which is a child of 'parent', with attributes set
    by parameters of the same name.
    """
    node = IdentifierNode()
    node.StartParsing('identifier', parent)
    node.HandleAttribute('name', name)
    node.HandleAttribute('id', id)
    node.HandleAttribute('comment', comment)
    node.HandleAttribute('systemid', systemid)
    node.EndParsing()
    return node