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