changeset 2348:8c2402a78bb0

beginning getting ZPT up to date: TAL first
author Richard Jones <richard@users.sourceforge.net>
date Fri, 21 May 2004 05:36:30 +0000
parents fbbda3b1816d
children b43efe461b3e
files roundup/cgi/TAL/DummyEngine.py roundup/cgi/TAL/HTMLParser.py roundup/cgi/TAL/HTMLTALParser.py roundup/cgi/TAL/TALDefs.py roundup/cgi/TAL/TALGenerator.py roundup/cgi/TAL/TALInterpreter.py roundup/cgi/TAL/TALParser.py roundup/cgi/TAL/TranslationContext.py roundup/cgi/TAL/XMLParser.py roundup/cgi/TAL/__init__.py roundup/cgi/TAL/markupbase.py roundup/cgi/TAL/talgettext.py
diffstat 12 files changed, 1385 insertions(+), 253 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/roundup/cgi/TAL/DummyEngine.py	Fri May 21 05:36:30 2004 +0000
@@ -0,0 +1,274 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. commented out ITALES references
+# 2. implemented ustr as str
+"""
+Dummy TALES engine so that I can test out the TAL implementation.
+"""
+
+import re
+import sys
+
+from TALDefs import NAME_RE, TALESError, ErrorInfo
+#from ITALES import ITALESCompiler, ITALESEngine
+#from DocumentTemplate.DT_Util import ustr
+ustr = str
+
+IDomain = None
+if sys.modules.has_key('Zope'):
+    try:
+        from Zope.I18n.ITranslationService import ITranslationService
+        from Zope.I18n.IDomain import IDomain
+    except ImportError:
+        pass
+if IDomain is None:
+    # Before 2.7, or not in Zope
+    class ITranslationService: pass
+    class IDomain: pass
+
+class _Default:
+    pass
+Default = _Default()
+
+name_match = re.compile(r"(?s)(%s):(.*)\Z" % NAME_RE).match
+
+class CompilerError(Exception):
+    pass
+
+class DummyEngine:
+
+    position = None
+    source_file = None
+
+    #__implements__ = ITALESCompiler, ITALESEngine
+
+    def __init__(self, macros=None):
+        if macros is None:
+            macros = {}
+        self.macros = macros
+        dict = {'nothing': None, 'default': Default}
+        self.locals = self.globals = dict
+        self.stack = [dict]
+        self.translationService = DummyTranslationService()
+
+    def getCompilerError(self):
+        return CompilerError
+
+    def getCompiler(self):
+        return self
+
+    def setSourceFile(self, source_file):
+        self.source_file = source_file
+
+    def setPosition(self, position):
+        self.position = position
+
+    def compile(self, expr):
+        return "$%s$" % expr
+
+    def uncompile(self, expression):
+        assert (expression.startswith("$") and expression.endswith("$"),
+            expression)
+        return expression[1:-1]
+
+    def beginScope(self):
+        self.stack.append(self.locals)
+
+    def endScope(self):
+        assert len(self.stack) > 1, "more endScope() than beginScope() calls"
+        self.locals = self.stack.pop()
+
+    def setLocal(self, name, value):
+        if self.locals is self.stack[-1]:
+            # Unmerge this scope's locals from previous scope of first set
+            self.locals = self.locals.copy()
+        self.locals[name] = value
+
+    def setGlobal(self, name, value):
+        self.globals[name] = value
+
+    def evaluate(self, expression):
+        assert (expression.startswith("$") and expression.endswith("$"),
+            expression)
+        expression = expression[1:-1]
+        m = name_match(expression)
+        if m:
+            type, expr = m.group(1, 2)
+        else:
+            type = "path"
+            expr = expression
+        if type in ("string", "str"):
+            return expr
+        if type in ("path", "var", "global", "local"):
+            return self.evaluatePathOrVar(expr)
+        if type == "not":
+            return not self.evaluate(expr)
+        if type == "exists":
+            return self.locals.has_key(expr) or self.globals.has_key(expr)
+        if type == "python":
+            try:
+                return eval(expr, self.globals, self.locals)
+            except:
+                raise TALESError("evaluation error in %s" % `expr`)
+        if type == "position":
+            # Insert the current source file name, line number,
+            # and column offset.
+            if self.position:
+                lineno, offset = self.position
+            else:
+                lineno, offset = None, None
+            return '%s (%s,%s)' % (self.source_file, lineno, offset)
+        raise TALESError("unrecognized expression: " + `expression`)
+
+    def evaluatePathOrVar(self, expr):
+        expr = expr.strip()
+        if self.locals.has_key(expr):
+            return self.locals[expr]
+        elif self.globals.has_key(expr):
+            return self.globals[expr]
+        else:
+            raise TALESError("unknown variable: %s" % `expr`)
+
+    def evaluateValue(self, expr):
+        return self.evaluate(expr)
+
+    def evaluateBoolean(self, expr):
+        return self.evaluate(expr)
+
+    def evaluateText(self, expr):
+        text = self.evaluate(expr)
+        if text is not None and text is not Default:
+            text = ustr(text)
+        return text
+
+    def evaluateStructure(self, expr):
+        # XXX Should return None or a DOM tree
+        return self.evaluate(expr)
+
+    def evaluateSequence(self, expr):
+        # XXX Should return a sequence
+        return self.evaluate(expr)
+
+    def evaluateMacro(self, macroName):
+        assert (macroName.startswith("$") and macroName.endswith("$"),
+            macroName)
+        macroName = macroName[1:-1]
+        file, localName = self.findMacroFile(macroName)
+        if not file:
+            # Local macro
+            macro = self.macros[localName]
+        else:
+            # External macro
+            import driver
+            program, macros = driver.compilefile(file)
+            macro = macros.get(localName)
+            if not macro:
+                raise TALESError("macro %s not found in file %s" %
+                                 (localName, file))
+        return macro
+
+    def findMacroDocument(self, macroName):
+        file, localName = self.findMacroFile(macroName)
+        if not file:
+            return file, localName
+        import driver
+        doc = driver.parsefile(file)
+        return doc, localName
+
+    def findMacroFile(self, macroName):
+        if not macroName:
+            raise TALESError("empty macro name")
+        i = macroName.rfind('/')
+        if i < 0:
+            # No slash -- must be a locally defined macro
+            return None, macroName
+        else:
+            # Up to last slash is the filename
+            fileName = macroName[:i]
+            localName = macroName[i+1:]
+            return fileName, localName
+
+    def setRepeat(self, name, expr):
+        seq = self.evaluateSequence(expr)
+        return Iterator(name, seq, self)
+
+    def createErrorInfo(self, err, position):
+        return ErrorInfo(err, position)
+
+    def getDefault(self):
+        return Default
+
+    def translate(self, domain, msgid, mapping, default=None):
+        return self.translationService.translate(domain, msgid, mapping,
+                                                 default=default)
+
+
+class Iterator:
+
+    # This is not an implementation of a Python iterator.  The next()
+    # method returns true or false to indicate whether another item is
+    # available; if there is another item, the iterator instance calls
+    # setLocal() on the evaluation engine passed to the constructor.
+
+    def __init__(self, name, seq, engine):
+        self.name = name
+        self.seq = seq
+        self.engine = engine
+        self.nextIndex = 0
+
+    def next(self):
+        i = self.nextIndex
+        try:
+            item = self.seq[i]
+        except IndexError:
+            return 0
+        self.nextIndex = i+1
+        self.engine.setLocal(self.name, item)
+        return 1
+
+class DummyDomain:
+    __implements__ = IDomain
+
+    def translate(self, msgid, mapping=None, context=None,
+                  target_language=None, default=None):
+        # This is a fake translation service which simply uppercases non
+        # ${name} placeholder text in the message id.
+        #
+        # First, transform a string with ${name} placeholders into a list of
+        # substrings.  Then upcase everything but the placeholders, then glue
+        # things back together.
+
+        # simulate an unknown msgid by returning None
+        if msgid == "don't translate me":
+            text = default
+        else:
+            text = msgid.upper()
+
+        def repl(m, mapping=mapping):
+            return ustr(mapping[m.group(m.lastindex).lower()])
+        cre = re.compile(r'\$(?:(%s)|\{(%s)\})' % (NAME_RE, NAME_RE))
+        return cre.sub(repl, text)
+
+class DummyTranslationService:
+    __implements__ = ITranslationService
+
+    def translate(self, domain, msgid, mapping=None, context=None,
+                  target_language=None, default=None):
+        return self.getDomain(domain).translate(msgid, mapping, context,
+                                                target_language,
+                                                default=default)
+
+    def getDomain(self, domain):
+        return DummyDomain()
--- a/roundup/cgi/TAL/HTMLParser.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/HTMLParser.py	Fri May 21 05:36:30 2004 +0000
@@ -1,5 +1,4 @@
 """A parser for HTML and XHTML."""
-__docformat__ = 'restructuredtext'
 
 # This file is based on sgmllib.py, but the API is slightly different.
 
@@ -11,7 +10,6 @@
 
 import markupbase
 import re
-import string
 
 # Regular expressions used for parsing
 
@@ -261,7 +259,7 @@
         match = tagfind.match(rawdata, i+1)
         assert match, 'unexpected call to parse_starttag()'
         k = match.end()
-        self.lasttag = tag = string.lower(rawdata[i+1:k])
+        self.lasttag = tag = rawdata[i+1:k].lower()
 
         while k < endpos:
             m = attrfind.match(rawdata, k)
@@ -274,16 +272,16 @@
                  attrvalue[:1] == '"' == attrvalue[-1:]:
                 attrvalue = attrvalue[1:-1]
                 attrvalue = self.unescape(attrvalue)
-            attrs.append((string.lower(attrname), attrvalue))
+            attrs.append((attrname.lower(), attrvalue))
             k = m.end()
 
-        end = string.strip(rawdata[k:endpos])
+        end = rawdata[k:endpos].strip()
         if end not in (">", "/>"):
             lineno, offset = self.getpos()
             if "\n" in self.__starttag_text:
-                lineno = lineno + string.count(self.__starttag_text, "\n")
+                lineno = lineno + self.__starttag_text.count("\n")
                 offset = len(self.__starttag_text) \
-                         - string.rfind(self.__starttag_text, "\n")
+                         - self.__starttag_text.rfind("\n")
             else:
                 offset = offset + len(self.__starttag_text)
             self.error("junk characters in start tag: %s"
@@ -340,7 +338,7 @@
         match = endtagfind.match(rawdata, i) # </ + tag + >
         if not match:
             self.error("bad end tag: %s" % `rawdata[i:j]`)
-        tag = string.lower(match.group(1))
+        tag = match.group(1).lower()
         if (  self.cdata_endtag is not None
               and tag != self.cdata_endtag):
             # Should be a mismatched end tag, but we'll treat it
@@ -396,9 +394,9 @@
     def unescape(self, s):
         if '&' not in s:
             return s
-        s = string.replace(s, "&lt;", "<")
-        s = string.replace(s, "&gt;", ">")
-        s = string.replace(s, "&apos;", "'")
-        s = string.replace(s, "&quot;", '"')
-        s = string.replace(s, "&amp;", "&") # Must be last
+        s = s.replace("&lt;", "<")
+        s = s.replace("&gt;", ">")
+        s = s.replace("&apos;", "'")
+        s = s.replace("&quot;", '"')
+        s = s.replace("&amp;", "&") # Must be last
         return s
--- a/roundup/cgi/TAL/HTMLTALParser.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/HTMLTALParser.py	Fri May 21 05:36:30 2004 +0000
@@ -2,25 +2,25 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-# 
+# FOR A PARTICULAR PURPOSE.
+#
 ##############################################################################
-"""Parse HTML and compile to TALInterpreter intermediate code.
 """
-__docformat__ = 'restructuredtext'
+Parse HTML and compile to TALInterpreter intermediate code.
+"""
 
 import sys
-import string
 
 from TALGenerator import TALGenerator
-from TALDefs import ZOPE_METAL_NS, ZOPE_TAL_NS, METALError, TALError
 from HTMLParser import HTMLParser, HTMLParseError
+from TALDefs import \
+     ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS, METALError, TALError, I18NError
 
 BOOLEAN_HTML_ATTRS = [
     # List of Boolean attributes in HTML that may be given in
@@ -75,7 +75,7 @@
                        % (tagstack[0], endtag))
             else:
                 msg = ('Open tags <%s> do not match close tag </%s>'
-                       % (string.join(tagstack, '>, <'), endtag))
+                       % ('>, <'.join(tagstack), endtag))
         else:
             msg = 'No tags are open to match </%s>' % endtag
         HTMLParseError.__init__(self, msg, position)
@@ -107,13 +107,20 @@
         self.gen = gen
         self.tagstack = []
         self.nsstack = []
-        self.nsdict = {'tal': ZOPE_TAL_NS, 'metal': ZOPE_METAL_NS}
+        self.nsdict = {'tal': ZOPE_TAL_NS,
+                       'metal': ZOPE_METAL_NS,
+                       'i18n': ZOPE_I18N_NS,
+                       }
 
     def parseFile(self, file):
         f = open(file)
         data = f.read()
         f.close()
-        self.parseString(data)
+        try:
+            self.parseString(data)
+        except TALError, e:
+            e.setFile(file)
+            raise
 
     def parseString(self, data):
         self.feed(data)
@@ -133,9 +140,14 @@
     def handle_starttag(self, tag, attrs):
         self.close_para_tags(tag)
         self.scan_xmlns(attrs)
-        tag, attrlist, taldict, metaldict = self.process_ns(tag, attrs)
+        tag, attrlist, taldict, metaldict, i18ndict \
+             = self.process_ns(tag, attrs)
+        if tag in EMPTY_HTML_TAGS and taldict.get("content"):
+            raise TALError(
+                "empty HTML tags cannot use tal:content: %s" % `tag`,
+                self.getpos())
         self.tagstack.append(tag)
-        self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
+        self.gen.emitStartElement(tag, attrlist, taldict, metaldict, i18ndict,
                                   self.getpos())
         if tag in EMPTY_HTML_TAGS:
             self.implied_endtag(tag, -1)
@@ -143,14 +155,19 @@
     def handle_startendtag(self, tag, attrs):
         self.close_para_tags(tag)
         self.scan_xmlns(attrs)
-        tag, attrlist, taldict, metaldict = self.process_ns(tag, attrs)
+        tag, attrlist, taldict, metaldict, i18ndict \
+             = self.process_ns(tag, attrs)
         if taldict.get("content"):
+            if tag in EMPTY_HTML_TAGS:
+                raise TALError(
+                    "empty HTML tags cannot use tal:content: %s" % `tag`,
+                    self.getpos())
             self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
-                                      self.getpos())
+                                      i18ndict, self.getpos())
             self.gen.emitEndElement(tag, implied=-1)
         else:
             self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
-                                      self.getpos(), isend=1)
+                                      i18ndict, self.getpos(), isend=1)
         self.pop_xmlns()
 
     def handle_endtag(self, tag):
@@ -236,7 +253,7 @@
     def scan_xmlns(self, attrs):
         nsnew = {}
         for key, value in attrs:
-            if key[:6] == "xmlns:":
+            if key.startswith("xmlns:"):
                 nsnew[key[6:]] = value
         if nsnew:
             self.nsstack.append(self.nsdict)
@@ -250,10 +267,10 @@
 
     def fixname(self, name):
         if ':' in name:
-            prefix, suffix = string.split(name, ':', 1)
+            prefix, suffix = name.split(':', 1)
             if prefix == 'xmlns':
                 nsuri = self.nsdict.get(suffix)
-                if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS):
+                if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS, ZOPE_I18N_NS):
                     return name, name, prefix
             else:
                 nsuri = self.nsdict.get(prefix)
@@ -261,12 +278,15 @@
                     return name, suffix, 'tal'
                 elif nsuri == ZOPE_METAL_NS:
                     return name, suffix,  'metal'
+                elif nsuri == ZOPE_I18N_NS:
+                    return name, suffix, 'i18n'
         return name, name, 0
 
     def process_ns(self, name, attrs):
         attrlist = []
         taldict = {}
         metaldict = {}
+        i18ndict = {}
         name, namebase, namens = self.fixname(name)
         for item in attrs:
             key, value = item
@@ -284,7 +304,12 @@
                     raise METALError("duplicate METAL attribute " +
                                      `keybase`, self.getpos())
                 metaldict[keybase] = value
+            elif ns == 'i18n':
+                if i18ndict.has_key(keybase):
+                    raise I18NError("duplicate i18n attribute " +
+                                    `keybase`, self.getpos())
+                i18ndict[keybase] = value
             attrlist.append(item)
         if namens in ('metal', 'tal'):
             taldict['tal tag'] = namens
-        return name, attrlist, taldict, metaldict
+        return name, attrlist, taldict, metaldict, i18ndict
--- a/roundup/cgi/TAL/TALDefs.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/TALDefs.py	Fri May 21 05:36:30 2004 +0000
@@ -2,37 +2,44 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-# 
+# FOR A PARTICULAR PURPOSE.
+#
 ##############################################################################
-"""Common definitions used by TAL and METAL compilation an transformation.
+# Modifications for Roundup:
+# 1. commented out ITALES references
 """
-__docformat__ = 'restructuredtext'
+Common definitions used by TAL and METAL compilation an transformation.
+"""
 
 from types import ListType, TupleType
 
-TAL_VERSION = "1.3.2"
+#from ITALES import ITALESErrorInfo
+
+TAL_VERSION = "1.4"
 
 XML_NS = "http://www.w3.org/XML/1998/namespace" # URI for XML namespace
 XMLNS_NS = "http://www.w3.org/2000/xmlns/" # URI for XML NS declarations
 
 ZOPE_TAL_NS = "http://xml.zope.org/namespaces/tal"
 ZOPE_METAL_NS = "http://xml.zope.org/namespaces/metal"
+ZOPE_I18N_NS = "http://xml.zope.org/namespaces/i18n"
 
-NAME_RE = "[a-zA-Z_][a-zA-Z0-9_]*"
+# This RE must exactly match the expression of the same name in the
+# zope.i18n.simpletranslationservice module:
+NAME_RE = "[a-zA-Z_][-a-zA-Z0-9_]*"
 
 KNOWN_METAL_ATTRIBUTES = [
     "define-macro",
     "use-macro",
     "define-slot",
     "fill-slot",
-    "slot"
+    "slot",
     ]
 
 KNOWN_TAL_ATTRIBUTES = [
@@ -47,6 +54,16 @@
     "tal tag",
     ]
 
+KNOWN_I18N_ATTRIBUTES = [
+    "translate",
+    "domain",
+    "target",
+    "source",
+    "attributes",
+    "data",
+    "name",
+    ]
+
 class TALError(Exception):
 
     def __init__(self, msg, position=(None, None)):
@@ -54,6 +71,10 @@
         self.msg = msg
         self.lineno = position[0]
         self.offset = position[1]
+        self.filename = None
+
+    def setFile(self, filename):
+        self.filename = filename
 
     def __str__(self):
         result = self.msg
@@ -61,6 +82,8 @@
             result = result + ", at line %d" % self.lineno
         if self.offset is not None:
             result = result + ", column %d" % (self.offset + 1)
+        if self.filename is not None:
+            result = result + ', in file %s' % self.filename
         return result
 
 class METALError(TALError):
@@ -69,8 +92,14 @@
 class TALESError(TALError):
     pass
 
+class I18NError(TALError):
+    pass
+
+
 class ErrorInfo:
 
+    #__implements__ = ITALESErrorInfo
+
     def __init__(self, err, position=(None, None)):
         if isinstance(err, Exception):
             self.type = err.__class__
@@ -81,20 +110,24 @@
         self.lineno = position[0]
         self.offset = position[1]
 
+
+
 import re
 _attr_re = re.compile(r"\s*([^\s]+)\s+([^\s].*)\Z", re.S)
 _subst_re = re.compile(r"\s*(?:(text|structure)\s+)?(.*)\Z", re.S)
 del re
 
-def parseAttributeReplacements(arg):
+def parseAttributeReplacements(arg, xml):
     dict = {}
     for part in splitParts(arg):
         m = _attr_re.match(part)
         if not m:
-            raise TALError("Bad syntax in attributes:" + `part`)
+            raise TALError("Bad syntax in attributes: " + `part`)
         name, expr = m.group(1, 2)
+        if not xml:
+            name = name.lower()
         if dict.has_key(name):
-            raise TALError("Duplicate attribute name in attributes:" + `part`)
+            raise TALError("Duplicate attribute name in attributes: " + `part`)
         dict[name] = expr
     return dict
 
@@ -110,11 +143,10 @@
 def splitParts(arg):
     # Break in pieces at undoubled semicolons and
     # change double semicolons to singles:
-    import string
-    arg = string.replace(arg, ";;", "\0")
-    parts = string.split(arg, ';')
-    parts = map(lambda s, repl=string.replace: repl(s, "\0", ";"), parts)
-    if len(parts) > 1 and not string.strip(parts[-1]):
+    arg = arg.replace(";;", "\0")
+    parts = arg.split(';')
+    parts = [p.replace("\0", ";") for p in parts]
+    if len(parts) > 1 and not parts[-1].strip():
         del parts[-1] # It ended in a semicolon
     return parts
 
@@ -139,7 +171,23 @@
             return version
     return None
 
-import cgi
-def quote(s, escape=cgi.escape):
-    return '"%s"' % escape(s, 1)
-del cgi
+import re
+_ent1_re = re.compile('&(?![A-Z#])', re.I)
+_entch_re = re.compile('&([A-Z][A-Z0-9]*)(?![A-Z0-9;])', re.I)
+_entn1_re = re.compile('&#(?![0-9X])', re.I)
+_entnx_re = re.compile('&(#X[A-F0-9]*)(?![A-F0-9;])', re.I)
+_entnd_re = re.compile('&(#[0-9][0-9]*)(?![0-9;])')
+del re
+
+def attrEscape(s):
+    """Replace special characters '&<>' by character entities,
+    except when '&' already begins a syntactically valid entity."""
+    s = _ent1_re.sub('&amp;', s)
+    s = _entch_re.sub(r'&amp;\1', s)
+    s = _entn1_re.sub('&amp;#', s)
+    s = _entnx_re.sub(r'&amp;\1', s)
+    s = _entnd_re.sub(r'&amp;\1', s)
+    s = s.replace('<', '&lt;')
+    s = s.replace('>', '&gt;')
+    s = s.replace('"', '&quot;')
+    return s
--- a/roundup/cgi/TAL/TALGenerator.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/TALGenerator.py	Fri May 21 05:36:30 2004 +0000
@@ -2,39 +2,57 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-# 
+# FOR A PARTICULAR PURPOSE.
+#
 ##############################################################################
-"""Code generator for TALInterpreter intermediate code.
 """
-__docformat__ = 'restructuredtext'
+Code generator for TALInterpreter intermediate code.
+"""
 
-import string
 import re
 import cgi
 
-from TALDefs import *
+import TALDefs
+
+from TALDefs import NAME_RE, TAL_VERSION
+from TALDefs import I18NError, METALError, TALError
+from TALDefs import parseSubstitution
+from TranslationContext import TranslationContext, DEFAULT_DOMAIN
+
+I18N_REPLACE = 1
+I18N_CONTENT = 2
+I18N_EXPRESSION = 3
+
+_name_rx = re.compile(NAME_RE)
+
 
 class TALGenerator:
 
     inMacroUse = 0
     inMacroDef = 0
     source_file = None
-    
+
     def __init__(self, expressionCompiler=None, xml=1, source_file=None):
         if not expressionCompiler:
             from DummyEngine import DummyEngine
             expressionCompiler = DummyEngine()
         self.expressionCompiler = expressionCompiler
         self.CompilerError = expressionCompiler.getCompilerError()
+        # This holds the emitted opcodes representing the input
         self.program = []
+        # The program stack for when we need to do some sub-evaluation for an
+        # intermediate result.  E.g. in an i18n:name tag for which the
+        # contents describe the ${name} value.
         self.stack = []
+        # Another stack of postponed actions.  Elements on this stack are a
+        # dictionary; key/values contain useful information that
+        # emitEndElement needs to finish its calculations
         self.todoStack = []
         self.macros = {}
         self.slots = {}
@@ -45,6 +63,8 @@
         if source_file is not None:
             self.source_file = source_file
             self.emit("setSourceFile", source_file)
+        self.i18nContext = TranslationContext()
+        self.i18nLevel = 0
 
     def getCode(self):
         assert not self.stack
@@ -54,7 +74,7 @@
     def optimize(self, program):
         output = []
         collect = []
-        rawseen = cursor = 0
+        cursor = 0
         if self.xml:
             endsep = "/>"
         else:
@@ -83,9 +103,15 @@
                 # instructions to be joined together.
                 output.append(self.optimizeArgsList(item))
                 continue
-            text = string.join(collect, "")
+            if opcode == 'noop':
+                # This is a spacer for end tags in the face of i18n:name
+                # attributes.  We can't let the optimizer collect immediately
+                # following end tags into the same rawtextOffset.
+                opcode = None
+                pass
+            text = "".join(collect)
             if text:
-                i = string.rfind(text, "\n")
+                i = text.rfind("\n")
                 if i >= 0:
                     i = len(text) - (i + 1)
                     output.append(("rawtextColumn", (text, i)))
@@ -93,7 +119,6 @@
                     output.append(("rawtextOffset", (text, len(text))))
             if opcode != None:
                 output.append(self.optimizeArgsList(item))
-            rawseen = cursor+1
             collect = []
         return self.optimizeCommonTriple(output)
 
@@ -103,9 +128,28 @@
         else:
             return item[0], tuple(item[1:])
 
-    actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4,
-                   0: 0, 1: 1, 2: 2, 3: 3, 4: 4}
+    # These codes are used to indicate what sort of special actions
+    # are needed for each special attribute.  (Simple attributes don't
+    # get action codes.)
+    #
+    # The special actions (which are modal) are handled by
+    # TALInterpreter.attrAction() and .attrAction_tal().
+    #
+    # Each attribute is represented by a tuple:
+    #
+    # (name, value)                 -- a simple name/value pair, with
+    #                                  no special processing
+    #
+    # (name, value, action, *extra) -- attribute with special
+    #                                  processing needs, action is a
+    #                                  code that indicates which
+    #                                  branch to take, and *extra
+    #                                  contains additional,
+    #                                  action-specific information
+    #                                  needed by the processing
+    #
     def optimizeStartTag(self, collect, name, attrlist, end):
+        # return true if the tag can be converted to plain text
         if not attrlist:
             collect.append("<%s%s" % (name, end))
             return 1
@@ -116,18 +160,15 @@
             if len(item) > 2:
                 opt = 0
                 name, value, action = item[:3]
-                action = self.actionIndex[action]
                 attrlist[i] = (name, value, action) + item[3:]
             else:
                 if item[1] is None:
                     s = item[0]
                 else:
-                    s = "%s=%s" % (item[0], quote(item[1]))
+                    s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1]))
                 attrlist[i] = item[0], s
-            if item[1] is None:
-                new.append(" " + item[0])
-            else:
-                new.append(" %s=%s" % (item[0], quote(item[1])))
+                new.append(" " + s)
+        # if no non-optimizable attributes were found, convert to plain text
         if opt:
             new.append(end)
             collect.extend(new)
@@ -139,9 +180,9 @@
         output = program[:2]
         prev2, prev1 = output
         for item in program[2:]:
-            if (  item[0] == "beginScope"
-                  and prev1[0] == "setPosition"
-                  and prev2[0] == "rawtextColumn"):
+            if ( item[0] == "beginScope"
+                 and prev1[0] == "setPosition"
+                 and prev2[0] == "rawtextColumn"):
                 position = output.pop()[1]
                 text, column = output.pop()[1]
                 prev1 = None, None
@@ -215,7 +256,7 @@
         if cexpr:
             cexpr = self.compileExpression(optTag[0])
         self.emit("optTag", name, cexpr, optTag[1], isend, start, program)
-        
+
     def emitRawText(self, text):
         self.emit("rawtext", text)
 
@@ -223,7 +264,7 @@
         self.emitRawText(cgi.escape(text))
 
     def emitDefines(self, defines):
-        for part in splitParts(defines):
+        for part in TALDefs.splitParts(defines):
             m = re.match(
                 r"(?s)\s*(?:(global|local)\s+)?(%s)\s+(.*)\Z" % NAME_RE, part)
             if not m:
@@ -278,9 +319,56 @@
             assert key == "structure"
             self.emit("insertStructure", cexpr, attrDict, program)
 
+    def emitI18nVariable(self, stuff):
+        # Used for i18n:name attributes.  arg is extra information describing
+        # how the contents of the variable should get filled in, and it will
+        # either be a 1-tuple or a 2-tuple.  If arg[0] is None, then the
+        # i18n:name value is taken implicitly from the contents of the tag,
+        # e.g. "I live in <span i18n:name="country">the USA</span>".  In this
+        # case, arg[1] is the opcode sub-program describing the contents of
+        # the tag.
+        #
+        # When arg[0] is not None, it contains the tal expression used to
+        # calculate the contents of the variable, e.g.
+        # "I live in <span i18n:name="country"
+        #                  tal:replace="here/countryOfOrigin" />"
+        varname, action, expression = stuff
+        m = _name_rx.match(varname)
+        if m is None or m.group() != varname:
+            raise TALError("illegal i18n:name: %r" % varname, self.position)
+        key = cexpr = None
+        program = self.popProgram()
+        if action == I18N_REPLACE:
+            # This is a tag with an i18n:name and a tal:replace (implicit or
+            # explicit).  Get rid of the first and last elements of the
+            # program, which are the start and end tag opcodes of the tag.
+            program = program[1:-1]
+        elif action == I18N_CONTENT:
+            # This is a tag with an i18n:name and a tal:content
+            # (explicit-only).  Keep the first and last elements of the
+            # program, so we keep the start and end tag output.
+            pass
+        else:
+            assert action == I18N_EXPRESSION
+            key, expr = parseSubstitution(expression)
+            cexpr = self.compileExpression(expr)
+        # XXX Would key be anything but 'text' or None?
+        assert key in ('text', None)
+        self.emit('i18nVariable', varname, program, cexpr)
+
+    def emitTranslation(self, msgid, i18ndata):
+        program = self.popProgram()
+        if i18ndata is None:
+            self.emit('insertTranslation', msgid, program)
+        else:
+            key, expr = parseSubstitution(i18ndata)
+            cexpr = self.compileExpression(expr)
+            assert key == 'text'
+            self.emit('insertTranslation', msgid, program, cexpr)
+
     def emitDefineMacro(self, macroName):
         program = self.popProgram()
-        macroName = string.strip(macroName)
+        macroName = macroName.strip()
         if self.macros.has_key(macroName):
             raise METALError("duplicate macro definition: %s" % `macroName`,
                              self.position)
@@ -299,7 +387,7 @@
 
     def emitDefineSlot(self, slotName):
         program = self.popProgram()
-        slotName = string.strip(slotName)
+        slotName = slotName.strip()
         if not re.match('%s$' % NAME_RE, slotName):
             raise METALError("invalid slot name: %s" % `slotName`,
                              self.position)
@@ -307,7 +395,7 @@
 
     def emitFillSlot(self, slotName):
         program = self.popProgram()
-        slotName = string.strip(slotName)
+        slotName = slotName.strip()
         if self.slots.has_key(slotName):
             raise METALError("duplicate fill-slot name: %s" % `slotName`,
                              self.position)
@@ -338,7 +426,7 @@
                 self.program[i] = ("rawtext", text[:m.start()])
                 collect.append(m.group())
         collect.reverse()
-        return string.join(collect, "")
+        return "".join(collect)
 
     def unEmitNewlineWhitespace(self):
         collect = []
@@ -357,7 +445,7 @@
                 break
             text, rest = m.group(1, 2)
             collect.reverse()
-            rest = rest + string.join(collect, "")
+            rest = rest + "".join(collect)
             del self.program[i:]
             if text:
                 self.emit("rawtext", text)
@@ -365,23 +453,30 @@
         return None
 
     def replaceAttrs(self, attrlist, repldict):
+        # Each entry in attrlist starts like (name, value).
+        # Result is (name, value, action, expr, xlat) if there is a
+        # tal:attributes entry for that attribute.  Additional attrs
+        # defined only by tal:attributes are added here.
+        #
+        # (name, value, action, expr, xlat)
         if not repldict:
             return attrlist
         newlist = []
         for item in attrlist:
             key = item[0]
             if repldict.has_key(key):
-                item = item[:2] + ("replace", repldict[key])
+                expr, xlat, msgid = repldict[key]
+                item = item[:2] + ("replace", expr, xlat, msgid)
                 del repldict[key]
             newlist.append(item)
-        for key, value in repldict.items(): # Add dynamic-only attributes
-            item = (key, None, "insert", value)
-            newlist.append(item)
+        # Add dynamic-only attributes
+        for key, (expr, xlat, msgid) in repldict.items():
+            newlist.append((key, None, "insert", expr, xlat, msgid))
         return newlist
 
-    def emitStartElement(self, name, attrlist, taldict, metaldict,
+    def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
                          position=(None, None), isend=0):
-        if not taldict and not metaldict:
+        if not taldict and not metaldict and not i18ndict:
             # Handle the simple, common case
             self.emitStartTag(name, attrlist, isend)
             self.todoPush({})
@@ -391,18 +486,24 @@
 
         self.position = position
         for key, value in taldict.items():
-            if key not in KNOWN_TAL_ATTRIBUTES:
+            if key not in TALDefs.KNOWN_TAL_ATTRIBUTES:
                 raise TALError("bad TAL attribute: " + `key`, position)
             if not (value or key == 'omit-tag'):
                 raise TALError("missing value for TAL attribute: " +
                                `key`, position)
         for key, value in metaldict.items():
-            if key not in KNOWN_METAL_ATTRIBUTES:
+            if key not in TALDefs.KNOWN_METAL_ATTRIBUTES:
                 raise METALError("bad METAL attribute: " + `key`,
-                position)
+                                 position)
             if not value:
                 raise TALError("missing value for METAL attribute: " +
                                `key`, position)
+        for key, value in i18ndict.items():
+            if key not in TALDefs.KNOWN_I18N_ATTRIBUTES:
+                raise I18NError("bad i18n attribute: " + `key`, position)
+            if not value and key in ("attributes", "data", "id"):
+                raise I18NError("missing value for i18n attribute: " +
+                                `key`, position)
         todo = {}
         defineMacro = metaldict.get("define-macro")
         useMacro = metaldict.get("use-macro")
@@ -417,13 +518,36 @@
         onError = taldict.get("on-error")
         omitTag = taldict.get("omit-tag")
         TALtag = taldict.get("tal tag")
+        i18nattrs = i18ndict.get("attributes")
+        # Preserve empty string if implicit msgids are used.  We'll generate
+        # code with the msgid='' and calculate the right implicit msgid during
+        # interpretation phase.
+        msgid = i18ndict.get("translate")
+        varname = i18ndict.get('name')
+        i18ndata = i18ndict.get('data')
+
+        if varname and not self.i18nLevel:
+            raise I18NError(
+                "i18n:name can only occur inside a translation unit",
+                position)
+
+        if i18ndata and not msgid:
+            raise I18NError("i18n:data must be accompanied by i18n:translate",
+                            position)
+
         if len(metaldict) > 1 and (defineMacro or useMacro):
             raise METALError("define-macro and use-macro cannot be used "
                              "together or with define-slot or fill-slot",
                              position)
-        if content and replace:
-            raise TALError("content and replace are mutually exclusive",
-                           position)
+        if replace:
+            if content:
+                raise TALError(
+                    "tal:content and tal:replace are mutually exclusive",
+                    position)
+            if msgid is not None:
+                raise I18NError(
+                    "i18n:translate and tal:replace are mutually exclusive",
+                    position)
 
         repeatWhitespace = None
         if repeat:
@@ -441,8 +565,8 @@
                 self.inMacroUse = 0
         else:
             if fillSlot:
-                raise METALError, ("fill-slot must be within a use-macro",
-                                   position)
+                raise METALError("fill-slot must be within a use-macro",
+                                 position)
         if not self.inMacroUse:
             if defineMacro:
                 self.pushProgram()
@@ -459,13 +583,29 @@
                 self.inMacroUse = 1
             if defineSlot:
                 if not self.inMacroDef:
-                    raise METALError, (
+                    raise METALError(
                         "define-slot must be within a define-macro",
                         position)
                 self.pushProgram()
                 todo["defineSlot"] = defineSlot
 
-        if taldict:
+        if defineSlot or i18ndict:
+
+            domain = i18ndict.get("domain") or self.i18nContext.domain
+            source = i18ndict.get("source") or self.i18nContext.source
+            target = i18ndict.get("target") or self.i18nContext.target
+            if (  domain != DEFAULT_DOMAIN
+                  or source is not None
+                  or target is not None):
+                self.i18nContext = TranslationContext(self.i18nContext,
+                                                      domain=domain,
+                                                      source=source,
+                                                      target=target)
+                self.emit("beginI18nContext",
+                          {"domain": domain, "source": source,
+                           "target": target})
+                todo["i18ncontext"] = 1
+        if taldict or i18ndict:
             dict = {}
             for item in attrlist:
                 key, value = item[:2]
@@ -493,18 +633,60 @@
             if repeatWhitespace:
                 self.emitText(repeatWhitespace)
         if content:
-            todo["content"] = content
-        if replace:
-            todo["replace"] = replace
+            if varname:
+                todo['i18nvar'] = (varname, I18N_CONTENT, None)
+                todo["content"] = content
+                self.pushProgram()
+            else:
+                todo["content"] = content
+        elif replace:
+            # tal:replace w/ i18n:name has slightly different semantics.  What
+            # we're actually replacing then is the contents of the ${name}
+            # placeholder.
+            if varname:
+                todo['i18nvar'] = (varname, I18N_EXPRESSION, replace)
+            else:
+                todo["replace"] = replace
             self.pushProgram()
+        # i18n:name w/o tal:replace uses the content as the interpolation
+        # dictionary values
+        elif varname:
+            todo['i18nvar'] = (varname, I18N_REPLACE, None)
+            self.pushProgram()
+        if msgid is not None:
+            self.i18nLevel += 1
+            todo['msgid'] = msgid
+        if i18ndata:
+            todo['i18ndata'] = i18ndata
         optTag = omitTag is not None or TALtag
         if optTag:
             todo["optional tag"] = omitTag, TALtag
             self.pushProgram()
-        if attrsubst:
-            repldict = parseAttributeReplacements(attrsubst)
+        if attrsubst or i18nattrs:
+            if attrsubst:
+                repldict = TALDefs.parseAttributeReplacements(attrsubst,
+                                                              self.xml)
+            else:
+                repldict = {}
+            if i18nattrs:
+                i18nattrs = _parseI18nAttributes(i18nattrs, attrlist, repldict,
+                                                 self.position, self.xml,
+                                                 self.source_file)
+            else:
+                i18nattrs = {}
+            # Convert repldict's name-->expr mapping to a
+            # name-->(compiled_expr, translate) mapping
             for key, value in repldict.items():
-                repldict[key] = self.compileExpression(value)
+                if i18nattrs.get(key, None):
+                    raise I18NError(
+                      ("attribute [%s] cannot both be part of tal:attributes" +
+                      " and have a msgid in i18n:attributes") % key,
+                    position)
+                ce = self.compileExpression(value)
+                repldict[key] = ce, key in i18nattrs, i18nattrs.get(key)
+            for key in i18nattrs:
+                if not repldict.has_key(key):
+                    repldict[key] = None, 1, i18nattrs.get(key)
         else:
             repldict = {}
         if replace:
@@ -513,7 +695,11 @@
         self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
         if optTag:
             self.pushProgram()
-        if content:
+        if content and not varname:
+            self.pushProgram()
+        if msgid is not None:
+            self.pushProgram()
+        if content and varname:
             self.pushProgram()
         if todo and position != (None, None):
             todo["position"] = position
@@ -543,6 +729,10 @@
         repldict = todo.get("repldict", {})
         scope = todo.get("scope")
         optTag = todo.get("optional tag")
+        msgid = todo.get('msgid')
+        i18ncontext = todo.get("i18ncontext")
+        varname = todo.get('i18nvar')
+        i18ndata = todo.get('i18ndata')
 
         if implied > 0:
             if defineMacro or useMacro or defineSlot or fillSlot:
@@ -554,14 +744,58 @@
             raise exc("%s attributes on <%s> require explicit </%s>" %
                       (what, name, name), position)
 
+        # If there's no tal:content or tal:replace in the tag with the
+        # i18n:name, tal:replace is the default.
         if content:
             self.emitSubstitution(content, {})
+        # If we're looking at an implicit msgid, emit the insertTranslation
+        # opcode now, so that the end tag doesn't become part of the implicit
+        # msgid.  If we're looking at an explicit msgid, it's better to emit
+        # the opcode after the i18nVariable opcode so we can better handle
+        # tags with both of them in them (and in the latter case, the contents
+        # would be thrown away for msgid purposes).
+        #
+        # Still, we should emit insertTranslation opcode before i18nVariable
+        # in case tal:content, i18n:translate and i18n:name in the same tag
+        if msgid is not None:
+            if (not varname) or (
+                varname and (varname[1] == I18N_CONTENT)):
+                self.emitTranslation(msgid, i18ndata)
+            self.i18nLevel -= 1
         if optTag:
             self.emitOptTag(name, optTag, isend)
         elif not isend:
+            # If we're processing the end tag for a tag that contained
+            # i18n:name, we need to make sure that optimize() won't collect
+            # immediately following end tags into the same rawtextOffset, so
+            # put a spacer here that the optimizer will recognize.
+            if varname:
+                self.emit('noop')
             self.emitEndTag(name)
+        # If i18n:name appeared in the same tag as tal:replace then we're
+        # going to do the substitution a little bit differently.  The results
+        # of the expression go into the i18n substitution dictionary.
         if replace:
             self.emitSubstitution(replace, repldict)
+        elif varname:
+            # o varname[0] is the variable name
+            # o varname[1] is either
+            #   - I18N_REPLACE for implicit tal:replace
+            #   - I18N_CONTENT for tal:content
+            #   - I18N_EXPRESSION for explicit tal:replace
+            # o varname[2] will be None for the first two actions and the
+            #   replacement tal expression for the third action.
+            assert (varname[1]
+                    in [I18N_REPLACE, I18N_CONTENT, I18N_EXPRESSION])
+            self.emitI18nVariable(varname)
+        # Do not test for "msgid is not None", i.e. we only want to test for
+        # explicit msgids here.  See comment above.
+        if msgid is not None:
+            # in case tal:content, i18n:translate and i18n:name in the
+            # same tag insertTranslation opcode has already been
+            # emitted
+            if varname and (varname[1] <> I18N_CONTENT):
+                self.emitTranslation(msgid, i18ndata)
         if repeat:
             self.emitRepeat(repeat)
         if condition:
@@ -570,6 +804,10 @@
             self.emitOnError(name, onError, optTag and optTag[1], isend)
         if scope:
             self.emit("endScope")
+        if i18ncontext:
+            self.emit("endI18nContext")
+            assert self.i18nContext.parent is not None
+            self.i18nContext = self.i18nContext.parent
         if defineSlot:
             self.emitDefineSlot(defineSlot)
         if fillSlot:
@@ -579,6 +817,54 @@
         if defineMacro:
             self.emitDefineMacro(defineMacro)
 
+
+def _parseI18nAttributes(i18nattrs, attrlist, repldict, position,
+                         xml, source_file):
+
+    def addAttribute(dic, attr, msgid, position, xml):
+        if not xml:
+            attr = attr.lower()
+        if attr in dic:
+            raise TALError(
+                "attribute may only be specified once in i18n:attributes: "
+                + attr,
+                position)
+        dic[attr] = msgid
+
+    d = {}
+    if ';' in i18nattrs:
+        i18nattrlist = i18nattrs.split(';')
+        i18nattrlist = [attr.strip().split()
+                        for attr in i18nattrlist if attr.strip()]
+        for parts in i18nattrlist:
+            if len(parts) > 2:
+                raise TALError("illegal i18n:attributes specification: %r"
+                                % parts, position)
+            if len(parts) == 2:
+                attr, msgid = parts
+            else:
+                # len(parts) == 1
+                attr = parts[0]
+                msgid = None
+            addAttribute(d, attr, msgid, position, xml)
+    else:
+        i18nattrlist = i18nattrs.split()
+        if len(i18nattrlist) == 2:
+            staticattrs = [attr[0] for attr in attrlist if len(attr) == 2]
+            if (not i18nattrlist[1] in staticattrs) and (
+                not i18nattrlist[1] in repldict):
+                attr, msgid = i18nattrlist
+                addAttribute(d, attr, msgid, position, xml)
+            else:
+                msgid = None
+                for attr in i18nattrlist:
+                    addAttribute(d, attr, msgid, position, xml)
+        else:
+            msgid = None
+            for attr in i18nattrlist:
+                addAttribute(d, attr, msgid, position, xml)
+    return d
+
 def test():
     t = TALGenerator()
     t.pushProgram()
--- a/roundup/cgi/TAL/TALInterpreter.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/TALInterpreter.py	Fri May 21 05:36:30 2004 +0000
@@ -2,32 +2,35 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-# 
+# FOR A PARTICULAR PURPOSE.
+#
 ##############################################################################
-"""Interpreter for a pre-compiled TAL program.
+# Modifications for Roundup:
+# 1. implemented ustr as str
 """
-__docformat__ = 'restructuredtext'
+Interpreter for a pre-compiled TAL program.
+"""
 
 import sys
 import getopt
-
+import re
+from types import ListType
 from cgi import escape
+# Do not use cStringIO here!  It's not unicode aware. :(
+from StringIO import StringIO
+#from DocumentTemplate.DT_Util import ustr
+ustr = str
 
-try:
-    from cStringIO import StringIO
-except ImportError:
-    from StringIO import StringIO
-
-from TALDefs import quote, TAL_VERSION, TALError, METALError
+from TALDefs import TAL_VERSION, TALError, METALError, attrEscape
 from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode
 from TALGenerator import TALGenerator
+from TranslationContext import TranslationContext
 
 BOOLEAN_HTML_ATTRS = [
     # List of Boolean attributes in HTML that should be rendered in
@@ -40,13 +43,43 @@
     "defer"
 ]
 
-EMPTY_HTML_TAGS = [
-    # List of HTML tags with an empty content model; these are
-    # rendered in minimized form, e.g. <img />.
-    # From http://www.w3.org/TR/xhtml1/#dtds
-    "base", "meta", "link", "hr", "br", "param", "img", "area",
-    "input", "col", "basefont", "isindex", "frame",
-]
+def normalize(text):
+    # Now we need to normalize the whitespace in implicit message ids and
+    # implicit $name substitution values by stripping leading and trailing
+    # whitespace, and folding all internal whitespace to a single space.
+    return ' '.join(text.split())
+
+
+NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*"
+_interp_regex = re.compile(r'(?<!\$)(\$(?:%(n)s|{%(n)s}))' %({'n': NAME_RE}))
+_get_var_regex = re.compile(r'%(n)s' %({'n': NAME_RE}))
+
+def interpolate(text, mapping):
+    """Interpolate ${keyword} substitutions.
+
+    This is called when no translation is provided by the translation
+    service.
+    """
+    if not mapping:
+        return text
+    # Find all the spots we want to substitute.
+    to_replace = _interp_regex.findall(text)
+    # Now substitute with the variables in mapping.
+    for string in to_replace:
+        var = _get_var_regex.findall(string)[0]
+        if mapping.has_key(var):
+            # Call ustr because we may have an integer for instance.
+            subst = ustr(mapping[var])
+            try:
+                text = text.replace(string, subst)
+            except UnicodeError:
+                # subst contains high-bit chars...
+                # As we have no way of knowing the correct encoding,
+                # substitue something instead of raising an exception.
+                subst = `subst`[1:-1]
+                text = text.replace(string, subst)
+    return text
+
 
 class AltTALGenerator(TALGenerator):
 
@@ -60,16 +93,18 @@
 
     def emit(self, *args):
         if self.enabled:
-            apply(TALGenerator.emit, (self,) + args)
+            TALGenerator.emit(self, *args)
 
-    def emitStartElement(self, name, attrlist, taldict, metaldict,
+    def emitStartElement(self, name, attrlist, taldict, metaldict, i18ndict,
                          position=(None, None), isend=0):
         metaldict = {}
         taldict = {}
+        i18ndict = {}
         if self.enabled and self.repldict:
             taldict["attributes"] = "x x"
         TALGenerator.emitStartElement(self, name, attrlist,
-                                      taldict, metaldict, position, isend)
+                                      taldict, metaldict, i18ndict,
+                                      position, isend)
 
     def replaceAttrs(self, attrlist, repldict):
         if self.enabled and self.repldict:
@@ -82,10 +117,10 @@
 
     def __init__(self, program, macros, engine, stream=None,
                  debug=0, wrap=60, metal=1, tal=1, showtal=-1,
-                 strictinsert=1, stackLimit=100):
+                 strictinsert=1, stackLimit=100, i18nInterpolate=1):
         self.program = program
         self.macros = macros
