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'''The <structure> element.
7'''
8
9import os
10import platform
11import re
12
13from grit import exception
14from grit import util
15from grit.node import base
16from grit.node import variant
17
18import grit.gather.admin_template
19import grit.gather.chrome_html
20import grit.gather.chrome_scaled_image
21import grit.gather.igoogle_strings
22import grit.gather.muppet_strings
23import grit.gather.policy_json
24import grit.gather.rc
25import grit.gather.tr_html
26import grit.gather.txt
27
28import grit.format.rc
29import grit.format.rc_header
30
31# Type of the gatherer to use for each type attribute
32_GATHERERS = {
33  'accelerators'        : grit.gather.rc.Accelerators,
34  'admin_template'      : grit.gather.admin_template.AdmGatherer,
35  'chrome_html'         : grit.gather.chrome_html.ChromeHtml,
36  'chrome_scaled_image' : grit.gather.chrome_scaled_image.ChromeScaledImage,
37  'dialog'              : grit.gather.rc.Dialog,
38  'igoogle'             : grit.gather.igoogle_strings.IgoogleStrings,
39  'menu'                : grit.gather.rc.Menu,
40  'muppet'              : grit.gather.muppet_strings.MuppetStrings,
41  'rcdata'              : grit.gather.rc.RCData,
42  'tr_html'             : grit.gather.tr_html.TrHtml,
43  'txt'                 : grit.gather.txt.TxtFile,
44  'version'             : grit.gather.rc.Version,
45  'policy_template_metafile' : grit.gather.policy_json.PolicyJson,
46}
47
48
49# TODO(joi) Print a warning if the 'variant_of_revision' attribute indicates
50# that a skeleton variant is older than the original file.
51
52
53class StructureNode(base.Node):
54  '''A <structure> element.'''
55
56  # Regular expression for a local variable definition.  Each definition
57  # is of the form NAME=VALUE, where NAME cannot contain '=' or ',' and
58  # VALUE must escape all commas: ',' -> ',,'.  Each variable definition
59  # should be separated by a comma with no extra whitespace.
60  # Example: THING1=foo,THING2=bar
61  variable_pattern = re.compile('([^,=\s]+)=((?:,,|[^,])*)')
62
63  def __init__(self):
64    super(StructureNode, self).__init__()
65
66    # Keep track of the last filename we flattened to, so we can
67    # avoid doing it more than once.
68    self._last_flat_filename = None
69
70    # See _Substitute; this substituter is used for local variables and
71    # the root substituter is used for global variables.
72    self.substituter = None
73
74  def _IsValidChild(self, child):
75    return isinstance(child, variant.SkeletonNode)
76
77  def _ParseVariables(self, variables):
78    '''Parse a variable string into a dictionary.'''
79    matches = StructureNode.variable_pattern.findall(variables)
80    return dict((name, value.replace(',,', ',')) for name, value in matches)
81
82  def EndParsing(self):
83    super(StructureNode, self).EndParsing()
84
85    # Now that we have attributes and children, instantiate the gatherers.
86    gathertype = _GATHERERS[self.attrs['type']]
87
88    self.gatherer = gathertype(self.attrs['file'],
89                               self.attrs['name'],
90                               self.attrs['encoding'])
91    self.gatherer.SetGrdNode(self)
92    self.gatherer.SetUberClique(self.UberClique())
93    if hasattr(self.GetRoot(), 'defines'):
94      self.gatherer.SetDefines(self.GetRoot().defines)
95    self.gatherer.SetAttributes(self.attrs)
96    if self.ExpandVariables():
97      self.gatherer.SetFilenameExpansionFunction(self._Substitute)
98
99    # Parse local variables and instantiate the substituter.
100    if self.attrs['variables']:
101      variables = self.attrs['variables']
102      self.substituter = util.Substituter()
103      self.substituter.AddSubstitutions(self._ParseVariables(variables))
104
105    self.skeletons = {}  # Maps expressions to skeleton gatherers
106    for child in self.children:
107      assert isinstance(child, variant.SkeletonNode)
108      skel = gathertype(child.attrs['file'],
109                        self.attrs['name'],
110                        child.GetEncodingToUse(),
111                        is_skeleton=True)
112      skel.SetGrdNode(self)  # TODO(benrg): Or child? Only used for ToRealPath
113      skel.SetUberClique(self.UberClique())
114      if hasattr(self.GetRoot(), 'defines'):
115        skel.SetDefines(self.GetRoot().defines)
116      if self.ExpandVariables():
117        skel.SetFilenameExpansionFunction(self._Substitute)
118      self.skeletons[child.attrs['expr']] = skel
119
120  def MandatoryAttributes(self):
121    return ['type', 'name', 'file']
122
123  def DefaultAttributes(self):
124    return { 'encoding' : 'cp1252',
125             'exclude_from_rc' : 'false',
126             'line_end' : 'unix',
127             'output_encoding' : 'utf-8',
128             'generateid': 'true',
129             'expand_variables' : 'false',
130             'output_filename' : '',
131             'fold_whitespace': 'false',
132             # Run an arbitrary command after translation is complete
133             # so that it doesn't interfere with what's in translation
134             # console.
135             'run_command' : '',
136             # Leave empty to run on all platforms, comma-separated
137             # for one or more specific platforms. Values must match
138             # output of platform.system().
139             'run_command_on_platforms' : '',
140             'allowexternalscript': 'false',
141             'flattenhtml': 'false',
142             'fallback_to_low_resolution': 'default',
143             # TODO(joi) this is a hack - should output all generated files
144             # as SCons dependencies; however, for now there is a bug I can't
145             # find where GRIT doesn't build the matching fileset, therefore
146             # this hack so that only the files you really need are marked as
147             # dependencies.
148             'sconsdep' : 'false',
149             'variables': '',
150             }
151
152  def IsExcludedFromRc(self):
153    return self.attrs['exclude_from_rc'] == 'true'
154
155  def Process(self, output_dir):
156    """Writes the processed data to output_dir.  In the case of a chrome_html
157    structure this will add references to other scale factors.  If flattening
158    this will also write file references to be base64 encoded data URLs.  The
159    name of the new file is returned."""
160    filename = self.ToRealPath(self.GetInputPath())
161    flat_filename = os.path.join(output_dir,
162        self.attrs['name'] + '_' + os.path.basename(filename))
163
164    if self._last_flat_filename == flat_filename:
165      return
166
167    with open(flat_filename, 'wb') as outfile:
168      if self.ExpandVariables():
169        text = self.gatherer.GetText()
170        file_contents = self._Substitute(text).encode('utf-8')
171      else:
172        file_contents = self.gatherer.GetData('', 'utf-8')
173      outfile.write(file_contents)
174
175    self._last_flat_filename = flat_filename
176    return os.path.basename(flat_filename)
177
178  def GetLineEnd(self):
179    '''Returns the end-of-line character or characters for files output because
180    of this node ('\r\n', '\n', or '\r' depending on the 'line_end' attribute).
181    '''
182    if self.attrs['line_end'] == 'unix':
183      return '\n'
184    elif self.attrs['line_end'] == 'windows':
185      return '\r\n'
186    elif self.attrs['line_end'] == 'mac':
187      return '\r'
188    else:
189      raise exception.UnexpectedAttribute(
190        "Attribute 'line_end' must be one of 'unix' (default), 'windows' or 'mac'")
191
192  def GetCliques(self):
193    return self.gatherer.GetCliques()
194
195  def GetDataPackPair(self, lang, encoding):
196    """Returns a (id, string|None) pair that represents the resource id and raw
197    bytes of the data (or None if no resource is generated).  This is used to
198    generate the data pack data file.
199    """
200    from grit.format import rc_header
201    id_map = rc_header.GetIds(self.GetRoot())
202    id = id_map[self.GetTextualIds()[0]]
203    if self.ExpandVariables():
204      text = self.gatherer.GetText()
205      return id, util.Encode(self._Substitute(text), encoding)
206    return id, self.gatherer.GetData(lang, encoding)
207
208  def GetHtmlResourceFilenames(self):
209    """Returns a set of all filenames inlined by this node."""
210    return self.gatherer.GetHtmlResourceFilenames()
211
212  def GetInputPath(self):
213    return self.gatherer.GetInputPath()
214
215  def GetTextualIds(self):
216    if not hasattr(self, 'gatherer'):
217      # This case is needed because this method is called by
218      # GritNode.ValidateUniqueIds before RunGatherers has been called.
219      # TODO(benrg): Fix this?
220      return [self.attrs['name']]
221    return self.gatherer.GetTextualIds()
222
223  def RunPreSubstitutionGatherer(self, debug=False):
224    if debug:
225      print 'Running gatherer %s for file %s' % (
226          str(type(self.gatherer)), self.GetInputPath())
227
228    # Note: Parse() is idempotent, therefore this method is also.
229    self.gatherer.Parse()
230    for skel in self.skeletons.values():
231      skel.Parse()
232
233  def GetSkeletonGatherer(self):
234    '''Returns the gatherer for the alternate skeleton that should be used,
235    based on the expressions for selecting skeletons, or None if the skeleton
236    from the English version of the structure should be used.
237    '''
238    for expr in self.skeletons:
239      if self.EvaluateCondition(expr):
240        return self.skeletons[expr]
241    return None
242
243  def HasFileForLanguage(self):
244    return self.attrs['type'] in ['tr_html', 'admin_template', 'txt',
245                                  'muppet', 'igoogle', 'chrome_scaled_image',
246                                  'chrome_html']
247
248  def ExpandVariables(self):
249    '''Variable expansion on structures is controlled by an XML attribute.
250
251    However, old files assume that expansion is always on for Rc files.
252
253    Returns:
254      A boolean.
255    '''
256    attrs = self.GetRoot().attrs
257    if 'grit_version' in attrs and attrs['grit_version'] > 1:
258      return self.attrs['expand_variables'] == 'true'
259    else:
260      return (self.attrs['expand_variables'] == 'true' or
261              self.attrs['file'].lower().endswith('.rc'))
262
263  def _Substitute(self, text):
264    '''Perform local and global variable substitution.'''
265    if self.substituter:
266      text = self.substituter.Substitute(text)
267    return self.GetRoot().GetSubstituter().Substitute(text)
268
269  def RunCommandOnCurrentPlatform(self):
270    if self.attrs['run_command_on_platforms'] == '':
271      return True
272    else:
273      target_platforms = self.attrs['run_command_on_platforms'].split(',')
274      return platform.system() in target_platforms
275
276  def FileForLanguage(self, lang, output_dir, create_file=True,
277                      return_if_not_generated=True):
278    '''Returns the filename of the file associated with this structure,
279    for the specified language.
280
281    Args:
282      lang: 'fr'
283      output_dir: 'c:\temp'
284      create_file: True
285    '''
286    assert self.HasFileForLanguage()
287    # If the source language is requested, and no extra changes are requested,
288    # use the existing file.
289    if ((not lang or lang == self.GetRoot().GetSourceLanguage()) and
290        self.attrs['expand_variables'] != 'true' and
291        (not self.attrs['run_command'] or
292         not self.RunCommandOnCurrentPlatform())):
293      if return_if_not_generated:
294        return self.ToRealPath(self.GetInputPath())
295      else:
296        return None
297
298    if self.attrs['output_filename'] != '':
299      filename = self.attrs['output_filename']
300    else:
301      filename = os.path.basename(self.attrs['file'])
302    assert len(filename)
303    filename = '%s_%s' % (lang, filename)
304    filename = os.path.join(output_dir, filename)
305
306    # Only create the output if it was requested by the call.
307    if create_file:
308      text = self.gatherer.Translate(
309          lang,
310          pseudo_if_not_available=self.PseudoIsAllowed(),
311          fallback_to_english=self.ShouldFallbackToEnglish(),
312          skeleton_gatherer=self.GetSkeletonGatherer())
313
314      file_contents = util.FixLineEnd(text, self.GetLineEnd())
315      if self.ExpandVariables():
316        # Note that we reapply substitution a second time here.
317        # This is because a) we need to look inside placeholders
318        # b) the substitution values are language-dependent
319        file_contents = self._Substitute(file_contents)
320
321      with open(filename, 'wb') as file_object:
322        output_stream = util.WrapOutputStream(file_object,
323                                              self.attrs['output_encoding'])
324        output_stream.write(file_contents)
325
326      if self.attrs['run_command'] and self.RunCommandOnCurrentPlatform():
327        # Run arbitrary commands after translation is complete so that it
328        # doesn't interfere with what's in translation console.
329        command = self.attrs['run_command'] % {'filename': filename}
330        result = os.system(command)
331        assert result == 0, '"%s" failed.' % command
332
333    return filename
334
335  def IsResourceMapSource(self):
336    return True
337
338  def GeneratesResourceMapEntry(self, output_all_resource_defines,
339                                is_active_descendant):
340    if output_all_resource_defines:
341      return True
342    return is_active_descendant
343
344  @staticmethod
345  def Construct(parent, name, type, file, encoding='cp1252'):
346    '''Creates a new node which is a child of 'parent', with attributes set
347    by parameters of the same name.
348    '''
349    node = StructureNode()
350    node.StartParsing('structure', parent)
351    node.HandleAttribute('name', name)
352    node.HandleAttribute('type', type)
353    node.HandleAttribute('file', file)
354    node.HandleAttribute('encoding', encoding)
355    node.EndParsing()
356    return node
357
358  def SubstituteMessages(self, substituter):
359    '''Propagates substitution to gatherer.
360
361    Args:
362      substituter: a grit.util.Substituter object.
363    '''
364    assert hasattr(self, 'gatherer')
365    if self.ExpandVariables():
366      self.gatherer.SubstituteMessages(substituter)
367
368