From: richard Date: Thu, 5 Sep 2002 00:37:09 +0000 (+0000) Subject: moved X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=686ad3763df6183d8fabb8ccbeb7e0fb7bbc4e22;p=roundup.git moved git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1068 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/PageTemplates/.cvsignore b/PageTemplates/.cvsignore deleted file mode 100644 index 0d20b64..0000000 --- a/PageTemplates/.cvsignore +++ /dev/null @@ -1 +0,0 @@ -*.pyc diff --git a/PageTemplates/Expressions.py b/PageTemplates/Expressions.py deleted file mode 100644 index b2adad2..0000000 --- a/PageTemplates/Expressions.py +++ /dev/null @@ -1,375 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## - -"""Page Template Expression Engine - -Page Template-specific implementation of TALES, with handlers -for Python expressions, string literals, and paths. -""" - -__version__='$Revision: 1.1 $'[11:-2] - -import re, sys -from TALES import Engine, CompilerError, _valid_name, NAME_RE, \ - Undefined, Default, _parse_expr -from string import strip, split, join, replace, lstrip -from Acquisition import aq_base, aq_inner, aq_parent - - -_engine = None -def getEngine(): - global _engine - if _engine is None: - from PathIterator import Iterator - _engine = Engine(Iterator) - installHandlers(_engine) - return _engine - -def installHandlers(engine): - reg = engine.registerType - pe = PathExpr - for pt in ('standard', 'path', 'exists', 'nocall'): - reg(pt, pe) - reg('string', StringExpr) - reg('python', PythonExpr) - reg('not', NotExpr) - reg('defer', DeferExpr) - -if sys.modules.has_key('Zope'): - import AccessControl - from AccessControl import getSecurityManager - try: - from AccessControl import Unauthorized - except ImportError: - Unauthorized = "Unauthorized" - if hasattr(AccessControl, 'full_read_guard'): - from ZRPythonExpr import PythonExpr, _SecureModuleImporter, \ - call_with_ns - else: - from ZPythonExpr import PythonExpr, _SecureModuleImporter, \ - call_with_ns -else: - from PythonExpr import getSecurityManager, PythonExpr - try: - from zExceptions import Unauthorized - except ImportError: - Unauthorized = "Unauthorized" - def call_with_ns(f, ns, arg=1): - if arg==2: - return f(None, ns) - else: - return f(ns) - - class _SecureModuleImporter: - """Simple version of the importer for use with trusted code.""" - __allow_access_to_unprotected_subobjects__ = 1 - def __getitem__(self, module): - __import__(module) - return sys.modules[module] - -SecureModuleImporter = _SecureModuleImporter() - -Undefs = (Undefined, AttributeError, KeyError, - TypeError, IndexError, Unauthorized) - -def render(ob, ns): - """ - Calls the object, possibly a document template, or just returns it if - not callable. (From DT_Util.py) - """ - if hasattr(ob, '__render_with_namespace__'): - ob = call_with_ns(ob.__render_with_namespace__, ns) - else: - base = aq_base(ob) - if callable(base): - try: - if getattr(base, 'isDocTemp', 0): - ob = call_with_ns(ob, ns, 2) - else: - ob = ob() - except AttributeError, n: - if str(n) != '__call__': - raise - return ob - -class SubPathExpr: - def __init__(self, path): - self._path = path = split(strip(path), '/') - self._base = base = path.pop(0) - if not _valid_name(base): - raise CompilerError, 'Invalid variable name "%s"' % base - # Parse path - self._dp = dp = [] - for i in range(len(path)): - e = path[i] - if e[:1] == '?' and _valid_name(e[1:]): - dp.append((i, e[1:])) - dp.reverse() - - def _eval(self, econtext, - list=list, isinstance=isinstance, StringType=type('')): - vars = econtext.vars - path = self._path - if self._dp: - path = list(path) # Copy! - for i, varname in self._dp: - val = vars[varname] - if isinstance(val, StringType): - path[i] = val - else: - # If the value isn't a string, assume it's a sequence - # of path names. - path[i:i+1] = list(val) - __traceback_info__ = base = self._base - if base == 'CONTEXTS': - ob = econtext.contexts - else: - ob = vars[base] - if isinstance(ob, DeferWrapper): - ob = ob() - if path: - ob = restrictedTraverse(ob, path, getSecurityManager()) - return ob - -class PathExpr: - def __init__(self, name, expr, engine): - self._s = expr - self._name = name - paths = split(expr, '|') - self._subexprs = [] - add = self._subexprs.append - for i in range(len(paths)): - path = lstrip(paths[i]) - if _parse_expr(path): - # This part is the start of another expression type, - # so glue it back together and compile it. - add(engine.compile(lstrip(join(paths[i:], '|')))) - break - add(SubPathExpr(path)._eval) - - def _exists(self, econtext): - for expr in self._subexprs: - try: - expr(econtext) - except Undefs: - pass - else: - return 1 - return 0 - - def _eval(self, econtext, - isinstance=isinstance, StringType=type(''), render=render): - for expr in self._subexprs[:-1]: - # Try all but the last subexpression, skipping undefined ones. - try: - ob = expr(econtext) - except Undefs: - pass - else: - break - else: - # On the last subexpression allow exceptions through. - ob = self._subexprs[-1](econtext) - - if self._name == 'nocall' or isinstance(ob, StringType): - return ob - # Return the rendered object - return render(ob, econtext.vars) - - def __call__(self, econtext): - if self._name == 'exists': - return self._exists(econtext) - return self._eval(econtext) - - def __str__(self): - return '%s expression %s' % (self._name, `self._s`) - - def __repr__(self): - return '%s:%s' % (self._name, `self._s`) - - -_interp = re.compile(r'\$(%(n)s)|\${(%(n)s(?:/%(n)s)*)}' % {'n': NAME_RE}) - -class StringExpr: - def __init__(self, name, expr, engine): - self._s = expr - if '%' in expr: - expr = replace(expr, '%', '%%') - self._vars = vars = [] - if '$' in expr: - parts = [] - for exp in split(expr, '$$'): - if parts: parts.append('$') - m = _interp.search(exp) - while m is not None: - parts.append(exp[:m.start()]) - parts.append('%s') - vars.append(PathExpr('path', m.group(1) or m.group(2), - engine)) - exp = exp[m.end():] - m = _interp.search(exp) - if '$' in exp: - raise CompilerError, ( - '$ must be doubled or followed by a simple path') - parts.append(exp) - expr = join(parts, '') - self._expr = expr - - def __call__(self, econtext): - vvals = [] - for var in self._vars: - v = var(econtext) - if isinstance(v, Exception): - raise v - vvals.append(v) - return self._expr % tuple(vvals) - - def __str__(self): - return 'string expression %s' % `self._s` - - def __repr__(self): - return 'string:%s' % `self._s` - -class NotExpr: - def __init__(self, name, expr, compiler): - self._s = expr = lstrip(expr) - self._c = compiler.compile(expr) - - def __call__(self, econtext): - return not econtext.evaluateBoolean(self._c) - - def __repr__(self): - return 'not:%s' % `self._s` - -class DeferWrapper: - def __init__(self, expr, econtext): - self._expr = expr - self._econtext = econtext - - def __str__(self): - return str(self()) - - def __call__(self): - return self._expr(self._econtext) - -class DeferExpr: - def __init__(self, name, expr, compiler): - self._s = expr = lstrip(expr) - self._c = compiler.compile(expr) - - def __call__(self, econtext): - return DeferWrapper(self._c, econtext) - - def __repr__(self): - return 'defer:%s' % `self._s` - - -def restrictedTraverse(self, path, securityManager, - get=getattr, has=hasattr, N=None, M=[], - TupleType=type(()) ): - - REQUEST = {'path': path} - REQUEST['TraversalRequestNameStack'] = path = path[:] # Copy! - if not path[0]: - # If the path starts with an empty string, go to the root first. - self = self.getPhysicalRoot() - if not securityManager.validateValue(self): - raise Unauthorized, name - path.pop(0) - - path.reverse() - validate = securityManager.validate - object = self - #print 'TRAVERSE', (object, path) - while path: - __traceback_info__ = REQUEST - name = path.pop() - - if isinstance(name, TupleType): - object = apply(object, name) - continue - - if name[0] == '_': - # Never allowed in a URL. - raise AttributeError, name - - if name=='..': - o = get(object, 'aq_parent', M) - if o is not M: - if not validate(object, object, name, o): - raise Unauthorized, name - object=o - continue - - t = get(object, '__bobo_traverse__', N) - if t is not N: - o=t(REQUEST, name) - - container = None - if has(o, 'im_self'): - container = o.im_self - elif (has(get(object, 'aq_base', object), name) - and get(object, name) == o): - container = object - if not validate(object, container, name, o): - raise Unauthorized, name - else: - # Try an attribute. - o = get(object, name, M) - # print '...', (object, name, M, o) - if o is not M: - # Check access to the attribute. - if has(object, 'aq_acquire'): - object.aq_acquire( - name, validate2, validate) - else: - if not validate(object, object, name, o): - raise Unauthorized, name - else: - # Try an item. - try: - # XXX maybe in Python 2.2 we can just check whether - # the object has the attribute "__getitem__" - # instead of blindly catching exceptions. - # print 'Try an item', (object, name) - o = object[name] - except AttributeError, exc: - if str(exc).find('__getitem__') >= 0: - # The object does not support the item interface. - # Try to re-raise the original attribute error. - # XXX I think this only happens with - # ExtensionClass instances. - get(object, name) - raise - except TypeError, exc: - if str(exc).find('unsubscriptable') >= 0: - # The object does not support the item interface. - # Try to re-raise the original attribute error. - # XXX This is sooooo ugly. - get(object, name) - raise - else: - # Check access to the item. - if not validate(object, object, name, o): - raise Unauthorized, name - #print '... object is now', `o` - object = o - - return object - - -def validate2(orig, inst, name, v, real_validate): - if not real_validate(orig, inst, name, v): - raise Unauthorized, name - return 1 - diff --git a/PageTemplates/PageTemplate.py b/PageTemplates/PageTemplate.py deleted file mode 100755 index d97b2a5..0000000 --- a/PageTemplates/PageTemplate.py +++ /dev/null @@ -1,207 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -"""Page Template module - -HTML- and XML-based template objects using TAL, TALES, and METAL. -""" - -__version__='$Revision: 1.1 $'[11:-2] - -import sys - -from TAL.TALParser import TALParser -from TAL.HTMLTALParser import HTMLTALParser -from TAL.TALGenerator import TALGenerator -from TAL.TALInterpreter import TALInterpreter -from Expressions import getEngine -from string import join, strip, rstrip, split, replace, lower, find -from cStringIO import StringIO -from ExtensionClass import Base -from ComputedAttribute import ComputedAttribute - - -class PageTemplate(Base): - "Page Templates using TAL, TALES, and METAL" - - content_type = 'text/html' - expand = 0 - _v_errors = () - _v_warnings = () - _v_program = None - _v_macros = None - _v_cooked = 0 - id = '(unknown)' - _text = '' - _error_start = '') - if errend >= 0: - text = text[errend + 4:] - if self._text != text: - self._text = text - self._cook() - - def read(self): - if not self._v_cooked: - self._cook() - if not self._v_errors: - if not self.expand: - return self._text - try: - return self.pt_render(source=1) - except: - return ('%s\n Macro expansion failed\n %s\n-->\n%s' % - (self._error_start, "%s: %s" % sys.exc_info()[:2], - self._text) ) - - return ('%s\n %s\n-->\n%s' % (self._error_start, - join(self._v_errors, '\n '), - self._text)) - - def _cook(self): - """Compile the TAL and METAL statments. - - Cooking must not fail due to compilation errors in templates. - """ - source_file = self.pt_source_file() - if self.html(): - gen = TALGenerator(getEngine(), xml=0, source_file=source_file) - parser = HTMLTALParser(gen) - else: - gen = TALGenerator(getEngine(), source_file=source_file) - parser = TALParser(gen) - - self._v_errors = () - try: - parser.parseString(self._text) - self._v_program, self._v_macros = parser.getCode() - except: - self._v_errors = ["Compilation failed", - "%s: %s" % sys.exc_info()[:2]] - self._v_warnings = parser.getWarnings() - self._v_cooked = 1 - - def html(self): - if not hasattr(getattr(self, 'aq_base', self), 'is_html'): - return self.content_type == 'text/html' - return self.is_html - -class _ModuleImporter: - def __getitem__(self, module): - mod = __import__(module) - path = split(module, '.') - for name in path[1:]: - mod = getattr(mod, name) - return mod - -ModuleImporter = _ModuleImporter() - -class PTRuntimeError(RuntimeError): - '''The Page Template has template errors that prevent it from rendering.''' - pass - - -class PageTemplateTracebackSupplement: - #__implements__ = ITracebackSupplement - - def __init__(self, pt): - self.object = pt - w = pt.pt_warnings() - e = pt.pt_errors() - if e: - w = list(w) + list(e) - self.warnings = w - diff --git a/PageTemplates/PathIterator.py b/PageTemplates/PathIterator.py deleted file mode 100644 index ded1d29..0000000 --- a/PageTemplates/PathIterator.py +++ /dev/null @@ -1,48 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## - -"""Path Iterator - -A TALES Iterator with the ability to use first() and last() on -subpaths of elements. -""" - -__version__='$Revision: 1.1 $'[11:-2] - -import TALES -from Expressions import restrictedTraverse, Undefs, getSecurityManager -from string import split - -class Iterator(TALES.Iterator): - def __bobo_traverse__(self, REQUEST, name): - if name in ('first', 'last'): - path = REQUEST['TraversalRequestNameStack'] - names = list(path) - names.reverse() - path[:] = [tuple(names)] - return getattr(self, name) - - def same_part(self, name, ob1, ob2): - if name is None: - return ob1 == ob2 - if isinstance(name, type('')): - name = split(name, '/') - name = filter(None, name) - securityManager = getSecurityManager() - try: - ob1 = restrictedTraverse(ob1, name, securityManager) - ob2 = restrictedTraverse(ob2, name, securityManager) - except Undefs: - return 0 - return ob1 == ob2 - diff --git a/PageTemplates/PythonExpr.py b/PageTemplates/PythonExpr.py deleted file mode 100644 index afa40d2..0000000 --- a/PageTemplates/PythonExpr.py +++ /dev/null @@ -1,82 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## - -"""Generic Python Expression Handler -""" - -__version__='$Revision: 1.1 $'[11:-2] - -from TALES import CompilerError -from string import strip, split, join, replace, lstrip -from sys import exc_info - -class getSecurityManager: - '''Null security manager''' - def validate(self, *args, **kwargs): - return 1 - addContext = removeContext = validateValue = validate - -class PythonExpr: - def __init__(self, name, expr, engine): - self.expr = expr = replace(strip(expr), '\n', ' ') - try: - d = {} - exec 'def f():\n return %s\n' % strip(expr) in d - self._f = d['f'] - except: - raise CompilerError, ('Python expression error:\n' - '%s: %s') % exc_info()[:2] - self._get_used_names() - - def _get_used_names(self): - self._f_varnames = vnames = [] - for vname in self._f.func_code.co_names: - if vname[0] not in '$_': - vnames.append(vname) - - def _bind_used_names(self, econtext): - # Bind template variables - names = {} - vars = econtext.vars - getType = econtext._engine.getTypes().get - for vname in self._f_varnames: - has, val = vars.has_get(vname) - if not has: - has = val = getType(vname) - if has: - val = ExprTypeProxy(vname, val, econtext) - if has: - names[vname] = val - return names - - def __call__(self, econtext): - __traceback_info__ = self.expr - f = self._f - f.func_globals.update(self._bind_used_names(econtext)) - return f() - - def __str__(self): - return 'Python expression "%s"' % self.expr - def __repr__(self): - return '' % self.expr - -class ExprTypeProxy: - '''Class that proxies access to an expression type handler''' - def __init__(self, name, handler, econtext): - self._name = name - self._handler = handler - self._econtext = econtext - def __call__(self, text): - return self._handler(self._name, text, - self._econtext._engine)(self._econtext) - diff --git a/PageTemplates/README.txt b/PageTemplates/README.txt deleted file mode 100644 index 013f0ae..0000000 --- a/PageTemplates/README.txt +++ /dev/null @@ -1,7 +0,0 @@ -See the -ZPT project Wiki for more information about Page Templates, or -the download page -for installation instructions and the most recent version of the software. - -This Product requires the TAL and ZTUtils packages to be installed in -your Python path (not Products). See the links above for more information. diff --git a/PageTemplates/TALES.py b/PageTemplates/TALES.py deleted file mode 100644 index 089ccbc..0000000 --- a/PageTemplates/TALES.py +++ /dev/null @@ -1,285 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -"""TALES - -An implementation of a generic TALES engine -""" - -__version__='$Revision: 1.1 $'[11:-2] - -import re, sys, ZTUtils -from MultiMapping import MultiMapping - -StringType = type('') - -NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*" -_parse_expr = re.compile(r"(%s):" % NAME_RE).match -_valid_name = re.compile('%s$' % NAME_RE).match - -class TALESError(Exception): - """Error during TALES expression evaluation""" - -class Undefined(TALESError): - '''Exception raised on traversal of an undefined path''' - -class RegistrationError(Exception): - '''TALES Type Registration Error''' - -class CompilerError(Exception): - '''TALES Compiler Error''' - -class Default: - '''Retain Default''' -Default = Default() - -_marker = [] - -class SafeMapping(MultiMapping): - '''Mapping with security declarations and limited method exposure. - - Since it subclasses MultiMapping, this class can be used to wrap - one or more mapping objects. Restricted Python code will not be - able to mutate the SafeMapping or the wrapped mappings, but will be - able to read any value. - ''' - __allow_access_to_unprotected_subobjects__ = 1 - push = pop = None - - _push = MultiMapping.push - _pop = MultiMapping.pop - - def has_get(self, key, _marker=[]): - v = self.get(key, _marker) - return v is not _marker, v - -class Iterator(ZTUtils.Iterator): - def __init__(self, name, seq, context): - ZTUtils.Iterator.__init__(self, seq) - self.name = name - self._context = context - - def next(self): - if ZTUtils.Iterator.next(self): - self._context.setLocal(self.name, self.item) - return 1 - return 0 - - -class ErrorInfo: - """Information about an exception passed to an on-error handler.""" - __allow_access_to_unprotected_subobjects__ = 1 - - 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] - - -class Engine: - '''Expression Engine - - An instance of this class keeps a mutable collection of expression - type handlers. It can compile expression strings by delegating to - these handlers. It can provide an expression Context, which is - capable of holding state and evaluating compiled expressions. - ''' - Iterator = Iterator - - def __init__(self, Iterator=None): - self.types = {} - if Iterator is not None: - self.Iterator = Iterator - - def registerType(self, name, handler): - if not _valid_name(name): - raise RegistrationError, 'Invalid Expression type "%s".' % name - types = self.types - if types.has_key(name): - raise RegistrationError, ( - 'Multiple registrations for Expression type "%s".' % - name) - types[name] = handler - - def getTypes(self): - return self.types - - def compile(self, expression): - m = _parse_expr(expression) - if m: - type = m.group(1) - expr = expression[m.end():] - else: - type = "standard" - expr = expression - try: - handler = self.types[type] - except KeyError: - raise CompilerError, ( - 'Unrecognized expression type "%s".' % type) - return handler(type, expr, self) - - def getContext(self, contexts=None, **kwcontexts): - if contexts is not None: - if kwcontexts: - kwcontexts.update(contexts) - else: - kwcontexts = contexts - return Context(self, kwcontexts) - - def getCompilerError(self): - return CompilerError - -class Context: - '''Expression Context - - An instance of this class holds context information that it can - use to evaluate compiled expressions. - ''' - - _context_class = SafeMapping - position = (None, None) - source_file = None - - def __init__(self, engine, contexts): - self._engine = engine - self.contexts = contexts - contexts['nothing'] = None - contexts['default'] = Default - - self.repeat_vars = rv = {} - # Wrap this, as it is visible to restricted code - contexts['repeat'] = rep = self._context_class(rv) - contexts['loop'] = rep # alias - - self.global_vars = gv = contexts.copy() - self.local_vars = lv = {} - self.vars = self._context_class(gv, lv) - - # Keep track of what needs to be popped as each scope ends. - self._scope_stack = [] - - def beginScope(self): - self._scope_stack.append([self.local_vars.copy()]) - - def endScope(self): - scope = self._scope_stack.pop() - self.local_vars = lv = scope[0] - v = self.vars - v._pop() - v._push(lv) - # Pop repeat variables, if any - i = len(scope) - 1 - while i: - name, value = scope[i] - if value is None: - del self.repeat_vars[name] - else: - self.repeat_vars[name] = value - i = i - 1 - - def setLocal(self, name, value): - self.local_vars[name] = value - - def setGlobal(self, name, value): - self.global_vars[name] = value - - def setRepeat(self, name, expr): - expr = self.evaluate(expr) - if not expr: - return self._engine.Iterator(name, (), self) - it = self._engine.Iterator(name, expr, self) - old_value = self.repeat_vars.get(name) - self._scope_stack[-1].append((name, old_value)) - self.repeat_vars[name] = it - return it - - def evaluate(self, expression, - isinstance=isinstance, StringType=StringType): - if isinstance(expression, StringType): - expression = self._engine.compile(expression) - __traceback_supplement__ = ( - TALESTracebackSupplement, self, expression) - v = expression(self) - return v - - evaluateValue = evaluate - - def evaluateBoolean(self, expr): - return not not self.evaluate(expr) - - def evaluateText(self, expr, None=None): - text = self.evaluate(expr) - if text is Default or text is None: - return text - return str(text) - - def evaluateStructure(self, expr): - return self.evaluate(expr) - evaluateStructure = evaluate - - def evaluateMacro(self, expr): - # XXX Should return None or a macro definition - return self.evaluate(expr) - evaluateMacro = evaluate - - def createErrorInfo(self, err, position): - return ErrorInfo(err, position) - - def getDefault(self): - return Default - - def setSourceFile(self, source_file): - self.source_file = source_file - - def setPosition(self, position): - self.position = position - - - -class TALESTracebackSupplement: - """Implementation of ITracebackSupplement""" - def __init__(self, context, expression): - self.context = context - self.source_url = context.source_file - self.line = context.position[0] - self.column = context.position[1] - self.expression = repr(expression) - - def getInfo(self, as_html=0): - import pprint - data = self.context.contexts.copy() - s = pprint.pformat(data) - if not as_html: - return ' - Names:\n %s' % string.replace(s, '\n', '\n ') - else: - from cgi import escape - return 'Names:
%s
' % (escape(s)) - return None - - - -class SimpleExpr: - '''Simple example of an expression type handler''' - def __init__(self, name, expr, engine): - self._name = name - self._expr = expr - def __call__(self, econtext): - return self._name, self._expr - def __repr__(self): - return '' % (self._name, `self._expr`) - diff --git a/PageTemplates/__init__.py b/PageTemplates/__init__.py deleted file mode 100644 index e1997a6..0000000 --- a/PageTemplates/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -__doc__='''Package wrapper for Page Templates - -This wrapper allows the Page Template modules to be segregated in a -separate package. - -$Id: __init__.py,v 1.1 2002-08-30 08:27:34 richard Exp $''' -__version__='$$'[11:-2] - - -# Placeholder for Zope Product data -misc_ = {} - -def initialize(context): - # Import lazily, and defer initialization to the module - import ZopePageTemplate - ZopePageTemplate.initialize(context) diff --git a/TAL/.cvsignore b/TAL/.cvsignore deleted file mode 100644 index 0cd1d94..0000000 --- a/TAL/.cvsignore +++ /dev/null @@ -1,2 +0,0 @@ -.path -*.pyc diff --git a/TAL/HTMLParser.py b/TAL/HTMLParser.py deleted file mode 100644 index 5ab076b..0000000 --- a/TAL/HTMLParser.py +++ /dev/null @@ -1,403 +0,0 @@ -"""A parser for HTML and XHTML.""" - -# This file is based on sgmllib.py, but the API is slightly different. - -# XXX There should be a way to distinguish between PCDATA (parsed -# character data -- the normal case), RCDATA (replaceable character -# data -- only char and entity references and end tags are special) -# and CDATA (character data -- only end tags are special). - - -import markupbase -import re -import string - -# Regular expressions used for parsing - -interesting_normal = re.compile('[&<]') -interesting_cdata = re.compile(r'<(/|\Z)') -incomplete = re.compile('&[a-zA-Z#]') - -entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]') -charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]') - -starttagopen = re.compile('<[a-zA-Z]') -piclose = re.compile('>') -endtagopen = re.compile('') -tagfind = re.compile('[a-zA-Z][-.a-zA-Z0-9:_]*') -attrfind = re.compile( - r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*' - r'(\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9./:;+*%?!&$\(\)_#=~]*))?') - -locatestarttagend = re.compile(r""" - <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name - (?:\s+ # whitespace before attribute name - (?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name - (?:\s*=\s* # value indicator - (?:'[^']*' # LITA-enclosed value - |\"[^\"]*\" # LIT-enclosed value - |[^'\">\s]+ # bare value - ) - )? - ) - )* - \s* # trailing whitespace -""", re.VERBOSE) -endendtag = re.compile('>') -endtagfind = re.compile('') - - -class HTMLParseError(Exception): - """Exception raised for all parse errors.""" - - def __init__(self, msg, position=(None, None)): - assert msg - self.msg = msg - self.lineno = position[0] - self.offset = position[1] - - 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) - return result - - -def _contains_at(s, sub, pos): - return s[pos:pos+len(sub)] == sub - - -class HTMLParser(markupbase.ParserBase): - """Find tags and other markup and call handler functions. - - Usage: - p = HTMLParser() - p.feed(data) - ... - p.close() - - Start tags are handled by calling self.handle_starttag() or - self.handle_startendtag(); end tags by self.handle_endtag(). The - data between tags is passed from the parser to the derived class - by calling self.handle_data() with the data as argument (the data - may be split up in arbitrary chunks). Entity references are - passed by calling self.handle_entityref() with the entity - reference as the argument. Numeric character references are - passed to self.handle_charref() with the string containing the - reference as the argument. - """ - - CDATA_CONTENT_ELEMENTS = ("script", "style") - - - def __init__(self): - """Initialize and reset this instance.""" - self.reset() - - def reset(self): - """Reset this instance. Loses all unprocessed data.""" - self.rawdata = '' - self.stack = [] - self.lasttag = '???' - self.interesting = interesting_normal - markupbase.ParserBase.reset(self) - - def feed(self, data): - """Feed data to the parser. - - Call this as often as you want, with as little or as much text - as you want (may include '\n'). - """ - self.rawdata = self.rawdata + data - self.goahead(0) - - def close(self): - """Handle any buffered data.""" - self.goahead(1) - - def error(self, message): - raise HTMLParseError(message, self.getpos()) - - __starttag_text = None - - def get_starttag_text(self): - """Return full source of start tag: '<...>'.""" - return self.__starttag_text - - cdata_endtag = None - - def set_cdata_mode(self, endtag=None): - self.cdata_endtag = endtag - self.interesting = interesting_cdata - - def clear_cdata_mode(self): - self.cdata_endtag = None - self.interesting = interesting_normal - - # Internal -- handle data as far as reasonable. May leave state - # and data to be processed by a subsequent call. If 'end' is - # true, force handling all data as if followed by EOF marker. - def goahead(self, end): - rawdata = self.rawdata - i = 0 - n = len(rawdata) - while i < n: - match = self.interesting.search(rawdata, i) # < or & - if match: - j = match.start() - else: - j = n - if i < j: self.handle_data(rawdata[i:j]) - i = self.updatepos(i, j) - if i == n: break - if rawdata[i] == '<': - if starttagopen.match(rawdata, i): # < + letter - k = self.parse_starttag(i) - elif endtagopen.match(rawdata, i): # - if not match: - return -1 - j = match.start() - self.handle_pi(rawdata[i+2: j]) - j = match.end() - return j - - # Internal -- handle starttag, return end or -1 if not terminated - def parse_starttag(self, i): - self.__starttag_text = None - endpos = self.check_for_whole_start_tag(i) - if endpos < 0: - return endpos - rawdata = self.rawdata - self.__starttag_text = rawdata[i:endpos] - - # Now parse the data between i+1 and j into a tag and attrs - attrs = [] - match = tagfind.match(rawdata, i+1) - assert match, 'unexpected call to parse_starttag()' - k = match.end() - self.lasttag = tag = string.lower(rawdata[i+1:k]) - - while k < endpos: - m = attrfind.match(rawdata, k) - if not m: - break - attrname, rest, attrvalue = m.group(1, 2, 3) - if not rest: - attrvalue = None - elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ - attrvalue[:1] == '"' == attrvalue[-1:]: - attrvalue = attrvalue[1:-1] - attrvalue = self.unescape(attrvalue) - attrs.append((string.lower(attrname), attrvalue)) - k = m.end() - - end = string.strip(rawdata[k:endpos]) - if end not in (">", "/>"): - lineno, offset = self.getpos() - if "\n" in self.__starttag_text: - lineno = lineno + string.count(self.__starttag_text, "\n") - offset = len(self.__starttag_text) \ - - string.rfind(self.__starttag_text, "\n") - else: - offset = offset + len(self.__starttag_text) - self.error("junk characters in start tag: %s" - % `rawdata[k:endpos][:20]`) - if end[-2:] == '/>': - # XHTML-style empty tag: - self.handle_startendtag(tag, attrs) - else: - self.handle_starttag(tag, attrs) - if tag in self.CDATA_CONTENT_ELEMENTS: - self.set_cdata_mode(tag) - return endpos - - # Internal -- check to see if we have a complete starttag; return end - # or -1 if incomplete. - def check_for_whole_start_tag(self, i): - rawdata = self.rawdata - m = locatestarttagend.match(rawdata, i) - if m: - j = m.end() - next = rawdata[j:j+1] - if next == ">": - return j + 1 - if next == "/": - s = rawdata[j:j+2] - if s == "/>": - return j + 2 - if s == "/": - # buffer boundary - return -1 - # else bogus input - self.updatepos(i, j + 1) - self.error("malformed empty start tag") - if next == "": - # end of input - return -1 - if next in ("abcdefghijklmnopqrstuvwxyz=/" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ"): - # end of input in or before attribute value, or we have the - # '/' from a '/>' ending - return -1 - self.updatepos(i, j) - self.error("malformed start tag") - raise AssertionError("we should not get here!") - - # Internal -- parse endtag, return end or -1 if incomplete - def parse_endtag(self, i): - rawdata = self.rawdata - assert rawdata[i:i+2] == " - if not match: - return -1 - j = match.end() - match = endtagfind.match(rawdata, i) # - if not match: - self.error("bad end tag: %s" % `rawdata[i:j]`) - tag = string.lower(match.group(1)) - if ( self.cdata_endtag is not None - and tag != self.cdata_endtag): - # Should be a mismatched end tag, but we'll treat it - # as text anyway, since most HTML authors aren't - # interested in the finer points of syntax. - self.handle_data(match.group(0)) - else: - self.handle_endtag(tag) - self.clear_cdata_mode() - return j - - # Overridable -- finish processing of start+end tag: - def handle_startendtag(self, tag, attrs): - self.handle_starttag(tag, attrs) - self.handle_endtag(tag) - - # Overridable -- handle start tag - def handle_starttag(self, tag, attrs): - pass - - # Overridable -- handle end tag - def handle_endtag(self, tag): - pass - - # Overridable -- handle character reference - def handle_charref(self, name): - pass - - # Overridable -- handle entity reference - def handle_entityref(self, name): - pass - - # Overridable -- handle data - def handle_data(self, data): - pass - - # Overridable -- handle comment - def handle_comment(self, data): - pass - - # Overridable -- handle declaration - def handle_decl(self, decl): - pass - - # Overridable -- handle processing instruction - def handle_pi(self, data): - pass - - def unknown_decl(self, data): - self.error("unknown declaration: " + `data`) - - # Internal -- helper to remove special character quoting - def unescape(self, s): - if '&' not in s: - return s - s = string.replace(s, "<", "<") - s = string.replace(s, ">", ">") - s = string.replace(s, "'", "'") - s = string.replace(s, """, '"') - s = string.replace(s, "&", "&") # Must be last - return s diff --git a/TAL/HTMLTALParser.py b/TAL/HTMLTALParser.py deleted file mode 100644 index 8d7f0db..0000000 --- a/TAL/HTMLTALParser.py +++ /dev/null @@ -1,290 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -Parse HTML and compile to TALInterpreter intermediate code. -""" - -import sys -import string - -from TALGenerator import TALGenerator -from TALDefs import ZOPE_METAL_NS, ZOPE_TAL_NS, METALError, TALError -from HTMLParser import HTMLParser, HTMLParseError - -BOOLEAN_HTML_ATTRS = [ - # List of Boolean attributes in HTML that may be given in - # minimized form (e.g. rather than ) - # 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. . - # 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 ' - % (tagstack[0], endtag)) - else: - msg = ('Open tags <%s> do not match close tag ' - % (string.join(tagstack, '>, <'), endtag)) - else: - msg = 'No tags are open to match ' % 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 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} - - def parseFile(self, file): - f = open(file) - data = f.read() - f.close() - self.parseString(data) - - 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 = self.process_ns(tag, attrs) - self.tagstack.append(tag) - self.gen.emitStartElement(tag, attrlist, taldict, metaldict, - 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 = self.process_ns(tag, attrs) - if taldict.get("content"): - self.gen.emitStartElement(tag, attrlist, taldict, metaldict, - self.getpos()) - self.gen.emitEndElement(tag, implied=-1) - else: - self.gen.emitStartElement(tag, attrlist, taldict, metaldict, - self.getpos(), isend=1) - self.pop_xmlns() - - def handle_endtag(self, tag): - if tag in EMPTY_HTML_TAGS: - # 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("" % data) - - def handle_decl(self, data): - self.gen.emitRawText("" % data) - - def handle_pi(self, data): - self.gen.emitRawText("" % data) - - # Internal thingies - - def scan_xmlns(self, attrs): - nsnew = {} - for key, value in attrs: - if key[:6] == "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 = string.split(name, ':', 1) - if prefix == 'xmlns': - nsuri = self.nsdict.get(suffix) - if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_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' - return name, name, 0 - - def process_ns(self, name, attrs): - attrlist = [] - taldict = {} - metaldict = {} - 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 - attrlist.append(item) - if namens in ('metal', 'tal'): - taldict['tal tag'] = namens - return name, attrlist, taldict, metaldict diff --git a/TAL/README.txt b/TAL/README.txt deleted file mode 100644 index 4a28816..0000000 --- a/TAL/README.txt +++ /dev/null @@ -1,97 +0,0 @@ -TAL - Template Attribute Language ---------------------------------- - -This is an implementation of TAL, the Zope Template Attribute -Language. For TAL, see the Zope Presentation Templates ZWiki: - - http://dev.zope.org/Wikis/DevSite/Projects/ZPT/FrontPage - -It is not a Zope product nor is it designed exclusively to run inside -of Zope, but if you have a Zope checkout that includes -Products/ParsedXML, its Expat parser will be used. - -Prerequisites -------------- - -You need: - -- A recent checkout of Zope2; don't forget to run the wo_pcgi.py - script to compile everything. (See above -- this is now optional.) - -- A recent checkout of the Zope2 product ParsedXML, accessible - throught /lib/python/Products/ParsedXML; don't forget to run - the setup.py script to compiles Expat. (Again, optional.) - -- Python 1.5.2; the driver script refuses to work with other versions - unless you specify the -n option; this is done so that I don't - accidentally use Python 2.x features. - -- Create a .path file containing proper module search path; it should - point the /lib/python directory that you want to use. - -How To Play ------------ - -(Don't forget to edit .path, see above!) - -The script driver.py takes an XML file with TAL markup as argument and -writes the expanded version to standard output. The filename argument -defaults to tests/input/test01.xml. - -Regression test ---------------- - -There are unit test suites in the 'tests' subdirectory; these can be -run with tests/run.py. This should print the testcase names plus -progress info, followed by a final line saying "OK". It requires that -../unittest.py exists. - -There are a number of test files in the 'tests' subdirectory, named -tests/input/test.xml and tests/input/test.html. The -Python script ./runtest.py calls driver.main() for each test file, and -should print " OK" for each one. These tests are also run as -part of the unit test suites, so tests/run.py is all you need. - -What's Here ------------ - -DummyEngine.py simple-minded TALES execution engine -TALInterpreter.py class to interpret intermediate code -TALGenerator.py class to generate intermediate code -XMLParser.py base class to parse XML, avoiding DOM -TALParser.py class to parse XML with TAL into intermediate code -HTMLTALParser.py class to parse HTML with TAL into intermediate code -HTMLParser.py HTML-parsing base class -driver.py script to demonstrate TAL expansion -timer.py script to time various processing phases -setpath.py hack to set sys.path and import ZODB -__init__.py empty file that makes this directory a package -runtest.py Python script to run file-comparison tests -ndiff.py helper for runtest.py to produce diffs -tests/ drectory with test files and output -tests/run.py Python script to run all tests - -Author and License ------------------- - -This code is written by Guido van Rossum (project lead), Fred Drake, -and Tim Peters. It is owned by Digital Creations and can be -redistributed under the Zope Public License. - -TO DO ------ - -(See also http://www.zope.org/Members/jim/ZPTIssueTracker .) - -- Need to remove leading whitespace and newline when omitting an - element (either through tal:replace with a value of nothing or - tal:condition with a false condition). - -- Empty TAL/METAL attributes are ignored: tal:replace="" is ignored - rather than causing an error. - -- HTMLTALParser.py and TALParser.py are silly names. Should be - HTMLTALCompiler.py and XMLTALCompiler.py (or maybe shortened, - without "TAL"?) - -- Should we preserve case of tags and attribute names in HTML? diff --git a/TAL/TALDefs.py b/TAL/TALDefs.py deleted file mode 100644 index dbc0443..0000000 --- a/TAL/TALDefs.py +++ /dev/null @@ -1,145 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -Common definitions used by TAL and METAL compilation an transformation. -""" - -from types import ListType, TupleType - -TAL_VERSION = "1.3.2" - -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" - -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", - ] - -class TALError(Exception): - - def __init__(self, msg, position=(None, None)): - assert msg != "" - self.msg = msg - self.lineno = position[0] - self.offset = position[1] - - 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) - return result - -class METALError(TALError): - pass - -class TALESError(TALError): - pass - -class ErrorInfo: - - 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): - 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 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: - import string - arg = string.replace(arg, ";;", "\0") - parts = string.split(arg, ';') - parts = map(lambda s, repl=string.replace: repl(s, "\0", ";"), parts) - if len(parts) > 1 and not string.strip(parts[-1]): - 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 cgi -def quote(s, escape=cgi.escape): - return '"%s"' % escape(s, 1) -del cgi diff --git a/TAL/TALGenerator.py b/TAL/TALGenerator.py deleted file mode 100644 index 0bfadea..0000000 --- a/TAL/TALGenerator.py +++ /dev/null @@ -1,583 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -Code generator for TALInterpreter intermediate code. -""" - -import string -import re -import cgi - -from TALDefs import * - -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() - self.program = [] - self.stack = [] - 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) - - def getCode(self): - assert not self.stack - assert not self.todoStack - return self.optimize(self.program), self.macros - - def optimize(self, program): - output = [] - collect = [] - rawseen = 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("" % 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 - text = string.join(collect, "") - if text: - i = string.rfind(text, "\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)) - rawseen = cursor+1 - collect = [] - return self.optimizeCommonTriple(output) - - def optimizeArgsList(self, item): - if len(item) == 2: - return item - else: - return item[0], tuple(item[1:]) - - actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4, - 0: 0, 1: 1, 2: 2, 3: 3, 4: 4} - def optimizeStartTag(self, collect, name, attrlist, end): - 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] - action = self.actionIndex[action] - attrlist[i] = (name, value, action) + item[3:] - else: - if item[1] is None: - s = item[0] - else: - s = "%s=%s" % (item[0], quote(item[1])) - attrlist[i] = item[0], s - if item[1] is None: - new.append(" " + item[0]) - else: - new.append(" %s=%s" % (item[0], quote(item[1]))) - 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 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): - 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, {}, []) - 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 emitDefineMacro(self, macroName): - program = self.popProgram() - macroName = string.strip(macroName) - 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 = string.strip(slotName) - 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 = string.strip(slotName) - 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 string.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 + string.join(collect, "") - del self.program[i:] - if text: - self.emit("rawtext", text) - return rest - return None - - def replaceAttrs(self, attrlist, repldict): - if not repldict: - return attrlist - newlist = [] - for item in attrlist: - key = item[0] - if repldict.has_key(key): - item = item[:2] + ("replace", repldict[key]) - del repldict[key] - newlist.append(item) - for key, value in repldict.items(): # Add dynamic-only attributes - item = (key, None, "insert", value) - newlist.append(item) - return newlist - - def emitStartElement(self, name, attrlist, taldict, metaldict, - position=(None, None), isend=0): - if not taldict and not metaldict: - # 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 KNOWN_TAL_ATTRIBUTES: - raise TALError("bad TAL attribute: " + `key`, position) - if not (value or key == 'omit-tag'): - raise TALError("missing value for TAL attribute: " + - `key`, position) - for key, value in metaldict.items(): - if key not in KNOWN_METAL_ATTRIBUTES: - raise METALError("bad METAL attribute: " + `key`, - position) - if not value: - raise TALError("missing value for METAL 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") - if len(metaldict) > 1 and (defineMacro or useMacro): - raise METALError("define-macro and use-macro cannot be used " - "together or with define-slot or fill-slot", - position) - if content and replace: - raise TALError("content and replace are mutually exclusive", - position) - - 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 taldict: - dict = {} - for item in attrlist: - key, value = item[:2] - dict[key] = value - self.emit("beginScope", dict) - todo["scope"] = 1 - if onError: - self.pushProgram() # handler - self.emitStartTag(name, list(attrlist)) # Must copy attrlist! - 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: - todo["content"] = content - if replace: - todo["replace"] = replace - self.pushProgram() - optTag = omitTag is not None or TALtag - if optTag: - todo["optional tag"] = omitTag, TALtag - self.pushProgram() - if attrsubst: - repldict = parseAttributeReplacements(attrsubst) - for key, value in repldict.items(): - repldict[key] = self.compileExpression(value) - else: - repldict = {} - if replace: - todo["repldict"] = repldict - repldict = {} - self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) - if optTag: - self.pushProgram() - if content: - 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") - define = todo.get("define") - repldict = todo.get("repldict", {}) - scope = todo.get("scope") - optTag = todo.get("optional tag") - - 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 " % - (what, name, name), position) - - if content: - self.emitSubstitution(content, {}) - if optTag: - self.emitOptTag(name, optTag, isend) - elif not isend: - self.emitEndTag(name) - if replace: - self.emitSubstitution(replace, repldict) - if repeat: - self.emitRepeat(repeat) - if condition: - self.emitCondition(condition) - if onError: - self.emitOnError(name, onError) - if scope: - self.emit("endScope") - if defineSlot: - self.emitDefineSlot(defineSlot) - if fillSlot: - self.emitFillSlot(fillSlot) - if useMacro: - self.emitUseMacro(useMacro) - if defineMacro: - self.emitDefineMacro(defineMacro) - -def test(): - t = TALGenerator() - t.pushProgram() - t.emit("bar") - p = t.popProgram() - t.emit("foo", p) - -if __name__ == "__main__": - test() diff --git a/TAL/TALInterpreter.py b/TAL/TALInterpreter.py deleted file mode 100644 index 0f42284..0000000 --- a/TAL/TALInterpreter.py +++ /dev/null @@ -1,626 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -Interpreter for a pre-compiled TAL program. -""" - -import sys -import getopt - -from cgi import escape -from string import join, lower, rfind -try: - from strop import lower, rfind -except ImportError: - pass - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -from TALDefs import quote, TAL_VERSION, TALError, METALError -from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode -from TALGenerator import TALGenerator - -BOOLEAN_HTML_ATTRS = [ - # List of Boolean attributes in HTML that should be rendered in - # minimized form (e.g. rather than ) - # From http://www.w3.org/TR/xhtml1/#guidelines (C.10) - # XXX 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" -] - -EMPTY_HTML_TAGS = [ - # List of HTML tags with an empty content model; these are - # rendered in minimized form, e.g. . - # From http://www.w3.org/TR/xhtml1/#dtds - "base", "meta", "link", "hr", "br", "param", "img", "area", - "input", "col", "basefont", "isindex", "frame", -] - -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: - apply(TALGenerator.emit, (self,) + args) - - def emitStartElement(self, name, attrlist, taldict, metaldict, - position=(None, None), isend=0): - metaldict = {} - taldict = {} - if self.enabled and self.repldict: - taldict["attributes"] = "x x" - TALGenerator.emitStartElement(self, name, attrlist, - taldict, metaldict, 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: - - def __init__(self, program, macros, engine, stream=None, - debug=0, wrap=60, metal=1, tal=1, showtal=-1, - strictinsert=1, stackLimit=100): - self.program = program - self.macros = macros - self.engine = engine - self.Default = engine.getDefault() - self.stream = stream or sys.stdout - self._stream_write = self.stream.write - 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.popMacro = self.macroStack.pop - self.position = None, None # (lineno, offset) - self.col = 0 - self.level = 0 - self.scopeLevel = 0 - self.sourceFile = None - - def saveState(self): - return (self.position, self.col, self.stream, - self.scopeLevel, self.level) - - def restoreState(self, state): - (self.position, self.col, self.stream, scopeLevel, level) = 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) - - def restoreOutputState(self, state): - (dummy, self.col, self.stream, scopeLevel, level) = 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]) - - def macroContext(self, what): - macroStack = self.macroStack - i = len(macroStack) - while i > 0: - i = i-1 - if macroStack[i][0] == what: - return i - return -1 - - def __call__(self): - assert self.level == 0 - assert self.scopeLevel == 0 - self.interpret(self.program) - assert self.level == 0 - assert self.scopeLevel == 0 - if self.col > 0: - self._stream_write("\n") - self.col = 0 - - def stream_write(self, s, - len=len, rfind=rfind): - self._stream_write(s) - i = rfind(s, '\n') - if i < 0: - self.col = self.col + len(s) - else: - self.col = len(s) - (i + 1) - - bytecode_handlers = {} - - def interpret(self, program, None=None): - 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. - _stream_write = self._stream_write - _stream_write("<" + name) - namelen = _len(name) - col = self.col + namelen + 1 - wrap = self.wrap - align = col + 1 - if align >= wrap/2: - align = 4 # Avoid a narrow column far to the right - attrAction = self.dispatch[""] - try: - for item in attrList: - if _len(item) == 2: - name, s = item - else: - ok, name, s = attrAction(self, item) - if not ok: - continue - slen = _len(s) - if (wrap and - col >= align and - col + 1 + slen > wrap): - _stream_write("\n" + " "*align) - col = align + slen - else: - s = " " + s - col = col + 1 + slen - _stream_write(s) - _stream_write(end) - col = col + endlen - finally: - self.col = col - bytecode_handlers["startTag"] = do_startTag - - def attrAction(self, item): - name, value, action = item[:3] - if action == 1 or (action > 1 and not self.showtal): - return 0, name, value - macs = self.macroStack - if action == 2 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 = rfind(name, ":") + 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 + "slot" - elif suffix == "fill-slot": - pass - else: - return 0, name, value - - if value is None: - value = name - else: - value = "%s=%s" % (name, quote(value)) - return 1, name, value - - def attrAction_tal(self, item): - name, value, action = item[:3] - if action > 1: - return self.attrAction(item) - ok = 1 - if self.html and lower(name) in BOOLEAN_HTML_ATTRS: - evalue = self.engine.evaluateBoolean(item[3]) - if evalue is self.Default: - if action == 1: # Cancelled insert - ok = 0 - elif evalue: - value = None - else: - ok = 0 - else: - evalue = self.engine.evaluateText(item[3]) - if evalue is self.Default: - if action == 1: # Cancelled insert - ok = 0 - else: - if evalue is None: - ok = 0 - value = evalue - if ok: - if value is None: - value = name - value = "%s=%s" % (name, quote(value)) - return ok, name, value - - bytecode_handlers[""] = attrAction - - def no_tag(self, start, program): - state = self.saveState() - self.stream = stream = 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 = '' % 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 dumpMacroStack(self, prefix, suffix, value): - sys.stderr.write("+---- %s%s = %s\n" % (prefix, suffix, value)) - for i in range(len(self.macroStack)): - what, macroName, slots = self.macroStack[i] - sys.stderr.write("| %2d. %-12s %-12s %s\n" % - (i, what, macroName, slots and slots.keys())) - sys.stderr.write("+--------------------------------------\n") - - def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)): - self._stream_write(s) - self.col = col - self.do_setPosition(position) - 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 - self.do_setPosition(position) - engine = self.engine - 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_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 - s = escape(text) - self._stream_write(s) - i = rfind(s, '\n') - if i < 0: - self.col = self.col + len(s) - else: - self.col = len(s) - (i + 1) - bytecode_handlers["insertText"] = do_insertText - - 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 = str(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, 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, 0) - p = TALParser(gen) - gen.enable(0) - p.parseFragment('') - gen.enable(1) - p.parseFragment(text) # Raises an exception if text is invalid - gen.enable(0) - p.parseFragment('', 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 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) - 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) - saved_source = self.sourceFile - saved_position = self.position # Used by Boa Constructor - self.interpret(macro) - if self.sourceFile != saved_source: - self.engine.setSourceFile(saved_source) - self.sourceFile = saved_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: - saved_source = self.sourceFile - saved_position = self.position # Used by Boa Constructor - macroName, slots = self.popMacro()[:2] - slot = slots.get(slotName) - if slot is not None: - self.interpret(slot) - if self.sourceFile != saved_source: - self.engine.setSourceFile(saved_source) - self.sourceFile = saved_source - self.pushMacro(macroName, slots, entering=0) - return - self.pushMacro(macroName, slots) - if len(macs) == 1: - self.interpret(block) - return - 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 = StringIO() - self._stream_write = stream.write - try: - self.interpret(block) - 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_tal - bytecode_handlers_tal["optTag"] = do_optTag_tal - - -def test(): - from driver import FILE, parsefile - from DummyEngine import DummyEngine - try: - opts, args = getopt.getopt(sys.argv[1:], "") - except getopt.error, msg: - print msg - sys.exit(2) - if args: - file = args[0] - else: - file = FILE - doc = parsefile(file) - compiler = TALCompiler(doc) - program, macros = compiler() - engine = DummyEngine() - interpreter = TALInterpreter(program, macros, engine) - interpreter() - -if __name__ == "__main__": - test() diff --git a/TAL/TALParser.py b/TAL/TALParser.py deleted file mode 100644 index f75414e..0000000 --- a/TAL/TALParser.py +++ /dev/null @@ -1,137 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -Parse XML and compile to TALInterpreter intermediate code. -""" - -import string -from XMLParser import XMLParser -from TALDefs import * -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 = self.process_ns(name, attrlist) - attrlist = self.xmlnsattrs() + attrlist - self.gen.emitStartElement(name, attrlist, taldict, metaldict) - - def process_ns(self, name, attrlist): - taldict = {} - metaldict = {} - 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",) - fixedattrlist.append(item) - if namens in ('metal', 'tal'): - taldict['tal tag'] = namens - return name, fixedattrlist, taldict, metaldict - - 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): - item = (key, uri, "xmlns") - else: - item = (key, uri) - newlist.append(item) - self.nsNew = [] - return newlist - - def fixname(self, name): - if ' ' in name: - uri, name = string.split(name, ' ') - 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' - 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() diff --git a/TAL/XMLParser.py b/TAL/XMLParser.py deleted file mode 100644 index 71a65ab..0000000 --- a/TAL/XMLParser.py +++ /dev/null @@ -1,90 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" -Generic expat-based XML parser base class. -""" - -import zLOG - -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: - zLOG.LOG("TAL.XMLParser", zLOG.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: - zLOG.LOG("TAL.XMLParser", zLOG.PROBLEM, - "Can't set expat handler %s" % name) - - def createParser(self, encoding=None): - global XMLParseError - try: - from Products.ParsedXML.Expat import pyexpat - XMLParseError = pyexpat.ExpatError - return pyexpat.ParserCreate(encoding, ' ') - except ImportError: - from xml.parsers import expat - XMLParseError = expat.ExpatError - return 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) diff --git a/TAL/__init__.py b/TAL/__init__.py deleted file mode 100644 index 080ed5d..0000000 --- a/TAL/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Corporation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## -""" Template Attribute Language package """ diff --git a/TAL/markupbase.py b/TAL/markupbase.py deleted file mode 100644 index eab134c..0000000 --- a/TAL/markupbase.py +++ /dev/null @@ -1,306 +0,0 @@ -"""Shared support for scanning document type declarations in HTML and XHTML.""" - -import re -import string - -_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match -_declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match - -del re - - -class ParserBase: - """Parser base class which provides some common support methods used - by the SGML/HTML and XHTML parsers.""" - - def reset(self): - self.lineno = 1 - self.offset = 0 - - def getpos(self): - """Return current line number and offset.""" - return self.lineno, self.offset - - # Internal -- update line number and offset. This should be - # called for each piece of data exactly once, in order -- in other - # words the concatenation of all the input strings to this - # function should be exactly the entire input. - def updatepos(self, i, j): - if i >= j: - return j - rawdata = self.rawdata - nlines = string.count(rawdata, "\n", i, j) - if nlines: - self.lineno = self.lineno + nlines - pos = string.rindex(rawdata, "\n", i, j) # Should not fail - self.offset = j-(pos+1) - else: - self.offset = self.offset + j-i - return j - - _decl_otherchars = '' - - # Internal -- parse declaration (for use by subclasses). - def parse_declaration(self, i): - # This is some sort of declaration; in "HTML as - # deployed," this should only be the document type - # declaration (""). - rawdata = self.rawdata - import sys - j = i + 2 - assert rawdata[i:j] == "' - n = len(rawdata) - decltype, j = self._scan_name(j, i) - if j < 0: - return j - if decltype == "doctype": - self._decl_otherchars = '' - while j < n: - c = rawdata[j] - if c == ">": - # end of declaration syntax - data = rawdata[i+2:j] - if decltype == "doctype": - self.handle_decl(data) - else: - self.unknown_decl(data) - return j + 1 - if c in "\"'": - m = _declstringlit_match(rawdata, j) - if not m: - return -1 # incomplete - j = m.end() - elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": - name, j = self._scan_name(j, i) - elif c in self._decl_otherchars: - j = j + 1 - elif c == "[": - if decltype == "doctype": - j = self._parse_doctype_subset(j + 1, i) - else: - self.error("unexpected '[' char in declaration") - else: - self.error( - "unexpected %s char in declaration" % `rawdata[j]`) - if j < 0: - return j - return -1 # incomplete - - # Internal -- scan past the internal subset in a n: - # end of buffer; incomplete - return -1 - if rawdata[j:j+4] == "') + if errend >= 0: + text = text[errend + 4:] + if self._text != text: + self._text = text + self._cook() + + def read(self): + if not self._v_cooked: + self._cook() + if not self._v_errors: + if not self.expand: + return self._text + try: + return self.pt_render(source=1) + except: + return ('%s\n Macro expansion failed\n %s\n-->\n%s' % + (self._error_start, "%s: %s" % sys.exc_info()[:2], + self._text) ) + + return ('%s\n %s\n-->\n%s' % (self._error_start, + join(self._v_errors, '\n '), + self._text)) + + def _cook(self): + """Compile the TAL and METAL statments. + + Cooking must not fail due to compilation errors in templates. + """ + source_file = self.pt_source_file() + if self.html(): + gen = TALGenerator(getEngine(), xml=0, source_file=source_file) + parser = HTMLTALParser(gen) + else: + gen = TALGenerator(getEngine(), source_file=source_file) + parser = TALParser(gen) + + self._v_errors = () + try: + parser.parseString(self._text) + self._v_program, self._v_macros = parser.getCode() + except: + self._v_errors = ["Compilation failed", + "%s: %s" % sys.exc_info()[:2]] + self._v_warnings = parser.getWarnings() + self._v_cooked = 1 + + def html(self): + if not hasattr(getattr(self, 'aq_base', self), 'is_html'): + return self.content_type == 'text/html' + return self.is_html + +class _ModuleImporter: + def __getitem__(self, module): + mod = __import__(module) + path = split(module, '.') + for name in path[1:]: + mod = getattr(mod, name) + return mod + +ModuleImporter = _ModuleImporter() + +class PTRuntimeError(RuntimeError): + '''The Page Template has template errors that prevent it from rendering.''' + pass + + +class PageTemplateTracebackSupplement: + #__implements__ = ITracebackSupplement + + def __init__(self, pt): + self.object = pt + w = pt.pt_warnings() + e = pt.pt_errors() + if e: + w = list(w) + list(e) + self.warnings = w + diff --git a/roundup/cgi/PageTemplates/PathIterator.py b/roundup/cgi/PageTemplates/PathIterator.py new file mode 100644 index 0000000..ded1d29 --- /dev/null +++ b/roundup/cgi/PageTemplates/PathIterator.py @@ -0,0 +1,48 @@ +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## + +"""Path Iterator + +A TALES Iterator with the ability to use first() and last() on +subpaths of elements. +""" + +__version__='$Revision: 1.1 $'[11:-2] + +import TALES +from Expressions import restrictedTraverse, Undefs, getSecurityManager +from string import split + +class Iterator(TALES.Iterator): + def __bobo_traverse__(self, REQUEST, name): + if name in ('first', 'last'): + path = REQUEST['TraversalRequestNameStack'] + names = list(path) + names.reverse() + path[:] = [tuple(names)] + return getattr(self, name) + + def same_part(self, name, ob1, ob2): + if name is None: + return ob1 == ob2 + if isinstance(name, type('')): + name = split(name, '/') + name = filter(None, name) + securityManager = getSecurityManager() + try: + ob1 = restrictedTraverse(ob1, name, securityManager) + ob2 = restrictedTraverse(ob2, name, securityManager) + except Undefs: + return 0 + return ob1 == ob2 + diff --git a/roundup/cgi/PageTemplates/PythonExpr.py b/roundup/cgi/PageTemplates/PythonExpr.py new file mode 100644 index 0000000..afa40d2 --- /dev/null +++ b/roundup/cgi/PageTemplates/PythonExpr.py @@ -0,0 +1,82 @@ +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## + +"""Generic Python Expression Handler +""" + +__version__='$Revision: 1.1 $'[11:-2] + +from TALES import CompilerError +from string import strip, split, join, replace, lstrip +from sys import exc_info + +class getSecurityManager: + '''Null security manager''' + def validate(self, *args, **kwargs): + return 1 + addContext = removeContext = validateValue = validate + +class PythonExpr: + def __init__(self, name, expr, engine): + self.expr = expr = replace(strip(expr), '\n', ' ') + try: + d = {} + exec 'def f():\n return %s\n' % strip(expr) in d + self._f = d['f'] + except: + raise CompilerError, ('Python expression error:\n' + '%s: %s') % exc_info()[:2] + self._get_used_names() + + def _get_used_names(self): + self._f_varnames = vnames = [] + for vname in self._f.func_code.co_names: + if vname[0] not in '$_': + vnames.append(vname) + + def _bind_used_names(self, econtext): + # Bind template variables + names = {} + vars = econtext.vars + getType = econtext._engine.getTypes().get + for vname in self._f_varnames: + has, val = vars.has_get(vname) + if not has: + has = val = getType(vname) + if has: + val = ExprTypeProxy(vname, val, econtext) + if has: + names[vname] = val + return names + + def __call__(self, econtext): + __traceback_info__ = self.expr + f = self._f + f.func_globals.update(self._bind_used_names(econtext)) + return f() + + def __str__(self): + return 'Python expression "%s"' % self.expr + def __repr__(self): + return '' % self.expr + +class ExprTypeProxy: + '''Class that proxies access to an expression type handler''' + def __init__(self, name, handler, econtext): + self._name = name + self._handler = handler + self._econtext = econtext + def __call__(self, text): + return self._handler(self._name, text, + self._econtext._engine)(self._econtext) + diff --git a/roundup/cgi/PageTemplates/README.txt b/roundup/cgi/PageTemplates/README.txt new file mode 100644 index 0000000..013f0ae --- /dev/null +++ b/roundup/cgi/PageTemplates/README.txt @@ -0,0 +1,7 @@ +See the +ZPT project Wiki for more information about Page Templates, or +the download page +for installation instructions and the most recent version of the software. + +This Product requires the TAL and ZTUtils packages to be installed in +your Python path (not Products). See the links above for more information. diff --git a/roundup/cgi/PageTemplates/TALES.py b/roundup/cgi/PageTemplates/TALES.py new file mode 100644 index 0000000..33b6e9f --- /dev/null +++ b/roundup/cgi/PageTemplates/TALES.py @@ -0,0 +1,286 @@ +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +"""TALES + +An implementation of a generic TALES engine +""" + +__version__='$Revision: 1.1 $'[11:-2] + +import re, sys +from roundup.cgi import ZTUtils +from MultiMapping import MultiMapping + +StringType = type('') + +NAME_RE = r"[a-zA-Z][a-zA-Z0-9_]*" +_parse_expr = re.compile(r"(%s):" % NAME_RE).match +_valid_name = re.compile('%s$' % NAME_RE).match + +class TALESError(Exception): + """Error during TALES expression evaluation""" + +class Undefined(TALESError): + '''Exception raised on traversal of an undefined path''' + +class RegistrationError(Exception): + '''TALES Type Registration Error''' + +class CompilerError(Exception): + '''TALES Compiler Error''' + +class Default: + '''Retain Default''' +Default = Default() + +_marker = [] + +class SafeMapping(MultiMapping): + '''Mapping with security declarations and limited method exposure. + + Since it subclasses MultiMapping, this class can be used to wrap + one or more mapping objects. Restricted Python code will not be + able to mutate the SafeMapping or the wrapped mappings, but will be + able to read any value. + ''' + __allow_access_to_unprotected_subobjects__ = 1 + push = pop = None + + _push = MultiMapping.push + _pop = MultiMapping.pop + + def has_get(self, key, _marker=[]): + v = self.get(key, _marker) + return v is not _marker, v + +class Iterator(ZTUtils.Iterator): + def __init__(self, name, seq, context): + ZTUtils.Iterator.__init__(self, seq) + self.name = name + self._context = context + + def next(self): + if ZTUtils.Iterator.next(self): + self._context.setLocal(self.name, self.item) + return 1 + return 0 + + +class ErrorInfo: + """Information about an exception passed to an on-error handler.""" + __allow_access_to_unprotected_subobjects__ = 1 + + 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] + + +class Engine: + '''Expression Engine + + An instance of this class keeps a mutable collection of expression + type handlers. It can compile expression strings by delegating to + these handlers. It can provide an expression Context, which is + capable of holding state and evaluating compiled expressions. + ''' + Iterator = Iterator + + def __init__(self, Iterator=None): + self.types = {} + if Iterator is not None: + self.Iterator = Iterator + + def registerType(self, name, handler): + if not _valid_name(name): + raise RegistrationError, 'Invalid Expression type "%s".' % name + types = self.types + if types.has_key(name): + raise RegistrationError, ( + 'Multiple registrations for Expression type "%s".' % + name) + types[name] = handler + + def getTypes(self): + return self.types + + def compile(self, expression): + m = _parse_expr(expression) + if m: + type = m.group(1) + expr = expression[m.end():] + else: + type = "standard" + expr = expression + try: + handler = self.types[type] + except KeyError: + raise CompilerError, ( + 'Unrecognized expression type "%s".' % type) + return handler(type, expr, self) + + def getContext(self, contexts=None, **kwcontexts): + if contexts is not None: + if kwcontexts: + kwcontexts.update(contexts) + else: + kwcontexts = contexts + return Context(self, kwcontexts) + + def getCompilerError(self): + return CompilerError + +class Context: + '''Expression Context + + An instance of this class holds context information that it can + use to evaluate compiled expressions. + ''' + + _context_class = SafeMapping + position = (None, None) + source_file = None + + def __init__(self, engine, contexts): + self._engine = engine + self.contexts = contexts + contexts['nothing'] = None + contexts['default'] = Default + + self.repeat_vars = rv = {} + # Wrap this, as it is visible to restricted code + contexts['repeat'] = rep = self._context_class(rv) + contexts['loop'] = rep # alias + + self.global_vars = gv = contexts.copy() + self.local_vars = lv = {} + self.vars = self._context_class(gv, lv) + + # Keep track of what needs to be popped as each scope ends. + self._scope_stack = [] + + def beginScope(self): + self._scope_stack.append([self.local_vars.copy()]) + + def endScope(self): + scope = self._scope_stack.pop() + self.local_vars = lv = scope[0] + v = self.vars + v._pop() + v._push(lv) + # Pop repeat variables, if any + i = len(scope) - 1 + while i: + name, value = scope[i] + if value is None: + del self.repeat_vars[name] + else: + self.repeat_vars[name] = value + i = i - 1 + + def setLocal(self, name, value): + self.local_vars[name] = value + + def setGlobal(self, name, value): + self.global_vars[name] = value + + def setRepeat(self, name, expr): + expr = self.evaluate(expr) + if not expr: + return self._engine.Iterator(name, (), self) + it = self._engine.Iterator(name, expr, self) + old_value = self.repeat_vars.get(name) + self._scope_stack[-1].append((name, old_value)) + self.repeat_vars[name] = it + return it + + def evaluate(self, expression, + isinstance=isinstance, StringType=StringType): + if isinstance(expression, StringType): + expression = self._engine.compile(expression) + __traceback_supplement__ = ( + TALESTracebackSupplement, self, expression) + v = expression(self) + return v + + evaluateValue = evaluate + + def evaluateBoolean(self, expr): + return not not self.evaluate(expr) + + def evaluateText(self, expr, None=None): + text = self.evaluate(expr) + if text is Default or text is None: + return text + return str(text) + + def evaluateStructure(self, expr): + return self.evaluate(expr) + evaluateStructure = evaluate + + def evaluateMacro(self, expr): + # XXX Should return None or a macro definition + return self.evaluate(expr) + evaluateMacro = evaluate + + def createErrorInfo(self, err, position): + return ErrorInfo(err, position) + + def getDefault(self): + return Default + + def setSourceFile(self, source_file): + self.source_file = source_file + + def setPosition(self, position): + self.position = position + + + +class TALESTracebackSupplement: + """Implementation of ITracebackSupplement""" + def __init__(self, context, expression): + self.context = context + self.source_url = context.source_file + self.line = context.position[0] + self.column = context.position[1] + self.expression = repr(expression) + + def getInfo(self, as_html=0): + import pprint + data = self.context.contexts.copy() + s = pprint.pformat(data) + if not as_html: + return ' - Names:\n %s' % string.replace(s, '\n', '\n ') + else: + from cgi import escape + return 'Names:
%s
' % (escape(s)) + return None + + + +class SimpleExpr: + '''Simple example of an expression type handler''' + def __init__(self, name, expr, engine): + self._name = name + self._expr = expr + def __call__(self, econtext): + return self._name, self._expr + def __repr__(self): + return '' % (self._name, `self._expr`) + diff --git a/roundup/cgi/PageTemplates/__init__.py b/roundup/cgi/PageTemplates/__init__.py new file mode 100644 index 0000000..3d58de0 --- /dev/null +++ b/roundup/cgi/PageTemplates/__init__.py @@ -0,0 +1,28 @@ +############################################################################## +# +# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +__doc__='''Package wrapper for Page Templates + +This wrapper allows the Page Template modules to be segregated in a +separate package. + +$Id: __init__.py,v 1.1 2002-09-05 00:37:09 richard Exp $''' +__version__='$$'[11:-2] + + +# Placeholder for Zope Product data +misc_ = {} + +def initialize(context): + # Import lazily, and defer initialization to the module + import ZopePageTemplate + ZopePageTemplate.initialize(context) diff --git a/roundup/cgi/TAL/.cvsignore b/roundup/cgi/TAL/.cvsignore new file mode 100644 index 0000000..0cd1d94 --- /dev/null +++ b/roundup/cgi/TAL/.cvsignore @@ -0,0 +1,2 @@ +.path +*.pyc diff --git a/roundup/cgi/TAL/HTMLParser.py b/roundup/cgi/TAL/HTMLParser.py new file mode 100644 index 0000000..5ab076b --- /dev/null +++ b/roundup/cgi/TAL/HTMLParser.py @@ -0,0 +1,403 @@ +"""A parser for HTML and XHTML.""" + +# This file is based on sgmllib.py, but the API is slightly different. + +# XXX There should be a way to distinguish between PCDATA (parsed +# character data -- the normal case), RCDATA (replaceable character +# data -- only char and entity references and end tags are special) +# and CDATA (character data -- only end tags are special). + + +import markupbase +import re +import string + +# Regular expressions used for parsing + +interesting_normal = re.compile('[&<]') +interesting_cdata = re.compile(r'<(/|\Z)') +incomplete = re.compile('&[a-zA-Z#]') + +entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]') +charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]') + +starttagopen = re.compile('<[a-zA-Z]') +piclose = re.compile('>') +endtagopen = re.compile('') +tagfind = re.compile('[a-zA-Z][-.a-zA-Z0-9:_]*') +attrfind = re.compile( + r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*' + r'(\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9./:;+*%?!&$\(\)_#=~]*))?') + +locatestarttagend = re.compile(r""" + <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name + (?:\s+ # whitespace before attribute name + (?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name + (?:\s*=\s* # value indicator + (?:'[^']*' # LITA-enclosed value + |\"[^\"]*\" # LIT-enclosed value + |[^'\">\s]+ # bare value + ) + )? + ) + )* + \s* # trailing whitespace +""", re.VERBOSE) +endendtag = re.compile('>') +endtagfind = re.compile('') + + +class HTMLParseError(Exception): + """Exception raised for all parse errors.""" + + def __init__(self, msg, position=(None, None)): + assert msg + self.msg = msg + self.lineno = position[0] + self.offset = position[1] + + 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) + return result + + +def _contains_at(s, sub, pos): + return s[pos:pos+len(sub)] == sub + + +class HTMLParser(markupbase.ParserBase): + """Find tags and other markup and call handler functions. + + Usage: + p = HTMLParser() + p.feed(data) + ... + p.close() + + Start tags are handled by calling self.handle_starttag() or + self.handle_startendtag(); end tags by self.handle_endtag(). The + data between tags is passed from the parser to the derived class + by calling self.handle_data() with the data as argument (the data + may be split up in arbitrary chunks). Entity references are + passed by calling self.handle_entityref() with the entity + reference as the argument. Numeric character references are + passed to self.handle_charref() with the string containing the + reference as the argument. + """ + + CDATA_CONTENT_ELEMENTS = ("script", "style") + + + def __init__(self): + """Initialize and reset this instance.""" + self.reset() + + def reset(self): + """Reset this instance. Loses all unprocessed data.""" + self.rawdata = '' + self.stack = [] + self.lasttag = '???' + self.interesting = interesting_normal + markupbase.ParserBase.reset(self) + + def feed(self, data): + """Feed data to the parser. + + Call this as often as you want, with as little or as much text + as you want (may include '\n'). + """ + self.rawdata = self.rawdata + data + self.goahead(0) + + def close(self): + """Handle any buffered data.""" + self.goahead(1) + + def error(self, message): + raise HTMLParseError(message, self.getpos()) + + __starttag_text = None + + def get_starttag_text(self): + """Return full source of start tag: '<...>'.""" + return self.__starttag_text + + cdata_endtag = None + + def set_cdata_mode(self, endtag=None): + self.cdata_endtag = endtag + self.interesting = interesting_cdata + + def clear_cdata_mode(self): + self.cdata_endtag = None + self.interesting = interesting_normal + + # Internal -- handle data as far as reasonable. May leave state + # and data to be processed by a subsequent call. If 'end' is + # true, force handling all data as if followed by EOF marker. + def goahead(self, end): + rawdata = self.rawdata + i = 0 + n = len(rawdata) + while i < n: + match = self.interesting.search(rawdata, i) # < or & + if match: + j = match.start() + else: + j = n + if i < j: self.handle_data(rawdata[i:j]) + i = self.updatepos(i, j) + if i == n: break + if rawdata[i] == '<': + if starttagopen.match(rawdata, i): # < + letter + k = self.parse_starttag(i) + elif endtagopen.match(rawdata, i): # + if not match: + return -1 + j = match.start() + self.handle_pi(rawdata[i+2: j]) + j = match.end() + return j + + # Internal -- handle starttag, return end or -1 if not terminated + def parse_starttag(self, i): + self.__starttag_text = None + endpos = self.check_for_whole_start_tag(i) + if endpos < 0: + return endpos + rawdata = self.rawdata + self.__starttag_text = rawdata[i:endpos] + + # Now parse the data between i+1 and j into a tag and attrs + attrs = [] + match = tagfind.match(rawdata, i+1) + assert match, 'unexpected call to parse_starttag()' + k = match.end() + self.lasttag = tag = string.lower(rawdata[i+1:k]) + + while k < endpos: + m = attrfind.match(rawdata, k) + if not m: + break + attrname, rest, attrvalue = m.group(1, 2, 3) + if not rest: + attrvalue = None + elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ + attrvalue[:1] == '"' == attrvalue[-1:]: + attrvalue = attrvalue[1:-1] + attrvalue = self.unescape(attrvalue) + attrs.append((string.lower(attrname), attrvalue)) + k = m.end() + + end = string.strip(rawdata[k:endpos]) + if end not in (">", "/>"): + lineno, offset = self.getpos() + if "\n" in self.__starttag_text: + lineno = lineno + string.count(self.__starttag_text, "\n") + offset = len(self.__starttag_text) \ + - string.rfind(self.__starttag_text, "\n") + else: + offset = offset + len(self.__starttag_text) + self.error("junk characters in start tag: %s" + % `rawdata[k:endpos][:20]`) + if end[-2:] == '/>': + # XHTML-style empty tag: + self.handle_startendtag(tag, attrs) + else: + self.handle_starttag(tag, attrs) + if tag in self.CDATA_CONTENT_ELEMENTS: + self.set_cdata_mode(tag) + return endpos + + # Internal -- check to see if we have a complete starttag; return end + # or -1 if incomplete. + def check_for_whole_start_tag(self, i): + rawdata = self.rawdata + m = locatestarttagend.match(rawdata, i) + if m: + j = m.end() + next = rawdata[j:j+1] + if next == ">": + return j + 1 + if next == "/": + s = rawdata[j:j+2] + if s == "/>": + return j + 2 + if s == "/": + # buffer boundary + return -1 + # else bogus input + self.updatepos(i, j + 1) + self.error("malformed empty start tag") + if next == "": + # end of input + return -1 + if next in ("abcdefghijklmnopqrstuvwxyz=/" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + # end of input in or before attribute value, or we have the + # '/' from a '/>' ending + return -1 + self.updatepos(i, j) + self.error("malformed start tag") + raise AssertionError("we should not get here!") + + # Internal -- parse endtag, return end or -1 if incomplete + def parse_endtag(self, i): + rawdata = self.rawdata + assert rawdata[i:i+2] == " + if not match: + return -1 + j = match.end() + match = endtagfind.match(rawdata, i) # + if not match: + self.error("bad end tag: %s" % `rawdata[i:j]`) + tag = string.lower(match.group(1)) + if ( self.cdata_endtag is not None + and tag != self.cdata_endtag): + # Should be a mismatched end tag, but we'll treat it + # as text anyway, since most HTML authors aren't + # interested in the finer points of syntax. + self.handle_data(match.group(0)) + else: + self.handle_endtag(tag) + self.clear_cdata_mode() + return j + + # Overridable -- finish processing of start+end tag: + def handle_startendtag(self, tag, attrs): + self.handle_starttag(tag, attrs) + self.handle_endtag(tag) + + # Overridable -- handle start tag + def handle_starttag(self, tag, attrs): + pass + + # Overridable -- handle end tag + def handle_endtag(self, tag): + pass + + # Overridable -- handle character reference + def handle_charref(self, name): + pass + + # Overridable -- handle entity reference + def handle_entityref(self, name): + pass + + # Overridable -- handle data + def handle_data(self, data): + pass + + # Overridable -- handle comment + def handle_comment(self, data): + pass + + # Overridable -- handle declaration + def handle_decl(self, decl): + pass + + # Overridable -- handle processing instruction + def handle_pi(self, data): + pass + + def unknown_decl(self, data): + self.error("unknown declaration: " + `data`) + + # Internal -- helper to remove special character quoting + def unescape(self, s): + if '&' not in s: + return s + s = string.replace(s, "<", "<") + s = string.replace(s, ">", ">") + s = string.replace(s, "'", "'") + s = string.replace(s, """, '"') + s = string.replace(s, "&", "&") # Must be last + return s diff --git a/roundup/cgi/TAL/HTMLTALParser.py b/roundup/cgi/TAL/HTMLTALParser.py new file mode 100644 index 0000000..8d7f0db --- /dev/null +++ b/roundup/cgi/TAL/HTMLTALParser.py @@ -0,0 +1,290 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +Parse HTML and compile to TALInterpreter intermediate code. +""" + +import sys +import string + +from TALGenerator import TALGenerator +from TALDefs import ZOPE_METAL_NS, ZOPE_TAL_NS, METALError, TALError +from HTMLParser import HTMLParser, HTMLParseError + +BOOLEAN_HTML_ATTRS = [ + # List of Boolean attributes in HTML that may be given in + # minimized form (e.g. rather than ) + # 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. . + # 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 ' + % (tagstack[0], endtag)) + else: + msg = ('Open tags <%s> do not match close tag ' + % (string.join(tagstack, '>, <'), endtag)) + else: + msg = 'No tags are open to match ' % 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 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} + + def parseFile(self, file): + f = open(file) + data = f.read() + f.close() + self.parseString(data) + + 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 = self.process_ns(tag, attrs) + self.tagstack.append(tag) + self.gen.emitStartElement(tag, attrlist, taldict, metaldict, + 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 = self.process_ns(tag, attrs) + if taldict.get("content"): + self.gen.emitStartElement(tag, attrlist, taldict, metaldict, + self.getpos()) + self.gen.emitEndElement(tag, implied=-1) + else: + self.gen.emitStartElement(tag, attrlist, taldict, metaldict, + self.getpos(), isend=1) + self.pop_xmlns() + + def handle_endtag(self, tag): + if tag in EMPTY_HTML_TAGS: + # 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("" % data) + + def handle_decl(self, data): + self.gen.emitRawText("" % data) + + def handle_pi(self, data): + self.gen.emitRawText("" % data) + + # Internal thingies + + def scan_xmlns(self, attrs): + nsnew = {} + for key, value in attrs: + if key[:6] == "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 = string.split(name, ':', 1) + if prefix == 'xmlns': + nsuri = self.nsdict.get(suffix) + if nsuri in (ZOPE_TAL_NS, ZOPE_METAL_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' + return name, name, 0 + + def process_ns(self, name, attrs): + attrlist = [] + taldict = {} + metaldict = {} + 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 + attrlist.append(item) + if namens in ('metal', 'tal'): + taldict['tal tag'] = namens + return name, attrlist, taldict, metaldict diff --git a/roundup/cgi/TAL/README.txt b/roundup/cgi/TAL/README.txt new file mode 100644 index 0000000..4a28816 --- /dev/null +++ b/roundup/cgi/TAL/README.txt @@ -0,0 +1,97 @@ +TAL - Template Attribute Language +--------------------------------- + +This is an implementation of TAL, the Zope Template Attribute +Language. For TAL, see the Zope Presentation Templates ZWiki: + + http://dev.zope.org/Wikis/DevSite/Projects/ZPT/FrontPage + +It is not a Zope product nor is it designed exclusively to run inside +of Zope, but if you have a Zope checkout that includes +Products/ParsedXML, its Expat parser will be used. + +Prerequisites +------------- + +You need: + +- A recent checkout of Zope2; don't forget to run the wo_pcgi.py + script to compile everything. (See above -- this is now optional.) + +- A recent checkout of the Zope2 product ParsedXML, accessible + throught /lib/python/Products/ParsedXML; don't forget to run + the setup.py script to compiles Expat. (Again, optional.) + +- Python 1.5.2; the driver script refuses to work with other versions + unless you specify the -n option; this is done so that I don't + accidentally use Python 2.x features. + +- Create a .path file containing proper module search path; it should + point the /lib/python directory that you want to use. + +How To Play +----------- + +(Don't forget to edit .path, see above!) + +The script driver.py takes an XML file with TAL markup as argument and +writes the expanded version to standard output. The filename argument +defaults to tests/input/test01.xml. + +Regression test +--------------- + +There are unit test suites in the 'tests' subdirectory; these can be +run with tests/run.py. This should print the testcase names plus +progress info, followed by a final line saying "OK". It requires that +../unittest.py exists. + +There are a number of test files in the 'tests' subdirectory, named +tests/input/test.xml and tests/input/test.html. The +Python script ./runtest.py calls driver.main() for each test file, and +should print " OK" for each one. These tests are also run as +part of the unit test suites, so tests/run.py is all you need. + +What's Here +----------- + +DummyEngine.py simple-minded TALES execution engine +TALInterpreter.py class to interpret intermediate code +TALGenerator.py class to generate intermediate code +XMLParser.py base class to parse XML, avoiding DOM +TALParser.py class to parse XML with TAL into intermediate code +HTMLTALParser.py class to parse HTML with TAL into intermediate code +HTMLParser.py HTML-parsing base class +driver.py script to demonstrate TAL expansion +timer.py script to time various processing phases +setpath.py hack to set sys.path and import ZODB +__init__.py empty file that makes this directory a package +runtest.py Python script to run file-comparison tests +ndiff.py helper for runtest.py to produce diffs +tests/ drectory with test files and output +tests/run.py Python script to run all tests + +Author and License +------------------ + +This code is written by Guido van Rossum (project lead), Fred Drake, +and Tim Peters. It is owned by Digital Creations and can be +redistributed under the Zope Public License. + +TO DO +----- + +(See also http://www.zope.org/Members/jim/ZPTIssueTracker .) + +- Need to remove leading whitespace and newline when omitting an + element (either through tal:replace with a value of nothing or + tal:condition with a false condition). + +- Empty TAL/METAL attributes are ignored: tal:replace="" is ignored + rather than causing an error. + +- HTMLTALParser.py and TALParser.py are silly names. Should be + HTMLTALCompiler.py and XMLTALCompiler.py (or maybe shortened, + without "TAL"?) + +- Should we preserve case of tags and attribute names in HTML? diff --git a/roundup/cgi/TAL/TALDefs.py b/roundup/cgi/TAL/TALDefs.py new file mode 100644 index 0000000..dbc0443 --- /dev/null +++ b/roundup/cgi/TAL/TALDefs.py @@ -0,0 +1,145 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +Common definitions used by TAL and METAL compilation an transformation. +""" + +from types import ListType, TupleType + +TAL_VERSION = "1.3.2" + +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" + +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", + ] + +class TALError(Exception): + + def __init__(self, msg, position=(None, None)): + assert msg != "" + self.msg = msg + self.lineno = position[0] + self.offset = position[1] + + 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) + return result + +class METALError(TALError): + pass + +class TALESError(TALError): + pass + +class ErrorInfo: + + 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): + 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 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: + import string + arg = string.replace(arg, ";;", "\0") + parts = string.split(arg, ';') + parts = map(lambda s, repl=string.replace: repl(s, "\0", ";"), parts) + if len(parts) > 1 and not string.strip(parts[-1]): + 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 cgi +def quote(s, escape=cgi.escape): + return '"%s"' % escape(s, 1) +del cgi diff --git a/roundup/cgi/TAL/TALGenerator.py b/roundup/cgi/TAL/TALGenerator.py new file mode 100644 index 0000000..0bfadea --- /dev/null +++ b/roundup/cgi/TAL/TALGenerator.py @@ -0,0 +1,583 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +Code generator for TALInterpreter intermediate code. +""" + +import string +import re +import cgi + +from TALDefs import * + +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() + self.program = [] + self.stack = [] + 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) + + def getCode(self): + assert not self.stack + assert not self.todoStack + return self.optimize(self.program), self.macros + + def optimize(self, program): + output = [] + collect = [] + rawseen = 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("" % 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 + text = string.join(collect, "") + if text: + i = string.rfind(text, "\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)) + rawseen = cursor+1 + collect = [] + return self.optimizeCommonTriple(output) + + def optimizeArgsList(self, item): + if len(item) == 2: + return item + else: + return item[0], tuple(item[1:]) + + actionIndex = {"replace":0, "insert":1, "metal":2, "tal":3, "xmlns":4, + 0: 0, 1: 1, 2: 2, 3: 3, 4: 4} + def optimizeStartTag(self, collect, name, attrlist, end): + 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] + action = self.actionIndex[action] + attrlist[i] = (name, value, action) + item[3:] + else: + if item[1] is None: + s = item[0] + else: + s = "%s=%s" % (item[0], quote(item[1])) + attrlist[i] = item[0], s + if item[1] is None: + new.append(" " + item[0]) + else: + new.append(" %s=%s" % (item[0], quote(item[1]))) + 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 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): + 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, {}, []) + 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 emitDefineMacro(self, macroName): + program = self.popProgram() + macroName = string.strip(macroName) + 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 = string.strip(slotName) + 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 = string.strip(slotName) + 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 string.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 + string.join(collect, "") + del self.program[i:] + if text: + self.emit("rawtext", text) + return rest + return None + + def replaceAttrs(self, attrlist, repldict): + if not repldict: + return attrlist + newlist = [] + for item in attrlist: + key = item[0] + if repldict.has_key(key): + item = item[:2] + ("replace", repldict[key]) + del repldict[key] + newlist.append(item) + for key, value in repldict.items(): # Add dynamic-only attributes + item = (key, None, "insert", value) + newlist.append(item) + return newlist + + def emitStartElement(self, name, attrlist, taldict, metaldict, + position=(None, None), isend=0): + if not taldict and not metaldict: + # 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 KNOWN_TAL_ATTRIBUTES: + raise TALError("bad TAL attribute: " + `key`, position) + if not (value or key == 'omit-tag'): + raise TALError("missing value for TAL attribute: " + + `key`, position) + for key, value in metaldict.items(): + if key not in KNOWN_METAL_ATTRIBUTES: + raise METALError("bad METAL attribute: " + `key`, + position) + if not value: + raise TALError("missing value for METAL 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") + if len(metaldict) > 1 and (defineMacro or useMacro): + raise METALError("define-macro and use-macro cannot be used " + "together or with define-slot or fill-slot", + position) + if content and replace: + raise TALError("content and replace are mutually exclusive", + position) + + 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 taldict: + dict = {} + for item in attrlist: + key, value = item[:2] + dict[key] = value + self.emit("beginScope", dict) + todo["scope"] = 1 + if onError: + self.pushProgram() # handler + self.emitStartTag(name, list(attrlist)) # Must copy attrlist! + 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: + todo["content"] = content + if replace: + todo["replace"] = replace + self.pushProgram() + optTag = omitTag is not None or TALtag + if optTag: + todo["optional tag"] = omitTag, TALtag + self.pushProgram() + if attrsubst: + repldict = parseAttributeReplacements(attrsubst) + for key, value in repldict.items(): + repldict[key] = self.compileExpression(value) + else: + repldict = {} + if replace: + todo["repldict"] = repldict + repldict = {} + self.emitStartTag(name, self.replaceAttrs(attrlist, repldict), isend) + if optTag: + self.pushProgram() + if content: + 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") + define = todo.get("define") + repldict = todo.get("repldict", {}) + scope = todo.get("scope") + optTag = todo.get("optional tag") + + 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 " % + (what, name, name), position) + + if content: + self.emitSubstitution(content, {}) + if optTag: + self.emitOptTag(name, optTag, isend) + elif not isend: + self.emitEndTag(name) + if replace: + self.emitSubstitution(replace, repldict) + if repeat: + self.emitRepeat(repeat) + if condition: + self.emitCondition(condition) + if onError: + self.emitOnError(name, onError) + if scope: + self.emit("endScope") + if defineSlot: + self.emitDefineSlot(defineSlot) + if fillSlot: + self.emitFillSlot(fillSlot) + if useMacro: + self.emitUseMacro(useMacro) + if defineMacro: + self.emitDefineMacro(defineMacro) + +def test(): + t = TALGenerator() + t.pushProgram() + t.emit("bar") + p = t.popProgram() + t.emit("foo", p) + +if __name__ == "__main__": + test() diff --git a/roundup/cgi/TAL/TALInterpreter.py b/roundup/cgi/TAL/TALInterpreter.py new file mode 100644 index 0000000..0f42284 --- /dev/null +++ b/roundup/cgi/TAL/TALInterpreter.py @@ -0,0 +1,626 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +Interpreter for a pre-compiled TAL program. +""" + +import sys +import getopt + +from cgi import escape +from string import join, lower, rfind +try: + from strop import lower, rfind +except ImportError: + pass + +try: + from cStringIO import StringIO +except ImportError: + from StringIO import StringIO + +from TALDefs import quote, TAL_VERSION, TALError, METALError +from TALDefs import isCurrentVersion, getProgramVersion, getProgramMode +from TALGenerator import TALGenerator + +BOOLEAN_HTML_ATTRS = [ + # List of Boolean attributes in HTML that should be rendered in + # minimized form (e.g. rather than ) + # From http://www.w3.org/TR/xhtml1/#guidelines (C.10) + # XXX 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" +] + +EMPTY_HTML_TAGS = [ + # List of HTML tags with an empty content model; these are + # rendered in minimized form, e.g. . + # From http://www.w3.org/TR/xhtml1/#dtds + "base", "meta", "link", "hr", "br", "param", "img", "area", + "input", "col", "basefont", "isindex", "frame", +] + +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: + apply(TALGenerator.emit, (self,) + args) + + def emitStartElement(self, name, attrlist, taldict, metaldict, + position=(None, None), isend=0): + metaldict = {} + taldict = {} + if self.enabled and self.repldict: + taldict["attributes"] = "x x" + TALGenerator.emitStartElement(self, name, attrlist, + taldict, metaldict, 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: + + def __init__(self, program, macros, engine, stream=None, + debug=0, wrap=60, metal=1, tal=1, showtal=-1, + strictinsert=1, stackLimit=100): + self.program = program + self.macros = macros + self.engine = engine + self.Default = engine.getDefault() + self.stream = stream or sys.stdout + self._stream_write = self.stream.write + 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.popMacro = self.macroStack.pop + self.position = None, None # (lineno, offset) + self.col = 0 + self.level = 0 + self.scopeLevel = 0 + self.sourceFile = None + + def saveState(self): + return (self.position, self.col, self.stream, + self.scopeLevel, self.level) + + def restoreState(self, state): + (self.position, self.col, self.stream, scopeLevel, level) = 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) + + def restoreOutputState(self, state): + (dummy, self.col, self.stream, scopeLevel, level) = 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]) + + def macroContext(self, what): + macroStack = self.macroStack + i = len(macroStack) + while i > 0: + i = i-1 + if macroStack[i][0] == what: + return i + return -1 + + def __call__(self): + assert self.level == 0 + assert self.scopeLevel == 0 + self.interpret(self.program) + assert self.level == 0 + assert self.scopeLevel == 0 + if self.col > 0: + self._stream_write("\n") + self.col = 0 + + def stream_write(self, s, + len=len, rfind=rfind): + self._stream_write(s) + i = rfind(s, '\n') + if i < 0: + self.col = self.col + len(s) + else: + self.col = len(s) - (i + 1) + + bytecode_handlers = {} + + def interpret(self, program, None=None): + 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. + _stream_write = self._stream_write + _stream_write("<" + name) + namelen = _len(name) + col = self.col + namelen + 1 + wrap = self.wrap + align = col + 1 + if align >= wrap/2: + align = 4 # Avoid a narrow column far to the right + attrAction = self.dispatch[""] + try: + for item in attrList: + if _len(item) == 2: + name, s = item + else: + ok, name, s = attrAction(self, item) + if not ok: + continue + slen = _len(s) + if (wrap and + col >= align and + col + 1 + slen > wrap): + _stream_write("\n" + " "*align) + col = align + slen + else: + s = " " + s + col = col + 1 + slen + _stream_write(s) + _stream_write(end) + col = col + endlen + finally: + self.col = col + bytecode_handlers["startTag"] = do_startTag + + def attrAction(self, item): + name, value, action = item[:3] + if action == 1 or (action > 1 and not self.showtal): + return 0, name, value + macs = self.macroStack + if action == 2 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 = rfind(name, ":") + 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 + "slot" + elif suffix == "fill-slot": + pass + else: + return 0, name, value + + if value is None: + value = name + else: + value = "%s=%s" % (name, quote(value)) + return 1, name, value + + def attrAction_tal(self, item): + name, value, action = item[:3] + if action > 1: + return self.attrAction(item) + ok = 1 + if self.html and lower(name) in BOOLEAN_HTML_ATTRS: + evalue = self.engine.evaluateBoolean(item[3]) + if evalue is self.Default: + if action == 1: # Cancelled insert + ok = 0 + elif evalue: + value = None + else: + ok = 0 + else: + evalue = self.engine.evaluateText(item[3]) + if evalue is self.Default: + if action == 1: # Cancelled insert + ok = 0 + else: + if evalue is None: + ok = 0 + value = evalue + if ok: + if value is None: + value = name + value = "%s=%s" % (name, quote(value)) + return ok, name, value + + bytecode_handlers[""] = attrAction + + def no_tag(self, start, program): + state = self.saveState() + self.stream = stream = 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 = '' % 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 dumpMacroStack(self, prefix, suffix, value): + sys.stderr.write("+---- %s%s = %s\n" % (prefix, suffix, value)) + for i in range(len(self.macroStack)): + what, macroName, slots = self.macroStack[i] + sys.stderr.write("| %2d. %-12s %-12s %s\n" % + (i, what, macroName, slots and slots.keys())) + sys.stderr.write("+--------------------------------------\n") + + def do_rawtextBeginScope(self, (s, col, position, closeprev, dict)): + self._stream_write(s) + self.col = col + self.do_setPosition(position) + 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 + self.do_setPosition(position) + engine = self.engine + 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_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 + s = escape(text) + self._stream_write(s) + i = rfind(s, '\n') + if i < 0: + self.col = self.col + len(s) + else: + self.col = len(s) - (i + 1) + bytecode_handlers["insertText"] = do_insertText + + 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 = str(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, 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, 0) + p = TALParser(gen) + gen.enable(0) + p.parseFragment('') + gen.enable(1) + p.parseFragment(text) # Raises an exception if text is invalid + gen.enable(0) + p.parseFragment('', 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 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) + 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) + saved_source = self.sourceFile + saved_position = self.position # Used by Boa Constructor + self.interpret(macro) + if self.sourceFile != saved_source: + self.engine.setSourceFile(saved_source) + self.sourceFile = saved_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: + saved_source = self.sourceFile + saved_position = self.position # Used by Boa Constructor + macroName, slots = self.popMacro()[:2] + slot = slots.get(slotName) + if slot is not None: + self.interpret(slot) + if self.sourceFile != saved_source: + self.engine.setSourceFile(saved_source) + self.sourceFile = saved_source + self.pushMacro(macroName, slots, entering=0) + return + self.pushMacro(macroName, slots) + if len(macs) == 1: + self.interpret(block) + return + 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 = StringIO() + self._stream_write = stream.write + try: + self.interpret(block) + 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_tal + bytecode_handlers_tal["optTag"] = do_optTag_tal + + +def test(): + from driver import FILE, parsefile + from DummyEngine import DummyEngine + try: + opts, args = getopt.getopt(sys.argv[1:], "") + except getopt.error, msg: + print msg + sys.exit(2) + if args: + file = args[0] + else: + file = FILE + doc = parsefile(file) + compiler = TALCompiler(doc) + program, macros = compiler() + engine = DummyEngine() + interpreter = TALInterpreter(program, macros, engine) + interpreter() + +if __name__ == "__main__": + test() diff --git a/roundup/cgi/TAL/TALParser.py b/roundup/cgi/TAL/TALParser.py new file mode 100644 index 0000000..f75414e --- /dev/null +++ b/roundup/cgi/TAL/TALParser.py @@ -0,0 +1,137 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +Parse XML and compile to TALInterpreter intermediate code. +""" + +import string +from XMLParser import XMLParser +from TALDefs import * +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 = self.process_ns(name, attrlist) + attrlist = self.xmlnsattrs() + attrlist + self.gen.emitStartElement(name, attrlist, taldict, metaldict) + + def process_ns(self, name, attrlist): + taldict = {} + metaldict = {} + 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",) + fixedattrlist.append(item) + if namens in ('metal', 'tal'): + taldict['tal tag'] = namens + return name, fixedattrlist, taldict, metaldict + + 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): + item = (key, uri, "xmlns") + else: + item = (key, uri) + newlist.append(item) + self.nsNew = [] + return newlist + + def fixname(self, name): + if ' ' in name: + uri, name = string.split(name, ' ') + 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' + 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() diff --git a/roundup/cgi/TAL/XMLParser.py b/roundup/cgi/TAL/XMLParser.py new file mode 100644 index 0000000..98cc4d3 --- /dev/null +++ b/roundup/cgi/TAL/XMLParser.py @@ -0,0 +1,89 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" +Generic expat-based XML parser base class. +""" + +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: + #zLOG.LOG("TAL.XMLParser", zLOG.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: + #zLOG.LOG("TAL.XMLParser", zLOG.PROBLEM, + # "Can't set expat handler %s" % name) + pass + + def createParser(self, encoding=None): + global XMLParseError + try: + from Products.ParsedXML.Expat import pyexpat + XMLParseError = pyexpat.ExpatError + return pyexpat.ParserCreate(encoding, ' ') + except ImportError: + from xml.parsers import expat + XMLParseError = expat.ExpatError + return 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) diff --git a/roundup/cgi/TAL/__init__.py b/roundup/cgi/TAL/__init__.py new file mode 100644 index 0000000..080ed5d --- /dev/null +++ b/roundup/cgi/TAL/__init__.py @@ -0,0 +1,14 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE +# +############################################################################## +""" Template Attribute Language package """ diff --git a/roundup/cgi/TAL/markupbase.py b/roundup/cgi/TAL/markupbase.py new file mode 100644 index 0000000..eab134c --- /dev/null +++ b/roundup/cgi/TAL/markupbase.py @@ -0,0 +1,306 @@ +"""Shared support for scanning document type declarations in HTML and XHTML.""" + +import re +import string + +_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match +_declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match + +del re + + +class ParserBase: + """Parser base class which provides some common support methods used + by the SGML/HTML and XHTML parsers.""" + + def reset(self): + self.lineno = 1 + self.offset = 0 + + def getpos(self): + """Return current line number and offset.""" + return self.lineno, self.offset + + # Internal -- update line number and offset. This should be + # called for each piece of data exactly once, in order -- in other + # words the concatenation of all the input strings to this + # function should be exactly the entire input. + def updatepos(self, i, j): + if i >= j: + return j + rawdata = self.rawdata + nlines = string.count(rawdata, "\n", i, j) + if nlines: + self.lineno = self.lineno + nlines + pos = string.rindex(rawdata, "\n", i, j) # Should not fail + self.offset = j-(pos+1) + else: + self.offset = self.offset + j-i + return j + + _decl_otherchars = '' + + # Internal -- parse declaration (for use by subclasses). + def parse_declaration(self, i): + # This is some sort of declaration; in "HTML as + # deployed," this should only be the document type + # declaration (""). + rawdata = self.rawdata + import sys + j = i + 2 + assert rawdata[i:j] == "' + n = len(rawdata) + decltype, j = self._scan_name(j, i) + if j < 0: + return j + if decltype == "doctype": + self._decl_otherchars = '' + while j < n: + c = rawdata[j] + if c == ">": + # end of declaration syntax + data = rawdata[i+2:j] + if decltype == "doctype": + self.handle_decl(data) + else: + self.unknown_decl(data) + return j + 1 + if c in "\"'": + m = _declstringlit_match(rawdata, j) + if not m: + return -1 # incomplete + j = m.end() + elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": + name, j = self._scan_name(j, i) + elif c in self._decl_otherchars: + j = j + 1 + elif c == "[": + if decltype == "doctype": + j = self._parse_doctype_subset(j + 1, i) + else: + self.error("unexpected '[' char in declaration") + else: + self.error( + "unexpected %s char in declaration" % `rawdata[j]`) + if j < 0: + return j + return -1 # incomplete + + # Internal -- scan past the internal subset in a n: + # end of buffer; incomplete + return -1 + if rawdata[j:j+4] == "