-        self.engine = engine
+        self.engine = engine # Execution engine (aka context)
         self.Default = engine.getDefault()
         self.stream = stream or sys.stdout
         self._stream_write = self.stream.write
@@ -107,28 +142,36 @@
         self.endsep = "/>"
         self.endlen = len(self.endsep)
         self.macroStack = []
-        self.popMacro = self.macroStack.pop
         self.position = None, None  # (lineno, offset)
         self.col = 0
         self.level = 0
         self.scopeLevel = 0
         self.sourceFile = None
+        self.i18nStack = []
+        self.i18nInterpolate = i18nInterpolate
+        self.i18nContext = TranslationContext()
+
+    def StringIO(self):
+        # Third-party products wishing to provide a full Unicode-aware
+        # StringIO can do so by monkey-patching this method.
+        return FasterStringIO()
 
     def saveState(self):
         return (self.position, self.col, self.stream,
-                self.scopeLevel, self.level)
+                self.scopeLevel, self.level, self.i18nContext)
 
     def restoreState(self, state):
-        (self.position, self.col, self.stream, scopeLevel, level) = state
+        (self.position, self.col, self.stream, scopeLevel, level, i18n) = state
         self._stream_write = self.stream.write
         assert self.level == level
         while self.scopeLevel > scopeLevel:
             self.engine.endScope()
             self.scopeLevel = self.scopeLevel - 1
         self.engine.setPosition(self.position)
