[Checkins] SVN: Sandbox/malthe/chameleon.zpt/ Initial implementation.

Malthe Borch mborch at gmail.com
Wed Sep 17 12:21:21 EDT 2008


Log message for revision 91209:
  Initial implementation.

Changed:
  A   Sandbox/malthe/chameleon.zpt/
  A   Sandbox/malthe/chameleon.zpt/README.txt
  A   Sandbox/malthe/chameleon.zpt/bootstrap.py
  A   Sandbox/malthe/chameleon.zpt/buildout.cfg
  A   Sandbox/malthe/chameleon.zpt/setup.py
  A   Sandbox/malthe/chameleon.zpt/src/
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/__init__.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/__init__.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/configure.zcml
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/expressions.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/i18n.txt
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/interfaces.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.txt
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.txt
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/__init__.py
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/helloworld.pt
  A   Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/test_doctests.py

-=-
Added: Sandbox/malthe/chameleon.zpt/README.txt
===================================================================
--- Sandbox/malthe/chameleon.zpt/README.txt	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/README.txt	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,60 @@
+Overview
+========
+
+This package provides a fast Zope Page Template implementation based
+on the Chameleon template compiler. It's largely compatible with
+``zope.pagetemplate``.
+
+Usage
+-----
+
+Load the package component configuration file (configure.zcml).
+
+Performance
+-----------
+
+Casual benchmarks pegs it 16x more performant than the reference
+implementations for Zope TAL and Genshi (using Python-expressions).
+
+Compiler notes
+--------------
+
+The compiler is largely compatible with the reference
+implementation. The TAL implementation is based on the 1.4 language
+specification.
+
+Some notable changes:
+
+1. Tuple unpacking is allowed when defining variables:
+
+      tal:define="(a, b, c) [1, 2, 3]"
+
+2. Generators are allowed in tal:repeat statements. Note that the
+   repeat variable is not available in this case.
+
+      tal:repeat="i <some generator>"
+
+3. Attribute-access to dictionary entries is allowed in
+   Python-expressions, e.g.
+
+      dictionary.key
+
+   can be used instead of ``dictionary['key']``.
+
+4. The default expression type is Python.
+
+.. _TAL: http://wiki.zope.org/ZPT/TALSpecification14
+
+
+Development
+-----------
+
+If you want to use the code directly from trunk (recommended only for
+development and testing usage), provide ``chameleon.zpt==dev`` as your
+dependency.
+
+svn://svn.zope.org/repos/main/Sandbox/malthe/chameleon.zpt/trunk#egg=chameleon.zpt-dev
+
+Want to contribute? Join #zope3-dev on Freenode IRC.
+
+

