1 ##############################################################################
2 #
3 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
4 # All Rights Reserved.
5 #
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.
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
10 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11 # FOR A PARTICULAR PURPOSE.
12 #
13 ##############################################################################
14 """
15 Code generator for TALInterpreter intermediate code.
16 """
18 import re
19 import cgi
21 import TALDefs
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
28 I18N_REPLACE = 1
29 I18N_CONTENT = 2
30 I18N_EXPRESSION = 3
32 _name_rx = re.compile(NAME_RE)
35 class TALGenerator:
37 inMacroUse = 0
38 inMacroDef = 0
39 source_file = None
41 def __init__(self, expressionCompiler=None, xml=1, source_file=None):
42 if not expressionCompiler:
43 from DummyEngine import DummyEngine
44 expressionCompiler = DummyEngine()
45 self.expressionCompiler = expressionCompiler
46 self.CompilerError = expressionCompiler.getCompilerError()
47 # This holds the emitted opcodes representing the input
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.
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
56 self.todoStack = []
57 self.macros = {}
58 self.slots = {}
59 self.slotStack = []
60 self.xml = xml
61 self.emit("version", TAL_VERSION)
62 self.emit("mode", xml and "xml" or "html")
63 if source_file is not None:
64 self.source_file = source_file
65 self.emit("setSourceFile", source_file)
66 self.i18nContext = TranslationContext()
67 self.i18nLevel = 0
69 def getCode(self):
70 assert not self.stack
71 assert not self.todoStack
72 return self.optimize(self.program), self.macros
74 def optimize(self, program):
75 output = []
76 collect = []
77 cursor = 0
78 if self.xml:
79 endsep = "/>"
80 else:
81 endsep = " />"
82 for cursor in xrange(len(program)+1):
83 try:
84 item = program[cursor]
85 except IndexError:
86 item = (None, None)
87 opcode = item[0]
88 if opcode == "rawtext":
89 collect.append(item[1])
90 continue
91 if opcode == "endTag":
92 collect.append("</%s>" % item[1])
93 continue
94 if opcode == "startTag":
95 if self.optimizeStartTag(collect, item[1], item[2], ">"):
96 continue
97 if opcode == "startEndTag":
98 if self.optimizeStartTag(collect, item[1], item[2], endsep):
99 continue
100 if opcode in ("beginScope", "endScope"):
101 # Push *Scope instructions in front of any text instructions;
102 # this allows text instructions separated only by *Scope
103 # instructions to be joined together.
104 output.append(self.optimizeArgsList(item))
105 continue
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)
113 if text:
114 i = text.rfind("\n")
115 if i >= 0:
116 i = len(text) - (i + 1)
117 output.append(("rawtextColumn", (text, i)))
118 else:
119 output.append(("rawtextOffset", (text, len(text))))
120 if opcode != None:
121 output.append(self.optimizeArgsList(item))
122 collect = []
123 return self.optimizeCommonTriple(output)
125 def optimizeArgsList(self, item):
126 if len(item) == 2:
127 return item
128 else:
129 return item[0], tuple(item[1:])
131 # These codes are used to indicate what sort of special actions
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 #
151 def optimizeStartTag(self, collect, name, attrlist, end):
152 # return true if the tag can be converted to plain text
153 if not attrlist:
154 collect.append("<%s%s" % (name, end))
155 return 1
156 opt = 1
157 new = ["<" + name]
158 for i in range(len(attrlist)):
159 item = attrlist[i]
160 if len(item) > 2:
161 opt = 0
162 name, value, action = item[:3]
163 attrlist[i] = (name, value, action) + item[3:]
164 else:
165 if item[1] is None:
166 s = item[0]
167 else:
168 s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1]))
169 attrlist[i] = item[0], s
170 new.append(" " + s)
171 # if no non-optimizable attributes were found, convert to plain text
172 if opt:
173 new.append(end)
174 collect.extend(new)
175 return opt
177 def optimizeCommonTriple(self, program):
178 if len(program) < 3:
179 return program
180 output = program[:2]
181 prev2, prev1 = output
182 for item in program[2:]:
183 if ( item[0] == "beginScope"
184 and prev1[0] == "setPosition"
185 and prev2[0] == "rawtextColumn"):
186 position = output.pop()[1]
187 text, column = output.pop()[1]
188 prev1 = None, None
189 closeprev = 0
190 if output and output[-1][0] == "endScope":
191 closeprev = 1
192 output.pop()
193 item = ("rawtextBeginScope",
194 (text, column, position, closeprev, item[1]))
195 output.append(item)
196 prev2 = prev1
197 prev1 = item
198 return output
200 def todoPush(self, todo):
201 self.todoStack.append(todo)
203 def todoPop(self):
204 return self.todoStack.pop()
206 def compileExpression(self, expr):
207 try:
208 return self.expressionCompiler.compile(expr)
209 except self.CompilerError, err:
210 raise TALError('%s in expression %s' % (err.args[0], `expr`),
211 self.position)
213 def pushProgram(self):
214 self.stack.append(self.program)
215 self.program = []
217 def popProgram(self):
218 program = self.program
219 self.program = self.stack.pop()
220 return self.optimize(program)
222 def pushSlots(self):
223 self.slotStack.append(self.slots)
224 self.slots = {}
226 def popSlots(self):
227 slots = self.slots
228 self.slots = self.slotStack.pop()
229 return slots
231 def emit(self, *instruction):
232 self.program.append(instruction)
234 def emitStartTag(self, name, attrlist, isend=0):
235 if isend:
236 opcode = "startEndTag"
237 else:
238 opcode = "startTag"
239 self.emit(opcode, name, attrlist)
241 def emitEndTag(self, name):
242 if self.xml and self.program and self.program[-1][0] == "startTag":
243 # Minimize empty element
244 self.program[-1] = ("startEndTag",) + self.program[-1][1:]
245 else:
246 self.emit("endTag", name)
248 def emitOptTag(self, name, optTag, isend):
249 program = self.popProgram() #block
250 start = self.popProgram() #start tag
251 if (isend or not program) and self.xml:
252 # Minimize empty element
253 start[-1] = ("startEndTag",) + start[-1][1:]
254 isend = 1
255 cexpr = optTag[0]
256 if cexpr:
257 cexpr = self.compileExpression(optTag[0])
258 self.emit("optTag", name, cexpr, optTag[1], isend, start, program)
260 def emitRawText(self, text):
261 self.emit("rawtext", text)
263 def emitText(self, text):
264 self.emitRawText(cgi.escape(text))
266 def emitDefines(self, defines):
267 for part in TALDefs.splitParts(defines):
268 m = re.match(
269 r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part)
270 if not m:
271 raise TALError("invalid define syntax: " + `part`,
272 self.position)
273 scope, name, expr = m.group(1, 2, 3)
274 scope = scope or "local"
275 cexpr = self.compileExpression(expr)
276 if scope == "local":
277 self.emit("setLocal", name, cexpr)
278 else:
279 self.emit("setGlobal", name, cexpr)
281 def emitOnError(self, name, onError, TALtag, isend):
282 block = self.popProgram()
283 key, expr = parseSubstitution(onError)
284 cexpr = self.compileExpression(expr)
285 if key == "text":
286 self.emit("insertText", cexpr, [])
287 else:
288 assert key == "structure"
289 self.emit("insertStructure", cexpr, {}, [])
290 if TALtag:
291 self.emitOptTag(name, (None, 1), isend)
292 else:
293 self.emitEndTag(name)
294 handler = self.popProgram()
295 self.emit("onError", block, handler)
297 def emitCondition(self, expr):
298 cexpr = self.compileExpression(expr)
299 program = self.popProgram()
300 self.emit("condition", cexpr, program)
302 def emitRepeat(self, arg):
303 m = re.match("(?s)\s*(%s)\s+(.*)\Z" % NAME_RE, arg)
304 if not m:
305 raise TALError("invalid repeat syntax: " + `arg`,
306 self.position)
307 name, expr = m.group(1, 2)
308 cexpr = self.compileExpression(expr)
309 program = self.popProgram()
310 self.emit("loop", name, cexpr, program)
312 def emitSubstitution(self, arg, attrDict={}):
313 key, expr = parseSubstitution(arg)
314 cexpr = self.compileExpression(expr)
315 program = self.popProgram()
316 if key == "text":
317 self.emit("insertText", cexpr, program)
318 else:
319 assert key == "structure"
320 self.emit("insertStructure", cexpr, attrDict, program)
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)
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)
369 def emitDefineMacro(self, macroName):
370 program = self.popProgram()
371 macroName = macroName.strip()
372 if self.macros.has_key(macroName):
373 raise METALError("duplicate macro definition: %s" % `macroName`,
374 self.position)
375 if not re.match('%s$' % NAME_RE, macroName):
376 raise METALError("invalid macro name: %s" % `macroName`,
377 self.position)
378 self.macros[macroName] = program
379 self.inMacroDef = self.inMacroDef - 1
380 self.emit("defineMacro", macroName, program)
382 def emitUseMacro(self, expr):
383 cexpr = self.compileExpression(expr)
384 program = self.popProgram()
385 self.inMacroUse = 0
386 self.emit("useMacro", expr, cexpr, self.popSlots(), program)
388 def emitDefineSlot(self, slotName):
389 program = self.popProgram()
390 slotName = slotName.strip()
391 if not re.match('%s$' % NAME_RE, slotName):
392 raise METALError("invalid slot name: %s" % `slotName`,
393 self.position)
394 self.emit("defineSlot", slotName, program)
396 def emitFillSlot(self, slotName):
397 program = self.popProgram()
398 slotName = slotName.strip()
399 if self.slots.has_key(slotName):
400 raise METALError("duplicate fill-slot name: %s" % `slotName`,
401 self.position)
402 if not re.match('%s$' % NAME_RE, slotName):
403 raise METALError("invalid slot name: %s" % `slotName`,
404 self.position)
405 self.slots[slotName] = program
406 self.inMacroUse = 1
407 self.emit("fillSlot", slotName, program)
409 def unEmitWhitespace(self):
410 collect = []
411 i = len(self.program) - 1
412 while i >= 0:
413 item = self.program[i]
414 if item[0] != "rawtext":
415 break
416 text = item[1]
417 if not re.match(r"\A\s*\Z", text):
418 break
419 collect.append(text)
420 i = i-1
421 del self.program[i+1:]
422 if i >= 0 and self.program[i][0] == "rawtext":
423 text = self.program[i][1]
424 m = re.search(r"\s+\Z", text)
425 if m:
426 self.program[i] = ("rawtext", text[:m.start()])
427 collect.append(m.group())
428 collect.reverse()
429 return "".join(collect)
431 def unEmitNewlineWhitespace(self):
432 collect = []
433 i = len(self.program)
434 while i > 0:
435 i = i-1
436 item = self.program[i]
437 if item[0] != "rawtext":
438 break
439 text = item[1]
440 if re.match(r"\A[ \t]*\Z", text):
441 collect.append(text)
442 continue
443 m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text)
444 if not m:
445 break
446 text, rest = m.group(1, 2)
447 collect.reverse()
448 rest = rest + "".join(collect)
449 del self.program[i:]
450 if text:
451 self.emit("rawtext", text)
452 return rest
453 return None
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)
462 if not repldict:
463 return attrlist
464 newlist = []
465 for item in attrlist:
466 key = item[0]
467 if repldict.has_key(key):
468 expr, xlat, msgid = repldict[key]
469 item = item[:2] + ("replace", expr, xlat, msgid)
470 del repldict[key]
471 newlist.append(item)
472 # Add dynamic-only attributes
473 for key, (expr, xlat, msgid) in repldict.items():
474 newlist.append((key, None, "insert", expr, xlat, msgid))
475 return newlist
477 def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
478 position=(None, None), isend=0):
479 if not taldict and not metaldict and not i18ndict:
480 # Handle the simple, common case
481 self.emitStartTag(name, attrlist, isend)
482 self.todoPush({})
483 if isend:
484 self.emitEndElement(name, isend)
485 return
487 self.position = position
488 for key, value in taldict.items():
489 if key not in TALDefs.KNOWN_TAL_ATTRIBUTES:
490 raise TALError("bad TAL attribute: " + `key`, position)
491 if not (value or key == 'omit-tag'):
492 raise TALError("missing value for TAL attribute: " +
493 `key`, position)
494 for key, value in metaldict.items():
495 if key not in TALDefs.KNOWN_METAL_ATTRIBUTES:
496 raise METALError("bad METAL attribute: " + `key`,
497 position)
498 if not value:
499 raise TALError("missing value for METAL attribute: " +
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)
507 todo = {}
508 defineMacro = metaldict.get("define-macro")
509 useMacro = metaldict.get("use-macro")
510 defineSlot = metaldict.get("define-slot")
511 fillSlot = metaldict.get("fill-slot")
512 define = taldict.get("define")
513 condition = taldict.get("condition")
514 repeat = taldict.get("repeat")
515 content = taldict.get("content")
516 replace = taldict.get("replace")
517 attrsubst = taldict.get("attributes")
518 onError = taldict.get("on-error")
519 omitTag = taldict.get("omit-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')
529 if varname and not self.i18nLevel:
530 raise I18NError(
531 "i18n:name can only occur inside a translation unit",
532 position)
534 if i18ndata and not msgid:
535 raise I18NError("i18n:data must be accompanied by i18n:translate",
536 position)
538 if len(metaldict) > 1 and (defineMacro or useMacro):
539 raise METALError("define-macro and use-macro cannot be used "
540 "together or with define-slot or fill-slot",
541 position)
542 if replace:
543 if content:
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)
552 repeatWhitespace = None
553 if repeat:
554 # Hack to include preceding whitespace in the loop program
555 repeatWhitespace = self.unEmitNewlineWhitespace()
556 if position != (None, None):
557 # XXX at some point we should insist on a non-trivial position
558 self.emit("setPosition", position)
559 if self.inMacroUse:
560 if fillSlot:
561 self.pushProgram()
562 if self.source_file is not None:
563 self.emit("setSourceFile", self.source_file)
564 todo["fillSlot"] = fillSlot
565 self.inMacroUse = 0
566 else:
567 if fillSlot:
568 raise METALError("fill-slot must be within a use-macro",
569 position)
570 if not self.inMacroUse:
571 if defineMacro:
572 self.pushProgram()
573 self.emit("version", TAL_VERSION)
574 self.emit("mode", self.xml and "xml" or "html")
575 if self.source_file is not None:
576 self.emit("setSourceFile", self.source_file)
577 todo["defineMacro"] = defineMacro
578 self.inMacroDef = self.inMacroDef + 1
579 if useMacro:
580 self.pushSlots()
581 self.pushProgram()
582 todo["useMacro"] = useMacro
583 self.inMacroUse = 1
584 if defineSlot:
585 if not self.inMacroDef:
586 raise METALError(
587 "define-slot must be within a define-macro",
588 position)
589 self.pushProgram()
590 todo["defineSlot"] = defineSlot
592 if defineSlot or i18ndict:
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:
609 dict = {}
610 for item in attrlist:
611 key, value = item[:2]
612 dict[key] = value
613 self.emit("beginScope", dict)
614 todo["scope"] = 1
615 if onError:
616 self.pushProgram() # handler
617 if TALtag:
618 self.pushProgram() # start
619 self.emitStartTag(name, list(attrlist)) # Must copy attrlist!
620 if TALtag:
621 self.pushProgram() # start
622 self.pushProgram() # block
623 todo["onError"] = onError
624 if define:
625 self.emitDefines(define)
626 todo["define"] = define
627 if condition:
628 self.pushProgram()
629 todo["condition"] = condition
630 if repeat:
631 todo["repeat"] = repeat
632 self.pushProgram()
633 if repeatWhitespace:
634 self.emitText(repeatWhitespace)
635 if content:
636 if varname:
637 todo['i18nvar'] = (varname, I18N_CONTENT, None)
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
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
661 optTag = omitTag is not None or TALtag
662 if optTag:
663 todo["optional tag"] = omitTag, TALtag
664 self.pushProgram()
665 if attrsubst or i18nattrs:
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
679 for key, value in repldict.items():
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)
690 else:
691 repldict = {}
692 if replace:
693 todo["repldict"] = repldict
694 repldict = {}
695 self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
696 if optTag:
697 self.pushProgram()
698 if content and not varname:
699 self.pushProgram()
700 if msgid is not None:
701 self.pushProgram()
702 if content and varname:
703 self.pushProgram()
704 if todo and position != (None, None):
705 todo["position"] = position
706 self.todoPush(todo)
707 if isend:
708 self.emitEndElement(name, isend)
710 def emitEndElement(self, name, isend=0, implied=0):
711 todo = self.todoPop()
712 if not todo:
713 # Shortcut
714 if not isend:
715 self.emitEndTag(name)
716 return
718 self.position = position = todo.get("position", (None, None))
719 defineMacro = todo.get("defineMacro")
720 useMacro = todo.get("useMacro")
721 defineSlot = todo.get("defineSlot")
722 fillSlot = todo.get("fillSlot")
723 repeat = todo.get("repeat")
724 content = todo.get("content")
725 replace = todo.get("replace")
726 condition = todo.get("condition")
727 onError = todo.get("onError")
728 define = todo.get("define")
729 repldict = todo.get("repldict", {})
730 scope = todo.get("scope")
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')
737 if implied > 0:
738 if defineMacro or useMacro or defineSlot or fillSlot:
739 exc = METALError
740 what = "METAL"
741 else:
742 exc = TALError
743 what = "TAL"
744 raise exc("%s attributes on <%s> require explicit </%s>" %
745 (what, name, name), position)
747 # If there's no tal:content or tal:replace in the tag with the
748 # i18n:name, tal:replace is the default.
749 if 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
765 if optTag:
766 self.emitOptTag(name, optTag, 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')
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.
778 if replace:
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)
799 if repeat:
800 self.emitRepeat(repeat)
801 if condition:
802 self.emitCondition(condition)
803 if onError:
804 self.emitOnError(name, onError, optTag and optTag[1], isend)
805 if scope:
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
811 if defineSlot:
812 self.emitDefineSlot(defineSlot)
813 if fillSlot:
814 self.emitFillSlot(fillSlot)
815 if useMacro:
816 self.emitUseMacro(useMacro)
817 if defineMacro:
818 self.emitDefineMacro(defineMacro)
821 def _parseI18nAttributes(i18nattrs, attrlist, repldict, position,
822 xml, source_file):
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
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
868 def test():
869 t = TALGenerator()
870 t.pushProgram()
871 t.emit("bar")
872 p = t.popProgram()
873 t.emit("foo", p)
875 if __name__ == "__main__":
876 test()