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'''Support for "policy_templates.json" format used by the policy template
7generator as a source for generating ADM,ADMX,etc files.'''
8
9import types
10import sys
11
12from grit.gather import skeleton_gatherer
13from grit import util
14from grit import tclib
15from xml.dom import minidom
16from xml.parsers.expat import ExpatError
17
18
19class PolicyJson(skeleton_gatherer.SkeletonGatherer):
20  '''Collects and translates the following strings from policy_templates.json:
21    - captions,descriptions and labels of policies
22    - captions of enumeration items
23    - misc strings from the 'messages' section
24     Translatable strings may have untranslateable placeholders with the same
25     format that is used in .grd files.
26  '''
27
28  def _ParsePlaceholder(self, placeholder, msg):
29    '''Extracts a placeholder from a DOM node and adds it to a tclib Message.
30
31    Args:
32      placeholder: A DOM node of the form:
33        <ph name="PLACEHOLDER_NAME">Placeholder text<ex>Example value</ex></ph>
34      msg: The placeholder is added to this message.
35    '''
36    text = []
37    example_text = []
38    for node1 in placeholder.childNodes:
39      if (node1.nodeType == minidom.Node.TEXT_NODE):
40        text.append(node1.data)
41      elif (node1.nodeType == minidom.Node.ELEMENT_NODE and
42            node1.tagName == 'ex'):
43        for node2 in node1.childNodes:
44          example_text.append(node2.toxml())
45      else:
46         raise Exception('Unexpected element inside a placeholder: ' +
47                         node2.toxml())
48    if example_text == []:
49      # In such cases the original text is okay for an example.
50      example_text = text
51    msg.AppendPlaceholder(tclib.Placeholder(
52        placeholder.attributes['name'].value,
53        ''.join(text).strip(),
54        ''.join(example_text).strip()))
55
56  def _ParseMessage(self, string, desc):
57    '''Parses a given string and adds it to the output as a translatable chunk
58    with a given description.
59
60    Args:
61      string: The message string to parse.
62      desc: The description of the message (for the translators).
63    '''
64    msg = tclib.Message(description=desc)
65    xml = '<msg>' + string + '</msg>'
66    try:
67      node = minidom.parseString(xml).childNodes[0]
68    except ExpatError:
69      reason = '''Input isn't valid XML (has < & > been escaped?): ''' + string
70      raise Exception, reason, sys.exc_info()[2]
71
72    for child in node.childNodes:
73      if child.nodeType == minidom.Node.TEXT_NODE:
74        msg.AppendText(child.data)
75      elif child.nodeType == minidom.Node.ELEMENT_NODE:
76        if child.tagName == 'ph':
77          self._ParsePlaceholder(child, msg)
78        else:
79          raise Exception("Not implemented.")
80      else:
81        raise Exception("Not implemented.")
82    self.skeleton_.append(self.uberclique.MakeClique(msg))
83
84  def _ParseNode(self, node):
85    '''Traverses the subtree of a DOM node, and register a tclib message for
86    all the <message> nodes.
87    '''
88    att_text = []
89    if node.attributes:
90      items = node.attributes.items()
91      items.sort()
92      for key, value in items:
93        att_text.append(' %s=\"%s\"' % (key, value))
94    self._AddNontranslateableChunk("<%s%s>" %
95                                   (node.tagName, ''.join(att_text)))
96    if node.tagName == 'message':
97      msg = tclib.Message(description=node.attributes['desc'])
98      for child in node.childNodes:
99        if child.nodeType == minidom.Node.TEXT_NODE:
100          if msg == None:
101            self._AddNontranslateableChunk(child.data)
102          else:
103            msg.AppendText(child.data)
104        elif child.nodeType == minidom.Node.ELEMENT_NODE:
105          if child.tagName == 'ph':
106            self._ParsePlaceholder(child, msg)
107        else:
108          assert False
109      self.skeleton_.append(self.uberclique.MakeClique(msg))
110    else:
111      for child in node.childNodes:
112        if child.nodeType == minidom.Node.TEXT_NODE:
113          self._AddNontranslateableChunk(child.data)
114        elif node.nodeType == minidom.Node.ELEMENT_NODE:
115          self._ParseNode(child)
116
117    self._AddNontranslateableChunk("</%s>" % node.tagName)
118
119  def _AddIndentedNontranslateableChunk(self, depth, string):
120    '''Adds a nontranslateable chunk of text to the internally stored output.
121
122    Args:
123      depth: The number of double spaces to prepend to the next argument string.
124      string: The chunk of text to add.
125    '''
126    result = []
127    while depth > 0:
128      result.append('  ')
129      depth = depth - 1
130    result.append(string)
131    self._AddNontranslateableChunk(''.join(result))
132
133  def _GetDescription(self, item, item_type, parent_item, key):
134    '''Creates a description for a translatable message. The description gives
135    some context for the person who will translate this message.
136
137    Args:
138      item: A policy or an enumeration item.
139      item_type: 'enum_item' | 'policy'
140      parent_item: The owner of item. (A policy of type group or enum.)
141      key: The name of the key to parse.
142      depth: The level of indentation.
143    '''
144    key_map = {
145      'desc': 'Description',
146      'caption': 'Caption',
147      'label': 'Label',
148    }
149    if item_type == 'policy':
150      return '%s of the policy named %s' % (key_map[key], item['name'])
151    elif item_type == 'enum_item':
152      return ('%s of the option named %s in policy %s' %
153              (key_map[key], item['name'], parent_item['name']))
154    else:
155      raise Exception('Unexpected type %s' % item_type)
156
157  def _AddPolicyKey(self, item, item_type, parent_item, key, depth):
158    '''Given a policy/enumeration item and a key, adds that key and its value
159    into the output.
160    E.g.:
161       'example_value': 123
162    If key indicates that the value is a translatable string, then it is parsed
163    as a translatable string.
164
165    Args:
166      item: A policy or an enumeration item.
167      item_type: 'enum_item' | 'policy'
168      parent_item: The owner of item. (A policy of type group or enum.)
169      key: The name of the key to parse.
170      depth: The level of indentation.
171    '''
172    self._AddIndentedNontranslateableChunk(depth, "'%s': " % key)
173    if key in ('desc', 'caption', 'label'):
174      self._AddNontranslateableChunk("'''")
175      self._ParseMessage(
176          item[key],
177          self._GetDescription(item, item_type, parent_item, key))
178      self._AddNontranslateableChunk("''',\n")
179    else:
180      str_val = item[key]
181      if type(str_val) == types.StringType:
182        str_val = "'%s'" % self.Escape(str_val)
183      else:
184        str_val = str(str_val)
185      self._AddNontranslateableChunk(str_val + ',\n')
186
187  def _AddItems(self, items, item_type, parent_item, depth):
188    '''Parses and adds a list of items from the JSON file. Items can be policies
189    or parts of an enum policy.
190
191    Args:
192      items: Either a list of policies or a list of dictionaries.
193      item_type: 'enum_item' | 'policy'
194      parent_item: If items contains a list of policies, then this is the policy
195        group that owns them. If items contains a list of enumeration items,
196        then this is the enum policy that holds them.
197      depth: Indicates the depth of our position in the JSON hierarchy. Used to
198        add nice line-indent to the output.
199    '''
200    for item1 in items:
201      self._AddIndentedNontranslateableChunk(depth, "{\n")
202      for key in item1.keys():
203        if key == 'items':
204          self._AddIndentedNontranslateableChunk(depth + 1, "'items': [\n")
205          self._AddItems(item1['items'], 'enum_item', item1, depth + 2)
206          self._AddIndentedNontranslateableChunk(depth + 1, "],\n")
207        elif key == 'policies':
208          self._AddIndentedNontranslateableChunk(depth + 1, "'policies': [\n")
209          self._AddItems(item1['policies'], 'policy', item1, depth + 2)
210          self._AddIndentedNontranslateableChunk(depth + 1, "],\n")
211        else:
212          self._AddPolicyKey(item1, item_type, parent_item, key, depth + 1)
213      self._AddIndentedNontranslateableChunk(depth, "},\n")
214
215  def _AddMessages(self):
216    '''Processed and adds the 'messages' section to the output.'''
217    self._AddNontranslateableChunk("  'messages': {\n")
218    for name, message in self.data['messages'].iteritems():
219      self._AddNontranslateableChunk("      '%s': {\n" % name)
220      self._AddNontranslateableChunk("        'text': '''")
221      self._ParseMessage(message['text'], message['desc'])
222      self._AddNontranslateableChunk("'''\n")
223      self._AddNontranslateableChunk("      },\n")
224    self._AddNontranslateableChunk("  },\n")
225
226  # Although we use the RegexpGatherer base class, we do not use the
227  # _RegExpParse method of that class to implement Parse().  Instead, we
228  # parse using a DOM parser.
229  def Parse(self):
230    if self.have_parsed_:
231      return
232    self.have_parsed_ = True
233
234    self.text_ = self._LoadInputFile()
235    if util.IsExtraVerbose():
236      print self.text_
237
238    self.data = eval(self.text_)
239
240    self._AddNontranslateableChunk('{\n')
241    self._AddNontranslateableChunk("  'policy_definitions': [\n")
242    self._AddItems(self.data['policy_definitions'], 'policy', None, 2)
243    self._AddNontranslateableChunk("  ],\n")
244    self._AddMessages()
245    self._AddNontranslateableChunk('\n}')
246
247  def Escape(self, text):
248    # \ -> \\
249    # ' -> \'
250    # " -> \"
251    return text.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
252