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|\\|\ \;') 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 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