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 @staticmethod 336 def Construct(parent, name, type, file, encoding='cp1252'): 337 '''Creates a new node which is a child of 'parent', with attributes set 338 by parameters of the same name. 339 ''' 340 node = StructureNode() 341 node.StartParsing('structure', parent) 342 node.HandleAttribute('name', name) 343 node.HandleAttribute('type', type) 344 node.HandleAttribute('file', file) 345 node.HandleAttribute('encoding', encoding) 346 node.EndParsing() 347 return node 348 349 def SubstituteMessages(self, substituter): 350 '''Propagates substitution to gatherer. 351 352 Args: 353 substituter: a grit.util.Substituter object. 354 ''' 355 assert hasattr(self, 'gatherer') 356 if self.ExpandVariables(): 357 self.gatherer.SubstituteMessages(substituter) 358 359