added rendertest/ directory
[swftools.git] / rendertest / athana.py
1 #!/usr/bin/python
2 """
3  Athana - standalone web server including the TAL template language
4
5  Copyright (C) 2007 Matthias Kramm <kramm@in.tum.de>
6
7  This program is free software: you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation, either version 3 of the License, or
10  (at your option) any later version.
11
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  GNU General Public License for more details.
16
17  You should have received a copy of the GNU General Public License
18  along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 """
20
21 #===============================================================
22 #
23 # Athana
24 #
25 # A standalone webserver based on Medusa and the Zope TAL Parser
26 #
27 # This file is distributed under the GPL, see file COPYING for details.
28 #
29 #===============================================================
30 """
31 Parse HTML and compile to TALInterpreter intermediate code.
32 """
33
34 RCS_ID =  '$Id: athana.py,v 1.15 2007/11/23 10:13:32 kramm Exp $'
35
36 import sys
37
38 from HTMLParser import HTMLParser, HTMLParseError
39
40 BOOLEAN_HTML_ATTRS = [
41     "compact", "nowrap", "ismap", "declare", "noshade", "checked",
42     "disabled", "readonly", "multiple", "selected", "noresize",
43     "defer"
44     ]
45
46 EMPTY_HTML_TAGS = [
47     "base", "meta", "link", "hr", "br", "param", "img", "area",
48     "input", "col", "basefont", "isindex", "frame",
49     ]
50
51 PARA_LEVEL_HTML_TAGS = [
52     "h1", "h2", "h3", "h4", "h5", "h6", "p",
53     ]
54
55 BLOCK_CLOSING_TAG_MAP = {
56     "tr": ("tr", "td", "th"),
57     "td": ("td", "th"),
58     "th": ("td", "th"),
59     "li": ("li",),
60     "dd": ("dd", "dt"),
61     "dt": ("dd", "dt"),
62     }
63
64 BLOCK_LEVEL_HTML_TAGS = [
65     "blockquote", "table", "tr", "th", "td", "thead", "tfoot", "tbody",
66     "noframe", "ul", "ol", "li", "dl", "dt", "dd", "div",
67     ]
68
69 TIGHTEN_IMPLICIT_CLOSE_TAGS = (PARA_LEVEL_HTML_TAGS
70                                + BLOCK_CLOSING_TAG_MAP.keys())
71
72
73 class NestingError(HTMLParseError):
74     """Exception raised when elements aren't properly nested."""
75
76     def __init__(self, tagstack, endtag, position=(None, None)):
77         self.endtag = endtag
78         if tagstack:
79             if len(tagstack) == 1:
80                 msg = ('Open tag <%s> does not match close tag </%s>'
81                        % (tagstack[0], endtag))
82             else:
83                 msg = ('Open tags <%s> do not match close tag </%s>'
84                        % ('>, <'.join(tagstack), endtag))
85         else:
86             msg = 'No tags are open to match </%s>' % endtag
87         HTMLParseError.__init__(self, msg, position)
88
89 class EmptyTagError(NestingError):
90     """Exception raised when empty elements have an end tag."""
91
92     def __init__(self, tag, position=(None, None)):
93         self.tag = tag
94         msg = 'Close tag </%s> should be removed' % tag
95         HTMLParseError.__init__(self, msg, position)
96
97 class OpenTagError(NestingError):
98     """Exception raised when a tag is not allowed in another tag."""
99
100     def __init__(self, tagstack, tag, position=(None, None)):
101         self.tag = tag
102         msg = 'Tag <%s> is not allowed in <%s>' % (tag, tagstack[-1])
103         HTMLParseError.__init__(self, msg, position)
104
105 class HTMLTALParser(HTMLParser):
106
107
108     def __init__(self, gen=None):
109         HTMLParser.__init__(self)
110         if gen is None:
111             gen = TALGenerator(xml=0)
112         self.gen = gen
113         self.tagstack = []
114         self.nsstack = []
115         self.nsdict = {'tal': ZOPE_TAL_NS,
116                        'metal': ZOPE_METAL_NS,
117                        'i18n': ZOPE_I18N_NS,
118                        }
119
120     def parseFile(self, file):
121         f = open(file)
122         data = f.read()
123         f.close()
124         try:
125             self.parseString(data)
126         except TALError, e:
127             e.setFile(file)
128             raise
129
130     def parseString(self, data):
131         self.feed(data)
132         self.close()
133         while self.tagstack:
134             self.implied_endtag(self.tagstack[-1], 2)
135         assert self.nsstack == [], self.nsstack
136
137     def getCode(self):
138         return self.gen.getCode()
139
140     def getWarnings(self):
141         return ()
142
143
144     def handle_starttag(self, tag, attrs):
145         self.close_para_tags(tag)
146         self.scan_xmlns(attrs)
147         tag, attrlist, taldict, metaldict, i18ndict \
148              = self.process_ns(tag, attrs)
149         if tag in EMPTY_HTML_TAGS and taldict.get("content"):
150             raise TALError(
151                 "empty HTML tags cannot use tal:content: %s" % `tag`,
152                 self.getpos())
153         self.tagstack.append(tag)
154         self.gen.emitStartElement(tag, attrlist, taldict, metaldict, i18ndict,
155                                   self.getpos())
156         if tag in EMPTY_HTML_TAGS:
157             self.implied_endtag(tag, -1)
158
159     def handle_startendtag(self, tag, attrs):
160         self.close_para_tags(tag)
161         self.scan_xmlns(attrs)
162         tag, attrlist, taldict, metaldict, i18ndict \
163              = self.process_ns(tag, attrs)
164         if taldict.get("content"):
165             if tag in EMPTY_HTML_TAGS:
166                 raise TALError(
167                     "empty HTML tags cannot use tal:content: %s" % `tag`,
168                     self.getpos())
169             self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
170                                       i18ndict, self.getpos())
171             self.gen.emitEndElement(tag, implied=-1)
172         else:
173             self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
174                                       i18ndict, self.getpos(), isend=1)
175         self.pop_xmlns()
176
177     def handle_endtag(self, tag):
178         if tag in EMPTY_HTML_TAGS:
179             raise EmptyTagError(tag, self.getpos())
180         self.close_enclosed_tags(tag)
181         self.gen.emitEndElement(tag)
182         self.pop_xmlns()
183         self.tagstack.pop()
184
185     def close_para_tags(self, tag):
186         if tag in EMPTY_HTML_TAGS:
187             return
188         close_to = -1
189         if BLOCK_CLOSING_TAG_MAP.has_key(tag):
190             blocks_to_close = BLOCK_CLOSING_TAG_MAP[tag]
191             for i in range(len(self.tagstack)):
192                 t = self.tagstack[i]
193                 if t in blocks_to_close:
194                     if close_to == -1:
195                         close_to = i
196                 elif t in BLOCK_LEVEL_HTML_TAGS:
197                     close_to = -1
198         elif tag in PARA_LEVEL_HTML_TAGS + BLOCK_LEVEL_HTML_TAGS:
199             i = len(self.tagstack) - 1
200             while i >= 0:
201                 closetag = self.tagstack[i]
202                 if closetag in BLOCK_LEVEL_HTML_TAGS:
203                     break
204                 if closetag in PARA_LEVEL_HTML_TAGS:
205                     if closetag != "p":
206                         raise OpenTagError(self.tagstack, tag, self.getpos())
207                     close_to = i
208                 i = i - 1
209         if close_to >= 0:
210             while len(self.tagstack) > close_to:
211                 self.implied_endtag(self.tagstack[-1], 1)
212
213     def close_enclosed_tags(self, tag):
214         if tag not in self.tagstack:
215             raise NestingError(self.tagstack, tag, self.getpos())
216         while tag != self.tagstack[-1]:
217             self.implied_endtag(self.tagstack[-1], 1)
218         assert self.tagstack[-1] == tag
219
220     def implied_endtag(self, tag, implied):
221         assert tag == self.tagstack[-1]
222         assert implied in (-1, 1, 2)
223         isend = (implied < 0)
224         if tag in TIGHTEN_IMPLICIT_CLOSE_TAGS:
225             white = self.gen.unEmitWhitespace()
226         else:
227             white = None
228         self.gen.emitEndElement(tag, isend=isend, implied=implied)
229         if white:
230             self.gen.emitRawText(white)
231         self.tagstack.pop()
232         self.pop_xmlns()
233
234     def handle_charref(self, name):
235         self.gen.emitRawText("&#%s;" % name)
236
237     def handle_entityref(self, name):
238         self.gen.emitRawText("&%s;" % name)
239
240     def handle_data(self, data):
241         self.gen.emitRawText(data)
242
243     def handle_comment(self, data):
244         self.gen.emitRawText("<!--%s-->" % data)
245
246     def handle_decl(self, data):
247         self.gen.emitRawText("<!%s>" % data)
248
249     def handle_pi(self, data):
250         self.gen.emitRawText("<?%s>" % data)
251
252
253     def scan_xmlns(self, attrs):
254         nsnew = {}
255         for key, value in attrs:
256             if key.startswith("xmlns:"):
257                 nsnew[key[6:]] = value
258         if nsnew:
259             self.nsstack.append(self.nsdict)
260             self.nsdict = self.nsdict.copy()
261             self.nsdict.update(nsnew)
262         else:
263             self.nsstack.append(self.nsdict)
264
265     def pop_xmlns(self):
266         self.nsdict = self.nsstack.pop()
267
268     def fixname(self, name):
269         if ':' in name:
270             prefix, suffix = name.split(':', 1)
271             if prefix == 'xmlns':
272                 nsuri = self.nsdict.get(suffix)
273                 if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS, ZOPE_I18N_NS):
274                     return name, name, prefix
275             else:
276                 nsuri = self.nsdict.get(prefix)
277                 if nsuri == ZOPE_TAL_NS:
278                     return name, suffix, 'tal'
279                 elif nsuri == ZOPE_METAL_NS:
280                     return name, suffix,  'metal'
281                 elif nsuri == ZOPE_I18N_NS:
282                     return name, suffix, 'i18n'
283         return name, name, 0
284
285     def process_ns(self, name, attrs):
286         attrlist = []
287         taldict = {}
288         metaldict = {}
289         i18ndict = {}
290         name, namebase, namens = self.fixname(name)
291         for item in attrs:
292             key, value = item
293             key, keybase, keyns = self.fixname(key)
294             ns = keyns or namens # default to tag namespace
295             if ns and ns != 'unknown':
296                 item = (key, value, ns)
297             if ns == 'tal':
298                 if taldict.has_key(keybase):
299                     raise TALError("duplicate TAL attribute " +
300                                    `keybase`, self.getpos())
301                 taldict[keybase] = value
302             elif ns == 'metal':
303                 if metaldict.has_key(keybase):
304                     raise METALError("duplicate METAL attribute " +
305                                      `keybase`, self.getpos())
306                 metaldict[keybase] = value
307             elif ns == 'i18n':
308                 if i18ndict.has_key(keybase):
309                     raise I18NError("duplicate i18n attribute " +
310                                     `keybase`, self.getpos())
311                 i18ndict[keybase] = value
312             attrlist.append(item)
313         if namens in ('metal', 'tal'):
314             taldict['tal tag'] = namens
315         return name, attrlist, taldict, metaldict, i18ndict
316 """
317 Generic expat-based XML parser base class.
318 """
319
320
321 class XMLParser:
322
323     ordered_attributes = 0
324
325     handler_names = [
326         "StartElementHandler",
327         "EndElementHandler",
328         "ProcessingInstructionHandler",
329         "CharacterDataHandler",
330         "UnparsedEntityDeclHandler",
331         "NotationDeclHandler",
332         "StartNamespaceDeclHandler",
333         "EndNamespaceDeclHandler",
334         "CommentHandler",
335         "StartCdataSectionHandler",
336         "EndCdataSectionHandler",
337         "DefaultHandler",
338         "DefaultHandlerExpand",
339         "NotStandaloneHandler",
340         "ExternalEntityRefHandler",
341         "XmlDeclHandler",
342         "StartDoctypeDeclHandler",
343         "EndDoctypeDeclHandler",
344         "ElementDeclHandler",
345         "AttlistDeclHandler"
346         ]
347
348     def __init__(self, encoding=None):
349         self.parser = p = self.createParser()
350         if self.ordered_attributes:
351             try:
352                 self.parser.ordered_attributes = self.ordered_attributes
353             except AttributeError:
354                 print "Can't set ordered_attributes"
355                 self.ordered_attributes = 0
356         for name in self.handler_names:
357             method = getattr(self, name, None)
358             if method is not None:
359                 try:
360                     setattr(p, name, method)
361                 except AttributeError:
362                     print "Can't set expat handler %s" % name
363
364     def createParser(self, encoding=None):
365         global XMLParseError
366         try:
367             from Products.ParsedXML.Expat import pyexpat
368             XMLParseError = pyexpat.ExpatError
369             return pyexpat.ParserCreate(encoding, ' ')
370         except ImportError:
371             from xml.parsers import expat
372             XMLParseError = expat.ExpatError
373             return expat.ParserCreate(encoding, ' ')
374
375     def parseFile(self, filename):
376         f = open(filename)
377         self.parseStream(f)
378         #self.parseStream(open(filename))
379
380     def parseString(self, s):
381         self.parser.Parse(s, 1)
382
383     def parseURL(self, url):
384         import urllib
385         self.parseStream(urllib.urlopen(url))
386
387     def parseStream(self, stream):
388         self.parser.ParseFile(stream)
389
390     def parseFragment(self, s, end=0):
391         self.parser.Parse(s, end)
392 """Interface that a TALES engine provides to the METAL/TAL implementation."""
393
394 try:
395     from Interface import Interface
396     from Interface.Attribute import Attribute
397 except:
398     class Interface: pass
399     def Attribute(*args): pass
400
401
402 class ITALESCompiler(Interface):
403     """Compile-time interface provided by a TALES implementation.
404
405     The TAL compiler needs an instance of this interface to support
406     compilation of TALES expressions embedded in documents containing
407     TAL and METAL constructs.
408     """
409
410     def getCompilerError():
411         """Return the exception class raised for compilation errors.
412         """
413
414     def compile(expression):
415         """Return a compiled form of 'expression' for later evaluation.
416
417         'expression' is the source text of the expression.
418
419         The return value may be passed to the various evaluate*()
420         methods of the ITALESEngine interface.  No compatibility is
421         required for the values of the compiled expression between
422         different ITALESEngine implementations.
423         """
424
425
426 class ITALESEngine(Interface):
427     """Render-time interface provided by a TALES implementation.
428
429     The TAL interpreter uses this interface to TALES to support
430     evaluation of the compiled expressions returned by
431     ITALESCompiler.compile().
432     """
433
434     def getCompiler():
435         """Return an object that supports ITALESCompiler."""
436
437     def getDefault():
438         """Return the value of the 'default' TALES expression.
439
440         Checking a value for a match with 'default' should be done
441         using the 'is' operator in Python.
442         """
443
444     def setPosition((lineno, offset)):
445         """Inform the engine of the current position in the source file.
446
447         This is used to allow the evaluation engine to report
448         execution errors so that site developers can more easily
449         locate the offending expression.
450         """
451
452     def setSourceFile(filename):
453         """Inform the engine of the name of the current source file.
454
455         This is used to allow the evaluation engine to report
456         execution errors so that site developers can more easily
457         locate the offending expression.
458         """
459
460     def beginScope():
461         """Push a new scope onto the stack of open scopes.
462         """
463
464     def endScope():
465         """Pop one scope from the stack of open scopes.
466         """
467
468     def evaluate(compiled_expression):
469         """Evaluate an arbitrary expression.
470
471         No constraints are imposed on the return value.
472         """
473
474     def evaluateBoolean(compiled_expression):
475         """Evaluate an expression that must return a Boolean value.
476         """
477
478     def evaluateMacro(compiled_expression):
479         """Evaluate an expression that must return a macro program.
480         """
481
482     def evaluateStructure(compiled_expression):
483         """Evaluate an expression that must return a structured
484         document fragment.
485
486         The result of evaluating 'compiled_expression' must be a
487         string containing a parsable HTML or XML fragment.  Any TAL
488         markup cnotained in the result string will be interpreted.
489         """
490
491     def evaluateText(compiled_expression):
492         """Evaluate an expression that must return text.
493
494         The returned text should be suitable for direct inclusion in
495         the output: any HTML or XML escaping or quoting is the
496         responsibility of the expression itself.
497         """
498
499     def evaluateValue(compiled_expression):
500         """Evaluate an arbitrary expression.
501
502         No constraints are imposed on the return value.
503         """
504
505     def createErrorInfo(exception, (lineno, offset)):
506         """Returns an ITALESErrorInfo object.
507
508         The returned object is used to provide information about the
509         error condition for the on-error handler.
510         """
511
512     def setGlobal(name, value):
513         """Set a global variable.
514
515         The variable will be named 'name' and have the value 'value'.
516         """
517
518     def setLocal(name, value):
519         """Set a local variable in the current scope.
520
521         The variable will be named 'name' and have the value 'value'.
522         """
523
524     def setRepeat(name, compiled_expression):
525         """
526         """
527
528     def translate(domain, msgid, mapping, default=None):
529         """
530         See ITranslationService.translate()
531         """
532
533
534 class ITALESErrorInfo(Interface):
535
536     type = Attribute("type",
537                      "The exception class.")
538
539     value = Attribute("value",
540                       "The exception instance.")
541
542     lineno = Attribute("lineno",
543                        "The line number the error occurred on in the source.")
544
545     offset = Attribute("offset",
546                        "The character offset at which the error occurred.")
547 """
548 Common definitions used by TAL and METAL compilation an transformation.
549 """
550
551 from types import ListType, TupleType
552
553
554 TAL_VERSION = "1.5"
555
556 XML_NS = "http://www.w3.org/XML/1998/namespace" # URI for XML namespace
557 XMLNS_NS = "http://www.w3.org/2000/xmlns/" # URI for XML NS declarations
558
559 ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal"
560 ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal"
561 ZOPE_I18N_NS = "http://xml.zope.org/namespaces/i18n"
562
563 NAME_RE = "[a-zA-Z_][-a-zA-Z0-9_]*"
564
565 KNOWN_METAL_ATTRIBUTES = [
566     "define-macro",
567     "use-macro",
568     "define-slot",
569     "fill-slot",
570     "slot",
571     ]
572
573 KNOWN_TAL_ATTRIBUTES = [
574     "define",
575     "condition",
576     "content",
577     "replace",
578     "repeat",
579     "attributes",
580     "on-error",
581     "omit-tag",
582     "tal tag",
583     ]
584
585 KNOWN_I18N_ATTRIBUTES = [
586     "translate",
587     "domain",
588     "target",
589     "source",
590     "attributes",
591     "data",
592     "name",
593     ]
594
595 class TALError(Exception):
596
597     def __init__(self, msg, position=(None, None)):
598         assert msg != ""
599         self.msg = msg
600         self.lineno = position[0]
601         self.offset = position[1]
602         self.filename = None
603
604     def setFile(self, filename):
605         self.filename = filename
606
607     def __str__(self):
608         result = self.msg
609         if self.lineno is not None:
610             result = result + ", at line %d" % self.lineno
611         if self.offset is not None:
612             result = result + ", column %d" % (self.offset + 1)
613         if self.filename is not None:
614             result = result + ', in file %s' % self.filename
615         return result
616
617 class METALError(TALError):
618     pass
619
620 class TALESError(TALError):
621     pass
622
623 class I18NError(TALError):
624     pass
625
626
627 class ErrorInfo:
628
629     __implements__ = ITALESErrorInfo
630
631     def __init__(self, err, position=(None, None)):
632         if isinstance(err, Exception):
633             self.type = err.__class__
634             self.value = err
635         else:
636             self.type = err
637             self.value = None
638         self.lineno = position[0]
639         self.offset = position[1]
640
641
642
643 import re
644 _attr_re = re.compile(r"\s*([^\s]+)\s+([^\s].*)\Z", re.S)
645 _subst_re = re.compile(r"\s*(?:(text|raw|structure)\s+)?(.*)\Z", re.S)
646 del re
647
648 def parseAttributeReplacements(arg, xml):
649     dict = {}
650     for part in splitParts(arg):
651         m = _attr_re.match(part)
652         if not m:
653             raise TALError("Bad syntax in attributes: " + `part`)
654         name, expr = m.group(1, 2)
655         if not xml:
656             name = name.lower()
657         if dict.has_key(name):
658             raise TALError("Duplicate attribute name in attributes: " + `part`)
659         dict[name] = expr
660     return dict
661
662 def parseSubstitution(arg, position=(None, None)):
663     m = _subst_re.match(arg)
664     if not m:
665         raise TALError("Bad syntax in substitution text: " + `arg`, position)
666     key, expr = m.group(1, 2)
667     if not key:
668         key = "text"
669     return key, expr
670
671 def splitParts(arg):
672     arg = arg.replace(";;", "\0")
673     parts = arg.split(';')
674     parts = [p.replace("\0", ";") for p in parts]
675     if len(parts) > 1 and not parts[-1].strip():
676         del parts[-1] # It ended in a semicolon
677     return parts
678
679 def isCurrentVersion(program):
680     version = getProgramVersion(program)
681     return version == TAL_VERSION
682
683 def getProgramMode(program):
684     version = getProgramVersion(program)
685     if (version == TAL_VERSION and isinstance(program[1], TupleType) and
686         len(program[1]) == 2):
687         opcode, mode = program[1]
688         if opcode == "mode":
689             return mode
690     return None
691
692 def getProgramVersion(program):
693     if (len(program) >= 2 and
694         isinstance(program[0], TupleType) and len(program[0]) == 2):
695         opcode, version = program[0]
696         if opcode == "version":
697             return version
698     return None
699
700 import re
701 _ent1_re = re.compile('&(?![A-Z#])', re.I)
702 _entch_re = re.compile('&([A-Z][A-Z0-9]*)(?![A-Z0-9;])', re.I)
703 _entn1_re = re.compile('&#(?![0-9X])', re.I)
704 _entnx_re = re.compile('&(#X[A-F0-9]*)(?![A-F0-9;])', re.I)
705 _entnd_re = re.compile('&(#[0-9][0-9]*)(?![0-9;])')
706 del re
707
708 def attrEscape(s):
709     """Replace special characters '&<>' by character entities,
710     except when '&' already begins a syntactically valid entity."""
711     s = _ent1_re.sub('&amp;', s)
712     s = _entch_re.sub(r'&amp;\1', s)
713     s = _entn1_re.sub('&amp;#', s)
714     s = _entnx_re.sub(r'&amp;\1', s)
715     s = _entnd_re.sub(r'&amp;\1', s)
716     s = s.replace('<', '&lt;')
717     s = s.replace('>', '&gt;')
718     s = s.replace('"', '&quot;')
719     return s
720 """
721 Code generator for TALInterpreter intermediate code.
722 """
723
724 import re
725 import cgi
726
727
728
729 I18N_REPLACE = 1
730 I18N_CONTENT = 2
731 I18N_EXPRESSION = 3
732
733 _name_rx = re.compile(NAME_RE)
734
735
736 class TALGenerator:
737
738     inMacroUse = 0
739     inMacroDef = 0
740     source_file = None
741
742     def __init__(self, expressionCompiler=None, xml=1, source_file=None):
743         if not expressionCompiler:
744             expressionCompiler = AthanaTALEngine()
745         self.expressionCompiler = expressionCompiler
746         self.CompilerError = expressionCompiler.getCompilerError()
747         self.program = []
748         self.stack = []
749         self.todoStack = []
750         self.macros = {}
751         self.slots = {}
752         self.slotStack = []
753         self.xml = xml
754         self.emit("version", TAL_VERSION)
755         self.emit("mode", xml and "xml" or "html")
756         if source_file is not None:
757             self.source_file = source_file
758             self.emit("setSourceFile", source_file)
759         self.i18nContext = TranslationContext()
760         self.i18nLevel = 0
761
762     def getCode(self):
763         assert not self.stack
764         assert not self.todoStack
765         return self.optimize(self.program), self.macros
766
767     def optimize(self, program):
768         output = []
769         collect = []
770         cursor = 0
771         if self.xml:
772             endsep = "/>"
773         else:
774             endsep = " />"
775         for cursor in xrange(len(program)+1):
776             try:
777                 item = program[cursor]
778             except IndexError:
779                 item = (None, None)
780             opcode = item[0]
781             if opcode == "rawtext":
782                 collect.append(item[1])
783                 continue
784             if opcode == "endTag":
785                 collect.append("</%s>" % item[1])
786                 continue
787             if opcode == "startTag":
788                 if self.optimizeStartTag(collect, item[1], item[2], ">"):
789                     continue
790             if opcode == "startEndTag":
791                 if self.optimizeStartTag(collect, item[1], item[2], endsep):
792                     continue
793             if opcode in ("beginScope", "endScope"):
794                 output.append(self.optimizeArgsList(item))
795                 continue
796             if opcode == 'noop':
797                 opcode = None
798                 pass
799             text = "".join(collect)
800             if text:
801                 i = text.rfind("\n")
802                 if i >= 0:
803                     i = len(text) - (i + 1)
804                     output.append(("rawtextColumn", (text, i)))
805                 else:
806                     output.append(("rawtextOffset", (text, len(text))))
807             if opcode != None:
808                 output.append(self.optimizeArgsList(item))
809             collect = []
810         return self.optimizeCommonTriple(output)
811
812     def optimizeArgsList(self, item):
813         if len(item) == 2:
814             return item
815         else:
816             return item[0], tuple(item[1:])
817
818     def optimizeStartTag(self, collect, name, attrlist, end):
819         if not attrlist:
820             collect.append("<%s%s" % (name, end))
821             return 1
822         opt = 1
823         new = ["<" + name]
824         for i in range(len(attrlist)):
825             item = attrlist[i]
826             if len(item) > 2:
827                 opt = 0
828                 name, value, action = item[:3]
829                 attrlist[i] = (name, value, action) + item[3:]
830             else:
831                 if item[1] is None:
832                     s = item[0]
833                 else:
834                     s = '%s="%s"' % (item[0], attrEscape(item[1]))
835                 attrlist[i] = item[0], s
836                 new.append(" " + s)
837         if opt:
838             new.append(end)
839             collect.extend(new)
840         return opt
841
842     def optimizeCommonTriple(self, program):
843         if len(program) < 3:
844             return program
845         output = program[:2]
846         prev2, prev1 = output
847         for item in program[2:]:
848             if ( item[0] == "beginScope"
849                  and prev1[0] == "setPosition"
850                  and prev2[0] == "rawtextColumn"):
851                 position = output.pop()[1]
852                 text, column = output.pop()[1]
853                 prev1 = None, None
854                 closeprev = 0
855                 if output and output[-1][0] == "endScope":
856                     closeprev = 1
857                     output.pop()
858                 item = ("rawtextBeginScope",
859                         (text, column, position, closeprev, item[1]))
860             output.append(item)
861             prev2 = prev1
862             prev1 = item
863         return output
864
865     def todoPush(self, todo):
866         self.todoStack.append(todo)
867
868     def todoPop(self):
869         return self.todoStack.pop()
870
871     def compileExpression(self, expr):
872         try:
873             return self.expressionCompiler.compile(expr)
874         except self.CompilerError, err:
875             raise TALError('%s in expression %s' % (err.args[0], `expr`),
876                            self.position)
877
878     def pushProgram(self):
879         self.stack.append(self.program)
880         self.program = []
881
882     def popProgram(self):
883         program = self.program
884         self.program = self.stack.pop()
885         return self.optimize(program)
886
887     def pushSlots(self):
888         self.slotStack.append(self.slots)
889         self.slots = {}
890
891     def popSlots(self):
892         slots = self.slots
893         self.slots = self.slotStack.pop()
894         return slots
895
896     def emit(self, *instruction):
897         self.program.append(instruction)
898
899     def emitStartTag(self, name, attrlist, isend=0):
900         if isend:
901             opcode = "startEndTag"
902         else:
903             opcode = "startTag"
904         self.emit(opcode, name, attrlist)
905
906     def emitEndTag(self, name):
907         if self.xml and self.program and self.program[-1][0] == "startTag":
908             self.program[-1] = ("startEndTag",) + self.program[-1][1:]
909         else:
910             self.emit("endTag", name)
911
912     def emitOptTag(self, name, optTag, isend):
913         program = self.popProgram() #block
914         start = self.popProgram() #start tag
915         if (isend or not program) and self.xml:
916             start[-1] = ("startEndTag",) + start[-1][1:]
917             isend = 1
918         cexpr = optTag[0]
919         if cexpr:
920             cexpr = self.compileExpression(optTag[0])
921         self.emit("optTag", name, cexpr, optTag[1], isend, start, program)
922
923     def emitRawText(self, text):
924         self.emit("rawtext", text)
925
926     def emitText(self, text):
927         self.emitRawText(cgi.escape(text))
928
929     def emitDefines(self, defines):
930         for part in splitParts(defines):
931             m = re.match(
932                 r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part)
933             if not m:
934                 raise TALError("invalid define syntax: " + `part`,
935                                self.position)
936             scope, name, expr = m.group(1, 2, 3)
937             scope = scope or "local"
938             cexpr = self.compileExpression(expr)
939             if scope == "local":
940                 self.emit("setLocal", name, cexpr)
941             else:
942                 self.emit("setGlobal", name, cexpr)
943
944     def emitOnError(self, name, onError, TALtag, isend):
945         block = self.popProgram()
946         key, expr = parseSubstitution(onError)
947         cexpr = self.compileExpression(expr)
948         if key == "text":
949             self.emit("insertText", cexpr, [])
950         elif key == "raw":
951             self.emit("insertRaw", cexpr, [])
952         else:
953             assert key == "structure"
954             self.emit("insertStructure", cexpr, {}, [])
955         if TALtag:
956             self.emitOptTag(name, (None, 1), isend)
957         else:
958             self.emitEndTag(name)
959         handler = self.popProgram()
960         self.emit("onError", block, handler)
961
962     def emitCondition(self, expr):
963         cexpr = self.compileExpression(expr)
964         program = self.popProgram()
965         self.emit("condition", cexpr, program)
966
967     def emitRepeat(self, arg):
968
969         
970         m = re.match("(?s)\s*(%s)\s+(.*)\Z" % NAME_RE, arg)
971         if not m:
972             raise TALError("invalid repeat syntax: " + `arg`,
973                            self.position)
974         name, expr = m.group(1, 2)
975         cexpr = self.compileExpression(expr)
976         program = self.popProgram()
977         self.emit("loop", name, cexpr, program)
978
979
980     def emitSubstitution(self, arg, attrDict={}):
981         key, expr = parseSubstitution(arg)
982         cexpr = self.compileExpression(expr)
983         program = self.popProgram()
984         if key == "text":
985             self.emit("insertText", cexpr, program)
986         elif key == "raw":
987             self.emit("insertRaw", cexpr, program)
988         else:
989             assert key == "structure"
990             self.emit("insertStructure", cexpr, attrDict, program)
991
992     def emitI18nVariable(self, stuff):
993         varname, action, expression = stuff
994         m = _name_rx.match(varname)
995         if m is None or m.group() != varname:
996             raise TALError("illegal i18n:name: %r" % varname, self.position)
997         key = cexpr = None
998         program = self.popProgram()
999         if action == I18N_REPLACE:
1000             program = program[1:-1]
1001         elif action == I18N_CONTENT:
1002             pass
1003         else:
1004             assert action == I18N_EXPRESSION
1005             key, expr = parseSubstitution(expression)
1006             cexpr = self.compileExpression(expr)
1007         self.emit('i18nVariable',
1008                   varname, program, cexpr, int(key == "structure"))
1009
1010     def emitTranslation(self, msgid, i18ndata):
1011         program = self.popProgram()
1012         if i18ndata is None:
1013             self.emit('insertTranslation', msgid, program)
1014         else:
1015             key, expr = parseSubstitution(i18ndata)
1016             cexpr = self.compileExpression(expr)
1017             assert key == 'text'
1018             self.emit('insertTranslation', msgid, program, cexpr)
1019
1020     def emitDefineMacro(self, macroName):
1021         program = self.popProgram()
1022         macroName = macroName.strip()
1023         if self.macros.has_key(macroName):
1024             raise METALError("duplicate macro definition: %s" % `macroName`,
1025                              self.position)
1026         if not re.match('%s$' % NAME_RE, macroName):
1027             raise METALError("invalid macro name: %s" % `macroName`,
1028                              self.position)
1029         self.macros[macroName] = program
1030         self.inMacroDef = self.inMacroDef - 1
1031         self.emit("defineMacro", macroName, program)
1032
1033     def emitUseMacro(self, expr):
1034         cexpr = self.compileExpression(expr)
1035         program = self.popProgram()
1036         self.inMacroUse = 0
1037         self.emit("useMacro", expr, cexpr, self.popSlots(), program)
1038
1039     def emitDefineSlot(self, slotName):
1040         program = self.popProgram()
1041         slotName = slotName.strip()
1042         if not re.match('%s$' % NAME_RE, slotName):
1043             raise METALError("invalid slot name: %s" % `slotName`,
1044                              self.position)
1045         self.emit("defineSlot", slotName, program)
1046
1047     def emitFillSlot(self, slotName):
1048         program = self.popProgram()
1049         slotName = slotName.strip()
1050         if self.slots.has_key(slotName):
1051             raise METALError("duplicate fill-slot name: %s" % `slotName`,
1052                              self.position)
1053         if not re.match('%s$' % NAME_RE, slotName):
1054             raise METALError("invalid slot name: %s" % `slotName`,
1055                              self.position)
1056         self.slots[slotName] = program
1057         self.inMacroUse = 1
1058         self.emit("fillSlot", slotName, program)
1059
1060     def unEmitWhitespace(self):
1061         collect = []
1062         i = len(self.program) - 1
1063         while i >= 0:
1064             item = self.program[i]
1065             if item[0] != "rawtext":
1066                 break
1067             text = item[1]
1068             if not re.match(r"\A\s*\Z", text):
1069                 break
1070             collect.append(text)
1071             i = i-1
1072         del self.program[i+1:]
1073         if i >= 0 and self.program[i][0] == "rawtext":
1074             text = self.program[i][1]
1075             m = re.search(r"\s+\Z", text)
1076             if m:
1077                 self.program[i] = ("rawtext", text[:m.start()])
1078                 collect.append(m.group())
1079         collect.reverse()
1080         return "".join(collect)
1081
1082     def unEmitNewlineWhitespace(self):
1083         collect = []
1084         i = len(self.program)
1085         while i > 0:
1086             i = i-1
1087             item = self.program[i]
1088             if item[0] != "rawtext":
1089                 break
1090             text = item[1]
1091             if re.match(r"\A[ \t]*\Z", text):
1092                 collect.append(text)
1093                 continue
1094             m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text)
1095             if not m:
1096                 break
1097             text, rest = m.group(1, 2)
1098             collect.reverse()
1099             rest = rest + "".join(collect)
1100             del self.program[i:]
1101             if text:
1102                 self.emit("rawtext", text)
1103             return rest
1104         return None
1105
1106     def replaceAttrs(self, attrlist, repldict):
1107         if not repldict:
1108             return attrlist
1109         newlist = []
1110         for item in attrlist:
1111             key = item[0]
1112             if repldict.has_key(key):
1113                 expr, xlat, msgid = repldict[key]
1114                 item = item[:2] + ("replace", expr, xlat, msgid)
1115                 del repldict[key]
1116             newlist.append(item)
1117         for key, (expr, xlat, msgid) in repldict.items():
1118             newlist.append((key, None, "insert", expr, xlat, msgid))
1119         return newlist
1120
1121     def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
1122                          position=(None, None), isend=0):
1123         if not taldict and not metaldict and not i18ndict:
1124             self.emitStartTag(name, attrlist, isend)
1125             self.todoPush({})
1126             if isend:
1127                 self.emitEndElement(name, isend)
1128             return
1129
1130         self.position = position
1131         for key, value in taldict.items():
1132             if key not in KNOWN_TAL_ATTRIBUTES:
1133                 raise TALError("bad TAL attribute: " + `key`, position)
1134             if not (value or key == 'omit-tag'):
1135                 raise TALError("missing value for TAL attribute: " +
1136                                `key`, position)
1137         for key, value in metaldict.items():
1138             if key not in KNOWN_METAL_ATTRIBUTES:
1139                 raise METALError("bad METAL attribute: " + `key`,
1140                                  position)
1141             if not value:
1142                 raise TALError("missing value for METAL attribute: " +
1143                                `key`, position)
1144         for key, value in i18ndict.items():
1145             if key not in KNOWN_I18N_ATTRIBUTES:
1146                 raise I18NError("bad i18n attribute: " + `key`, position)
1147             if not value and key in ("attributes", "data", "id"):
1148                 raise I18NError("missing value for i18n attribute: " +
1149                                 `key`, position)
1150         todo = {}
1151         defineMacro = metaldict.get("define-macro")
1152         useMacro = metaldict.get("use-macro")
1153         defineSlot = metaldict.get("define-slot")
1154         fillSlot = metaldict.get("fill-slot")
1155         define = taldict.get("define")
1156         condition = taldict.get("condition")
1157         repeat = taldict.get("repeat")
1158         content = taldict.get("content")
1159         replace = taldict.get("replace")
1160         attrsubst = taldict.get("attributes")
1161         onError = taldict.get("on-error")
1162         omitTag = taldict.get("omit-tag")
1163         TALtag = taldict.get("tal tag")
1164         i18nattrs = i18ndict.get("attributes")
1165         msgid = i18ndict.get("translate")
1166         varname = i18ndict.get('name')
1167         i18ndata = i18ndict.get('data')
1168
1169         if varname and not self.i18nLevel:
1170             raise I18NError(
1171                 "i18n:name can only occur inside a translation unit",
1172                 position)
1173
1174         if i18ndata and not msgid:
1175             raise I18NError("i18n:data must be accompanied by i18n:translate",
1176                             position)
1177
1178         if len(metaldict) > 1 and (defineMacro or useMacro):
1179             raise METALError("define-macro and use-macro cannot be used "
1180                              "together or with define-slot or fill-slot",
1181                              position)
1182         if replace:
1183             if content:
1184                 raise TALError(
1185                     "tal:content and tal:replace are mutually exclusive",
1186                     position)
1187             if msgid is not None:
1188                 raise I18NError(
1189                     "i18n:translate and tal:replace are mutually exclusive",
1190                     position)
1191
1192         repeatWhitespace = None
1193         if repeat:
1194             repeatWhitespace = self.unEmitNewlineWhitespace()
1195         if position != (None, None):
1196             self.emit("setPosition", position)
1197         if self.inMacroUse:
1198             if fillSlot:
1199                 self.pushProgram()
1200                 if self.source_file is not None:
1201                     self.emit("setSourceFile", self.source_file)
1202                 todo["fillSlot"] = fillSlot
1203                 self.inMacroUse = 0
1204         else:
1205             if fillSlot:
1206                 raise METALError("fill-slot must be within a use-macro",
1207                                  position)
1208         if not self.inMacroUse:
1209             if defineMacro:
1210                 self.pushProgram()
1211                 self.emit("version", TAL_VERSION)
1212                 self.emit("mode", self.xml and "xml" or "html")
1213                 if self.source_file is not None:
1214                     self.emit("setSourceFile", self.source_file)
1215                 todo["defineMacro"] = defineMacro
1216                 self.inMacroDef = self.inMacroDef + 1
1217             if useMacro:
1218                 self.pushSlots()
1219                 self.pushProgram()
1220                 todo["useMacro"] = useMacro
1221                 self.inMacroUse = 1
1222             if defineSlot:
1223                 if not self.inMacroDef:
1224                     raise METALError(
1225                         "define-slot must be within a define-macro",
1226                         position)
1227                 self.pushProgram()
1228                 todo["defineSlot"] = defineSlot
1229
1230         if defineSlot or i18ndict:
1231
1232             domain = i18ndict.get("domain") or self.i18nContext.domain
1233             source = i18ndict.get("source") or self.i18nContext.source
1234             target = i18ndict.get("target") or self.i18nContext.target
1235             if (  domain != DEFAULT_DOMAIN
1236                   or source is not None
1237                   or target is not None):
1238                 self.i18nContext = TranslationContext(self.i18nContext,
1239                                                       domain=domain,
1240                                                       source=source,
1241                                                       target=target)
1242                 self.emit("beginI18nContext",
1243                           {"domain": domain, "source": source,
1244                            "target": target})
1245                 todo["i18ncontext"] = 1
1246         if taldict or i18ndict:
1247             dict = {}
1248             for item in attrlist:
1249                 key, value = item[:2]
1250                 dict[key] = value
1251             self.emit("beginScope", dict)
1252             todo["scope"] = 1
1253         if onError:
1254             self.pushProgram() # handler
1255             if TALtag:
1256                 self.pushProgram() # start
1257             self.emitStartTag(name, list(attrlist)) # Must copy attrlist!
1258             if TALtag:
1259                 self.pushProgram() # start
1260             self.pushProgram() # block
1261             todo["onError"] = onError
1262         if define:
1263             self.emitDefines(define)
1264             todo["define"] = define
1265         if condition:
1266             self.pushProgram()
1267             todo["condition"] = condition
1268         if repeat:
1269             todo["repeat"] = repeat
1270             self.pushProgram()
1271             if repeatWhitespace:
1272                 self.emitText(repeatWhitespace)
1273         if content:
1274             if varname:
1275                 todo['i18nvar'] = (varname, I18N_CONTENT, None)
1276                 todo["content"] = content
1277                 self.pushProgram()
1278             else:
1279                 todo["content"] = content
1280         elif replace:
1281             if varname:
1282                 todo['i18nvar'] = (varname, I18N_EXPRESSION, replace)
1283             else:
1284                 todo["replace"] = replace
1285             self.pushProgram()
1286         elif varname:
1287             todo['i18nvar'] = (varname, I18N_REPLACE, None)
1288             self.pushProgram()
1289         if msgid is not None:
1290             self.i18nLevel += 1
1291             todo['msgid'] = msgid
1292         if i18ndata:
1293             todo['i18ndata'] = i18ndata
1294         optTag = omitTag is not None or TALtag
1295         if optTag:
1296             todo["optional tag"] = omitTag, TALtag
1297             self.pushProgram()
1298         if attrsubst or i18nattrs:
1299             if attrsubst:
1300                 repldict = parseAttributeReplacements(attrsubst,
1301                                                               self.xml)
1302             else:
1303                 repldict = {}
1304             if i18nattrs:
1305                 i18nattrs = _parseI18nAttributes(i18nattrs, attrlist, repldict,
1306                                                  self.position, self.xml,
1307                                                  self.source_file)
1308             else:
1309                 i18nattrs = {}
1310             for key, value in repldict.items():
1311                 if i18nattrs.get(key, None):
1312                     raise I18NError(
1313                       ("attribute [%s] cannot both be part of tal:attributes" +
1314                       " and have a msgid in i18n:attributes") % key,
1315                     position)
1316                 ce = self.compileExpression(value)
1317                 repldict[key] = ce, key in i18nattrs, i18nattrs.get(key)
1318             for key in i18nattrs:
1319                 if not repldict.has_key(key):
1320                     repldict[key] = None, 1, i18nattrs.get(key)
1321         else:
1322             repldict = {}
1323         if replace:
1324             todo["repldict"] = repldict
1325             repldict = {}
1326         self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
1327         if optTag:
1328             self.pushProgram()
1329         if content and not varname:
1330             self.pushProgram()
1331         if msgid is not None:
1332             self.pushProgram()
1333         if content and varname:
1334             self.pushProgram()
1335         if todo and position != (None, None):
1336             todo["position"] = position
1337         self.todoPush(todo)
1338         if isend:
1339             self.emitEndElement(name, isend)
1340
1341     def emitEndElement(self, name, isend=0, implied=0):
1342         todo = self.todoPop()
1343         if not todo:
1344             if not isend:
1345                 self.emitEndTag(name)
1346             return
1347
1348         self.position = position = todo.get("position", (None, None))
1349         defineMacro = todo.get("defineMacro")
1350         useMacro = todo.get("useMacro")
1351         defineSlot = todo.get("defineSlot")
1352         fillSlot = todo.get("fillSlot")
1353         repeat = todo.get("repeat")
1354         content = todo.get("content")
1355         replace = todo.get("replace")
1356         condition = todo.get("condition")
1357         onError = todo.get("onError")
1358         repldict = todo.get("repldict", {})
1359         scope = todo.get("scope")
1360         optTag = todo.get("optional tag")
1361         msgid = todo.get('msgid')
1362         i18ncontext = todo.get("i18ncontext")
1363         varname = todo.get('i18nvar')
1364         i18ndata = todo.get('i18ndata')
1365
1366         if implied > 0:
1367             if defineMacro or useMacro or defineSlot or fillSlot:
1368                 exc = METALError
1369                 what = "METAL"
1370             else:
1371                 exc = TALError
1372                 what = "TAL"
1373             raise exc("%s attributes on <%s> require explicit </%s>" %
1374                       (what, name, name), position)
1375
1376         if content:
1377             self.emitSubstitution(content, {})
1378         if msgid is not None:
1379             if (not varname) or (
1380                 varname and (varname[1] == I18N_CONTENT)):
1381                 self.emitTranslation(msgid, i18ndata)
1382             self.i18nLevel -= 1
1383         if optTag:
1384             self.emitOptTag(name, optTag, isend)
1385         elif not isend:
1386             if varname:
1387                 self.emit('noop')
1388             self.emitEndTag(name)
1389         if replace:
1390             self.emitSubstitution(replace, repldict)
1391         elif varname:
1392             assert (varname[1]
1393                     in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION])
1394             self.emitI18nVariable(varname)
1395         if msgid is not None: 
1396             if varname and (varname[1] <> I18N_CONTENT):
1397                 self.emitTranslation(msgid, i18ndata)
1398         if repeat:
1399             self.emitRepeat(repeat)
1400         if condition:
1401             self.emitCondition(condition)
1402         if onError:
1403             self.emitOnError(name, onError, optTag and optTag[1], isend)
1404         if scope:
1405             self.emit("endScope")
1406         if i18ncontext:
1407             self.emit("endI18nContext")
1408             assert self.i18nContext.parent is not None
1409             self.i18nContext = self.i18nContext.parent
1410         if defineSlot:
1411             self.emitDefineSlot(defineSlot)
1412         if fillSlot:
1413             self.emitFillSlot(fillSlot)
1414         if useMacro:
1415             self.emitUseMacro(useMacro)
1416         if defineMacro:
1417             self.emitDefineMacro(defineMacro)
1418
1419 def _parseI18nAttributes(i18nattrs, attrlist, repldict, position,
1420                          xml, source_file):
1421
1422     def addAttribute(dic, attr, msgid, position, xml):
1423         if not xml:
1424             attr = attr.lower()
1425         if attr in dic:
1426             raise TALError(
1427                 "attribute may only be specified once in i18n:attributes: "
1428                 + attr,
1429                 position)
1430         dic[attr] = msgid
1431
1432     d = {}
1433     if ';' in i18nattrs:
1434         i18nattrlist = i18nattrs.split(';')
1435         i18nattrlist = [attr.strip().split() 
1436                         for attr in i18nattrlist if attr.strip()]
1437         for parts in i18nattrlist:
1438             if len(parts) > 2:
1439                 raise TALError("illegal i18n:attributes specification: %r"
1440                                 % parts, position)
1441             if len(parts) == 2:
1442                 attr, msgid = parts
1443             else:
1444                 attr = parts[0]
1445                 msgid = None
1446             addAttribute(d, attr, msgid, position, xml)
1447     else:
1448         i18nattrlist = i18nattrs.split()
1449         if len(i18nattrlist) == 1:
1450             addAttribute(d, i18nattrlist[0], None, position, xml)
1451         elif len(i18nattrlist) == 2:
1452             staticattrs = [attr[0] for attr in attrlist if len(attr) == 2]
1453             if (not i18nattrlist[1] in staticattrs) and (
1454                 not i18nattrlist[1] in repldict):
1455                 attr, msgid = i18nattrlist
1456                 addAttribute(d, attr, msgid, position, xml)    
1457             else:
1458                 import warnings
1459                 warnings.warn(I18N_ATTRIBUTES_WARNING
1460                 % (source_file, str(position), i18nattrs)
1461                 , DeprecationWarning)
1462                 msgid = None
1463                 for attr in i18nattrlist:
1464                     addAttribute(d, attr, msgid, position, xml)    
1465         else:    
1466             import warnings
1467             warnings.warn(I18N_ATTRIBUTES_WARNING
1468             % (source_file, str(position), i18nattrs)
1469             , DeprecationWarning)
1470             msgid = None
1471             for attr in i18nattrlist:
1472                 addAttribute(d, attr, msgid, position, xml)    
1473     return d
1474
1475 I18N_ATTRIBUTES_WARNING = (
1476     'Space separated attributes in i18n:attributes'
1477     ' are deprecated (i18n:attributes="value title"). Please use'
1478     ' semicolon to separate attributes'
1479     ' (i18n:attributes="value; title").'
1480     '\nFile %s at row, column %s\nAttributes %s')
1481
1482 """Interpreter for a pre-compiled TAL program.
1483
1484 """
1485 import cgi
1486 import sys
1487 import getopt
1488 import re
1489 from cgi import escape
1490
1491 from StringIO import StringIO
1492
1493
1494
1495 class ConflictError:
1496     pass
1497
1498 class MessageID:
1499     pass
1500
1501
1502
1503 BOOLEAN_HTML_ATTRS = [
1504     "compact", "nowrap", "ismap", "declare", "noshade", "checked",
1505     "disabled", "readonly", "multiple", "selected", "noresize",
1506     "defer"
1507 ]
1508
1509 def _init():
1510     d = {}
1511     for s in BOOLEAN_HTML_ATTRS:
1512         d[s] = 1
1513     return d
1514
1515 BOOLEAN_HTML_ATTRS = _init()
1516
1517 _nulljoin = ''.join
1518 _spacejoin = ' '.join
1519
1520 def normalize(text):
1521     return _spacejoin(text.split())
1522
1523
1524 NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
1525 _interp_regex = re.compile(r'(?<!\$)(\$(?:%(n)s|{%(n)s}))' %({'n': NAME_RE}))
1526 _get_var_regex = re.compile(r'%(n)s' %({'n': NAME_RE}))
1527
1528 def interpolate(text, mapping):
1529     """Interpolate ${keyword} substitutions.
1530
1531     This is called when no translation is provided by the translation
1532     service.
1533     """
1534     if not mapping:
1535         return text
1536     to_replace = _interp_regex.findall(text)
1537     for string in to_replace:
1538         var = _get_var_regex.findall(string)[0]
1539         if mapping.has_key(var):
1540             subst = ustr(mapping[var])
1541             try:
1542                 text = text.replace(string, subst)
1543             except UnicodeError:
1544                 subst = `subst`[1:-1]
1545                 text = text.replace(string, subst)
1546     return text
1547
1548
1549 class AltTALGenerator(TALGenerator):
1550
1551     def __init__(self, repldict, expressionCompiler=None, xml=0):
1552         self.repldict = repldict
1553         self.enabled = 1
1554         TALGenerator.__init__(self, expressionCompiler, xml)
1555
1556     def enable(self, enabled):
1557         self.enabled = enabled
1558
1559     def emit(self, *args):
1560         if self.enabled:
1561             TALGenerator.emit(self, *args)
1562
1563     def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
1564                          position=(None, None), isend=0):
1565         metaldict = {}
1566         taldict = {}
1567         i18ndict = {}
1568         if self.enabled and self.repldict:
1569             taldict["attributes"] = "x x"
1570         TALGenerator.emitStartElement(self, name, attrlist,
1571                                       taldict, metaldict, i18ndict,
1572                                       position, isend)
1573
1574     def replaceAttrs(self, attrlist, repldict):
1575         if self.enabled and self.repldict:
1576             repldict = self.repldict
1577             self.repldict = None
1578         return TALGenerator.replaceAttrs(self, attrlist, repldict)
1579
1580
1581 class TALInterpreter:
1582
1583     def __init__(self, program, macros, engine, stream=None,
1584                  debug=0, wrap=60, metal=1, tal=1, showtal=-1,
1585                  strictinsert=1, stackLimit=100, i18nInterpolate=1):
1586         self.program = program
1587         self.macros = macros
1588         self.engine = engine # Execution engine (aka context)
1589         self.Default = engine.getDefault()
1590         self.stream = stream or sys.stdout
1591         self._stream_write = self.stream.write
1592         self.debug = debug
1593         self.wrap = wrap
1594         self.metal = metal
1595         self.tal = tal
1596         if tal:
1597             self.dispatch = self.bytecode_handlers_tal
1598         else:
1599             self.dispatch = self.bytecode_handlers
1600         assert showtal in (-1, 0, 1)
1601         if showtal == -1:
1602             showtal = (not tal)
1603         self.showtal = showtal
1604         self.strictinsert = strictinsert
1605         self.stackLimit = stackLimit
1606         self.html = 0
1607         self.endsep = "/>"
1608         self.endlen = len(self.endsep)
1609         self.macroStack = []
1610         self.position = None, None  # (lineno, offset)
1611         self.col = 0
1612         self.level = 0
1613         self.scopeLevel = 0
1614         self.sourceFile = None
1615         self.i18nStack = []
1616         self.i18nInterpolate = i18nInterpolate
1617         self.i18nContext = TranslationContext()
1618
1619     def StringIO(self):
1620         return FasterStringIO()
1621
1622     def saveState(self):
1623         return (self.position, self.col, self.stream,
1624                 self.scopeLevel, self.level, self.i18nContext)
1625
1626     def restoreState(self, state):
1627         (self.position, self.col, self.stream,
1628          scopeLevel, level, i18n) = state
1629         self._stream_write = self.stream.write
1630         assert self.level == level
1631         while self.scopeLevel > scopeLevel:
1632             self.engine.endScope()
1633             self.scopeLevel = self.scopeLevel - 1
1634         self.engine.setPosition(self.position)
1635         self.i18nContext = i18n
1636
1637     def restoreOutputState(self, state):
1638         (dummy, self.col, self.stream,
1639          scopeLevel, level, i18n) = state
1640         self._stream_write = self.stream.write
1641         assert self.level == level
1642         assert self.scopeLevel == scopeLevel
1643
1644     def pushMacro(self, macroName, slots, entering=1):
1645         if len(self.macroStack) >= self.stackLimit:
1646             raise METALError("macro nesting limit (%d) exceeded "
1647                              "by %s" % (self.stackLimit, `macroName`))
1648         self.macroStack.append([macroName, slots, entering, self.i18nContext])
1649
1650     def popMacro(self):
1651         return self.macroStack.pop()
1652
1653     def __call__(self):
1654         assert self.level == 0
1655         assert self.scopeLevel == 0
1656         assert self.i18nContext.parent is None
1657         self.interpret(self.program)
1658         assert self.level == 0
1659         assert self.scopeLevel == 0
1660         assert self.i18nContext.parent is None
1661         if self.col > 0:
1662             self._stream_write("\n")
1663             self.col = 0
1664
1665     def interpretWithStream(self, program, stream):
1666         oldstream = self.stream
1667         self.stream = stream
1668         self._stream_write = stream.write
1669         try:
1670             self.interpret(program)
1671         finally:
1672             self.stream = oldstream
1673             self._stream_write = oldstream.write
1674
1675     def stream_write(self, s,
1676                      len=len):
1677         self._stream_write(s)
1678         i = s.rfind('\n')
1679         if i < 0:
1680             self.col = self.col + len(s)
1681         else:
1682             self.col = len(s) - (i + 1)
1683
1684     bytecode_handlers = {}
1685
1686     def interpret(self, program):
1687         oldlevel = self.level
1688         self.level = oldlevel + 1
1689         handlers = self.dispatch
1690         try:
1691             if self.debug:
1692                 for (opcode, args) in program:
1693                     s = "%sdo_%s(%s)\n" % ("    "*self.level, opcode,
1694                                            repr(args))
1695                     if len(s) > 80:
1696                         s = s[:76] + "...\n"
1697                     sys.stderr.write(s)
1698                     handlers[opcode](self, args)
1699             else:
1700                 for (opcode, args) in program:
1701                     handlers[opcode](self, args)
1702         finally:
1703             self.level = oldlevel
1704
1705     def do_version(self, version):
1706         assert version == TAL_VERSION
1707     bytecode_handlers["version"] = do_version
1708
1709     def do_mode(self, mode):
1710         assert mode in ("html", "xml")
1711         self.html = (mode == "html")
1712         if self.html:
1713             self.endsep = " />"
1714         else:
1715             self.endsep = "/>"
1716         self.endlen = len(self.endsep)
1717     bytecode_handlers["mode"] = do_mode
1718
1719     def do_setSourceFile(self, source_file):
1720         self.sourceFile = source_file
1721         self.engine.setSourceFile(source_file)
1722     bytecode_handlers["setSourceFile"] = do_setSourceFile
1723
1724     def do_setPosition(self, position):
1725         self.position = position
1726         self.engine.setPosition(position)
1727     bytecode_handlers["setPosition"] = do_setPosition
1728
1729     def do_startEndTag(self, stuff):
1730         self.do_startTag(stuff, self.endsep, self.endlen)
1731     bytecode_handlers["startEndTag"] = do_startEndTag
1732
1733     def do_startTag(self, (name, attrList),
1734                     end=">", endlen=1, _len=len):
1735         self._currentTag = name
1736         L = ["<", name]
1737         append = L.append
1738         col = self.col + _len(name) + 1
1739         wrap = self.wrap
1740         align = col + 1
1741         if align >= wrap/2:
1742             align = 4  # Avoid a narrow column far to the right
1743         attrAction = self.dispatch["<attrAction>"]
1744         try:
1745             for item in attrList:
1746                 if _len(item) == 2:
1747                     name, s = item
1748                 else:
1749                     if item[2] in ('metal', 'tal', 'xmlns', 'i18n'):
1750                         if not self.showtal:
1751                             continue
1752                         ok, name, s = self.attrAction(item)
1753                     else:
1754                         ok, name, s = attrAction(self, item)
1755                     if not ok:
1756                         continue
1757                 slen = _len(s)
1758                 if (wrap and
1759                     col >= align and
1760                     col + 1 + slen > wrap):
1761                     append("\n")
1762                     append(" "*align)
1763                     col = align + slen
1764                 else:
1765                     append(" ")
1766                     col = col + 1 + slen
1767                 append(s)
1768             append(end)
1769             col = col + endlen
1770         finally:
1771             self._stream_write(_nulljoin(L))
1772             self.col = col
1773     bytecode_handlers["startTag"] = do_startTag
1774
1775     def attrAction(self, item):
1776         name, value, action = item[:3]
1777         if action == 'insert':
1778             return 0, name, value
1779         macs = self.macroStack
1780         if action == 'metal' and self.metal and macs:
1781             if len(macs) > 1 or not macs[-1][2]:
1782                 return 0, name, value
1783             macs[-1][2] = 0
1784             i = name.rfind(":") + 1
1785             prefix, suffix = name[:i], name[i:]
1786             if suffix == "define-macro":
1787                 name = prefix + "use-macro"
1788                 value = macs[-1][0] # Macro name
1789             elif suffix == "define-slot":
1790                 name = prefix + "fill-slot"
1791             elif suffix == "fill-slot":
1792                 pass
1793             else:
1794                 return 0, name, value
1795
1796         if value is None:
1797             value = name
1798         else:
1799             value = '%s="%s"' % (name, attrEscape(value))
1800         return 1, name, value
1801
1802     def attrAction_tal(self, item):
1803         name, value, action = item[:3]
1804         ok = 1
1805         expr, xlat, msgid = item[3:]
1806         if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
1807             evalue = self.engine.evaluateBoolean(item[3])
1808             if evalue is self.Default:
1809                 if action == 'insert': # Cancelled insert
1810                     ok = 0
1811             elif evalue:
1812                 value = None
1813             else:
1814                 ok = 0
1815         elif expr is not None:
1816             evalue = self.engine.evaluateText(item[3])
1817             if evalue is self.Default:
1818                 if action == 'insert': # Cancelled insert
1819                     ok = 0
1820             else:
1821                 if evalue is None:
1822                     ok = 0
1823                 value = evalue
1824         else:
1825             evalue = None
1826
1827         if ok:
1828             if xlat:
1829                 translated = self.translate(msgid or value, value, {})
1830                 if translated is not None:
1831                     value = translated
1832             if value is None:
1833                 value = name
1834             elif evalue is self.Default:
1835                 value = attrEscape(value)
1836             else:
1837                 value = escape(value, quote=1)
1838             value = '%s="%s"' % (name, value)
1839         return ok, name, value
1840     bytecode_handlers["<attrAction>"] = attrAction
1841
1842     def no_tag(self, start, program):
1843         state = self.saveState()
1844         self.stream = stream = self.StringIO()
1845         self._stream_write = stream.write
1846         self.interpret(start)
1847         self.restoreOutputState(state)
1848         self.interpret(program)
1849
1850     def do_optTag(self, (name, cexpr, tag_ns, isend, start, program),
1851                   omit=0):
1852         if tag_ns and not self.showtal:
1853             return self.no_tag(start, program)
1854
1855         self.interpret(start)
1856         if not isend:
1857             self.interpret(program)
1858             s = '</%s>' % name
1859             self._stream_write(s)
1860             self.col = self.col + len(s)
1861
1862     def do_optTag_tal(self, stuff):
1863         cexpr = stuff[1]
1864         if cexpr is not None and (cexpr == '' or
1865                                   self.engine.evaluateBoolean(cexpr)):
1866             self.no_tag(stuff[-2], stuff[-1])
1867         else:
1868             self.do_optTag(stuff)
1869     bytecode_handlers["optTag"] = do_optTag
1870
1871     def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)):
1872         self._stream_write(s)
1873         self.col = col
1874         self.position = position
1875         self.engine.setPosition(position)
1876         if closeprev:
1877             engine = self.engine
1878             engine.endScope()
1879             engine.beginScope()
1880         else:
1881             self.engine.beginScope()
1882             self.scopeLevel = self.scopeLevel + 1
1883
1884     def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
1885         self._stream_write(s)
1886         self.col = col
1887         engine = self.engine
1888         self.position = position
1889         engine.setPosition(position)
1890         if closeprev:
1891             engine.endScope()
1892             engine.beginScope()
1893         else:
1894             engine.beginScope()
1895             self.scopeLevel = self.scopeLevel + 1
1896         engine.setLocal("attrs", dict)
1897     bytecode_handlers["rawtextBeginScope"] = do_rawtextBeginScope
1898
1899     def do_beginScope(self, dict):
1900         self.engine.beginScope()
1901         self.scopeLevel = self.scopeLevel + 1
1902
1903     def do_beginScope_tal(self, dict):
1904         engine = self.engine
1905         engine.beginScope()
1906         engine.setLocal("attrs", dict)
1907         self.scopeLevel = self.scopeLevel + 1
1908     bytecode_handlers["beginScope"] = do_beginScope
1909
1910     def do_endScope(self, notused=None):
1911         self.engine.endScope()
1912         self.scopeLevel = self.scopeLevel - 1
1913     bytecode_handlers["endScope"] = do_endScope
1914
1915     def do_setLocal(self, notused):
1916         pass
1917
1918     def do_setLocal_tal(self, (name, expr)):
1919         self.engine.setLocal(name, self.engine.evaluateValue(expr))
1920     bytecode_handlers["setLocal"] = do_setLocal
1921
1922     def do_setGlobal_tal(self, (name, expr)):
1923         self.engine.setGlobal(name, self.engine.evaluateValue(expr))
1924     bytecode_handlers["setGlobal"] = do_setLocal
1925
1926     def do_beginI18nContext(self, settings):
1927         get = settings.get
1928         self.i18nContext = TranslationContext(self.i18nContext,
1929                                               domain=get("domain"),
1930                                               source=get("source"),
1931                                               target=get("target"))
1932     bytecode_handlers["beginI18nContext"] = do_beginI18nContext
1933
1934     def do_endI18nContext(self, notused=None):
1935         self.i18nContext = self.i18nContext.parent
1936         assert self.i18nContext is not None
1937     bytecode_handlers["endI18nContext"] = do_endI18nContext
1938
1939     def do_insertText(self, stuff):
1940         self.interpret(stuff[1])
1941
1942     def do_insertText_tal(self, stuff):
1943         text = self.engine.evaluateText(stuff[0])
1944         if text is None:
1945             return
1946         if text is self.Default:
1947             self.interpret(stuff[1])
1948             return
1949         if isinstance(text, MessageID):
1950             text = self.engine.translate(text.domain, text, text.mapping)
1951         s = escape(text)
1952         self._stream_write(s)
1953         i = s.rfind('\n')
1954         if i < 0:
1955             self.col = self.col + len(s)
1956         else:
1957             self.col = len(s) - (i + 1)
1958     bytecode_handlers["insertText"] = do_insertText
1959     
1960     def do_insertRawText_tal(self, stuff):
1961         text = self.engine.evaluateText(stuff[0])
1962         if text is None:
1963             return
1964         if text is self.Default:
1965             self.interpret(stuff[1])
1966             return
1967         if isinstance(text, MessageID):
1968             text = self.engine.translate(text.domain, text, text.mapping)
1969         s = text
1970         self._stream_write(s)
1971         i = s.rfind('\n')
1972         if i < 0:
1973             self.col = self.col + len(s)
1974         else:
1975             self.col = len(s) - (i + 1)
1976
1977     def do_i18nVariable(self, stuff):
1978         varname, program, expression, structure = stuff
1979         if expression is None:
1980             state = self.saveState()
1981             try:
1982                 tmpstream = self.StringIO()
1983                 self.interpretWithStream(program, tmpstream)
1984                 if self.html and self._currentTag == "pre":
1985                     value = tmpstream.getvalue()
1986                 else:
1987                     value = normalize(tmpstream.getvalue())
1988             finally:
1989                 self.restoreState(state)
1990         else:
1991             if structure:
1992                 value = self.engine.evaluateStructure(expression)
1993             else:
1994                 value = self.engine.evaluate(expression)
1995
1996             if isinstance(value, MessageID):
1997                 value = self.engine.translate(value.domain, value,
1998                                               value.mapping)
1999
2000             if not structure:
2001                 value = cgi.escape(ustr(value))
2002
2003         i18ndict, srepr = self.i18nStack[-1]
2004         i18ndict[varname] = value
2005         placeholder = '${%s}' % varname
2006         srepr.append(placeholder)
2007         self._stream_write(placeholder)
2008     bytecode_handlers['i18nVariable'] = do_i18nVariable
2009
2010     def do_insertTranslation(self, stuff):
2011         i18ndict = {}
2012         srepr = []
2013         obj = None
2014         self.i18nStack.append((i18ndict, srepr))
2015         msgid = stuff[0]
2016         currentTag = self._currentTag
2017         tmpstream = self.StringIO()
2018         self.interpretWithStream(stuff[1], tmpstream)
2019         default = tmpstream.getvalue()
2020         if not msgid:
2021             if self.html and currentTag == "pre":
2022                 msgid = default
2023             else:
2024                 msgid = normalize(default)
2025         self.i18nStack.pop()
2026         if len(stuff) > 2:
2027             obj = self.engine.evaluate(stuff[2])
2028         xlated_msgid = self.translate(msgid, default, i18ndict, obj)
2029         assert xlated_msgid is not None
2030         self._stream_write(xlated_msgid)
2031     bytecode_handlers['insertTranslation'] = do_insertTranslation
2032
2033     def do_insertStructure(self, stuff):
2034         self.interpret(stuff[2])
2035
2036     def do_insertStructure_tal(self, (expr, repldict, block)):
2037         structure = self.engine.evaluateStructure(expr)
2038         if structure is None:
2039             return
2040         if structure is self.Default:
2041             self.interpret(block)
2042             return
2043         text = ustr(structure)
2044         if not (repldict or self.strictinsert):
2045             self.stream_write(text)
2046             return
2047         if self.html:
2048             self.insertHTMLStructure(text, repldict)
2049         else:
2050             self.insertXMLStructure(text, repldict)
2051     bytecode_handlers["insertStructure"] = do_insertStructure
2052
2053     def insertHTMLStructure(self, text, repldict):
2054         gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
2055         p = HTMLTALParser(gen) # Raises an exception if text is invalid
2056         p.parseString(text)
2057         program, macros = p.getCode()
2058         self.interpret(program)
2059
2060     def insertXMLStructure(self, text, repldict):
2061         gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
2062         p = TALParser(gen)
2063         gen.enable(0)
2064         p.parseFragment('<!DOCTYPE foo PUBLIC "foo" "bar"><foo>')
2065         gen.enable(1)
2066         p.parseFragment(text) # Raises an exception if text is invalid
2067         gen.enable(0)
2068         p.parseFragment('</foo>', 1)
2069         program, macros = gen.getCode()
2070         self.interpret(program)
2071
2072     def do_loop(self, (name, expr, block)):
2073         self.interpret(block)
2074
2075     def do_loop_tal(self, (name, expr, block)):
2076         iterator = self.engine.setRepeat(name, expr)
2077         while iterator.next():
2078             self.interpret(block)
2079     bytecode_handlers["loop"] = do_loop
2080
2081     def translate(self, msgid, default, i18ndict, obj=None):
2082         if obj:
2083             i18ndict.update(obj)
2084         if not self.i18nInterpolate:
2085             return msgid
2086         return self.engine.translate(self.i18nContext.domain,
2087                                      msgid, i18ndict, default=default)
2088
2089     def do_rawtextColumn(self, (s, col)):
2090         self._stream_write(s)
2091         self.col = col
2092     bytecode_handlers["rawtextColumn"] = do_rawtextColumn
2093
2094     def do_rawtextOffset(self, (s, offset)):
2095         self._stream_write(s)
2096         self.col = self.col + offset
2097     bytecode_handlers["rawtextOffset"] = do_rawtextOffset
2098
2099     def do_condition(self, (condition, block)):
2100         if not self.tal or self.engine.evaluateBoolean(condition):
2101             self.interpret(block)
2102     bytecode_handlers["condition"] = do_condition
2103
2104     def do_defineMacro(self, (macroName, macro)):
2105         macs = self.macroStack
2106         if len(macs) == 1:
2107             entering = macs[-1][2]
2108             if not entering:
2109                 macs.append(None)
2110                 self.interpret(macro)
2111                 assert macs[-1] is None
2112                 macs.pop()
2113                 return
2114         self.interpret(macro)
2115     bytecode_handlers["defineMacro"] = do_defineMacro
2116
2117     def do_useMacro(self, (macroName, macroExpr, compiledSlots, block)):
2118         if not self.metal:
2119             self.interpret(block)
2120             return
2121         macro = self.engine.evaluateMacro(macroExpr)
2122         if macro is self.Default:
2123             macro = block
2124         else:
2125             if not isCurrentVersion(macro):
2126                 raise METALError("macro %s has incompatible version %s" %
2127                                  (`macroName`, `getProgramVersion(macro)`),
2128                                  self.position)
2129             mode = getProgramMode(macro)
2130             #if mode != (self.html and "html" or "xml"):
2131             #    raise METALError("macro %s has incompatible mode %s" %
2132             #                     (`macroName`, `mode`), self.position)
2133
2134         self.pushMacro(macroName, compiledSlots)
2135         prev_source = self.sourceFile
2136         self.interpret(macro)
2137         if self.sourceFile != prev_source:
2138             self.engine.setSourceFile(prev_source)
2139             self.sourceFile = prev_source
2140         self.popMacro()
2141     bytecode_handlers["useMacro"] = do_useMacro
2142
2143     def do_fillSlot(self, (slotName, block)):
2144         self.interpret(block)
2145     bytecode_handlers["fillSlot"] = do_fillSlot
2146
2147     def do_defineSlot(self, (slotName, block)):
2148         if not self.metal:
2149             self.interpret(block)
2150             return
2151         macs = self.macroStack
2152         if macs and macs[-1] is not None:
2153             macroName, slots = self.popMacro()[:2]
2154             slot = slots.get(slotName)
2155             if slot is not None:
2156                 prev_source = self.sourceFile
2157                 self.interpret(slot)
2158                 if self.sourceFile != prev_source:
2159                     self.engine.setSourceFile(prev_source)
2160                     self.sourceFile = prev_source
2161                 self.pushMacro(macroName, slots, entering=0)
2162                 return
2163             self.pushMacro(macroName, slots)
2164         self.interpret(block)
2165     bytecode_handlers["defineSlot"] = do_defineSlot
2166
2167     def do_onError(self, (block, handler)):
2168         self.interpret(block)
2169
2170     def do_onError_tal(self, (block, handler)):
2171         state = self.saveState()
2172         self.stream = stream = self.StringIO()
2173         self._stream_write = stream.write
2174         try:
2175             self.interpret(block)
2176         except ConflictError:
2177             raise
2178         except:
2179             exc = sys.exc_info()[1]
2180             self.restoreState(state)
2181             engine = self.engine
2182             engine.beginScope()
2183             error = engine.createErrorInfo(exc, self.position)
2184             engine.setLocal('error', error)
2185             try:
2186                 self.interpret(handler)
2187             finally:
2188                 engine.endScope()
2189         else:
2190             self.restoreOutputState(state)
2191             self.stream_write(stream.getvalue())
2192     bytecode_handlers["onError"] = do_onError
2193
2194     bytecode_handlers_tal = bytecode_handlers.copy()
2195     bytecode_handlers_tal["rawtextBeginScope"] = do_rawtextBeginScope_tal
2196     bytecode_handlers_tal["beginScope"] = do_beginScope_tal
2197     bytecode_handlers_tal["setLocal"] = do_setLocal_tal
2198     bytecode_handlers_tal["setGlobal"] = do_setGlobal_tal
2199     bytecode_handlers_tal["insertStructure"] = do_insertStructure_tal
2200     bytecode_handlers_tal["insertText"] = do_insertText_tal
2201     bytecode_handlers_tal["insertRaw"] = do_insertRawText_tal
2202     bytecode_handlers_tal["loop"] = do_loop_tal
2203     bytecode_handlers_tal["onError"] = do_onError_tal
2204     bytecode_handlers_tal["<attrAction>"] = attrAction_tal
2205     bytecode_handlers_tal["optTag"] = do_optTag_tal
2206
2207
2208 class FasterStringIO(StringIO):
2209     """Append-only version of StringIO.
2210
2211     This let's us have a much faster write() method.
2212     """
2213     def close(self):
2214         if not self.closed:
2215             self.write = _write_ValueError
2216             StringIO.close(self)
2217
2218     def seek(self, pos, mode=0):
2219         raise RuntimeError("FasterStringIO.seek() not allowed")
2220
2221     def write(self, s):
2222         self.buflist.append(s)
2223         self.len = self.pos = self.pos + len(s)
2224
2225
2226 def _write_ValueError(s):
2227     raise ValueError, "I/O operation on closed file"
2228 """
2229 Parse XML and compile to TALInterpreter intermediate code.
2230 """
2231
2232
2233 class TALParser(XMLParser):
2234
2235     ordered_attributes = 1
2236
2237     def __init__(self, gen=None): # Override
2238         XMLParser.__init__(self)
2239         if gen is None:
2240             gen = TALGenerator()
2241         self.gen = gen
2242         self.nsStack = []
2243         self.nsDict = {XML_NS: 'xml'}
2244         self.nsNew = []
2245
2246     def getCode(self):
2247         return self.gen.getCode()
2248
2249     def getWarnings(self):
2250         return ()
2251
2252     def StartNamespaceDeclHandler(self, prefix, uri):
2253         self.nsStack.append(self.nsDict.copy())
2254         self.nsDict[uri] = prefix
2255         self.nsNew.append((prefix, uri))
2256
2257     def EndNamespaceDeclHandler(self, prefix):
2258         self.nsDict = self.nsStack.pop()
2259
2260     def StartElementHandler(self, name, attrs):
2261         if self.ordered_attributes:
2262             attrlist = []
2263             for i in range(0, len(attrs), 2):
2264                 key = attrs[i]
2265                 value = attrs[i+1]
2266                 attrlist.append((key, value))
2267         else:
2268             attrlist = attrs.items()
2269             attrlist.sort() # For definiteness
2270         name, attrlist, taldict, metaldict, i18ndict \
2271               = self.process_ns(name, attrlist)
2272         attrlist = self.xmlnsattrs() + attrlist
2273         self.gen.emitStartElement(name, attrlist, taldict, metaldict, i18ndict)
2274
2275     def process_ns(self, name, attrlist):
2276         taldict = {}
2277         metaldict = {}
2278         i18ndict = {}
2279         fixedattrlist = []
2280         name, namebase, namens = self.fixname(name)
2281         for key, value in attrlist:
2282             key, keybase, keyns = self.fixname(key)
2283             ns = keyns or namens # default to tag namespace
2284             item = key, value
2285             if ns == 'metal':
2286                 metaldict[keybase] = value
2287                 item = item + ("metal",)
2288             elif ns == 'tal':
2289                 taldict[keybase] = value
2290                 item = item + ("tal",)
2291             elif ns == 'i18n':
2292                 i18ndict[keybase] = value
2293                 item = item + ('i18n',)
2294             fixedattrlist.append(item)
2295         if namens in ('metal', 'tal', 'i18n'):
2296             taldict['tal tag'] = namens
2297         return name, fixedattrlist, taldict, metaldict, i18ndict
2298
2299     def xmlnsattrs(self):
2300         newlist = []
2301         for prefix, uri in self.nsNew:
2302             if prefix:
2303                 key = "xmlns:" + prefix
2304             else:
2305                 key = "xmlns"
2306             if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS):
2307                 item = (key, uri, "xmlns")
2308             else:
2309                 item = (key, uri)
2310             newlist.append(item)
2311         self.nsNew = []
2312         return newlist
2313
2314     def fixname(self, name):
2315         if ' ' in name:
2316             uri, name = name.split(' ')
2317             prefix = self.nsDict[uri]
2318             prefixed = name
2319             if prefix:
2320                 prefixed = "%s:%s" % (prefix, name)
2321             ns = 'x'
2322             if uri == ZOPE_TAL_NS:
2323                 ns = 'tal'
2324             elif uri == ZOPE_METAL_NS:
2325                 ns = 'metal'
2326             elif uri == ZOPE_I18N_NS:
2327                 ns = 'i18n'
2328             return (prefixed, name, ns)
2329         return (name, name, None)
2330
2331     def EndElementHandler(self, name):
2332         name = self.fixname(name)[0]
2333         self.gen.emitEndElement(name)
2334
2335     def DefaultHandler(self, text):
2336         self.gen.emitRawText(text)
2337
2338 """Translation context object for the TALInterpreter's I18N support.
2339
2340 The translation context provides a container for the information
2341 needed to perform translation of a marked string from a page template.
2342
2343 """
2344
2345 DEFAULT_DOMAIN = "default"
2346
2347 class TranslationContext:
2348     """Information about the I18N settings of a TAL processor."""
2349
2350     def __init__(self, parent=None, domain=None, target=None, source=None):
2351         if parent:
2352             if not domain:
2353                 domain = parent.domain
2354             if not target:
2355                 target = parent.target
2356             if not source:
2357                 source = parent.source
2358         elif domain is None:
2359             domain = DEFAULT_DOMAIN
2360
2361         self.parent = parent
2362         self.domain = domain
2363         self.target = target
2364         self.source = source
2365 """
2366 Dummy TALES engine so that I can test out the TAL implementation.
2367 """
2368
2369 import re
2370 import sys
2371 import stat
2372 import os
2373 import traceback
2374
2375 class _Default:
2376     pass
2377 Default = _Default()
2378
2379 name_match = re.compile(r"(?s)(%s):(.*)\Z" % NAME_RE).match
2380
2381 class CompilerError(Exception):
2382     pass
2383
2384 class AthanaTALEngine:
2385
2386     position = None
2387     source_file = None
2388
2389     __implements__ = ITALESCompiler, ITALESEngine
2390
2391     def __init__(self, macros=None, context=None, webcontext=None, language=None, request=None):
2392         if macros is None:
2393             macros = {}
2394         self.macros = macros
2395         dict = {'nothing': None, 'default': Default}
2396         if context is not None:
2397             dict.update(context)
2398
2399         self.locals = self.globals = dict
2400         self.stack = [dict]
2401         self.webcontext = webcontext
2402         self.language = language
2403         self.request = request
2404
2405     def compilefile(self, file, mode=None):
2406         assert mode in ("html", "xml", None)
2407         #file =  join_paths(GLOBAL_ROOT_DIR,join_paths(self.webcontext.root, file))
2408         if mode is None:
2409             ext = os.path.splitext(file)[1]
2410             if ext.lower() in (".html", ".htm"):
2411                 mode = "html"
2412             else:
2413                 mode = "xml"
2414         if mode == "html":
2415             p = HTMLTALParser(TALGenerator(self))
2416         else:
2417             p = TALParser(TALGenerator(self))
2418         p.parseFile(file)
2419         return p.getCode()
2420
2421     def getCompilerError(self):
2422         return CompilerError
2423
2424     def getCompiler(self):
2425         return self
2426
2427     def setSourceFile(self, source_file):
2428         self.source_file = source_file
2429
2430     def setPosition(self, position):
2431         self.position = position
2432
2433     def compile(self, expr):
2434         return "$%s$" % expr
2435
2436     def uncompile(self, expression):
2437         assert (expression.startswith("$") and expression.endswith("$"),
2438             expression)
2439         return expression[1:-1]
2440
2441     def beginScope(self):
2442         self.stack.append(self.locals)
2443
2444     def endScope(self):
2445         assert len(self.stack) > 1, "more endScope() than beginScope() calls"
2446         self.locals = self.stack.pop()
2447
2448     def setLocal(self, name, value):
2449         if self.locals is self.stack[-1]:
2450             self.locals = self.locals.copy()
2451         self.locals[name] = value
2452
2453     def setGlobal(self, name, value):
2454         self.globals[name] = value
2455
2456     def evaluate(self, expression):
2457         assert (expression.startswith("$") and expression.endswith("$"),
2458             expression)
2459         expression = expression[1:-1]
2460         m = name_match(expression)
2461         if m:
2462             type, expr = m.group(1, 2)
2463         else:
2464             type = "path"
2465             expr = expression
2466         if type in ("string", "str"):
2467             return expr
2468         if type in ("path", "var", "global", "local"):
2469             return self.evaluatePathOrVar(expr)
2470         if type == "not":
2471             return not self.evaluate(expr)
2472         if type == "exists":
2473             return self.locals.has_key(expr) or self.globals.has_key(expr)
2474         if type == "python":
2475             try:
2476                 return eval(expr, self.globals, self.locals)
2477             except:
2478                 print "Error in python expression"
2479                 print sys.exc_info()[0], sys.exc_info()[1]
2480                 traceback.print_tb(sys.exc_info()[2])
2481                 raise TALESError("evaluation error in %s" % `expr`)
2482
2483         if type == "position":
2484             if self.position:
2485                 lineno, offset = self.position
2486             else:
2487                 lineno, offset = None, None
2488             return '%s (%s,%s)' % (self.source_file, lineno, offset)
2489         raise TALESError("unrecognized expression: " + `expression`)
2490
2491     def evaluatePathOrVar(self, expr):
2492         expr = expr.strip()
2493         _expr=expr
2494         _f=None
2495         if expr.rfind("/")>0:
2496             pos=expr.rfind("/")
2497             _expr = expr[0:pos]
2498             _f = expr[pos+1:]
2499         if self.locals.has_key(_expr):
2500             if _f:
2501                 return getattr(self.locals[_expr],_f)
2502             else:
2503                 return self.locals[_expr]
2504         elif self.globals.has_key(_expr):
2505             if _f:
2506                 return getattr(self.globals[_expr], _f)
2507             else:
2508                 return self.globals[_expr]
2509         else:
2510             raise TALESError("unknown variable: %s" % `_expr`)
2511
2512     def evaluateValue(self, expr):
2513         return self.evaluate(expr)
2514
2515     def evaluateBoolean(self, expr):
2516         return self.evaluate(expr)
2517
2518     def evaluateText(self, expr):
2519         text = self.evaluate(expr)
2520         if text is not None and text is not Default:
2521             text = ustr(text)
2522         return text
2523
2524     def evaluateStructure(self, expr):
2525         return self.evaluate(expr)
2526
2527     def evaluateSequence(self, expr):
2528         return self.evaluate(expr)
2529
2530     def evaluateMacro(self, macroName):
2531         assert (macroName.startswith("$") and macroName.endswith("$"),
2532             macroName)
2533         macroName = macroName[1:-1]
2534         file, localName = self.findMacroFile(macroName)
2535         if not file:
2536             macro = self.macros[localName]
2537         else:
2538             program, macros = self.compilefile(file)
2539             macro = macros.get(localName)
2540             if not macro:
2541                 raise TALESError("macro %s not found in file %s" %
2542                                  (localName, file))
2543         return macro
2544
2545     def findMacroDocument(self, macroName):
2546         file, localName = self.findMacroFile(macroName)
2547         if not file:
2548             return file, localName
2549         doc = parsefile(file)
2550         return doc, localName
2551
2552     def findMacroFile(self, macroName):
2553         if not macroName:
2554             raise TALESError("empty macro name")
2555         i = macroName.rfind('/')
2556         if i < 0:
2557             print "NO Macro"
2558             return None, macroName
2559         else:
2560             fileName = getMacroFile(macroName[:i])
2561             localName = macroName[i+1:]
2562             return fileName, localName
2563
2564     def setRepeat(self, name, expr):
2565         seq = self.evaluateSequence(expr)
2566         self.locals[name] = Iterator(name, seq, self)
2567         return self.locals[name]
2568
2569     def createErrorInfo(self, err, position):
2570         return ErrorInfo(err, position)
2571
2572     def getDefault(self):
2573         return Default
2574
2575     def translate(self, domain, msgid, mapping, default=None):
2576         global translators
2577         text = default or msgid
2578         for f in translators:
2579             text = f(msgid, language=self.language, request=self.request)
2580             try:
2581                 text = f(msgid, language=self.language, request=self.request)
2582                 if text and text!=msgid:
2583                     break
2584             except: 
2585                 pass
2586         def repl(m, mapping=mapping):
2587             return ustr(mapping[m.group(m.lastindex).lower()])
2588         return VARIABLE.sub(repl, text)
2589
2590
2591 class Iterator:
2592    
2593     def __init__(self, name, seq, engine):
2594         self.name = name
2595         self.seq = seq
2596         self.engine = engine
2597         self.nextIndex = 0
2598
2599     def next(self):
2600         self.index = i = self.nextIndex
2601         try:
2602             item = self.seq[i]
2603         except IndexError:
2604             return 0
2605         self.nextIndex = i+1
2606         self.engine.setLocal(self.name, item)
2607         return 1
2608
2609     def even(self):
2610         print "-even-"
2611         return not self.index % 2
2612
2613     def odd(self):
2614         print "-odd-"
2615         return self.index % 2
2616
2617     def number(self):
2618         return self.nextIndex
2619
2620     def parity(self):
2621         if self.index % 2:
2622             return 'odd'
2623         return 'even'
2624
2625     def first(self, name=None):
2626         if self.start: return 1
2627         return not self.same_part(name, self._last, self.item)
2628
2629     def last(self, name=None):
2630         if self.end: return 1
2631         return not self.same_part(name, self.item, self._next)
2632
2633     def length(self):
2634         return len(self.seq)
2635     
2636
2637 VARIABLE = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
2638
2639 parsed_files = {}
2640 parsed_strings = {}
2641
2642 def runTAL(writer, context=None, string=None, file=None, macro=None, language=None, request=None):
2643
2644     if file:
2645         file = getMacroFile(file)
2646
2647     if context is None:
2648         context = {}
2649
2650     if string and not file:
2651         if string in parsed_strings:
2652             program,macros = parsed_strings[string]
2653         else:
2654             program,macros = None,None
2655     elif file and not string:
2656         if file in parsed_files:
2657             (program,macros,mtime) = parsed_files[file]
2658             mtime_file = os.stat(file)[stat.ST_MTIME]
2659             if mtime != mtime_file:
2660                 program,macros = None,None
2661                 mtime = mtime_file
2662         else:
2663             program,macros,mtime = None,None,None
2664    
2665     if not (program and macros):
2666         if file and file.endswith("xml"):
2667             talparser = TALParser(TALGenerator(AthanaTALEngine()))
2668         else:
2669             talparser = HTMLTALParser(TALGenerator(AthanaTALEngine()))
2670         if string:
2671             talparser.parseString(string)
2672             (program, macros) = talparser.getCode()
2673             parsed_strings[string] = (program,macros)
2674         else:
2675             talparser.parseFile(file)
2676             (program, macros) = talparser.getCode()
2677             parsed_files[file] = (program,macros,mtime)
2678
2679     if macro and macro in macros:
2680         program = macros[macro]
2681     engine = AthanaTALEngine(macros, context, language=language, request=request)
2682     TALInterpreter(program, macros, engine, writer, wrap=0)()
2683
2684 def processTAL(context=None, string=None, file=None, macro=None, language=None, request=None):
2685     class STRWriter:
2686         def __init__(self):
2687             self.string = ""
2688         def write(self,text):
2689             if type(text) == type(u''):
2690                 self.string += text.encode("utf-8")
2691             else:
2692                 self.string += text
2693         def getvalue(self):
2694             return self.string
2695     wr = STRWriter()
2696     runTAL(wr, context, string=string, file=file, macro=macro, language=language, request=request)
2697     return wr.getvalue()
2698
2699
2700 class MyWriter:
2701     def write(self,s):
2702         sys.stdout.write(s)
2703
2704 def test():
2705     p = TALParser(TALGenerator(AthanaTALEngine()))
2706     file = "test.xml"
2707     if sys.argv[1:]:
2708         file = sys.argv[1]
2709     p.parseFile(file)
2710     program, macros = p.getCode()
2711
2712     class Node:
2713         def getText(self):
2714             return "TEST"
2715
2716     engine = AthanaTALEngine(macros, {'node': Node()})
2717     TALInterpreter(program, macros, engine, MyWriter(), wrap=0)()
2718
2719
2720 def ustr(v):
2721     """Convert any object to a plain string or unicode string,
2722     minimising the chance of raising a UnicodeError. This
2723     even works with uncooperative objects like Exceptions
2724     """
2725     if type(v) == type(""): #isinstance(v, basestring):
2726         return v
2727     else:
2728         fn = getattr(v,'__str__',None)
2729         if fn is not None:
2730             v = fn()
2731             if isinstance(v, basestring):
2732                 return v
2733             else:
2734                 raise ValueError('__str__ returned wrong type')
2735         return str(v)
2736
2737
2738 # ================ MEDUSA ===============
2739
2740 # python modules
2741 import os
2742 import re
2743 import select
2744 import socket
2745 import string
2746 import sys
2747 import time
2748 import stat
2749 import string
2750 import mimetypes
2751 import glob
2752 from cgi import escape
2753 from urllib import unquote, splitquery
2754
2755 # async modules
2756 import asyncore
2757 import socket
2758
2759 class async_chat (asyncore.dispatcher):
2760     """This is an abstract class.  You must derive from this class, and add
2761     the two methods collect_incoming_data() and found_terminator()"""
2762
2763     # these are overridable defaults
2764
2765     ac_in_buffer_size       = 4096
2766     ac_out_buffer_size      = 4096
2767
2768     def __init__ (self, conn=None):
2769         self.ac_in_buffer = ''
2770         self.ac_out_buffer = ''
2771         self.producer_fifo = fifo()
2772         asyncore.dispatcher.__init__ (self, conn)
2773
2774     def collect_incoming_data(self, data):
2775         raise NotImplementedError, "must be implemented in subclass"
2776
2777     def found_terminator(self):
2778         raise NotImplementedError, "must be implemented in subclass"
2779
2780     def set_terminator (self, term):
2781         "Set the input delimiter.  Can be a fixed string of any length, an integer, or None"
2782         self.terminator = term
2783
2784     def get_terminator (self):
2785         return self.terminator
2786
2787     # grab some more data from the socket,
2788     # throw it to the collector method,
2789     # check for the terminator,
2790     # if found, transition to the next state.
2791
2792     def handle_read (self):
2793
2794         try:
2795             data = self.recv (self.ac_in_buffer_size)
2796         except socket.error, why:
2797             self.handle_error()
2798             return
2799
2800         self.ac_in_buffer = self.ac_in_buffer + data
2801
2802         # Continue to search for self.terminator in self.ac_in_buffer,
2803         # while calling self.collect_incoming_data.  The while loop
2804         # is necessary because we might read several data+terminator
2805         # combos with a single recv(1024).
2806
2807         while self.ac_in_buffer:
2808             lb = len(self.ac_in_buffer)
2809             terminator = self.get_terminator()
2810             if terminator is None or terminator == '':
2811                 # no terminator, collect it all
2812                 self.collect_incoming_data (self.ac_in_buffer)
2813                 self.ac_in_buffer = ''
2814             elif isinstance(terminator, int):
2815                 # numeric terminator
2816                 n = terminator
2817                 if lb < n:
2818                     self.collect_incoming_data (self.ac_in_buffer)
2819                     self.ac_in_buffer = ''
2820                     self.terminator = self.terminator - lb
2821                 else:
2822                     self.collect_incoming_data (self.ac_in_buffer[:n])
2823                     self.ac_in_buffer = self.ac_in_buffer[n:]
2824                     self.terminator = 0
2825                     self.found_terminator()
2826             else:
2827                 # 3 cases:
2828                 # 1) end of buffer matches terminator exactly:
2829                 #    collect data, transition
2830                 # 2) end of buffer matches some prefix:
2831                 #    collect data to the prefix
2832                 # 3) end of buffer does not match any prefix:
2833                 #    collect data
2834                 terminator_len = len(terminator)
2835                 index = self.ac_in_buffer.find(terminator)
2836                 if index != -1:
2837                     # we found the terminator
2838                     if index > 0:
2839                         # don't bother reporting the empty string (source of subtle bugs)
2840                         self.collect_incoming_data (self.ac_in_buffer[:index])
2841                     self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
2842                     # This does the Right Thing if the terminator is changed here.
2843                     self.found_terminator()
2844                 else:
2845                     # check for a prefix of the terminator
2846                     index = find_prefix_at_end (self.ac_in_buffer, terminator)
2847                     if index:
2848                         if index != lb:
2849                             # we found a prefix, collect up to the prefix
2850                             self.collect_incoming_data (self.ac_in_buffer[:-index])
2851                             self.ac_in_buffer = self.ac_in_buffer[-index:]
2852                         break
2853                     else:
2854                         # no prefix, collect it all
2855                         self.collect_incoming_data (self.ac_in_buffer)
2856                         self.ac_in_buffer = ''
2857
2858     def handle_write (self):
2859         self.initiate_send ()
2860
2861     def handle_close (self):
2862         self.close()
2863
2864     def push (self, data):
2865         self.producer_fifo.push (simple_producer (data))
2866         self.initiate_send()
2867
2868     def push_with_producer (self, producer):
2869         self.producer_fifo.push (producer)
2870         self.initiate_send()
2871
2872     def readable (self):
2873         "predicate for inclusion in the readable for select()"
2874         return (len(self.ac_in_buffer) <= self.ac_in_buffer_size)
2875
2876     def writable (self):
2877         "predicate for inclusion in the writable for select()"
2878         # return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected)
2879         # this is about twice as fast, though not as clear.
2880         return not (
2881                 (self.ac_out_buffer == '') and
2882                 self.producer_fifo.is_empty() and
2883                 self.connected
2884                 )
2885
2886     def close_when_done (self):
2887         "automatically close this channel once the outgoing queue is empty"
2888         self.producer_fifo.push (None)
2889
2890     # refill the outgoing buffer by calling the more() method
2891     # of the first producer in the queue
2892     def refill_buffer (self):
2893         while 1:
2894             if len(self.producer_fifo):
2895                 p = self.producer_fifo.first()
2896                 # a 'None' in the producer fifo is a sentinel,
2897                 # telling us to close the channel.
2898                 if p is None:
2899                     if not self.ac_out_buffer:
2900                         self.producer_fifo.pop()
2901                         self.close()
2902                     return
2903                 elif isinstance(p, str):
2904                     self.producer_fifo.pop()
2905                     self.ac_out_buffer = self.ac_out_buffer + p
2906                     return
2907                 data = p.more()
2908                 if data:
2909                     self.ac_out_buffer = self.ac_out_buffer + data
2910                     return
2911                 else:
2912                     self.producer_fifo.pop()
2913             else:
2914                 return
2915
2916     def initiate_send (self):
2917         obs = self.ac_out_buffer_size
2918         # try to refill the buffer
2919         if (len (self.ac_out_buffer) < obs):
2920             self.refill_buffer()
2921
2922         if self.ac_out_buffer and self.connected:
2923             # try to send the buffer
2924             try:
2925                 num_sent = self.send (self.ac_out_buffer[:obs])
2926                 if num_sent:
2927                     self.ac_out_buffer = self.ac_out_buffer[num_sent:]
2928
2929             except socket.error, why:
2930                 self.handle_error()
2931                 return
2932
2933     def discard_buffers (self):
2934         # Emergencies only!
2935         self.ac_in_buffer = ''
2936         self.ac_out_buffer = ''
2937         while self.producer_fifo:
2938             self.producer_fifo.pop()
2939
2940
2941 class simple_producer:
2942
2943     def __init__ (self, data, buffer_size=512):
2944         self.data = data
2945         self.buffer_size = buffer_size
2946
2947     def more (self):
2948         if len (self.data) > self.buffer_size:
2949             result = self.data[:self.buffer_size]
2950             self.data = self.data[self.buffer_size:]
2951             return result
2952         else:
2953             result = self.data
2954             self.data = ''
2955             return result
2956
2957 class fifo:
2958     def __init__ (self, list=None):
2959         if not list:
2960             self.list = []
2961         else:
2962             self.list = list
2963
2964     def __len__ (self):
2965         return len(self.list)
2966
2967     def is_empty (self):
2968         return self.list == []
2969
2970     def first (self):
2971         return self.list[0]
2972
2973     def push (self, data):
2974         self.list.append (data)
2975
2976     def pop (self):
2977         if self.list:
2978             return (1, self.list.pop(0))
2979         else:
2980             return (0, None)
2981
2982 # Given 'haystack', see if any prefix of 'needle' is at its end.  This
2983 # assumes an exact match has already been checked.  Return the number of
2984 # characters matched.
2985 # for example:
2986 # f_p_a_e ("qwerty\r", "\r\n") => 1
2987 # f_p_a_e ("qwertydkjf", "\r\n") => 0
2988 # f_p_a_e ("qwerty\r\n", "\r\n") => <undefined>
2989
2990 # this could maybe be made faster with a computed regex?
2991 # [answer: no; circa Python-2.0, Jan 2001]
2992 # new python:   28961/s
2993 # old python:   18307/s
2994 # re:        12820/s
2995 # regex:     14035/s
2996
2997 def find_prefix_at_end (haystack, needle):
2998     l = len(needle) - 1
2999     while l and not haystack.endswith(needle[:l]):
3000         l -= 1
3001     return l
3002
3003 class counter:
3004     "general-purpose counter"
3005
3006     def __init__ (self, initial_value=0):
3007         self.value = initial_value
3008
3009     def increment (self, delta=1):
3010         result = self.value
3011         try:
3012             self.value = self.value + delta
3013         except OverflowError:
3014             self.value = long(self.value) + delta
3015         return result
3016
3017     def decrement (self, delta=1):
3018         result = self.value
3019         try:
3020             self.value = self.value - delta
3021         except OverflowError:
3022             self.value = long(self.value) - delta
3023         return result
3024
3025     def as_long (self):
3026         return long(self.value)
3027
3028     def __nonzero__ (self):
3029         return self.value != 0
3030
3031     def __repr__ (self):
3032         return '<counter value=%s at %x>' % (self.value, id(self))
3033
3034     def __str__ (self):
3035         s = str(long(self.value))
3036         if s[-1:] == 'L':
3037             s = s[:-1]
3038         return s
3039
3040
3041 # http_date
3042 def concat (*args):
3043     return ''.join (args)
3044
3045 def join (seq, field=' '):
3046     return field.join (seq)
3047
3048 def group (s):
3049     return '(' + s + ')'
3050
3051 short_days = ['sun','mon','tue','wed','thu','fri','sat']
3052 long_days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday']
3053
3054 short_day_reg = group (join (short_days, '|'))
3055 long_day_reg = group (join (long_days, '|'))
3056
3057 daymap = {}
3058 for i in range(7):
3059     daymap[short_days[i]] = i
3060     daymap[long_days[i]] = i
3061
3062 hms_reg = join (3 * [group('[0-9][0-9]')], ':')
3063
3064 months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
3065
3066 monmap = {}
3067 for i in range(12):
3068     monmap[months[i]] = i+1
3069
3070 months_reg = group (join (months, '|'))
3071
3072 # From draft-ietf-http-v11-spec-07.txt/3.3.1
3073 #       Sun, 06 Nov 1994 08:49:37 GMT  ; RFC 822, updated by RFC 1123
3074 #       Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
3075 #       Sun Nov  6 08:49:37 1994       ; ANSI C's asctime() format
3076
3077 # rfc822 format
3078 rfc822_date = join (
3079         [concat (short_day_reg,','),    # day
3080          group('[0-9][0-9]?'),                  # date
3081          months_reg,                                    # month
3082          group('[0-9]+'),                               # year
3083          hms_reg,                                               # hour minute second
3084          'gmt'
3085          ],
3086         ' '
3087         )
3088
3089 rfc822_reg = re.compile (rfc822_date)
3090
3091 def unpack_rfc822 (m):
3092     g = m.group
3093     a = string.atoi
3094     return (
3095             a(g(4)),                # year
3096             monmap[g(3)],   # month
3097             a(g(2)),                # day
3098             a(g(5)),                # hour
3099             a(g(6)),                # minute
3100             a(g(7)),                # second
3101             0,
3102             0,
3103             0
3104             )
3105
3106 # rfc850 format
3107 rfc850_date = join (
3108         [concat (long_day_reg,','),
3109          join (
3110                  [group ('[0-9][0-9]?'),
3111                   months_reg,
3112                   group ('[0-9]+')
3113                   ],
3114                  '-'
3115                  ),
3116          hms_reg,
3117          'gmt'
3118          ],
3119         ' '
3120         )
3121
3122 rfc850_reg = re.compile (rfc850_date)
3123 # they actually unpack the same way
3124 def unpack_rfc850 (m):
3125     g = m.group
3126     a = string.atoi
3127     return (
3128             a(g(4)),                # year
3129             monmap[g(3)],   # month
3130             a(g(2)),                # day
3131             a(g(5)),                # hour
3132             a(g(6)),                # minute
3133             a(g(7)),                # second
3134             0,
3135             0,
3136             0
3137             )
3138
3139 # parsdate.parsedate    - ~700/sec.
3140 # parse_http_date       - ~1333/sec.
3141
3142 def build_http_date (when):
3143     return time.strftime ('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(when))
3144
3145 time_offset = 0
3146
3147 def parse_http_date (d):
3148     global time_offset
3149     d = string.lower (d)
3150     tz = time.timezone
3151     m = rfc850_reg.match (d)
3152     if m and m.end() == len(d):
3153         retval = int (time.mktime (unpack_rfc850(m)) - tz)
3154     else:
3155         m = rfc822_reg.match (d)
3156         if m and m.end() == len(d):
3157             try:
3158                 retval = int (time.mktime (unpack_rfc822(m)) - tz)
3159             except OverflowError:
3160                 return 0
3161         else:
3162             return 0
3163     # Thanks to Craig Silverstein <csilvers@google.com> for pointing
3164     # out the DST discrepancy
3165     if time.daylight and time.localtime(retval)[-1] == 1: # DST correction
3166         retval = retval + (tz - time.altzone)
3167     return retval - time_offset
3168
3169 def check_date():
3170     global time_offset
3171     tmpfile = join_paths(GLOBAL_TEMP_DIR, "datetest"+str(random.random())+".tmp")
3172     open(tmpfile,"wb").close()
3173     time1 = os.stat(tmpfile)[stat.ST_MTIME]
3174     os.unlink(tmpfile)
3175     time2 = parse_http_date(build_http_date(time.time()))
3176     time_offset = time2-time1
3177     print time_offset
3178
3179 # producers
3180
3181 class simple_producer:
3182     "producer for a string"
3183     def __init__ (self, data, buffer_size=1024):
3184         self.data = data
3185         self.buffer_size = buffer_size
3186
3187     def more (self):
3188         if len (self.data) > self.buffer_size:
3189             result = self.data[:self.buffer_size]
3190             self.data = self.data[self.buffer_size:]
3191             return result
3192         else:
3193             result = self.data
3194             self.data = ''
3195             return result
3196
3197 class file_producer:
3198     "producer wrapper for file[-like] objects"
3199
3200     # match http_channel's outgoing buffer size
3201     out_buffer_size = 1<<16
3202
3203     def __init__ (self, file):
3204         self.done = 0
3205         self.file = file
3206
3207     def more (self):
3208         if self.done:
3209             return ''
3210         else:
3211             data = self.file.read (self.out_buffer_size)
3212             if not data:
3213                 self.file.close()
3214                 del self.file
3215                 self.done = 1
3216                 return ''
3217             else:
3218                 return data
3219
3220 # A simple output producer.  This one does not [yet] have
3221 # the safety feature builtin to the monitor channel:  runaway
3222 # output will not be caught.
3223
3224 # don't try to print from within any of the methods
3225 # of this object.
3226
3227 class output_producer:
3228     "Acts like an output file; suitable for capturing sys.stdout"
3229     def __init__ (self):
3230         self.data = ''
3231
3232     def write (self, data):
3233         lines = string.splitfields (data, '\n')
3234         data = string.join (lines, '\r\n')
3235         self.data = self.data + data
3236
3237     def writeline (self, line):
3238         self.data = self.data + line + '\r\n'
3239
3240     def writelines (self, lines):
3241         self.data = self.data + string.joinfields (
3242                 lines,
3243                 '\r\n'
3244                 ) + '\r\n'
3245
3246     def flush (self):
3247         pass
3248
3249     def softspace (self, *args):
3250         pass
3251
3252     def more (self):
3253         if self.data:
3254             result = self.data[:512]
3255             self.data = self.data[512:]
3256             return result
3257         else:
3258             return ''
3259
3260 class composite_producer:
3261     "combine a fifo of producers into one"
3262     def __init__ (self, producers):
3263         self.producers = producers
3264
3265     def more (self):
3266         while len(self.producers):
3267             p = self.producers[0]
3268             d = p.more()
3269             if d:
3270                 return d
3271             else:
3272                 self.producers.pop(0)
3273         else:
3274             return ''
3275
3276
3277 class globbing_producer:
3278     """
3279     'glob' the output from a producer into a particular buffer size.
3280     helps reduce the number of calls to send().  [this appears to
3281     gain about 30% performance on requests to a single channel]
3282     """
3283
3284     def __init__ (self, producer, buffer_size=1<<16):
3285         self.producer = producer
3286         self.buffer = ''
3287         self.buffer_size = buffer_size
3288
3289     def more (self):
3290         while len(self.buffer) < self.buffer_size:
3291             data = self.producer.more()
3292             if data:
3293                 self.buffer = self.buffer + data
3294             else:
3295                 break
3296         r = self.buffer
3297         self.buffer = ''
3298         return r
3299
3300
3301 class hooked_producer:
3302     """
3303     A producer that will call <function> when it empties,.
3304     with an argument of the number of bytes produced.  Useful
3305     for logging/instrumentation purposes.
3306     """
3307
3308     def __init__ (self, producer, function):
3309         self.producer = producer
3310         self.function = function
3311         self.bytes = 0
3312
3313     def more (self):
3314         if self.producer:
3315             result = self.producer.more()
3316             if not result:
3317                 self.producer = None
3318                 self.function (self.bytes)
3319             else:
3320                 self.bytes = self.bytes + len(result)
3321             return result
3322         else:
3323             return ''
3324
3325 # HTTP 1.1 emphasizes that an advertised Content-Length header MUST be
3326 # correct.  In the face of Strange Files, it is conceivable that
3327 # reading a 'file' may produce an amount of data not matching that
3328 # reported by os.stat() [text/binary mode issues, perhaps the file is
3329 # being appended to, etc..]  This makes the chunked encoding a True
3330 # Blessing, and it really ought to be used even with normal files.
3331 # How beautifully it blends with the concept of the producer.
3332
3333 class chunked_producer:
3334     """A producer that implements the 'chunked' transfer coding for HTTP/1.1.
3335     Here is a sample usage:
3336             request['Transfer-Encoding'] = 'chunked'
3337             request.push (
3338                     producers.chunked_producer (your_producer)
3339                     )
3340             request.done()
3341     """
3342
3343     def __init__ (self, producer, footers=None):
3344         self.producer = producer
3345         self.footers = footers
3346
3347     def more (self):
3348         if self.producer:
3349             data = self.producer.more()
3350             if data:
3351                 return '%x\r\n%s\r\n' % (len(data), data)
3352             else:
3353                 self.producer = None
3354                 if self.footers:
3355                     return string.join (
3356                             ['0'] + self.footers,
3357                             '\r\n'
3358                             ) + '\r\n\r\n'
3359                 else:
3360                     return '0\r\n\r\n'
3361         else:
3362             return ''
3363
3364 class escaping_producer:
3365
3366     "A producer that escapes a sequence of characters"
3367     " Common usage: escaping the CRLF.CRLF sequence in SMTP, NNTP, etc..."
3368
3369     def __init__ (self, producer, esc_from='\r\n.', esc_to='\r\n..'):
3370         self.producer = producer
3371         self.esc_from = esc_from
3372         self.esc_to = esc_to
3373         self.buffer = ''
3374         self.find_prefix_at_end = find_prefix_at_end
3375
3376     def more (self):
3377         esc_from = self.esc_from
3378         esc_to   = self.esc_to
3379
3380         buffer = self.buffer + self.producer.more()
3381
3382         if buffer:
3383             buffer = string.replace (buffer, esc_from, esc_to)
3384             i = self.find_prefix_at_end (buffer, esc_from)
3385             if i:
3386                 # we found a prefix
3387                 self.buffer = buffer[-i:]
3388                 return buffer[:-i]
3389             else:
3390                 # no prefix, return it all
3391                 self.buffer = ''
3392                 return buffer
3393         else:
3394             return buffer
3395
3396 class tail_logger:
3397     "Keep track of the last <size> log messages"
3398     def __init__ (self, logger, size=500):
3399         self.size = size
3400         self.logger = logger
3401         self.messages = []
3402
3403     def log (self, message):
3404         self.messages.append (strip_eol (message))
3405         if len (self.messages) > self.size:
3406             del self.messages[0]
3407         self.logger.log (message)
3408
3409
3410 def html_repr (object):
3411     so = escape (repr (object))
3412     if hasattr (object, 'hyper_respond'):
3413         return '<a href="/status/object/%d/">%s</a>' % (id (object), so)
3414     else:
3415         return so
3416
3417 def html_reprs (list, front='', back=''):
3418     reprs = map (
3419             lambda x,f=front,b=back: '%s%s%s' % (f,x,b),
3420             map (lambda x: escape (html_repr(x)), list)
3421             )
3422     reprs.sort()
3423     return reprs
3424
3425 # for example, tera, giga, mega, kilo
3426 # p_d (n, (1024, 1024, 1024, 1024))
3427 # smallest divider goes first - for example
3428 # minutes, hours, days
3429 # p_d (n, (60, 60, 24))
3430
3431 def progressive_divide (n, parts):
3432     result = []
3433     for part in parts:
3434         n, rem = divmod (n, part)
3435         result.append (rem)
3436     result.append (n)
3437     return result
3438
3439 # b,k,m,g,t
3440 def split_by_units (n, units, dividers, format_string):
3441     divs = progressive_divide (n, dividers)
3442     result = []
3443     for i in range(len(units)):
3444         if divs[i]:
3445             result.append (format_string % (divs[i], units[i]))
3446     result.reverse()
3447     if not result:
3448         return [format_string % (0, units[0])]
3449     else:
3450         return result
3451
3452 def english_bytes (n):
3453     return split_by_units (
3454             n,
3455             ('','K','M','G','T'),
3456             (1024, 1024, 1024, 1024, 1024),
3457             '%d %sB'
3458             )
3459
3460 def english_time (n):
3461     return split_by_units (
3462             n,
3463             ('secs', 'mins', 'hours', 'days', 'weeks', 'years'),
3464             (         60,     60,      24,     7,       52),
3465             '%d %s'
3466             )
3467
3468 class file_logger:
3469
3470     # pass this either a path or a file object.
3471     def __init__ (self, file, flush=1, mode='a'):
3472         if type(file) == type(''):
3473             if (file == '-'):
3474                 self.file = sys.stdout
3475             else:
3476                 self.file = open (file, mode)
3477         else:
3478             self.file = file
3479         self.do_flush = flush
3480
3481     def __repr__ (self):
3482         return '<file logger: %s>' % self.file
3483
3484     def write (self, data):
3485         self.file.write (data)
3486         self.maybe_flush()
3487
3488     def writeline (self, line):
3489         self.file.writeline (line)
3490         self.maybe_flush()
3491
3492     def writelines (self, lines):
3493         self.file.writelines (lines)
3494         self.maybe_flush()
3495
3496     def maybe_flush (self):
3497         if self.do_flush:
3498             self.file.flush()
3499
3500     def flush (self):
3501         self.file.flush()
3502
3503     def softspace (self, *args):
3504         pass
3505
3506     def log (self, message):
3507         if message[-1] not in ('\r', '\n'):
3508             self.write (message + '\n')
3509         else:
3510             self.write (message)
3511
3512     def debug(self, message):
3513         self.log(message)
3514
3515 class unresolving_logger:
3516     "Just in case you don't want to resolve"
3517     def __init__ (self, logger):
3518         self.logger = logger
3519
3520     def log (self, ip, message):
3521         self.logger.log ('%s:%s' % (ip, message))
3522
3523
3524 def strip_eol (line):
3525     while line and line[-1] in '\r\n':
3526         line = line[:-1]
3527     return line
3528
3529 VERSION_STRING = string.split(RCS_ID)[2]
3530 ATHANA_VERSION = "0.2.1"
3531
3532 # ===========================================================================
3533 #                                                       Request Object
3534 # ===========================================================================
3535
3536 class http_request:
3537
3538     # default reply code
3539     reply_code = 200
3540
3541     request_counter = counter()
3542
3543     # Whether to automatically use chunked encoding when
3544     #
3545     #   HTTP version is 1.1
3546     #   Content-Length is not set
3547     #   Chunked encoding is not already in effect
3548     #
3549     # If your clients are having trouble, you might want to disable this.
3550     use_chunked = 1
3551
3552     # by default, this request object ignores user data.
3553     collector = None
3554
3555     def __init__ (self, *args):
3556         # unpack information about the request
3557         (self.channel, self.request,
3558          self.command, self.uri, self.version,
3559          self.header) = args
3560
3561         self.outgoing = []
3562         self.reply_headers = {
3563                 'Server'        : 'Athana/%s' % ATHANA_VERSION,
3564                 'Date'          : build_http_date (time.time()),
3565                 'Expires'       : build_http_date (time.time())
3566                 }
3567         self.request_number = http_request.request_counter.increment()
3568         self._split_uri = None
3569         self._header_cache = {}
3570
3571     # --------------------------------------------------
3572     # reply header management
3573     # --------------------------------------------------
3574     def __setitem__ (self, key, value):
3575         try:
3576             if key=='Set-Cookie':
3577                 self.reply_headers[key] += [value]
3578             else:
3579                 self.reply_headers[key] = [value]