+        self.i18nContext = i18n
 
     def restoreOutputState(self, state):
-        (dummy, self.col, self.stream, scopeLevel, level) = state
+        (dummy, self.col, self.stream, scopeLevel, level, i18n) = state
         self._stream_write = self.stream.write
         assert self.level == level
         assert self.scopeLevel == scopeLevel
@@ -137,28 +180,25 @@
         if len(self.macroStack) >= self.stackLimit:
             raise METALError("macro nesting limit (%d) exceeded "
                              "by %s" % (self.stackLimit, `macroName`))
-        self.macroStack.append([macroName, slots, entering])
+        self.macroStack.append([macroName, slots, entering, self.i18nContext])
 
-    def macroContext(self, what):
-        macroStack = self.macroStack
-        i = len(macroStack)
-        while i > 0:
-            i = i-1
-            if macroStack[i][0] == what:
-                return i
-        return -1
+    def popMacro(self):
+        return self.macroStack.pop()
 
     def __call__(self):
         assert self.level == 0
         assert self.scopeLevel == 0
+        assert self.i18nContext.parent is None
         self.interpret(self.program)
         assert self.level == 0
         assert self.scopeLevel == 0
+        assert self.i18nContext.parent is None
         if self.col > 0:
             self._stream_write("\n")
             self.col = 0
 
