Deprecate TAL package and make it a facade of zope.tal.

parent 7544aa4b
......@@ -13,238 +13,13 @@
##############################################################################
"""
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
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:
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
text = msgid
if msgid == "don't translate me":
if default is not None:
text = default
else:
text = msgid.upper()
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.dummyengine', '2.12')
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)
from zope.tal.dummyengine import DummyTranslationDomain as DummyDomain
class DummyTranslationService:
......
......@@ -13,303 +13,8 @@
##############################################################################
"""
Parse HTML and compile to TALInterpreter intermediate code.
"""
import sys
from TALGenerator import TALGenerator
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
# minimized form (e.g. <img ismap> rather than <img ismap="">)
# From http://www.w3.org/TR/xhtml1/#guidelines (C.10)
"compact", "nowrap", "ismap", "declare", "noshade", "checked",
"disabled", "readonly", "multiple", "selected", "noresize",
"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",
]
PARA_LEVEL_HTML_TAGS = [
# List of HTML elements that close open paragraph-level elements
# and are themselves paragraph-level.
"h1", "h2", "h3", "h4", "h5", "h6", "p",
]
BLOCK_CLOSING_TAG_MAP = {
"tr": ("tr", "td", "th"),
"td": ("td", "th"),
"th": ("td", "th"),
"li": ("li",),
"dd": ("dd", "dt"),
"dt": ("dd", "dt"),
}
BLOCK_LEVEL_HTML_TAGS = [
# List of HTML tags that denote larger sections than paragraphs.
"blockquote", "table", "tr", "th", "td", "thead", "tfoot", "tbody",
"noframe", "ul", "ol", "li", "dl", "dt", "dd", "div",
]
TIGHTEN_IMPLICIT_CLOSE_TAGS = (PARA_LEVEL_HTML_TAGS
+ BLOCK_CLOSING_TAG_MAP.keys())
class NestingError(HTMLParseError):
"""Exception raised when elements aren't properly nested."""
def __init__(self, tagstack, endtag, position=(None, None)):
self.endtag = endtag
if tagstack:
if len(tagstack) == 1:
msg = ('Open tag <%s> does not match close tag </%s>'
% (tagstack[0], endtag))
else:
msg = ('Open tags <%s> do not match close tag </%s>'
% ('>, <'.join(tagstack), endtag))
else:
msg = 'No tags are open to match </%s>' % endtag
HTMLParseError.__init__(self, msg, position)
class EmptyTagError(NestingError):
"""Exception raised when empty elements have an end tag."""
def __init__(self, tag, position=(None, None)):
self.tag = tag
msg = 'Close tag </%s> should be removed' % tag
HTMLParseError.__init__(self, msg, position)
class OpenTagError(NestingError):
"""Exception raised when a tag is not allowed in another tag."""
def __init__(self, tagstack, tag, position=(None, None)):
self.tag = tag
msg = 'Tag <%s> is not allowed in <%s>' % (tag, tagstack[-1])
HTMLParseError.__init__(self, msg, position)
class HTMLTALParser(HTMLParser):
# External API
def __init__(self, gen=None):
HTMLParser.__init__(self)
if gen is None:
gen = TALGenerator(xml=0)
self.gen = gen
self.tagstack = []
self.nsstack = []
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()
try:
self.parseString(data)
except TALError, e:
e.setFile(file)
raise
def parseString(self, data):
self.feed(data)
self.close()
while self.tagstack:
self.implied_endtag(self.tagstack[-1], 2)
assert self.nsstack == [], self.nsstack
def getCode(self):
return self.gen.getCode()
def getWarnings(self):
return ()
# Overriding HTMLParser methods
def handle_starttag(self, tag, attrs):
self.close_para_tags(tag)
self.scan_xmlns(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, i18ndict,
self.getpos())
if tag in EMPTY_HTML_TAGS:
self.implied_endtag(tag, -1)
def handle_startendtag(self, tag, attrs):
self.close_para_tags(tag)
self.scan_xmlns(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,
i18ndict, self.getpos())
self.gen.emitEndElement(tag, implied=-1)
else:
self.gen.emitStartElement(tag, attrlist, taldict, metaldict,
i18ndict, self.getpos(), isend=1)
self.pop_xmlns()
def handle_endtag(self, tag):
if tag in EMPTY_HTML_TAGS:
# </img> etc. in the source is an error
raise EmptyTagError(tag, self.getpos())
self.close_enclosed_tags(tag)
self.gen.emitEndElement(tag)
self.pop_xmlns()
self.tagstack.pop()
def close_para_tags(self, tag):
if tag in EMPTY_HTML_TAGS:
return
close_to = -1
if BLOCK_CLOSING_TAG_MAP.has_key(tag):
blocks_to_close = BLOCK_CLOSING_TAG_MAP[tag]
for i in range(len(self.tagstack)):
t = self.tagstack[i]
if t in blocks_to_close:
if close_to == -1:
close_to = i
elif t in BLOCK_LEVEL_HTML_TAGS:
close_to = -1
elif tag in PARA_LEVEL_HTML_TAGS + BLOCK_LEVEL_HTML_TAGS:
i = len(self.tagstack) - 1
while i >= 0:
closetag = self.tagstack[i]
if closetag in BLOCK_LEVEL_HTML_TAGS:
break
if closetag in PARA_LEVEL_HTML_TAGS:
if closetag != "p":
raise OpenTagError(self.tagstack, tag, self.getpos())
close_to = i
i = i - 1
if close_to >= 0:
while len(self.tagstack) > close_to:
self.implied_endtag(self.tagstack[-1], 1)
def close_enclosed_tags(self, tag):
if tag not in self.tagstack:
raise NestingError(self.tagstack, tag, self.getpos())
while tag != self.tagstack[-1]:
self.implied_endtag(self.tagstack[-1], 1)
assert self.tagstack[-1] == tag
def implied_endtag(self, tag, implied):
assert tag == self.tagstack[-1]
assert implied in (-1, 1, 2)
isend = (implied < 0)
if tag in TIGHTEN_IMPLICIT_CLOSE_TAGS:
# Pick out trailing whitespace from the program, and
# insert the close tag before the whitespace.
white = self.gen.unEmitWhitespace()
else:
white = None
self.gen.emitEndElement(tag, isend=isend, implied=implied)
if white:
self.gen.emitRawText(white)
self.tagstack.pop()
self.pop_xmlns()
def handle_charref(self, name):
self.gen.emitRawText("&#%s;" % name)
def handle_entityref(self, name):
self.gen.emitRawText("&%s;" % name)
def handle_data(self, data):
self.gen.emitRawText(data)
def handle_comment(self, data):
self.gen.emitRawText("<!--%s-->" % data)
def handle_decl(self, data):
self.gen.emitRawText("<!%s>" % data)
def handle_pi(self, data):
self.gen.emitRawText("<?%s>" % data)
# Internal thingies
def scan_xmlns(self, attrs):
nsnew = {}
for key, value in attrs:
if key.startswith("xmlns:"):
nsnew[key[6:]] = value
if nsnew:
self.nsstack.append(self.nsdict)
self.nsdict = self.nsdict.copy()
self.nsdict.update(nsnew)
else:
self.nsstack.append(self.nsdict)
def pop_xmlns(self):
self.nsdict = self.nsstack.pop()
def fixname(self, name):
if ':' in name:
prefix, suffix = name.split(':', 1)
if prefix == 'xmlns':
nsuri = self.nsdict.get(suffix)
if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_NS, ZOPE_I18N_NS):
return name, name, prefix
else:
nsuri = self.nsdict.get(prefix)
if nsuri == ZOPE_TAL_NS:
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)
ns = keyns or namens # default to tag namespace
if ns and ns != 'unknown':
item = (key, value, ns)
if ns == 'tal':
if taldict.has_key(keybase):
raise TALError("duplicate TAL attribute " +
`keybase`, self.getpos())
taldict[keybase] = value
elif ns == 'metal':
if metaldict.has_key(keybase):
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, i18ndict
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.htmltalparser', '2.12')
"""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
import zope.deferredimport
zope.deferredimport.deprecatedFrom(
"The TAL implementation has moved to zope.tal. Import expression "
"interfaces from zope.tal.interfaces. The old references will be "
"gone in Zope 2.12.",
'zope.tal.interfaces'
'ITALExpressionCompiler', 'ITALExpressionEngine', 'ITALExpressionErrorInfo'
)
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 getCompiler():
"""Return an object that supports ITALESCompiler."""
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, default=None):
"""
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.")
......@@ -13,179 +13,16 @@
##############################################################################
"""
Common definitions used by TAL and METAL compilation an transformation.
"""
from types import ListType, TupleType
from ITALES import ITALESErrorInfo
TAL_VERSION = "1.5"
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"
# 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",
]
KNOWN_TAL_ATTRIBUTES = [
"define",
"condition",
"content",
"replace",
"repeat",
"attributes",
"on-error",
"omit-tag",
"tal tag",
]
KNOWN_I18N_ATTRIBUTES = [
"translate",
"domain",
"target",
"source",
"attributes",
"data",
"name",
]
class TALError(Exception):
def __init__(self, msg, position=(None, None)):
assert msg != ""
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
if self.lineno is not None:
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):
pass
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__
self.value = err
else:
self.type = err
self.value = None
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, xml):
dict = {}
for part in splitParts(arg):
m = _attr_re.match(part)
if not m:
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`)
dict[name] = expr
return dict
def parseSubstitution(arg, position=(None, None)):
m = _subst_re.match(arg)
if not m:
raise TALError("Bad syntax in substitution text: " + `arg`, position)
key, expr = m.group(1, 2)
if not key:
key = "text"
return key, expr
def splitParts(arg):
# Break in pieces at undoubled semicolons and
# change double semicolons to singles:
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
def isCurrentVersion(program):
version = getProgramVersion(program)
return version == TAL_VERSION
def getProgramMode(program):
version = getProgramVersion(program)
if (version == TAL_VERSION and isinstance(program[1], TupleType) and
len(program[1]) == 2):
opcode, mode = program[1]
if opcode == "mode":
return mode
return None
def getProgramVersion(program):
if (len(program) >= 2 and
isinstance(program[0], TupleType) and len(program[0]) == 2):
opcode, version = program[0]
if opcode == "version":
return version
return None
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
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.taldefs', '2.12')
import zope.deferredimport
zope.deferredimport.deprecated(
"TALESError has been renamed TALExpressionError and should be "
"imported from zope.tal.taldefs. This reference will be gone in "
"Zope 2.12.",
TALESError = 'zope.tal.taldefs.TALExpressionError'
)
......@@ -13,880 +13,8 @@
##############################################################################
"""
Code generator for TALInterpreter intermediate code.
"""
import re
import cgi
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 = {}
self.slotStack = []
self.xml = xml
self.emit("version", TAL_VERSION)
self.emit("mode", xml and "xml" or "html")
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
assert not self.todoStack
return self.optimize(self.program), self.macros
def optimize(self, program):
output = []
collect = []
cursor = 0
if self.xml:
endsep = "/>"
else:
endsep = " />"
for cursor in xrange(len(program)+1):
try:
item = program[cursor]
except IndexError:
item = (None, None)
opcode = item[0]
if opcode == "rawtext":
collect.append(item[1])
continue
if opcode == "endTag":
collect.append("</%s>" % item[1])
continue
if opcode == "startTag":
if self.optimizeStartTag(collect, item[1], item[2], ">"):
continue
if opcode == "startEndTag":
if self.optimizeStartTag(collect, item[1], item[2], endsep):
continue
if opcode in ("beginScope", "endScope"):
# Push *Scope instructions in front of any text instructions;
# this allows text instructions separated only by *Scope
# 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")
if i >= 0:
i = len(text) - (i + 1)
output.append(("rawtextColumn", (text, i)))
else:
output.append(("rawtextOffset", (text, len(text))))
if opcode != None:
output.append(self.optimizeArgsList(item))
collect = []
return self.optimizeCommonTriple(output)
def optimizeArgsList(self, item):
if len(item) == 2:
return item
else:
return item[0], tuple(item[1:])
# 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
opt = 1
new = ["<" + name]
for i in range(len(attrlist)):
item = attrlist[i]
if len(item) > 2:
opt = 0
name, value, action = item[:3]
attrlist[i] = (name, value, action) + item[3:]
else:
if item[1] is None:
s = item[0]
else:
s = '%s="%s"' % (item[0], TALDefs.attrEscape(item[1]))
attrlist[i] = item[0], s
new.append(" " + s)
# if no non-optimizable attributes were found, convert to plain text
if opt:
new.append(end)
collect.extend(new)
return opt
def optimizeCommonTriple(self, program):
if len(program) < 3:
return program
output = program[:2]
prev2, prev1 = output
for item in program[2:]:
if ( item[0] == "beginScope"
and prev1[0] == "setPosition"
and prev2[0] == "rawtextColumn"):
position = output.pop()[1]
text, column = output.pop()[1]
prev1 = None, None
closeprev = 0
if output and output[-1][0] == "endScope":
closeprev = 1
output.pop()
item = ("rawtextBeginScope",
(text, column, position, closeprev, item[1]))
output.append(item)
prev2 = prev1
prev1 = item
return output
def todoPush(self, todo):
self.todoStack.append(todo)
def todoPop(self):
return self.todoStack.pop()
def compileExpression(self, expr):
try:
return self.expressionCompiler.compile(expr)
except self.CompilerError, err:
raise TALError('%s in expression %s' % (err.args[0], `expr`),
self.position)
def pushProgram(self):
self.stack.append(self.program)
self.program = []
def popProgram(self):
program = self.program
self.program = self.stack.pop()
return self.optimize(program)
def pushSlots(self):
self.slotStack.append(self.slots)
self.slots = {}
def popSlots(self):
slots = self.slots
self.slots = self.slotStack.pop()
return slots
def emit(self, *instruction):
self.program.append(instruction)
def emitStartTag(self, name, attrlist, isend=0):
if isend:
opcode = "startEndTag"
else:
opcode = "startTag"
self.emit(opcode, name, attrlist)
def emitEndTag(self, name):
if self.xml and self.program and self.program[-1][0] == "startTag":
# Minimize empty element
self.program[-1] = ("startEndTag",) + self.program[-1][1:]
else:
self.emit("endTag", name)
def emitOptTag(self, name, optTag, isend):
program = self.popProgram() #block
start = self.popProgram() #start tag
if (isend or not program) and self.xml:
# Minimize empty element
start[-1] = ("startEndTag",) + start[-1][1:]
isend = 1
cexpr = optTag[0]
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)
def emitText(self, text):
self.emitRawText(cgi.escape(text))
def emitDefines(self, 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:
raise TALError("invalid define syntax: " + `part`,
self.position)
scope, name, expr = m.group(1, 2, 3)
scope = scope or "local"
cexpr = self.compileExpression(expr)
if scope == "local":
self.emit("setLocal", name, cexpr)
else:
self.emit("setGlobal", name, cexpr)
def emitOnError(self, name, onError, TALtag, isend):
block = self.popProgram()
key, expr = parseSubstitution(onError)
cexpr = self.compileExpression(expr)
if key == "text":
self.emit("insertText", cexpr, [])
else:
assert key == "structure"
self.emit("insertStructure", cexpr, {}, [])
if TALtag:
self.emitOptTag(name, (None, 1), isend)
else:
self.emitEndTag(name)
handler = self.popProgram()
self.emit("onError", block, handler)
def emitCondition(self, expr):
cexpr = self.compileExpression(expr)
program = self.popProgram()
self.emit("condition", cexpr, program)
def emitRepeat(self, arg):
m = re.match("(?s)\s*(%s)\s+(.*)\Z" % NAME_RE, arg)
if not m:
raise TALError("invalid repeat syntax: " + `arg`,
self.position)
name, expr = m.group(1, 2)
cexpr = self.compileExpression(expr)
program = self.popProgram()
self.emit("loop", name, cexpr, program)
def emitSubstitution(self, arg, attrDict={}):
key, expr = parseSubstitution(arg)
cexpr = self.compileExpression(expr)
program = self.popProgram()
if key == "text":
self.emit("insertText", cexpr, program)
else:
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)
self.emit('i18nVariable',
varname, program, cexpr, int(key == "structure"))
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()
if self.macros.has_key(macroName):
raise METALError("duplicate macro definition: %s" % `macroName`,
self.position)
if not re.match('%s$' % NAME_RE, macroName):
raise METALError("invalid macro name: %s" % `macroName`,
self.position)
self.macros[macroName] = program
self.inMacroDef = self.inMacroDef - 1
self.emit("defineMacro", macroName, program)
def emitUseMacro(self, expr):
cexpr = self.compileExpression(expr)
program = self.popProgram()
self.inMacroUse = 0
self.emit("useMacro", expr, cexpr, self.popSlots(), program)
def emitDefineSlot(self, slotName):
program = self.popProgram()
slotName = slotName.strip()
if not re.match('%s$' % NAME_RE, slotName):
raise METALError("invalid slot name: %s" % `slotName`,
self.position)
self.emit("defineSlot", slotName, program)
def emitFillSlot(self, slotName):
program = self.popProgram()
slotName = slotName.strip()
if self.slots.has_key(slotName):
raise METALError("duplicate fill-slot name: %s" % `slotName`,
self.position)
if not re.match('%s$' % NAME_RE, slotName):
raise METALError("invalid slot name: %s" % `slotName`,
self.position)
self.slots[slotName] = program
self.inMacroUse = 1
self.emit("fillSlot", slotName, program)
def unEmitWhitespace(self):
collect = []
i = len(self.program) - 1
while i >= 0:
item = self.program[i]
if item[0] != "rawtext":
break
text = item[1]
if not re.match(r"\A\s*\Z", text):
break
collect.append(text)
i = i-1
del self.program[i+1:]
if i >= 0 and self.program[i][0] == "rawtext":
text = self.program[i][1]
m = re.search(r"\s+\Z", text)
if m:
self.program[i] = ("rawtext", text[:m.start()])
collect.append(m.group())
collect.reverse()
return "".join(collect)
def unEmitNewlineWhitespace(self):
collect = []
i = len(self.program)
while i > 0:
i = i-1
item = self.program[i]
if item[0] != "rawtext":
break
text = item[1]
if re.match(r"\A[ \t]*\Z", text):
collect.append(text)
continue
m = re.match(r"(?s)^(.*)(\n[ \t]*)\Z", text)
if not m:
break
text, rest = m.group(1, 2)
collect.reverse()
rest = rest + "".join(collect)
del self.program[i:]
if text:
self.emit("rawtext", text)
return rest
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):
expr, xlat, msgid = repldict[key]
item = item[:2] + ("replace", expr, xlat, msgid)
del repldict[key]
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, i18ndict,
position=(None, None), isend=0):
if not taldict and not metaldict and not i18ndict:
# Handle the simple, common case
self.emitStartTag(name, attrlist, isend)
self.todoPush({})
if isend:
self.emitEndElement(name, isend)
return
self.position = position
for key, value in taldict.items():
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 TALDefs.KNOWN_METAL_ATTRIBUTES:
raise METALError("bad METAL attribute: " + `key`,
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")
defineSlot = metaldict.get("define-slot")
fillSlot = metaldict.get("fill-slot")
define = taldict.get("define")
condition = taldict.get("condition")
repeat = taldict.get("repeat")
content = taldict.get("content")
replace = taldict.get("replace")
attrsubst = taldict.get("attributes")
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 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:
# Hack to include preceding whitespace in the loop program
repeatWhitespace = self.unEmitNewlineWhitespace()
if position != (None, None):
# XXX at some point we should insist on a non-trivial position
self.emit("setPosition", position)
if self.inMacroUse:
if fillSlot:
self.pushProgram()
if self.source_file is not None:
self.emit("setSourceFile", self.source_file)
todo["fillSlot"] = fillSlot
self.inMacroUse = 0
else:
if fillSlot:
raise METALError("fill-slot must be within a use-macro",
position)
if not self.inMacroUse:
if defineMacro:
self.pushProgram()
self.emit("version", TAL_VERSION)
self.emit("mode", self.xml and "xml" or "html")
if self.source_file is not None:
self.emit("setSourceFile", self.source_file)
todo["defineMacro"] = defineMacro
self.inMacroDef = self.inMacroDef + 1
if useMacro:
self.pushSlots()
self.pushProgram()
todo["useMacro"] = useMacro
self.inMacroUse = 1
if defineSlot:
if not self.inMacroDef:
raise METALError(
"define-slot must be within a define-macro",
position)
self.pushProgram()
todo["defineSlot"] = defineSlot
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]
dict[key] = value
self.emit("beginScope", dict)
todo["scope"] = 1
if onError:
self.pushProgram() # handler
if TALtag:
self.pushProgram() # start
self.emitStartTag(name, list(attrlist)) # Must copy attrlist!
if TALtag:
self.pushProgram() # start
self.pushProgram() # block
todo["onError"] = onError
if define:
self.emitDefines(define)
todo["define"] = define
if condition:
self.pushProgram()
todo["condition"] = condition
if repeat:
todo["repeat"] = repeat
self.pushProgram()
if repeatWhitespace:
self.emitText(repeatWhitespace)
if content:
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 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():
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:
todo["repldict"] = repldict
repldict = {}
self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend)
if optTag:
self.pushProgram()
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
self.todoPush(todo)
if isend:
self.emitEndElement(name, isend)
def emitEndElement(self, name, isend=0, implied=0):
todo = self.todoPop()
if not todo:
# Shortcut
if not isend:
self.emitEndTag(name)
return
self.position = position = todo.get("position", (None, None))
defineMacro = todo.get("defineMacro")
useMacro = todo.get("useMacro")
defineSlot = todo.get("defineSlot")
fillSlot = todo.get("fillSlot")
repeat = todo.get("repeat")
content = todo.get("content")
replace = todo.get("replace")
condition = todo.get("condition")
onError = todo.get("onError")
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:
exc = METALError
what = "METAL"
else:
exc = TALError
what = "TAL"
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. This
# can include a 'text' or 'structure' indicator.
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:
self.emitCondition(condition)
if onError:
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:
self.emitFillSlot(fillSlot)
if useMacro:
self.emitUseMacro(useMacro)
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) == 1:
addAttribute(d, i18nattrlist[0], None, position, xml)
elif 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:
import warnings
warnings.warn(I18N_ATTRIBUTES_WARNING
% (source_file, str(position), i18nattrs)
, DeprecationWarning)
msgid = None
for attr in i18nattrlist:
addAttribute(d, attr, msgid, position, xml)
else:
import warnings
warnings.warn(I18N_ATTRIBUTES_WARNING
% (source_file, str(position), i18nattrs)
, DeprecationWarning)
msgid = None
for attr in i18nattrlist:
addAttribute(d, attr, msgid, position, xml)
return d
I18N_ATTRIBUTES_WARNING = (
'Space separated attributes in i18n:attributes'
' are deprecated (i18n:attributes="value title"). Please use'
' semicolon to separate attributes'
' (i18n:attributes="value; title").'
'\nFile %s at row, column %s\nAttributes %s')
def test():
t = TALGenerator()
t.pushProgram()
t.emit("bar")
p = t.popProgram()
t.emit("foo", p)
if __name__ == "__main__":
test()
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.talgenerator', '2.12')
......@@ -13,828 +13,16 @@
##############################################################################
"""Interpreter for a pre-compiled TAL program.
BBB 2005/05/01 -- to be removed after 12 months
$Id$
"""
import cgi
import sys
import re
# Do not use cStringIO here! It's not unicode aware. :(
from StringIO import StringIO
from DocumentTemplate.DT_Util import ustr
from ZODB.POSException import ConflictError
from zope.i18nmessageid import Message
from TALDefs import attrEscape, TAL_VERSION, METALError
from TALDefs import isCurrentVersion
from TALDefs import getProgramVersion, getProgramMode
from TALGenerator import TALGenerator
from TranslationContext import TranslationContext
I18nMessageTypes = (Message,)
# TODO: In Python 2.4 we can use frozenset() instead of dict.fromkeys()
BOOLEAN_HTML_ATTRS = dict.fromkeys([
# List of Boolean attributes in HTML that should be rendered in
# minimized form (e.g. <img ismap> rather than <img ismap="">)
# From http://www.w3.org/TR/xhtml1/#guidelines (C.10)
# TODO: The problem with this is that this is not valid XML and
# can't be parsed back!
"compact", "nowrap", "ismap", "declare", "noshade", "checked",
"disabled", "readonly", "multiple", "selected", "noresize",
"defer"
])
_nulljoin = ''.join
_spacejoin = ' '.join
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 _spacejoin(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):
def __init__(self, repldict, expressionCompiler=None, xml=0):
self.repldict = repldict
self.enabled = 1
TALGenerator.__init__(self, expressionCompiler, xml)
def enable(self, enabled):
self.enabled = enabled
def emit(self, *args):
if self.enabled:
TALGenerator.emit(self, *args)
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, i18ndict,
position, isend)
def replaceAttrs(self, attrlist, repldict):
if self.enabled and self.repldict:
repldict = self.repldict
self.repldict = None
return TALGenerator.replaceAttrs(self, attrlist, repldict)
class TALInterpreter:
"""TAL interpreter.
"""
def __init__(self, program, macros, engine, stream=None,
debug=0, wrap=60, metal=1, tal=1, showtal=-1,
strictinsert=1, stackLimit=100, i18nInterpolate=1):
"""Create a TAL interpreter.
Optional arguments:
stream -- output stream (defaults to sys.stdout).
debug -- enable debugging output to sys.stderr (off by default).
wrap -- try to wrap attributes on opening tags to this number of
column (default: 60).
metal -- enable METAL macro processing (on by default).
tal -- enable TAL processing (on by default).
showtal -- do not strip away TAL directives. A special value of
-1 (which is the default setting) enables showtal when TAL
processing is disabled, and disables showtal when TAL processing is
enabled. Note that you must use 0, 1, or -1; true boolean values
are not supported (TODO: why?).
strictinsert -- enable TAL processing and stricter HTML/XML
checking on text produced by structure inserts (on by default).
Note that Zope turns this value off by default.
stackLimit -- set macro nesting limit (default: 100).
i18nInterpolate -- enable i18n translations (default: on).
"""
self.program = program
self.macros = macros
self.engine = engine # Execution engine (aka context)
self.Default = engine.getDefault()
self._currentTag = ""
self._stream_stack = [stream or sys.stdout]
self.popStream()
self.debug = debug
self.wrap = wrap
self.metal = metal
self.tal = tal
if tal:
self.dispatch = self.bytecode_handlers_tal
else:
self.dispatch = self.bytecode_handlers
assert showtal in (-1, 0, 1)
if showtal == -1:
showtal = (not tal)
self.showtal = showtal
self.strictinsert = strictinsert
self.stackLimit = stackLimit
self.html = 0
self.endsep = "/>"
self.endlen = len(self.endsep)
self.macroStack = []
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._stream_stack,
self.scopeLevel, self.level, self.i18nContext)
def restoreState(self, state):
(self.position, self.col, self.stream,
self._stream_stack, 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,
self._stream_stack, scopeLevel, level, i18n) = state
self._stream_write = self.stream.write
assert self.level == level
assert self.scopeLevel == scopeLevel
def pushMacro(self, macroName, slots, entering=1):
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.i18nContext])
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 pushStream(self, newstream):
self._stream_stack.append(self.stream)
self.stream = newstream
self._stream_write = self.stream.write
def popStream(self):
self.stream = self._stream_stack.pop()
self._stream_write = self.stream.write
def stream_write(self, s,
len=len):
self._stream_write(s)
i = s.rfind('\n')
if i < 0:
self.col = self.col + len(s)
else:
self.col = len(s) - (i + 1)
bytecode_handlers = {}
def interpret(self, program):
oldlevel = self.level
self.level = oldlevel + 1
handlers = self.dispatch
try:
if self.debug:
for (opcode, args) in program:
s = "%sdo_%s(%s)\n" % (" "*self.level, opcode,
repr(args))
if len(s) > 80:
s = s[:76] + "...\n"
sys.stderr.write(s)
handlers[opcode](self, args)
else:
for (opcode, args) in program:
handlers[opcode](self, args)
finally:
self.level = oldlevel
def do_version(self, version):
assert version == TAL_VERSION
bytecode_handlers["version"] = do_version
def do_mode(self, mode):
assert mode in ("html", "xml")
self.html = (mode == "html")
if self.html:
self.endsep = " />"
else:
self.endsep = "/>"
self.endlen = len(self.endsep)
bytecode_handlers["mode"] = do_mode
def do_setSourceFile(self, source_file):
self.sourceFile = source_file
self.engine.setSourceFile(source_file)
bytecode_handlers["setSourceFile"] = do_setSourceFile
def do_setPosition(self, position):
self.position = position
self.engine.setPosition(position)
bytecode_handlers["setPosition"] = do_setPosition
def do_startEndTag(self, stuff):
self.do_startTag(stuff, self.endsep, self.endlen)
bytecode_handlers["startEndTag"] = do_startEndTag
def do_startTag(self, (name, attrList),
end=">", endlen=1, _len=len):
# The bytecode generator does not cause calls to this method
# for start tags with no attributes; those are optimized down
# to rawtext events. Hence, there is no special "fast path"
# for that case.
self._currentTag = name
L = ["<", name]
append = L.append
col = self.col + _len(name) + 1
wrap = self.wrap
align = col + 1
if align >= wrap/2:
align = 4 # Avoid a narrow column far to the right
attrAction = self.dispatch["<attrAction>"]
try:
for item in attrList:
if _len(item) == 2:
name, s = item
else:
# 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):
append("\n")
append(" "*align)
col = align + slen
else:
append(" ")
col = col + 1 + slen
append(s)
append(end)
col = col + endlen
finally:
self._stream_write(_nulljoin(L))
self.col = col
bytecode_handlers["startTag"] = do_startTag
def attrAction(self, item):
name, value, action = item[:3]
if action == 'insert':
return 0, name, value
macs = self.macroStack
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
# Clear 'entering' flag
macs[-1][2] = 0
# Convert or drop depth-one METAL attributes.
i = name.rfind(":") + 1
prefix, suffix = name[:i], name[i:]
if suffix == "define-macro":
# Convert define-macro as we enter depth one.
name = prefix + "use-macro"
value = macs[-1][0] # Macro name
elif suffix == "define-slot":
name = prefix + "fill-slot"
elif suffix == "fill-slot":
pass
else:
return 0, name, value
if value is None:
value = name
else:
value = '%s="%s"' % (name, attrEscape(value))
return 1, name, value
def attrAction_tal(self, item):
name, value, action = item[:3]
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 == 'insert': # Cancelled insert
ok = 0
elif evalue:
value = None
else:
ok = 0
elif 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
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
elif evalue is self.Default:
value = attrEscape(value)
else:
value = cgi.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 = self.StringIO()
self._stream_write = stream.write
self.interpret(start)
self.restoreOutputState(state)
self.interpret(program)
def do_optTag(self, (name, cexpr, tag_ns, isend, start, program),
omit=0):
if tag_ns and not self.showtal:
return self.no_tag(start, program)
self.interpret(start)
if not isend:
self.interpret(program)
s = '</%s>' % name
self._stream_write(s)
self.col = self.col + len(s)
def do_optTag_tal(self, stuff):
cexpr = stuff[1]
if cexpr is not None and (cexpr == '' or
self.engine.evaluateBoolean(cexpr)):
self.no_tag(stuff[-2], stuff[-1])
else:
self.do_optTag(stuff)
bytecode_handlers["optTag"] = do_optTag
def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)):
self._stream_write(s)
self.col = col
self.do_setPosition(position)
if closeprev:
engine = self.engine
engine.endScope()
engine.beginScope()
else:
self.engine.beginScope()
self.scopeLevel = self.scopeLevel + 1
def do_rawtextBeginScope_tal(self, (s, col, position, closeprev, dict)):
self._stream_write(s)
self.col = col
engine = self.engine
self.position = position
engine.setPosition(position)
if closeprev:
engine.endScope()
engine.beginScope()
else:
engine.beginScope()
self.scopeLevel = self.scopeLevel + 1
engine.setLocal("attrs", dict)
bytecode_handlers["rawtextBeginScope"] = do_rawtextBeginScope
def do_beginScope(self, dict):
self.engine.beginScope()
self.scopeLevel = self.scopeLevel + 1
def do_beginScope_tal(self, dict):
engine = self.engine
engine.beginScope()
engine.setLocal("attrs", dict)
self.scopeLevel = self.scopeLevel + 1
bytecode_handlers["beginScope"] = do_beginScope
def do_endScope(self, notused=None):
self.engine.endScope()
self.scopeLevel = self.scopeLevel - 1
bytecode_handlers["endScope"] = do_endScope
def do_setLocal(self, notused):
pass
def do_setLocal_tal(self, (name, expr)):
self.engine.setLocal(name, self.engine.evaluateValue(expr))
bytecode_handlers["setLocal"] = do_setLocal
def do_setGlobal_tal(self, (name, expr)):
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])
def do_insertText_tal(self, stuff):
text = self.engine.evaluateText(stuff[0])
if text is None:
return
if text is self.Default:
self.interpret(stuff[1])
return
if isinstance(text, I18nMessageTypes):
# Translate this now.
text = self.engine.translate(text.domain, text,
text.mapping, default=text.default)
s = cgi.escape(text)
self._stream_write(s)
i = s.rfind('\n')
if i < 0:
self.col = self.col + len(s)
else:
self.col = len(s) - (i + 1)
bytecode_handlers["insertText"] = do_insertText
def do_i18nVariable(self, stuff):
varname, program, expression, structure = 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.pushStream(tmpstream)
try:
self.interpret(program)
finally:
self.popStream()
if self.html and self._currentTag == "pre":
value = tmpstream.getvalue()
else:
value = normalize(tmpstream.getvalue())
finally:
self.restoreState(state)
else:
# Evaluate the value to be associated with the variable in the
# i18n interpolation dictionary.
if structure:
value = self.engine.evaluateStructure(expression)
else:
value = self.engine.evaluate(expression)
# evaluate() does not do any I18n, so we do it here.
if isinstance(value, I18nMessageTypes):
# Translate this now.
# XXX
value = self.engine.translate(value.domain, value,
value.mapping, value.default)
if not structure:
value = cgi.escape(ustr(value))
# 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.
currentTag = self._currentTag
tmpstream = self.StringIO()
self.pushStream(tmpstream)
try:
self.interpret(stuff[1])
finally:
self.popStream()
# 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.
default = tmpstream.getvalue()
if not msgid:
if self.html and currentTag == "pre":
msgid = default
else:
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)
# TODO: I can't decide whether we want to cgi escape the translated
# string or not. OTOH 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>".
assert xlated_msgid is not None
self._stream_write(xlated_msgid)
bytecode_handlers['insertTranslation'] = do_insertTranslation
def do_insertStructure(self, stuff):
self.interpret(stuff[2])
def do_insertStructure_tal(self, (expr, repldict, block)):
structure = self.engine.evaluateStructure(expr)
if structure is None:
return
if structure is self.Default:
self.interpret(block)
return
text = ustr(structure)
if not (repldict or self.strictinsert):
# Take a shortcut, no error checking
self.stream_write(text)
return
if self.html:
self.insertHTMLStructure(text, repldict)
else:
self.insertXMLStructure(text, repldict)
bytecode_handlers["insertStructure"] = do_insertStructure
def insertHTMLStructure(self, text, repldict):
from HTMLTALParser import HTMLTALParser
gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
p = HTMLTALParser(gen) # Raises an exception if text is invalid
p.parseString(text)
program, macros = p.getCode()
self.interpret(program)
def insertXMLStructure(self, text, repldict):
from TALParser import TALParser
gen = AltTALGenerator(repldict, self.engine.getCompiler(), 0)
p = TALParser(gen)
gen.enable(0)
p.parseFragment('<!DOCTYPE foo PUBLIC "foo" "bar"><foo>')
gen.enable(1)
p.parseFragment(text) # Raises an exception if text is invalid
gen.enable(0)
p.parseFragment('</foo>', 1)
program, macros = gen.getCode()
self.interpret(program)
def do_loop(self, (name, expr, block)):
self.interpret(block)
def do_loop_tal(self, (name, expr, block)):
iterator = self.engine.setRepeat(name, expr)
while iterator.next():
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
# TODO: 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
bytecode_handlers["rawtextColumn"] = do_rawtextColumn
def do_rawtextOffset(self, (s, offset)):
self._stream_write(s)
self.col = self.col + offset
bytecode_handlers["rawtextOffset"] = do_rawtextOffset
def do_condition(self, (condition, block)):
if not self.tal or self.engine.evaluateBoolean(condition):
self.interpret(block)
bytecode_handlers["condition"] = do_condition
def do_defineMacro(self, (macroName, macro)):
macs = self.macroStack
if len(macs) == 1:
entering = macs[-1][2]
if not entering:
macs.append(None)
self.interpret(macro)
assert macs[-1] is None
macs.pop()
return
self.interpret(macro)
bytecode_handlers["defineMacro"] = do_defineMacro
def do_useMacro(self, (macroName, macroExpr, compiledSlots, block)):
if not self.metal:
self.interpret(block)
return
macro = self.engine.evaluateMacro(macroExpr)
if macro is self.Default:
macro = block
else:
if not isCurrentVersion(macro):
raise METALError("macro %s has incompatible version %s" %
(`macroName`, `getProgramVersion(macro)`),
self.position)
mode = getProgramMode(macro)
if mode != (self.html and "html" or "xml"):
raise METALError("macro %s has incompatible mode %s" %
(`macroName`, `mode`), self.position)
self.pushMacro(macroName, compiledSlots)
prev_source = self.sourceFile
self.interpret(macro)
if self.sourceFile != prev_source:
self.engine.setSourceFile(prev_source)
self.sourceFile = prev_source
self.popMacro()
bytecode_handlers["useMacro"] = do_useMacro
def do_fillSlot(self, (slotName, block)):
# This is only executed if the enclosing 'use-macro' evaluates
# to 'default'.
self.interpret(block)
bytecode_handlers["fillSlot"] = do_fillSlot
def do_defineSlot(self, (slotName, block)):
if not self.metal:
self.interpret(block)
return
macs = self.macroStack
if macs and macs[-1] is not None:
macroName, slots = self.popMacro()[:2]
slot = slots.get(slotName)
if slot is not None:
prev_source = self.sourceFile
try:
self.interpret(slot)
finally:
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)
# Falling out of the 'if' allows the macro to be interpreted.
self.interpret(block)
bytecode_handlers["defineSlot"] = do_defineSlot
def do_onError(self, (block, handler)):
self.interpret(block)
def do_onError_tal(self, (block, handler)):
state = self.saveState()
self.stream = stream = self.StringIO()
self._stream_write = stream.write
try:
self.interpret(block)
except ConflictError:
raise
except:
exc = sys.exc_info()[1]
self.restoreState(state)
engine = self.engine
engine.beginScope()
error = engine.createErrorInfo(exc, self.position)
engine.setLocal('error', error)
try:
self.interpret(handler)
finally:
engine.endScope()
else:
self.restoreOutputState(state)
self.stream_write(stream.getvalue())
bytecode_handlers["onError"] = do_onError
bytecode_handlers_tal = bytecode_handlers.copy()
bytecode_handlers_tal["rawtextBeginScope"] = do_rawtextBeginScope_tal
bytecode_handlers_tal["beginScope"] = do_beginScope_tal
bytecode_handlers_tal["setLocal"] = do_setLocal_tal
bytecode_handlers_tal["setGlobal"] = do_setGlobal_tal
bytecode_handlers_tal["insertStructure"] = do_insertStructure_tal
bytecode_handlers_tal["insertText"] = do_insertText_tal
bytecode_handlers_tal["loop"] = do_loop_tal
bytecode_handlers_tal["onError"] = do_onError_tal
bytecode_handlers_tal["<attrAction>"] = attrAction_tal
bytecode_handlers_tal["optTag"] = do_optTag_tal
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)
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"
import zope.deprecation
zope.deprecation.moved('zope.tal.talinterpreter', '2.12')
import zope.deferredimport
zope.deferredimport.deprecated(
"'interpolate' has moved to zope.i18n. This reference will be gone "
"in Zope 2.12.",
interpolate = 'zope.i18n:interpolate'
)
......@@ -13,131 +13,8 @@
##############################################################################
"""
Parse XML and compile to TALInterpreter intermediate code.
"""
from XMLParser import XMLParser
from TALDefs import XML_NS, ZOPE_I18N_NS, ZOPE_METAL_NS, ZOPE_TAL_NS
from TALGenerator import TALGenerator
class TALParser(XMLParser):
ordered_attributes = 1
def __init__(self, gen=None): # Override
XMLParser.__init__(self)
if gen is None:
gen = TALGenerator()
self.gen = gen
self.nsStack = []
self.nsDict = {XML_NS: 'xml'}
self.nsNew = []
def getCode(self):
return self.gen.getCode()
def getWarnings(self):
return ()
def StartNamespaceDeclHandler(self, prefix, uri):
self.nsStack.append(self.nsDict.copy())
self.nsDict[uri] = prefix
self.nsNew.append((prefix, uri))
def EndNamespaceDeclHandler(self, prefix):
self.nsDict = self.nsStack.pop()
def StartElementHandler(self, name, attrs):
if self.ordered_attributes:
# attrs is a list of alternating names and values
attrlist = []
for i in range(0, len(attrs), 2):
key = attrs[i]
value = attrs[i+1]
attrlist.append((key, value))
else:
# attrs is a dict of {name: value}
attrlist = attrs.items()
attrlist.sort() # For definiteness
name, attrlist, taldict, metaldict, i18ndict \
= self.process_ns(name, attrlist)
attrlist = self.xmlnsattrs() + attrlist
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:
key, keybase, keyns = self.fixname(key)
ns = keyns or namens # default to tag namespace
item = key, value
if ns == 'metal':
metaldict[keybase] = value
item = item + ("metal",)
elif ns == 'tal':
taldict[keybase] = value
item = item + ("tal",)
elif ns == 'i18n':
i18ndict[keybase] = value
item = item + ('i18n',)
fixedattrlist.append(item)
if namens in ('metal', 'tal', 'i18n'):
taldict['tal tag'] = namens
return name, fixedattrlist, taldict, metaldict, i18ndict
def xmlnsattrs(self):
newlist = []
for prefix, uri in self.nsNew:
if prefix:
key = "xmlns:" + prefix
else:
key = "xmlns"
if uri in (ZOPE_METAL_NS, ZOPE_TAL_NS, ZOPE_I18N_NS):
item = (key, uri, "xmlns")
else:
item = (key, uri)
newlist.append(item)
self.nsNew = []
return newlist
def fixname(self, name):
if ' ' in name:
uri, name = name.split(' ')
prefix = self.nsDict[uri]
prefixed = name
if prefix:
prefixed = "%s:%s" % (prefix, name)
ns = 'x'
if uri == ZOPE_TAL_NS:
ns = 'tal'
elif uri == ZOPE_METAL_NS:
ns = 'metal'
elif uri == ZOPE_I18N_NS:
ns = 'i18n'
return (prefixed, name, ns)
return (name, name, None)
def EndElementHandler(self, name):
name = self.fixname(name)[0]
self.gen.emitEndElement(name)
def DefaultHandler(self, text):
self.gen.emitRawText(text)
def test():
import sys
p = TALParser()
file = "tests/input/test01.xml"
if sys.argv[1:]:
file = sys.argv[1]
p.parseFile(file)
program, macros = p.getCode()
from TALInterpreter import TALInterpreter
from DummyEngine import DummyEngine
engine = DummyEngine(macros)
TALInterpreter(program, macros, engine, sys.stdout, wrap=0)()
if __name__ == "__main__":
test()
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.talparser', '2.12')
......@@ -13,29 +13,9 @@
##############################################################################
"""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.
BBB 2005/05/01 -- to be removed after 12 months
$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
import zope.deprecation
zope.deprecation.moved('zope.tal.translationcontext', '2.12')
......@@ -13,75 +13,11 @@
##############################################################################
"""
Generic expat-based XML parser base class.
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.xmlparser', '2.12')
import xml.parsers.expat
from logging import getLogger
LOG = getLogger('TAL')
XMLParseError = xml.parsers.expat.ExpatError
class XMLParser:
ordered_attributes = 0
handler_names = [
"StartElementHandler",
"EndElementHandler",
"ProcessingInstructionHandler",
"CharacterDataHandler",
"UnparsedEntityDeclHandler",
"NotationDeclHandler",
"StartNamespaceDeclHandler",
"EndNamespaceDeclHandler",
"CommentHandler",
"StartCdataSectionHandler",
"EndCdataSectionHandler",
"DefaultHandler",
"DefaultHandlerExpand",
"NotStandaloneHandler",
"ExternalEntityRefHandler",
"XmlDeclHandler",
"StartDoctypeDeclHandler",
"EndDoctypeDeclHandler",
"ElementDeclHandler",
"AttlistDeclHandler"
]
def __init__(self, encoding=None):
self.parser = p = self.createParser()
if self.ordered_attributes:
try:
self.parser.ordered_attributes = self.ordered_attributes
except AttributeError:
LOG.info("Can't set ordered_attributes")
self.ordered_attributes = 0
for name in self.handler_names:
method = getattr(self, name, None)
if method is not None:
try:
setattr(p, name, method)
except AttributeError:
LOG.error("Can't set expat handler %s" % name)
def createParser(self, encoding=None):
return xml.parsers.expat.ParserCreate(encoding, ' ')
def parseFile(self, filename):
self.parseStream(open(filename))
def parseString(self, s):
self.parser.Parse(s, 1)
def parseURL(self, url):
import urllib
self.parseStream(urllib.urlopen(url))
def parseStream(self, stream):
self.parser.ParseFile(stream)
def parseFragment(self, s, end=0):
self.parser.Parse(s, end)
......@@ -34,165 +34,11 @@ Options:
Leave TAL/METAL attributes in output
-i
Leave I18N substitution strings un-interpolated.
"""
import os
import sys
import getopt
if __name__ == "__main__":
import setpath # Local hack to tweak sys.path etc.
# Import local classes
import TALDefs
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, default=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,
default=default)
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():
macros = 0
mode = None
showcode = 0
showtal = -1
strictinsert = 1
i18nInterpolate = 1
try:
opts, args = getopt.getopt(sys.argv[1:], "hHxlmsti",
['help', 'html', 'xml'])
except getopt.error, msg:
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 opt == '-l':
strictinsert = 0
if opt == '-m':
macros = 1
if opt == '-n':
versionTest = 0
if opt in ('-x', '--xml'):
if mode == 'html':
usage(1, '--html and --xml are mutually exclusive')
mode = "xml"
if opt == '-s':
showcode = 1
if opt == '-t':
showtal = 1
if opt == '-i':
i18nInterpolate = 0
if args:
file = args[0]
else:
file = FILE
it = compilefile(file, mode)
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, i18nInterpolate=1):
from TALInterpreter import TALInterpreter
program, macros = it
assert TALDefs.isCurrentVersion(program)
if engine is None:
engine = DummyEngine(macros)
TALInterpreter(program, macros, engine, stream, wrap=0,
tal=tal, showtal=showtal, strictinsert=strictinsert,
i18nInterpolate=i18nInterpolate)()
def compilefile(file, mode=None):
assert mode in ("html", "xml", None)
if mode is None:
ext = os.path.splitext(file)[1]
if ext.lower() in (".html", ".htm"):
mode = "html"
else:
mode = "xml"
if mode == "html":
from HTMLTALParser import HTMLTALParser
p = HTMLTALParser()
else:
from TALParser import TALParser
p = TALParser()
p.parseFile(file)
return p.getCode()
def showit(it):
from pprint import pprint
pprint(it)
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.driver', '2.12')
if __name__ == "__main__":
main()
#! /usr/bin/env python
# Module ndiff version 1.6.0
# Released to the public domain 08-Dec-2000,
# by Tim Peters (tim.one@home.com).
# Provided as-is; use at your own risk; no warranty; no promises; enjoy!
"""ndiff [-q] file1 file2
or
ndiff (-r1 | -r2) < ndiff_output > file1_or_file2
Print a human-friendly file difference report to stdout. Both inter-
and intra-line differences are noted. In the second form, recreate file1
(-r1) or file2 (-r2) on stdout, from an ndiff report on stdin.
In the first form, if -q ("quiet") is not specified, the first two lines
of output are
-: file1
+: file2
Each remaining line begins with a two-letter code:
"- " line unique to file1
"+ " line unique to file2
" " line common to both files
"? " line not present in either input file
Lines beginning with "? " attempt to guide the eye to intraline
differences, and were not present in either input file. These lines can be
confusing if the source files contain tab characters.
The first file can be recovered by retaining only lines that begin with
" " or "- ", and deleting those 2-character prefixes; use ndiff with -r1.
The second file can be recovered similarly, but by retaining only " " and
"+ " lines; use ndiff with -r2; or, on Unix, the second file can be
recovered by piping the output through
sed -n '/^[+ ] /s/^..//p'
See module comments for details and programmatic interface.
"""
__version__ = 1, 5, 0
# SequenceMatcher tries to compute a "human-friendly diff" between
# two sequences (chiefly picturing a file as a sequence of lines,
# and a line as a sequence of characters, here). Unlike e.g. UNIX(tm)
# diff, the fundamental notion is the longest *contiguous* & junk-free
# matching subsequence. That's what catches peoples' eyes. The
# Windows(tm) windiff has another interesting notion, pairing up elements
# that appear uniquely in each sequence. That, and the method here,
# appear to yield more intuitive difference reports than does diff. This
# method appears to be the least vulnerable to synching up on blocks
# of "junk lines", though (like blank lines in ordinary text files,
# or maybe "<P>" lines in HTML files). That may be because this is
# the only method of the 3 that has a *concept* of "junk" <wink>.
#
# Note that ndiff makes no claim to produce a *minimal* diff. To the
# contrary, minimal diffs are often counter-intuitive, because they
# synch up anywhere possible, sometimes accidental matches 100 pages
# apart. Restricting synch points to contiguous matches preserves some
# notion of locality, at the occasional cost of producing a longer diff.
#
# With respect to junk, an earlier version of ndiff simply refused to
# *start* a match with a junk element. The result was cases like this:
# before: private Thread currentThread;
# after: private volatile Thread currentThread;
# If you consider whitespace to be junk, the longest contiguous match
# not starting with junk is "e Thread currentThread". So ndiff reported
# that "e volatil" was inserted between the 't' and the 'e' in "private".
# While an accurate view, to people that's absurd. The current version
# looks for matching blocks that are entirely junk-free, then extends the
# longest one of those as far as possible but only with matching junk.
# So now "currentThread" is matched, then extended to suck up the
# preceding blank; then "private" is matched, and extended to suck up the
# following blank; then "Thread" is matched; and finally ndiff reports
# that "volatile " was inserted before "Thread". The only quibble
# remaining is that perhaps it was really the case that " volatile"
# was inserted after "private". I can live with that <wink>.
#
# NOTE on junk: the module-level names
# IS_LINE_JUNK
# IS_CHARACTER_JUNK
# can be set to any functions you like. The first one should accept
# a single string argument, and return true iff the string is junk.
# The default is whether the regexp r"\s*#?\s*$" matches (i.e., a
# line without visible characters, except for at most one splat).
# The second should accept a string of length 1 etc. The default is
# whether the character is a blank or tab (note: bad idea to include
# newline in this!).
#
# After setting those, you can call fcompare(f1name, f2name) with the
# names of the files you want to compare. The difference report
# is sent to stdout. Or you can call main(args), passing what would
# have been in sys.argv[1:] had the cmd-line form been used.
TRACE = 0
# define what "junk" means
import re
def IS_LINE_JUNK(line, pat=re.compile(r"\s*#?\s*$").match):
return pat(line) is not None
def IS_CHARACTER_JUNK(ch, ws=" \t"):
return ch in ws
del re
class SequenceMatcher:
def __init__(self, isjunk=None, a='', b=''):
# Members:
# a
# first sequence
# b
# second sequence; differences are computed as "what do
# we need to do to 'a' to change it into 'b'?"
# b2j
# for x in b, b2j[x] is a list of the indices (into b)
# at which x appears; junk elements do not appear
# b2jhas
# b2j.has_key
# fullbcount
# for x in b, fullbcount[x] == the number of times x
# appears in b; only materialized if really needed (used
# only for computing quick_ratio())
# matching_blocks
# a list of (i, j, k) triples, where a[i:i+k] == b[j:j+k];
# ascending & non-overlapping in i and in j; terminated by
# a dummy (len(a), len(b), 0) sentinel
# opcodes
# a list of (tag, i1, i2, j1, j2) tuples, where tag is
# one of
# 'replace' a[i1:i2] should be replaced by b[j1:j2]
# 'delete' a[i1:i2] should be deleted
# 'insert' b[j1:j2] should be inserted
# 'equal' a[i1:i2] == b[j1:j2]
# isjunk
# a user-supplied function taking a sequence element and
# returning true iff the element is "junk" -- this has
# subtle but helpful effects on the algorithm, which I'll
# get around to writing up someday <0.9 wink>.
# DON'T USE! Only __chain_b uses this. Use isbjunk.
# isbjunk
# for x in b, isbjunk(x) == isjunk(x) but much faster;
# it's really the has_key method of a hidden dict.
# DOES NOT WORK for x in a!
self.isjunk = isjunk
self.a = self.b = None
self.set_seqs(a, b)
def set_seqs(self, a, b):
self.set_seq1(a)
self.set_seq2(b)
def set_seq1(self, a):
if a is self.a:
return
self.a = a
self.matching_blocks = self.opcodes = None
def set_seq2(self, b):
if b is self.b:
return
self.b = b
self.matching_blocks = self.opcodes = None
self.fullbcount = None
self.__chain_b()
# For each element x in b, set b2j[x] to a list of the indices in
# b where x appears; the indices are in increasing order; note that
# the number of times x appears in b is len(b2j[x]) ...
# when self.isjunk is defined, junk elements don't show up in this
# map at all, which stops the central find_longest_match method
# from starting any matching block at a junk element ...
# also creates the fast isbjunk function ...
# note that this is only called when b changes; so for cross-product
# kinds of matches, it's best to call set_seq2 once, then set_seq1
# repeatedly
def __chain_b(self):
# Because isjunk is a user-defined (not C) function, and we test
# for junk a LOT, it's important to minimize the number of calls.
# Before the tricks described here, __chain_b was by far the most
# time-consuming routine in the whole module! If anyone sees
# Jim Roskind, thank him again for profile.py -- I never would
# have guessed that.
# The first trick is to build b2j ignoring the possibility
# of junk. I.e., we don't call isjunk at all yet. Throwing
# out the junk later is much cheaper than building b2j "right"
# from the start.
b = self.b
self.b2j = b2j = {}
self.b2jhas = b2jhas = b2j.has_key
for i in xrange(len(b)):
elt = b[i]
if b2jhas(elt):
b2j[elt].append(i)
else:
b2j[elt] = [i]
# Now b2j.keys() contains elements uniquely, and especially when
# the sequence is a string, that's usually a good deal smaller
# than len(string). The difference is the number of isjunk calls
# saved.
isjunk, junkdict = self.isjunk, {}
if isjunk:
for elt in b2j.keys():
if isjunk(elt):
junkdict[elt] = 1 # value irrelevant; it's a set
del b2j[elt]
# Now for x in b, isjunk(x) == junkdict.has_key(x), but the
# latter is much faster. Note too that while there may be a
# lot of junk in the sequence, the number of *unique* junk
# elements is probably small. So the memory burden of keeping
# this dict alive is likely trivial compared to the size of b2j.
self.isbjunk = junkdict.has_key
def find_longest_match(self, alo, ahi, blo, bhi):
"""Find longest matching block in a[alo:ahi] and b[blo:bhi].
If isjunk is not defined:
Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
alo <= i <= i+k <= ahi
blo <= j <= j+k <= bhi
and for all (i',j',k') meeting those conditions,
k >= k'
i <= i'
and if i == i', j <= j'
In other words, of all maximal matching blocks, return one
that starts earliest in a, and of all those maximal matching
blocks that start earliest in a, return the one that starts
earliest in b.
If isjunk is defined, first the longest matching block is
determined as above, but with the additional restriction that
no junk element appears in the block. Then that block is
extended as far as possible by matching (only) junk elements on
both sides. So the resulting block never matches on junk except
as identical junk happens to be adjacent to an "interesting"
match.
If no blocks match, return (alo, blo, 0).
"""
# CAUTION: stripping common prefix or suffix would be incorrect.
# E.g.,
# ab
# acab
# Longest matching block is "ab", but if common prefix is
# stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
# strip, so ends up claiming that ab is changed to acab by
# inserting "ca" in the middle. That's minimal but unintuitive:
# "it's obvious" that someone inserted "ac" at the front.
# Windiff ends up at the same place as diff, but by pairing up
# the unique 'b's and then matching the first two 'a's.
a, b, b2j, isbjunk = self.a, self.b, self.b2j, self.isbjunk
besti, bestj, bestsize = alo, blo, 0
# find longest junk-free match
# during an iteration of the loop, j2len[j] = length of longest
# junk-free match ending with a[i-1] and b[j]
j2len = {}
nothing = []
for i in xrange(alo, ahi):
# look at all instances of a[i] in b; note that because
# b2j has no junk keys, the loop is skipped if a[i] is junk
j2lenget = j2len.get
newj2len = {}
for j in b2j.get(a[i], nothing):
# a[i] matches b[j]
if j < blo:
continue
if j >= bhi:
break
k = newj2len[j] = j2lenget(j-1, 0) + 1
if k > bestsize:
besti, bestj, bestsize = i-k+1, j-k+1, k
j2len = newj2len
# Now that we have a wholly interesting match (albeit possibly
# empty!), we may as well suck up the matching junk on each
# side of it too. Can't think of a good reason not to, and it
# saves post-processing the (possibly considerable) expense of
# figuring out what to do with it. In the case of an empty
# interesting match, this is clearly the right thing to do,
# because no other kind of match is possible in the regions.
while besti > alo and bestj > blo and \
isbjunk(b[bestj-1]) and \
a[besti-1] == b[bestj-1]:
besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
while besti+bestsize < ahi and bestj+bestsize < bhi and \
isbjunk(b[bestj+bestsize]) and \
a[besti+bestsize] == b[bestj+bestsize]:
bestsize = bestsize + 1
if TRACE:
print "get_matching_blocks", alo, ahi, blo, bhi
print " returns", besti, bestj, bestsize
return besti, bestj, bestsize
def get_matching_blocks(self):
if self.matching_blocks is not None:
return self.matching_blocks
self.matching_blocks = []
la, lb = len(self.a), len(self.b)
self.__helper(0, la, 0, lb, self.matching_blocks)
self.matching_blocks.append( (la, lb, 0) )
if TRACE:
print '*** matching blocks', self.matching_blocks
return self.matching_blocks
# builds list of matching blocks covering a[alo:ahi] and
# b[blo:bhi], appending them in increasing order to answer
def __helper(self, alo, ahi, blo, bhi, answer):
i, j, k = x = self.find_longest_match(alo, ahi, blo, bhi)
# a[alo:i] vs b[blo:j] unknown
# a[i:i+k] same as b[j:j+k]
# a[i+k:ahi] vs b[j+k:bhi] unknown
if k:
if alo < i and blo < j:
self.__helper(alo, i, blo, j, answer)
answer.append(x)
if i+k < ahi and j+k < bhi:
self.__helper(i+k, ahi, j+k, bhi, answer)
def ratio(self):
"""Return a measure of the sequences' similarity (float in [0,1]).
Where T is the total number of elements in both sequences, and
M is the number of matches, this is 2*M / T.
Note that this is 1 if the sequences are identical, and 0 if
they have nothing in common.
"""
matches = reduce(lambda sum, triple: sum + triple[-1],
self.get_matching_blocks(), 0)
return 2.0 * matches / (len(self.a) + len(self.b))
def quick_ratio(self):
"""Return an upper bound on ratio() relatively quickly."""
# viewing a and b as multisets, set matches to the cardinality
# of their intersection; this counts the number of matches
# without regard to order, so is clearly an upper bound
if self.fullbcount is None:
self.fullbcount = fullbcount = {}
for elt in self.b:
fullbcount[elt] = fullbcount.get(elt, 0) + 1
fullbcount = self.fullbcount
# avail[x] is the number of times x appears in 'b' less the
# number of times we've seen it in 'a' so far ... kinda
avail = {}
availhas, matches = avail.has_key, 0
for elt in self.a:
if availhas(elt):
numb = avail[elt]
else:
numb = fullbcount.get(elt, 0)
avail[elt] = numb - 1
if numb > 0:
matches = matches + 1
return 2.0 * matches / (len(self.a) + len(self.b))
def real_quick_ratio(self):
"""Return an upper bound on ratio() very quickly"""
la, lb = len(self.a), len(self.b)
# can't have more matches than the number of elements in the
# shorter sequence
return 2.0 * min(la, lb) / (la + lb)
def get_opcodes(self):
if self.opcodes is not None:
return self.opcodes
i = j = 0
self.opcodes = answer = []
for ai, bj, size in self.get_matching_blocks():
# invariant: we've pumped out correct diffs to change
# a[:i] into b[:j], and the next matching block is
# a[ai:ai+size] == b[bj:bj+size]. So we need to pump
# out a diff to change a[i:ai] into b[j:bj], pump out
# the matching block, and move (i,j) beyond the match
tag = ''
if i < ai and j < bj:
tag = 'replace'
elif i < ai:
tag = 'delete'
elif j < bj:
tag = 'insert'
if tag:
answer.append( (tag, i, ai, j, bj) )
i, j = ai+size, bj+size
# the list of matching blocks is terminated by a
# sentinel with size 0
if size:
answer.append( ('equal', ai, i, bj, j) )
return answer
# meant for dumping lines
def dump(tag, x, lo, hi):
for i in xrange(lo, hi):
print tag, x[i],
def plain_replace(a, alo, ahi, b, blo, bhi):
assert alo < ahi and blo < bhi
# dump the shorter block first -- reduces the burden on short-term
# memory if the blocks are of very different sizes
if bhi - blo < ahi - alo:
dump('+', b, blo, bhi)
dump('-', a, alo, ahi)
else:
dump('-', a, alo, ahi)
dump('+', b, blo, bhi)
# When replacing one block of lines with another, this guy searches
# the blocks for *similar* lines; the best-matching pair (if any) is
# used as a synch point, and intraline difference marking is done on
# the similar pair. Lots of work, but often worth it.
def fancy_replace(a, alo, ahi, b, blo, bhi):
if TRACE:
print '*** fancy_replace', alo, ahi, blo, bhi
dump('>', a, alo, ahi)
dump('<', b, blo, bhi)
# don't synch up unless the lines have a similarity score of at
# least cutoff; best_ratio tracks the best score seen so far
best_ratio, cutoff = 0.74, 0.75
cruncher = SequenceMatcher(IS_CHARACTER_JUNK)
eqi, eqj = None, None # 1st indices of equal lines (if any)
# search for the pair that matches best without being identical
# (identical lines must be junk lines, & we don't want to synch up
# on junk -- unless we have to)
for j in xrange(blo, bhi):
bj = b[j]
cruncher.set_seq2(bj)
for i in xrange(alo, ahi):
ai = a[i]
if ai == bj:
if eqi is None:
eqi, eqj = i, j
continue
cruncher.set_seq1(ai)
# computing similarity is expensive, so use the quick
# upper bounds first -- have seen this speed up messy
# compares by a factor of 3.
# note that ratio() is only expensive to compute the first
# time it's called on a sequence pair; the expensive part
# of the computation is cached by cruncher
if cruncher.real_quick_ratio() > best_ratio and \
cruncher.quick_ratio() > best_ratio and \
cruncher.ratio() > best_ratio:
best_ratio, best_i, best_j = cruncher.ratio(), i, j
if best_ratio < cutoff:
# no non-identical "pretty close" pair
if eqi is None:
# no identical pair either -- treat it as a straight replace
plain_replace(a, alo, ahi, b, blo, bhi)
return
# no close pair, but an identical pair -- synch up on that
best_i, best_j, best_ratio = eqi, eqj, 1.0
else:
# there's a close pair, so forget the identical pair (if any)
eqi = None
# a[best_i] very similar to b[best_j]; eqi is None iff they're not
# identical
if TRACE:
print '*** best_ratio', best_ratio, best_i, best_j
dump('>', a, best_i, best_i+1)
dump('<', b, best_j, best_j+1)
# pump out diffs from before the synch point
fancy_helper(a, alo, best_i, b, blo, best_j)
# do intraline marking on the synch pair
aelt, belt = a[best_i], b[best_j]
if eqi is None:
# pump out a '-', '?', '+', '?' quad for the synched lines
atags = btags = ""
cruncher.set_seqs(aelt, belt)
for tag, ai1, ai2, bj1, bj2 in cruncher.get_opcodes():
la, lb = ai2 - ai1, bj2 - bj1
if tag == 'replace':
atags = atags + '^' * la
btags = btags + '^' * lb
elif tag == 'delete':
atags = atags + '-' * la
elif tag == 'insert':
btags = btags + '+' * lb
elif tag == 'equal':
atags = atags + ' ' * la
btags = btags + ' ' * lb
else:
raise ValueError, 'unknown tag ' + `tag`
printq(aelt, belt, atags, btags)
else:
# the synch pair is identical
print ' ', aelt,
# pump out diffs from after the synch point
fancy_helper(a, best_i+1, ahi, b, best_j+1, bhi)
def fancy_helper(a, alo, ahi, b, blo, bhi):
if alo < ahi:
if blo < bhi:
fancy_replace(a, alo, ahi, b, blo, bhi)
else:
dump('-', a, alo, ahi)
elif blo < bhi:
dump('+', b, blo, bhi)
# Crap to deal with leading tabs in "?" output. Can hurt, but will
# probably help most of the time.
def printq(aline, bline, atags, btags):
common = min(count_leading(aline, "\t"),
count_leading(bline, "\t"))
common = min(common, count_leading(atags[:common], " "))
print "-", aline,
if count_leading(atags, " ") < len(atags):
print "?", "\t" * common + atags[common:]
print "+", bline,
if count_leading(btags, " ") < len(btags):
print "?", "\t" * common + btags[common:]
def count_leading(line, ch):
i, n = 0, len(line)
while i < n and line[i] == ch:
i = i + 1
return i
def fail(msg):
import sys
out = sys.stderr.write
out(msg + "\n\n")
out(__doc__)
return 0
# open a file & return the file object; gripe and return 0 if it
# couldn't be opened
def fopen(fname):
try:
return open(fname, 'r')
except IOError, detail:
return fail("couldn't open " + fname + ": " + str(detail))
# open two files & spray the diff to stdout; return false iff a problem
def fcompare(f1name, f2name):
f1 = fopen(f1name)
f2 = fopen(f2name)
if not f1 or not f2:
return 0
a = f1.readlines(); f1.close()
b = f2.readlines(); f2.close()
cruncher = SequenceMatcher(IS_LINE_JUNK, a, b)
for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
if tag == 'replace':
fancy_replace(a, alo, ahi, b, blo, bhi)
elif tag == 'delete':
dump('-', a, alo, ahi)
elif tag == 'insert':
dump('+', b, blo, bhi)
elif tag == 'equal':
dump(' ', a, alo, ahi)
else:
raise ValueError, 'unknown tag ' + `tag`
return 1
# crack args (sys.argv[1:] is normal) & compare;
# return false iff a problem
def main(args):
import getopt
try:
opts, args = getopt.getopt(args, "qr:")
except getopt.error, detail:
return fail(str(detail))
noisy = 1
qseen = rseen = 0
for opt, val in opts:
if opt == "-q":
qseen = 1
noisy = 0
elif opt == "-r":
rseen = 1
whichfile = val
if qseen and rseen:
return fail("can't specify both -q and -r")
if rseen:
if args:
return fail("no args allowed with -r option")
if whichfile in "12":
restore(whichfile)
return 1
return fail("-r value must be 1 or 2")
if len(args) != 2:
return fail("need 2 filename args")
f1name, f2name = args
if noisy:
print '-:', f1name
print '+:', f2name
return fcompare(f1name, f2name)
def restore(which):
import sys
tag = {"1": "- ", "2": "+ "}[which]
prefixes = (" ", tag)
for line in sys.stdin.readlines():
if line[:2] in prefixes:
print line[2:],
# BBB 2005/05/01 -- to be removed after 12 months
import zope.deprecation
zope.deprecation.moved('zope.tal.ndiff', '2.12')
if __name__ == '__main__':
import sys
......
......@@ -14,138 +14,11 @@
##############################################################################
"""
Driver program to run METAL and TAL regression tests.
"""
import sys
import os
from cStringIO import StringIO
import glob
import traceback
if __name__ == "__main__":
import setpath # Local hack to tweak sys.path etc.
import driver
import tests.utils
def showdiff(a, b):
import ndiff
cruncher = ndiff.SequenceMatcher(ndiff.IS_LINE_JUNK, a, b)
for tag, alo, ahi, blo, bhi in cruncher.get_opcodes():
if tag == "equal":
continue
print nicerange(alo, ahi) + tag[0] + nicerange(blo, bhi)
ndiff.dump('<', a, alo, ahi)
if a and b:
print '---'
ndiff.dump('>', b, blo, bhi)
def nicerange(lo, hi):
if hi <= lo+1:
return str(lo+1)
else:
return "%d,%d" % (lo+1, hi)
def main():
opts = []
args = sys.argv[1:]
quiet = 0
unittesting = 0
if args and args[0] == "-q":
quiet = 1
del args[0]
if args and args[0] == "-Q":
unittesting = 1
del args[0]
while args and args[0].startswith('-'):
opts.append(args[0])
del args[0]
if not args:
prefix = os.path.join("tests", "input", "test*.")
if tests.utils.skipxml:
xmlargs = []
else:
xmlargs = glob.glob(prefix + "xml")
xmlargs.sort()
htmlargs = glob.glob(prefix + "html")
htmlargs.sort()
args = xmlargs + htmlargs
if not args:
sys.stderr.write("No tests found -- please supply filenames\n")
sys.exit(1)
errors = 0
for arg in args:
locopts = []
if arg.find("metal") >= 0 and "-m" not in opts:
locopts.append("-m")
if not unittesting:
print arg,
sys.stdout.flush()
if tests.utils.skipxml and arg.endswith(".xml"):
print "SKIPPED (XML parser not available)"
continue
save = sys.stdout, sys.argv
try:
try:
sys.stdout = stdout = StringIO()
sys.argv = [""] + opts + locopts + [arg]
driver.main()
finally:
sys.stdout, sys.argv = save
except SystemExit:
raise
except:
errors = 1
if quiet:
print sys.exc_type
sys.stdout.flush()
else:
if unittesting:
print
else:
print "Failed:"
sys.stdout.flush()
traceback.print_exc()
continue
head, tail = os.path.split(arg)
outfile = os.path.join(
head.replace("input", "output"),
tail)
try:
f = open(outfile)
except IOError:
expected = None
print "(missing file %s)" % outfile,
else:
expected = f.readlines()
f.close()
stdout.seek(0)
if hasattr(stdout, "readlines"):
actual = stdout.readlines()
else:
actual = readlines(stdout)
if actual == expected:
if not unittesting:
print "OK"
else:
if unittesting:
print
else:
print "not OK"
errors = 1
if not quiet and expected is not None:
showdiff(expected, actual)
if errors:
sys.exit(1)
def readlines(f):
L = []
while 1:
line = f.readline()
if not line:
break
L.append(line)
return L
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.runtest', '2.12')
if __name__ == "__main__":
main()
#!/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.1 (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.
#
##############################################################################
"""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.2.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]
# ---------------------------------------------
# only non-empty msgids are added to dictionary
# (changed by heinrichbernd - 2004/09/07)
# ---------------------------------------------
if msgid:
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')
# BBB 2005/05/01 -- to be removed after 12 months
import zope.deprecation
zope.deprecation.moved('zope.tal.talgettext', '2.12')
if __name__ == '__main__':
main()
......@@ -14,44 +14,11 @@
##############################################################################
"""
Helper program to time compilation and interpretation
"""
import sys
import time
import getopt
from cPickle import dumps, loads
from cStringIO import StringIO
from driver import FILE, compilefile, interpretit
def main():
count = 10
try:
opts, args = getopt.getopt(sys.argv[1:], "n:")
except getopt.error, msg:
print msg
sys.exit(2)
for o, a in opts:
if o == "-n":
count = int(a)
if not args:
args = [FILE]
for file in args:
print file
dummyfile = StringIO()
it = timefunc(count, compilefile, file)
timefunc(count, interpretit, it, None, dummyfile)
def timefunc(count, func, *args):
sys.stderr.write("%-14s: " % func.__name__)
sys.stderr.flush()
t0 = time.clock()
for i in range(count):
result = func(*args)
t1 = time.clock()
sys.stderr.write("%6.3f secs for %d calls, i.e. %4.0f msecs per call\n"
% ((t1-t0), count, 1000*(t1-t0)/count))
return result
BBB 2005/05/01 -- to be removed after 12 months
"""
import zope.deprecation
zope.deprecation.moved('zope.tal.timer', '2.12')
if __name__ == "__main__":
main()
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment