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.
6 """Miscellaneous node types.
13 from grit import constants
14 from grit import exception
16 import grit.format.rc_header
17 from grit.node import base
18 from grit.node import io
19 from grit.node import message
23 # TODO(jennyz): remove this fixed set of RTL language array
24 # now that generic expand_variable code exists.
37 def _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
42 Returns a tuple, the absolute path of SRCDIR followed by the
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']))
49 def ReplaceVariable(matchobj):
50 for key, value in defines.iteritems():
51 if matchobj.group(1) == key:
56 for grd_filename in first_ids_dict:
57 new_grd_filename = re.sub(r'<\(([A-Za-z_]+)\)', ReplaceVariable,
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)
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))
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])
72 return (src_root_dir, first_ids_dict)
75 class 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.
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)
87 class IfNode(SplicingNode):
88 """A node for conditional inclusion of resources.
91 def MandatoryAttributes(self):
94 def _IsValidChild(self, child):
95 return (isinstance(child, (ThenNode, ElseNode)) or
96 super(IfNode, self)._IsValidChild(child))
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
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()
113 # Equivalent to having all children inside <then> with an empty <else>
114 return super(IfNode, self).ActiveChildren() if cond else []
117 class ThenNode(SplicingNode):
118 """A <then> node. Can only appear directly inside an <if> node."""
122 class ElseNode(SplicingNode):
123 """An <else> node. Can only appear directly inside an <if> node."""
127 class PartNode(SplicingNode):
128 """A node for inclusion of sub-grd (*.grp) files.
132 super(PartNode, self).__init__()
133 self.started_inclusion = False
135 def MandatoryAttributes(self):
138 def _IsValidChild(self, child):
139 return self.started_inclusion and super(PartNode, self)._IsValidChild(child)
142 class ReleaseNode(base.Node):
143 """The <release> element."""
145 def _IsValidChild(self, child):
146 from grit.node import empty
147 return isinstance(child, (empty.IncludesNode, empty.MessagesNode,
148 empty.StructuresNode, empty.IdentifiersNode))
150 def _IsValidAttribute(self, name, value):
152 (name == 'seq' and int(value) <= self.GetRoot().GetCurrentRelease()) or
153 name == 'allow_pseudo'
156 def MandatoryAttributes(self):
159 def DefaultAttributes(self):
160 return { 'allow_pseudo' : 'true' }
162 def GetReleaseNumber():
163 """Returns the sequence number of this release."""
164 return self.attribs['seq']
166 class GritNode(base.Node):
167 """The <grit> root element."""
170 super(GritNode, self).__init__()
171 self.output_language = ''
173 self.substituter = None
174 self.target_platform = sys.platform
176 def _IsValidChild(self, child):
177 from grit.node import empty
178 return isinstance(child, (ReleaseNode, empty.TranslationsNode,
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']:
187 if name in ['latest_public_release', 'current_release'] and value.strip(
192 def MandatoryAttributes(self):
193 return ['latest_public_release', 'current_release']
195 def DefaultAttributes(self):
198 'first_ids_file': '',
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
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')
214 self.ValidateUniqueIds()
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
219 if 'enc_check' not in self.attrs or self.attrs['enc_check'] == '':
220 self.attrs['enc_check'] = constants.ENCODING_CHECK
222 assert self.attrs['enc_check'] == constants.ENCODING_CHECK, (
223 'Are you sure your .grd file is in the correct encoding (UTF-8)?')
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.
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
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
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
246 if len(duplicate_names):
247 raise exception.DuplicateKey(', '.join(duplicate_names))
250 def GetCurrentRelease(self):
251 """Returns the current release number."""
252 return int(self.attrs['current_release'])
254 def GetLatestPublicRelease(self):
255 """Returns the latest public release number."""
256 return int(self.attrs['latest_public_release'])
258 def GetSourceLanguage(self):
259 """Returns the language code of the source language."""
260 return self.attrs['source_lang_id']
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']
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).
279 self.base_dir = os.path.normpath(os.path.join(dir, self.attrs['base_dir']))
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()
285 if hasattr(self, 'base_dir'):
288 return self.GetOriginalBaseDir()
290 def GetOriginalBaseDir(self):
291 """Returns the base directory, as set in the .grd file.
293 return self.attrs['base_dir']
295 def SetShouldOutputAllResourceDefines(self, value):
296 """Overrides the value of output_all_resource_defines found in the grd file.
298 self.attrs['output_all_resource_defines'] = 'true' if value else 'false'
300 def ShouldOutputAllResourceDefines(self):
301 """Returns true if all resource defines should be output, false if
302 defines for resources not emitted to resource files should be
305 return self.attrs['output_all_resource_defines'] == 'true'
307 def GetRcHeaderFormat(self):
308 return self.attrs['rc_header_format']
310 def AssignRcHeaderFormat(self, rc_header_format):
311 self.attrs['rc_header_format'] = rc_header_format
313 def GetInputFiles(self):
314 """Returns the list of files that are read to produce the output."""
316 # Importing this here avoids a circular dependency in the imports.
317 # pylint: disable-msg=C6204
318 from grit.node import include
319 from grit.node import misc
320 from grit.node import structure
321 from grit.node import variant
323 # Check if the input is required for any output configuration.
325 old_output_language = self.output_language
326 for lang, ctx in self.GetConfigurations():
327 self.SetOutputLanguage(lang or self.GetSourceLanguage())
328 self.SetOutputContext(ctx)
329 for node in self.ActiveDescendants():
330 if isinstance(node, (io.FileNode, include.IncludeNode, misc.PartNode,
331 structure.StructureNode, variant.SkeletonNode)):
332 input_files.add(node.GetInputPath())
333 self.SetOutputLanguage(old_output_language)
334 return sorted(map(self.ToRealPath, input_files))
336 def GetFirstIdsFile(self):
337 """Returns a usable path to the first_ids file, if set, otherwise
340 The first_ids_file attribute is by default relative to the
341 base_dir of the .grd file, but may be prefixed by GRIT_DIR/,
342 which makes it relative to the directory of grit.py
343 (e.g. GRIT_DIR/../gritsettings/resource_ids).
345 if not self.attrs['first_ids_file']:
348 path = self.attrs['first_ids_file']
349 GRIT_DIR_PREFIX = 'GRIT_DIR'
350 if (path.startswith(GRIT_DIR_PREFIX)
351 and path[len(GRIT_DIR_PREFIX)] in ['/', '\\']):
352 return util.PathFromRoot(path[len(GRIT_DIR_PREFIX) + 1:])
354 return self.ToRealPath(path)
356 def GetOutputFiles(self):
357 """Returns the list of <output> nodes that are descendants of this node's
358 <outputs> child and are not enclosed by unsatisfied <if> conditionals.
360 for child in self.children:
361 if child.name == 'outputs':
362 return [node for node in child.ActiveDescendants()
363 if node.name == 'output']
364 raise exception.MissingElement()
366 def GetConfigurations(self):
367 """Returns the distinct (language, context) pairs from the output nodes.
369 return set((n.GetLanguage(), n.GetContext()) for n in self.GetOutputFiles())
371 def GetSubstitutionMessages(self):
372 """Returns the list of <message sub_variable="true"> nodes."""
373 return [n for n in self.ActiveDescendants()
374 if isinstance(n, message.MessageNode)
375 and n.attrs['sub_variable'] == 'true']
377 def SetOutputLanguage(self, output_language):
378 """Set the output language. Prepares substitutions.
380 The substitutions are reset every time the language is changed.
381 They include messages designated as variables, and language codes for html
385 output_language: a two-letter language code (eg: 'en', 'ar'...) or ''
387 if not output_language:
388 # We do not specify the output language for .grh files,
389 # so we get an empty string as the default.
390 # The value should match grit.clique.MessageClique.source_language.
391 output_language = self.GetSourceLanguage()
392 if output_language != self.output_language:
393 self.output_language = output_language
394 self.substituter = None # force recalculate
396 def SetOutputContext(self, output_context):
397 self.output_context = output_context
398 self.substituter = None # force recalculate
400 def SetDefines(self, defines):
401 self.defines = defines
402 self.substituter = None # force recalculate
404 def SetTargetPlatform(self, target_platform):
405 self.target_platform = target_platform
407 def GetSubstituter(self):
408 if self.substituter is None:
409 self.substituter = util.Substituter()
410 self.substituter.AddMessages(self.GetSubstitutionMessages(),
411 self.output_language)
412 if self.output_language in _RTL_LANGS:
413 direction = 'dir="RTL"'
415 direction = 'dir="LTR"'
416 self.substituter.AddSubstitutions({
417 'GRITLANGCODE': self.output_language,
418 'GRITDIR': direction,
420 from grit.format import rc # avoid circular dep
421 rc.RcSubstitutions(self.substituter, self.output_language)
422 return self.substituter
424 def AssignFirstIds(self, filename_or_stream, defines):
425 """Assign first ids to each grouping node based on values from the
426 first_ids file (if specified on the <grit> node).
428 # If the input is a stream, then we're probably in a unit test and
429 # should skip this step.
430 if type(filename_or_stream) not in (str, unicode):
433 # Nothing to do if the first_ids_filename attribute isn't set.
434 first_ids_filename = self.GetFirstIdsFile()
435 if not first_ids_filename:
438 src_root_dir, first_ids = _ReadFirstIdsFromFile(first_ids_filename,
440 from grit.node import empty
441 for node in self.Preorder():
442 if isinstance(node, empty.GroupingNode):
443 abs_filename = os.path.abspath(filename_or_stream)
444 if abs_filename[:len(src_root_dir)] != src_root_dir:
445 filename = os.path.basename(filename_or_stream)
447 filename = abs_filename[len(src_root_dir) + 1:]
448 filename = filename.replace('\\', '/')
450 if node.attrs['first_id'] != '':
452 "Don't set the first_id attribute when using the first_ids_file "
453 "attribute on the <grit> node, update %s instead." %
457 id_list = first_ids[filename][node.name]
460 print 'Resource id not set for %s (%s)!' % (filename, node.name)
461 print ('Please update %s to include an entry for %s. See the '
462 'comments in resource_ids for information on why you need to '
463 'update that file.' % (first_ids_filename, filename))
468 node.attrs['first_id'] = str(id_list.pop(0))
469 except IndexError, e:
470 raise Exception('Please update %s and add a first id for %s (%s).'
471 % (first_ids_filename, filename, node.name))
473 def RunGatherers(self, debug=False):
474 '''Call RunPreSubstitutionGatherer() on every node of the tree, then apply
475 substitutions, then call RunPostSubstitutionGatherer() on every node.
477 The substitutions step requires that the output language has been set.
478 Locally, get the Substitution messages and add them to the substituter.
479 Also add substitutions for language codes in the Rc.
482 debug: will print information while running gatherers.
484 for node in self.ActiveDescendants():
485 if hasattr(node, 'RunPreSubstitutionGatherer'):
487 node.RunPreSubstitutionGatherer(debug=debug)
489 assert self.output_language
490 self.SubstituteMessages(self.GetSubstituter())
492 for node in self.ActiveDescendants():
493 if hasattr(node, 'RunPostSubstitutionGatherer'):
495 node.RunPostSubstitutionGatherer(debug=debug)
498 class IdentifierNode(base.Node):
499 """A node for specifying identifiers that should appear in the resource
500 header file, and be unique amongst all other resource identifiers, but don't
501 have any other attributes or reference any resources.
504 def MandatoryAttributes(self):
507 def DefaultAttributes(self):
508 return { 'comment' : '', 'id' : '', 'systemid': 'false' }
511 """Returns the id of this identifier if it has one, None otherwise
513 if 'id' in self.attrs:
514 return self.attrs['id']
517 def EndParsing(self):
518 """Handles system identifiers."""
519 super(IdentifierNode, self).EndParsing()
520 if self.attrs['systemid'] == 'true':
521 util.SetupSystemIdentifiers((self.attrs['name'],))
524 def Construct(parent, name, id, comment, systemid='false'):
525 """Creates a new node which is a child of 'parent', with attributes set
526 by parameters of the same name.
528 node = IdentifierNode()
529 node.StartParsing('identifier', parent)
530 node.HandleAttribute('name', name)
531 node.HandleAttribute('id', id)
532 node.HandleAttribute('comment', comment)
533 node.HandleAttribute('systemid', systemid)