-    def stream_write(self, s, len=len):
+    def stream_write(self, s,
+                     len=len):
         self._stream_write(s)
         i = s.rfind('\n')
         if i < 0:
@@ -168,6 +208,16 @@
 
     bytecode_handlers = {}
 
+    def interpretWithStream(self, program, stream):
+        oldstream = self.stream
+        self.stream = stream
+        self._stream_write = stream.write
+        try:
+            self.interpret(program)
+        finally:
+            self.stream = oldstream
+            self._stream_write = oldstream.write
+
     def interpret(self, program):
         oldlevel = self.level
         self.level = oldlevel + 1
@@ -175,8 +225,8 @@
         try:
             if self.debug:
                 for (opcode, args) in program:
-                    s = "%sdo_%s%s\n" % ("    "*self.level, opcode,
-                                      repr(args))
+                    s = "%sdo_%s(%s)\n" % ("    "*self.level, opcode,
+                                           repr(args))
                     if len(s) > 80:
                         s = s[:76] + "...\n"
                     sys.stderr.write(s)
@@ -221,10 +271,9 @@
         # for start tags with no attributes; those are optimized down
         # to rawtext events.  Hence, there is no special "fast path"
         # for that case.
-        _stream_write = self._stream_write
-        _stream_write("<" + name)
-        namelen = _len(name)
-        col = self.col + namelen + 1
+        L = ["<", name]
+        append = L.append
+        col = self.col + _len(name) + 1
         wrap = self.wrap
         align = col + 1
         if align >= wrap/2:
