[Zope-Checkins] CVS: Zope/lib/python/TAL - ITALES.py:1.2 TranslationContext.py:1.2 DummyEngine.py:1.33 HTMLTALParser.py:1.35 TALDefs.py:1.29 TALGenerator.py:1.56 TALInterpreter.py:1.71 TALParser.py:1.20 XMLParser.py:1.10 driver.py:1.29 runtest.py:1.23
Florent Guillaume
fg@nuxeo.com
Wed, 18 Sep 2002 11:12:49 -0400
Update of /cvs-repository/Zope/lib/python/TAL
In directory cvs.zope.org:/tmp/cvs-serv23968/lib/python/TAL
Modified Files:
DummyEngine.py HTMLTALParser.py TALDefs.py TALGenerator.py
TALInterpreter.py TALParser.py XMLParser.py driver.py
runtest.py
Added Files:
ITALES.py TranslationContext.py
Log Message:
Merge of the Zope-2_6-i18n-branch into HEAD.
Impacted code:
- TAL: merge of the 2.7 i18n stuff, unicode fixes, tests.
- PageTemplates: addition of a global translation service and of its use
by the TALES engine, unicode fixes, tests.
- StructuredText: unicode fixes, tests.
=== Zope/lib/python/TAL/ITALES.py 1.1 => 1.2 ===
--- /dev/null Wed Sep 18 11:12:48 2002
+++ Zope/lib/python/TAL/ITALES.py Wed Sep 18 11:12:48 2002
@@ -0,0 +1,153 @@
+"""Interface that a TALES engine provides to the METAL/TAL implementation."""
+
+try:
+ from Interface import Interface
+ from Interface.Attribute import Attribute
+except:
+ # Before 2.7
+ class Interface: pass
+ def Attribute(*args): pass
+
+
+class ITALESCompiler(Interface):
+ """Compile-time interface provided by a TALES implementation.
+
+ The TAL compiler needs an instance of this interface to support
+ compilation of TALES expressions embedded in documents containing
+ TAL and METAL constructs.
+ """
+
+ def getCompilerError():
+ """Return the exception class raised for compilation errors.
+ """
+
+ def compile(expression):
+ """Return a compiled form of 'expression' for later evaluation.
+
+ 'expression' is the source text of the expression.
+
+ The return value may be passed to the various evaluate*()
+ methods of the ITALESEngine interface. No compatibility is
+ required for the values of the compiled expression between
+ different ITALESEngine implementations.
+ """
+
+
+class ITALESEngine(Interface):
+ """Render-time interface provided by a TALES implementation.
+
+ The TAL interpreter uses this interface to TALES to support
+ evaluation of the compiled expressions returned by
+ ITALESCompiler.compile().
+ """
+
+ def getDefault():
+ """Return the value of the 'default' TALES expression.
+
+ Checking a value for a match with 'default' should be done
+ using the 'is' operator in Python.
+ """
+
+ def setPosition((lineno, offset)):
+ """Inform the engine of the current position in the source file.
+
+ This is used to allow the evaluation engine to report
+ execution errors so that site developers can more easily
+ locate the offending expression.
+ """
+
+ def setSourceFile(filename):
+ """Inform the engine of the name of the current source file.
+
+ This is used to allow the evaluation engine to report
+ execution errors so that site developers can more easily
+ locate the offending expression.
+ """
+
+ def beginScope():
+ """Push a new scope onto the stack of open scopes.
+ """
+
+ def endScope():
+ """Pop one scope from the stack of open scopes.
+ """
+
+ def evaluate(compiled_expression):
+ """Evaluate an arbitrary expression.
+
+ No constraints are imposed on the return value.
+ """
+
+ def evaluateBoolean(compiled_expression):
+ """Evaluate an expression that must return a Boolean value.
+ """
+
+ def evaluateMacro(compiled_expression):
+ """Evaluate an expression that must return a macro program.
+ """
+
+ def evaluateStructure(compiled_expression):
+ """Evaluate an expression that must return a structured
+ document fragment.
+
+ The result of evaluating 'compiled_expression' must be a
+ string containing a parsable HTML or XML fragment. Any TAL
+ markup cnotained in the result string will be interpreted.
+ """
+
+ def evaluateText(compiled_expression):
+ """Evaluate an expression that must return text.
+
+ The returned text should be suitable for direct inclusion in
+ the output: any HTML or XML escaping or quoting is the
+ responsibility of the expression itself.
+ """
+
+ def evaluateValue(compiled_expression):
+ """Evaluate an arbitrary expression.
+
+ No constraints are imposed on the return value.
+ """
+
+ def createErrorInfo(exception, (lineno, offset)):
+ """Returns an ITALESErrorInfo object.
+
+ The returned object is used to provide information about the
+ error condition for the on-error handler.
+ """
+
+ def setGlobal(name, value):
+ """Set a global variable.
+
+ The variable will be named 'name' and have the value 'value'.
+ """
+
+ def setLocal(name, value):
+ """Set a local variable in the current scope.
+
+ The variable will be named 'name' and have the value 'value'.
+ """
+
+ def setRepeat(name, compiled_expression):
+ """
+ """
+
+ def translate(domain, msgid, mapping):
+ """
+ See ITranslationService.translate()
+ """
+
+
+class ITALESErrorInfo(Interface):
+
+ type = Attribute("type",
+ "The exception class.")
+
+ value = Attribute("value",
+ "The exception instance.")
+
+ lineno = Attribute("lineno",
+ "The line number the error occurred on in the source.")
+
+ offset = Attribute("offset",
+ "The character offset at which the error occurred.")
=== Zope/lib/python/TAL/TranslationContext.py 1.1 => 1.2 ===
--- /dev/null Wed Sep 18 11:12:49 2002
+++ Zope/lib/python/TAL/TranslationContext.py Wed Sep 18 11:12:48 2002
@@ -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$
+"""
+
+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
=== Zope/lib/python/TAL/DummyEngine.py 1.32 => 1.33 ===
--- Zope/lib/python/TAL/DummyEngine.py:1.32 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/DummyEngine.py Wed Sep 18 11:12:48 2002
@@ -8,7 +8,7 @@
# 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.
#
##############################################################################
"""
@@ -18,13 +18,20 @@
import re
import sys
-import driver
-
from TALDefs import NAME_RE, TALESError, ErrorInfo
+from ITALES import ITALESCompiler, ITALESEngine
+from DocumentTemplate.DT_Util import ustr
+try:
+ from Zope.I18n.ITranslationService import ITranslationService
+ from Zope.I18n.IDomain import IDomain
+except ImportError:
+ # Before 2.7
+ class ITranslationService: pass
+ class IDomain: pass
-class Default:
+class _Default:
pass
-Default = Default()
+Default = _Default()
name_match = re.compile(r"(?s)(%s):(.*)\Z" % NAME_RE).match
@@ -36,6 +43,8 @@
position = None
source_file = None
+ __implements__ = ITALESCompiler, ITALESEngine
+
def __init__(self, macros=None):
if macros is None:
macros = {}
@@ -43,6 +52,7 @@
dict = {'nothing': None, 'default': Default}
self.locals = self.globals = dict
self.stack = [dict]
+ self.translationService = DummyTranslationService()
def getCompilerError(self):
return CompilerError
@@ -90,13 +100,7 @@
if type in ("string", "str"):
return expr
if type in ("path", "var", "global", "local"):
- 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`)
+ return self.evaluatePathOrVar(expr)
if type == "not":
return not self.evaluate(expr)
if type == "exists":
@@ -116,6 +120,15 @@
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)
@@ -125,7 +138,7 @@
def evaluateText(self, expr):
text = self.evaluate(expr)
if text is not None and text is not Default:
- text = str(text)
+ text = ustr(text)
return text
def evaluateStructure(self, expr):
@@ -146,6 +159,7 @@
macro = self.macros[localName]
else:
# External macro
+ import driver
program, macros = driver.compilefile(file)
macro = macros.get(localName)
if not macro:
@@ -157,6 +171,7 @@
file, localName = self.findMacroFile(macroName)
if not file:
return file, localName
+ import driver
doc = driver.parsefile(file)
return doc, localName
@@ -183,6 +198,10 @@
def getDefault(self):
return Default
+ def translate(self, domain, msgid, mapping):
+ return self.translationService.translate(domain, msgid, mapping)
+
+
class Iterator:
def __init__(self, name, seq, engine):
@@ -200,3 +219,31 @@
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):
+ # 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.
+ def repl(m, mapping=mapping):
+ return mapping[m.group(m.lastindex).lower()]
+ cre = re.compile(r'\$(?:([_A-Z]\w*)|\{([_A-Z]\w*)\})')
+ return cre.sub(repl, msgid.upper())
+
+class DummyTranslationService:
+ __implements__ = ITranslationService
+
+ def translate(self, domain, msgid, mapping=None, context=None,
+ target_language=None):
+ # Ignore domain
+ return self.getDomain(domain).translate(msgid, mapping, context,
+ target_language)
+
+ def getDomain(self, domain):
+ return DummyDomain()
=== Zope/lib/python/TAL/HTMLTALParser.py 1.34 => 1.35 ===
--- Zope/lib/python/TAL/HTMLTALParser.py:1.34 Tue Aug 27 14:31:17 2002
+++ Zope/lib/python/TAL/HTMLTALParser.py Wed Sep 18 11:12:48 2002
@@ -8,7 +8,7 @@
# 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.
#
##############################################################################
"""
@@ -18,8 +18,9 @@
import sys
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
@@ -106,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)
@@ -132,9 +140,10 @@
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)
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)
@@ -142,14 +151,15 @@
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"):
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):
@@ -252,7 +262,7 @@
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)
@@ -260,20 +270,20 @@
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
key, keybase, keyns = self.fixname(key)
- if ':' in key and not keyns:
- ns = 0
- else:
- ns = keyns or namens # default to tag namespace
+ ns = keyns or namens # default to tag namespace
if ns and ns != 'unknown':
item = (key, value, ns)
if ns == 'tal':
@@ -286,7 +296,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
=== Zope/lib/python/TAL/TALDefs.py 1.28 => 1.29 ===
--- Zope/lib/python/TAL/TALDefs.py:1.28 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALDefs.py Wed Sep 18 11:12:48 2002
@@ -8,7 +8,7 @@
# 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.
#
##############################################################################
"""
@@ -17,13 +17,16 @@
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_]*"
@@ -32,7 +35,6 @@
"use-macro",
"define-slot",
"fill-slot",
- "slot",
]
KNOWN_TAL_ATTRIBUTES = [
@@ -47,6 +49,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 +66,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 +77,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 +87,13 @@
class TALESError(TALError):
pass
+class I18NError(TALError):
+ pass
+
class ErrorInfo:
+
+ __implements__ = ITALESErrorInfo
def __init__(self, err, position=(None, None)):
if isinstance(err, Exception):
=== Zope/lib/python/TAL/TALGenerator.py 1.55 => 1.56 ===
--- Zope/lib/python/TAL/TALGenerator.py:1.55 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALGenerator.py Wed Sep 18 11:12:48 2002
@@ -8,7 +8,7 @@
# 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.
#
##############################################################################
"""
@@ -18,7 +18,16 @@
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
class TALGenerator:
@@ -32,8 +41,15 @@
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 = {}
@@ -44,6 +60,7 @@
if source_file is not None:
self.source_file = source_file
self.emit("setSourceFile", source_file)
+ self.i18nContext = TranslationContext()
def getCode(self):
assert not self.stack
@@ -82,6 +99,12 @@
# instructions to be joined together.
output.append(self.optimizeArgsList(item))
continue
+ 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 = text.rfind("\n")
@@ -102,9 +125,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
@@ -115,18 +157,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.quote(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)
@@ -222,7 +261,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:
@@ -274,6 +313,49 @@
assert key == "structure"
self.emit("insertStructure", cexpr, attrDict, program)
+ def emitI18nVariable(self, varname, action, expression):
+ # 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" />"
+ 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 = macroName.strip()
@@ -361,23 +443,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 = repldict[key]
+ item = item[:2] + ("replace", expr, xlat)
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) in repldict.items():
+ newlist.append((key, None, "insert", expr, xlat))
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({})
@@ -387,18 +476,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")
@@ -413,13 +508,31 @@
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 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:
@@ -437,8 +550,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()
@@ -455,13 +568,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]
@@ -487,16 +616,43 @@
if content:
todo["content"] = content
if replace:
- todo["replace"] = 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, 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, None)
+ self.pushProgram()
+ if msgid is not None:
+ 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)
+ else:
+ repldict = {}
+ if i18nattrs:
+ i18nattrs = i18nattrs.split()
+ 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)
+ repldict[key] = self.compileExpression(value), key in i18nattrs
+ for key in i18nattrs:
+ if not repldict.has_key(key):
+ repldict[key] = None, 1
else:
repldict = {}
if replace:
@@ -507,6 +663,8 @@
self.pushProgram()
if content:
self.pushProgram()
+ if msgid is not None:
+ self.pushProgram()
if todo and position != (None, None):
todo["position"] = position
self.todoPush(todo)
@@ -535,6 +693,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:
@@ -546,14 +708,51 @@
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.
+ i18nNameAction = I18N_REPLACE
if content:
+ if varname:
+ i18nNameAction = I18N_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).
+ if msgid is not None and not varname:
+ self.emitTranslation(msgid, i18ndata)
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:
+ if varname[1] is not None:
+ i18nNameAction = I18N_EXPRESSION
+ # o varname[0] is the variable name
+ # o i18nNameAction is either
+ # - I18N_REPLACE for implicit tal:replace
+ # - I18N_CONTENT for tal:content
+ # - I18N_EXPRESSION for explicit tal:replace
+ # o varname[1] will be None for the first two actions and the
+ # replacement tal expression for the third action.
+ self.emitI18nVariable(varname[0], i18nNameAction, varname[1])
+ # 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 and varname:
+ self.emitTranslation(msgid, i18ndata)
if repeat:
self.emitRepeat(repeat)
if condition:
@@ -562,6 +761,10 @@
self.emitOnError(name, onError)
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:
=== Zope/lib/python/TAL/TALInterpreter.py 1.70 => 1.71 ===
--- Zope/lib/python/TAL/TALInterpreter.py:1.70 Sun Sep 8 22:16:26 2002
+++ Zope/lib/python/TAL/TALInterpreter.py Wed Sep 18 11:12:48 2002
@@ -8,7 +8,7 @@
# 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.
#
##############################################################################
"""
@@ -17,17 +17,17 @@
import sys
import getopt
-
+import re
+from types import ListType
from cgi import escape
-
-try:
- from cStringIO import StringIO
-except ImportError:
- from StringIO import StringIO
+# Do not use cStringIO here! It's not unicode aware. :(
+from StringIO import StringIO
+from DocumentTemplate.DT_Util import ustr
from TALDefs import quote, TAL_VERSION, TALError, METALError
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 +40,12 @@
"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())
+
class AltTALGenerator(TALGenerator):
@@ -62,14 +61,16 @@
if self.enabled:
apply(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 +83,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,12 +108,14 @@
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
@@ -121,19 +124,20 @@
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
@@ -142,7 +146,12 @@
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 popMacro(self):
+ stuff = self.macroStack.pop()
+ self.i18nContext = stuff[3]
+ return stuff
def macroContext(self, what):
macroStack = self.macroStack
@@ -156,9 +165,11 @@
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
@@ -174,15 +185,20 @@
bytecode_handlers = {}
- def interpret(self, program, None=None):
+ def interpret(self, program, tmpstream=None):
oldlevel = self.level
self.level = oldlevel + 1
handlers = self.dispatch
+ if tmpstream:
+ ostream = self.stream
+ owrite = self._stream_write
+ self.stream = tmpstream
+ self._stream_write = tmpstream.write
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)
@@ -192,6 +208,9 @@
handlers[opcode](self, args)
finally:
self.level = oldlevel
+ if tmpstream:
+ self.stream = ostream
+ self._stream_write = owrite
def do_version(self, version):
assert version == TAL_VERSION
@@ -229,8 +248,7 @@
# for that case.
_stream_write = self._stream_write
_stream_write("<" + name)
- namelen = _len(name)
- col = self.col + namelen + 1
+ col = self.col + _len(name) + 1
wrap = self.wrap
align = col + 1
if align >= wrap/2:
@@ -262,10 +280,11 @@
def attrAction(self, item):
name, value, action = item[:3]
- if action == 1 or (action > 1 and not self.showtal):
+ if action == 'insert' or (action in ('metal', 'tal', 'xmlns', 'i18n')
+ and not self.showtal):
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
@@ -293,33 +312,43 @@
def attrAction_tal(self, item):
name, value, action = item[:3]
- if action > 1:
+ if action in ('metal', 'tal', 'xmlns', 'i18n'):
return self.attrAction(item)
ok = 1
+ expr, 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:
- evalue = self.engine.evaluateText(item[3])
- if evalue is self.Default:
- if action == 1: # Cancelled insert
- ok = 0
- else:
- if evalue is None:
- ok = 0
- value = evalue
+ if expr is not None:
+ evalue = self.engine.evaluateText(item[3])
+ if evalue is self.Default:
+ if action == 'insert': # Cancelled insert
+ ok = 0
+ else:
+ if evalue is None:
+ ok = 0
+ value = evalue
if ok:
+ if msgid:
+ value = self.i18n_attribute(value)
if value is None:
value = name
value = "%s=%s" % (name, quote(value))
return ok, name, value
+ def i18n_attribute(self, s):
+ # s is the value of an attribute before translation
+ # it may have been computed
+ return self.translate(s, {})
+
+
bytecode_handlers["<attrAction>"] = attrAction
def no_tag(self, start, program):
@@ -334,7 +363,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)
@@ -354,7 +383,7 @@
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]
+ what, macroName, slots = self.macroStack[i][:3]
sys.stderr.write("| %2d. %-12s %-12s %s\n" %
(i, what, macroName, slots and slots.keys()))
sys.stderr.write("+--------------------------------------\n")
@@ -412,6 +441,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])
@@ -431,6 +473,72 @@
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.interpret(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.interpret(stuff[1], tmpstream)
+ # 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(tmpstream.getvalue())
+ 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, i18ndict, obj)
+ # XXX I can't decide whether we want to cgi escape the translated
+ # string or not. OT1H not doing this could introduce a cross-site
+ # scripting vector by allowing translators to sneak JavaScript into
+ # translations. OTOH, for implicit interpolation values, we don't
+ # want to escape stuff like ${name} <= "<b>Timmy</b>".
+ #s = escape(xlated_msgid)
+ s = xlated_msgid
+ # If there are i18n variables to interpolate into this string, better
+ # do it now.
+ self._stream_write(s)
+ bytecode_handlers['insertTranslation'] = do_insertTranslation
+
def do_insertStructure(self, stuff):
self.interpret(stuff[2])
@@ -441,7 +549,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)
@@ -482,6 +590,24 @@
self.interpret(block)
bytecode_handlers["loop"] = do_loop
+ def translate(self, msgid, i18ndict=None, obj=None):
+ # XXX is this right?
+ if i18ndict is None:
+ i18ndict = {}
+ if obj:
+ i18ndict.update(obj)
+ # XXX need to fill this in with TranslationService calls. For now,
+ # we'll just do simple interpolation based on a $-strings to %-strings
+ # algorithm in Mailman.
+ if not self.i18nInterpolate:
+ return msgid
+ # 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(self.i18nContext.domain, msgid, i18ndict)
+
def do_rawtextColumn(self, (s, col)):
self._stream_write(s)
self.col = col
@@ -504,6 +630,7 @@
if not entering:
macs.append(None)
self.interpret(macro)
+ assert macs[-1] is None
macs.pop()
return
self.interpret(macro)
@@ -526,12 +653,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
@@ -547,21 +673,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
=== Zope/lib/python/TAL/TALParser.py 1.19 => 1.20 ===
--- Zope/lib/python/TAL/TALParser.py:1.19 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/TALParser.py Wed Sep 18 11:12:48 2002
@@ -8,7 +8,7 @@
# 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.
#
##############################################################################
"""
@@ -16,7 +16,7 @@
"""
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):
@@ -58,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:
@@ -77,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 = []
@@ -89,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)
@@ -109,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)
=== Zope/lib/python/TAL/XMLParser.py 1.9 => 1.10 ===
--- Zope/lib/python/TAL/XMLParser.py:1.9 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/XMLParser.py Wed Sep 18 11:12:48 2002
@@ -50,7 +50,7 @@
try:
self.parser.ordered_attributes = self.ordered_attributes
except AttributeError:
- zLOG.LOG("TAL.XMLParser", zLOG.INFO,
+ zLOG.LOG("TAL.XMLParser", zLOG.INFO,
"Can't set ordered_attributes")
self.ordered_attributes = 0
for name in self.handler_names:
=== Zope/lib/python/TAL/driver.py 1.28 => 1.29 ===
--- Zope/lib/python/TAL/driver.py:1.28 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/driver.py Wed Sep 18 11:12:48 2002
@@ -9,11 +9,31 @@
# 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.
#
##############################################################################
"""
Driver program to test METAL and TAL implementation.
+
+Usage: driver.py [options] [file]
+Options:
+ -h / --help
+ Print this message and exit.
+ -H / --html
+ -x / --xml
+ Explicitly choose HTML or XML input. The default is to automatically
+ select based on the file extension. These options are mutually
+ exclusive.
+ -l
+ Lenient structure insertion.
+ -m
+ Macro expansion only
+ -s
+ Print intermediate opcodes only
+ -t
+ Leave TAL/METAL attributes in output
+ -i
+ Leave I18N substitution strings un-interpolated.
"""
import os
@@ -26,68 +46,131 @@
# Import local classes
import TALDefs
-import DummyEngine
+from DummyEngine import DummyEngine
+from DummyEngine import DummyTranslationService
FILE = "tests/input/test01.xml"
+class TestTranslations(DummyTranslationService):
+ def translate(self, domain, msgid, mapping=None, context=None,
+ target_language=None):
+ if msgid == 'timefmt':
+ return '%(minutes)s minutes after %(hours)s %(ampm)s' % mapping
+ elif msgid == 'jobnum':
+ return '%(jobnum)s is the JOB NUMBER' % mapping
+ elif msgid == 'verify':
+ s = 'Your contact email address is recorded as %(email)s'
+ return s % mapping
+ elif msgid == 'mailto:${request/submitter}':
+ return 'mailto:bperson@dom.ain'
+ elif msgid == 'origin':
+ return '%(name)s was born in %(country)s' % mapping
+ return DummyTranslationService.translate(self, domain, msgid,
+ mapping, context,
+ target_language)
+
+class TestEngine(DummyEngine):
+ def __init__(self, macros=None):
+ DummyEngine.__init__(self, macros)
+ self.translationService = TestTranslations()
+
+ def evaluatePathOrVar(self, expr):
+ if expr == 'here/currentTime':
+ return {'hours' : 6,
+ 'minutes': 59,
+ 'ampm' : 'PM',
+ }
+ elif expr == 'context/@@object_name':
+ return '7'
+ elif expr == 'request/submitter':
+ return 'aperson@dom.ain'
+ return DummyEngine.evaluatePathOrVar(self, expr)
+
+
+# This is a disgusting hack so that we can use engines that actually know
+# something about certain object paths. TimeEngine knows about
+# here/currentTime.
+ENGINES = {'test23.html': TestEngine,
+ 'test24.html': TestEngine,
+ 'test26.html': TestEngine,
+ 'test27.html': TestEngine,
+ 'test28.html': TestEngine,
+ 'test29.html': TestEngine,
+ 'test30.html': TestEngine,
+ 'test31.html': TestEngine,
+ 'test32.html': TestEngine,
+ }
+
+def usage(code, msg=''):
+ # Python 2.1 required
+ print >> sys.stderr, __doc__
+ if msg:
+ print >> sys.stderr, msg
+ sys.exit(code)
+
def main():
- versionTest = 1
macros = 0
mode = None
showcode = 0
showtal = -1
strictinsert = 1
+ i18nInterpolate = 1
try:
- opts, args = getopt.getopt(sys.argv[1:], "hxlmnst")
+ opts, args = getopt.getopt(sys.argv[1:], "hHxlmsti",
+ ['help', 'html', 'xml'])
except getopt.error, msg:
- sys.stderr.write("\n%s\n" % str(msg))
- sys.stderr.write(
- "usage: driver.py [-h|-x] [-l] [-m] [-n] [-s] [-t] [file]\n")
- sys.stderr.write("-h/-x -- HTML/XML input (default auto)\n")
- sys.stderr.write("-l -- lenient structure insertion\n")
- sys.stderr.write("-m -- macro expansion only\n")
- sys.stderr.write("-n -- turn off the Python 1.5.2 test\n")
- sys.stderr.write("-s -- print intermediate code\n")
- sys.stderr.write("-t -- leave tal/metal attributes in output\n")
- sys.exit(2)
- for o, a in opts:
- if o == '-h':
+ usage(2, msg)
+ for opt, arg in opts:
+ if opt in ('-h', '--help'):
+ usage(0)
+ if opt in ('-H', '--html'):
+ if mode == 'xml':
+ usage(1, '--html and --xml are mutually exclusive')
mode = "html"
- if o == '-l':
+ if opt == '-l':
strictinsert = 0
- if o == '-m':
+ if opt == '-m':
macros = 1
- if o == '-n':
+ if opt == '-n':
versionTest = 0
- if o == '-x':
+ if opt in ('-x', '--xml'):
+ if mode == 'html':
+ usage(1, '--html and --xml are mutually exclusive')
mode = "xml"
- if o == '-s':
+ if opt == '-s':
showcode = 1
- if o == '-t':
+ if opt == '-t':
showtal = 1
- if not versionTest:
- if sys.version[:5] != "1.5.2":
- sys.stderr.write(
- "Use Python 1.5.2 only; use -n to disable this test\n")
- sys.exit(2)
+ if opt == '-i':
+ i18nInterpolate = 0
if args:
file = args[0]
else:
file = FILE
it = compilefile(file, mode)
- if showcode: showit(it)
- else: interpretit(it, tal=(not macros), showtal=showtal,
- strictinsert=strictinsert)
+ if showcode:
+ showit(it)
+ else:
+ # See if we need a special engine for this test
+ engine = None
+ engineClass = ENGINES.get(os.path.basename(file))
+ if engineClass is not None:
+ engine = engineClass(macros)
+ interpretit(it, engine=engine,
+ tal=(not macros), showtal=showtal,
+ strictinsert=strictinsert,
+ i18nInterpolate=i18nInterpolate)
def interpretit(it, engine=None, stream=None, tal=1, showtal=-1,
- strictinsert=1):
+ strictinsert=1, i18nInterpolate=1):
from TALInterpreter import TALInterpreter
program, macros = it
assert TALDefs.isCurrentVersion(program)
if engine is None:
- engine = DummyEngine.DummyEngine(macros)
+ engine = DummyEngine(macros)
TALInterpreter(program, macros, engine, stream, wrap=0,
- tal=tal, showtal=showtal, strictinsert=strictinsert)()
+ tal=tal, showtal=showtal, strictinsert=strictinsert,
+ i18nInterpolate=i18nInterpolate)()
def compilefile(file, mode=None):
assert mode in ("html", "xml", None)
=== Zope/lib/python/TAL/runtest.py 1.22 => 1.23 ===
--- Zope/lib/python/TAL/runtest.py:1.22 Wed Aug 14 17:58:54 2002
+++ Zope/lib/python/TAL/runtest.py Wed Sep 18 11:12:48 2002
@@ -71,8 +71,8 @@
htmlargs.sort()
args = xmlargs + htmlargs
if not args:
- sys.stderr.write("No tests found -- please supply filenames\n")
- sys.exit(1)
+ sys.stderr.write("No tests found -- please supply filenames\n")
+ sys.exit(1)
errors = 0
for arg in args:
locopts = []