OLD | NEW |
| (Empty) |
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 | |
10 import re | |
11 | |
12 from grit import exception | |
13 from grit import lazy_re | |
14 from grit import tclib | |
15 | |
16 from 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|\\|\ \;') | |
28 | |
29 # How to escape certain characters | |
30 _ESCAPE_CHARS = { | |
31 '"' : '""', | |
32 '\n' : '\\n', | |
33 '\t' : '\\t', | |
34 '\\' : '\\\\', | |
35 ' ' : ' ' | |
36 } | |
37 | |
38 # How to unescape certain strings | |
39 _UNESCAPE_CHARS = dict([[value, key] for key, value in _ESCAPE_CHARS.items()]) | |
40 | |
41 | |
42 | |
43 class 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_fi
le)) | |
94 | |
95 self.text_ = out.strip() | |
96 | |
97 | |
98 class 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 | |
153 class 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_DESCRIPTI
ON) | |
207 self._RegExpParse(self.menu_re_, self.text_) | |
208 | |
209 | |
210 class 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 r
eserved." | |
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 | |
273 class 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_fi
le)) | |
310 | |
311 self.text_ = out | |
312 | |
313 self._RegExpParse(self.dialog_re_, out) | |
314 | |
315 | |
316 class 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_) | |
OLD | NEW |