Premiere version : mise en route du suivi.
[auf_roundup.git] / roundup / cgi / TAL / .svn / text-base / TALInterpreter.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 # Modifications for Roundup:
15 # 1. implemented ustr as str
16 """
17 Interpreter for a pre-compiled TAL program.
18 """
19
20 import sys
21 import getopt
22 import re
23 from types import ListType
24 from cgi import escape
25 # Do not use cStringIO here!  It's not unicode aware. :(
26 from StringIO import StringIO
27 #from DocumentTemplate.DT_Util import ustr
28 ustr = str
29
30 from TALDefs import TAL_VERSION, TALError, METALError, attrEscape
31 from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode
32 from TALGenerator import TALGenerator
33 from TranslationContext import TranslationContext
34
35 BOOLEAN_HTML_ATTRS = [
36     # List of Boolean attributes in HTML that should be rendered in
37     # minimized form (e.g. <img ismap> rather than <img ismap="">)
38     # From http://www.w3.org/TR/xhtml1/#guidelines (C.10)
39     # XXX The problem with this is that this is not valid XML and
40     # can't be parsed back!
41     "compact", "nowrap", "ismap", "declare", "noshade", "checked",
42     "disabled", "readonly", "multiple", "selected", "noresize",
43     "defer"
44 ]
45
46 def normalize(text):
47     # Now we need to normalize the whitespace in implicit message ids and
48     # implicit $name substitution values by stripping leading and trailing
49     # whitespace, and folding all internal whitespace to a single space.
50     return ' '.join(text.split())
51
52
53 NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
54 _interp_regex = re.compile(r'(?<!\$)(\$(?:%(n)s|{%(n)s}))' %({'n': NAME_RE}))
55 _get_var_regex = re.compile(r'%(n)s' %({'n': NAME_RE}))
56
57 def interpolate(text, mapping):
58     """Interpolate ${keyword} substitutions.
59
60     This is called when no translation is provided by the translation
61     service.
62     """
63     if not mapping:
64         return text
65     # Find all the spots we want to substitute.
66     to_replace = _interp_regex.findall(text)
67     # Now substitute with the variables in mapping.
68     for string in to_replace:
69         var = _get_var_regex.findall(string)[0]
70         if mapping.has_key(var):
71             # Call ustr because we may have an integer for instance.
72             subst = ustr(mapping[var])
73             try:
74                 text = text.replace(string, subst)
75             except UnicodeError:
76                 # subst contains high-bit chars...
77                 # As we have no way of knowing the correct encoding,
78                 # substitue something instead of raising an exception.
79                 subst = `subst`[1:-1]
80                 text = text.replace(string, subst)
81     return text
82
83
84 class AltTALGenerator(TALGenerator):
85
86     def __init__(self, repldict, expressionCompiler=None, xml=0):
87         self.repldict = repldict
88         self.enabled = 1
89         TALGenerator.__init__(self, expressionCompiler, xml)
90
91     def enable(self, enabled):
92         self.enabled = enabled
93
94     def emit(self, *args):
95         if self.enabled:
96             TALGenerator.emit(self, *args)
97
98     def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
99                          position=(None, None), isend=0):
100         metaldict = {}
101         taldict = {}
102         i18ndict = {}
103         if self.enabled and self.repldict:
104             taldict["attributes"] = "x x"
105         TALGenerator.emitStartElement(self, name, attrlist,
106                                       taldict, metaldict, i18ndict,
107                                       position, isend)
108
109     def replaceAttrs(self, attrlist, repldict):
110         if self.enabled and self.repldict:
111             repldict = self.repldict
112             self.repldict = None
113         return TALGenerator.replaceAttrs(self, attrlist, repldict)
114
115
116 class TALInterpreter:
117
118     def __init__(self, program, macros, engine, stream=None,
119                  debug=0, wrap=60, metal=1, tal=1, showtal=-1,
120                  strictinsert=1, stackLimit=100, i18nInterpolate=1):
121         self.program = program
122         self.macros = macros
123         self.engine = engine # Execution engine (aka context)
124         self.Default = engine.getDefault()
125         self.stream = stream or sys.stdout
126         self._stream_write = self.stream.write
127         self.debug = debug
128         self.wrap = wrap
129         self.metal = metal
130         self.tal = tal
131         if tal:
132             self.dispatch = self.bytecode_handlers_tal
133         else:
134             self.dispatch = self.bytecode_handlers
135         assert showtal in (-1, 0, 1)
136         if showtal == -1:
137             showtal = (not tal)
138         self.showtal = showtal
139         self.strictinsert = strictinsert
140         self.stackLimit = stackLimit
141         self.html = 0
142         self.endsep = "/>"
143         self.endlen = len(self.endsep)
144         self.macroStack = []
145         self.position = None, None  # (lineno, offset)
146         self.col = 0
147         self.level = 0
148         self.scopeLevel = 0
149         self.sourceFile = None
150         self.i18nStack = []
151         self.i18nInterpolate = i18nInterpolate
152         self.i18nContext = TranslationContext()
153
154     def StringIO(self):
155         # Third-party products wishing to provide a full Unicode-aware
156         # StringIO can do so by monkey-patching this method.
157         return FasterStringIO()
158
159     def saveState(self):
160         return (self.position, self.col, self.stream,
161                 self.scopeLevel, self.level, self.i18nContext)
162
163     def restoreState(self, state):
164         (self.position, self.col, self.stream, scopeLevel, level, i18n) = state
165         self._stream_write = self.stream.write
166         assert self.level == level
167         while self.scopeLevel > scopeLevel:
168             self.engine.endScope()
169             self.scopeLevel = self.scopeLevel - 1
170         self.engine.setPosition(self.position)
171         self.i18nContext = i18n
172
173     def restoreOutputState(self, state):
174         (dummy, self.col, self.stream, scopeLevel, level, i18n) = state
175         self._stream_write = self.stream.write
176         assert self.level == level
177         assert self.scopeLevel == scopeLevel
178
179     def pushMacro(self, macroName, slots, entering=1):
180         if len(self.macroStack) >= self.stackLimit:
181             raise METALError("macro nesting limit (%d) exceeded "
182                              "by %s" % (self.stackLimit, `macroName`))
183         self.macroStack.append([macroName, slots, entering, self.i18nContext])
184
185     def popMacro(self):
186         return self.macroStack.pop()
187
188     def __call__(self):
189         assert self.level == 0
190         assert self.scopeLevel == 0
191         assert self.i18nContext.parent is None
192         self.interpret(self.program)
193         assert self.level == 0
194         assert self.scopeLevel == 0
195         assert self.i18nContext.parent is None
196         if self.col > 0:
197             self._stream_write("\n")
198             self.col = 0
199
200     def stream_write(self, s,
201                      len=len):
202         self._stream_write(s)
203         i = s.rfind('\n')
204         if i < 0:
205             self.col = self.col + len(s)
206         else:
207             self.col = len(s) - (i + 1)
208
209     bytecode_handlers = {}
210
211     def interpretWithStream(self, program, stream):
212         oldstream = self.stream
213         self.stream = stream
214         self._stream_write = stream.write
215         try:
216             self.interpret(program)
217         finally:
218             self.stream = oldstream
219             self._stream_write = oldstream.write
220
221     def interpret(self, program):
222         oldlevel = self.level
223         self.level = oldlevel + 1
224         handlers = self.dispatch
225         try:
226             if self.debug:
227                 for (opcode, args) in program:
228                     s = "%sdo_%s(%s)\n" % ("    "*self.level, opcode,
229                                            repr(args))
230                     if len(s) > 80:
231                         s = s[:76] + "...\n"
232                     sys.stderr.write(s)
233                     handlers[opcode](self, args)
234             else:
235                 for (opcode, args) in program:
236                     handlers[opcode](self, args)
237         finally:
238             self.level = oldlevel
239
240     def do_version(self, version):
241         assert version == TAL_VERSION
242     bytecode_handlers["version"] = do_version
243
244     def do_mode(self, mode):
245         assert mode in ("html", "xml")
246         self.html = (mode == "html")
247         if self.html:
248             self.endsep = " />"
249         else:
250             self.endsep = "/>"
251         self.endlen = len(self.endsep)
252     bytecode_handlers["mode"] = do_mode
253
254     def do_setSourceFile(self, source_file):
255         self.sourceFile = source_file
256         self.engine.setSourceFile(source_file)
257     bytecode_handlers["setSourceFile"] = do_setSourceFile
258
259     def do_setPosition(self, position):
260         self.position = position
261         self.engine.setPosition(position)
262     bytecode_handlers["setPosition"] = do_setPosition
263
264     def do_startEndTag(self, stuff):
265         self.do_startTag(stuff, self.endsep, self.endlen)
266     bytecode_handlers["startEndTag"] = do_startEndTag
267
268     def do_startTag(self, (name, attrList),
269                     end=">", endlen=1, _len=len):
270         # The bytecode generator does not cause calls to this method
271         # for start tags with no attributes; those are optimized down
272         # to rawtext events.  Hence, there is no special "fast path"
273         # for that case.
274         L = ["<", name]
275         append = L.append
276         col = self.col + _len(name) + 1
277         wrap = self.wrap
278         align = col + 1
279         if align >= wrap/2:
280             align = 4  # Avoid a narrow column far to the right
281         attrAction = self.dispatch["<attrAction>"]
282         try:
283             for item in attrList:
284                 if _len(item) == 2:
285                     name, s = item
286                 else:
287                     # item[2] is the 'action' field:
288                     if item[2] in ('metal', 'tal', 'xmlns', 'i18n'):
289                         if not self.showtal:
290                             continue
291                         ok, name, s = self.attrAction(item)
292                     else:
293                         ok, name, s = attrAction(self, item)
294                     if not ok:
295                         continue
296                 slen = _len(s)
297                 if (wrap and
298                     col >= align and
299                     col + 1 + slen > wrap):
300                     append("\n")
301                     append(" "*align)
302                     col = align + slen
303                 else:
304                     append(" ")
305                     col = col + 1 + slen
306                 append(s)
307             append(end)
308             self._stream_write("".join(L))
309             col = col + endlen
310         finally:
311             self.col = col
312     bytecode_handlers["startTag"] = do_startTag
313
314     def attrAction(self, item):
315         name, value, action = item[:3]
316         if action == 'insert':
317             return 0, name, value
318         macs = self.macroStack
319         if action == 'metal' and self.metal and macs:
320             if len(macs) > 1 or not macs[-1][2]:
321                 # Drop all METAL attributes at a use-depth above one.
322                 return 0, name, value
323             # Clear 'entering' flag
324             macs[-1][2] = 0
325             # Convert or drop depth-one METAL attributes.
326             i = name.rfind(":") + 1
327             prefix, suffix = name[:i], name[i:]
328             if suffix == "define-macro":
329                 # Convert define-macro as we enter depth one.
330                 name = prefix + "use-macro"
331                 value = macs[-1][0] # Macro name
332             elif suffix == "define-slot":
333                 name = prefix + "fill-slot"
334             elif suffix == "fill-slot":
335                 pass
336             else:
337                 return 0, name, value
338
339         if value is None:
340             value = name
341         else:
342             value = '%s="%s"' % (name, attrEscape(value))
343         return 1, name, value
344
345     def attrAction_tal(self, item):
346         name, value, action = item[:3]
347         ok = 1
348         expr, xlat, msgid = item[3:]
349         if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
350             evalue = self.engine.evaluateBoolean(item[3])
351             if evalue is self.Default:
352                 if action == 'insert': # Cancelled insert
353                     ok = 0
354             elif evalue:
355                 value = None
356             else:
357                 ok = 0
358         elif expr is not None:
359             evalue = self.engine.evaluateText(item[3])
360             if evalue is self.Default:
361                 if action == 'insert': # Cancelled insert
362                     ok = 0
363             else:
364                 if evalue is None:
365                     ok = 0
366                 value = evalue
367         else:
368             evalue = None
369
370         if ok:
371             if xlat:
372                 translated = self.translate(msgid or value, value, {})
373                 if translated is not None:
374                     value = translated
375             if value is None:
376                 value = name
377             elif evalue is self.Default:
378                 value = attrEscape(value)
379             else:
380                 value = escape(value, quote=1)
381             value = '%s="%s"' % (name, value)
382         return ok, name, value
383     bytecode_handlers["<attrAction>"] = attrAction
384
385     def no_tag(self, start, program):
386         state = self.saveState()
387         self.stream = stream = self.StringIO()
388         self._stream_write = stream.write
389         self.interpret(start)
390         self.restoreOutputState(state)
391         self.interpret(program)
392
393     def do_optTag(self, (name, cexpr, tag_ns, isend, start, program),
394                   omit=0):
395         if tag_ns and not self.showtal:
396             return self.no_tag(start, program)
397
398         self.interpret(start)
399         if not isend:
400             self.interpret(program)
401             s = '</%s>' % name
402             self._stream_write(s)
403             self.col = self.col + len(s)
404
405     def do_optTag_tal(self, stuff):
406         cexpr = stuff[1]
407         if cexpr is not None and (cexpr == '' or
408                                   self.engine.evaluateBoolean(cexpr)):
409             self.no_tag(stuff[-2], stuff[-1])
410         else:
411             self.do_optTag(stuff)
412     bytecode_handlers["optTag"] = do_optTag
413
414     def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)):
415         self._stream_write(s)
416         self.col = col
417         self.position = position
418         self.engine.setPosition(position)
419         if closeprev:
420             engine = self.engine
421             engine.endScope()
422             engine.beginScope()
423         else:
424             self.engine.beginScope()
425             self.scopeLevel = self.scopeLevel + 1
426
427     def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
428         self._stream_write(s)
429         self.col = col
430         engine = self.engine
431         self.position = position
432         engine.setPosition(position)
433         if closeprev:
434             engine.endScope()
435             engine.beginScope()
436         else:
437             engine.beginScope()
438             self.scopeLevel = self.scopeLevel + 1
439         engine.setLocal("attrs", dict)
440     bytecode_handlers["rawtextBeginScope"] = do_rawtextBeginScope
441
442     def do_beginScope(self, dict):
443         self.engine.beginScope()
444         self.scopeLevel = self.scopeLevel + 1
445
446     def do_beginScope_tal(self, dict):
447         engine = self.engine
448         engine.beginScope()
449         engine.setLocal("attrs", dict)
450         self.scopeLevel = self.scopeLevel + 1
451     bytecode_handlers["beginScope"] = do_beginScope
452
453     def do_endScope(self, notused=None):
454         self.engine.endScope()
455         self.scopeLevel = self.scopeLevel - 1
456     bytecode_handlers["endScope"] = do_endScope
457
458     def do_setLocal(self, notused):
459         pass
460
461     def do_setLocal_tal(self, (name, expr)):
462         self.engine.setLocal(name, self.engine.evaluateValue(expr))
463     bytecode_handlers["setLocal"] = do_setLocal
464
465     def do_setGlobal_tal(self, (name, expr)):
466         self.engine.setGlobal(name, self.engine.evaluateValue(expr))
467     bytecode_handlers["setGlobal"] = do_setLocal
468
469     def do_beginI18nContext(self, settings):
470         get = settings.get
471         self.i18nContext = TranslationContext(self.i18nContext,
472                                               domain=get("domain"),
473                                               source=get("source"),
474                                               target=get("target"))
475     bytecode_handlers["beginI18nContext"] = do_beginI18nContext
476
477     def do_endI18nContext(self, notused=None):
478         self.i18nContext = self.i18nContext.parent
479         assert self.i18nContext is not None
480     bytecode_handlers["endI18nContext"] = do_endI18nContext
481
482     def do_insertText(self, stuff):
483         self.interpret(stuff[1])
484
485     def do_insertText_tal(self, stuff):
486         text = self.engine.evaluateText(stuff[0])
487         if text is None:
488             return
489         if text is self.Default:
490             self.interpret(stuff[1])
491             return
492         s = escape(text)
493         self._stream_write(s)
494         i = s.rfind('\n')
495         if i < 0:
496             self.col = self.col + len(s)
497         else:
498             self.col = len(s) - (i + 1)
499     bytecode_handlers["insertText"] = do_insertText
500
501     def do_i18nVariable(self, stuff):
502         varname, program, expression = stuff
503         if expression is None:
504             # The value is implicitly the contents of this tag, so we have to
505             # evaluate the mini-program to get the value of the variable.
506             state = self.saveState()
507             try:
508                 tmpstream = self.StringIO()
509                 self.interpretWithStream(program, tmpstream)
510                 value = normalize(tmpstream.getvalue())
511             finally:
512                 self.restoreState(state)
513         else:
514             # Evaluate the value to be associated with the variable in the
515             # i18n interpolation dictionary.
516             value = self.engine.evaluate(expression)
517         # Either the i18n:name tag is nested inside an i18n:translate in which
518         # case the last item on the stack has the i18n dictionary and string
519         # representation, or the i18n:name and i18n:translate attributes are
520         # in the same tag, in which case the i18nStack will be empty.  In that
521         # case we can just output the ${name} to the stream
522         i18ndict, srepr = self.i18nStack[-1]
523         i18ndict[varname] = value
524         placeholder = '${%s}' % varname
525         srepr.append(placeholder)
526         self._stream_write(placeholder)
527     bytecode_handlers['i18nVariable'] = do_i18nVariable
528
529     def do_insertTranslation(self, stuff):
530         i18ndict = {}
531         srepr = []
532         obj = None
533         self.i18nStack.append((i18ndict, srepr))
534         msgid = stuff[0]
535         # We need to evaluate the content of the tag because that will give us
536         # several useful pieces of information.  First, the contents will
537         # include an implicit message id, if no explicit one was given.
538         # Second, it will evaluate any i18nVariable definitions in the body of
539         # the translation (necessary for $varname substitutions).
540         #
541         # Use a temporary stream to capture the interpretation of the
542         # subnodes, which should /not/ go to the output stream.
543         tmpstream = self.StringIO()
544         self.interpretWithStream(stuff[1], tmpstream)
545         default = tmpstream.getvalue()
546         # We only care about the evaluated contents if we need an implicit
547         # message id.  All other useful information will be in the i18ndict on
548         # the top of the i18nStack.
549         if msgid == '':
550             msgid = normalize(default)
551         self.i18nStack.pop()
552         # See if there is was an i18n:data for msgid
553         if len(stuff) > 2:
554             obj = self.engine.evaluate(stuff[2])
555         xlated_msgid = self.translate(msgid, default, i18ndict, obj)
556         assert xlated_msgid is not None, self.position
557         self._stream_write(xlated_msgid)
558     bytecode_handlers['insertTranslation'] = do_insertTranslation
559
560     def do_insertStructure(self, stuff):
561         self.interpret(stuff[2])
562
563     def do_insertStructure_tal(self, (expr, repldict, block)):
564         structure = self.engine.evaluateStructure(expr)
565         if structure is None:
566             return
567         if structure is self.Default:
568             self.interpret(block)
569             return
570         text = ustr(structure)
571         if not (repldict or self.strictinsert):
572             # Take a shortcut, no error checking
573             self.stream_write(text)
574             return
575         if self.html:
576             self.insertHTMLStructure(text, repldict)
577         else:
578             self.insertXMLStructure(text, repldict)
579     bytecode_handlers["insertStructure"] = do_insertStructure
580
581     def insertHTMLStructure(self, text, repldict):
582         from HTMLTALParser import HTMLTALParser
583         gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
584         p = HTMLTALParser(gen) # Raises an exception if text is invalid
585         p.parseString(text)
586         program, macros = p.getCode()
587         self.interpret(program)
588
589     def insertXMLStructure(self, text, repldict):
590         from TALParser import TALParser
591         gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
592         p = TALParser(gen)
593         gen.enable(0)
594         p.parseFragment('<!DOCTYPE foo PUBLIC "foo" "bar"><foo>')
595         gen.enable(1)
596         p.parseFragment(text) # Raises an exception if text is invalid
597         gen.enable(0)
598         p.parseFragment('</foo>', 1)
599         program, macros = gen.getCode()
600         self.interpret(program)
601
602     def do_loop(self, (name, expr, block)):
603         self.interpret(block)
604
605     def do_loop_tal(self, (name, expr, block)):
606         iterator = self.engine.setRepeat(name, expr)
607         while iterator.next():
608             self.interpret(block)
609     bytecode_handlers["loop"] = do_loop
610
611     def translate(self, msgid, default, i18ndict, obj=None):
612         if obj:
613             i18ndict.update(obj)
614         if not self.i18nInterpolate:
615             return msgid
616         # XXX We need to pass in one of context or target_language
617         return self.engine.translate(self.i18nContext.domain,
618                                      msgid, i18ndict, default=default)
619
620     def do_rawtextColumn(self, (s, col)):
621         self._stream_write(s)
622         self.col = col
623     bytecode_handlers["rawtextColumn"] = do_rawtextColumn
624
625     def do_rawtextOffset(self, (s, offset)):
626         self._stream_write(s)
627         self.col = self.col + offset
628     bytecode_handlers["rawtextOffset"] = do_rawtextOffset
629
630     def do_condition(self, (condition, block)):
631         if not self.tal or self.engine.evaluateBoolean(condition):
632             self.interpret(block)
633     bytecode_handlers["condition"] = do_condition
634
635     def do_defineMacro(self, (macroName, macro)):
636         macs = self.macroStack
637         if len(macs) == 1:
638             entering = macs[-1][2]
639             if not entering:
640                 macs.append(None)
641                 self.interpret(macro)
642                 assert macs[-1] is None
643                 macs.pop()
644                 return
645         self.interpret(macro)
646     bytecode_handlers["defineMacro"] = do_defineMacro
647
648     def do_useMacro(self, (macroName, macroExpr, compiledSlots, block)):
649         if not self.metal:
650             self.interpret(block)
651             return
652         macro = self.engine.evaluateMacro(macroExpr)
653         if macro is self.Default:
654             macro = block
655         else:
656             if not isCurrentVersion(macro):
657                 raise METALError("macro %s has incompatible version %s" %
658                                  (`macroName`, `getProgramVersion(macro)`),
659                                  self.position)
660             mode = getProgramMode(macro)
661             if mode != (self.html and "html" or "xml"):
662                 raise METALError("macro %s has incompatible mode %s" %
663                                  (`macroName`, `mode`), self.position)
664         self.pushMacro(macroName, compiledSlots)
665         prev_source = self.sourceFile
666         self.interpret(macro)
667         if self.sourceFile != prev_source:
668             self.engine.setSourceFile(prev_source)
669             self.sourceFile = prev_source
670         self.popMacro()
671     bytecode_handlers["useMacro"] = do_useMacro
672
673     def do_fillSlot(self, (slotName, block)):
674         # This is only executed if the enclosing 'use-macro' evaluates
675         # to 'default'.
676         self.interpret(block)
677     bytecode_handlers["fillSlot"] = do_fillSlot
678
679     def do_defineSlot(self, (slotName, block)):
680         if not self.metal:
681             self.interpret(block)
682             return
683         macs = self.macroStack
684         if macs and macs[-1] is not None:
685             macroName, slots = self.popMacro()[:2]
686             slot = slots.get(slotName)
687             if slot is not None:
688                 prev_source = self.sourceFile
689                 self.interpret(slot)
690                 if self.sourceFile != prev_source:
691                     self.engine.setSourceFile(prev_source)
692                     self.sourceFile = prev_source
693                 self.pushMacro(macroName, slots, entering=0)
694                 return
695             self.pushMacro(macroName, slots)
696             # Falling out of the 'if' allows the macro to be interpreted.
697         self.interpret(block)
698     bytecode_handlers["defineSlot"] = do_defineSlot
699
700     def do_onError(self, (block, handler)):
701         self.interpret(block)
702
703     def do_onError_tal(self, (block, handler)):
704         state = self.saveState()
705         self.stream = stream = self.StringIO()
706         self._stream_write = stream.write
707         try:
708             self.interpret(block)
709         except:
710             exc = sys.exc_info()[1]
711             self.restoreState(state)
712             engine = self.engine
713             engine.beginScope()
714             error = engine.createErrorInfo(exc, self.position)
715             engine.setLocal('error', error)
716             try:
717                 self.interpret(handler)
718             finally:
719                 engine.endScope()
720         else:
721             self.restoreOutputState(state)
722             self.stream_write(stream.getvalue())
723     bytecode_handlers["onError"] = do_onError
724
725     bytecode_handlers_tal = bytecode_handlers.copy()
726     bytecode_handlers_tal["rawtextBeginScope"] = do_rawtextBeginScope_tal
727     bytecode_handlers_tal["beginScope"] = do_beginScope_tal
728     bytecode_handlers_tal["setLocal"] = do_setLocal_tal
729     bytecode_handlers_tal["setGlobal"] = do_setGlobal_tal
730     bytecode_handlers_tal["insertStructure"] = do_insertStructure_tal
731     bytecode_handlers_tal["insertText"] = do_insertText_tal
732     bytecode_handlers_tal["loop"] = do_loop_tal
733     bytecode_handlers_tal["onError"] = do_onError_tal
734     bytecode_handlers_tal["<attrAction>"] = attrAction_tal
735     bytecode_handlers_tal["optTag"] = do_optTag_tal
736
737
738 class FasterStringIO(StringIO):
739     """Append-only version of StringIO.
740
741     This let's us have a much faster write() method.
742     """
743     def close(self):
744         if not self.closed:
745             self.write = _write_ValueError
746             StringIO.close(self)
747
748     def seek(self, pos, mode=0):
749         raise RuntimeError("FasterStringIO.seek() not allowed")
750
751     def write(self, s):
752         #assert self.pos == self.len
753         self.buflist.append(s)
754         self.len = self.pos = self.pos + len(s)
755
756
757 def _write_ValueError(s):
758     raise ValueError, "I/O operation on closed file"