@@ -235,20 +284,28 @@
                 if _len(item) == 2:
                     name, s = item
                 else:
-                    ok, name, s = attrAction(self, item)
+                    # item[2] is the 'action' field:
+                    if item[2] in ('metal', 'tal', 'xmlns', 'i18n'):
+                        if not self.showtal:
+                            continue
+                        ok, name, s = self.attrAction(item)
+                    else:
+                        ok, name, s = attrAction(self, item)
                     if not ok:
                         continue
                 slen = _len(s)
                 if (wrap and
                     col >= align and
                     col + 1 + slen > wrap):
-                    _stream_write("\n" + " "*align)
+                    append("\n")
+                    append(" "*align)
                     col = align + slen
                 else:
-                    s = " " + s
+                    append(" ")
                     col = col + 1 + slen
-                _stream_write(s)
-            _stream_write(end)
+                append(s)
+            append(end)
+            self._stream_write("".join(L))
             col = col + endlen
         finally:
             self.col = col
@@ -256,10 +313,10 @@
 
     def attrAction(self, item):
         name, value, action = item[:3]
-        if action == 1 or (action > 1 and not self.showtal):
+        if action == 'insert':
             return 0, name, value
         macs = self.macroStack
-        if action == 2 and self.metal and macs:
+        if action == 'metal' and self.metal and macs:
             if len(macs) > 1 or not macs[-1][2]:
                 # Drop all METAL attributes at a use-depth above one.
                 return 0, name, value
@@ -273,7 +330,7 @@
                 name = prefix + "use-macro"
                 value = macs[-1][0] # Macro name
             elif suffix == "define-slot":
-                name = prefix + "slot"
+                name = prefix + "fill-slot"
             elif suffix == "fill-slot":
                 pass
             else:
@@ -282,43 +339,52 @@
         if value is None:
             value = name
         else:
-            value = "%s=%s" % (name, quote(value))
+            value = '%s="%s"' % (name, attrEscape(value))
         return 1, name, value
 
     def attrAction_tal(self, item):
         name, value, action = item[:3]
-        if action > 1:
-            return self.attrAction(item)
         ok = 1
+        expr, xlat, msgid = item[3:]
         if self.html and name.lower() in BOOLEAN_HTML_ATTRS:
             evalue = self.engine.evaluateBoolean(item[3])
             if evalue is self.Default:
-                if action == 1: # Cancelled insert
+                if action == 'insert': # Cancelled insert
                     ok = 0
             elif evalue:
                 value = None
             else:
                 ok = 0
-        else:
+        elif expr is not None:
             evalue = self.engine.evaluateText(item[3])
             if evalue is self.Default:
-                if action == 1: # Cancelled insert
+                if action == 'insert': # Cancelled insert
                     ok = 0
             else:
                 if evalue is None:
                     ok = 0
                 value = evalue
+        else:
+            evalue = None
+
         if ok:
+            if xlat:
+                translated = self.translate(msgid or value, value, {})
+                if translated is not None:
+                    value = translated
             if value is None:
                 value = name
-            value = "%s=%s" % (name, quote(value))
+            elif evalue is self.Default:
+                value = attrEscape(value)
+            else:
+                value = escape(value, quote=1)
+            value = '%s="%s"' % (name, value)
         return ok, name, value
-
     bytecode_handlers["<attrAction>"] = attrAction
 
     def no_tag(self, start, program):
         state = self.saveState()
-        self.stream = stream = StringIO()
+        self.stream = stream = self.StringIO()
         self._stream_write = stream.write
         self.interpret(start)
         self.restoreOutputState(state)
@@ -328,7 +394,7 @@
                   omit=0):
         if tag_ns and not self.showtal:
             return self.no_tag(start, program)
-            
+
         self.interpret(start)
         if not isend:
             self.interpret(program)
@@ -345,18 +411,11 @@
             self.do_optTag(stuff)
     bytecode_handlers["optTag"] = do_optTag
 
-    def dumpMacroStack(self, prefix, suffix, value):
-        sys.stderr.write("+---- %s%s = %s\n" % (prefix, suffix, value))
-        for i in range(len(self.macroStack)):
-            what, macroName, slots = self.macroStack[i]
-            sys.stderr.write("| %2d. %-12s %-12s %s\n" %
-                             (i, what, macroName, slots and slots.keys()))
-        sys.stderr.write("+--------------------------------------\n")
-
     def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)):
         self._stream_write(s)
         self.col = col
-        self.do_setPosition(position)
+        self.position = position
+        self.engine.setPosition(position)
         if closeprev:
             engine = self.engine
             engine.endScope()
@@ -368,8 +427,9 @@
     def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
         self._stream_write(s)
         self.col = col
-        self.do_setPosition(position)
         engine = self.engine
+        self.position = position
+        engine.setPosition(position)
         if closeprev:
             engine.endScope()
             engine.beginScope()
@@ -406,6 +466,19 @@
         self.engine.setGlobal(name, self.engine.evaluateValue(expr))
     bytecode_handlers["setGlobal"] = do_setLocal
 
+    def do_beginI18nContext(self, settings):
+        get = settings.get
+        self.i18nContext = TranslationContext(self.i18nContext,
+                                              domain=get("domain"),
+                                              source=get("source"),
+                                              target=get("target"))
+    bytecode_handlers["beginI18nContext"] = do_beginI18nContext
+
+    def do_endI18nContext(self, notused=None):
+        self.i18nContext = self.i18nContext.parent
+        assert self.i18nContext is not None
+    bytecode_handlers["endI18nContext"] = do_endI18nContext
+
     def do_insertText(self, stuff):
         self.interpret(stuff[1])
 
@@ -425,6 +498,65 @@
             self.col = len(s) - (i + 1)
     bytecode_handlers["insertText"] = do_insertText
 
+    def do_i18nVariable(self, stuff):
+        varname, program, expression = stuff
+        if expression is None:
+            # The value is implicitly the contents of this tag, so we have to
+            # evaluate the mini-program to get the value of the variable.
+            state = self.saveState()
+            try:
+                tmpstream = self.StringIO()
+                self.interpretWithStream(program, tmpstream)
+                value = normalize(tmpstream.getvalue())
+            finally:
+                self.restoreState(state)
+        else:
+            # Evaluate the value to be associated with the variable in the
+            # i18n interpolation dictionary.
+            value = self.engine.evaluate(expression)
+        # Either the i18n:name tag is nested inside an i18n:translate in which
+        # case the last item on the stack has the i18n dictionary and string
+        # representation, or the i18n:name and i18n:translate attributes are
+        # in the same tag, in which case the i18nStack will be empty.  In that
+        # case we can just output the ${name} to the stream
+        i18ndict, srepr = self.i18nStack[-1]
+        i18ndict[varname] = value
+        placeholder = '${%s}' % varname
+        srepr.append(placeholder)
+        self._stream_write(placeholder)
+    bytecode_handlers['i18nVariable'] = do_i18nVariable
+
+    def do_insertTranslation(self, stuff):
+        i18ndict = {}
+        srepr = []
+        obj = None
+        self.i18nStack.append((i18ndict, srepr))
+        msgid = stuff[0]
+        # We need to evaluate the content of the tag because that will give us
+        # several useful pieces of information.  First, the contents will
+        # include an implicit message id, if no explicit one was given.
+        # Second, it will evaluate any i18nVariable definitions in the body of
+        # the translation (necessary for $varname substitutions).
+        #
+        # Use a temporary stream to capture the interpretation of the
+        # subnodes, which should /not/ go to the output stream.
+        tmpstream = self.StringIO()
+        self.interpretWithStream(stuff[1], tmpstream)
+        default = tmpstream.getvalue()
+        # We only care about the evaluated contents if we need an implicit
+        # message id.  All other useful information will be in the i18ndict on
+        # the top of the i18nStack.
+        if msgid == '':
+            msgid = normalize(default)
+        self.i18nStack.pop()
+        # See if there is was an i18n:data for msgid
+        if len(stuff) > 2:
+            obj = self.engine.evaluate(stuff[2])
+        xlated_msgid = self.translate(msgid, default, i18ndict, obj)
+        assert xlated_msgid is not None, self.position
+        self._stream_write(xlated_msgid)
+    bytecode_handlers['insertTranslation'] = do_insertTranslation
+
     def do_insertStructure(self, stuff):
         self.interpret(stuff[2])
 
@@ -435,7 +567,7 @@
         if structure is self.Default:
             self.interpret(block)
             return
-        text = str(structure)
+        text = ustr(structure)
         if not (repldict or self.strictinsert):
             # Take a shortcut, no error checking
             self.stream_write(text)
@@ -448,7 +580,7 @@
 
     def insertHTMLStructure(self, text, repldict):
         from HTMLTALParser import HTMLTALParser
