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'''Handling of the <message> element. 7''' 8 9import re 10import types 11 12from grit.node import base 13 14import grit.format.rc_header 15import grit.format.rc 16 17from grit import clique 18from grit import exception 19from grit import lazy_re 20from grit import tclib 21from grit import util 22 23# Finds whitespace at the start and end of a string which can be multiline. 24_WHITESPACE = lazy_re.compile('(?P<start>\s*)(?P<body>.+?)(?P<end>\s*)\Z', 25 re.DOTALL | re.MULTILINE) 26 27 28class MessageNode(base.ContentNode): 29 '''A <message> element.''' 30 31 # For splitting a list of things that can be separated by commas or 32 # whitespace 33 _SPLIT_RE = lazy_re.compile('\s*,\s*|\s+') 34 35 def __init__(self): 36 super(MessageNode, self).__init__() 37 # Valid after EndParsing, this is the MessageClique that contains the 38 # source message and any translations of it that have been loaded. 39 self.clique = None 40 41 # We don't send leading and trailing whitespace into the translation 42 # console, but rather tack it onto the source message and any 43 # translations when formatting them into RC files or what have you. 44 self.ws_at_start = '' # Any whitespace characters at the start of the text 45 self.ws_at_end = '' # --"-- at the end of the text 46 47 # A list of "shortcut groups" this message is in. We check to make sure 48 # that shortcut keys (e.g. &J) within each shortcut group are unique. 49 self.shortcut_groups_ = [] 50 51 # Formatter-specific data used to control the output of individual strings. 52 # formatter_data is a space separated list of C preprocessor-style 53 # definitions. Names without values are given the empty string value. 54 # Example: "foo=5 bar baz=100" 55 self.formatter_data = {} 56 57 def _IsValidChild(self, child): 58 return isinstance(child, (PhNode)) 59 60 def _IsValidAttribute(self, name, value): 61 if name not in ['name', 'offset', 'translateable', 'desc', 'meaning', 62 'internal_comment', 'shortcut_groups', 'custom_type', 63 'validation_expr', 'use_name_for_id', 'sub_variable', 64 'formatter_data']: 65 return False 66 if (name in ('translateable', 'sub_variable') and 67 value not in ['true', 'false']): 68 return False 69 return True 70 71 def MandatoryAttributes(self): 72 return ['name|offset'] 73 74 def DefaultAttributes(self): 75 return { 76 'custom_type' : '', 77 'desc' : '', 78 'formatter_data' : '', 79 'internal_comment' : '', 80 'meaning' : '', 81 'shortcut_groups' : '', 82 'sub_variable' : 'false', 83 'translateable' : 'true', 84 'use_name_for_id' : 'false', 85 'validation_expr' : '', 86 } 87 88 def HandleAttribute(self, attrib, value): 89 base.ContentNode.HandleAttribute(self, attrib, value) 90 if attrib == 'formatter_data': 91 # Parse value, a space-separated list of defines, into a dict. 92 # Example: "foo=5 bar" -> {'foo':'5', 'bar':''} 93 for item in value.split(): 94 name, sep, val = item.partition('=') 95 self.formatter_data[name] = val 96 97 def GetTextualIds(self): 98 ''' 99 Returns the concatenation of the parent's node first_id and 100 this node's offset if it has one, otherwise just call the 101 superclass' implementation 102 ''' 103 if 'offset' in self.attrs: 104 # we search for the first grouping node in the parents' list 105 # to take care of the case where the first parent is an <if> node 106 grouping_parent = self.parent 107 import grit.node.empty 108 while grouping_parent and not isinstance(grouping_parent, 109 grit.node.empty.GroupingNode): 110 grouping_parent = grouping_parent.parent 111 112 assert 'first_id' in grouping_parent.attrs 113 return [grouping_parent.attrs['first_id'] + '_' + self.attrs['offset']] 114 else: 115 return super(MessageNode, self).GetTextualIds() 116 117 def IsTranslateable(self): 118 return self.attrs['translateable'] == 'true' 119 120 def EndParsing(self): 121 super(MessageNode, self).EndParsing() 122 123 # Make the text (including placeholder references) and list of placeholders, 124 # then strip and store leading and trailing whitespace and create the 125 # tclib.Message() and a clique to contain it. 126 127 text = '' 128 placeholders = [] 129 for item in self.mixed_content: 130 if isinstance(item, types.StringTypes): 131 text += item 132 else: 133 presentation = item.attrs['name'].upper() 134 text += presentation 135 ex = ' ' 136 if len(item.children): 137 ex = item.children[0].GetCdata() 138 original = item.GetCdata() 139 placeholders.append(tclib.Placeholder(presentation, original, ex)) 140 141 m = _WHITESPACE.match(text) 142 if m: 143 self.ws_at_start = m.group('start') 144 self.ws_at_end = m.group('end') 145 text = m.group('body') 146 147 self.shortcut_groups_ = self._SPLIT_RE.split(self.attrs['shortcut_groups']) 148 self.shortcut_groups_ = [i for i in self.shortcut_groups_ if i != ''] 149 150 description_or_id = self.attrs['desc'] 151 if description_or_id == '' and 'name' in self.attrs: 152 description_or_id = 'ID: %s' % self.attrs['name'] 153 154 assigned_id = None 155 if self.attrs['use_name_for_id'] == 'true': 156 assigned_id = self.attrs['name'] 157 message = tclib.Message(text=text, placeholders=placeholders, 158 description=description_or_id, 159 meaning=self.attrs['meaning'], 160 assigned_id=assigned_id) 161 self.InstallMessage(message) 162 163 def InstallMessage(self, message): 164 '''Sets this node's clique from a tclib.Message instance. 165 166 Args: 167 message: A tclib.Message. 168 ''' 169 self.clique = self.UberClique().MakeClique(message, self.IsTranslateable()) 170 for group in self.shortcut_groups_: 171 self.clique.AddToShortcutGroup(group) 172 if self.attrs['custom_type'] != '': 173 self.clique.SetCustomType(util.NewClassInstance(self.attrs['custom_type'], 174 clique.CustomType)) 175 elif self.attrs['validation_expr'] != '': 176 self.clique.SetCustomType( 177 clique.OneOffCustomType(self.attrs['validation_expr'])) 178 179 def SubstituteMessages(self, substituter): 180 '''Applies substitution to this message. 181 182 Args: 183 substituter: a grit.util.Substituter object. 184 ''' 185 message = substituter.SubstituteMessage(self.clique.GetMessage()) 186 if message is not self.clique.GetMessage(): 187 self.InstallMessage(message) 188 189 def GetCliques(self): 190 if self.clique: 191 return [self.clique] 192 else: 193 return [] 194 195 def Translate(self, lang): 196 '''Returns a translated version of this message. 197 ''' 198 assert self.clique 199 msg = self.clique.MessageForLanguage(lang, 200 self.PseudoIsAllowed(), 201 self.ShouldFallbackToEnglish() 202 ).GetRealContent() 203 return msg.replace('[GRITLANGCODE]', lang) 204 205 def NameOrOffset(self): 206 if 'name' in self.attrs: 207 return self.attrs['name'] 208 else: 209 return self.attrs['offset'] 210 211 def ExpandVariables(self): 212 '''We always expand variables on Messages.''' 213 return True 214 215 def GetDataPackPair(self, lang, encoding): 216 '''Returns a (id, string) pair that represents the string id and the string 217 in the specified encoding, where |encoding| is one of the encoding values 218 accepted by util.Encode. This is used to generate the data pack data file. 219 ''' 220 from grit.format import rc_header 221 id_map = rc_header.GetIds(self.GetRoot()) 222 id = id_map[self.GetTextualIds()[0]] 223 224 message = self.ws_at_start + self.Translate(lang) + self.ws_at_end 225 return id, util.Encode(message, encoding) 226 227 def IsResourceMapSource(self): 228 return True 229 230 def GeneratesResourceMapEntry(self, output_all_resource_defines, 231 is_active_descendant): 232 return is_active_descendant 233 234 @staticmethod 235 def Construct(parent, message, name, desc='', meaning='', translateable=True): 236 '''Constructs a new message node that is a child of 'parent', with the 237 name, desc, meaning and translateable attributes set using the same-named 238 parameters and the text of the message and any placeholders taken from 239 'message', which must be a tclib.Message() object.''' 240 # Convert type to appropriate string 241 translateable = 'true' if translateable else 'false' 242 243 node = MessageNode() 244 node.StartParsing('message', parent) 245 node.HandleAttribute('name', name) 246 node.HandleAttribute('desc', desc) 247 node.HandleAttribute('meaning', meaning) 248 node.HandleAttribute('translateable', translateable) 249 250 items = message.GetContent() 251 for ix, item in enumerate(items): 252 if isinstance(item, types.StringTypes): 253 # Ensure whitespace at front and back of message is correctly handled. 254 if ix == 0: 255 item = "'''" + item 256 if ix == len(items) - 1: 257 item = item + "'''" 258 259 node.AppendContent(item) 260 else: 261 phnode = PhNode() 262 phnode.StartParsing('ph', node) 263 phnode.HandleAttribute('name', item.GetPresentation()) 264 phnode.AppendContent(item.GetOriginal()) 265 266 if len(item.GetExample()) and item.GetExample() != ' ': 267 exnode = ExNode() 268 exnode.StartParsing('ex', phnode) 269 exnode.AppendContent(item.GetExample()) 270 exnode.EndParsing() 271 phnode.AddChild(exnode) 272 273 phnode.EndParsing() 274 node.AddChild(phnode) 275 276 node.EndParsing() 277 return node 278 279class PhNode(base.ContentNode): 280 '''A <ph> element.''' 281 282 def _IsValidChild(self, child): 283 return isinstance(child, ExNode) 284 285 def MandatoryAttributes(self): 286 return ['name'] 287 288 def EndParsing(self): 289 super(PhNode, self).EndParsing() 290 # We only allow a single example for each placeholder 291 if len(self.children) > 1: 292 raise exception.TooManyExamples() 293 294 def GetTextualIds(self): 295 # The 'name' attribute is not an ID. 296 return [] 297 298 299class ExNode(base.ContentNode): 300 '''An <ex> element.''' 301 pass 302