Code

documentation cleanup
[roundup.git] / roundup / cgi / TAL / TALInterpreter.py
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 """Interpreter for a pre-compiled TAL program.
15 """
16 __docformat__ = 'restructuredtext'
18 import sys
19 import getopt
21 from cgi import escape
23 try:
24     from cStringIO import StringIO
25 except ImportError:
26     from StringIO import StringIO
28 from TALDefs import quote, TAL_VERSION, TALError, METALError
29 from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode
30 from TALGenerator import TALGenerator
32 BOOLEAN_HTML_ATTRS = [
33     # List of Boolean attributes in HTML that should be rendered in
34     # minimized form (e.g. <img ismap> rather than <img ismap="">)
35     # From http://www.w3.org/TR/xhtml1/#guidelines (C.10)
36     # XXX The problem with this is that this is not valid XML and
37     # can't be parsed back!
38     "compact", "nowrap", "ismap", "declare", "noshade", "checked",
39     "disabled", "readonly", "multiple", "selected", "noresize",
40     "defer"
41 ]
43 EMPTY_HTML_TAGS = [
44     # List of HTML tags with an empty content model; these are
45     # rendered in minimized form, e.g. <img />.
46     # From http://www.w3.org/TR/xhtml1/#dtds
47     "base", "meta", "link", "hr", "br", "param", "img", "area",
48     "input", "col", "basefont", "isindex", "frame",
49 ]
51 class AltTALGenerator(TALGenerator):
53     def __init__(self, repldict, expressionCompiler=None, xml=0):
54         self.repldict = repldict
55         self.enabled = 1
56         TALGenerator.__init__(self, expressionCompiler, xml)
58     def enable(self, enabled):
59         self.enabled = enabled
61     def emit(self, *args):
62         if self.enabled:
63             apply(TALGenerator.emit, (self,) + args)
65     def emitStartElement(self, name, attrlist, taldict, metaldict,
66                          position=(None, None), isend=0):
67         metaldict = {}
68         taldict = {}
69         if self.enabled and self.repldict:
70             taldict["attributes"] = "x x"
71         TALGenerator.emitStartElement(self, name, attrlist,
72                                       taldict, metaldict, position, isend)
74     def replaceAttrs(self, attrlist, repldict):
75         if self.enabled and self.repldict:
76             repldict = self.repldict
77             self.repldict = None
78         return TALGenerator.replaceAttrs(self, attrlist, repldict)
81 class TALInterpreter:
83     def __init__(self, program, macros, engine, stream=None,
84                  debug=0, wrap=60, metal=1, tal=1, showtal=-1,
85                  strictinsert=1, stackLimit=100):
86         self.program = program
87         self.macros = macros
88         self.engine = engine
89         self.Default = engine.getDefault()
90         self.stream = stream or sys.stdout
91         self._stream_write = self.stream.write
92         self.debug = debug
93         self.wrap = wrap
94         self.metal = metal
95         self.tal = tal
96         if tal:
97             self.dispatch = self.bytecode_handlers_tal
98         else:
99             self.dispatch = self.bytecode_handlers
100         assert showtal in (-1, 0, 1)
101         if showtal == -1:
102             showtal = (not tal)
103         self.showtal = showtal
104         self.strictinsert = strictinsert
105         self.stackLimit = stackLimit
106         self.html = 0
107         self.endsep = "/>"
108         self.endlen = len(self.endsep)
109         self.macroStack = []
110         self.popMacro = self.macroStack.pop
111         self.position = None, None  # (lineno, offset)
112         self.col = 0
113         self.level = 0
114         self.scopeLevel = 0
115         self.sourceFile = None
117     def saveState(self):
118         return (self.position, self.col, self.stream,
119                 self.scopeLevel, self.level)
121     def restoreState(self, state):
122         (self.position, self.col, self.stream, scopeLevel, level) = state
123         self._stream_write = self.stream.write
124         assert self.level == level
125         while self.scopeLevel > scopeLevel:
126             self.engine.endScope()
127             self.scopeLevel = self.scopeLevel - 1
128         self.engine.setPosition(self.position)
130     def restoreOutputState(self, state):
131         (dummy, self.col, self.stream, scopeLevel, level) = state
132         self._stream_write = self.stream.write
133         assert self.level == level
134         assert self.scopeLevel == scopeLevel
136     def pushMacro(self, macroName, slots, entering=1):
137         if len(self.macroStack) >= self.stackLimit:
138             raise METALError("macro nesting limit (%d) exceeded "
139                              "by %s" % (self.stackLimit, `macroName`))
140         self.macroStack.append([macroName, slots, entering])
142     def macroContext(self, what):
143         macroStack = self.macroStack
144         i = len(macroStack)
145         while i > 0:
146             i = i-1
147             if macroStack[i][0] == what:
148                 return i
149         return -1
151     def __call__(self):
152         assert self.level == 0
153         assert self.scopeLevel == 0
154         self.interpret(self.program)
155         assert self.level == 0
156         assert self.scopeLevel == 0
157         if self.col > 0:
158             self._stream_write("\n")
159             self.col = 0
161     def stream_write(self, s, len=len):
162         self._stream_write(s)
163         i = s.rfind('\n')
164         if i < 0:
165             self.col = self.col + len(s)
166         else:
167             self.col = len(s) - (i + 1)
169     bytecode_handlers = {}
171     def interpret(self, program):
172         oldlevel = self.level
173         self.level = oldlevel + 1
174         handlers = self.dispatch
175         try:
176             if self.debug:
177                 for (opcode, args) in program:
178                     s = "%sdo_%s%s\n" % ("    "*self.level, opcode,
179                                       repr(args))
180                     if len(s) > 80:
181                         s = s[:76] + "...\n"
182                     sys.stderr.write(s)
183                     handlers[opcode](self, args)
184             else:
185                 for (opcode, args) in program:
186                     handlers[opcode](self, args)
187         finally:
188             self.level = oldlevel
190     def do_version(self, version):
191         assert version == TAL_VERSION
192     bytecode_handlers["version"] = do_version
194     def do_mode(self, mode):
195         assert mode in ("html", "xml")
196         self.html = (mode == "html")
197         if self.html:
198             self.endsep = " />"
199         else:
200             self.endsep = "/>"
201         self.endlen = len(self.endsep)
202     bytecode_handlers["mode"] = do_mode
204     def do_setSourceFile(self, source_file):
205         self.sourceFile = source_file
206         self.engine.setSourceFile(source_file)
207     bytecode_handlers["setSourceFile"] = do_setSourceFile
209     def do_setPosition(self, position):
210         self.position = position
211         self.engine.setPosition(position)
212     bytecode_handlers["setPosition"] = do_setPosition
214     def do_startEndTag(self, stuff):
215         self.do_startTag(stuff, self.endsep, self.endlen)
216     bytecode_handlers["startEndTag"] = do_startEndTag
218     def do_startTag(self, (name, attrList),
219                     end=">", endlen=1, _len=len):
220         # The bytecode generator does not cause calls to this method
221         # for start tags with no attributes; those are optimized down
222         # to rawtext events.  Hence, there is no special "fast path"
223         # for that case.
224         _stream_write = self._stream_write
225         _stream_write("<" + name)
226         namelen = _len(name)
227         col = self.col + namelen + 1
228         wrap = self.wrap
229         align = col + 1
230         if align >= wrap/2:
231             align = 4  # Avoid a narrow column far to the right
232         attrAction = self.dispatch["<attrAction>"]
233         try:
234             for item in attrList:
235                 if _len(item) == 2:
236                     name, s = item
237                 else:
238                     ok, name, s = attrAction(self, item)
239                     if not ok:
240                         continue
241                 slen = _len(s)
242                 if (wrap and
243                     col >= align and
244                     col + 1 + slen > wrap):
245                     _stream_write("\n" + " "*align)
246                     col = align + slen
247                 else:
248                     s = " " + s
249                     col = col + 1 + slen
250                 _stream_write(s)
251             _stream_write(end)
252             col = col + endlen
253         finally:
254             self.col = col
255     bytecode_handlers["startTag"] = do_startTag
257     def attrAction(self, item):
258         name, value, action = item[:3]
259         if action == 1 or (action > 1 and not self.showtal):
260             return 0, name, value
261         macs = self.macroStack
262         if action == 2 and self.metal and macs:
263             if len(macs) > 1 or not macs[-1][2]:
264                 # Drop all METAL attributes at a use-depth above one.
265                 return 0, name, value
266             # Clear 'entering' flag
267             macs[-1][2] = 0
268             # Convert or drop depth-one METAL attributes.
269             i = name.rfind(":") + 1
270             prefix, suffix = name[:i], name[i:]
271             if suffix == "define-macro":
272                 # Convert define-macro as we enter depth one.
273                 name = prefix + "use-macro"
274                 value = macs[-1][0] # Macro name
275             elif suffix == "define-slot":
276                 name = prefix + "slot"
277             elif suffix == "fill-slot":
278                 pass
279             else:
280                 return 0, name, value
282         if value is None:
283             value = name
284         else:
285             value = "%s=%s" % (name, quote(value))
286         return 1, name, value
288     def attrAction_tal(self, item):
289         name, value, action = item[:3]
290         if action > 1:
291             return self.attrAction(item)
292         ok = 1
293         if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
294             evalue = self.engine.evaluateBoolean(item[3])
295             if evalue is self.Default:
296                 if action == 1: # Cancelled insert
297                     ok = 0
298             elif evalue:
299                 value = None
300             else:
301                 ok = 0
302         else:
303             evalue = self.engine.evaluateText(item[3])
304             if evalue is self.Default:
305                 if action == 1: # Cancelled insert
306                     ok = 0
307             else:
308                 if evalue is None:
309                     ok = 0
310                 value = evalue
311         if ok:
312             if value is None:
313                 value = name
314             value = "%s=%s" % (name, quote(value))
315         return ok, name, value
317     bytecode_handlers["<attrAction>"] = attrAction
319     def no_tag(self, start, program):
320         state = self.saveState()
321         self.stream = stream = StringIO()
322         self._stream_write = stream.write
323         self.interpret(start)
324         self.restoreOutputState(state)
325         self.interpret(program)
327     def do_optTag(self, (name, cexpr, tag_ns, isend, start, program),
328                   omit=0):
329         if tag_ns and not self.showtal:
330             return self.no_tag(start, program)
331             
332         self.interpret(start)
333         if not isend:
334             self.interpret(program)
335             s = '</%s>' % name
336             self._stream_write(s)
337             self.col = self.col + len(s)
339     def do_optTag_tal(self, stuff):
340         cexpr = stuff[1]
341         if cexpr is not None and (cexpr == '' or
342                                   self.engine.evaluateBoolean(cexpr)):
343             self.no_tag(stuff[-2], stuff[-1])
344         else:
345             self.do_optTag(stuff)
346     bytecode_handlers["optTag"] = do_optTag
348     def dumpMacroStack(self, prefix, suffix, value):
349         sys.stderr.write("+---- %s%s = %s\n" % (prefix, suffix, value))
350         for i in range(len(self.macroStack)):
351             what, macroName, slots = self.macroStack[i]
352             sys.stderr.write("| %2d. %-12s %-12s %s\n" %
353                              (i, what, macroName, slots and slots.keys()))
354         sys.stderr.write("+--------------------------------------\n")
356     def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)):
357         self._stream_write(s)
358         self.col = col
359         self.do_setPosition(position)
360         if closeprev:
361             engine = self.engine
362             engine.endScope()
363             engine.beginScope()
364         else:
365             self.engine.beginScope()
366             self.scopeLevel = self.scopeLevel + 1
368     def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
369         self._stream_write(s)
370         self.col = col
371         self.do_setPosition(position)
372         engine = self.engine
373         if closeprev:
374             engine.endScope()
375             engine.beginScope()
376         else:
377             engine.beginScope()
378             self.scopeLevel = self.scopeLevel + 1
379         engine.setLocal("attrs", dict)
380     bytecode_handlers["rawtextBeginScope"] = do_rawtextBeginScope
382     def do_beginScope(self, dict):
383         self.engine.beginScope()
384         self.scopeLevel = self.scopeLevel + 1
386     def do_beginScope_tal(self, dict):
387         engine = self.engine
388         engine.beginScope()
389         engine.setLocal("attrs", dict)
390         self.scopeLevel = self.scopeLevel + 1
391     bytecode_handlers["beginScope"] = do_beginScope
393     def do_endScope(self, notused=None):
394         self.engine.endScope()
395         self.scopeLevel = self.scopeLevel - 1
396     bytecode_handlers["endScope"] = do_endScope
398     def do_setLocal(self, notused):
399         pass
401     def do_setLocal_tal(self, (name, expr)):
402         self.engine.setLocal(name, self.engine.evaluateValue(expr))
403     bytecode_handlers["setLocal"] = do_setLocal
405     def do_setGlobal_tal(self, (name, expr)):
406         self.engine.setGlobal(name, self.engine.evaluateValue(expr))
407     bytecode_handlers["setGlobal"] = do_setLocal
409     def do_insertText(self, stuff):
410         self.interpret(stuff[1])
412     def do_insertText_tal(self, stuff):
413         text = self.engine.evaluateText(stuff[0])
414         if text is None:
415             return
416         if text is self.Default:
417             self.interpret(stuff[1])
418             return
419         s = escape(text)
420         self._stream_write(s)
421         i = s.rfind('\n')
422         if i < 0:
423             self.col = self.col + len(s)
424         else:
425             self.col = len(s) - (i + 1)
426     bytecode_handlers["insertText"] = do_insertText
428     def do_insertStructure(self, stuff):
429         self.interpret(stuff[2])
431     def do_insertStructure_tal(self, (expr, repldict, block)):
432         structure = self.engine.evaluateStructure(expr)
433         if structure is None:
434             return
435         if structure is self.Default:
436             self.interpret(block)
437             return
438         text = str(structure)
439         if not (repldict or self.strictinsert):
440             # Take a shortcut, no error checking
441             self.stream_write(text)
442             return
443         if self.html:
444             self.insertHTMLStructure(text, repldict)
445         else:
446             self.insertXMLStructure(text, repldict)
447     bytecode_handlers["insertStructure"] = do_insertStructure
449     def insertHTMLStructure(self, text, repldict):
450         from HTMLTALParser import HTMLTALParser
451         gen = AltTALGenerator(repldict, self.engine, 0)
452         p = HTMLTALParser(gen) # Raises an exception if text is invalid
453         p.parseString(text)
454         program, macros = p.getCode()
455         self.interpret(program)
457     def insertXMLStructure(self, text, repldict):
458         from TALParser import TALParser
459         gen = AltTALGenerator(repldict, self.engine, 0)
460         p = TALParser(gen)
461         gen.enable(0)
462         p.parseFragment('<!DOCTYPE foo PUBLIC "foo" "bar"><foo>')
463         gen.enable(1)
464         p.parseFragment(text) # Raises an exception if text is invalid
465         gen.enable(0)
466         p.parseFragment('</foo>', 1)
467         program, macros = gen.getCode()
468         self.interpret(program)
470     def do_loop(self, (name, expr, block)):
471         self.interpret(block)
473     def do_loop_tal(self, (name, expr, block)):
474         iterator = self.engine.setRepeat(name, expr)
475         while iterator.next():
476             self.interpret(block)
477     bytecode_handlers["loop"] = do_loop
479     def do_rawtextColumn(self, (s, col)):
480         self._stream_write(s)
481         self.col = col
482     bytecode_handlers["rawtextColumn"] = do_rawtextColumn
484     def do_rawtextOffset(self, (s, offset)):
485         self._stream_write(s)
486         self.col = self.col + offset
487     bytecode_handlers["rawtextOffset"] = do_rawtextOffset
489     def do_condition(self, (condition, block)):
490         if not self.tal or self.engine.evaluateBoolean(condition):
491             self.interpret(block)
492     bytecode_handlers["condition"] = do_condition
494     def do_defineMacro(self, (macroName, macro)):
495         macs = self.macroStack
496         if len(macs) == 1:
497             entering = macs[-1][2]
498             if not entering:
499                 macs.append(None)
500                 self.interpret(macro)
501                 macs.pop()
502                 return
503         self.interpret(macro)
504     bytecode_handlers["defineMacro"] = do_defineMacro
506     def do_useMacro(self, (macroName, macroExpr, compiledSlots, block)):
507         if not self.metal:
508             self.interpret(block)
509             return
510         macro = self.engine.evaluateMacro(macroExpr)
511         if macro is self.Default:
512             macro = block
513         else:
514             if not isCurrentVersion(macro):
515                 raise METALError("macro %s has incompatible version %s" %
516                                  (`macroName`, `getProgramVersion(macro)`),
517                                  self.position)
518             mode = getProgramMode(macro)
519             if mode != (self.html and "html" or "xml"):
520                 raise METALError("macro %s has incompatible mode %s" %
521                                  (`macroName`, `mode`), self.position)
522         self.pushMacro(macroName, compiledSlots)
523         saved_source = self.sourceFile
524         saved_position = self.position  # Used by Boa Constructor
525         self.interpret(macro)
526         if self.sourceFile != saved_source:
527             self.engine.setSourceFile(saved_source)
528             self.sourceFile = saved_source
529         self.popMacro()
530     bytecode_handlers["useMacro"] = do_useMacro
532     def do_fillSlot(self, (slotName, block)):
533         # This is only executed if the enclosing 'use-macro' evaluates
534         # to 'default'.
535         self.interpret(block)
536     bytecode_handlers["fillSlot"] = do_fillSlot
538     def do_defineSlot(self, (slotName, block)):
539         if not self.metal:
540             self.interpret(block)
541             return
542         macs = self.macroStack
543         if macs and macs[-1] is not None:
544             saved_source = self.sourceFile
545             saved_position = self.position  # Used by Boa Constructor
546             macroName, slots = self.popMacro()[:2]
547             slot = slots.get(slotName)
548             if slot is not None:
549                 self.interpret(slot)
550                 if self.sourceFile != saved_source:
551                     self.engine.setSourceFile(saved_source)
552                     self.sourceFile = saved_source
553                 self.pushMacro(macroName, slots, entering=0)
554                 return
555             self.pushMacro(macroName, slots)
556             if len(macs) == 1:
557                 self.interpret(block)
558                 return
559         self.interpret(block)
560     bytecode_handlers["defineSlot"] = do_defineSlot
562     def do_onError(self, (block, handler)):
563         self.interpret(block)
565     def do_onError_tal(self, (block, handler)):
566         state = self.saveState()
567         self.stream = stream = StringIO()
568         self._stream_write = stream.write
569         try:
570             self.interpret(block)
571         except:
572             exc = sys.exc_info()[1]
573             self.restoreState(state)
574             engine = self.engine
575             engine.beginScope()
576             error = engine.createErrorInfo(exc, self.position)
577             engine.setLocal('error', error)
578             try:
579                 self.interpret(handler)
580             finally:
581                 engine.endScope()
582         else:
583             self.restoreOutputState(state)
584             self.stream_write(stream.getvalue())
585     bytecode_handlers["onError"] = do_onError
587     bytecode_handlers_tal = bytecode_handlers.copy()
588     bytecode_handlers_tal["rawtextBeginScope"] = do_rawtextBeginScope_tal
589     bytecode_handlers_tal["beginScope"] = do_beginScope_tal
590     bytecode_handlers_tal["setLocal"] = do_setLocal_tal
591     bytecode_handlers_tal["setGlobal"] = do_setGlobal_tal
592     bytecode_handlers_tal["insertStructure"] = do_insertStructure_tal
593     bytecode_handlers_tal["insertText"] = do_insertText_tal
594     bytecode_handlers_tal["loop"] = do_loop_tal
595     bytecode_handlers_tal["onError"] = do_onError_tal
596     bytecode_handlers_tal["<attrAction>"] = attrAction_tal
597     bytecode_handlers_tal["optTag"] = do_optTag_tal
600 def test():
601     from driver import FILE, parsefile
602     from DummyEngine import DummyEngine
603     try:
604         opts, args = getopt.getopt(sys.argv[1:], "")
605     except getopt.error, msg:
606         print msg
607         sys.exit(2)
608     if args:
609         file = args[0]
610     else:
611         file = FILE
612     doc = parsefile(file)
613     compiler = TALCompiler(doc)
614     program, macros = compiler()
615     engine = DummyEngine()
616     interpreter = TALInterpreter(program, macros, engine)
617     interpreter()
619 if __name__ == "__main__":
620     test()