-        gen = AltTALGenerator(repldict, self.engine, 0)
+        gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
         p = HTMLTALParser(gen) # Raises an exception if text is invalid
         p.parseString(text)
         program, macros = p.getCode()
@@ -456,7 +588,7 @@
 
     def insertXMLStructure(self, text, repldict):
         from TALParser import TALParser
-        gen = AltTALGenerator(repldict, self.engine, 0)
+        gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
         p = TALParser(gen)
         gen.enable(0)
         p.parseFragment('<!DOCTYPE foo PUBLIC "foo" "bar"><foo>')
@@ -476,6 +608,15 @@
             self.interpret(block)
     bytecode_handlers["loop"] = do_loop
 
+    def translate(self, msgid, default, i18ndict, obj=None):
+        if obj:
+            i18ndict.update(obj)
+        if not self.i18nInterpolate:
+            return msgid
+        # XXX We need to pass in one of context or target_language
+        return self.engine.translate(self.i18nContext.domain,
+                                     msgid, i18ndict, default=default)
+
     def do_rawtextColumn(self, (s, col)):
         self._stream_write(s)
         self.col = col
@@ -498,6 +639,7 @@
             if not entering:
                 macs.append(None)
                 self.interpret(macro)
+                assert macs[-1] is None
                 macs.pop()
                 return
         self.interpret(macro)
@@ -520,12 +662,11 @@
                 raise METALError("macro %s has incompatible mode %s" %
                                  (`macroName`, `mode`), self.position)
         self.pushMacro(macroName, compiledSlots)
-        saved_source = self.sourceFile
-        saved_position = self.position  # Used by Boa Constructor
+        prev_source = self.sourceFile
         self.interpret(macro)
-        if self.sourceFile != saved_source:
-            self.engine.setSourceFile(saved_source)
-            self.sourceFile = saved_source
+        if self.sourceFile != prev_source:
+            self.engine.setSourceFile(prev_source)
+            self.sourceFile = prev_source
         self.popMacro()
     bytecode_handlers["useMacro"] = do_useMacro
 
@@ -541,21 +682,18 @@
             return
         macs = self.macroStack
         if macs and macs[-1] is not None:
-            saved_source = self.sourceFile
-            saved_position = self.position  # Used by Boa Constructor
             macroName, slots = self.popMacro()[:2]
             slot = slots.get(slotName)
             if slot is not None:
+                prev_source = self.sourceFile
                 self.interpret(slot)
-                if self.sourceFile != saved_source:
-                    self.engine.setSourceFile(saved_source)
-                    self.sourceFile = saved_source
+                if self.sourceFile != prev_source:
+                    self.engine.setSourceFile(prev_source)
+                    self.sourceFile = prev_source
                 self.pushMacro(macroName, slots, entering=0)
                 return
             self.pushMacro(macroName, slots)
-            if len(macs) == 1:
-                self.interpret(block)
-                return
+            # Falling out of the 'if' allows the macro to be interpreted.
         self.interpret(block)
     bytecode_handlers["defineSlot"] = do_defineSlot
 
@@ -564,7 +702,7 @@
 
     def do_onError_tal(self, (block, handler)):
         state = self.saveState()
-        self.stream = stream = StringIO()
+        self.stream = stream = self.StringIO()
         self._stream_write = stream.write
         try:
             self.interpret(block)
@@ -597,24 +735,24 @@
     bytecode_handlers_tal["optTag"] = do_optTag_tal
 
 
-def test():
-    from driver import FILE, parsefile
-    from DummyEngine import DummyEngine
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], "")
-    except getopt.error, msg:
-        print msg
-        sys.exit(2)
-    if args:
-        file = args[0]
-    else:
-        file = FILE
-    doc = parsefile(file)
-    compiler = TALCompiler(doc)
-    program, macros = compiler()
-    engine = DummyEngine()
-    interpreter = TALInterpreter(program, macros, engine)
-    interpreter()
+class FasterStringIO(StringIO):
+    """Append-only version of StringIO.
+
+    This let's us have a much faster write() method.
+    """
+    def close(self):
+        if not self.closed:
+            self.write = _write_ValueError
+            StringIO.close(self)
 
-if __name__ == "__main__":
-    test()
+    def seek(self, pos, mode=0):
+        raise RuntimeError("FasterStringIO.seek() not allowed")
+
+    def write(self, s):
+        #assert self.pos == self.len
+        self.buflist.append(s)
+        self.len = self.pos = self.pos + len(s)
+
+
+def _write_ValueError(s):
+    raise ValueError, "I/O operation on closed file"
--- a/roundup/cgi/TAL/TALParser.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/TALParser.py	Fri May 21 05:36:30 2004 +0000
@@ -2,22 +2,21 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-# 
+# FOR A PARTICULAR PURPOSE.
+#
 ##############################################################################
-"""Parse XML and compile to TALInterpreter intermediate code.
 """
-__docformat__ = 'restructuredtext'
+Parse XML and compile to TALInterpreter intermediate code.
+"""
 
-import string
 from XMLParser import XMLParser
-from TALDefs import *
+from TALDefs import XML_NS, ZOPE_I18N_NS, ZOPE_METAL_NS, ZOPE_TAL_NS
 from TALGenerator import TALGenerator
 
 class TALParser(XMLParser):
@@ -59,13 +58,15 @@
             # attrs is a dict of {name: value}
             attrlist = attrs.items()
             attrlist.sort() # For definiteness
-        name, attrlist, taldict, metaldict = self.process_ns(name, attrlist)
+        name, attrlist, taldict, metaldict, i18ndict \
+              = self.process_ns(name, attrlist)
         attrlist = self.xmlnsattrs() + attrlist
-        self.gen.emitStartElement(name, attrlist, taldict, metaldict)
+        self.gen.emitStartElement(name, attrlist, taldict, metaldict, i18ndict)
 
     def process_ns(self, name, attrlist):
         taldict = {}
         metaldict = {}
+        i18ndict = {}
         fixedattrlist = []
         name, namebase, namens = self.fixname(name)
         for key, value in attrlist:
@@ -78,10 +79,14 @@
             elif ns == 'tal':
                 taldict[keybase] = value
                 item = item + ("tal",)
+            elif ns == 'i18n':
+                assert 0, "dealing with i18n: " + `(keybase, value)`
+                i18ndict[keybase] = value
+                item = item + ('i18n',)
             fixedattrlist.append(item)
-        if namens in ('metal', 'tal'):
+        if namens in ('metal', 'tal', 'i18n'):
             taldict['tal tag'] = namens
-        return name, fixedattrlist, taldict, metaldict
+        return name, fixedattrlist, taldict, metaldict, i18ndict
 
     def xmlnsattrs(self):
         newlist = []
@@ -90,7 +95,7 @@
                 key = "xmlns:" + prefix
             else:
                 key = "xmlns"
-            if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS):
+            if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS):
                 item = (key, uri, "xmlns")
             else:
                 item = (key, uri)
@@ -100,7 +105,7 @@
 
     def fixname(self, name):
         if ' ' in name:
-            uri, name = string.split(name, ' ')
+            uri, name = name.split(' ')
             prefix = self.nsDict[uri]
             prefixed = name
             if prefix:
@@ -110,6 +115,8 @@
                 ns = 'tal'
             elif uri == ZOPE_METAL_NS:
                 ns = 'metal'
+            elif uri == ZOPE_I18N_NS:
+                ns = 'i18n'
             return (prefixed, name, ns)
         return (name, name, None)
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/roundup/cgi/TAL/TranslationContext.py	Fri May 21 05:36:30 2004 +0000
@@ -0,0 +1,41 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Translation context object for the TALInterpreter's I18N support.
+
+The translation context provides a container for the information
+needed to perform translation of a marked string from a page template.
+
+$Id: TranslationContext.py,v 1.1 2004-05-21 05:36:30 richard Exp $
+"""
+
+DEFAULT_DOMAIN = "default"
+
+class TranslationContext:
+    """Information about the I18N settings of a TAL processor."""
+
+    def __init__(self, parent=None, domain=None, target=None, source=None):
+        if parent:
+            if not domain:
+                domain = parent.domain
+            if not target:
+                target = parent.target
+            if not source:
+                source = parent.source
+        elif domain is None:
+            domain = DEFAULT_DOMAIN
+
+        self.parent = parent
+        self.domain = domain
+        self.target = target
+        self.source = source
--- a/roundup/cgi/TAL/XMLParser.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/XMLParser.py	Fri May 21 05:36:30 2004 +0000
@@ -2,23 +2,22 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE
-# 
+#
 ##############################################################################
-"""Generic expat-based XML parser base class.
+# Modifications for Roundup:
+# 1. commented out zLOG references
+"""
+Generic expat-based XML parser base class.
+"""
 
-Modified for Roundup 0.5 release:
-
-- removed dependency on zLOG
-
-"""
-__docformat__ = 'restructuredtext'
+#import zLOG
 
 class XMLParser:
 
--- a/roundup/cgi/TAL/__init__.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/__init__.py	Fri May 21 05:36:30 2004 +0000
@@ -2,14 +2,13 @@
 #
 # Copyright (c) 2001, 2002 Zope Corporation and Contributors.
 # All Rights Reserved.
-# 
+#
 # This software is subject to the provisions of the Zope Public License,
 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
 # FOR A PARTICULAR PURPOSE
-# 
+#
 ##############################################################################
-"""Template Attribute Language package """
-__docformat__ = 'restructuredtext'
+""" Template Attribute Language package """
--- a/roundup/cgi/TAL/markupbase.py	Thu May 20 23:16:58 2004 +0000
+++ b/roundup/cgi/TAL/markupbase.py	Fri May 21 05:36:30 2004 +0000
@@ -1,9 +1,6 @@
-"""Shared support for scanning document type declarations in HTML and XHTML.
-"""
-__docformat__ = 'restructuredtext'
+"""Shared support for scanning document type declarations in HTML and XHTML."""
 
-import re
-import string
+import re, string
 
 _declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match
 _declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match
@@ -23,6 +20,13 @@
         """Return current line number and offset."""
         return self.lineno, self.offset
 
+    def error(self, message):
+        """Return an error, showing current line number and offset.
+
+        Concrete subclasses *must* override this method.
+        """
+        raise NotImplementedError
+
     # Internal -- update line number and offset.  This should be
     # called for each piece of data exactly once, in order -- in other
     # words the concatenation of all the input strings to this
