Premiere version : mise en route du suivi.
[auf_roundup.git] / roundup / cgi / TAL / .svn / text-base / TALGenerator.py.svn-base
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 """
17
18 import re
19 import cgi
20
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
34
35 class TALGenerator:
36
37     inMacroUse = 0
38     inMacroDef = 0
39     source_file = None
40
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
68
69     def getCode(self):
70         assert not self.stack
71         assert not self.todoStack
72         return self.optimize(self.program), self.macros
73
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)
124
125     def optimizeArgsList(self, item):
126         if len(item) == 2:
127             return item
128         else:
129             return item[0], tuple(item[1:])
130
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
176
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
199
200     def todoPush(self, todo):
201         self.todoStack.append(todo)
202
203     def todoPop(self):
204         return self.todoStack.pop()
205
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)
212
213     def pushProgram(self):
214         self.stack.append(self.program)
215         self.program = []
216
217     def popProgram(self):
218         program = self.program
219         self.program = self.stack.pop()
220         return self.optimize(program)
221
222     def pushSlots(self):
223         self.slotStack.append(self.slots)
224         self.slots = {}
225
226     def popSlots(self):
227         slots = self.slots
228         self.slots = self.slotStack.pop()
229         return slots
230
231     def emit(self, *instruction):
232         self.program.append(instruction)
233
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)
240
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)
247
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)
259
260     def emitRawText(self, text):
261         self.emit("rawtext", text)
262
263     def emitText(self, text):
264         self.emitRawText(cgi.escape(text))
265
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)
280
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)
296
297     def emitCondition(self, expr):
298         cexpr = self.compileExpression(expr)
299         program = self.popProgram()
300         self.emit("condition", cexpr, program)
301
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)
311
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)
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
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)
381
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)
387
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)
395
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)
408
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)
430
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
454
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
476
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
486
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')
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
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)
551
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
591
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:
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)
709
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
717
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')
736
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)
746
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)
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
868 def test():
869     t = TALGenerator()
870     t.pushProgram()
871     t.emit("bar")
872     p = t.popProgram()
873     t.emit("foo", p)
874
875 if __name__ == "__main__":
876     test()