Mercurial > p > roundup > code
comparison roundup/cgi/TAL/TALGenerator.py @ 2348:8c2402a78bb0
beginning getting ZPT up to date: TAL first
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Fri, 21 May 2004 05:36:30 +0000 |
| parents | fc52d57c6c3e |
| children | 198b6e810c67 |
comparison
equal
deleted
inserted
replaced
| 2347:fbbda3b1816d | 2348:8c2402a78bb0 |
|---|---|
| 1 ############################################################################## | 1 ############################################################################## |
| 2 # | 2 # |
| 3 # Copyright (c) 2001, 2002 Zope Corporation and Contributors. | 3 # Copyright (c) 2001, 2002 Zope Corporation and Contributors. |
| 4 # All Rights Reserved. | 4 # All Rights Reserved. |
| 5 # | 5 # |
| 6 # This software is subject to the provisions of the Zope Public License, | 6 # This software is subject to the provisions of the Zope Public License, |
| 7 # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. | 7 # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. |
| 8 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED | 8 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
| 9 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | 9 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| 10 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS | 10 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
| 11 # FOR A PARTICULAR PURPOSE | 11 # FOR A PARTICULAR PURPOSE. |
| 12 # | 12 # |
| 13 ############################################################################## | 13 ############################################################################## |
| 14 """Code generator for TALInterpreter intermediate code. | |
| 15 """ | 14 """ |
| 16 __docformat__ = 'restructuredtext' | 15 Code generator for TALInterpreter intermediate code. |
| 17 | 16 """ |
| 18 import string | 17 |
| 19 import re | 18 import re |
| 20 import cgi | 19 import cgi |
| 21 | 20 |
| 22 from TALDefs import * | 21 import TALDefs |
| 22 | |
| 23 from TALDefs import NAME_RE, TAL_VERSION | |
| 24 from TALDefs import I18NError, METALError, TALError | |
| 25 from TALDefs import parseSubstitution | |
| 26 from TranslationContext import TranslationContext, DEFAULT_DOMAIN | |
| 27 | |
| 28 I18N_REPLACE = 1 | |
| 29 I18N_CONTENT = 2 | |
| 30 I18N_EXPRESSION = 3 | |
| 31 | |
| 32 _name_rx = re.compile(NAME_RE) | |
| 33 | |
| 23 | 34 |
| 24 class TALGenerator: | 35 class TALGenerator: |
| 25 | 36 |
| 26 inMacroUse = 0 | 37 inMacroUse = 0 |
| 27 inMacroDef = 0 | 38 inMacroDef = 0 |
| 28 source_file = None | 39 source_file = None |
| 29 | 40 |
| 30 def __init__(self, expressionCompiler=None, xml=1, source_file=None): | 41 def __init__(self, expressionCompiler=None, xml=1, source_file=None): |
| 31 if not expressionCompiler: | 42 if not expressionCompiler: |
| 32 from DummyEngine import DummyEngine | 43 from DummyEngine import DummyEngine |
| 33 expressionCompiler = DummyEngine() | 44 expressionCompiler = DummyEngine() |
| 34 self.expressionCompiler = expressionCompiler | 45 self.expressionCompiler = expressionCompiler |
| 35 self.CompilerError = expressionCompiler.getCompilerError() | 46 self.CompilerError = expressionCompiler.getCompilerError() |
| 47 # This holds the emitted opcodes representing the input | |
| 36 self.program = [] | 48 self.program = [] |
| 49 # The program stack for when we need to do some sub-evaluation for an | |
| 50 # intermediate result. E.g. in an i18n:name tag for which the | |
| 51 # contents describe the ${name} value. | |
| 37 self.stack = [] | 52 self.stack = [] |
| 53 # Another stack of postponed actions. Elements on this stack are a | |
| 54 # dictionary; key/values contain useful information that | |
| 55 # emitEndElement needs to finish its calculations | |
| 38 self.todoStack = [] | 56 self.todoStack = [] |
| 39 self.macros = {} | 57 self.macros = {} |
| 40 self.slots = {} | 58 self.slots = {} |
| 41 self.slotStack = [] | 59 self.slotStack = [] |
| 42 self.xml = xml | 60 self.xml = xml |
| 43 self.emit("version", TAL_VERSION) | 61 self.emit("version", TAL_VERSION) |
| 44 self.emit("mode", xml and "xml" or "html") | 62 self.emit("mode", xml and "xml" or "html") |
| 45 if source_file is not None: | 63 if source_file is not None: |
| 46 self.source_file = source_file | 64 self.source_file = source_file |
| 47 self.emit("setSourceFile", source_file) | 65 self.emit("setSourceFile", source_file) |
| 66 self.i18nContext = TranslationContext() | |
| 67 self.i18nLevel = 0 | |
| 48 | 68 |
| 49 def getCode(self): | 69 def getCode(self): |
| 50 assert not self.stack | 70 assert not self.stack |
| 51 assert not self.todoStack | 71 assert not self.todoStack |
| 52 return self.optimize(self.program), self.macros | 72 return self.optimize(self.program), self.macros |
| 53 | 73 |
| 54 def optimize(self, program): | 74 def optimize(self, program): |
| 55 output = [] | 75 output = [] |
| 56 collect = [] | 76 collect = [] |
| 57 rawseen = cursor = 0 | 77 cursor = 0 |
| 58 if self.xml: | 78 if self.xml: |
| 59 endsep = "/>" | 79 endsep = "/>" |
| 60 else: | 80 else: |
| 61 endsep = " />" | 81 endsep = " />" |
| 62 for cursor in xrange(len(program)+1): | 82 for cursor in xrange(len(program)+1): |
| 81 # Push *Scope instructions in front of any text instructions; | 101 # Push *Scope instructions in front of any text instructions; |
| 82 # this allows text instructions separated only by *Scope | 102 # this allows text instructions separated only by *Scope |
| 83 # instructions to be joined together. | 103 # instructions to be joined together. |
| 84 output.append(self.optimizeArgsList(item)) | 104 output.append(self.optimizeArgsList(item)) |
| 85 continue | 105 continue |
| 86 text = string.join(collect, "") | 106 if opcode == 'noop': |
| 107 # This is a spacer for end tags in the face of i18n:name | |
| 108 # attributes. We can't let the optimizer collect immediately | |
| 109 # following end tags into the same rawtextOffset. | |
| 110 opcode = None | |
| 111 pass | |
| 112 text = "".join(collect) | |
| 87 if text: | 113 if text: |
| 88 i = string.rfind(text, "\n") | 114 i = text.rfind("\n") |
| 89 if i >= 0: | 115 if i >= 0: |
| 90 i = len(text) - (i + 1) | 116 i = len(text) - (i + 1) |
| 91 output.append(("rawtextColumn", (text, i))) | 117 output.append(("rawtextColumn", (text, i))) |
| 92 else: | 118 else: |
| 93 output.append(("rawtextOffset", (text, len(text)))) | 119 output.append(("rawtextOffset", (text, len(text)))) |
| 94 if opcode != None: | 120 if opcode != None: |
| 95 output.append(self.optimizeArgsList(item)) | 121 output.append(self.optimizeArgsList(item)) |
| 96 rawseen = cursor+1 | |
| 97 collect = [] | 122 collect = [] |
| 98 return self.optimizeCommonTriple(output) | 123 return self.optimizeCommonTriple(output) |
| 99 | 124 |
| 100 def optimizeArgsList(self, item): | 125 def optimizeArgsList(self, item): |
| 101 if len(item) == 2: | 126 if len(item) == 2: |
| 102 return item | 127 return item |
| 103 else: | 128 else: |
| 104 return item[0], tuple(item[1:]) | 129 return item[0], tuple(item[1:]) |
| 105 | 130 |
| 106 actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4, | 131 # These codes are used to indicate what sort of special actions |
| 107 0: 0, 1: 1, 2: 2, 3: 3, 4: 4} | 132 # are needed for each special attribute. (Simple attributes don't |
| 133 # get action codes.) | |
| 134 # | |
| 135 # The special actions (which are modal) are handled by | |
| 136 # TALInterpreter.attrAction() and .attrAction_tal(). | |
| 137 # | |
| 138 # Each attribute is represented by a tuple: | |
| 139 # | |
| 140 # (name, value) -- a simple name/value pair, with | |
| 141 # no special processing | |
| 142 # | |
| 143 # (name, value, action, *extra) -- attribute with special | |
| 144 # processing needs, action is a | |
| 145 # code that indicates which | |
| 146 # branch to take, and *extra | |
| 147 # contains additional, | |
| 148 # action-specific information | |
| 149 # needed by the processing | |
| 150 # | |
| 108 def optimizeStartTag(self, collect, name, attrlist, end): | 151 def optimizeStartTag(self, collect, name, attrlist, end): |
| 152 # return true if the tag can be converted to plain text | |
| 109 if not attrlist: | 153 if not attrlist: |
| 110 collect.append("<%s%s" % (name, end)) | 154 collect.append("<%s%s" % (name, end)) |
| 111 return 1 | 155 return 1 |
| 112 opt = 1 | 156 opt = 1 |
| 113 new = ["<" + name] | 157 new = ["<" + name] |
| 114 for i in range(len(attrlist)): | 158 for i in range(len(attrlist)): |
| 115 item = attrlist[i] | 159 item = attrlist[i] |
| 116 if len(item) > 2: | 160 if len(item) > 2: |
| 117 opt = 0 | 161 opt = 0 |
| 118 name, value, action = item[:3] | 162 name, value, action = item[:3] |
| 119 action = self.actionIndex[action] | |
| 120 attrlist[i] = (name, value, action) + item[3:] | 163 attrlist[i] = (name, value, action) + item[3:] |
| 121 else: | 164 else: |
| 122 if item[1] is None: | 165 if item[1] is None: |
| 123 s = item[0] | 166 s = item[0] |
| 124 else: | 167 else: |
| 125 s = "%s=%s" % (item[0], quote(item[1])) | 168 s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1])) |
| 126 attrlist[i] = item[0], s | 169 attrlist[i] = item[0], s |
| 127 if item[1] is None: | 170 new.append(" " + s) |
| 128 new.append(" " + item[0]) | 171 # if no non-optimizable attributes were found, convert to plain text |
| 129 else: | |
| 130 new.append(" %s=%s" % (item[0], quote(item[1]))) | |
| 131 if opt: | 172 if opt: |
| 132 new.append(end) | 173 new.append(end) |
| 133 collect.extend(new) | 174 collect.extend(new) |
| 134 return opt | 175 return opt |
| 135 | 176 |
| 137 if len(program) < 3: | 178 if len(program) < 3: |
| 138 return program | 179 return program |
| 139 output = program[:2] | 180 output = program[:2] |
| 140 prev2, prev1 = output | 181 prev2, prev1 = output |
| 141 for item in program[2:]: | 182 for item in program[2:]: |
| 142 if ( item[0] == "beginScope" | 183 if ( item[0] == "beginScope" |
| 143 and prev1[0] == "setPosition" | 184 and prev1[0] == "setPosition" |
| 144 and prev2[0] == "rawtextColumn"): | 185 and prev2[0] == "rawtextColumn"): |
| 145 position = output.pop()[1] | 186 position = output.pop()[1] |
| 146 text, column = output.pop()[1] | 187 text, column = output.pop()[1] |
| 147 prev1 = None, None | 188 prev1 = None, None |
| 148 closeprev = 0 | 189 closeprev = 0 |
| 149 if output and output[-1][0] == "endScope": | 190 if output and output[-1][0] == "endScope": |
| 213 isend = 1 | 254 isend = 1 |
| 214 cexpr = optTag[0] | 255 cexpr = optTag[0] |
| 215 if cexpr: | 256 if cexpr: |
| 216 cexpr = self.compileExpression(optTag[0]) | 257 cexpr = self.compileExpression(optTag[0]) |
| 217 self.emit("optTag", name, cexpr, optTag[1], isend, start, program) | 258 self.emit("optTag", name, cexpr, optTag[1], isend, start, program) |
| 218 | 259 |
| 219 def emitRawText(self, text): | 260 def emitRawText(self, text): |
| 220 self.emit("rawtext", text) | 261 self.emit("rawtext", text) |
| 221 | 262 |
| 222 def emitText(self, text): | 263 def emitText(self, text): |
| 223 self.emitRawText(cgi.escape(text)) | 264 self.emitRawText(cgi.escape(text)) |
| 224 | 265 |
| 225 def emitDefines(self, defines): | 266 def emitDefines(self, defines): |
| 226 for part in splitParts(defines): | 267 for part in TALDefs.splitParts(defines): |
| 227 m = re.match( | 268 m = re.match( |
| 228 r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part) | 269 r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part) |
| 229 if not m: | 270 if not m: |
| 230 raise TALError("invalid define syntax: " + `part`, | 271 raise TALError("invalid define syntax: " + `part`, |
| 231 self.position) | 272 self.position) |
| 276 self.emit("insertText", cexpr, program) | 317 self.emit("insertText", cexpr, program) |
| 277 else: | 318 else: |
| 278 assert key == "structure" | 319 assert key == "structure" |
| 279 self.emit("insertStructure", cexpr, attrDict, program) | 320 self.emit("insertStructure", cexpr, attrDict, program) |
| 280 | 321 |
| 322 def emitI18nVariable(self, stuff): | |
| 323 # Used for i18n:name attributes. arg is extra information describing | |
| 324 # how the contents of the variable should get filled in, and it will | |
| 325 # either be a 1-tuple or a 2-tuple. If arg[0] is None, then the | |
| 326 # i18n:name value is taken implicitly from the contents of the tag, | |
| 327 # e.g. "I live in <span i18n:name="country">the USA</span>". In this | |
| 328 # case, arg[1] is the opcode sub-program describing the contents of | |
| 329 # the tag. | |
| 330 # | |
| 331 # When arg[0] is not None, it contains the tal expression used to | |
| 332 # calculate the contents of the variable, e.g. | |
| 333 # "I live in <span i18n:name="country" | |
| 334 # tal:replace="here/countryOfOrigin" />" | |
| 335 varname, action, expression = stuff | |
| 336 m = _name_rx.match(varname) | |
| 337 if m is None or m.group() != varname: | |
| 338 raise TALError("illegal i18n:name: %r" % varname, self.position) | |
| 339 key = cexpr = None | |
| 340 program = self.popProgram() | |
| 341 if action == I18N_REPLACE: | |
| 342 # This is a tag with an i18n:name and a tal:replace (implicit or | |
| 343 # explicit). Get rid of the first and last elements of the | |
| 344 # program, which are the start and end tag opcodes of the tag. | |
| 345 program = program[1:-1] | |
| 346 elif action == I18N_CONTENT: | |
| 347 # This is a tag with an i18n:name and a tal:content | |
| 348 # (explicit-only). Keep the first and last elements of the | |
| 349 # program, so we keep the start and end tag output. | |
| 350 pass | |
| 351 else: | |
| 352 assert action == I18N_EXPRESSION | |
| 353 key, expr = parseSubstitution(expression) | |
| 354 cexpr = self.compileExpression(expr) | |
| 355 # XXX Would key be anything but 'text' or None? | |
| 356 assert key in ('text', None) | |
| 357 self.emit('i18nVariable', varname, program, cexpr) | |
| 358 | |
| 359 def emitTranslation(self, msgid, i18ndata): | |
| 360 program = self.popProgram() | |
| 361 if i18ndata is None: | |
| 362 self.emit('insertTranslation', msgid, program) | |
| 363 else: | |
| 364 key, expr = parseSubstitution(i18ndata) | |
| 365 cexpr = self.compileExpression(expr) | |
| 366 assert key == 'text' | |
| 367 self.emit('insertTranslation', msgid, program, cexpr) | |
| 368 | |
| 281 def emitDefineMacro(self, macroName): | 369 def emitDefineMacro(self, macroName): |
| 282 program = self.popProgram() | 370 program = self.popProgram() |
| 283 macroName = string.strip(macroName) | 371 macroName = macroName.strip() |
| 284 if self.macros.has_key(macroName): | 372 if self.macros.has_key(macroName): |
| 285 raise METALError("duplicate macro definition: %s" % `macroName`, | 373 raise METALError("duplicate macro definition: %s" % `macroName`, |
| 286 self.position) | 374 self.position) |
| 287 if not re.match('%s$' % NAME_RE, macroName): | 375 if not re.match('%s$' % NAME_RE, macroName): |
| 288 raise METALError("invalid macro name: %s" % `macroName`, | 376 raise METALError("invalid macro name: %s" % `macroName`, |
| 297 self.inMacroUse = 0 | 385 self.inMacroUse = 0 |
| 298 self.emit("useMacro", expr, cexpr, self.popSlots(), program) | 386 self.emit("useMacro", expr, cexpr, self.popSlots(), program) |
| 299 | 387 |
| 300 def emitDefineSlot(self, slotName): | 388 def emitDefineSlot(self, slotName): |
| 301 program = self.popProgram() | 389 program = self.popProgram() |
| 302 slotName = string.strip(slotName) | 390 slotName = slotName.strip() |
| 303 if not re.match('%s$' % NAME_RE, slotName): | 391 if not re.match('%s$' % NAME_RE, slotName): |
| 304 raise METALError("invalid slot name: %s" % `slotName`, | 392 raise METALError("invalid slot name: %s" % `slotName`, |
| 305 self.position) | 393 self.position) |
| 306 self.emit("defineSlot", slotName, program) | 394 self.emit("defineSlot", slotName, program) |
| 307 | 395 |
| 308 def emitFillSlot(self, slotName): | 396 def emitFillSlot(self, slotName): |
| 309 program = self.popProgram() | 397 program = self.popProgram() |
| 310 slotName = string.strip(slotName) | 398 slotName = slotName.strip() |
| 311 if self.slots.has_key(slotName): | 399 if self.slots.has_key(slotName): |
| 312 raise METALError("duplicate fill-slot name: %s" % `slotName`, | 400 raise METALError("duplicate fill-slot name: %s" % `slotName`, |
| 313 self.position) | 401 self.position) |
| 314 if not re.match('%s$' % NAME_RE, slotName): | 402 if not re.match('%s$' % NAME_RE, slotName): |
| 315 raise METALError("invalid slot name: %s" % `slotName`, | 403 raise METALError("invalid slot name: %s" % `slotName`, |
| 336 m = re.search(r"\s+\Z", text) | 424 m = re.search(r"\s+\Z", text) |
| 337 if m: | 425 if m: |
| 338 self.program[i] = ("rawtext", text[:m.start()]) | 426 self.program[i] = ("rawtext", text[:m.start()]) |
| 339 collect.append(m.group()) | 427 collect.append(m.group()) |
| 340 collect.reverse() | 428 collect.reverse() |
| 341 return string.join(collect, "") | 429 return "".join(collect) |
| 342 | 430 |
| 343 def unEmitNewlineWhitespace(self): | 431 def unEmitNewlineWhitespace(self): |
| 344 collect = [] | 432 collect = [] |
| 345 i = len(self.program) | 433 i = len(self.program) |
| 346 while i > 0: | 434 while i > 0: |
| 355 m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text) | 443 m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text) |
| 356 if not m: | 444 if not m: |
| 357 break | 445 break |
| 358 text, rest = m.group(1, 2) | 446 text, rest = m.group(1, 2) |
| 359 collect.reverse() | 447 collect.reverse() |
| 360 rest = rest + string.join(collect, "") | 448 rest = rest + "".join(collect) |
| 361 del self.program[i:] | 449 del self.program[i:] |
| 362 if text: | 450 if text: |
| 363 self.emit("rawtext", text) | 451 self.emit("rawtext", text) |
| 364 return rest | 452 return rest |
| 365 return None | 453 return None |
| 366 | 454 |
| 367 def replaceAttrs(self, attrlist, repldict): | 455 def replaceAttrs(self, attrlist, repldict): |
| 456 # Each entry in attrlist starts like (name, value). | |
| 457 # Result is (name, value, action, expr, xlat) if there is a | |
| 458 # tal:attributes entry for that attribute. Additional attrs | |
| 459 # defined only by tal:attributes are added here. | |
| 460 # | |
| 461 # (name, value, action, expr, xlat) | |
| 368 if not repldict: | 462 if not repldict: |
| 369 return attrlist | 463 return attrlist |
| 370 newlist = [] | 464 newlist = [] |
| 371 for item in attrlist: | 465 for item in attrlist: |
| 372 key = item[0] | 466 key = item[0] |
| 373 if repldict.has_key(key): | 467 if repldict.has_key(key): |
| 374 item = item[:2] + ("replace", repldict[key]) | 468 expr, xlat, msgid = repldict[key] |
| 469 item = item[:2] + ("replace", expr, xlat, msgid) | |
| 375 del repldict[key] | 470 del repldict[key] |
| 376 newlist.append(item) | 471 newlist.append(item) |
| 377 for key, value in repldict.items(): # Add dynamic-only attributes | 472 # Add dynamic-only attributes |
| 378 item = (key, None, "insert", value) | 473 for key, (expr, xlat, msgid) in repldict.items(): |
| 379 newlist.append(item) | 474 newlist.append((key, None, "insert", expr, xlat, msgid)) |
| 380 return newlist | 475 return newlist |
| 381 | 476 |
| 382 def emitStartElement(self, name, attrlist, taldict, metaldict, | 477 def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict, |
| 383 position=(None, None), isend=0): | 478 position=(None, None), isend=0): |
| 384 if not taldict and not metaldict: | 479 if not taldict and not metaldict and not i18ndict: |
| 385 # Handle the simple, common case | 480 # Handle the simple, common case |
| 386 self.emitStartTag(name, attrlist, isend) | 481 self.emitStartTag(name, attrlist, isend) |
| 387 self.todoPush({}) | 482 self.todoPush({}) |
| 388 if isend: | 483 if isend: |
| 389 self.emitEndElement(name, isend) | 484 self.emitEndElement(name, isend) |
| 390 return | 485 return |
| 391 | 486 |
| 392 self.position = position | 487 self.position = position |
| 393 for key, value in taldict.items(): | 488 for key, value in taldict.items(): |
| 394 if key not in KNOWN_TAL_ATTRIBUTES: | 489 if key not in TALDefs.KNOWN_TAL_ATTRIBUTES: |
| 395 raise TALError("bad TAL attribute: " + `key`, position) | 490 raise TALError("bad TAL attribute: " + `key`, position) |
| 396 if not (value or key == 'omit-tag'): | 491 if not (value or key == 'omit-tag'): |
| 397 raise TALError("missing value for TAL attribute: " + | 492 raise TALError("missing value for TAL attribute: " + |
| 398 `key`, position) | 493 `key`, position) |
| 399 for key, value in metaldict.items(): | 494 for key, value in metaldict.items(): |
| 400 if key not in KNOWN_METAL_ATTRIBUTES: | 495 if key not in TALDefs.KNOWN_METAL_ATTRIBUTES: |
| 401 raise METALError("bad METAL attribute: " + `key`, | 496 raise METALError("bad METAL attribute: " + `key`, |
| 402 position) | 497 position) |
| 403 if not value: | 498 if not value: |
| 404 raise TALError("missing value for METAL attribute: " + | 499 raise TALError("missing value for METAL attribute: " + |
| 405 `key`, position) | 500 `key`, position) |
| 501 for key, value in i18ndict.items(): | |
| 502 if key not in TALDefs.KNOWN_I18N_ATTRIBUTES: | |
| 503 raise I18NError("bad i18n attribute: " + `key`, position) | |
| 504 if not value and key in ("attributes", "data", "id"): | |
| 505 raise I18NError("missing value for i18n attribute: " + | |
| 506 `key`, position) | |
| 406 todo = {} | 507 todo = {} |
| 407 defineMacro = metaldict.get("define-macro") | 508 defineMacro = metaldict.get("define-macro") |
| 408 useMacro = metaldict.get("use-macro") | 509 useMacro = metaldict.get("use-macro") |
| 409 defineSlot = metaldict.get("define-slot") | 510 defineSlot = metaldict.get("define-slot") |
| 410 fillSlot = metaldict.get("fill-slot") | 511 fillSlot = metaldict.get("fill-slot") |
| 415 replace = taldict.get("replace") | 516 replace = taldict.get("replace") |
| 416 attrsubst = taldict.get("attributes") | 517 attrsubst = taldict.get("attributes") |
| 417 onError = taldict.get("on-error") | 518 onError = taldict.get("on-error") |
| 418 omitTag = taldict.get("omit-tag") | 519 omitTag = taldict.get("omit-tag") |
| 419 TALtag = taldict.get("tal tag") | 520 TALtag = taldict.get("tal tag") |
| 521 i18nattrs = i18ndict.get("attributes") | |
| 522 # Preserve empty string if implicit msgids are used. We'll generate | |
| 523 # code with the msgid='' and calculate the right implicit msgid during | |
| 524 # interpretation phase. | |
| 525 msgid = i18ndict.get("translate") | |
| 526 varname = i18ndict.get('name') | |
| 527 i18ndata = i18ndict.get('data') | |
| 528 | |
| 529 if varname and not self.i18nLevel: | |
| 530 raise I18NError( | |
| 531 "i18n:name can only occur inside a translation unit", | |
| 532 position) | |
| 533 | |
| 534 if i18ndata and not msgid: | |
| 535 raise I18NError("i18n:data must be accompanied by i18n:translate", | |
| 536 position) | |
| 537 | |
| 420 if len(metaldict) > 1 and (defineMacro or useMacro): | 538 if len(metaldict) > 1 and (defineMacro or useMacro): |
| 421 raise METALError("define-macro and use-macro cannot be used " | 539 raise METALError("define-macro and use-macro cannot be used " |
| 422 "together or with define-slot or fill-slot", | 540 "together or with define-slot or fill-slot", |
| 423 position) | 541 position) |
| 424 if content and replace: | 542 if replace: |
| 425 raise TALError("content and replace are mutually exclusive", | 543 if content: |
| 426 position) | 544 raise TALError( |
| 545 "tal:content and tal:replace are mutually exclusive", | |
| 546 position) | |
| 547 if msgid is not None: | |
| 548 raise I18NError( | |
| 549 "i18n:translate and tal:replace are mutually exclusive", | |
| 550 position) | |
| 427 | 551 |
| 428 repeatWhitespace = None | 552 repeatWhitespace = None |
| 429 if repeat: | 553 if repeat: |
| 430 # Hack to include preceding whitespace in the loop program | 554 # Hack to include preceding whitespace in the loop program |
| 431 repeatWhitespace = self.unEmitNewlineWhitespace() | 555 repeatWhitespace = self.unEmitNewlineWhitespace() |
| 439 self.emit("setSourceFile", self.source_file) | 563 self.emit("setSourceFile", self.source_file) |
| 440 todo["fillSlot"] = fillSlot | 564 todo["fillSlot"] = fillSlot |
| 441 self.inMacroUse = 0 | 565 self.inMacroUse = 0 |
| 442 else: | 566 else: |
| 443 if fillSlot: | 567 if fillSlot: |
| 444 raise METALError, ("fill-slot must be within a use-macro", | 568 raise METALError("fill-slot must be within a use-macro", |
| 445 position) | 569 position) |
| 446 if not self.inMacroUse: | 570 if not self.inMacroUse: |
| 447 if defineMacro: | 571 if defineMacro: |
| 448 self.pushProgram() | 572 self.pushProgram() |
| 449 self.emit("version", TAL_VERSION) | 573 self.emit("version", TAL_VERSION) |
| 450 self.emit("mode", self.xml and "xml" or "html") | 574 self.emit("mode", self.xml and "xml" or "html") |
| 457 self.pushProgram() | 581 self.pushProgram() |
| 458 todo["useMacro"] = useMacro | 582 todo["useMacro"] = useMacro |
| 459 self.inMacroUse = 1 | 583 self.inMacroUse = 1 |
| 460 if defineSlot: | 584 if defineSlot: |
| 461 if not self.inMacroDef: | 585 if not self.inMacroDef: |
| 462 raise METALError, ( | 586 raise METALError( |
| 463 "define-slot must be within a define-macro", | 587 "define-slot must be within a define-macro", |
| 464 position) | 588 position) |
| 465 self.pushProgram() | 589 self.pushProgram() |
| 466 todo["defineSlot"] = defineSlot | 590 todo["defineSlot"] = defineSlot |
| 467 | 591 |
| 468 if taldict: | 592 if defineSlot or i18ndict: |
| 593 | |
| 594 domain = i18ndict.get("domain") or self.i18nContext.domain | |
| 595 source = i18ndict.get("source") or self.i18nContext.source | |
| 596 target = i18ndict.get("target") or self.i18nContext.target | |
| 597 if ( domain != DEFAULT_DOMAIN | |
| 598 or source is not None | |
| 599 or target is not None): | |
| 600 self.i18nContext = TranslationContext(self.i18nContext, | |
| 601 domain=domain, | |
| 602 source=source, | |
| 603 target=target) | |
| 604 self.emit("beginI18nContext", | |
| 605 {"domain": domain, "source": source, | |
| 606 "target": target}) | |
| 607 todo["i18ncontext"] = 1 | |
| 608 if taldict or i18ndict: | |
| 469 dict = {} | 609 dict = {} |
| 470 for item in attrlist: | 610 for item in attrlist: |
| 471 key, value = item[:2] | 611 key, value = item[:2] |
| 472 dict[key] = value | 612 dict[key] = value |
| 473 self.emit("beginScope", dict) | 613 self.emit("beginScope", dict) |
| 491 todo["repeat"] = repeat | 631 todo["repeat"] = repeat |
| 492 self.pushProgram() | 632 self.pushProgram() |
| 493 if repeatWhitespace: | 633 if repeatWhitespace: |
| 494 self.emitText(repeatWhitespace) | 634 self.emitText(repeatWhitespace) |
| 495 if content: | 635 if content: |
| 496 todo["content"] = content | 636 if varname: |
| 497 if replace: | 637 todo['i18nvar'] = (varname, I18N_CONTENT, None) |
| 498 todo["replace"] = replace | 638 todo["content"] = content |
| 639 self.pushProgram() | |
| 640 else: | |
| 641 todo["content"] = content | |
| 642 elif replace: | |
| 643 # tal:replace w/ i18n:name has slightly different semantics. What | |
| 644 # we're actually replacing then is the contents of the ${name} | |
| 645 # placeholder. | |
| 646 if varname: | |
| 647 todo['i18nvar'] = (varname, I18N_EXPRESSION, replace) | |
| 648 else: | |
| 649 todo["replace"] = replace | |
| 499 self.pushProgram() | 650 self.pushProgram() |
| 651 # i18n:name w/o tal:replace uses the content as the interpolation | |
| 652 # dictionary values | |
| 653 elif varname: | |
| 654 todo['i18nvar'] = (varname, I18N_REPLACE, None) | |
| 655 self.pushProgram() | |
| 656 if msgid is not None: | |
| 657 self.i18nLevel += 1 | |
| 658 todo['msgid'] = msgid | |
| 659 if i18ndata: | |
| 660 todo['i18ndata'] = i18ndata | |
| 500 optTag = omitTag is not None or TALtag | 661 optTag = omitTag is not None or TALtag |
| 501 if optTag: | 662 if optTag: |
| 502 todo["optional tag"] = omitTag, TALtag | 663 todo["optional tag"] = omitTag, TALtag |
| 503 self.pushProgram() | 664 self.pushProgram() |
| 504 if attrsubst: | 665 if attrsubst or i18nattrs: |
| 505 repldict = parseAttributeReplacements(attrsubst) | 666 if attrsubst: |
| 667 repldict = TALDefs.parseAttributeReplacements(attrsubst, | |
| 668 self.xml) | |
| 669 else: | |
| 670 repldict = {} | |
| 671 if i18nattrs: | |
| 672 i18nattrs = _parseI18nAttributes(i18nattrs, attrlist, repldict, | |
| 673 self.position, self.xml, | |
| 674 self.source_file) | |
| 675 else: | |
| 676 i18nattrs = {} | |
| 677 # Convert repldict's name-->expr mapping to a | |
| 678 # name-->(compiled_expr, translate) mapping | |
| 506 for key, value in repldict.items(): | 679 for key, value in repldict.items(): |
| 507 repldict[key] = self.compileExpression(value) | 680 if i18nattrs.get(key, None): |
| 681 raise I18NError( | |
| 682 ("attribute [%s] cannot both be part of tal:attributes" + | |
| 683 " and have a msgid in i18n:attributes") % key, | |
| 684 position) | |
| 685 ce = self.compileExpression(value) | |
| 686 repldict[key] = ce, key in i18nattrs, i18nattrs.get(key) | |
| 687 for key in i18nattrs: | |
| 688 if not repldict.has_key(key): | |
| 689 repldict[key] = None, 1, i18nattrs.get(key) | |
| 508 else: | 690 else: |
| 509 repldict = {} | 691 repldict = {} |
| 510 if replace: | 692 if replace: |
| 511 todo["repldict"] = repldict | 693 todo["repldict"] = repldict |
| 512 repldict = {} | 694 repldict = {} |
| 513 self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) | 695 self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) |
| 514 if optTag: | 696 if optTag: |
| 515 self.pushProgram() | 697 self.pushProgram() |
| 516 if content: | 698 if content and not varname: |
| 699 self.pushProgram() | |
| 700 if msgid is not None: | |
| 701 self.pushProgram() | |
| 702 if content and varname: | |
| 517 self.pushProgram() | 703 self.pushProgram() |
| 518 if todo and position != (None, None): | 704 if todo and position != (None, None): |
| 519 todo["position"] = position | 705 todo["position"] = position |
| 520 self.todoPush(todo) | 706 self.todoPush(todo) |
| 521 if isend: | 707 if isend: |
| 541 onError = todo.get("onError") | 727 onError = todo.get("onError") |
| 542 define = todo.get("define") | 728 define = todo.get("define") |
| 543 repldict = todo.get("repldict", {}) | 729 repldict = todo.get("repldict", {}) |
| 544 scope = todo.get("scope") | 730 scope = todo.get("scope") |
| 545 optTag = todo.get("optional tag") | 731 optTag = todo.get("optional tag") |
| 732 msgid = todo.get('msgid') | |
| 733 i18ncontext = todo.get("i18ncontext") | |
| 734 varname = todo.get('i18nvar') | |
| 735 i18ndata = todo.get('i18ndata') | |
| 546 | 736 |
| 547 if implied > 0: | 737 if implied > 0: |
| 548 if defineMacro or useMacro or defineSlot or fillSlot: | 738 if defineMacro or useMacro or defineSlot or fillSlot: |
| 549 exc = METALError | 739 exc = METALError |
| 550 what = "METAL" | 740 what = "METAL" |
| 552 exc = TALError | 742 exc = TALError |
| 553 what = "TAL" | 743 what = "TAL" |
| 554 raise exc("%s attributes on <%s> require explicit </%s>" % | 744 raise exc("%s attributes on <%s> require explicit </%s>" % |
| 555 (what, name, name), position) | 745 (what, name, name), position) |
| 556 | 746 |
| 747 # If there's no tal:content or tal:replace in the tag with the | |
| 748 # i18n:name, tal:replace is the default. | |
| 557 if content: | 749 if content: |
| 558 self.emitSubstitution(content, {}) | 750 self.emitSubstitution(content, {}) |
| 751 # If we're looking at an implicit msgid, emit the insertTranslation | |
| 752 # opcode now, so that the end tag doesn't become part of the implicit | |
| 753 # msgid. If we're looking at an explicit msgid, it's better to emit | |
| 754 # the opcode after the i18nVariable opcode so we can better handle | |
| 755 # tags with both of them in them (and in the latter case, the contents | |
| 756 # would be thrown away for msgid purposes). | |
| 757 # | |
| 758 # Still, we should emit insertTranslation opcode before i18nVariable | |
| 759 # in case tal:content, i18n:translate and i18n:name in the same tag | |
| 760 if msgid is not None: | |
| 761 if (not varname) or ( | |
| 762 varname and (varname[1] == I18N_CONTENT)): | |
| 763 self.emitTranslation(msgid, i18ndata) | |
| 764 self.i18nLevel -= 1 | |
| 559 if optTag: | 765 if optTag: |
| 560 self.emitOptTag(name, optTag, isend) | 766 self.emitOptTag(name, optTag, isend) |
| 561 elif not isend: | 767 elif not isend: |
| 768 # If we're processing the end tag for a tag that contained | |
| 769 # i18n:name, we need to make sure that optimize() won't collect | |
| 770 # immediately following end tags into the same rawtextOffset, so | |
| 771 # put a spacer here that the optimizer will recognize. | |
| 772 if varname: | |
| 773 self.emit('noop') | |
| 562 self.emitEndTag(name) | 774 self.emitEndTag(name) |
| 775 # If i18n:name appeared in the same tag as tal:replace then we're | |
| 776 # going to do the substitution a little bit differently. The results | |
| 777 # of the expression go into the i18n substitution dictionary. | |
| 563 if replace: | 778 if replace: |
| 564 self.emitSubstitution(replace, repldict) | 779 self.emitSubstitution(replace, repldict) |
| 780 elif varname: | |
| 781 # o varname[0] is the variable name | |
| 782 # o varname[1] is either | |
| 783 # - I18N_REPLACE for implicit tal:replace | |
| 784 # - I18N_CONTENT for tal:content | |
| 785 # - I18N_EXPRESSION for explicit tal:replace | |
| 786 # o varname[2] will be None for the first two actions and the | |
| 787 # replacement tal expression for the third action. | |
| 788 assert (varname[1] | |
| 789 in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION]) | |
| 790 self.emitI18nVariable(varname) | |
| 791 # Do not test for "msgid is not None", i.e. we only want to test for | |
| 792 # explicit msgids here. See comment above. | |
| 793 if msgid is not None: | |
| 794 # in case tal:content, i18n:translate and i18n:name in the | |
| 795 # same tag insertTranslation opcode has already been | |
| 796 # emitted | |
| 797 if varname and (varname[1] <> I18N_CONTENT): | |
| 798 self.emitTranslation(msgid, i18ndata) | |
| 565 if repeat: | 799 if repeat: |
| 566 self.emitRepeat(repeat) | 800 self.emitRepeat(repeat) |
| 567 if condition: | 801 if condition: |
| 568 self.emitCondition(condition) | 802 self.emitCondition(condition) |
| 569 if onError: | 803 if onError: |
| 570 self.emitOnError(name, onError, optTag and optTag[1], isend) | 804 self.emitOnError(name, onError, optTag and optTag[1], isend) |
| 571 if scope: | 805 if scope: |
| 572 self.emit("endScope") | 806 self.emit("endScope") |
| 807 if i18ncontext: | |
| 808 self.emit("endI18nContext") | |
| 809 assert self.i18nContext.parent is not None | |
| 810 self.i18nContext = self.i18nContext.parent | |
| 573 if defineSlot: | 811 if defineSlot: |
| 574 self.emitDefineSlot(defineSlot) | 812 self.emitDefineSlot(defineSlot) |
| 575 if fillSlot: | 813 if fillSlot: |
| 576 self.emitFillSlot(fillSlot) | 814 self.emitFillSlot(fillSlot) |
| 577 if useMacro: | 815 if useMacro: |
| 578 self.emitUseMacro(useMacro) | 816 self.emitUseMacro(useMacro) |
| 579 if defineMacro: | 817 if defineMacro: |
| 580 self.emitDefineMacro(defineMacro) | 818 self.emitDefineMacro(defineMacro) |
| 581 | 819 |
| 820 | |
| 821 def _parseI18nAttributes(i18nattrs, attrlist, repldict, position, | |
| 822 xml, source_file): | |
| 823 | |
| 824 def addAttribute(dic, attr, msgid, position, xml): | |
| 825 if not xml: | |
| 826 attr = attr.lower() | |
| 827 if attr in dic: | |
| 828 raise TALError( | |
| 829 "attribute may only be specified once in i18n:attributes: " | |
| 830 + attr, | |
| 831 position) | |
| 832 dic[attr] = msgid | |
| 833 | |
| 834 d = {} | |
| 835 if ';' in i18nattrs: | |
| 836 i18nattrlist = i18nattrs.split(';') | |
| 837 i18nattrlist = [attr.strip().split() | |
| 838 for attr in i18nattrlist if attr.strip()] | |
| 839 for parts in i18nattrlist: | |
| 840 if len(parts) > 2: | |
| 841 raise TALError("illegal i18n:attributes specification: %r" | |
| 842 % parts, position) | |
| 843 if len(parts) == 2: | |
| 844 attr, msgid = parts | |
| 845 else: | |
| 846 # len(parts) == 1 | |
| 847 attr = parts[0] | |
| 848 msgid = None | |
| 849 addAttribute(d, attr, msgid, position, xml) | |
| 850 else: | |
| 851 i18nattrlist = i18nattrs.split() | |
| 852 if len(i18nattrlist) == 2: | |
| 853 staticattrs = [attr[0] for attr in attrlist if len(attr) == 2] | |
| 854 if (not i18nattrlist[1] in staticattrs) and ( | |
| 855 not i18nattrlist[1] in repldict): | |
| 856 attr, msgid = i18nattrlist | |
| 857 addAttribute(d, attr, msgid, position, xml) | |
| 858 else: | |
| 859 msgid = None | |
| 860 for attr in i18nattrlist: | |
| 861 addAttribute(d, attr, msgid, position, xml) | |
| 862 else: | |
| 863 msgid = None | |
| 864 for attr in i18nattrlist: | |
| 865 addAttribute(d, attr, msgid, position, xml) | |
| 866 return d | |
| 867 | |
| 582 def test(): | 868 def test(): |
| 583 t = TALGenerator() | 869 t = TALGenerator() |
| 584 t.pushProgram() | 870 t.pushProgram() |
| 585 t.emit("bar") | 871 t.emit("bar") |
| 586 p = t.popProgram() | 872 p = t.popProgram() |
