OLD | NEW |
| (Empty) |
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 | |
9 import os.path | |
10 import re | |
11 import sys | |
12 | |
13 from grit import constants | |
14 from grit import exception | |
15 from grit import util | |
16 import grit.format.rc_header | |
17 from grit.node import base | |
18 from grit.node import io | |
19 from 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 | |
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 | |
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 | |
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. | |
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 | |
87 class 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 | |
117 class ThenNode(SplicingNode): | |
118 """A <then> node. Can only appear directly inside an <if> node.""" | |
119 pass | |
120 | |
121 | |
122 class ElseNode(SplicingNode): | |
123 """An <else> node. Can only appear directly inside an <if> node.""" | |
124 pass | |
125 | |
126 | |
127 class 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 | |
142 class 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 | |
166 class 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 SetShouldOutputAllResourceDefines(self, value): | |
296 """Overrides the value of output_all_resource_defines found in the grd file. | |
297 """ | |
298 self.attrs['output_all_resource_defines'] = 'true' if value else 'false' | |
299 | |
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 | |
303 skipped. | |
304 """ | |
305 return self.attrs['output_all_resource_defines'] == 'true' | |
306 | |
307 def GetRcHeaderFormat(self): | |
308 return self.attrs['rc_header_format'] | |
309 | |
310 def AssignRcHeaderFormat(self, rc_header_format): | |
311 self.attrs['rc_header_format'] = rc_header_format | |
312 | |
313 def GetInputFiles(self): | |
314 """Returns the list of files that are read to produce the output.""" | |
315 | |
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 | |
322 | |
323 # Check if the input is required for any output configuration. | |
324 input_files = set() | |
325 old_output_language = self.output_language | |
326 for lang, ctx, fallback in self.GetConfigurations(): | |
327 self.SetOutputLanguage(lang or self.GetSourceLanguage()) | |
328 self.SetOutputContext(ctx) | |
329 self.SetFallbackToDefaultLayout(fallback) | |
330 | |
331 for node in self.ActiveDescendants(): | |
332 if isinstance(node, (io.FileNode, include.IncludeNode, misc.PartNode, | |
333 structure.StructureNode, variant.SkeletonNode)): | |
334 input_path = node.GetInputPath() | |
335 if input_path is not None: | |
336 input_files.add(self.ToRealPath(input_path)) | |
337 | |
338 # If it's a flattened node, grab inlined resources too. | |
339 if ((node.name == 'structure' or node.name == 'include') | |
340 and node.attrs['flattenhtml'] == 'true'): | |
341 if node.name == 'structure': | |
342 node.RunPreSubstitutionGatherer() | |
343 input_files.update(node.GetHtmlResourceFilenames()) | |
344 | |
345 self.SetOutputLanguage(old_output_language) | |
346 return sorted(input_files) | |
347 | |
348 def GetFirstIdsFile(self): | |
349 """Returns a usable path to the first_ids file, if set, otherwise | |
350 returns None. | |
351 | |
352 The first_ids_file attribute is by default relative to the | |
353 base_dir of the .grd file, but may be prefixed by GRIT_DIR/, | |
354 which makes it relative to the directory of grit.py | |
355 (e.g. GRIT_DIR/../gritsettings/resource_ids). | |
356 """ | |
357 if not self.attrs['first_ids_file']: | |
358 return None | |
359 | |
360 path = self.attrs['first_ids_file'] | |
361 GRIT_DIR_PREFIX = 'GRIT_DIR' | |
362 if (path.startswith(GRIT_DIR_PREFIX) | |
363 and path[len(GRIT_DIR_PREFIX)] in ['/', '\\']): | |
364 return util.PathFromRoot(path[len(GRIT_DIR_PREFIX) + 1:]) | |
365 else: | |
366 return self.ToRealPath(path) | |
367 | |
368 def GetOutputFiles(self): | |
369 """Returns the list of <output> nodes that are descendants of this node's | |
370 <outputs> child and are not enclosed by unsatisfied <if> conditionals. | |
371 """ | |
372 for child in self.children: | |
373 if child.name == 'outputs': | |
374 return [node for node in child.ActiveDescendants() | |
375 if node.name == 'output'] | |
376 raise exception.MissingElement() | |
377 | |
378 def GetConfigurations(self): | |
379 """Returns the distinct (language, context, fallback_to_default_layout) | |
380 triples from the output nodes. | |
381 """ | |
382 return set((n.GetLanguage(), n.GetContext(), n.GetFallbackToDefaultLayout())
for n in self.GetOutputFiles()) | |
383 | |
384 def GetSubstitutionMessages(self): | |
385 """Returns the list of <message sub_variable="true"> nodes.""" | |
386 return [n for n in self.ActiveDescendants() | |
387 if isinstance(n, message.MessageNode) | |
388 and n.attrs['sub_variable'] == 'true'] | |
389 | |
390 def SetOutputLanguage(self, output_language): | |
391 """Set the output language. Prepares substitutions. | |
392 | |
393 The substitutions are reset every time the language is changed. | |
394 They include messages designated as variables, and language codes for html | |
395 and rc files. | |
396 | |
397 Args: | |
398 output_language: a two-letter language code (eg: 'en', 'ar'...) or '' | |
399 """ | |
400 if not output_language: | |
401 # We do not specify the output language for .grh files, | |
402 # so we get an empty string as the default. | |
403 # The value should match grit.clique.MessageClique.source_language. | |
404 output_language = self.GetSourceLanguage() | |
405 if output_language != self.output_language: | |
406 self.output_language = output_language | |
407 self.substituter = None # force recalculate | |
408 | |
409 def SetOutputContext(self, output_context): | |
410 self.output_context = output_context | |
411 self.substituter = None # force recalculate | |
412 | |
413 def SetFallbackToDefaultLayout(self, fallback_to_default_layout): | |
414 self.fallback_to_default_layout = fallback_to_default_layout | |
415 self.substituter = None # force recalculate | |
416 | |
417 def SetDefines(self, defines): | |
418 self.defines = defines | |
419 self.substituter = None # force recalculate | |
420 | |
421 def SetTargetPlatform(self, target_platform): | |
422 self.target_platform = target_platform | |
423 | |
424 def GetSubstituter(self): | |
425 if self.substituter is None: | |
426 self.substituter = util.Substituter() | |
427 self.substituter.AddMessages(self.GetSubstitutionMessages(), | |
428 self.output_language) | |
429 if self.output_language in _RTL_LANGS: | |
430 direction = 'dir="RTL"' | |
431 else: | |
432 direction = 'dir="LTR"' | |
433 self.substituter.AddSubstitutions({ | |
434 'GRITLANGCODE': self.output_language, | |
435 'GRITDIR': direction, | |
436 }) | |
437 from grit.format import rc # avoid circular dep | |
438 rc.RcSubstitutions(self.substituter, self.output_language) | |
439 return self.substituter | |
440 | |
441 def AssignFirstIds(self, filename_or_stream, defines): | |
442 """Assign first ids to each grouping node based on values from the | |
443 first_ids file (if specified on the <grit> node). | |
444 """ | |
445 # If the input is a stream, then we're probably in a unit test and | |
446 # should skip this step. | |
447 if type(filename_or_stream) not in (str, unicode): | |
448 return | |
449 | |
450 # Nothing to do if the first_ids_filename attribute isn't set. | |
451 first_ids_filename = self.GetFirstIdsFile() | |
452 if not first_ids_filename: | |
453 return | |
454 | |
455 src_root_dir, first_ids = _ReadFirstIdsFromFile(first_ids_filename, | |
456 defines) | |
457 from grit.node import empty | |
458 for node in self.Preorder(): | |
459 if isinstance(node, empty.GroupingNode): | |
460 abs_filename = os.path.abspath(filename_or_stream) | |
461 if abs_filename[:len(src_root_dir)] != src_root_dir: | |
462 filename = os.path.basename(filename_or_stream) | |
463 else: | |
464 filename = abs_filename[len(src_root_dir) + 1:] | |
465 filename = filename.replace('\\', '/') | |
466 | |
467 if node.attrs['first_id'] != '': | |
468 raise Exception( | |
469 "Don't set the first_id attribute when using the first_ids_file " | |
470 "attribute on the <grit> node, update %s instead." % | |
471 first_ids_filename) | |
472 | |
473 try: | |
474 id_list = first_ids[filename][node.name] | |
475 except KeyError, e: | |
476 print '-' * 78 | |
477 print 'Resource id not set for %s (%s)!' % (filename, node.name) | |
478 print ('Please update %s to include an entry for %s. See the ' | |
479 'comments in resource_ids for information on why you need to ' | |
480 'update that file.' % (first_ids_filename, filename)) | |
481 print '-' * 78 | |
482 raise e | |
483 | |
484 try: | |
485 node.attrs['first_id'] = str(id_list.pop(0)) | |
486 except IndexError, e: | |
487 raise Exception('Please update %s and add a first id for %s (%s).' | |
488 % (first_ids_filename, filename, node.name)) | |
489 | |
490 def RunGatherers(self, debug=False): | |
491 '''Call RunPreSubstitutionGatherer() on every node of the tree, then apply | |
492 substitutions, then call RunPostSubstitutionGatherer() on every node. | |
493 | |
494 The substitutions step requires that the output language has been set. | |
495 Locally, get the Substitution messages and add them to the substituter. | |
496 Also add substitutions for language codes in the Rc. | |
497 | |
498 Args: | |
499 debug: will print information while running gatherers. | |
500 ''' | |
501 for node in self.ActiveDescendants(): | |
502 if hasattr(node, 'RunPreSubstitutionGatherer'): | |
503 with node: | |
504 node.RunPreSubstitutionGatherer(debug=debug) | |
505 | |
506 assert self.output_language | |
507 self.SubstituteMessages(self.GetSubstituter()) | |
508 | |
509 for node in self.ActiveDescendants(): | |
510 if hasattr(node, 'RunPostSubstitutionGatherer'): | |
511 with node: | |
512 node.RunPostSubstitutionGatherer(debug=debug) | |
513 | |
514 | |
515 class IdentifierNode(base.Node): | |
516 """A node for specifying identifiers that should appear in the resource | |
517 header file, and be unique amongst all other resource identifiers, but don't | |
518 have any other attributes or reference any resources. | |
519 """ | |
520 | |
521 def MandatoryAttributes(self): | |
522 return ['name'] | |
523 | |
524 def DefaultAttributes(self): | |
525 return { 'comment' : '', 'id' : '', 'systemid': 'false' } | |
526 | |
527 def GetId(self): | |
528 """Returns the id of this identifier if it has one, None otherwise | |
529 """ | |
530 if 'id' in self.attrs: | |
531 return self.attrs['id'] | |
532 return None | |
533 | |
534 def EndParsing(self): | |
535 """Handles system identifiers.""" | |
536 super(IdentifierNode, self).EndParsing() | |
537 if self.attrs['systemid'] == 'true': | |
538 util.SetupSystemIdentifiers((self.attrs['name'],)) | |
539 | |
540 @staticmethod | |
541 def Construct(parent, name, id, comment, systemid='false'): | |
542 """Creates a new node which is a child of 'parent', with attributes set | |
543 by parameters of the same name. | |
544 """ | |
545 node = IdentifierNode() | |
546 node.StartParsing('identifier', parent) | |
547 node.HandleAttribute('name', name) | |
548 node.HandleAttribute('id', id) | |
549 node.HandleAttribute('comment', comment) | |
550 node.HandleAttribute('systemid', systemid) | |
551 node.EndParsing() | |
552 return node | |
OLD | NEW |