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 gathering resources from RC files.
7'''
8
9
10import re
11
12from grit import exception
13from grit import lazy_re
14from grit import tclib
15
16from grit.gather import regexp
17
18
19# Find portions that need unescaping in resource strings.  We need to be
20# careful that a \\n is matched _first_ as a \\ rather than matching as
21# a \ followed by a \n.
22# TODO(joi) Handle ampersands if we decide to change them into <ph>
23# TODO(joi) May need to handle other control characters than \n
24_NEED_UNESCAPE = lazy_re.compile(r'""|\\\\|\\n|\\t')
25
26# Find portions that need escaping to encode string as a resource string.
27_NEED_ESCAPE = lazy_re.compile(r'"|\n|\t|\\|\&nbsp\;')
28
29# How to escape certain characters
30_ESCAPE_CHARS = {
31  '"' : '""',
32  '\n' : '\\n',
33  '\t' : '\\t',
34  '\\' : '\\\\',
35  '&nbsp;' : ' '
36}
37
38# How to unescape certain strings
39_UNESCAPE_CHARS = dict([[value, key] for key, value in _ESCAPE_CHARS.items()])
40
41
42
43class Section(regexp.RegexpGatherer):
44  '''A section from a resource file.'''
45
46  @staticmethod
47  def Escape(text):
48    '''Returns a version of 'text' with characters escaped that need to be
49    for inclusion in a resource section.'''
50    def Replace(match):
51      return _ESCAPE_CHARS[match.group()]
52    return _NEED_ESCAPE.sub(Replace, text)
53
54  @staticmethod
55  def UnEscape(text):
56    '''Returns a version of 'text' with escaped characters unescaped.'''
57    def Replace(match):
58      return _UNESCAPE_CHARS[match.group()]
59    return _NEED_UNESCAPE.sub(Replace, text)
60
61  def _RegExpParse(self, rexp, text_to_parse):
62    '''Overrides _RegExpParse to add shortcut group handling.  Otherwise
63    the same.
64    '''
65    super(Section, self)._RegExpParse(rexp, text_to_parse)
66
67    if not self.is_skeleton and len(self.GetTextualIds()) > 0:
68      group_name = self.GetTextualIds()[0]
69      for c in self.GetCliques():
70        c.AddToShortcutGroup(group_name)
71
72  def ReadSection(self):
73    rc_text = self._LoadInputFile()
74
75    out = ''
76    begin_count = 0
77    assert self.extkey
78    first_line_re = re.compile(r'\s*' + self.extkey + r'\b')
79    for line in rc_text.splitlines(True):
80      if out or first_line_re.match(line):
81        out += line
82
83      # we stop once we reach the END for the outermost block.
84      begin_count_was = begin_count
85      if len(out) > 0 and line.strip() == 'BEGIN':
86        begin_count += 1
87      elif len(out) > 0 and line.strip() == 'END':
88        begin_count -= 1
89      if begin_count_was == 1 and begin_count == 0:
90        break
91
92    if len(out) == 0:
93      raise exception.SectionNotFound('%s in file %s' % (self.extkey, self.rc_file))
94
95    self.text_ = out.strip()
96
97
98class Dialog(Section):
99  '''A resource section that contains a dialog resource.'''
100
101  # A typical dialog resource section looks like this:
102  #
103  # IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
104  # STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
105  # CAPTION "About"
106  # FONT 8, "System", 0, 0, 0x0
107  # BEGIN
108  #     ICON            IDI_KLONK,IDC_MYICON,14,9,20,20
109  #     LTEXT           "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
110  #                     SS_NOPREFIX
111  #     LTEXT           "Copyright (C) 2005",IDC_STATIC,49,20,119,8
112  #     DEFPUSHBUTTON   "OK",IDOK,195,6,30,11,WS_GROUP
113  #     CONTROL         "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
114  #                     BS_AUTORADIOBUTTON,46,51,84,10
115  # END
116
117  # We are using a sorted set of keys, and we assume that the
118  # group name used for descriptions (type) will come after the "text"
119  # group in alphabetical order. We also assume that there cannot be
120  # more than one description per regular expression match.
121  # If that's not the case some descriptions will be clobbered.
122  dialog_re_ = lazy_re.compile('''
123    # The dialog's ID in the first line
124    (?P<id1>[A-Z0-9_]+)\s+DIALOG(EX)?
125    |
126    # The caption of the dialog
127    (?P<type1>CAPTION)\s+"(?P<text1>.*?([^"]|""))"\s
128    |
129    # Lines for controls that have text and an ID
130    \s+(?P<type2>[A-Z]+)\s+"(?P<text2>.*?([^"]|"")?)"\s*,\s*(?P<id2>[A-Z0-9_]+)\s*,
131    |
132    # Lines for controls that have text only
133    \s+(?P<type3>[A-Z]+)\s+"(?P<text3>.*?([^"]|"")?)"\s*,
134    |
135    # Lines for controls that reference other resources
136    \s+[A-Z]+\s+[A-Z0-9_]+\s*,\s*(?P<id3>[A-Z0-9_]*[A-Z][A-Z0-9_]*)
137    |
138    # This matches "NOT SOME_STYLE" so that it gets consumed and doesn't get
139    # matched by the next option (controls that have only an ID and then just
140    # numbers)
141    \s+NOT\s+[A-Z][A-Z0-9_]+
142    |
143    # Lines for controls that have only an ID and then just numbers
144    \s+[A-Z]+\s+(?P<id4>[A-Z0-9_]*[A-Z][A-Z0-9_]*)\s*,
145    ''', re.MULTILINE | re.VERBOSE)
146
147  def Parse(self):
148    '''Knows how to parse dialog resource sections.'''
149    self.ReadSection()
150    self._RegExpParse(self.dialog_re_, self.text_)
151
152
153class Menu(Section):
154  '''A resource section that contains a menu resource.'''
155
156  # A typical menu resource section looks something like this:
157  #
158  # IDC_KLONK MENU
159  # BEGIN
160  #     POPUP "&File"
161  #     BEGIN
162  #         MENUITEM "E&xit",                       IDM_EXIT
163  #         MENUITEM "This be ""Klonk"" me like",   ID_FILE_THISBE
164  #         POPUP "gonk"
165  #         BEGIN
166  #             MENUITEM "Klonk && is ""good""",           ID_GONK_KLONKIS
167  #         END
168  #     END
169  #     POPUP "&Help"
170  #     BEGIN
171  #         MENUITEM "&About ...",                  IDM_ABOUT
172  #     END
173  # END
174
175  # Description used for the messages generated for menus, to explain to
176  # the translators how to handle them.
177  MENU_MESSAGE_DESCRIPTION = (
178    'This message represents a menu. Each of the items appears in sequence '
179    '(some possibly within sub-menus) in the menu. The XX01XX placeholders '
180    'serve to separate items. Each item contains an & (ampersand) character '
181    'in front of the keystroke that should be used as a shortcut for that item '
182    'in the menu. Please make sure that no two items in the same menu share '
183    'the same shortcut.'
184  )
185
186  # A dandy regexp to suck all the IDs and translateables out of a menu
187  # resource
188  menu_re_ = lazy_re.compile('''
189    # Match the MENU ID on the first line
190    ^(?P<id1>[A-Z0-9_]+)\s+MENU
191    |
192    # Match the translateable caption for a popup menu
193    POPUP\s+"(?P<text1>.*?([^"]|""))"\s
194    |
195    # Match the caption & ID of a MENUITEM
196    MENUITEM\s+"(?P<text2>.*?([^"]|""))"\s*,\s*(?P<id2>[A-Z0-9_]+)
197    ''', re.MULTILINE | re.VERBOSE)
198
199  def Parse(self):
200    '''Knows how to parse menu resource sections.  Because it is important that
201    menu shortcuts are unique within the menu, we return each menu as a single
202    message with placeholders to break up the different menu items, rather than
203    return a single message per menu item.  we also add an automatic description
204    with instructions for the translators.'''
205    self.ReadSection()
206    self.single_message_ = tclib.Message(description=self.MENU_MESSAGE_DESCRIPTION)
207    self._RegExpParse(self.menu_re_, self.text_)
208
209
210class Version(Section):
211  '''A resource section that contains a VERSIONINFO resource.'''
212
213  # A typical version info resource can look like this:
214  #
215  # VS_VERSION_INFO VERSIONINFO
216  #  FILEVERSION 1,0,0,1
217  #  PRODUCTVERSION 1,0,0,1
218  #  FILEFLAGSMASK 0x3fL
219  # #ifdef _DEBUG
220  #  FILEFLAGS 0x1L
221  # #else
222  #  FILEFLAGS 0x0L
223  # #endif
224  #  FILEOS 0x4L
225  #  FILETYPE 0x2L
226  #  FILESUBTYPE 0x0L
227  # BEGIN
228  #     BLOCK "StringFileInfo"
229  #     BEGIN
230  #         BLOCK "040904e4"
231  #         BEGIN
232  #             VALUE "CompanyName", "TODO: <Company name>"
233  #             VALUE "FileDescription", "TODO: <File description>"
234  #             VALUE "FileVersion", "1.0.0.1"
235  #             VALUE "LegalCopyright", "TODO: (c) <Company name>.  All rights reserved."
236  #             VALUE "InternalName", "res_format_test.dll"
237  #             VALUE "OriginalFilename", "res_format_test.dll"
238  #             VALUE "ProductName", "TODO: <Product name>"
239  #             VALUE "ProductVersion", "1.0.0.1"
240  #         END
241  #     END
242  #     BLOCK "VarFileInfo"
243  #     BEGIN
244  #         VALUE "Translation", 0x409, 1252
245  #     END
246  # END
247  #
248  #
249  # In addition to the above fields, VALUE fields named "Comments" and
250  # "LegalTrademarks" may also be translateable.
251
252  version_re_ = lazy_re.compile('''
253    # Match the ID on the first line
254    ^(?P<id1>[A-Z0-9_]+)\s+VERSIONINFO
255    |
256    # Match all potentially translateable VALUE sections
257    \s+VALUE\s+"
258    (
259      CompanyName|FileDescription|LegalCopyright|
260      ProductName|Comments|LegalTrademarks
261    )",\s+"(?P<text1>.*?([^"]|""))"\s
262    ''', re.MULTILINE | re.VERBOSE)
263
264  def Parse(self):
265    '''Knows how to parse VERSIONINFO resource sections.'''
266    self.ReadSection()
267    self._RegExpParse(self.version_re_, self.text_)
268
269  # TODO(joi) May need to override the Translate() method to change the
270  # "Translation" VALUE block to indicate the correct language code.
271
272
273class RCData(Section):
274  '''A resource section that contains some data .'''
275
276  # A typical rcdataresource section looks like this:
277  #
278  # IDR_BLAH        RCDATA      { 1, 2, 3, 4 }
279
280  dialog_re_ = lazy_re.compile('''
281    ^(?P<id1>[A-Z0-9_]+)\s+RCDATA\s+(DISCARDABLE)?\s+\{.*?\}
282    ''', re.MULTILINE | re.VERBOSE | re.DOTALL)
283
284  def Parse(self):
285    '''Implementation for resource types w/braces (not BEGIN/END)
286    '''
287    rc_text = self._LoadInputFile()
288
289    out = ''
290    begin_count = 0
291    openbrace_count = 0
292    assert self.extkey
293    first_line_re = re.compile(r'\s*' + self.extkey + r'\b')
294    for line in rc_text.splitlines(True):
295      if out or first_line_re.match(line):
296        out += line
297
298      # We stop once the braces balance (could happen in one line).
299      begin_count_was = begin_count
300      if len(out) > 0:
301        openbrace_count += line.count('{')
302        begin_count += line.count('{')
303        begin_count -= line.count('}')
304      if ((begin_count_was == 1 and begin_count == 0) or
305         (openbrace_count > 0 and begin_count == 0)):
306        break
307
308    if len(out) == 0:
309      raise exception.SectionNotFound('%s in file %s' % (self.extkey, self.rc_file))
310
311    self.text_ = out
312
313    self._RegExpParse(self.dialog_re_, out)
314
315
316class Accelerators(Section):
317  '''An ACCELERATORS table.
318  '''
319
320  # A typical ACCELERATORS section looks like this:
321  #
322  # IDR_ACCELERATOR1 ACCELERATORS
323  # BEGIN
324  #   "^C",           ID_ACCELERATOR32770,    ASCII,  NOINVERT
325  #   "^V",           ID_ACCELERATOR32771,    ASCII,  NOINVERT
326  #   VK_INSERT,      ID_ACCELERATOR32772,    VIRTKEY, CONTROL, NOINVERT
327  # END
328
329  accelerators_re_ = lazy_re.compile('''
330    # Match the ID on the first line
331    ^(?P<id1>[A-Z0-9_]+)\s+ACCELERATORS\s+
332    |
333    # Match accelerators specified as VK_XXX
334    \s+VK_[A-Z0-9_]+,\s*(?P<id2>[A-Z0-9_]+)\s*,
335    |
336    # Match accelerators specified as e.g. "^C"
337    \s+"[^"]*",\s+(?P<id3>[A-Z0-9_]+)\s*,
338    ''', re.MULTILINE | re.VERBOSE)
339
340  def Parse(self):
341    '''Knows how to parse ACCELERATORS resource sections.'''
342    self.ReadSection()
343    self._RegExpParse(self.accelerators_re_, self.text_)
344