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