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