Added: Sandbox/malthe/chameleon.zpt/bootstrap.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/bootstrap.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/bootstrap.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,52 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id: bootstrap.py 71627 2006-12-20 16:46:11Z jim $
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+ez = {}
+exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                     ).read() in ez
+ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+import pkg_resources
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+if sys.platform == 'win32':
+    cmd = '"%s"' % cmd # work around spawn lamosity on windows
+
+ws = pkg_resources.working_set
+assert os.spawnle(
+    os.P_WAIT, sys.executable, sys.executable,
+    '-c', cmd, '-mqNxd', tmpeggs, 'zc.buildout',
+    dict(os.environ,
+         PYTHONPATH=
+         ws.find(pkg_resources.Requirement.parse('setuptools')).location
+         ),
+    ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout')
+import zc.buildout.buildout
+zc.buildout.buildout.main(sys.argv[1:] + ['bootstrap'])
+shutil.rmtree(tmpeggs)

Added: Sandbox/malthe/chameleon.zpt/buildout.cfg
===================================================================
--- Sandbox/malthe/chameleon.zpt/buildout.cfg	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/buildout.cfg	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,14 @@
+[buildout]
+develop = .
+parts = test
+
+[test]
+recipe = zc.recipe.testrunner
+environment = test-environment
+eggs =
+   Chameleon [lxml]
+   chameleon.zpt
+
+[test-environment]
+CHAMELEON_DEBUG = False
+CHAMELEON_CACHE = False
\ No newline at end of file

Added: Sandbox/malthe/chameleon.zpt/setup.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/setup.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/setup.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,35 @@
+from setuptools import setup, find_packages
+import sys
+
+version = '1.0dev'
+
+install_requires = [
+    'setuptools',
+    'zope.interface',
+    'zope.component',
+    'zope.i18n >= 3.5',
+    'Chameleon',
+    ]
+
+setup(name='chameleon.zpt',
+      version=version,
+      description="Zope Page Template engine based on Chameleon",
+      long_description=open("README.txt").read(),
+      classifiers=[
+        "Programming Language :: Python",
+        "Topic :: Text Processing :: Markup :: HTML",
+        "Topic :: Text Processing :: Markup :: XML",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+        ],
+      keywords='',
+      author='Malthe Borch and the Zope Community',
+      author_email='zope-dev at zope.org',
+      url='',
+      license='BSD',
+      namespace_packages=['chameleon'],
+      packages = find_packages('src'),
+      package_dir = {'':'src'},
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=install_requires,
+      )

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/__init__.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/__init__.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/__init__.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,6 @@
+# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+try:
+    __import__('pkg_resources').declare_namespace(__name__)
+except ImportError:
+    from pkgutil import extend_path
+    __path__ = extend_path(__path__, __name__)

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/__init__.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/__init__.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/__init__.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1 @@
+#

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/configure.zcml
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/configure.zcml	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/configure.zcml	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,14 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           package="chameleon.zpt">
+           
+  <include package="zope.component" file="meta.zcml" />
+  
+  <utility
+     name="python"
+     component=".expressions.python_translator" />
+
+  <adapter
+     name="string"
+     factory=".expressions.StringTranslator" />
+
+</configure>

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/expressions.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/expressions.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/expressions.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,556 @@
+import re
+import parser
+
+from zope import interface
+from zope import component
+
+from chameleon.core import types
+
+import interfaces
+
+class ExpressionTranslator(object):
+    """Base class for TALES expression translation."""
+
+    interface.implements(interfaces.IExpressionTranslator)
+    
+    re_pragma = re.compile(r'^\s*(?P<pragma>[a-z]+):')
+    re_method = re.compile(r'^(?P<name>[A-Za-z0-9_]+)'
+                           '(\((?P<args>[A-Za-z0-9_]+\s*(,\s*[A-Za-z0-9_]+)*)\))?')
+
+    def translator(self, name):
+        return component.queryUtility(
+            interfaces.IExpressionTranslator, name=name) or \
+            component.queryAdapter(
+            self, interfaces.IExpressionTranslator, name=name)
+    
+    def declaration(self, string):
+        """Variable declaration.
+        
+        >>> declaration = ExpressionTranslator().declaration
+
+        Single variable:
+
+        >>> declaration("variable")
+        declaration('variable')
+
+        Multiple variables:
+
+        >>> declaration("variable1, variable2")
+        declaration('variable1', 'variable2')
+
+        Repeat not allowed:
+
+        >>> declaration('repeat')
+        Traceback (most recent call last):
+         ...
+        ValueError: Invalid variable name 'repeat' (reserved).
+
+        >>> declaration('_disallowed')
+        Traceback (most recent call last):
+         ...
+        ValueError: Invalid variable name '_disallowed' (starts with an underscore).
+        """
+
+        variables = []
+        for var in string.split(', '):
+            var = var.strip()
+
+            if var in ('repeat',):
+                raise ValueError, "Invalid variable name '%s' (reserved)." % var
+
+            if var.startswith('_') and not var.startswith('_tmp'):
+                raise ValueError(
+                    "Invalid variable name '%s' (starts with an underscore)." % var)
+
+            variables.append(var)
+
+        return types.declaration(variables)
+
+    def mapping(self, string):
+        """Semicolon-separated mapping.
+        
+        >>> mapping = ExpressionTranslator().mapping
+
+        >>> mapping("abc def")
+        mapping(('abc', 'def'),)
+
+        >>> mapping("abc def;")
+        mapping(('abc', 'def'),)
+
+        >>> mapping("abc")
+        mapping(('abc', None),)
+
+        >>> mapping("abc;")
+        mapping(('abc', None),)
+
+        >>> mapping("abc; def ghi")
+        mapping(('abc', None), ('def', 'ghi'))
+        """
+
+        defs = string.split(';')
+        mappings = []
+        for d in defs:
+            d = d.strip()
+            if d == '':
+                continue
+
+            while '  ' in d:
+                d = d.replace('  ', ' ')
+
+            parts = d.split(' ')
+            if len(parts) == 1:
+                mappings.append((d, None))
+            elif len(parts) == 2:
+                mappings.append((parts[0], parts[1]))
+            else:
+                raise ValueError, "Invalid mapping (%s)." % string
+
+        return types.mapping(mappings)
+
+    def definitions(self, string):
+        """Semi-colon separated variable definitions.
+        
+        >>> class MockExpressionTranslator(ExpressionTranslator):
+        ...     def validate(self, string):
+        ...         if string == '' or ';' in string:
+        ...             raise SyntaxError()
+        ...
+        ...     def tales(self, string):
+        ...         self.validate(string)
+        ...         return types.value(string.strip())
+
+        >>> definitions = MockExpressionTranslator().definitions
+        
+        Single define:
+        
+        >>> definitions("variable expression")
+        definitions((declaration('variable'), value('expression')),)
+        
+        Multiple defines:
+        
+        >>> definitions("variable1 expression1; variable2 expression2")
+        definitions((declaration('variable1'), value('expression1')),
+                    (declaration('variable2'), value('expression2')))
+        
+        Tuple define:
+        
+        >>> definitions("(variable1, variable2) (expression1, expression2)")
+        definitions((declaration('variable1', 'variable2'),
+                    value('(expression1, expression2)')),)
+
+        Global defines:
+
+        >>> definitions("global variable expression")
+        definitions((declaration('variable', global_scope=True), value('expression')),)
+
+        Space, the 'in' operator and '=' may be used to separate
+        variable from expression.
+
+        >>> definitions("variable in expression")
+        definitions((declaration('variable'), value('expression')),)        
+        
+        >>> definitions("variable1 = expression1; variable2 = expression2")
+        definitions((declaration('variable1'), value('expression1')),
+                    (declaration('variable2'), value('expression2')))
+
+        >>> definitions("variable1=expression1; variable2=expression2")
+        definitions((declaration('variable1'), value('expression1')),
+                    (declaration('variable2'), value('expression2')))
+        
+        A define clause that ends in a semicolon:
+        
+        >>> definitions("variable expression;")
+        definitions((declaration('variable'), value('expression')),)
+        
+        A define clause with a trivial expression (we do allow this):
+        
+        >>> definitions("variable")
+        definitions((declaration('variable'), None),)
+        
+        A proper define clause following one with a trivial expression:
+        
+        >>> definitions("variable1 expression; variable2")
+        definitions((declaration('variable1'), value('expression')),
+                    (declaration('variable2'), None))
+        """
+
+        string = string.replace('\n', '').strip()
+
+        defines = []
+        i = 0
+        while i < len(string):
+            global_scope = False
+            if string.startswith('global'):
+                global_scope = True
+                i += 6
+
+            while string[i] == ' ':
+                i += 1
+
+            # get variable definition
+            if string[i] == '(':
+                j = string.find(')', i+1)
+                if j == -1:
+                    raise ValueError, "Invalid variable tuple definition (%s)." % string
+                var = self.declaration(string[i+1:j])
+                j += 1
+            else:
+                j = string.find('=', i + 1)
+                k = string.find(' ', i + 1)
+                if k < j and k > -1 or j < 0:
+                    j = k
+                
+                if j < 0:
+                    var = self.declaration(string[i:])
+                    j = len(string)
+                else:
+                    var = self.declaration(string[i:j])
+
+            var.global_scope = global_scope
+            
+            # get expression
+            i = j + len(string) - j - len(string[j:].lstrip())
+
+            token = string[i:]
+            if token.startswith('=='):
+                raise ValueError("Invalid variable definition (%s)." % string)
+            elif token.startswith('='):
+                i += 1
+            elif token.startswith('in '):
+                i += 3
+
+            try:
+                expr = self.tales(string[i:])
+                j = -1
+            except SyntaxError, e:
+                expr = None
+                j = len(string)
+            
+            while j > i:
+                j = string.rfind(';', i, j)
+                if j < 0:
+                    raise e
+
+                try:
+                    expr = self.tales(string[i:j])
+                except SyntaxError, e:
+                    if string.rfind(';', i, j) > 0:
+                        continue
+                    raise e
+                
+                break
+                
+            defines.append((var, expr))
+
+            if j < 0:
+                break
+            
+            i = j + 1
+
+        return types.definitions(defines)
+
+    def definition(self, string):
+        defs = self.definitions(string)
+        if len(defs) != 1:
+            raise ValueError, "Multiple definitions not allowed."
+
+        return defs[0]
+
+    def output(self, string):
+        """String output; supports 'structure' keyword.
+        
+        >>> class MockExpressionTranslator(ExpressionTranslator):
+        ...     def validate(self, string):
+        ...         return True
+        ...
+        ...     def translate(self, string):
+        ...         return types.value(string)
+
+        >>> output = MockExpressionTranslator().output
+
+        >>> output("context/title")
+        escape(value('context/title'),)
+
+        >>> output("context/pretty_title_or_id|context/title")
+        escape(value('context/pretty_title_or_id'), value('context/title'))
+
+        >>> output("structure context/title")
+        value('context/title')        
+        """
+        
+        if string.startswith('structure '):
+            return self.tales(string[len('structure'):])
+        
+        expression = self.tales(string)
+
+        if isinstance(expression, types.parts):
+            return types.escape(expression)
+
+        return types.escape((expression,))
+            
+    def tales(self, string):
+        """We need to implement the ``validate`` and
+        ``translate``-methods. Let's define that an expression is
+        valid if it contains an odd number of characters.
+        
+        >>> class MockExpressionTranslator(ExpressionTranslator):
+        ...     def validate(self, string):
+        ...         return True
+        ...
+        ...     def translate(self, string):
+        ...         return types.value(string)
+
+        >>> tales = MockExpressionTranslator().tales
+                
+        >>> tales('a')
+        value('a')
+
+        >>> tales('a|b')
+        parts(value('a'), value('b'))
+    
+        """
+
+        string = string.replace('\n', '').strip()
+
+        if not string:
+            return types.parts()
+
+        parts = []
+
+        # default translator is ``self``
+        translator = self
+
+        i = j = 0
+        while i < len(string):
+            if translator is self:
+                match = self.re_pragma.match(string[i:])
+                if match is not None:
+                    pragma = match.group('pragma')
+                    translator = self.translator(pragma)
+                    if translator is not None:
+                        i += match.end()
+                        continue
+
+                    translator = self
+
+            j = string.find('|', j + 1)
+            if j == -1:
+                j = len(string)
+
+            expr = string[i:j]
+
+            try:
+                translator.validate(expr)
+            except Exception, e:
+                if j < len(string):
+                    continue
+
+                # re-raise with traceback
+                translator.validate(expr)
+
+            value = translator.translate(expr)
+            parts.append(value)
+            translator = self
+            
+            i = j + 1
+
+        if len(parts) == 1:
+            return parts[0]
+
+        return types.parts(parts)
+
+class PythonTranslator(ExpressionTranslator):
+    """Implements Python expression translation."""
+    
+    def validate(self, string):
+        """We use the ``parser`` module to determine if
+        an expression is a valid python expression."""
+
+        if isinstance(string, unicode):
+            string = string.encode('utf-8')
+            
+        parser.expr(string.strip())
+
+    def translate(self, string):
+        if isinstance(string, str):
+            string = string.decode('utf-8')
+
+        return types.value(string.strip())
+
+python_translator = PythonTranslator()
+
+class StringTranslator(ExpressionTranslator):
+    """Implements string translation expression."""
+
+    component.adapts(interfaces.IExpressionTranslator)
+    
+    re_interpolation = re.compile(r'(?P<prefix>[^\\]\$|^\$)({((?P<expression>.*)})?|'
+                                  '(?P<variable>[A-Za-z][A-Za-z0-9_]*))')
+    
+    def __init__(self, translator):
+        self._translator = translator
+
+    def validate(self, string):
+        self.interpolate(string)
+        self.split(string)
+            
+    def translate(self, string):
+        return types.join(self.split(string))
+            
+    def split(self, string):
+        """Split up an interpolation string expression into parts that
+        are either unicode strings or ``value``-tuples.
+
+        >>> class MockTranslator(object):
+        ...     def tales(self, string):
+        ...         return types.value(string)
+        
+        >>> split = StringTranslator(MockTranslator()).split
+
+        >>> split("${abc}")
+        (value('abc'),)
+
+        >>> split(" ${abc}")
+        (' ', value('abc'))
+
+        >>> split("abc${def}")
+        ('abc', value('def'))
+
+        >>> split("${def}abc")
+        (value('def'), 'abc')
+
+        >>> split("abc${def}ghi")
+        ('abc', value('def'), 'ghi')
+
+        >>> split("abc${def}ghi${jkl}")
+        ('abc', value('def'), 'ghi', value('jkl'))
+
+        >>> split("abc${def}")
+        ('abc', value('def'))
+
+        >>> print split(u"abc${ghi}")
+        (u'abc', value('ghi'))
+        
+        """
+
+        m = self.interpolate(string)
+        if m is None:
+            return (self._unescape(string),)
+
+        prefix = m.group('prefix')
+        parts = []
+        
+        start = m.start() + len(prefix) - 1
+        if start > 0:
+            text = string[:start]
+            parts.append(self._unescape(text))
+
+        expression = m.group('expression')
+        variable = m.group('variable')
+
+        if expression:
+            parts.append(self._translator.tales(expression))
+        elif variable:
+            parts.append(self._translator.tales(variable))
+                
+        rest = string[m.end():]
+        if len(rest):
+            parts.extend(self.split(rest))
+
+        return tuple(parts)
+
+    def interpolate(self, string):
+        """Search for an interpolation and return a match.
+
+        >>> class MockTranslator(object):
+        ...     def tales(self, string):
+        ...         return types.parts((types.value(string),))
+
+        >>> interpolate = StringTranslator(MockTranslator()).interpolate
+        
+        >>> interpolate('${abc}').group('expression')
+        'abc'
+
+        >>> interpolate(' ${abc}').group('expression')
+        'abc'
+
+        >>> interpolate('abc${def}').group('expression')
+        'def'
+
+        >>> interpolate('abc${def}ghi${jkl}').group('expression')
+        'def'
+
+        >>> interpolate('$abc').group('variable')
+        'abc'
+
+        >>> interpolate('${abc')
+        Traceback (most recent call last):
+          ...
+        SyntaxError: Interpolation expressions must be of the form ${<expression>} (${abc)
+        
+        """
+
+        m = self.re_interpolation.search(string)
+        if m is None:
+            return None
+
+        expression = m.group('expression')
+        variable = m.group('variable')
+        
+        if expression:
+            left = m.start()+len(m.group('prefix'))+1
+            right = string.find('}')
+
+            while right != -1:
+                match = string[left:right]
+                try:
+                    exp = self._translator.tales(match)
+                    break
+                except SyntaxError:
+                    right = string.find('}', right)
+            else:
+                raise
+
+            string = string[:right+1]
+            return self.re_interpolation.search(string)
+
+        if m is None or (expression is None and variable is None):
+            raise SyntaxError(
+                "Interpolation expressions must be of the "
+                "form ${<expression>} (%s)" % string)
+
+        if expression and not m.group('expression'):
+            raise SyntaxError(expression)
+
+        return m
+
+    def _unescape(self, string):
+        """
+        >>> unescape = StringTranslator(None)._unescape
+        
+        >>> unescape('string:Hello World')
+        'string:Hello World'
+        
+        >>> unescape('; string:Hello World')
+        Traceback (most recent call last):
+         ...
+        SyntaxError: Semi-colons in string-expressions must be escaped.
+
+        >>> unescape(';; string:Hello World')
+        '; string:Hello World'
+
+        >>> unescape('string:Hello World;')
+        'string:Hello World;'
+        
+        """
+        
+        i = string.rfind(';')
+        if i < 0 or i == len(string) - 1:
+            return string
+        
+        j = string.rfind(';'+';')
+        if j < 0 or i != j + 1:
+            raise SyntaxError(
+                "Semi-colons in string-expressions must be escaped.")
+        
+        return string.replace(';;', ';')

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/i18n.txt
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/i18n.txt	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/i18n.txt	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,276 @@
+Internationalization
+--------------------
+
+Page templates support the i18n attribute language. The implementation
+is based on this document:
+
+  * http://wiki.zope.org/zope3/ZPTInternationalizationSupport
+
+With the exception of i18n:data and i18n:source, the implementation is
+complete.
+
+To get set started, let's provide German mock translations for all
+msgids:
+
+  >>> from zope import component
+  >>> from zope import interface
+
+  >>> from zope.i18n.interfaces import ITranslationDomain
+  >>> class MockTranslationDomain(object):
+  ...     interface.implements(ITranslationDomain)
+  ...
+  ...     def translate(self, msgid, mapping=None, context=None,
+  ...                   target_language=None, default=None):
+  ...         if target_language != 'de':
+  ...             return default
+  ...
+  ...         return "Mock translation of '%s'." % msgid
+
+  >>> td = MockTranslationDomain()
+  >>> component.provideUtility(td, ITranslationDomain, name="test")
+
+Translation of tag contents
+---------------------------
+
+First, a simple example:
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <span i18n:domain="test" i18n:translate="test_msgid">
+  ...     Default
+  ...   </span>
+  ... </div>"""
+
+First we need to turn the template into a callable:
+
+  >>> from chameleon.zpt.template import PageTemplate
+  >>> template = PageTemplate(body)
+
+Let's try rendering this template without passing a target language.
+    
+  >>> print template.render()
+  <div>
+    <span>
+      Default
+    </span>
+  </div>
+
+Now we'll render the template again---passing German as the language.
+
+  >>> print template.render(target_language='de')
+  <div>
+    <span>Mock translation of 'test_msgid'.</span>
+  </div>
+
+Let's try infering the translation message id from the tag body.
+  
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <span i18n:domain="test" i18n:translate="">
+  ...     Default
+  ...   </span>
+  ... </div>"""
+
+Not passing a language:
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <span>
+      Default
+    </span>
+  </div>
+
+Passing German:
+  
+  >>> print template.render(target_language='de')
+  <div>
+    <span>Mock translation of 'Default'.</span>
+  </div>
+
+We could also add a named block inside the tag.
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <p i18n:domain="test" i18n:translate="">
+  ...     <span i18n:name="count">18</span> bananas.
+  ...   </p>
+  ... </div>"""
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <p>
+      <span>18</span> bananas.
+    </p>
+  </div>
+    
+  >>> print template.render(target_language='de')
+  <div>
+    <p>Mock translation of '${count} bananas.'.</p>
+  </div>
+
+Translation of tag attributes
+-----------------------------
+
+A simple example to start with.
+  
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <span i18n:domain="test" title="Simple Title" i18n:attributes="title">
+  ...     Default
+  ...   </span>
+  ... </div>"""
+
+Not passing a language:
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <span title="Simple Title">
+      Default
+    </span>
+  </div>
+
+Passing German:
+
+  >>> print template.render(target_language='de')
+  <div>
+    <span title="Mock translation of 'Simple Title'.">
+      Default
+    </span>
+  </div>
+
+Use an explicit msgid:
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <span i18n:domain="test" title="Simple Title"
+  ...         i18n:attributes="title title_simple">
+  ...     Default
+  ...   </span>
+  ... </div>"""
+
+Not passing a language:
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <span title="Simple Title">
+      Default
+    </span>
+  </div>
+
+Passing German:
+
+  >>> print template.render(target_language='de')
+  <div>
+    <span title="Mock translation of 'title_simple'.">
+      Default
+    </span>
+  </div>
+
+Use an explicit msgid with a trailing semicolon.
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <span i18n:domain="test" title="Simple Title"
+  ...         i18n:attributes="title title_simple;">
+  ...     Default
+  ...   </span>
+  ... </div>"""
+
+Not passing a language:
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <span title="Simple Title">
+      Default
+    </span>
+  </div>
+
+Passing German:
+
+  >>> print template.render(target_language='de')
+  <div>
+    <span title="Mock translation of 'title_simple'.">
+      Default
+    </span>
+  </div>
+
+Use multiple attributes on the same tag.
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n">
+  ...   <span i18n:domain="test" title="Simple Title"
+  ...         longdesc="A not so short description."
+  ...         i18n:attributes="title title_simple; longdesc desc_short">
+  ...     Default
+  ...   </span>
+  ... </div>"""
+
+Not passing a language:
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <span title="Simple Title" longdesc="A not so short description.">
+      Default
+    </span>
+  </div>
+
+Passing German:
+
+  >>> print template.render(target_language='de')
+  <div>
+    <span title="Mock translation of 'title_simple'."
+          longdesc="Mock translation of 'desc_short'.">
+      Default
+    </span>
+  </div>
+
+Translation of tag content and tag attributes
+---------------------------------------------
+
+A simple example to start with.
+  
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <span i18n:domain="test" i18n:translate="tid"
+  ...         title="Title" i18n:attributes="title aid">
+  ...     Default, "default", 'default'
+  ...   </span>
+  ...   <span i18n:domain="test" i18n:translate=""
+  ...         tal:content="string:tid">
+  ...     Default, "default", 'default'
+  ...   </span>
+  ... </div>"""
+
+Not passing a language:
+
+  >>> template = PageTemplate(body)
+  >>> print template.render()
+  <div>
+    <span title="Title">
+      Default, "default", 'default'
+    </span>
+    <span>tid</span>
+  </div>
+
+Passing German:
+
+  >>> print template.render(target_language='de')
+  <div>
+    <span title="Mock translation of 'aid'.">Mock translation of 'tid'.</span>
+    <span>Mock translation of 'tid'.</span>
+  </div>

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/interfaces.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/interfaces.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/interfaces.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,62 @@
+from zope import interface
+
+class IExpressionTranslator(interface.Interface):
+    """This interface defines an expression translation utility or
+    adapter; most implementations will subclass
+    ``chameleon.zpt.language.TALES`` and override one or more
+    methods."""
+
+    def validate(string):
+        """Void method which raises a syntax error if ``string`` is
+        not a valid exception."""
+
+    def translate(string):
+        """Translates ``string`` into an expression type (see
+        ``chameleon.core.types``)."""
+
+    def tales(string):
+        """TALES Expression.
+
+        Specification:
+
+            tales ::= (pragma:) expression ['|' tales]
+            
+        """
+            
+    def declaration(string):
+        """A variable definition.
+        
+        Specification:
+        
+           variables ::= variable_name [',' variables]
+
+        This corresponds to Python variable assignment which supports
+        assignment in tuples.
+        """
+        
+    def mapping(string):
+        """A mapping definition.
+
+        Specification:
+
+            mapping ::= token value [';' mapping]
+
+        """
+
+    def definition(string):
+        """Variable Assignment.
+
+        Specification:
+        
+           definition ::= declaration expression
+
+        """
+        
+    def definitions(string):
+        """Multiple variable definitions.
+        
+        Specification:
+
+           definitions ::= definition [';' definitions]
+           
+        """

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,220 @@
+from zope import component
+
+import itertools
+
+from chameleon.core import translation
+from chameleon.core import config
+from chameleon.core import etree
+from chameleon.core import utils
+
+import interfaces
+import expressions
+
+class ZopePageTemplateElement(translation.Element):
+    """Zope Page Template element.
+
+    Implements the ZPT subset of the attribute template language.
+    """
+
+    class node(translation.Node):
+        @property
+        def omit(self):
+            if self.element.tal_omit is not None:
+                return self.element.tal_omit or True
+            if self.element.meta_omit is not None:
+                return self.element.meta_omit or True
+            if self.element.tal_replace or self.element.meta_replace:
+                return True
+            if self.element.metal_use or self.element.metal_fillslot:
+                return True
+            
+        @property
+        def define(self):
+            return self.element.tal_define
+
+        @property
+        def condition(self):
+            return self.element.tal_condition
+
+        @property
+        def repeat(self):
+            return self.element.tal_repeat
+
+        @property
+        def content(self):
+            return self.element.tal_content or self.element.tal_replace or \
+                   self.element.meta_replace
+
+        @property
+        def skip(self):
+            if self.define_slot:
+                variable = self.symbols.slot + self.define_slot
+                if variable in itertools.chain(*self.stream.scope):
+                    return True
+
+            return self.content or \
+                   self.use_macro or self.translate is not None
+
+        @property
+        def dynamic_attributes(self):
+            return (self.element.tal_attributes or ()) + \
+                   (self.element.meta_attributes or ())
+
+        @property
+        def translated_attributes(self):
+            return self.element.i18n_attributes
+        
+        @property
+        def static_attributes(self):
+            return utils.get_attributes_from_namespace(
+                self.element, config.XHTML_NS)
+            
+        @property
+        def translate(self):
+            return self.element.i18n_translate
+
+        @property
+        def translation_name(self):
+            return self.element.i18n_name
+
+        @property
+        def translation_domain(self):
+            return self.element.i18n_domain
+
+        @property
+        def use_macro(self):
+            return self.element.metal_use
+        
+        @property
+        def define_slot(self):
+            return self.element.metal_defineslot
+
+        @property
+        def fill_slot(self):
+            return self.element.metal_fillslot        
+
+        @property
+        def cdata(self):
+            return self.element.meta_cdata
+
+    node = property(node)
+
+    @property
+    def translator(self):
+        while self.meta_translator is None:
+            self = self.getparent()
+            if self is None:
+                raise ValueError("Default expression not set.")
+
+        return component.getUtility(
+            interfaces.IExpressionTranslator, name=self.meta_translator)
+
+    metal_define = None
+    metal_use = None
+    metal_fillslot = None
+    metal_defineslot = None
+
+    i18n_domain = None
+    i18n_translate = None
+    i18n_attributes = None
+    
+    tal_define = None
+    tal_condition = None
+    tal_replace = None
+    tal_content = None
+    tal_repeat = None
+    tal_attributes = None
+
+    meta_translator = etree.Annotation(
+        utils.meta_attr('translator'))
+
+class XHTMLElement(ZopePageTemplateElement):
+    """XHTML namespace element."""
+
+    tal_define = utils.attribute(
+        utils.tal_attr('define'), lambda p: p.definitions)
+    tal_condition = utils.attribute(
+        utils.tal_attr('condition'), lambda p: p.tales)
+    tal_repeat = utils.attribute(
+        utils.tal_attr('repeat'), lambda p: p.definition)
+    tal_attributes = utils.attribute(
+        utils.tal_attr('attributes'), lambda p: p.definitions)
+    tal_content = utils.attribute(
+        utils.tal_attr('content'), lambda p: p.output)
+    tal_replace = utils.attribute(
+        utils.tal_attr('replace'), lambda p: p.output)
+    tal_omit = utils.attribute(
+        utils.tal_attr('omit-tag'), lambda p: p.tales)
+    tal_default_expression = utils.attribute(
+        utils.tal_attr('default-expression'), encoding='ascii')
+    metal_define = utils.attribute(
+        utils.metal_attr('define-macro'))
+    metal_use = utils.attribute(
+        utils.metal_attr('use-macro'), lambda p: p.tales)
+    metal_fillslot = utils.attribute(
+        utils.metal_attr('fill-slot'))
+    metal_defineslot = utils.attribute(
+        utils.metal_attr('define-slot'))
+    i18n_translate = utils.attribute(
+        utils.i18n_attr('translate'))
+    i18n_attributes = utils.attribute(
+        utils.i18n_attr('attributes'), lambda p: p.mapping)
+    i18n_domain = utils.attribute(
+        utils.i18n_attr('domain'))
+    i18n_name = utils.attribute(
+        utils.i18n_attr('name'))
+    
+class MetaElement(XHTMLElement, translation.MetaElement):
+    pass
+
+class TALElement(ZopePageTemplateElement):
+    """TAL namespace element."""
+    
+    tal_define = utils.attribute(
+        ("define", utils.tal_attr("define")), lambda p: p.definitions)
+    tal_condition = utils.attribute(
+        ("condition", utils.tal_attr("condition")), lambda p: p.tales)
+    tal_replace = utils.attribute(
+        ("replace", utils.tal_attr("replace")), lambda p: p.output)
+    tal_repeat = utils.attribute(
+        ("repeat", utils.tal_attr("repeat")), lambda p: p.definition)
+    tal_attributes = utils.attribute(
+        ("attributes", utils.tal_attr("attributes")), lambda p: p.tales)
+    tal_content = utils.attribute(
+        ("content", utils.tal_attr("content")), lambda p: p.output)
+    tal_omit = utils.attribute(
+        ("omit-tag", utils.tal_attr("omit-tag")), lambda p: p.tales, u"")    
+    
+class METALElement(ZopePageTemplateElement):
+    """METAL namespace element."""
+
+    tal_omit = True
+    
+    metal_define = utils.attribute(
+        ("define-macro", utils.metal_attr("define-macro")))
+    metal_use = utils.attribute(
+        ('use-macro', utils.metal_attr('use-macro')), lambda p: p.tales)
+    metal_fillslot = utils.attribute(
+        ('fill-slot', utils.metal_attr('fill-slot')))
+    metal_defineslot = utils.attribute(
+        ('define-slot', utils.metal_attr('define-slot')))
+
+class Parser(etree.Parser):
+    """Zope Page Template parser."""
+    
+    element_mapping = {
+        config.XHTML_NS: {None: XHTMLElement},
+        config.META_NS: {None: MetaElement},
+        config.TAL_NS: {None: TALElement},
+        config.METAL_NS: {None: METALElement}}
+
+    default_expression = 'python'
+
+    def __init__(self, default_expression=None):
+        if default_expression is not None:
+            self.default_expression = default_expression
+
+    def parse(self, body):
+        root, doctype = super(Parser, self).parse(body)
+        root.meta_translator = self.default_expression
+        return root, doctype

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.txt
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.txt	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/language.txt	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,290 @@
+Zope Page Templates (ZPT)
+=========================
+
+This test demonstrates the compilation of TAL and METAL attribute
+languages.
+
+TAL
+---
+  
+:: Namespace elements
+
+  >>> print render("""\
+  ... <tal:block xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   Hello, world!
+  ... </tal:block>""")
+  <BLANKLINE>
+    Hello, world!
+  <BLANKLINE>
+
+tal:define, tal:attributes, tal:contents    
+  
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <span id="test"
+  ...         class="dummy"
+  ...         onclick=""
+  ...         tal:define="a 'abc'"
+  ...         tal:attributes="class 'def' + a; style 'hij'; onClick 'alert();'"
+  ...         tal:content="a + 'ghi'" />
+  ...   <span tal:replace="'Hello World!'">Hello <b>Universe</b>!</span>
+  ...   <span tal:replace="'Hello World!'"><b>Hello Universe!</b></span>
+  ...   <span tal:content="None" />
+  ... </div>""")
+    <div>
+      <span class="defabc" style="hij" onclick="alert();" id="test">abcghi</span>
+      Hello World!
+      Hello World!
+      <span></span>
+    </div>
+
+tal:attributes 'checked' and 'selected' toggles
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <option tal:attributes="selected True"/>
+  ...   <option tal:attributes="selected None"/>
+  ...   <input tal:attributes="checked True"/>
+  ...   <input tal:attributes="checked False"/>
+  ... </div>""")
+    <div>
+       <option selected="True" />
+       <option />
+       <input checked="True" />
+       <input />
+    </div>
+
+tal:repeat
+    
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <ul>
+  ...     <li tal:repeat="i range(5)"><span tal:replace="'Item ' + str(i) + ')'" /></li>
+  ...   </ul>
+  ... </div>""")
+    <div>
+      <ul>
+        <li>Item 0)</li>
+        <li>Item 1)</li>
+        <li>Item 2)</li>
+        <li>Item 3)</li>
+        <li>Item 4)</li>
+      </ul>
+    </div>
+
+tal:repeat (repeat-variable)
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <ul>
+  ...     <li tal:repeat="i range(3)"><span tal:replace="str(i) + ' ' + str(repeat['i'].even())" /></li>
+  ...   </ul>
+  ... </div>""")
+    <div>
+      <ul>
+        <li>0 True</li>
+        <li>1 False</li>
+        <li>2 True</li>
+      </ul>
+    </div>
+
+tal:condition
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <div tal:condition="True">
+  ...     Show me!
+  ...   </div>
+  ...   <div tal:condition="False">
+  ...     Do not show me!
+  ...   </div>
+  ... </div>""")
+    <div>
+      <div>
+        Show me!
+      </div>
+    </div>
+
+:: TAL elements with namespace prefix
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <tal:example replace="'Hello World!'" />
+  ...   <tal:example tal:replace="'Hello World!'" />
+  ...   <tal:div content="'Hello World!'" />
+  ...   <tal:multiple repeat="i range(3)" replace="i" />
+  ...   <tal:div condition="True">True</tal:div>
+  ... </div>""")
+    <div>
+      Hello World!
+      Hello World!
+      Hello World!
+      0
+      1
+      2
+      True
+    </div>
+
+tal:omit-tag
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <p tal:omit-tag="">No paragraph here.</p>
+  ...   <p tal:omit-tag="True">No paragraph here either.</p>
+  ...   <p tal:omit-tag="False">A paragraph here.</p>
+  ... </div>""")
+    <div>
+      No paragraph here.
+      No paragraph here either.
+      <p>A paragraph here.</p>
+    </div>
+
+:: Unicode with dynamic attributes and content
+    
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <img tal:attributes="title '%sHello%s' % (chr(60), chr(62))" />
+  ...   <span tal:replace="structure '%sbr /%s' % (chr(60), chr(62))" />
+  ...   <span tal:replace="'%sbr /%s' % (chr(60), chr(62))" />
+  ...   <span tal:content="unicode('La Pe\xc3\xb1a', 'utf-8')" />
+  ... </div>""")
+    <div>
+      <img title="&lt;Hello&gt;" />
+      <br />
+      &lt;br /&gt;
+      <span>La Peña</span>
+    </div>
+
+:: Using the "string:" expression
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <span tal:replace="string:${greeting}, world!" />
+  ...   <img tal:attributes="alt string:Leonardo da Vinci;; Musee du Louvre, 1503;
+  ...                        title string:Mona Lisa" />
+  ... </div>""", request=object(), greeting=u'Hello')
+  <div>
+    Hello, world!
+    <img alt="Leonardo da Vinci; Musee du Louvre, 1503" title="Mona Lisa" />
+  </div>
+
+:: Using different expressions with try-except operator (|)
+  
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...      <span tal:replace="abc|1" />
+  ...      <span tal:replace="2|abc" />
+  ... </div>""")
+  <div>
+       1
+       2
+  </div>
+
+:: Using the "structure" TAL pragma.
+
+  >>> print render("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal">
+  ...   <span tal:replace="structure dir" />
+  ... </div>""")
+  <div>
+    <built-in function dir>
+  </div>
+
+METAL
+-----
+
+metal:define-macro, metal:use-macro
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal"
+  ...      xmlns:metal="http://xml.zope.org/namespaces/metal">
+  ...   <div class="greeting" metal:define-macro="greeting">
+  ...     Hello, <span tal:replace="name|string:earth" />!
+  ...   </div>
+  ...   <div tal:define="name 'world'">
+  ...     <div metal:use-macro="template.macros['greeting']" />
+  ...   </div>  
+  ... </div>"""
+
+  >>> from chameleon.core.testing import MockTemplate
+  >>> from chameleon.zpt.language import Parser
+
+  >>> template = MockTemplate(body, Parser())
+  >>> print render(body, template=template)
+  <div>
+    <div class="greeting">
+      Hello, earth!
+    </div>
+    <div>
+      <div class="greeting">
+      Hello, world!
+    </div>
+  <BLANKLINE>
+    </div>  
+  </div>
+
+metal:define-slot, metal:fill-slot
+
+  >>> body = """\
+  ... <div xmlns="http://www.w3.org/1999/xhtml"
+  ...      xmlns:tal="http://xml.zope.org/namespaces/tal"
+  ...      xmlns:metal="http://xml.zope.org/namespaces/metal">
+  ...   <div metal:define-macro="greeting">
+  ...     Hey, <span class="name" metal:define-slot="name">
+  ...     a <em>stranger!</em></span>
+  ...   </div>
+  ...   <div metal:use-macro="template.macros['greeting']">
+  ...     This will be omitted
+  ...     <span metal:fill-slot="name">earth!</span>
+  ...   </div>
+  ...   <div metal:use-macro="template.macros['greeting']">
+  ...     <div>
+  ...       <metal:earth fill-slot="name">earth!</metal:earth>
+  ...     </div>
+  ...   </div>
+  ...   <div metal:use-macro="template.macros['greeting']">
+  ...     <!-- display fallback greeting -->
+  ...   </div>
+  ...   <div metal:use-macro="template.macros['greeting']">
+  ...     <span metal:fill-slot="dummy">dummy!</span>
+  ...   </div>
+  ... </div>"""
+
+  >>> template = MockTemplate(body, Parser())
+  >>> print render(body, template=template)
+  <div>
+    <div>
+      Hey, <span class="name">
+      a <em>stranger!</em></span>
+    </div>
+    <div>
+      Hey, <span class="name">earth!</span>
+    </div>
+  <BLANKLINE>
+    <div>
+      Hey, <span class="name">earth!</span>
+    </div>
+  <BLANKLINE>
+    <div>
+      Hey, <span class="name">
+      a <em>stranger!</em></span>
+    </div>
+  <BLANKLINE>
+    <div>
+      Hey, <span class="name">
+      a <em>stranger!</em></span>
+    </div>
+  <BLANKLINE>
+  </div>

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,51 @@
+from zope import i18n
+
+from chameleon.core import template
+from chameleon.core import config
+
+import language
+
+def prepare_language_support(**kwargs):
+    target_language = kwargs.get('target_language')
+
+    if config.DISABLE_I18N:
+        if target_language:
+            del kwargs['target_language']
+        return
+    
+    if not target_language:
+        context = kwargs.get(config.SYMBOLS.i18n_context)
+        target_language = i18n.negotiate(context)
+
+        if target_language:
+            kwargs['target_language'] = target_language    
+
+class PageTemplate(template.Template):
+    __doc__ = template.Template.__doc__ # for Sphinx autodoc
+
+    default_parser = language.Parser()
+    
+    def __init__(self, body, parser=None, format=None, doctype=None):
+        if parser is None:
+            parser = self.default_parser
+        super(PageTemplate, self).__init__(body, parser, format, doctype)
+
+    def render(self, **kwargs):
+        prepare_language_support(**kwargs)
+        return super(PageTemplate, self).render(**kwargs)
+
+class PageTemplateFile(template.TemplateFile):
+    __doc__ = template.TemplateFile.__doc__ # for Sphinx autodoc
+
+    default_parser = language.Parser()
+    
+    def __init__(self, filename, parser=None, format=None,
+                 doctype=None, **kwargs):
+        if parser is None:
+            parser = self.default_parser
+        super(PageTemplateFile, self).__init__(filename, parser, format,
+                                               doctype, **kwargs)
+
+    def render(self, **kwargs):
+        prepare_language_support(**kwargs)
+        return super(PageTemplateFile, self).render(**kwargs)

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.txt
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.txt	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/template.txt	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,34 @@
+Template classes
+================
+
+The ``chameleon.zpt`` package provides the ``PageTemplate`` and
+``PageTemplateFile`` classes which allow easy usage of templates in
+your application.
+
+Usage
+-----
+
+  >>> from chameleon.zpt.template import PageTemplate
+  
+  >>> print PageTemplate("""\
+  ... <div xmlns="http://www.w3.org/1999/xhtml">
+  ...   Hello World!
+  ... </div>""")()
+  <div>
+    Hello World!
+  </div>
+
+  >>> from chameleon.zpt.template import PageTemplateFile
+  >>> from chameleon.zpt import tests
+  
+  >>> path = tests.__path__[0]
+  >>> t = PageTemplateFile(path+'/helloworld.pt')
+  >>> print t()
+  <div>
+    Hello World!
+  </div>
+
+  >>> import os
+  >>> t.filename.startswith(os.sep)
+  True
+

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/__init__.py
===================================================================

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/helloworld.pt
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/helloworld.pt	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/helloworld.pt	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,3 @@
+<div xmlns="http://www.w3.org/1999/xhtml">
+  Hello World!
+</div>

Added: Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/test_doctests.py
===================================================================
--- Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/test_doctests.py	                        (rev 0)
+++ Sandbox/malthe/chameleon.zpt/src/chameleon/zpt/tests/test_doctests.py	2008-09-17 16:21:21 UTC (rev 91209)
@@ -0,0 +1,46 @@
+import zope.testing
+import unittest
+
+OPTIONFLAGS = (zope.testing.doctest.ELLIPSIS |
+               zope.testing.doctest.NORMALIZE_WHITESPACE)
+
+import zope.component.testing
+import zope.configuration.xmlconfig
+
+import chameleon.core.config
+import chameleon.core.testing
+
+import chameleon.zpt
+import chameleon.zpt.language
+
+def render_template(body, **kwargs):
+    parser = chameleon.zpt.language.Parser()
+    return chameleon.core.testing.compile_template(parser, body, **kwargs)
+
+def setUp(suite):
+    zope.component.testing.setUp(suite)
+    zope.configuration.xmlconfig.XMLConfig('configure.zcml', chameleon.zpt)()
+
+def test_suite():
+    filesuites = 'language.txt', 'template.txt', 'i18n.txt'
+    testsuites = 'language',
+
+    globs = dict(render=render_template)
+    
+    chameleon.core.config.DISK_CACHE = False
+    
+    return unittest.TestSuite(
+        [zope.testing.doctest.DocTestSuite(
+        "chameleon.zpt."+doctest, optionflags=OPTIONFLAGS,
+        setUp=setUp, tearDown=zope.component.testing.tearDown) \
+         for doctest in testsuites] + 
+        
+        [zope.testing.doctest.DocFileSuite(
+        doctest, optionflags=OPTIONFLAGS,
+        globs=globs,
+        setUp=setUp, tearDown=zope.component.testing.tearDown,
+        package="chameleon.zpt") for doctest in filesuites]
+        )
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')



More information about the Checkins mailing list