Code

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