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