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 @staticmethod 228 def Construct(parent, message, name, desc='', meaning='', translateable=True): 229 '''Constructs a new message node that is a child of 'parent', with the 230 name, desc, meaning and translateable attributes set using the same-named 231 parameters and the text of the message and any placeholders taken from 232 'message', which must be a tclib.Message() object.''' 233 # Convert type to appropriate string 234 translateable = 'true' if translateable else 'false' 235 236 node = MessageNode() 237 node.StartParsing('message', parent) 238 node.HandleAttribute('name', name) 239 node.HandleAttribute('desc', desc) 240 node.HandleAttribute('meaning', meaning) 241 node.HandleAttribute('translateable', translateable) 242 243 items = message.GetContent() 244 for ix, item in enumerate(items): 245 if isinstance(item, types.StringTypes): 246 # Ensure whitespace at front and back of message is correctly handled. 247 if ix == 0: 248 item = "'''" + item 249 if ix == len(items) - 1: 250 item = item + "'''" 251 252 node.AppendContent(item) 253 else: 254 phnode = PhNode() 255 phnode.StartParsing('ph', node) 256 phnode.HandleAttribute('name', item.GetPresentation()) 257 phnode.AppendContent(item.GetOriginal()) 258 259 if len(item.GetExample()) and item.GetExample() != ' ': 260 exnode = ExNode() 261 exnode.StartParsing('ex', phnode) 262 exnode.AppendContent(item.GetExample()) 263 exnode.EndParsing() 264 phnode.AddChild(exnode) 265 266 phnode.EndParsing() 267 node.AddChild(phnode) 268 269 node.EndParsing() 270 return node 271 272class PhNode(base.ContentNode): 273 '''A <ph> element.''' 274 275 def _IsValidChild(self, child): 276 return isinstance(child, ExNode) 277 278 def MandatoryAttributes(self): 279 return ['name'] 280 281 def EndParsing(self): 282 super(PhNode, self).EndParsing() 283 # We only allow a single example for each placeholder 284 if len(self.children) > 1: 285 raise exception.TooManyExamples() 286 287 def GetTextualIds(self): 288 # The 'name' attribute is not an ID. 289 return [] 290 291 292class ExNode(base.ContentNode): 293 '''An <ex> element.''' 294 pass 295