@@ -31,10 +35,10 @@
         if i >= j:
             return j
         rawdata = self.rawdata
-        nlines = string.count(rawdata, "\n", i, j)
+        nlines = rawdata.count("\n", i, j)
         if nlines:
             self.lineno = self.lineno + nlines
-            pos = string.rindex(rawdata, "\n", i, j) # Should not fail
+            pos = rawdata.rindex("\n", i, j) # Should not fail
             self.offset = j-(pos+1)
         else:
             self.offset = self.offset + j-i
@@ -171,7 +175,7 @@
             return -1
         # style content model; just skip until '>'
         if '>' in rawdata[j:]:
-            return string.find(rawdata, ">", j) + 1
+            return rawdata.find(">", j) + 1
         return -1
 
     # Internal -- scan past <!ATTLIST declarations
@@ -195,10 +199,10 @@
             if c == "(":
                 # an enumerated type; look for ')'
                 if ")" in rawdata[j:]:
-                    j = string.find(rawdata, ")", j) + 1
+                    j = rawdata.find(")", j) + 1
                 else:
                     return -1
-                while rawdata[j:j+1] in string.whitespace:
+                while rawdata[j:j+1].isspace():
                     j = j + 1
                 if not rawdata[j:]:
                     # end of buffer, incomplete
@@ -299,10 +303,10 @@
         m = _declname_match(rawdata, i)
         if m:
             s = m.group()
-            name = string.strip(s)
+            name = s.strip()
             if (i + len(s)) == n:
                 return None, -1  # end of buffer
-            return string.lower(name), m.end()
+            return name.lower(), m.end()
         else:
             self.updatepos(declstartpos, i)
-            self.error("expected name token", self.getpos())
+            self.error("expected name token")
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/roundup/cgi/TAL/talgettext.py	Fri May 21 05:36:30 2004 +0000
@@ -0,0 +1,313 @@
+#!/usr/bin/env python
+##############################################################################
+#
+# Copyright (c) 2002 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+# Modifications for Roundup:
+# 1. commented out ITALES references
+
+"""Program to extract internationalization markup from Page Templates.
+
+Once you have marked up a Page Template file with i18n: namespace tags, use
+this program to extract GNU gettext .po file entries.
+
+Usage: talgettext.py [options] files
+Options:
+    -h / --help
+        Print this message and exit.
+    -o / --output <file>
+        Output the translation .po file to <file>.
+    -u / --update <file>
+        Update the existing translation <file> with any new translation strings
+        found.
+"""
+
+import sys
+import time
+import getopt
+import traceback
+
+from TAL.HTMLTALParser import HTMLTALParser
+from TAL.TALInterpreter import TALInterpreter
+from TAL.DummyEngine import DummyEngine
+#from ITALES import ITALESEngine
+from TAL.TALDefs import TALESError
+
+__version__ = '$Revision: 1.1 $'
+
+pot_header = '''\
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\\n"
+"POT-Creation-Date: %(time)s\\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"
+"Language-Team: LANGUAGE <LL@li.org>\\n"
+"MIME-Version: 1.0\\n"
+"Content-Type: text/plain; charset=CHARSET\\n"
+"Content-Transfer-Encoding: ENCODING\\n"
+"Generated-By: talgettext.py %(version)s\\n"
+'''
+
+NLSTR = '"\n"'
+
+try:
+    True
+except NameError:
+    True=1
+    False=0
+
+def usage(code, msg=''):
+    # Python 2.1 required
+    print >> sys.stderr, __doc__
+    if msg:
+        print >> sys.stderr, msg
+    sys.exit(code)
+
+
+class POTALInterpreter(TALInterpreter):
+    def translate(self, msgid, default, i18ndict=None, obj=None):
+        # XXX is this right?
+        if i18ndict is None:
+            i18ndict = {}
+        if obj:
+            i18ndict.update(obj)
+        # XXX Mmmh, it seems that sometimes the msgid is None; is that really
+        # possible?
+        if msgid is None:
+            return None
+        # XXX We need to pass in one of context or target_language
+        return self.engine.translate(msgid, self.i18nContext.domain, i18ndict,
+                                     position=self.position, default=default)
+
+
+class POEngine(DummyEngine):
+    #__implements__ = ITALESEngine
+
+    def __init__(self, macros=None):
+        self.catalog = {}
+        DummyEngine.__init__(self, macros)
+
+    def evaluate(*args):
+        return '' # who cares
+
+    def evaluatePathOrVar(*args):
+        return '' # who cares
+
+    def evaluateSequence(self, expr):
+        return (0,) # dummy
+
+    def evaluateBoolean(self, expr):
+        return True # dummy
+
+    def translate(self, msgid, domain=None, mapping=None, default=None,
+                  # XXX position is not part of the ITALESEngine
+                  #     interface
+                  position=None):
+
+        if domain not in self.catalog:
+            self.catalog[domain] = {}
+        domain = self.catalog[domain]
+
+        if msgid not in domain:
+            domain[msgid] = []
+        domain[msgid].append((self.file, position))
+        return 'x'
+
+
+class UpdatePOEngine(POEngine):
+    """A slightly-less braindead POEngine which supports loading an existing
+    .po file first."""
+
+    def __init__ (self, macros=None, filename=None):
+        POEngine.__init__(self, macros)
+
+        self._filename = filename
+        self._loadFile()
+        self.base = self.catalog
+        self.catalog = {}
+
+    def __add(self, id, s, fuzzy):
+        "Add a non-fuzzy translation to the dictionary."
+        if not fuzzy and str:
+            # check for multi-line values and munge them appropriately
+            if '\n' in s:
+                lines = s.rstrip().split('\n')
+                s = NLSTR.join(lines)
+            self.catalog[id] = s
+
+    def _loadFile(self):
+        # shamelessly cribbed from Python's Tools/i18n/msgfmt.py
+        # 25-Mar-2003 Nathan R. Yergler (nathan@zope.org)
+        # 14-Apr-2003 Hacked by Barry Warsaw (barry@zope.com)
+
+        ID = 1
+        STR = 2
+
+        try:
+            lines = open(self._filename).readlines()
+        except IOError, msg:
+            print >> sys.stderr, msg
+            sys.exit(1)
+
+        section = None
+        fuzzy = False
+
+        # Parse the catalog
+        lno = 0
+        for l in lines:
+            lno += True
+            # If we get a comment line after a msgstr, this is a new entry
+            if l[0] == '#' and section == STR:
+                self.__add(msgid, msgstr, fuzzy)
+                section = None
+                fuzzy = False
+            # Record a fuzzy mark
+            if l[:2] == '#,' and l.find('fuzzy'):
+                fuzzy = True
+            # Skip comments
+            if l[0] == '#':
+                continue
+            # Now we are in a msgid section, output previous section
+            if l.startswith('msgid'):
+                if section == STR:
+                    self.__add(msgid, msgstr, fuzzy)
+                section = ID
+                l = l[5:]
+                msgid = msgstr = ''
+            # Now we are in a msgstr section
+            elif l.startswith('msgstr'):
+                section = STR
+                l = l[6:]
+            # Skip empty lines
+            if not l.strip():
+                continue
+            # XXX: Does this always follow Python escape semantics?
+            l = eval(l)
+            if section == ID:
+                msgid += l
+            elif section == STR:
+                msgstr += '%s\n' % l
+            else:
+                print >> sys.stderr, 'Syntax error on %s:%d' % (infile, lno), \
+                      'before:'
+                print >> sys.stderr, l
+                sys.exit(1)
+        # Add last entry
+        if section == STR:
+            self.__add(msgid, msgstr, fuzzy)
+
+    def evaluate(self, expression):
+        try:
+            return POEngine.evaluate(self, expression)
+        except TALESError:
+            pass
+
+    def evaluatePathOrVar(self, expr):
+        return 'who cares'
+
+    def translate(self, msgid, domain=None, mapping=None, default=None,
+                  position=None):
+        if msgid not in self.base:
+            POEngine.translate(self, msgid, domain, mapping, default, position)
+        return 'x'
+
+
+def main():
+    try:
+        opts, args = getopt.getopt(
+            sys.argv[1:],
+            'ho:u:',
+            ['help', 'output=', 'update='])
+    except getopt.error, msg:
+        usage(1, msg)
+
+    outfile = None
+    engine = None
+    update_mode = False
+    for opt, arg in opts:
+        if opt in ('-h', '--help'):
+            usage(0)
+        elif opt in ('-o', '--output'):
+            outfile = arg
+        elif opt in ('-u', '--update'):
+            update_mode = True
+            if outfile is None:
+                outfile = arg
+            engine = UpdatePOEngine(filename=arg)
+
+    if not args:
+        print 'nothing to do'
+        return
+
+    # We don't care about the rendered output of the .pt file
+    class Devnull:
+        def write(self, s):
+            pass
+
+    # check if we've already instantiated an engine;
+    # if not, use the stupidest one available
+    if not engine:
+        engine = POEngine()
+
+    # process each file specified
+    for filename in args:
+        try:
+            engine.file = filename
+            p = HTMLTALParser()
+            p.parseFile(filename)
+            program, macros = p.getCode()
+            POTALInterpreter(program, macros, engine, stream=Devnull(),
+                             metal=False)()
+        except: # Hee hee, I love bare excepts!
+            print 'There was an error processing', filename
+            traceback.print_exc()
+
+    # Now output the keys in the engine.  Write them to a file if --output or
+    # --update was specified; otherwise use standard out.
+    if (outfile is None):
+        outfile = sys.stdout
+    else:
+        outfile = file(outfile, update_mode and "a" or "w")
+
+    catalog = {}
+    for domain in engine.catalog.keys():
+        catalog.update(engine.catalog[domain])
+
+    messages = catalog.copy()
+    try:
+        messages.update(engine.base)
+    except AttributeError:
+        pass
+    if '' not in messages:
+        print >> outfile, pot_header % {'time': time.ctime(),
+                                        'version': __version__}
+
+    msgids = catalog.keys()
+    # XXX: You should not sort by msgid, but by filename and position. (SR)
+    msgids.sort()
+    for msgid in msgids:
+        positions = catalog[msgid]
+        for filename, position in positions:
+            outfile.write('#: %s:%s\n' % (filename, position[0]))
+
+        outfile.write('msgid "%s"\n' % msgid)
+        outfile.write('msgstr ""\n')
+        outfile.write('\n')
+
+
+if __name__ == '__main__':
+    main()

Roundup Issue Tracker: http://roundup-tracker.org/