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"""Miscellaneous node types.
7"""
8
9import os.path
10import re
11import sys
12
13from grit import constants
14from grit import exception
15from grit import util
16import grit.format.rc_header
17from grit.node import base
18from grit.node import io
19from grit.node import message
20
21
22# RTL languages
23# TODO(jennyz): remove this fixed set of RTL language array
24# now that generic expand_variable code exists.
25_RTL_LANGS = (
26    'ar',  # Arabic
27    'fa',  # Farsi
28    'iw',  # Hebrew
29    'ks',  # Kashmiri
30    'ku',  # Kurdish
31    'ps',  # Pashto
32    'ur',  # Urdu
33    'yi',  # Yiddish
34)
35
36
37def _ReadFirstIdsFromFile(filename, defines):
38  """Read the starting resource id values from |filename|.  We also
39  expand variables of the form <(FOO) based on defines passed in on
40  the command line.
41
42  Returns a tuple, the absolute path of SRCDIR followed by the
43  first_ids dictionary.
44  """
45  first_ids_dict = eval(util.ReadFile(filename, util.RAW_TEXT))
46  src_root_dir = os.path.abspath(os.path.join(os.path.dirname(filename),
47                                              first_ids_dict['SRCDIR']))
48
49  def ReplaceVariable(matchobj):
50    for key, value in defines.iteritems():
51      if matchobj.group(1) == key:
52        value = os.path.abspath(value)[len(src_root_dir) + 1:]
53        return value
54    return ''
55
56  renames = []
57  for grd_filename in first_ids_dict:
58    new_grd_filename = re.sub(r'<\(([A-Za-z_]+)\)', ReplaceVariable,
59                              grd_filename)
60    if new_grd_filename != grd_filename:
61      new_grd_filename = new_grd_filename.replace('\\', '/')
62      renames.append((grd_filename, new_grd_filename))
63
64  for grd_filename, new_grd_filename in renames:
65    first_ids_dict[new_grd_filename] = first_ids_dict[grd_filename]
66    del(first_ids_dict[grd_filename])
67
68  return (src_root_dir, first_ids_dict)
69
70
71class SplicingNode(base.Node):
72  """A node whose children should be considered to be at the same level as
73  its siblings for most purposes. This includes <if> and <part> nodes.
74  """
75
76  def _IsValidChild(self, child):
77    assert self.parent, '<%s> node should never be root.' % self.name
78    if isinstance(child, SplicingNode):
79      return True  # avoid O(n^2) behavior
80    return self.parent._IsValidChild(child)
81
82
83class IfNode(SplicingNode):
84  """A node for conditional inclusion of resources.
85  """
86
87  def MandatoryAttributes(self):
88    return ['expr']
89
90  def _IsValidChild(self, child):
91    return (isinstance(child, (ThenNode, ElseNode)) or
92            super(IfNode, self)._IsValidChild(child))
93
94  def EndParsing(self):
95    children = self.children
96    self.if_then_else = False
97    if any(isinstance(node, (ThenNode, ElseNode)) for node in children):
98      if (len(children) != 2 or not isinstance(children[0], ThenNode) or
99                                not isinstance(children[1], ElseNode)):
100        raise exception.UnexpectedChild(
101            '<if> element must be <if><then>...</then><else>...</else></if>')
102      self.if_then_else = True
103
104  def ActiveChildren(self):
105    cond = self.EvaluateCondition(self.attrs['expr'])
106    if self.if_then_else:
107      return self.children[0 if cond else 1].ActiveChildren()
108    else:
109      # Equivalent to having all children inside <then> with an empty <else>
110      return super(IfNode, self).ActiveChildren() if cond else []
111
112
113class ThenNode(SplicingNode):
114  """A <then> node. Can only appear directly inside an <if> node."""
115  pass
116
117
118class ElseNode(SplicingNode):
119  """An <else> node. Can only appear directly inside an <if> node."""
120  pass
121
122
123class PartNode(SplicingNode):
124  """A node for inclusion of sub-grd (*.grp) files.
125  """
126
127  def __init__(self):
128    super(PartNode, self).__init__()
129    self.started_inclusion = False
130
131  def MandatoryAttributes(self):
132    return ['file']
133
134  def _IsValidChild(self, child):
135    return self.started_inclusion and super(PartNode, self)._IsValidChild(child)
136
137
138class ReleaseNode(base.Node):
139  """The <release> element."""
140
141  def _IsValidChild(self, child):
142    from grit.node import empty
143    return isinstance(child, (empty.IncludesNode, empty.MessagesNode,
144                              empty.StructuresNode, empty.IdentifiersNode))
145
146  def _IsValidAttribute(self, name, value):
147    return (
148      (name == 'seq' and int(value) <= self.GetRoot().GetCurrentRelease()) or
149      name == 'allow_pseudo'
150    )
151
152  def MandatoryAttributes(self):
153    return ['seq']
154
155  def DefaultAttributes(self):
156    return { 'allow_pseudo' : 'true' }
157
158  def GetReleaseNumber():
159    """Returns the sequence number of this release."""
160    return self.attribs['seq']
161
162class GritNode(base.Node):
163  """The <grit> root element."""
164
165  def __init__(self):
166    super(GritNode, self).__init__()
167    self.output_language = ''
168    self.defines = {}
169    self.substituter = None
170    self.target_platform = sys.platform
171
172  def _IsValidChild(self, child):
173    from grit.node import empty
174    return isinstance(child, (ReleaseNode, empty.TranslationsNode,
175                              empty.OutputsNode))
176
177  def _IsValidAttribute(self, name, value):
178    if name not in ['base_dir', 'first_ids_file', 'source_lang_id',
179                    'latest_public_release', 'current_release',
180                    'enc_check', 'tc_project', 'grit_version',
181                    'output_all_resource_defines']:
182      return False
183    if name in ['latest_public_release', 'current_release'] and value.strip(
184      '0123456789') != '':
185      return False
186    return True
187
188  def MandatoryAttributes(self):
189    return ['latest_public_release', 'current_release']
190
191  def DefaultAttributes(self):
192    return {
193      'base_dir' : '.',
194      'first_ids_file': '',
195      'grit_version': 1,
196      'source_lang_id' : 'en',
197      'enc_check' : constants.ENCODING_CHECK,
198      'tc_project' : 'NEED_TO_SET_tc_project_ATTRIBUTE',
199      'output_all_resource_defines': 'true'
200    }
201
202  def EndParsing(self):
203    super(GritNode, self).EndParsing()
204    if (int(self.attrs['latest_public_release'])
205        > int(self.attrs['current_release'])):
206      raise exception.Parsing('latest_public_release cannot have a greater '
207                              'value than current_release')
208
209    self.ValidateUniqueIds()
210
211    # Add the encoding check if it's not present (should ensure that it's always
212    # present in all .grd files generated by GRIT). If it's present, assert if
213    # it's not correct.
214    if 'enc_check' not in self.attrs or self.attrs['enc_check'] == '':
215      self.attrs['enc_check'] = constants.ENCODING_CHECK
216    else:
217      assert self.attrs['enc_check'] == constants.ENCODING_CHECK, (
218        'Are you sure your .grd file is in the correct encoding (UTF-8)?')
219
220  def ValidateUniqueIds(self):
221    """Validate that 'name' attribute is unique in all nodes in this tree
222    except for nodes that are children of <if> nodes.
223    """
224    unique_names = {}
225    duplicate_names = []
226    # To avoid false positives from mutually exclusive <if> clauses, check
227    # against whatever the output condition happens to be right now.
228    # TODO(benrg): do something better.
229    for node in self.ActiveDescendants():
230      if node.attrs.get('generateid', 'true') == 'false':
231        continue  # Duplication not relevant in that case
232
233      for node_id in node.GetTextualIds():
234        if util.SYSTEM_IDENTIFIERS.match(node_id):
235          continue  # predefined IDs are sometimes used more than once
236
237        if node_id in unique_names and node_id not in duplicate_names:
238          duplicate_names.append(node_id)
239        unique_names[node_id] = 1
240
241    if len(duplicate_names):
242      raise exception.DuplicateKey(', '.join(duplicate_names))
243
244
245  def GetCurrentRelease(self):
246    """Returns the current release number."""
247    return int(self.attrs['current_release'])
248
249  def GetLatestPublicRelease(self):
250    """Returns the latest public release number."""
251    return int(self.attrs['latest_public_release'])
252
253  def GetSourceLanguage(self):
254    """Returns the language code of the source language."""
255    return self.attrs['source_lang_id']
256
257  def GetTcProject(self):
258    """Returns the name of this project in the TranslationConsole, or
259    'NEED_TO_SET_tc_project_ATTRIBUTE' if it is not defined."""
260    return self.attrs['tc_project']
261
262  def SetOwnDir(self, dir):
263    """Informs the 'grit' element of the directory the file it is in resides.
264    This allows it to calculate relative paths from the input file, which is
265    what we desire (rather than from the current path).
266
267    Args:
268      dir: r'c:\bla'
269
270    Return:
271      None
272    """
273    assert dir
274    self.base_dir = os.path.normpath(os.path.join(dir, self.attrs['base_dir']))
275
276  def GetBaseDir(self):
277    """Returns the base directory, relative to the working directory.  To get
278    the base directory as set in the .grd file, use GetOriginalBaseDir()
279    """
280    if hasattr(self, 'base_dir'):
281      return self.base_dir
282    else:
283      return self.GetOriginalBaseDir()
284
285  def GetOriginalBaseDir(self):
286    """Returns the base directory, as set in the .grd file.
287    """
288    return self.attrs['base_dir']
289
290  def ShouldOutputAllResourceDefines(self):
291    """Returns true if all resource defines should be output, false if
292    defines for resources not emitted to resource files should be
293    skipped.
294    """
295    return self.attrs['output_all_resource_defines'] == 'true'
296
297  def GetInputFiles(self):
298    """Returns the list of files that are read to produce the output."""
299
300    # Importing this here avoids a circular dependency in the imports.
301    # pylint: disable-msg=C6204
302    from grit.node import include
303    from grit.node import misc
304    from grit.node import structure
305    from grit.node import variant
306
307    # Check if the input is required for any output configuration.
308    input_files = set()
309    old_output_language = self.output_language
310    for lang, ctx in self.GetConfigurations():
311      self.SetOutputLanguage(lang or self.GetSourceLanguage())
312      self.SetOutputContext(ctx)
313      for node in self.ActiveDescendants():
314        if isinstance(node, (io.FileNode, include.IncludeNode, misc.PartNode,
315                             structure.StructureNode, variant.SkeletonNode)):
316          input_files.add(node.GetInputPath())
317    self.SetOutputLanguage(old_output_language)
318    return sorted(map(self.ToRealPath, input_files))
319
320  def GetFirstIdsFile(self):
321    """Returns a usable path to the first_ids file, if set, otherwise
322    returns None.
323
324    The first_ids_file attribute is by default relative to the
325    base_dir of the .grd file, but may be prefixed by GRIT_DIR/,
326    which makes it relative to the directory of grit.py
327    (e.g. GRIT_DIR/../gritsettings/resource_ids).
328    """
329    if not self.attrs['first_ids_file']:
330      return None
331
332    path = self.attrs['first_ids_file']
333    GRIT_DIR_PREFIX = 'GRIT_DIR'
334    if (path.startswith(GRIT_DIR_PREFIX)
335        and path[len(GRIT_DIR_PREFIX)] in ['/', '\\']):
336      return util.PathFromRoot(path[len(GRIT_DIR_PREFIX) + 1:])
337    else:
338      return self.ToRealPath(path)
339
340  def GetOutputFiles(self):
341    """Returns the list of <output> nodes that are descendants of this node's
342    <outputs> child and are not enclosed by unsatisfied <if> conditionals.
343    """
344    for child in self.children:
345      if child.name == 'outputs':
346        return [node for node in child.ActiveDescendants()
347                     if node.name == 'output']
348    raise exception.MissingElement()
349
350  def GetConfigurations(self):
351    """Returns the distinct (language, context) pairs from the output nodes.
352    """
353    return set((n.GetLanguage(), n.GetContext()) for n in self.GetOutputFiles())
354
355  def GetSubstitutionMessages(self):
356    """Returns the list of <message sub_variable="true"> nodes."""
357    return [n for n in self.ActiveDescendants()
358            if isinstance(n, message.MessageNode)
359                and n.attrs['sub_variable'] == 'true']
360
361  def SetOutputLanguage(self, output_language):
362    """Set the output language. Prepares substitutions.
363
364    The substitutions are reset every time the language is changed.
365    They include messages designated as variables, and language codes for html
366    and rc files.
367
368    Args:
369      output_language: a two-letter language code (eg: 'en', 'ar'...) or ''
370    """
371    if not output_language:
372      # We do not specify the output language for .grh files,
373      # so we get an empty string as the default.
374      # The value should match grit.clique.MessageClique.source_language.
375      output_language = self.GetSourceLanguage()
376    if output_language != self.output_language:
377      self.output_language = output_language
378      self.substituter = None  # force recalculate
379
380  def SetOutputContext(self, output_context):
381    self.output_context = output_context
382    self.substituter = None  # force recalculate
383
384  def SetDefines(self, defines):
385    self.defines = defines
386    self.substituter = None  # force recalculate
387
388  def SetTargetPlatform(self, target_platform):
389    self.target_platform = target_platform
390
391  def GetSubstituter(self):
392    if self.substituter is None:
393      self.substituter = util.Substituter()
394      self.substituter.AddMessages(self.GetSubstitutionMessages(),
395                                   self.output_language)
396      if self.output_language in _RTL_LANGS:
397        direction = 'dir="RTL"'
398      else:
399        direction = 'dir="LTR"'
400      self.substituter.AddSubstitutions({
401          'GRITLANGCODE': self.output_language,
402          'GRITDIR': direction,
403      })
404      from grit.format import rc  # avoid circular dep
405      rc.RcSubstitutions(self.substituter, self.output_language)
406    return self.substituter
407
408  def AssignFirstIds(self, filename_or_stream, defines):
409    """Assign first ids to each grouping node based on values from the
410    first_ids file (if specified on the <grit> node).
411    """
412    # If the input is a stream, then we're probably in a unit test and
413    # should skip this step.
414    if type(filename_or_stream) not in (str, unicode):
415      return
416
417    # Nothing to do if the first_ids_filename attribute isn't set.
418    first_ids_filename = self.GetFirstIdsFile()
419    if not first_ids_filename:
420      return
421
422    src_root_dir, first_ids = _ReadFirstIdsFromFile(first_ids_filename,
423                                                    defines)
424    from grit.node import empty
425    for node in self.Preorder():
426      if isinstance(node, empty.GroupingNode):
427        abs_filename = os.path.abspath(filename_or_stream)
428        if abs_filename[:len(src_root_dir)] != src_root_dir:
429          filename = os.path.basename(filename_or_stream)
430        else:
431          filename = abs_filename[len(src_root_dir) + 1:]
432          filename = filename.replace('\\', '/')
433
434        if node.attrs['first_id'] != '':
435          raise Exception(
436              "Don't set the first_id attribute when using the first_ids_file "
437              "attribute on the <grit> node, update %s instead." %
438              first_ids_filename)
439
440        try:
441          id_list = first_ids[filename][node.name]
442        except KeyError, e:
443          print '-' * 78
444          print 'Resource id not set for %s (%s)!' % (filename, node.name)
445          print ('Please update %s to include an entry for %s.  See the '
446                 'comments in resource_ids for information on why you need to '
447                 'update that file.' % (first_ids_filename, filename))
448          print '-' * 78
449          raise e
450
451        try:
452          node.attrs['first_id'] = str(id_list.pop(0))
453        except IndexError, e:
454          raise Exception('Please update %s and add a first id for %s (%s).'
455                          % (first_ids_filename, filename, node.name))
456
457  def RunGatherers(self, debug=False):
458    '''Call RunPreSubstitutionGatherer() on every node of the tree, then apply
459    substitutions, then call RunPostSubstitutionGatherer() on every node.
460
461    The substitutions step requires that the output language has been set.
462    Locally, get the Substitution messages and add them to the substituter.
463    Also add substitutions for language codes in the Rc.
464
465    Args:
466      debug: will print information while running gatherers.
467    '''
468    for node in self.ActiveDescendants():
469      if hasattr(node, 'RunPreSubstitutionGatherer'):
470        with node:
471          node.RunPreSubstitutionGatherer(debug=debug)
472
473    assert self.output_language
474    self.SubstituteMessages(self.GetSubstituter())
475
476    for node in self.ActiveDescendants():
477      if hasattr(node, 'RunPostSubstitutionGatherer'):
478        with node:
479          node.RunPostSubstitutionGatherer(debug=debug)
480
481
482class IdentifierNode(base.Node):
483  """A node for specifying identifiers that should appear in the resource
484  header file, and be unique amongst all other resource identifiers, but don't
485  have any other attributes or reference any resources.
486  """
487
488  def MandatoryAttributes(self):
489    return ['name']
490
491  def DefaultAttributes(self):
492    return { 'comment' : '', 'id' : '', 'systemid': 'false' }
493
494  def GetId(self):
495    """Returns the id of this identifier if it has one, None otherwise
496    """
497    if 'id' in self.attrs:
498      return self.attrs['id']
499    return None
500
501  def EndParsing(self):
502    """Handles system identifiers."""
503    super(IdentifierNode, self).EndParsing()
504    if self.attrs['systemid'] == 'true':
505      util.SetupSystemIdentifiers((self.attrs['name'],))
506
507  @staticmethod
508  def Construct(parent, name, id, comment, systemid='false'):
509    """Creates a new node which is a child of 'parent', with attributes set
510    by parameters of the same name.
511    """
512    node = IdentifierNode()
513    node.StartParsing('identifier', parent)
514    node.HandleAttribute('name', name)
515    node.HandleAttribute('id', id)
516    node.HandleAttribute('comment', comment)
517    node.HandleAttribute('systemid', systemid)
518    node.EndParsing()
519    return node
520