diff options
author | Matthias Klose <doko@debian.org> | 2019-09-05 07:39:45 +0200 |
---|---|---|
committer | Matthias Klose <doko@debian.org> | 2019-09-05 07:39:45 +0200 |
commit | db3cd9bfd6d279eb6fb61a0588dc0bfc9a4e4639 (patch) | |
tree | 54dce106ebe361e51024fa13420057643725b938 |
Import python-chameleon_3.6.2.orig.tar.gz
[dgit import orig python-chameleon_3.6.2.orig.tar.gz]
395 files changed, 16220 insertions, 0 deletions
diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1db69d6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,185 @@ +The majority of the code in Chameleon is supplied under this license: + + A copyright notice accompanies this license document that identifies + the copyright holders. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + 1. Redistributions in source code must retain the accompanying + copyright notice, this list of conditions, and the following + disclaimer. + + 2. Redistributions in binary form must reproduce the accompanying + copyright notice, this list of conditions, and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + 3. Names of the copyright holders must not be used to endorse or + promote products derived from this software without prior + written permission from the copyright holders. + + 4. If any files are modified, you must cause the modified files to + carry prominent notices stating that you changed the files and + the date of any change. + + Disclaimer + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND + ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +Portions of the code in Chameleon are supplied under the ZPL (headers +within individiual files indicate that these portions are licensed +under the ZPL): + + Zope Public License (ZPL) Version 2.1 + ------------------------------------- + + A copyright notice accompanies this license document that + identifies the copyright holders. + + This license has been certified as open source. It has also + been designated as GPL compatible by the Free Software + Foundation (FSF). + + Redistribution and use in source and binary forms, with or + without modification, are permitted provided that the + following conditions are met: + + 1. Redistributions in source code must retain the + accompanying copyright notice, this list of conditions, + and the following disclaimer. + + 2. Redistributions in binary form must reproduce the accompanying + copyright notice, this list of conditions, and the + following disclaimer in the documentation and/or other + materials provided with the distribution. + + 3. Names of the copyright holders must not be used to + endorse or promote products derived from this software + without prior written permission from the copyright + holders. + + 4. The right to distribute this software or to use it for + any purpose does not give you the right to use + Servicemarks (sm) or Trademarks (tm) of the copyright + holders. Use of them is covered by separate agreement + with the copyright holders. + + 5. If any files are modified, you must cause the modified + files to carry prominent notices stating that you changed + the files and the date of any change. + + Disclaimer + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' + AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT + NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN + NO EVENT SHALL THE COPYRIGHT HOLDERS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + +Portions of the code in Chameleon are supplied under the BSD license +(headers within individiual files indicate that these portions are +licensed under this license): + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. The name of the author may not be used to endorse or promote + products derived from this software without specific prior + written permission. + + THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS + OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Portions of the code in Chameleon are supplied under the Python +License (headers within individiual files indicate that these portions +are licensed under this license): + + PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 + -------------------------------------------- + + 1. This LICENSE AGREEMENT is between the Python Software Foundation + ("PSF"), and the Individual or Organization ("Licensee") accessing and + otherwise using this software ("Python") in source or binary form and + its associated documentation. + + 2. Subject to the terms and conditions of this License Agreement, PSF + hereby grants Licensee a nonexclusive, royalty-free, world-wide + license to reproduce, analyze, test, perform and/or display publicly, + prepare derivative works, distribute, and otherwise use Python + alone or in any derivative version, provided, however, that PSF's + License Agreement and PSF's notice of copyright, i.e., "Copyright (c) + 2001, 2002, 2003, 2004 Python Software Foundation; All Rights Reserved" + are retained in Python alone or in any derivative version prepared + by Licensee. + + 3. In the event Licensee prepares a derivative work that is based on + or incorporates Python or any part thereof, and wants to make + the derivative work available to others as provided herein, then + Licensee hereby agrees to include in any such work a brief summary of + the changes made to Python. + + 4. PSF is making Python available to Licensee on an "AS IS" + basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND + DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS + FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT + INFRINGE ANY THIRD PARTY RIGHTS. + + 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON + FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS + A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, + OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + + 6. This License Agreement will automatically terminate upon a material + breach of its terms and conditions. + + 7. Nothing in this License Agreement shall be deemed to create any + relationship of agency, partnership, or joint venture between PSF and + Licensee. This License Agreement does not grant permission to use PSF + trademarks or trade name in a trademark sense to endorse or promote + products or services of Licensee, or any third party. + + 8. By copying, installing or otherwise using Python, Licensee + agrees to be bound by the terms and conditions of this License + Agreement. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f8668de --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include src/chameleon/tests/inputs * +recursive-include src/chameleon/tests/outputs * +include LICENSE.txt diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..f543054 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,1539 @@ +Metadata-Version: 1.1 +Name: Chameleon +Version: 3.6.2 +Summary: Fast HTML/XML Template Compiler. +Home-page: https://chameleon.readthedocs.io +Author: Malthe Borch +Author-email: mborch@gmail.com +License: BSD-like (http://repoze.org/license.html) +Description: Overview + ======== + + Chameleon is an HTML/XML template engine for `Python + <http://www.python.org>`_. It uses the *page templates* language. + + You can use it in any Python web application with just about any + version of Python (2.7 and up, including 3.4+ and `pypy + <http://pypy.org>`_). + + Visit the `documentation <https://chameleon.readthedocs.io/en/latest/>`_ + for more information. + + License and Copyright + --------------------- + + This software is made available as-is under a BSD-like license [1]_ + (see included copyright notice). + + + Notes + ----- + + .. [1] This software is licensed under the `Repoze + <http://repoze.org/license.html>`_ license. + + + Changes + ======= + + 3.6.2 (2019-06-22) + ------------------ + + - Fix SyntaxWarnings in Python 3.8 resulting from comparing literals with 'is'. + See https://github.com/plone/Products.CMFPlone/issues/2890. + + 3.6.1 (2019-04-01) + ------------------ + + - Fix limited search expression for illegal double hyphens in HTML + comments to fix issue #289. + + 3.6 (2019-02-19) + ---------------- + + - Exclude `RuntimeError` (or `RecursionError` when available) from + exception wrapping. + + - Fix double dollar '$$' escaping such that a double dollar is always + resolved, either as an interpolation expression, or as an escape + where it is substituted by a single dollar symbol. This is now + consistent with Zope's handling of this character. + + Backslash-escaping of dollar-based string interpolation is no longer + supported. The documentation has been updated to reflect this + change. + + This fixes issue #283. Note that this reverses some of the changes + introduced to fix issue #265. + + - Drop support for Python 3.3. + + 3.5 (2018-10-17) + ---------------- + + - Add support for Python 3.8. + + - Add support for TAL attributes in an XML declaration tag. This fixes + issue #269. + + - Add support for custom exception handling for the `tal:on-error` + statement. There is now an option `on_error_handler` available + as a template configuration (issue #266). + + - Fix issue where double '$$' escaping would affect non-interpolation + expressions such as the bare '$$' (issue #265). + + - Fix an issue where backslash dollar escaping would leave the + backslash character still in place. + + 3.4 (2018-07-14) + ---------------- + + Bugfixes: + + - Fix regression with translations in case of multiple nodes. + + + 3.3 (2018-05-23) + ---------------- + + Bugfixes: + + - Reset error token when rendering internal macro calls. + + - Fix edge case in exception handler causing recursion. + [MatthewWilkes] + + + 3.2 (2017-10-06) + ---------------- + + Features: + + - Add the automatic variable ``macroname`` that's bound to the name of + the executing macro. Fixes https://github.com/malthe/chameleon/issues/238 + + - A tokenizer can now be configured on the template class. This is + useful in the case where the template file input is modified before + parsing (for example, where some tags are stripped away) such that + token positions need to be offset accordingly for error locations to + be rendered correctly. + + - Expression errors now display source marker (previously only + filename, line and column was shown). + + - No longer require Python source files to import modules. + [mrh1997] + + Optimizations: + + - Exception tracking now defers metadata allocation to time of error. + + + 3.1 (2017-02-21) + ---------------- + + Features: + + - Add option ``restricted_namespace`` which controls whether to + restrict namespaces to those defined and used by the page template + language. + [hansroh] + + Bugs: + + - Fixed attribute HTML entity escaping issue where an entity such as + ``&`` would be encoded twice. + + Optimizations: + + - Simplify exception tracking, reducing bytecode size significantly. + + - Avoid checking if a static string is ``None`` during expression + interpolation. + + + 3.0 (2016-12-07) + ---------------- + + Bugs: + + - Fix issue on Python 2 where an exception was not cleared when using + the pipe operator and was thus accessible through `sys.exc_info()`. + + - The "exists" expression no longer leaks error information. + + - Escape '$$' into '$' in both content and string expressions. + + - Fix use of macro definition inside translation block. + + Improvements: + + - Allow unquoted attribute values. + + - Wrap attribute error thrown when trying to use a non-macro as a + macro as a `RenderError` to get proper error output. + + - Throw a parse error if '--' (double hyphen) appears in an XML + comment. + + - The `i18n:target` attribute now overrides a default + `target_language` variable and is passed to the translation + function. + + - Include filename in the on-disk cache module name. Previously, + only the SHA digest in hex representation would be used, making + it difficult to see where the module came from. This fixes issue + #132. + + - Add support for non-ascii attribute names. + [sank] + + Compatibility: + + - Drop support for Python 2.6, 3.1, and 3.2. + + + 2.25 (2016-09-24) + ----------------- + + - Add explicit support / testing for Python 3.5. + + - Add ``\r`` to negative regex matches to the chameleon parser, where ``\n`` is used but ``\r`` was missing. + Fixes a case, where the tag name was parsed into ``html\r`` instead of ``html``. + Fixes: https://github.com/malthe/chameleon/issues/219 + + + 2.24 (2015-10-28) + ----------------- + + - Fixed Python 3.5 compatibility. + + - Fixed brown bag release. + + + 2.23 (2015-10-26) + ----------------- + + - Added ``enable_data_attributes`` option that allows using HTML5 data + attributes as control attributes instead or in addition to XML + namespace attributes. + + + 2.22 (2015-02-06) + ----------------- + + - Fix brown bag release. + + + 2.21 (2015-02-06) + ----------------- + + - Added ``RenderError`` exception which indicates that an error + occurred during the evaluation of an expression. + + - Clean up ``TemplateError`` exception implementation. + + + 2.20 (2015-01-12) + ----------------- + + - Pass ``search_path`` to template class when loaded using + ``TemplateLoader`` (or one of the derived classes). + [faassen] + + + 2.19 (2015-01-06) + ----------------- + + - Fix logging deprecation. + + - Fix environment-based configuration logging error. + + + 2.18 (2014-11-03) + ----------------- + + - Fix minor compilation error. + + + 2.17 (2014-11-03) + ----------------- + + - Add support for ``i18n:context``. + [wiggy] + + - Add missing 'parity' repeat property. + [voxspox] + + - Don't modify environment when getting variables from it. + [fschulze] + + + 2.16 (2014-05-06) + ----------------- + + - If a repeat expression evaluates to ``None`` then it is now + equivalent to an empty set. + + This changes a behavior introduced in 2.14. + + This fixes issue #172. + + - Remove fossil test dependency on deprecated ``distribute``. + + - Add explicit support / testing for Python 3.3 / 3.4. + + - Drop explicit support for Python 2.5 (out of maintenance, and no longer + supported by ``tox`` or ``Travis-CI``). + + + 2.15 (2014-03-11) + ----------------- + + - Add Support for Python 3.4's ``NameConstant``. + [brakhane] + + + 2.14 (2013-11-28) + ----------------- + + - Element repetition using the ``TAL`` namespace no longer includes + whitespace. This fixes issue #110. + + - Use absolute import for ``chameleon.interfaces`` module. This fixes + issue #161. + + + 2.13-1 (2013-10-24) + ------------------- + + - Fixing brown bag release. + + 2.13 (2013-10-21) + ----------------- + + Bugfixes: + + - The template cache mechanism now includes additional configuration + settings as part of the cache key such as ``strict`` and + ``trim_attribute_space``. + [ossmkitty] + + - Fix cache issue where sometimes cached templates would not load + correctly. + [ossmkitty] + + - In debug-mode, correctly remove temporary files when the module + loader is garbage-collected (on ``__del__``). + [graffic] + + - Fix error message when duplicate i18n:name directives are used in a + translation. + + - Using the three-argument form of ``getattr`` on a + ``chameleon.tal.RepeatDict`` no longer raises ``KeyError``, + letting the default provided to ``getattr`` be used. This fixes + attempting to adapt a ``RepeatDict`` to a Zope interface under + PyPy. + + 2.12 (2013-03-26) + ----------------- + + Changes: + + - When a ``tal:case`` condition succeeds, no other case now will. + + Bugfixes: + + - Implicit translation now correctly extracts and normalizes complete + sentences, instead of words. + [witsch] + + - The ``default`` symbol in a ``tal:case`` condition now allows the + element only if no other case succeeds. + + + 2.11 (2012-11-15) + ----------------- + + Bugfixes: + + - An issue was resolved where a METAL statement was combined with a + ``tal:on-error`` handler. + + - Fix minor parser issue with incorrectly formatted processing + instructions. + + - Provide proper error handling for Python inline code blocks. + + Features: + + - The simple translation function now supports the + ``translationstring`` interface. + + Optimizations: + + - Minor optimization which correctly detects when an element has no + attributes. + + + 2.10 (2012-10-12) + ----------------- + + Deprecations: + + - The ``fast_translate`` function has been deprecated. Instead, the + default translation function is now always a function that simply + interpolates the mapping onto the message default or id. + + The motivation is that since version 2.9, the ``context`` argument + is non-trivial: the ``econtext`` mapping is passed. This breaks an + expectation on the Zope platform that the ``context`` parameter is + the HTTP request. Previously, with Chameleon this parameter was + simply not provided and so that did not cause issues as such. + + - The ``ast24`` module has been renamed to ``ast25``. This should help + clear up any confusion that Chameleon 2.x might be support a Python + interpreter less than version 2.5 (it does not). + + Features: + + - The ``ProxyExpr`` expression class (and hence the ``load:`` + expression type) is now a TALES-expression. In practical terms, this + means that the expression type (which computes a string result using + the standard ``"${...}"`` interpolation syntax and proxies the + result through a function) now supports fallback using the pipe + operator (``"|"``). This fixes issue #128. + + - An attempt to interpolate using the empty string as the expression + (i.e. ``${}``) now does nothing: the string ``${}`` is simply output + as is. + + - Added support for adding, modifying, and removing attributes using a + dictionary expression in ``tal:attributes`` (analogous to Genshi's + ``py:attrs`` directive):: + + <div tal:attributes="name value; attrs" /> + + In the example above, ``name`` is an identifier, while ``value`` and + ``attrs`` are Python expressions. However, ``attrs`` must evaluate + to a Python dictionary object (more concisely, the value must + implement the dictionary API-methods ``update()`` and ``items()``). + + Optimizations: + + - In order to cut down on the size of the compiled function objects, + some conversion and quoting statements have been put into + functions. In one measurement, the reduction was 35%. The benchmark + suite does *not* report of an increased render time (actually + slightly decreased). + + Bugfixes: + + - An exception is now raised if a trivial string is passed for + ``metal:fill-slot``. This fixes issue #89. + + - An empty string is now never translated. Not really a bug, but it's + been reported in as an issue (#92) because some translation + frameworks handle this case incorrectly. + + - The template module loader (file cache) now correctly encodes + generated template source code as UTF-8. This fixes issue #125. + + - Fixed issue where a closure might be reused unsafely in nested + template rendering. + + - Fixed markup class ``__repr__`` method. This fixes issue #124. + + - Added missing return statement to fix printing the non-abbreviated + filename in case of an exception. + [tomo] + + 2.9.2 (2012-06-06) + ------------------ + + Bugfixes: + + - Fixed a PyPy incompatibility. + + - Fixed issue #109 which caused testing failures on some platforms. + + 2.9.1 (2012-06-01) + ------------------ + + Bugfixes: + + - Fixed issue #103. The ``tal:on-error`` statement now always adds an + explicit end-tag to the element, even with a substitution content of + nothing. + + - Fixed issue #113. The ``tal:on-error`` statement now works correctly + also for dynamic attributes. That is, the fallback tag now includes + only static attributes. + + - Fixed name error which prevented the benchmark from running + correctly. + + Compatibility: + + - Fixed deprecation warning on Python 3 for zope interface implements + declaration. This fixes issue #116. + + 2.9.0 (2012-05-31) + ------------------ + + Features: + + - The translation function now gets the ``econtext`` argument as the + value for ``context``. Note that historically, this was usually an + HTTP request which might provide language negotiation data through a + dictionary interface. + [alvinyue] + + Bugfixes: + + - Fixed import alias issue which would lead to a syntax error in + generated Python code. Fixes issue #114. + + 2.8.5 (2012-05-02) + ------------------ + + Bugfixes: + + - Fixed minor installation issues on Python 2.5 and 3. + [ppaez] + + - Ensure output is unicode even when trivial (an empty string). + + 2.8.4 (2012-04-18) + ------------------ + + Features: + + - In exception output, long filenames are now truncated to 60 + characters of output, preventing line wrap which makes it difficult + to scan the exception output. + + Bugfixes: + + - Include filename and location in exception output for exceptions + raised during compilation. + + - If a trivial translation substitution variable is given (i.e. an + empty string), simply ignore it. This fixes issue #106. + + 2.8.3 (2012-04-16) + ------------------ + + Features: + + - Log template source on debug-level before cooking. + + - The `target_language` argument, if given, is now available as a + variable in templates. + + 2.8.2 (2012-03-30) + ------------------ + + Features: + + - Temporary caches used in debug mode are cleaned up eagerly, rather + than waiting for process termination. + [mitchellrj] + + Bugfixes: + + - The `index`, `start` and `end` methods on the TAL repeat object are + now callable. This fixes an incompatibility with ZPT. + + - The loader now correctly handles absolute paths on Windows. + [rdale] + + 2.8.1 (2012-03-29) + ------------------ + + Features: + + - The exception formatter now lists errors in 'wrapping order'. This + means that the innermost, and presumably most relevant exception is + shown last. + + Bugfixes: + + - The exception formatter now correctly recognizes nested errors and + does not rewrap the dynamically generated exception class. + + - The exception formatter now correctly sets the ``__module__`` + attribute to that of the original exception class. + + 2.8.0 (2012-02-29) + ------------------ + + Features: + + - Added support for code blocks using the `<?python ... ?>` processing + instruction syntax. + + The scope is name assignments is up until the nearest macro + definition, or the template itself if macros are not used. + + Bugfixes: + + - Fall back to the exception class' ``__new__`` method to safely + create an exception object that is not implemented in Python. + + - The exception formatter now keeps track of already formatted + exceptions, and ignores them from further output. + + 2.7.4 (2012-02-27) + ------------------ + + - The error handler now invokes the ``__init__`` method of + ``BaseException`` instead of the possibly overriden method (which + may take required arguments). This fixes issue #97. + [j23d, malthe] + + 2.7.3 (2012-01-16) + ------------------ + + Bugfixes: + + - The trim whitespace option now correctly trims actual whitespace to + a single character, appearing either to the left or to the right of + an element prefix or suffix string. + + 2.7.2 (2012-01-08) + ------------------ + + Features: + + - Added option ``trim_attribute_space`` that decides whether attribute + whitespace is stripped (at most down to a single space). This option + exists to provide compatibility with the reference + implementation. Fixes issue #85. + + Bugfixes: + + - Ignore unhashable builtins when generating a reverse builtin + map to quickly look up a builtin value. + [malthe] + + - Apply translation mapping even when a translation function is not + available. This fixes issue #83. + [malthe] + + - Fixed issue #80. The translation domain for a slot is defined by the + source document, i.e. the template providing the content for a slot + whether it be the default or provided through ``metal:fill-slot``. + [jcbrand] + + - In certain circumstances, a Unicode non-breaking space character would cause + a define clause to fail to parse. + + 2.7.1 (2011-12-29) + ------------------ + + Features: + + - Enable expression interpolation in CDATA. + + - The page template class now implements dictionary access to macros:: + + template[name] + + This is a short-hand for:: + + template.macros[name] + + Bugfixes: + + - An invalid define clause would be silently ignored; we now raise a + language error exception. This fixes issue #79. + + - Fixed regression where ``${...}`` interpolation expressions could + not span multiple lines. This fixes issue #77. + + 2.7.0 (2011-12-13) + ------------------ + + Features: + + - The ``load:`` expression now derives from the string expression such + that the ``${...}`` operator can be used for expression + interpolation. + + - The ``load:`` expression now accepts asset specs; these are resolved + by the ``pkg_resources.resource_filename`` function:: + + <package_name>:<path> + + An example from the test suite:: + + chameleon:tests/inputs/hello_world.pt + + Bugfixes: + + - If an attribute name for translation was not a valid Python + identifier, the compiler would generate invalid code. This has been + fixed, and the compiler now also throws an exception if an attribute + specification contains a comma. (Note that the only valid separator + character is the semicolon, when specifying attributes for + translation via the ``i18n:translate`` statement). This addresses + issue #76. + + 2.6.2 (2011-12-08) + ------------------ + + Bugfixes: + + - Fixed issue where ``tal:on-error`` would not respect + ``tal:omit-tag`` or namespace elements which are omitted by default + (such as ``<tal:block />``). + + - Fixed issue where ``macros`` attribute would not be available on + file-based templates due to incorrect initialization. + + - The ``TryExcept`` and ``TryFinally`` AST nodes are not available on + Python 3.3. These have been aliased to ``Try``. This fixes issue + #75. + + Features: + + - The TAL repeat item now makes a security declaration that grants + access to unprotected subobjects on the Zope 2 platform:: + + __allow_access_to_unprotected_subobjects__ = True + + This is required for legacy compatibility and does not affect other + environments. + + - The template object now has a method ``write(body)`` which + explicitly decodes and cooks a string input. + + - Added configuration option ``loader_class`` which sets the class + used to create the template loader object. + + The class (essentially a callable) is created at template + construction time. + + 2.6.1 (2011-11-30) + ------------------ + + Bugfixes: + + - Decode HTML entities in expression interpolation strings. This fixes + issue #74. + + - Allow ``xml`` and ``xmlns`` attributes on TAL, I18N and METAL + namespace elements. This fixes issue #73. + + 2.6.0 (2011-11-24) + ------------------ + + Features: + + - Added support for implicit translation: + + The ``implicit_i18n_translate`` option enables implicit translation + of text. The ``implicit_i18n_attributes`` enables implicit + translation of attributes. The latter must be a set and for an + attribute to be implicitly translated, its lowercase string value + must be included in the set. + + - Added option ``strict`` (enabled by default) which decides whether + expressions are required to be valid at compile time. That is, if + not set, an exception is only raised for an invalid expression at + evaluation time. + + - An expression error now results in an exception only if the + expression is attempted evaluated during a rendering. + + - Added a configuration option ``prepend_relative_search_path`` which + decides whether the path relative to a file-based template is + prepended to the load search path. The default is ``True``. + + - Added a configuration option ``search_path`` to the file-based + template class, which adds additional paths to the template load + instance bound to the ``load:`` expression. The option takes a + string path or an iterable yielding string paths. The default value + is the empty set. + + Bugfixes: + + - Exception instances now support pickle/unpickle. + + - An attributes in i18n:attributes no longer needs to match an + existing or dynamic attribute in order to appear in the + element. This fixes issue #66. + + 2.5.3 (2011-10-23) + ------------------ + + Bugfixes: + + - Fixed an issue where a nested macro slot definition would fail even + though there existed a parent macro definition. This fixes issue + #69. + + 2.5.2 (2011-10-12) + ------------------ + + Bugfixes: + + - Fixed an issue where technically invalid input would result in a + compiler error. + + Features: + + - The markup class now inherits from the unicode string type such that + it's compatible with the string interface. + + 2.5.1 (2011-09-29) + ------------------ + + Bugfixes: + + - The symbol names "convert", "decode" and "translate" are now no + longer set as read-only *compiler internals*. This fixes issue #65. + + - Fixed an issue where a macro extension chain nested two levels (a + template uses a macro that extends a macro) would lose the middle + slot definitions if slots were defined nested. + + The compiler now throws an error if a nested slot definition is used + outside a macro extension context. + + 2.5.0 (2011-09-23) + ------------------ + + Features: + + - An expression type ``structure:`` is now available which wraps the + expression result as *structure* such that it is not escaped on + insertion, e.g.:: + + <div id="content"> + ${structure: context.body} + </div> + + This also means that the ``structure`` keyword for ``tal:content`` + and ``tal:replace`` now has an alternative spelling via the + expression type ``structure:``. + + - The string-based template constructor now accepts encoded input. + + 2.4.6 (2011-09-23) + ------------------ + + Bugfixes: + + - The ``tal:on-error`` statement should catch all exceptions. + + - Fixed issue that would prevent escaping of interpolation expression + values appearing in text. + + 2.4.5 (2011-09-21) + ------------------ + + Bugfixes: + + - The ``tal:on-error`` handler should have a ``error`` variable + defined that has the value of the exception thrown. + + - The ``tal:on-error`` statement is a substitution statement and + should support the "text" and "structure" insertion methods. + + 2.4.4 (2011-09-15) + ------------------ + + Bugfixes: + + - An encoding specified in the XML document preamble is now read and + used to decode the template input to unicode. This fixes issue #55. + + - Encoded expression input on Python 3 is now correctly + decoded. Previously, the string representation output would be + included instead of an actually decoded string. + + - Expression result conversion steps are now correctly included in + error handling such that the exception output points to the + expression location. + + 2.4.3 (2011-09-13) + ------------------ + + Features: + + - When an encoding is provided, pass the 'ignore' flag to avoid + decoding issues with bad input. + + Bugfixes: + + - Fixed pypy compatibility issue (introduced in previous release). + + 2.4.2 (2011-09-13) + ------------------ + + Bugfixes: + + - Fixed an issue in the compiler where an internal variable (such as a + translation default value) would be cached, resulting in variable + scope corruption (see issue #49). + + 2.4.1 (2011-09-08) + ------------------ + + Bugfixes: + + - Fixed an issue where a default value for an attribute would + sometimes spill over into another attribute. + + - Fixed issue where the use of the ``default`` name in an attribute + interpolation expression would print the attribute value. This is + unexpected, because it's an expression, not a static text suitable + for output. An attribute value of ``default`` now correctly drops + the attribute. + + 2.4.0 (2011-08-22) + ------------------ + + Features: + + - Added an option ``boolean_attributes`` to evaluate and render a + provided set of attributes using a boolean logic: if the attribute + is a true value, the value will be the attribute name, otherwise the + attribute is dropped. + + In the reference implementation, the following attributes are + configured as boolean values when the template is rendered in + HTML-mode:: + + "compact", "nowrap", "ismap", "declare", "noshade", + "checked", "disabled", "readonly", "multiple", "selected", + "noresize", "defer" + + Note that in Chameleon, these attributes must be manually provided. + + Bugfixes: + + - The carriage return character (used on Windows platforms) would + incorrectly be included in Python comments. + + It is now replaced with a line break. + + This fixes issue #44. + + 2.3.8 (2011-08-19) + ------------------ + + - Fixed import error that affected Python 2.5 only. + + 2.3.7 (2011-08-19) + ------------------ + + Features: + + - Added an option ``literal_false`` that disables the default behavior + of dropping an attribute for a value of ``False`` (in addition to + ``None``). This modified behavior is the behavior exhibited in + reference implementation. + + Bugfixes: + + - Undo attribute special HTML attribute behavior (see previous + release). + + This turned out not to be a compatible behavior; rather, boolean + values should simply be coerced to a string. + + Meanwhile, the reference implementation does support an HTML mode in + which the special attribute behavior is exhibited. + + We do not currently support this mode. + + 2.3.6 (2011-08-18) + ------------------ + + Features: + + - Certain HTML attribute names now have a special behavior for a + attribute value of ``True`` (or ``default`` if no default is + defined). For these attributes, this return value will result in the + name being printed as the value:: + + <input type="input" tal:attributes="checked True" /> + + will be rendered as:: + + <input type="input" checked="checked" /> + + This behavior is compatible with the reference implementation. + + 2.3.5 (2011-08-18) + ------------------ + + Features: + + - Added support for the set operator (``{item, item, ...}``). + + Bugfixes: + + - If macro is defined on the same element as a translation name, this + no longer results in a "translation name not allowed outside + translation" error. This fixes issue #43. + + - Attribute fallback to dictionary lookup now works on multiple items + (e.g. ``d1.d2.d2``). This fixes issue #42. + + 2.3.4 (2011-08-16) + ------------------ + + Features: + + - When inserting content in either attributes or text, a value of + ``True`` (like ``False`` and ``None``) will result in no + action. + + - Use statically assigned variables for ``"attrs"`` and + ``"default"``. This change yields a performance improvement of + 15-20%. + + - The template loader class now accepts an optional argument + ``default_extension`` which accepts a filename extension which will + be appended to the filename if there's not already an extension. + + Bugfixes: + + - The default symbol is now ``True`` for an attribute if the attribute + default is not provided. Note that the result is that the attribute + is dropped. This fixes issue #41. + + - Fixed an issue where assignment to a variable ``"type"`` would + fail. This fixes issue #40. + + - Fixed an issue where an (unsuccesful) assignment for a repeat loop + to a compiler internal name would not result in an error. + + - If the translation function returns the identical object, manually + coerce it to string. This fixes a compatibility issue with + translation functions which do not convert non-string objects to a + string value, but simply return them unchanged. + + 2.3.3 (2011-08-15) + ------------------ + + Features: + + - The ``load:`` expression now passes the initial keyword arguments to + its template loader (e.g. ``auto_reload`` and ``encoding``). + + - In the exception output, string variable values are now limited to a + limited output of characters, single line only. + + Bugfixes: + + - Fixed horizontal alignment of exception location info + (i.e. 'String:', 'Filename:' and 'Location:') such that they match + the template exception formatter. + + 2.3.2 (2011-08-11) + ------------------ + + Bugfixes: + + - Fixed issue where i18n:domain would not be inherited through macros + and slots. This fixes issue #37. + + 2.3.1 (2011-08-11) + ------------------ + + Features: + + - The ``Builtin`` node type may now be used to represent any Python + local or global name. This allows expression compilers to refer to + e.g. ``get`` or ``getitem``, or to explicit require a builtin object + such as one from the ``extra_builtins`` dictionary. + + Bugfixes: + + - Builtins which are not explicitly disallowed may now be redefined + and used as variables (e.g. ``nothing``). + + - Fixed compiler issue with circular node annotation loop. + + 2.3 (2011-08-10) + ---------------- + + Features: + + - Added support for the following syntax to disable inline evaluation + in a comment: + + <!--? comment appears verbatim (no ${...} evaluation) --> + + Note that the initial question mark character (?) will be omitted + from output. + + - The parser now accepts '<' and '>' in attributes. Note that this is + invalid markup. Previously, the '<' would not be accepted as a valid + attribute value, but this would result in an 'unexpected end tag' + error elsewhere. This fixes issue #38. + + - The expression compiler now provides methods ``assign_text`` and + ``assign_value`` such that a template engine might configure this + value conversion to support e.g. encoded strings. + + Note that currently, the only client for the ``assign_text`` method + is the string expression type. + + - Enable template loader for string-based template classes. Note that + the ``filename`` keyword argument may be provided on initialization + to identify the template source by filename. This fixes issue #36. + + - Added ``extra_builtins`` option to the page template class. These + builtins are added to the default builtins dictionary at cook time + and may be provided at initialization using the ``extra_builtins`` + keyword argument. + + Bugfixes: + + - If a translation domain is set for a fill slot, use this setting + instead of the macro template domain. + + - The Python expression compiler now correctly decodes HTML entities + ``'gt'`` and ``'lt'``. This fixes issue #32. + + - The string expression compiler now correctly handles encoded text + (when support for encoded strings is enabled). This fixes issue #35. + + - Fixed an issue where setting the ``filename`` attribute on a + file-based template would not automatically cause an invalidation. + + - Exceptions raised by Chameleon can now be copied via + ``copy.copy``. This fixes issue #36. + [leorochael] + + - If copying the exception fails in the exception handler, simply + re-raise the original exception and log a warning. + + 2.2 (2011-07-28) + ---------------- + + Features: + + - Added new expression type ``load:`` that allows loading a + template. Both relative and absolute paths are supported. If the + path given is relative, then it will be resolved with respect to the + directory of the template. + + - Added support for dynamic evaluation of expressions. + + Note that this is to support legacy applications. It is not + currently wired into the provided template classes. + + - Template classes now have a ``builtins`` attribute which may be used + to define built-in variables always available in the template + variable scope. + + Incompatibilities: + + - The file-based template class no longer accepts a parameter + ``loader``. This parameter would be used to load a template from a + relative path, using a ``find(filename)`` method. This was however, + undocumented, and probably not very useful since we have the + ``TemplateLoader`` mechanism already. + + - The compiled template module now contains an ``initialize`` function + which takes values that map to the template builtins. The return + value of this function is a dictionary that contains the render + functions. + + Bugfixes: + + - The file-based template class no longer verifies the existance of a + template file (using ``os.lstat``). This now happens implicitly if + eager parsing is enabled, or otherwise when first needed (e.g. at + render time). + + This is classified as a bug fix because the previous behavior was + probably not what you'd expect, especially if an application + initializes a lot of templates without needing to render them + immediately. + + 2.1.1 (2011-07-28) + ------------------ + + Features: + + - Improved exception display. The expression string is now shown in + the context of the original source (if available) with a marker + string indicating the location of the expression in the template + source. + + Bugfixes: + + - The ``structure`` insertion mode now correctly decodes entities for + any expression type (including ``string:``). This fixes issue #30. + + - Don't show internal variables in the exception formatter variable + listing. + + 2.1 (2011-07-25) + ---------------- + + Features: + + - Expression interpolation (using the ``${...}`` operator and + previously also ``$identifier``) now requires braces everywhere + except inside the ``string:`` expression type. + + This change is motivated by a number of legacy templates in which + the interpolation format without braces ``$identifier`` appears as + text. + + 2.0.2 (2011-07-25) + ------------------ + + Bugfixes: + + - Don't use dynamic variable scope for lambda-scoped variables (#27). + + - Avoid duplication of exception class and message in traceback. + + - Fixed issue where a ``metal:fill-slot`` would be ignored if a macro + was set to be used on the same element (#16). + + 2.0.1 (2011-07-23) + ------------------ + + Bugfixes: + + - Fixed issue where global variable definition from macro slots would + fail (they would instead be local). This also affects error + reporting from inside slots because this would be recorded + internally as a global. + + - Fixed issue with template cache digest (used for filenames); modules + are now invalidated whenever any changes are made to the + distribution set available (packages on ``sys.path``). + + - Fixed exception handler to better let exceptions propagate through + the renderer. + + - The disk-based module compiler now mangles template source filenames + such that the output Python module is valid and at root level (dots + and hyphens are replaced by an underscore). This fixes issue #17. + + - Fixed translations (i18n) on Python 2.5. + + 2.0 (2011-07-14) + ---------------- + + - Point release. + + 2.0-rc14 (2011-07-13) + --------------------- + + Bugfixes: + + - The tab character (``\t``) is now parsed correctly when used inside + tags. + + Features: + + - The ``RepeatDict`` class now works as a proxy behind a seperate + dictionary instance. + + - Added template constructor option ``keep_body`` which is a flag + (also available as a class attribute) that controls whether to save + the template body input in the ``body`` attribute. + + This is disabled by default, unless debug-mode is enabled. + + - The page template loader class now accepts an optional ``formats`` + argument which can be used to select an alternative template class. + + 2.0-rc13 (2011-07-07) + --------------------- + + Bugfixes: + + - The backslash character (followed by optional whitespace and a line + break) was not correctly interpreted as a continuation for Python + expressions. + + Features: + + - The Python expression implementation is now more flexible for + external subclassing via a new ``parse`` method. + + 2.0-rc12 (2011-07-04) + --------------------- + + Bugfixes: + + - Initial keyword arguments passed to a template now no longer "leak" + into the template variable space after a macro call. + + - An unexpected end tag is now an unrecoverable error. + + Features: + + - Improve exception output. + + 2.0-rc11 (2011-05-26) + --------------------- + + Bugfixes: + + - Fixed issue where variable names that begin with an underscore were + seemingly allowed, but their use resulted in a compiler error. + + Features: + + - Template variable names are now allowed to be prefixed with a single + underscore, but not two or more (reserved for internal use). + + Examples of valid names:: + + item + ITEM + _item + camelCase + underscore_delimited + help + + - Added support for Genshi's comment "drop" syntax:: + + <!--! This comment will be dropped --> + + Note the additional exclamation (!) character. + + This fixes addresses issue #10. + + 2.0-rc10 (2011-05-24) + --------------------- + + Bugfixes: + + - The ``tal:attributes`` statement now correctly operates + case-insensitive. The attribute name given in the statement will + replace an existing attribute with the same name, without respect to + case. + + Features: + + - Added ``meta:interpolation`` statement to control expression + interpolation setting. + + Strings that disable the setting: ``"off"`` and ``"false"``. + Strings that enable the setting: ``"on"`` and ``"true"``. + + - Expression interpolation now works inside XML comments. + + 2.0-rc9 (2011-05-05) + -------------------- + + Features: + + - Better debugging support for string decode and conversion. If a + naive join fails, each element in the output will now be attempted + coerced to unicode to try and trigger the failure near to the bad + string. + + 2.0-rc8 (2011-04-11) + -------------------- + + Bugfixes: + + - If a macro defines two slots with the same name, a caller will now + fill both with a single usage. + + - If a valid of ``None`` is provided as the translation function + argument, we now fall back to the class default. + + 2.0-rc7 (2011-03-29) + -------------------- + + Bugfixes: + + - Fixed issue with Python 2.5 compatibility AST. This affected at + least PyPy 1.4. + + Features: + + - The ``auto_reload`` setting now defaults to the class value; the + base template class gives a default value of + ``chameleon.config.AUTO_RELOAD``. This change allows a subclass to + provide a custom default value (such as an application-specific + debug mode setting). + + + 2.0-rc6 (2011-03-19) + -------------------- + + Features: + + - Added support for ``target_language`` keyword argument to render + method. If provided, the argument will be curried onto the + translation function. + + Bugfixes: + + - The HTML entities 'lt', 'gt' and 'quot' appearing inside content + subtition expressions are now translated into their native character + values. This fixes an issue where you could not dynamically create + elements using the ``structure`` (which is possible in ZPT). The + need to create such structure stems from the lack of an expression + interpolation operator in ZPT. + + - Fixed duplicate file pointer issue with test suite (affected Windows + platforms only). This fixes issue #9. + [oliora] + + - Use already open file using ``os.fdopen`` when trying to write out + the module source. This fixes LP #731803. + + + 2.0-rc5 (2011-03-07) + -------------------- + + Bugfixes: + + - Fixed a number of issues concerning the escaping of attribute + values: + + 1) Static attribute values are now included as they appear in the + source. + + This means that invalid attribute values such as ``"true && + false"`` are now left alone. It's not the job of the template + engine to correct such markup, at least not in the default mode + of operation. + + 2) The string expression compiler no longer unescapes + values. Instead, this is left to each expression + compiler. Currently only the Python expression compiler unescapes + its input. + + 3) The dynamic escape code sequence now correctly only replaces + ampersands that are part of an HTML escape format. + + Imports: + + - The page template classes and the loader class can now be imported + directly from the ``chameleon`` module. + + Features: + + - If a custom template loader is not provided, relative paths are now + resolved using ``os.abspath`` (i.e. to the current working + directory). + + - Absolute paths are normalized using ``os.path.normpath`` and + ``os.path.expanduser``. This ensures that all paths are kept in + their "canonical" form. + + + 2.0-rc4 (2011-03-03) + -------------------- + + Bugfixes: + + - Fixed an issue where the output of an end-to-end string expression + would raise an exception if the expression evaluated to ``None`` (it + should simply output nothing). + + - The ``convert`` function (which is configurable on the template + class level) now defaults to the ``translate`` function (at + run-time). + + This fixes an issue where message objects were not translated (and + thus converted to a string) using the a provided ``translate`` + function. + + - Fixed string interpolation issue where an expression immediately + succeeded by a right curly bracket would not parse. + + This fixes issue #5. + + - Fixed error where ``tal:condition`` would be evaluated after + ``tal:repeat``. + + Features: + + - Python expression is now a TALES expression. That means that the + pipe operator can be used to chain two or more expressions in a + try-except sequence. + + This behavior was ported from the 1.x series. Note that while it's + still possible to use the pipe character ("|") in an expression, it + must now be escaped. + + - The template cache can now be shared by multiple processes. + + + 2.0-rc3 (2011-03-02) + -------------------- + + Bugfixes: + + - Fixed ``atexit`` handler. + + This fixes issue #3. + + - If a cache directory is specified, it will now be used even when not + in debug mode. + + - Allow "comment" attribute in the TAL namespace. + + This fixes an issue in the sense that the reference engine allows + any attribute within the TAL namespace. However, only "comment" is + in common use. + + - The template constructor now accepts a flag ``debug`` which puts the + template *instance* into debug-mode regardless of the global + setting. + + This fixes issue #1. + + Features: + + - Added exception handler for exceptions raised while evaluating an + expression. + + This handler raises (or attempts to) a new exception of the type + ``RenderError``, with an additional base class of the original + exception class. The string value of the exception is a formatted + error message which includes the expression that caused the + exception. + + If we are unable to create the exception class, the original + exception is re-raised. + + 2.0-rc2 (2011-02-28) + -------------------- + + - Fixed upload issue. + + 2.0-rc1 (2011-02-28) + -------------------- + + - Initial public release. See documentation for what's new in this + series. + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..23fde21 --- /dev/null +++ b/README.rst @@ -0,0 +1,25 @@ +Overview +======== + +Chameleon is an HTML/XML template engine for `Python +<http://www.python.org>`_. It uses the *page templates* language. + +You can use it in any Python web application with just about any +version of Python (2.7 and up, including 3.4+ and `pypy +<http://pypy.org>`_). + +Visit the `documentation <https://chameleon.readthedocs.io/en/latest/>`_ +for more information. + +License and Copyright +--------------------- + +This software is made available as-is under a BSD-like license [1]_ +(see included copyright notice). + + +Notes +----- + +.. [1] This software is licensed under the `Repoze + <http://repoze.org/license.html>`_ license. diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8bfd5a1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..65ec358 --- /dev/null +++ b/setup.py @@ -0,0 +1,71 @@ +__version__ = '3.6.2' + +import os + +from setuptools import setup, find_packages +from setuptools.command.test import test + +here = os.path.abspath(os.path.dirname(__file__)) +try: + README = open(os.path.join(here, 'README.rst')).read() + CHANGES = open(os.path.join(here, 'CHANGES.rst')).read() +except: # doesn't work under tox/pip + README = '' + CHANGES = '' + +install_requires = [] + + +class Benchmark(test): + description = "Run benchmarks" + + def finalize_options(self): + self.distribution.tests_require = [ + 'zope.pagetemplate', + 'zope.component', + 'zope.i18n', + 'zope.testing'] + + test.finalize_options(self) + + def run_tests(self): + from chameleon import benchmark + print("running benchmark...") + + benchmark.start() + +setup( + name="Chameleon", + version=__version__, + description="Fast HTML/XML Template Compiler.", + long_description="\n\n".join((README, CHANGES)), + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + ], + author="Malthe Borch", + author_email="mborch@gmail.com", + url="https://chameleon.readthedocs.io", + license='BSD-like (http://repoze.org/license.html)', + packages=find_packages('src'), + package_dir = {'': 'src'}, + include_package_data=True, + install_requires=install_requires, + zip_safe=False, + test_suite="chameleon.tests", + cmdclass={ + 'benchmark': Benchmark, + } + ) + diff --git a/src/Chameleon.egg-info/PKG-INFO b/src/Chameleon.egg-info/PKG-INFO new file mode 100644 index 0000000..f543054 --- /dev/null +++ b/src/Chameleon.egg-info/PKG-INFO @@ -0,0 +1,1539 @@ +Metadata-Version: 1.1 +Name: Chameleon +Version: 3.6.2 +Summary: Fast HTML/XML Template Compiler. +Home-page: https://chameleon.readthedocs.io +Author: Malthe Borch +Author-email: mborch@gmail.com +License: BSD-like (http://repoze.org/license.html) +Description: Overview + ======== + + Chameleon is an HTML/XML template engine for `Python + <http://www.python.org>`_. It uses the *page templates* language. + + You can use it in any Python web application with just about any + version of Python (2.7 and up, including 3.4+ and `pypy + <http://pypy.org>`_). + + Visit the `documentation <https://chameleon.readthedocs.io/en/latest/>`_ + for more information. + + License and Copyright + --------------------- + + This software is made available as-is under a BSD-like license [1]_ + (see included copyright notice). + + + Notes + ----- + + .. [1] This software is licensed under the `Repoze + <http://repoze.org/license.html>`_ license. + + + Changes + ======= + + 3.6.2 (2019-06-22) + ------------------ + + - Fix SyntaxWarnings in Python 3.8 resulting from comparing literals with 'is'. + See https://github.com/plone/Products.CMFPlone/issues/2890. + + 3.6.1 (2019-04-01) + ------------------ + + - Fix limited search expression for illegal double hyphens in HTML + comments to fix issue #289. + + 3.6 (2019-02-19) + ---------------- + + - Exclude `RuntimeError` (or `RecursionError` when available) from + exception wrapping. + + - Fix double dollar '$$' escaping such that a double dollar is always + resolved, either as an interpolation expression, or as an escape + where it is substituted by a single dollar symbol. This is now + consistent with Zope's handling of this character. + + Backslash-escaping of dollar-based string interpolation is no longer + supported. The documentation has been updated to reflect this + change. + + This fixes issue #283. Note that this reverses some of the changes + introduced to fix issue #265. + + - Drop support for Python 3.3. + + 3.5 (2018-10-17) + ---------------- + + - Add support for Python 3.8. + + - Add support for TAL attributes in an XML declaration tag. This fixes + issue #269. + + - Add support for custom exception handling for the `tal:on-error` + statement. There is now an option `on_error_handler` available + as a template configuration (issue #266). + + - Fix issue where double '$$' escaping would affect non-interpolation + expressions such as the bare '$$' (issue #265). + + - Fix an issue where backslash dollar escaping would leave the + backslash character still in place. + + 3.4 (2018-07-14) + ---------------- + + Bugfixes: + + - Fix regression with translations in case of multiple nodes. + + + 3.3 (2018-05-23) + ---------------- + + Bugfixes: + + - Reset error token when rendering internal macro calls. + + - Fix edge case in exception handler causing recursion. + [MatthewWilkes] + + + 3.2 (2017-10-06) + ---------------- + + Features: + + - Add the automatic variable ``macroname`` that's bound to the name of + the executing macro. Fixes https://github.com/malthe/chameleon/issues/238 + + - A tokenizer can now be configured on the template class. This is + useful in the case where the template file input is modified before + parsing (for example, where some tags are stripped away) such that + token positions need to be offset accordingly for error locations to + be rendered correctly. + + - Expression errors now display source marker (previously only + filename, line and column was shown). + + - No longer require Python source files to import modules. + [mrh1997] + + Optimizations: + + - Exception tracking now defers metadata allocation to time of error. + + + 3.1 (2017-02-21) + ---------------- + + Features: + + - Add option ``restricted_namespace`` which controls whether to + restrict namespaces to those defined and used by the page template + language. + [hansroh] + + Bugs: + + - Fixed attribute HTML entity escaping issue where an entity such as + ``&`` would be encoded twice. + + Optimizations: + + - Simplify exception tracking, reducing bytecode size significantly. + + - Avoid checking if a static string is ``None`` during expression + interpolation. + + + 3.0 (2016-12-07) + ---------------- + + Bugs: + + - Fix issue on Python 2 where an exception was not cleared when using + the pipe operator and was thus accessible through `sys.exc_info()`. + + - The "exists" expression no longer leaks error information. + + - Escape '$$' into '$' in both content and string expressions. + + - Fix use of macro definition inside translation block. + + Improvements: + + - Allow unquoted attribute values. + + - Wrap attribute error thrown when trying to use a non-macro as a + macro as a `RenderError` to get proper error output. + + - Throw a parse error if '--' (double hyphen) appears in an XML + comment. + + - The `i18n:target` attribute now overrides a default + `target_language` variable and is passed to the translation + function. + + - Include filename in the on-disk cache module name. Previously, + only the SHA digest in hex representation would be used, making + it difficult to see where the module came from. This fixes issue + #132. + + - Add support for non-ascii attribute names. + [sank] + + Compatibility: + + - Drop support for Python 2.6, 3.1, and 3.2. + + + 2.25 (2016-09-24) + ----------------- + + - Add explicit support / testing for Python 3.5. + + - Add ``\r`` to negative regex matches to the chameleon parser, where ``\n`` is used but ``\r`` was missing. + Fixes a case, where the tag name was parsed into ``html\r`` instead of ``html``. + Fixes: https://github.com/malthe/chameleon/issues/219 + + + 2.24 (2015-10-28) + ----------------- + + - Fixed Python 3.5 compatibility. + + - Fixed brown bag release. + + + 2.23 (2015-10-26) + ----------------- + + - Added ``enable_data_attributes`` option that allows using HTML5 data + attributes as control attributes instead or in addition to XML + namespace attributes. + + + 2.22 (2015-02-06) + ----------------- + + - Fix brown bag release. + + + 2.21 (2015-02-06) + ----------------- + + - Added ``RenderError`` exception which indicates that an error + occurred during the evaluation of an expression. + + - Clean up ``TemplateError`` exception implementation. + + + 2.20 (2015-01-12) + ----------------- + + - Pass ``search_path`` to template class when loaded using + ``TemplateLoader`` (or one of the derived classes). + [faassen] + + + 2.19 (2015-01-06) + ----------------- + + - Fix logging deprecation. + + - Fix environment-based configuration logging error. + + + 2.18 (2014-11-03) + ----------------- + + - Fix minor compilation error. + + + 2.17 (2014-11-03) + ----------------- + + - Add support for ``i18n:context``. + [wiggy] + + - Add missing 'parity' repeat property. + [voxspox] + + - Don't modify environment when getting variables from it. + [fschulze] + + + 2.16 (2014-05-06) + ----------------- + + - If a repeat expression evaluates to ``None`` then it is now + equivalent to an empty set. + + This changes a behavior introduced in 2.14. + + This fixes issue #172. + + - Remove fossil test dependency on deprecated ``distribute``. + + - Add explicit support / testing for Python 3.3 / 3.4. + + - Drop explicit support for Python 2.5 (out of maintenance, and no longer + supported by ``tox`` or ``Travis-CI``). + + + 2.15 (2014-03-11) + ----------------- + + - Add Support for Python 3.4's ``NameConstant``. + [brakhane] + + + 2.14 (2013-11-28) + ----------------- + + - Element repetition using the ``TAL`` namespace no longer includes + whitespace. This fixes issue #110. + + - Use absolute import for ``chameleon.interfaces`` module. This fixes + issue #161. + + + 2.13-1 (2013-10-24) + ------------------- + + - Fixing brown bag release. + + 2.13 (2013-10-21) + ----------------- + + Bugfixes: + + - The template cache mechanism now includes additional configuration + settings as part of the cache key such as ``strict`` and + ``trim_attribute_space``. + [ossmkitty] + + - Fix cache issue where sometimes cached templates would not load + correctly. + [ossmkitty] + + - In debug-mode, correctly remove temporary files when the module + loader is garbage-collected (on ``__del__``). + [graffic] + + - Fix error message when duplicate i18n:name directives are used in a + translation. + + - Using the three-argument form of ``getattr`` on a + ``chameleon.tal.RepeatDict`` no longer raises ``KeyError``, + letting the default provided to ``getattr`` be used. This fixes + attempting to adapt a ``RepeatDict`` to a Zope interface under + PyPy. + + 2.12 (2013-03-26) + ----------------- + + Changes: + + - When a ``tal:case`` condition succeeds, no other case now will. + + Bugfixes: + + - Implicit translation now correctly extracts and normalizes complete + sentences, instead of words. + [witsch] + + - The ``default`` symbol in a ``tal:case`` condition now allows the + element only if no other case succeeds. + + + 2.11 (2012-11-15) + ----------------- + + Bugfixes: + + - An issue was resolved where a METAL statement was combined with a + ``tal:on-error`` handler. + + - Fix minor parser issue with incorrectly formatted processing + instructions. + + - Provide proper error handling for Python inline code blocks. + + Features: + + - The simple translation function now supports the + ``translationstring`` interface. + + Optimizations: + + - Minor optimization which correctly detects when an element has no + attributes. + + + 2.10 (2012-10-12) + ----------------- + + Deprecations: + + - The ``fast_translate`` function has been deprecated. Instead, the + default translation function is now always a function that simply + interpolates the mapping onto the message default or id. + + The motivation is that since version 2.9, the ``context`` argument + is non-trivial: the ``econtext`` mapping is passed. This breaks an + expectation on the Zope platform that the ``context`` parameter is + the HTTP request. Previously, with Chameleon this parameter was + simply not provided and so that did not cause issues as such. + + - The ``ast24`` module has been renamed to ``ast25``. This should help + clear up any confusion that Chameleon 2.x might be support a Python + interpreter less than version 2.5 (it does not). + + Features: + + - The ``ProxyExpr`` expression class (and hence the ``load:`` + expression type) is now a TALES-expression. In practical terms, this + means that the expression type (which computes a string result using + the standard ``"${...}"`` interpolation syntax and proxies the + result through a function) now supports fallback using the pipe + operator (``"|"``). This fixes issue #128. + + - An attempt to interpolate using the empty string as the expression + (i.e. ``${}``) now does nothing: the string ``${}`` is simply output + as is. + + - Added support for adding, modifying, and removing attributes using a + dictionary expression in ``tal:attributes`` (analogous to Genshi's + ``py:attrs`` directive):: + + <div tal:attributes="name value; attrs" /> + + In the example above, ``name`` is an identifier, while ``value`` and + ``attrs`` are Python expressions. However, ``attrs`` must evaluate + to a Python dictionary object (more concisely, the value must + implement the dictionary API-methods ``update()`` and ``items()``). + + Optimizations: + + - In order to cut down on the size of the compiled function objects, + some conversion and quoting statements have been put into + functions. In one measurement, the reduction was 35%. The benchmark + suite does *not* report of an increased render time (actually + slightly decreased). + + Bugfixes: + + - An exception is now raised if a trivial string is passed for + ``metal:fill-slot``. This fixes issue #89. + + - An empty string is now never translated. Not really a bug, but it's + been reported in as an issue (#92) because some translation + frameworks handle this case incorrectly. + + - The template module loader (file cache) now correctly encodes + generated template source code as UTF-8. This fixes issue #125. + + - Fixed issue where a closure might be reused unsafely in nested + template rendering. + + - Fixed markup class ``__repr__`` method. This fixes issue #124. + + - Added missing return statement to fix printing the non-abbreviated + filename in case of an exception. + [tomo] + + 2.9.2 (2012-06-06) + ------------------ + + Bugfixes: + + - Fixed a PyPy incompatibility. + + - Fixed issue #109 which caused testing failures on some platforms. + + 2.9.1 (2012-06-01) + ------------------ + + Bugfixes: + + - Fixed issue #103. The ``tal:on-error`` statement now always adds an + explicit end-tag to the element, even with a substitution content of + nothing. + + - Fixed issue #113. The ``tal:on-error`` statement now works correctly + also for dynamic attributes. That is, the fallback tag now includes + only static attributes. + + - Fixed name error which prevented the benchmark from running + correctly. + + Compatibility: + + - Fixed deprecation warning on Python 3 for zope interface implements + declaration. This fixes issue #116. + + 2.9.0 (2012-05-31) + ------------------ + + Features: + + - The translation function now gets the ``econtext`` argument as the + value for ``context``. Note that historically, this was usually an + HTTP request which might provide language negotiation data through a + dictionary interface. + [alvinyue] + + Bugfixes: + + - Fixed import alias issue which would lead to a syntax error in + generated Python code. Fixes issue #114. + + 2.8.5 (2012-05-02) + ------------------ + + Bugfixes: + + - Fixed minor installation issues on Python 2.5 and 3. + [ppaez] + + - Ensure output is unicode even when trivial (an empty string). + + 2.8.4 (2012-04-18) + ------------------ + + Features: + + - In exception output, long filenames are now truncated to 60 + characters of output, preventing line wrap which makes it difficult + to scan the exception output. + + Bugfixes: + + - Include filename and location in exception output for exceptions + raised during compilation. + + - If a trivial translation substitution variable is given (i.e. an + empty string), simply ignore it. This fixes issue #106. + + 2.8.3 (2012-04-16) + ------------------ + + Features: + + - Log template source on debug-level before cooking. + + - The `target_language` argument, if given, is now available as a + variable in templates. + + 2.8.2 (2012-03-30) + ------------------ + + Features: + + - Temporary caches used in debug mode are cleaned up eagerly, rather + than waiting for process termination. + [mitchellrj] + + Bugfixes: + + - The `index`, `start` and `end` methods on the TAL repeat object are + now callable. This fixes an incompatibility with ZPT. + + - The loader now correctly handles absolute paths on Windows. + [rdale] + + 2.8.1 (2012-03-29) + ------------------ + + Features: + + - The exception formatter now lists errors in 'wrapping order'. This + means that the innermost, and presumably most relevant exception is + shown last. + + Bugfixes: + + - The exception formatter now correctly recognizes nested errors and + does not rewrap the dynamically generated exception class. + + - The exception formatter now correctly sets the ``__module__`` + attribute to that of the original exception class. + + 2.8.0 (2012-02-29) + ------------------ + + Features: + + - Added support for code blocks using the `<?python ... ?>` processing + instruction syntax. + + The scope is name assignments is up until the nearest macro + definition, or the template itself if macros are not used. + + Bugfixes: + + - Fall back to the exception class' ``__new__`` method to safely + create an exception object that is not implemented in Python. + + - The exception formatter now keeps track of already formatted + exceptions, and ignores them from further output. + + 2.7.4 (2012-02-27) + ------------------ + + - The error handler now invokes the ``__init__`` method of + ``BaseException`` instead of the possibly overriden method (which + may take required arguments). This fixes issue #97. + [j23d, malthe] + + 2.7.3 (2012-01-16) + ------------------ + + Bugfixes: + + - The trim whitespace option now correctly trims actual whitespace to + a single character, appearing either to the left or to the right of + an element prefix or suffix string. + + 2.7.2 (2012-01-08) + ------------------ + + Features: + + - Added option ``trim_attribute_space`` that decides whether attribute + whitespace is stripped (at most down to a single space). This option + exists to provide compatibility with the reference + implementation. Fixes issue #85. + + Bugfixes: + + - Ignore unhashable builtins when generating a reverse builtin + map to quickly look up a builtin value. + [malthe] + + - Apply translation mapping even when a translation function is not + available. This fixes issue #83. + [malthe] + + - Fixed issue #80. The translation domain for a slot is defined by the + source document, i.e. the template providing the content for a slot + whether it be the default or provided through ``metal:fill-slot``. + [jcbrand] + + - In certain circumstances, a Unicode non-breaking space character would cause + a define clause to fail to parse. + + 2.7.1 (2011-12-29) + ------------------ + + Features: + + - Enable expression interpolation in CDATA. + + - The page template class now implements dictionary access to macros:: + + template[name] + + This is a short-hand for:: + + template.macros[name] + + Bugfixes: + + - An invalid define clause would be silently ignored; we now raise a + language error exception. This fixes issue #79. + + - Fixed regression where ``${...}`` interpolation expressions could + not span multiple lines. This fixes issue #77. + + 2.7.0 (2011-12-13) + ------------------ + + Features: + + - The ``load:`` expression now derives from the string expression such + that the ``${...}`` operator can be used for expression + interpolation. + + - The ``load:`` expression now accepts asset specs; these are resolved + by the ``pkg_resources.resource_filename`` function:: + + <package_name>:<path> + + An example from the test suite:: + + chameleon:tests/inputs/hello_world.pt + + Bugfixes: + + - If an attribute name for translation was not a valid Python + identifier, the compiler would generate invalid code. This has been + fixed, and the compiler now also throws an exception if an attribute + specification contains a comma. (Note that the only valid separator + character is the semicolon, when specifying attributes for + translation via the ``i18n:translate`` statement). This addresses + issue #76. + + 2.6.2 (2011-12-08) + ------------------ + + Bugfixes: + + - Fixed issue where ``tal:on-error`` would not respect + ``tal:omit-tag`` or namespace elements which are omitted by default + (such as ``<tal:block />``). + + - Fixed issue where ``macros`` attribute would not be available on + file-based templates due to incorrect initialization. + + - The ``TryExcept`` and ``TryFinally`` AST nodes are not available on + Python 3.3. These have been aliased to ``Try``. This fixes issue + #75. + + Features: + + - The TAL repeat item now makes a security declaration that grants + access to unprotected subobjects on the Zope 2 platform:: + + __allow_access_to_unprotected_subobjects__ = True + + This is required for legacy compatibility and does not affect other + environments. + + - The template object now has a method ``write(body)`` which + explicitly decodes and cooks a string input. + + - Added configuration option ``loader_class`` which sets the class + used to create the template loader object. + + The class (essentially a callable) is created at template + construction time. + + 2.6.1 (2011-11-30) + ------------------ + + Bugfixes: + + - Decode HTML entities in expression interpolation strings. This fixes + issue #74. + + - Allow ``xml`` and ``xmlns`` attributes on TAL, I18N and METAL + namespace elements. This fixes issue #73. + + 2.6.0 (2011-11-24) + ------------------ + + Features: + + - Added support for implicit translation: + + The ``implicit_i18n_translate`` option enables implicit translation + of text. The ``implicit_i18n_attributes`` enables implicit + translation of attributes. The latter must be a set and for an + attribute to be implicitly translated, its lowercase string value + must be included in the set. + + - Added option ``strict`` (enabled by default) which decides whether + expressions are required to be valid at compile time. That is, if + not set, an exception is only raised for an invalid expression at + evaluation time. + + - An expression error now results in an exception only if the + expression is attempted evaluated during a rendering. + + - Added a configuration option ``prepend_relative_search_path`` which + decides whether the path relative to a file-based template is + prepended to the load search path. The default is ``True``. + + - Added a configuration option ``search_path`` to the file-based + template class, which adds additional paths to the template load + instance bound to the ``load:`` expression. The option takes a + string path or an iterable yielding string paths. The default value + is the empty set. + + Bugfixes: + + - Exception instances now support pickle/unpickle. + + - An attributes in i18n:attributes no longer needs to match an + existing or dynamic attribute in order to appear in the + element. This fixes issue #66. + + 2.5.3 (2011-10-23) + ------------------ + + Bugfixes: + + - Fixed an issue where a nested macro slot definition would fail even + though there existed a parent macro definition. This fixes issue + #69. + + 2.5.2 (2011-10-12) + ------------------ + + Bugfixes: + + - Fixed an issue where technically invalid input would result in a + compiler error. + + Features: + + - The markup class now inherits from the unicode string type such that + it's compatible with the string interface. + + 2.5.1 (2011-09-29) + ------------------ + + Bugfixes: + + - The symbol names "convert", "decode" and "translate" are now no + longer set as read-only *compiler internals*. This fixes issue #65. + + - Fixed an issue where a macro extension chain nested two levels (a + template uses a macro that extends a macro) would lose the middle + slot definitions if slots were defined nested. + + The compiler now throws an error if a nested slot definition is used + outside a macro extension context. + + 2.5.0 (2011-09-23) + ------------------ + + Features: + + - An expression type ``structure:`` is now available which wraps the + expression result as *structure* such that it is not escaped on + insertion, e.g.:: + + <div id="content"> + ${structure: context.body} + </div> + + This also means that the ``structure`` keyword for ``tal:content`` + and ``tal:replace`` now has an alternative spelling via the + expression type ``structure:``. + + - The string-based template constructor now accepts encoded input. + + 2.4.6 (2011-09-23) + ------------------ + + Bugfixes: + + - The ``tal:on-error`` statement should catch all exceptions. + + - Fixed issue that would prevent escaping of interpolation expression + values appearing in text. + + 2.4.5 (2011-09-21) + ------------------ + + Bugfixes: + + - The ``tal:on-error`` handler should have a ``error`` variable + defined that has the value of the exception thrown. + + - The ``tal:on-error`` statement is a substitution statement and + should support the "text" and "structure" insertion methods. + + 2.4.4 (2011-09-15) + ------------------ + + Bugfixes: + + - An encoding specified in the XML document preamble is now read and + used to decode the template input to unicode. This fixes issue #55. + + - Encoded expression input on Python 3 is now correctly + decoded. Previously, the string representation output would be + included instead of an actually decoded string. + + - Expression result conversion steps are now correctly included in + error handling such that the exception output points to the + expression location. + + 2.4.3 (2011-09-13) + ------------------ + + Features: + + - When an encoding is provided, pass the 'ignore' flag to avoid + decoding issues with bad input. + + Bugfixes: + + - Fixed pypy compatibility issue (introduced in previous release). + + 2.4.2 (2011-09-13) + ------------------ + + Bugfixes: + + - Fixed an issue in the compiler where an internal variable (such as a + translation default value) would be cached, resulting in variable + scope corruption (see issue #49). + + 2.4.1 (2011-09-08) + ------------------ + + Bugfixes: + + - Fixed an issue where a default value for an attribute would + sometimes spill over into another attribute. + + - Fixed issue where the use of the ``default`` name in an attribute + interpolation expression would print the attribute value. This is + unexpected, because it's an expression, not a static text suitable + for output. An attribute value of ``default`` now correctly drops + the attribute. + + 2.4.0 (2011-08-22) + ------------------ + + Features: + + - Added an option ``boolean_attributes`` to evaluate and render a + provided set of attributes using a boolean logic: if the attribute + is a true value, the value will be the attribute name, otherwise the + attribute is dropped. + + In the reference implementation, the following attributes are + configured as boolean values when the template is rendered in + HTML-mode:: + + "compact", "nowrap", "ismap", "declare", "noshade", + "checked", "disabled", "readonly", "multiple", "selected", + "noresize", "defer" + + Note that in Chameleon, these attributes must be manually provided. + + Bugfixes: + + - The carriage return character (used on Windows platforms) would + incorrectly be included in Python comments. + + It is now replaced with a line break. + + This fixes issue #44. + + 2.3.8 (2011-08-19) + ------------------ + + - Fixed import error that affected Python 2.5 only. + + 2.3.7 (2011-08-19) + ------------------ + + Features: + + - Added an option ``literal_false`` that disables the default behavior + of dropping an attribute for a value of ``False`` (in addition to + ``None``). This modified behavior is the behavior exhibited in + reference implementation. + + Bugfixes: + + - Undo attribute special HTML attribute behavior (see previous + release). + + This turned out not to be a compatible behavior; rather, boolean + values should simply be coerced to a string. + + Meanwhile, the reference implementation does support an HTML mode in + which the special attribute behavior is exhibited. + + We do not currently support this mode. + + 2.3.6 (2011-08-18) + ------------------ + + Features: + + - Certain HTML attribute names now have a special behavior for a + attribute value of ``True`` (or ``default`` if no default is + defined). For these attributes, this return value will result in the + name being printed as the value:: + + <input type="input" tal:attributes="checked True" /> + + will be rendered as:: + + <input type="input" checked="checked" /> + + This behavior is compatible with the reference implementation. + + 2.3.5 (2011-08-18) + ------------------ + + Features: + + - Added support for the set operator (``{item, item, ...}``). + + Bugfixes: + + - If macro is defined on the same element as a translation name, this + no longer results in a "translation name not allowed outside + translation" error. This fixes issue #43. + + - Attribute fallback to dictionary lookup now works on multiple items + (e.g. ``d1.d2.d2``). This fixes issue #42. + + 2.3.4 (2011-08-16) + ------------------ + + Features: + + - When inserting content in either attributes or text, a value of + ``True`` (like ``False`` and ``None``) will result in no + action. + + - Use statically assigned variables for ``"attrs"`` and + ``"default"``. This change yields a performance improvement of + 15-20%. + + - The template loader class now accepts an optional argument + ``default_extension`` which accepts a filename extension which will + be appended to the filename if there's not already an extension. + + Bugfixes: + + - The default symbol is now ``True`` for an attribute if the attribute + default is not provided. Note that the result is that the attribute + is dropped. This fixes issue #41. + + - Fixed an issue where assignment to a variable ``"type"`` would + fail. This fixes issue #40. + + - Fixed an issue where an (unsuccesful) assignment for a repeat loop + to a compiler internal name would not result in an error. + + - If the translation function returns the identical object, manually + coerce it to string. This fixes a compatibility issue with + translation functions which do not convert non-string objects to a + string value, but simply return them unchanged. + + 2.3.3 (2011-08-15) + ------------------ + + Features: + + - The ``load:`` expression now passes the initial keyword arguments to + its template loader (e.g. ``auto_reload`` and ``encoding``). + + - In the exception output, string variable values are now limited to a + limited output of characters, single line only. + + Bugfixes: + + - Fixed horizontal alignment of exception location info + (i.e. 'String:', 'Filename:' and 'Location:') such that they match + the template exception formatter. + + 2.3.2 (2011-08-11) + ------------------ + + Bugfixes: + + - Fixed issue where i18n:domain would not be inherited through macros + and slots. This fixes issue #37. + + 2.3.1 (2011-08-11) + ------------------ + + Features: + + - The ``Builtin`` node type may now be used to represent any Python + local or global name. This allows expression compilers to refer to + e.g. ``get`` or ``getitem``, or to explicit require a builtin object + such as one from the ``extra_builtins`` dictionary. + + Bugfixes: + + - Builtins which are not explicitly disallowed may now be redefined + and used as variables (e.g. ``nothing``). + + - Fixed compiler issue with circular node annotation loop. + + 2.3 (2011-08-10) + ---------------- + + Features: + + - Added support for the following syntax to disable inline evaluation + in a comment: + + <!--? comment appears verbatim (no ${...} evaluation) --> + + Note that the initial question mark character (?) will be omitted + from output. + + - The parser now accepts '<' and '>' in attributes. Note that this is + invalid markup. Previously, the '<' would not be accepted as a valid + attribute value, but this would result in an 'unexpected end tag' + error elsewhere. This fixes issue #38. + + - The expression compiler now provides methods ``assign_text`` and + ``assign_value`` such that a template engine might configure this + value conversion to support e.g. encoded strings. + + Note that currently, the only client for the ``assign_text`` method + is the string expression type. + + - Enable template loader for string-based template classes. Note that + the ``filename`` keyword argument may be provided on initialization + to identify the template source by filename. This fixes issue #36. + + - Added ``extra_builtins`` option to the page template class. These + builtins are added to the default builtins dictionary at cook time + and may be provided at initialization using the ``extra_builtins`` + keyword argument. + + Bugfixes: + + - If a translation domain is set for a fill slot, use this setting + instead of the macro template domain. + + - The Python expression compiler now correctly decodes HTML entities + ``'gt'`` and ``'lt'``. This fixes issue #32. + + - The string expression compiler now correctly handles encoded text + (when support for encoded strings is enabled). This fixes issue #35. + + - Fixed an issue where setting the ``filename`` attribute on a + file-based template would not automatically cause an invalidation. + + - Exceptions raised by Chameleon can now be copied via + ``copy.copy``. This fixes issue #36. + [leorochael] + + - If copying the exception fails in the exception handler, simply + re-raise the original exception and log a warning. + + 2.2 (2011-07-28) + ---------------- + + Features: + + - Added new expression type ``load:`` that allows loading a + template. Both relative and absolute paths are supported. If the + path given is relative, then it will be resolved with respect to the + directory of the template. + + - Added support for dynamic evaluation of expressions. + + Note that this is to support legacy applications. It is not + currently wired into the provided template classes. + + - Template classes now have a ``builtins`` attribute which may be used + to define built-in variables always available in the template + variable scope. + + Incompatibilities: + + - The file-based template class no longer accepts a parameter + ``loader``. This parameter would be used to load a template from a + relative path, using a ``find(filename)`` method. This was however, + undocumented, and probably not very useful since we have the + ``TemplateLoader`` mechanism already. + + - The compiled template module now contains an ``initialize`` function + which takes values that map to the template builtins. The return + value of this function is a dictionary that contains the render + functions. + + Bugfixes: + + - The file-based template class no longer verifies the existance of a + template file (using ``os.lstat``). This now happens implicitly if + eager parsing is enabled, or otherwise when first needed (e.g. at + render time). + + This is classified as a bug fix because the previous behavior was + probably not what you'd expect, especially if an application + initializes a lot of templates without needing to render them + immediately. + + 2.1.1 (2011-07-28) + ------------------ + + Features: + + - Improved exception display. The expression string is now shown in + the context of the original source (if available) with a marker + string indicating the location of the expression in the template + source. + + Bugfixes: + + - The ``structure`` insertion mode now correctly decodes entities for + any expression type (including ``string:``). This fixes issue #30. + + - Don't show internal variables in the exception formatter variable + listing. + + 2.1 (2011-07-25) + ---------------- + + Features: + + - Expression interpolation (using the ``${...}`` operator and + previously also ``$identifier``) now requires braces everywhere + except inside the ``string:`` expression type. + + This change is motivated by a number of legacy templates in which + the interpolation format without braces ``$identifier`` appears as + text. + + 2.0.2 (2011-07-25) + ------------------ + + Bugfixes: + + - Don't use dynamic variable scope for lambda-scoped variables (#27). + + - Avoid duplication of exception class and message in traceback. + + - Fixed issue where a ``metal:fill-slot`` would be ignored if a macro + was set to be used on the same element (#16). + + 2.0.1 (2011-07-23) + ------------------ + + Bugfixes: + + - Fixed issue where global variable definition from macro slots would + fail (they would instead be local). This also affects error + reporting from inside slots because this would be recorded + internally as a global. + + - Fixed issue with template cache digest (used for filenames); modules + are now invalidated whenever any changes are made to the + distribution set available (packages on ``sys.path``). + + - Fixed exception handler to better let exceptions propagate through + the renderer. + + - The disk-based module compiler now mangles template source filenames + such that the output Python module is valid and at root level (dots + and hyphens are replaced by an underscore). This fixes issue #17. + + - Fixed translations (i18n) on Python 2.5. + + 2.0 (2011-07-14) + ---------------- + + - Point release. + + 2.0-rc14 (2011-07-13) + --------------------- + + Bugfixes: + + - The tab character (``\t``) is now parsed correctly when used inside + tags. + + Features: + + - The ``RepeatDict`` class now works as a proxy behind a seperate + dictionary instance. + + - Added template constructor option ``keep_body`` which is a flag + (also available as a class attribute) that controls whether to save + the template body input in the ``body`` attribute. + + This is disabled by default, unless debug-mode is enabled. + + - The page template loader class now accepts an optional ``formats`` + argument which can be used to select an alternative template class. + + 2.0-rc13 (2011-07-07) + --------------------- + + Bugfixes: + + - The backslash character (followed by optional whitespace and a line + break) was not correctly interpreted as a continuation for Python + expressions. + + Features: + + - The Python expression implementation is now more flexible for + external subclassing via a new ``parse`` method. + + 2.0-rc12 (2011-07-04) + --------------------- + + Bugfixes: + + - Initial keyword arguments passed to a template now no longer "leak" + into the template variable space after a macro call. + + - An unexpected end tag is now an unrecoverable error. + + Features: + + - Improve exception output. + + 2.0-rc11 (2011-05-26) + --------------------- + + Bugfixes: + + - Fixed issue where variable names that begin with an underscore were + seemingly allowed, but their use resulted in a compiler error. + + Features: + + - Template variable names are now allowed to be prefixed with a single + underscore, but not two or more (reserved for internal use). + + Examples of valid names:: + + item + ITEM + _item + camelCase + underscore_delimited + help + + - Added support for Genshi's comment "drop" syntax:: + + <!--! This comment will be dropped --> + + Note the additional exclamation (!) character. + + This fixes addresses issue #10. + + 2.0-rc10 (2011-05-24) + --------------------- + + Bugfixes: + + - The ``tal:attributes`` statement now correctly operates + case-insensitive. The attribute name given in the statement will + replace an existing attribute with the same name, without respect to + case. + + Features: + + - Added ``meta:interpolation`` statement to control expression + interpolation setting. + + Strings that disable the setting: ``"off"`` and ``"false"``. + Strings that enable the setting: ``"on"`` and ``"true"``. + + - Expression interpolation now works inside XML comments. + + 2.0-rc9 (2011-05-05) + -------------------- + + Features: + + - Better debugging support for string decode and conversion. If a + naive join fails, each element in the output will now be attempted + coerced to unicode to try and trigger the failure near to the bad + string. + + 2.0-rc8 (2011-04-11) + -------------------- + + Bugfixes: + + - If a macro defines two slots with the same name, a caller will now + fill both with a single usage. + + - If a valid of ``None`` is provided as the translation function + argument, we now fall back to the class default. + + 2.0-rc7 (2011-03-29) + -------------------- + + Bugfixes: + + - Fixed issue with Python 2.5 compatibility AST. This affected at + least PyPy 1.4. + + Features: + + - The ``auto_reload`` setting now defaults to the class value; the + base template class gives a default value of + ``chameleon.config.AUTO_RELOAD``. This change allows a subclass to + provide a custom default value (such as an application-specific + debug mode setting). + + + 2.0-rc6 (2011-03-19) + -------------------- + + Features: + + - Added support for ``target_language`` keyword argument to render + method. If provided, the argument will be curried onto the + translation function. + + Bugfixes: + + - The HTML entities 'lt', 'gt' and 'quot' appearing inside content + subtition expressions are now translated into their native character + values. This fixes an issue where you could not dynamically create + elements using the ``structure`` (which is possible in ZPT). The + need to create such structure stems from the lack of an expression + interpolation operator in ZPT. + + - Fixed duplicate file pointer issue with test suite (affected Windows + platforms only). This fixes issue #9. + [oliora] + + - Use already open file using ``os.fdopen`` when trying to write out + the module source. This fixes LP #731803. + + + 2.0-rc5 (2011-03-07) + -------------------- + + Bugfixes: + + - Fixed a number of issues concerning the escaping of attribute + values: + + 1) Static attribute values are now included as they appear in the + source. + + This means that invalid attribute values such as ``"true && + false"`` are now left alone. It's not the job of the template + engine to correct such markup, at least not in the default mode + of operation. + + 2) The string expression compiler no longer unescapes + values. Instead, this is left to each expression + compiler. Currently only the Python expression compiler unescapes + its input. + + 3) The dynamic escape code sequence now correctly only replaces + ampersands that are part of an HTML escape format. + + Imports: + + - The page template classes and the loader class can now be imported + directly from the ``chameleon`` module. + + Features: + + - If a custom template loader is not provided, relative paths are now + resolved using ``os.abspath`` (i.e. to the current working + directory). + + - Absolute paths are normalized using ``os.path.normpath`` and + ``os.path.expanduser``. This ensures that all paths are kept in + their "canonical" form. + + + 2.0-rc4 (2011-03-03) + -------------------- + + Bugfixes: + + - Fixed an issue where the output of an end-to-end string expression + would raise an exception if the expression evaluated to ``None`` (it + should simply output nothing). + + - The ``convert`` function (which is configurable on the template + class level) now defaults to the ``translate`` function (at + run-time). + + This fixes an issue where message objects were not translated (and + thus converted to a string) using the a provided ``translate`` + function. + + - Fixed string interpolation issue where an expression immediately + succeeded by a right curly bracket would not parse. + + This fixes issue #5. + + - Fixed error where ``tal:condition`` would be evaluated after + ``tal:repeat``. + + Features: + + - Python expression is now a TALES expression. That means that the + pipe operator can be used to chain two or more expressions in a + try-except sequence. + + This behavior was ported from the 1.x series. Note that while it's + still possible to use the pipe character ("|") in an expression, it + must now be escaped. + + - The template cache can now be shared by multiple processes. + + + 2.0-rc3 (2011-03-02) + -------------------- + + Bugfixes: + + - Fixed ``atexit`` handler. + + This fixes issue #3. + + - If a cache directory is specified, it will now be used even when not + in debug mode. + + - Allow "comment" attribute in the TAL namespace. + + This fixes an issue in the sense that the reference engine allows + any attribute within the TAL namespace. However, only "comment" is + in common use. + + - The template constructor now accepts a flag ``debug`` which puts the + template *instance* into debug-mode regardless of the global + setting. + + This fixes issue #1. + + Features: + + - Added exception handler for exceptions raised while evaluating an + expression. + + This handler raises (or attempts to) a new exception of the type + ``RenderError``, with an additional base class of the original + exception class. The string value of the exception is a formatted + error message which includes the expression that caused the + exception. + + If we are unable to create the exception class, the original + exception is re-raised. + + 2.0-rc2 (2011-02-28) + -------------------- + + - Fixed upload issue. + + 2.0-rc1 (2011-02-28) + -------------------- + + - Initial public release. See documentation for what's new in this + series. + +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy diff --git a/src/Chameleon.egg-info/SOURCES.txt b/src/Chameleon.egg-info/SOURCES.txt new file mode 100644 index 0000000..17b18db --- /dev/null +++ b/src/Chameleon.egg-info/SOURCES.txt @@ -0,0 +1,393 @@ +LICENSE.txt +MANIFEST.in +README.rst +setup.py +src/Chameleon.egg-info/PKG-INFO +src/Chameleon.egg-info/SOURCES.txt +src/Chameleon.egg-info/dependency_links.txt +src/Chameleon.egg-info/not-zip-safe +src/Chameleon.egg-info/top_level.txt +src/chameleon/__init__.py +src/chameleon/ast25.py +src/chameleon/astutil.py +src/chameleon/benchmark.py +src/chameleon/codegen.py +src/chameleon/compiler.py +src/chameleon/config.py +src/chameleon/exc.py +src/chameleon/i18n.py +src/chameleon/interfaces.py +src/chameleon/loader.py +src/chameleon/metal.py +src/chameleon/namespaces.py +src/chameleon/nodes.py +src/chameleon/parser.py +src/chameleon/program.py +src/chameleon/py25.py +src/chameleon/py26.py +src/chameleon/tal.py +src/chameleon/tales.py +src/chameleon/template.py +src/chameleon/tokenize.py +src/chameleon/utils.py +src/chameleon/tests/__init__.py +src/chameleon/tests/test_doctests.py +src/chameleon/tests/test_exc.py +src/chameleon/tests/test_loader.py +src/chameleon/tests/test_parser.py +src/chameleon/tests/test_sniffing.py +src/chameleon/tests/test_templates.py +src/chameleon/tests/test_tokenizer.py +src/chameleon/tests/inputs/001-interpolation.txt +src/chameleon/tests/inputs/001-variable-scope.html +src/chameleon/tests/inputs/001-variable-scope.pt +src/chameleon/tests/inputs/001.xml +src/chameleon/tests/inputs/002-repeat-scope.pt +src/chameleon/tests/inputs/002.xml +src/chameleon/tests/inputs/003-content.pt +src/chameleon/tests/inputs/003.xml +src/chameleon/tests/inputs/004-attributes.pt +src/chameleon/tests/inputs/004.xml +src/chameleon/tests/inputs/005-default.pt +src/chameleon/tests/inputs/005.xml +src/chameleon/tests/inputs/006-attribute-interpolation.pt +src/chameleon/tests/inputs/006.xml +src/chameleon/tests/inputs/007-content-interpolation.pt +src/chameleon/tests/inputs/007.xml +src/chameleon/tests/inputs/008-builtins.pt +src/chameleon/tests/inputs/008.xml +src/chameleon/tests/inputs/009-literals.pt +src/chameleon/tests/inputs/009.xml +src/chameleon/tests/inputs/010-structure.pt +src/chameleon/tests/inputs/010.xml +src/chameleon/tests/inputs/011-messages.pt +src/chameleon/tests/inputs/011.xml +src/chameleon/tests/inputs/012-translation.pt +src/chameleon/tests/inputs/012.xml +src/chameleon/tests/inputs/013-repeat-nested.pt +src/chameleon/tests/inputs/013.xml +src/chameleon/tests/inputs/014-repeat-nested-similar.pt +src/chameleon/tests/inputs/014.xml +src/chameleon/tests/inputs/015-translation-nested.pt +src/chameleon/tests/inputs/015.xml +src/chameleon/tests/inputs/016-explicit-translation.pt +src/chameleon/tests/inputs/016.xml +src/chameleon/tests/inputs/017-omit-tag.pt +src/chameleon/tests/inputs/017.xml +src/chameleon/tests/inputs/018-translation-nested-dynamic.pt +src/chameleon/tests/inputs/018.xml +src/chameleon/tests/inputs/019-replace.pt +src/chameleon/tests/inputs/019.xml +src/chameleon/tests/inputs/020-on-error.pt +src/chameleon/tests/inputs/020.xml +src/chameleon/tests/inputs/021-translation-domain.pt +src/chameleon/tests/inputs/021.xml +src/chameleon/tests/inputs/022-switch.pt +src/chameleon/tests/inputs/022.xml +src/chameleon/tests/inputs/023-condition.pt +src/chameleon/tests/inputs/023.xml +src/chameleon/tests/inputs/024-namespace-elements.pt +src/chameleon/tests/inputs/024.xml +src/chameleon/tests/inputs/025-repeat-whitespace.pt +src/chameleon/tests/inputs/025.xml +src/chameleon/tests/inputs/026-repeat-variable.pt +src/chameleon/tests/inputs/026.xml +src/chameleon/tests/inputs/027-attribute-replacement.pt +src/chameleon/tests/inputs/027.xml +src/chameleon/tests/inputs/028-attribute-toggle.pt +src/chameleon/tests/inputs/028.xml +src/chameleon/tests/inputs/029-attribute-ordering.pt +src/chameleon/tests/inputs/029.xml +src/chameleon/tests/inputs/030-repeat-tuples.pt +src/chameleon/tests/inputs/030.xml +src/chameleon/tests/inputs/031-namespace-with-tal.pt +src/chameleon/tests/inputs/031.xml +src/chameleon/tests/inputs/032-master-template.pt +src/chameleon/tests/inputs/032.xml +src/chameleon/tests/inputs/033-use-macro-trivial.pt +src/chameleon/tests/inputs/033.xml +src/chameleon/tests/inputs/034-use-template-as-macro.pt +src/chameleon/tests/inputs/034.xml +src/chameleon/tests/inputs/035-use-macro-with-fill-slot.pt +src/chameleon/tests/inputs/035.xml +src/chameleon/tests/inputs/036-use-macro-inherits-dynamic-scope.pt +src/chameleon/tests/inputs/036.xml +src/chameleon/tests/inputs/037-use-macro-local-variable-scope.pt +src/chameleon/tests/inputs/037.xml +src/chameleon/tests/inputs/038-use-macro-globals.pt +src/chameleon/tests/inputs/038.xml +src/chameleon/tests/inputs/039-globals.pt +src/chameleon/tests/inputs/039.xml +src/chameleon/tests/inputs/040-macro-using-template-symbol.pt +src/chameleon/tests/inputs/040.xml +src/chameleon/tests/inputs/041-translate-nested-names.pt +src/chameleon/tests/inputs/041.xml +src/chameleon/tests/inputs/042-use-macro-fill-footer.pt +src/chameleon/tests/inputs/042.xml +src/chameleon/tests/inputs/043-macro-nested-dynamic-vars.pt +src/chameleon/tests/inputs/043.xml +src/chameleon/tests/inputs/044-tuple-define.pt +src/chameleon/tests/inputs/044.xml +src/chameleon/tests/inputs/045-namespaces.pt +src/chameleon/tests/inputs/045.xml +src/chameleon/tests/inputs/046-extend-macro.pt +src/chameleon/tests/inputs/046.xml +src/chameleon/tests/inputs/047-use-extended-macro.pt +src/chameleon/tests/inputs/047.xml +src/chameleon/tests/inputs/048-use-extended-macro-fill-original.pt +src/chameleon/tests/inputs/048.xml +src/chameleon/tests/inputs/049-entities-in-attributes.pt +src/chameleon/tests/inputs/049.xml +src/chameleon/tests/inputs/050-define-macro-and-use-not-extend.pt +src/chameleon/tests/inputs/050.xml +src/chameleon/tests/inputs/051-use-non-extended-macro.pt +src/chameleon/tests/inputs/051.xml +src/chameleon/tests/inputs/052-i18n-domain-inside-filled-slot.pt +src/chameleon/tests/inputs/052.xml +src/chameleon/tests/inputs/053-special-characters-in-attributes.pt +src/chameleon/tests/inputs/053.xml +src/chameleon/tests/inputs/054-import-expression.pt +src/chameleon/tests/inputs/054.xml +src/chameleon/tests/inputs/055-attribute-fallback-to-dict-lookup.pt +src/chameleon/tests/inputs/055.xml +src/chameleon/tests/inputs/056-comment-attribute.pt +src/chameleon/tests/inputs/056.xml +src/chameleon/tests/inputs/057-order.pt +src/chameleon/tests/inputs/057.xml +src/chameleon/tests/inputs/058-script.pt +src/chameleon/tests/inputs/058.xml +src/chameleon/tests/inputs/059-embedded-javascript.pt +src/chameleon/tests/inputs/059.xml +src/chameleon/tests/inputs/060-macro-with-multiple-same-slots.pt +src/chameleon/tests/inputs/060.xml +src/chameleon/tests/inputs/061-fill-one-slot-but-two-defined.pt +src/chameleon/tests/inputs/061.xml +src/chameleon/tests/inputs/062-comments-and-expressions.pt +src/chameleon/tests/inputs/062.xml +src/chameleon/tests/inputs/063-continuation.pt +src/chameleon/tests/inputs/063.xml +src/chameleon/tests/inputs/064-tags-and-special-characters.pt +src/chameleon/tests/inputs/064.xml +src/chameleon/tests/inputs/065-use-macro-in-fill.pt +src/chameleon/tests/inputs/065.xml +src/chameleon/tests/inputs/066-load-expression.pt +src/chameleon/tests/inputs/066.xml +src/chameleon/tests/inputs/067-attribute-decode.pt +src/chameleon/tests/inputs/067.xml +src/chameleon/tests/inputs/068-less-than-greater-than-in-attributes.pt +src/chameleon/tests/inputs/068.xml +src/chameleon/tests/inputs/069-translation-domain-and-macro.pt +src/chameleon/tests/inputs/069.xml +src/chameleon/tests/inputs/070-translation-domain-and-use-macro.pt +src/chameleon/tests/inputs/070.xml +src/chameleon/tests/inputs/071-html-attribute-defaults.pt +src/chameleon/tests/inputs/071.xml +src/chameleon/tests/inputs/072-repeat-interpolation.pt +src/chameleon/tests/inputs/072.xml +src/chameleon/tests/inputs/073-utf8-encoded.pt +src/chameleon/tests/inputs/073.xml +src/chameleon/tests/inputs/074-encoded-template.pt +src/chameleon/tests/inputs/074.xml +src/chameleon/tests/inputs/075-nested-macros.pt +src/chameleon/tests/inputs/075.xml +src/chameleon/tests/inputs/076-nested-macro-override.pt +src/chameleon/tests/inputs/076.xml +src/chameleon/tests/inputs/077-i18n-attributes.pt +src/chameleon/tests/inputs/077.xml +src/chameleon/tests/inputs/078-tags-and-newlines.pt +src/chameleon/tests/inputs/078.xml +src/chameleon/tests/inputs/079-implicit-i18n.pt +src/chameleon/tests/inputs/079.xml +src/chameleon/tests/inputs/080-xmlns-namespace-on-tal.pt +src/chameleon/tests/inputs/080.xml +src/chameleon/tests/inputs/081-load-spec.pt +src/chameleon/tests/inputs/081.xml +src/chameleon/tests/inputs/082-load-spec-computed.pt +src/chameleon/tests/inputs/082.xml +src/chameleon/tests/inputs/083-template-dict-to-macro.pt +src/chameleon/tests/inputs/083.xml +src/chameleon/tests/inputs/084-interpolation-in-cdata.pt +src/chameleon/tests/inputs/084.xml +src/chameleon/tests/inputs/085-nested-translation.pt +src/chameleon/tests/inputs/085.xml +src/chameleon/tests/inputs/086-self-closing.pt +src/chameleon/tests/inputs/086.xml +src/chameleon/tests/inputs/087-code-blocks.pt +src/chameleon/tests/inputs/087.xml +src/chameleon/tests/inputs/088-python-newlines.pt +src/chameleon/tests/inputs/088.xml +src/chameleon/tests/inputs/089-load-fallback.pt +src/chameleon/tests/inputs/089.xml +src/chameleon/tests/inputs/090-tuple-expression.pt +src/chameleon/tests/inputs/090.xml +src/chameleon/tests/inputs/091-repeat-none.pt +src/chameleon/tests/inputs/091.xml +src/chameleon/tests/inputs/092.xml +src/chameleon/tests/inputs/093.xml +src/chameleon/tests/inputs/094.xml +src/chameleon/tests/inputs/095.xml +src/chameleon/tests/inputs/096.xml +src/chameleon/tests/inputs/097.xml +src/chameleon/tests/inputs/098.xml +src/chameleon/tests/inputs/099.xml +src/chameleon/tests/inputs/100.xml +src/chameleon/tests/inputs/101-unclosed-tags.html +src/chameleon/tests/inputs/101.xml +src/chameleon/tests/inputs/102-unquoted-attributes.html +src/chameleon/tests/inputs/102.xml +src/chameleon/tests/inputs/103-simple-attribute.html +src/chameleon/tests/inputs/103.xml +src/chameleon/tests/inputs/104.xml +src/chameleon/tests/inputs/105.xml +src/chameleon/tests/inputs/106.xml +src/chameleon/tests/inputs/107.xml +src/chameleon/tests/inputs/108.xml +src/chameleon/tests/inputs/109.xml +src/chameleon/tests/inputs/110.xml +src/chameleon/tests/inputs/111.xml +src/chameleon/tests/inputs/112.xml +src/chameleon/tests/inputs/113.xml +src/chameleon/tests/inputs/114.xml +src/chameleon/tests/inputs/115.xml +src/chameleon/tests/inputs/116.xml +src/chameleon/tests/inputs/117.xml +src/chameleon/tests/inputs/118.xml +src/chameleon/tests/inputs/119.xml +src/chameleon/tests/inputs/120-translation-context.pt +src/chameleon/tests/inputs/121-translation-comment.pt +src/chameleon/tests/inputs/122-translation-ignore.pt +src/chameleon/tests/inputs/123-html5-data-attributes.pt +src/chameleon/tests/inputs/124-translation-target.pt +src/chameleon/tests/inputs/125-macro-translation-ordering.pt +src/chameleon/tests/inputs/126-define-escaping.pt +src/chameleon/tests/inputs/238-macroname.pt +src/chameleon/tests/inputs/greeting.pt +src/chameleon/tests/inputs/hello_world.pt +src/chameleon/tests/inputs/hello_world.txt +src/chameleon/tests/inputs/multinode-implicit-i18n.pt +src/chameleon/tests/outputs/001.html +src/chameleon/tests/outputs/001.pt +src/chameleon/tests/outputs/001.txt +src/chameleon/tests/outputs/002.pt +src/chameleon/tests/outputs/003.pt +src/chameleon/tests/outputs/004.pt +src/chameleon/tests/outputs/005.pt +src/chameleon/tests/outputs/006.pt +src/chameleon/tests/outputs/007.pt +src/chameleon/tests/outputs/008.pt +src/chameleon/tests/outputs/009.pt +src/chameleon/tests/outputs/010.pt +src/chameleon/tests/outputs/011-en.pt +src/chameleon/tests/outputs/011.pt +src/chameleon/tests/outputs/012-en.pt +src/chameleon/tests/outputs/012.pt +src/chameleon/tests/outputs/013.pt +src/chameleon/tests/outputs/014.pt +src/chameleon/tests/outputs/015-en.pt +src/chameleon/tests/outputs/015.pt +src/chameleon/tests/outputs/016-en.pt +src/chameleon/tests/outputs/016.pt +src/chameleon/tests/outputs/017.pt +src/chameleon/tests/outputs/018-en.pt +src/chameleon/tests/outputs/018.pt +src/chameleon/tests/outputs/019.pt +src/chameleon/tests/outputs/020.pt +src/chameleon/tests/outputs/021-en.pt +src/chameleon/tests/outputs/021.pt +src/chameleon/tests/outputs/022.pt +src/chameleon/tests/outputs/023.pt +src/chameleon/tests/outputs/024.pt +src/chameleon/tests/outputs/025.pt +src/chameleon/tests/outputs/026.pt +src/chameleon/tests/outputs/027.pt +src/chameleon/tests/outputs/028.pt +src/chameleon/tests/outputs/029.pt +src/chameleon/tests/outputs/030.pt +src/chameleon/tests/outputs/031.pt +src/chameleon/tests/outputs/032.pt +src/chameleon/tests/outputs/033.pt +src/chameleon/tests/outputs/034.pt +src/chameleon/tests/outputs/035.pt +src/chameleon/tests/outputs/036.pt +src/chameleon/tests/outputs/037.pt +src/chameleon/tests/outputs/038.pt +src/chameleon/tests/outputs/039.pt +src/chameleon/tests/outputs/040.pt +src/chameleon/tests/outputs/041.pt +src/chameleon/tests/outputs/042.pt +src/chameleon/tests/outputs/043.pt +src/chameleon/tests/outputs/044.pt +src/chameleon/tests/outputs/045.pt +src/chameleon/tests/outputs/046.pt +src/chameleon/tests/outputs/047.pt +src/chameleon/tests/outputs/048.pt +src/chameleon/tests/outputs/049.pt +src/chameleon/tests/outputs/050.pt +src/chameleon/tests/outputs/051.pt +src/chameleon/tests/outputs/052.pt +src/chameleon/tests/outputs/053.pt +src/chameleon/tests/outputs/054.pt +src/chameleon/tests/outputs/055.pt +src/chameleon/tests/outputs/056.pt +src/chameleon/tests/outputs/057.pt +src/chameleon/tests/outputs/058.pt +src/chameleon/tests/outputs/059.pt +src/chameleon/tests/outputs/060.pt +src/chameleon/tests/outputs/061.pt +src/chameleon/tests/outputs/062.pt +src/chameleon/tests/outputs/063.pt +src/chameleon/tests/outputs/064.pt +src/chameleon/tests/outputs/065.pt +src/chameleon/tests/outputs/066.pt +src/chameleon/tests/outputs/067.pt +src/chameleon/tests/outputs/068.pt +src/chameleon/tests/outputs/069-en.pt +src/chameleon/tests/outputs/069.pt +src/chameleon/tests/outputs/070-en.pt +src/chameleon/tests/outputs/070.pt +src/chameleon/tests/outputs/071.pt +src/chameleon/tests/outputs/072.pt +src/chameleon/tests/outputs/073.pt +src/chameleon/tests/outputs/074.pt +src/chameleon/tests/outputs/075.pt +src/chameleon/tests/outputs/076.pt +src/chameleon/tests/outputs/077-en.pt +src/chameleon/tests/outputs/077.pt +src/chameleon/tests/outputs/078.pt +src/chameleon/tests/outputs/079-en.pt +src/chameleon/tests/outputs/079.pt +src/chameleon/tests/outputs/080.pt +src/chameleon/tests/outputs/081.pt +src/chameleon/tests/outputs/082.pt +src/chameleon/tests/outputs/083.pt +src/chameleon/tests/outputs/084.pt +src/chameleon/tests/outputs/085-en.pt +src/chameleon/tests/outputs/085.pt +src/chameleon/tests/outputs/086.pt +src/chameleon/tests/outputs/087.pt +src/chameleon/tests/outputs/088.pt +src/chameleon/tests/outputs/089.pt +src/chameleon/tests/outputs/090.pt +src/chameleon/tests/outputs/091.pt +src/chameleon/tests/outputs/101.html +src/chameleon/tests/outputs/102.html +src/chameleon/tests/outputs/103.html +src/chameleon/tests/outputs/120-en.pt +src/chameleon/tests/outputs/120.pt +src/chameleon/tests/outputs/121.pt +src/chameleon/tests/outputs/122.pt +src/chameleon/tests/outputs/123.pt +src/chameleon/tests/outputs/124-en.pt +src/chameleon/tests/outputs/124.pt +src/chameleon/tests/outputs/125.pt +src/chameleon/tests/outputs/126.pt +src/chameleon/tests/outputs/238.pt +src/chameleon/tests/outputs/greeting.pt +src/chameleon/tests/outputs/hello_world.pt +src/chameleon/tests/outputs/hello_world.txt +src/chameleon/tests/outputs/multinode-en.pt +src/chameleon/tests/outputs/multinode.pt +src/chameleon/zpt/__init__.py +src/chameleon/zpt/loader.py +src/chameleon/zpt/program.py +src/chameleon/zpt/template.py
\ No newline at end of file diff --git a/src/Chameleon.egg-info/dependency_links.txt b/src/Chameleon.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Chameleon.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/Chameleon.egg-info/not-zip-safe b/src/Chameleon.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/Chameleon.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/src/Chameleon.egg-info/top_level.txt b/src/Chameleon.egg-info/top_level.txt new file mode 100644 index 0000000..33b5aad --- /dev/null +++ b/src/Chameleon.egg-info/top_level.txt @@ -0,0 +1 @@ +chameleon diff --git a/src/chameleon/__init__.py b/src/chameleon/__init__.py new file mode 100644 index 0000000..e873987 --- /dev/null +++ b/src/chameleon/__init__.py @@ -0,0 +1,6 @@ +from .zpt.template import PageTemplate +from .zpt.template import PageTemplateFile +from .zpt.template import PageTextTemplate +from .zpt.template import PageTextTemplateFile +from .zpt.loader import TemplateLoader as PageTemplateLoader +from .exc import TemplateError diff --git a/src/chameleon/ast25.py b/src/chameleon/ast25.py new file mode 100644 index 0000000..08eb77b --- /dev/null +++ b/src/chameleon/ast25.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2008 by Armin Ronacher. +# License: Python License. +# + +import _ast + +from _ast import * + + +def fix_missing_locations(node): + """ + When you compile a node tree with compile(), the compiler expects lineno and + col_offset attributes for every node that supports them. This is rather + tedious to fill in for generated nodes, so this helper adds these attributes + recursively where not already set, by setting them to the values of the + parent node. It works recursively starting at *node*. + """ + def _fix(node, lineno, col_offset): + if 'lineno' in node._attributes: + if not hasattr(node, 'lineno'): + node.lineno = lineno + else: + lineno = node.lineno + if 'col_offset' in node._attributes: + if not hasattr(node, 'col_offset'): + node.col_offset = col_offset + else: + col_offset = node.col_offset + for child in iter_child_nodes(node): + _fix(child, lineno, col_offset) + _fix(node, 1, 0) + return node + + +def iter_child_nodes(node): + """ + Yield all direct child nodes of *node*, that is, all fields that are nodes + and all items of fields that are lists of nodes. + """ + for name, field in iter_fields(node): + if isinstance(field, (AST, _ast.AST)): + yield field + elif isinstance(field, list): + for item in field: + if isinstance(item, (AST, _ast.AST)): + yield item + + +def iter_fields(node): + """ + Yield a tuple of ``(fieldname, value)`` for each field in ``node._fields`` + that is present on *node*. + """ + + for field in node._fields or (): + try: + yield field, getattr(node, field) + except AttributeError: + pass + + +def walk(node): + """ + Recursively yield all child nodes of *node*, in no specified order. This is + useful if you only want to modify nodes in place and don't care about the + context. + """ + from collections import deque + todo = deque([node]) + while todo: + node = todo.popleft() + todo.extend(iter_child_nodes(node)) + yield node + + +class NodeVisitor(object): + """ + A node visitor base class that walks the abstract syntax tree and calls a + visitor function for every node found. This function may return a value + which is forwarded by the `visit` method. + + This class is meant to be subclassed, with the subclass adding visitor + methods. + + Per default the visitor functions for the nodes are ``'visit_'`` + + class name of the node. So a `TryFinally` node visit function would + be `visit_TryFinally`. This behavior can be changed by overriding + the `visit` method. If no visitor function exists for a node + (return value `None`) the `generic_visit` visitor is used instead. + + Don't use the `NodeVisitor` if you want to apply changes to nodes during + traversing. For this a special visitor exists (`NodeTransformer`) that + allows modifications. + """ + + def visit(self, node): + """Visit a node.""" + method = 'visit_' + node.__class__.__name__ + visitor = getattr(self, method, self.generic_visit) + return visitor(node) + + def generic_visit(self, node): + """Called if no explicit visitor function exists for a node.""" + for field, value in iter_fields(node): + if isinstance(value, list): + for item in value: + if isinstance(item, (AST, _ast.AST)): + self.visit(item) + elif isinstance(value, (AST, _ast.AST)): + self.visit(value) + + +class AST(object): + _fields = () + _attributes = 'lineno', 'col_offset' + + def __init__(self, *args, **kwargs): + self.__dict__.update(kwargs) + self._fields = self._fields or () + for name, value in zip(self._fields, args): + setattr(self, name, value) + + +for name, cls in _ast.__dict__.items(): + if isinstance(cls, type) and issubclass(cls, _ast.AST): + try: + cls.__bases__ = (AST, ) + cls.__bases__ + except TypeError: + pass + + +class ExceptHandler(AST): + _fields = "type", "name", "body" diff --git a/src/chameleon/astutil.py b/src/chameleon/astutil.py new file mode 100644 index 0000000..6be2266 --- /dev/null +++ b/src/chameleon/astutil.py @@ -0,0 +1,1019 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2008-2009 Edgewall Software +# All rights reserved. +# +# This software is licensed as described in the file COPYING, which +# you should have received as part of this distribution. The terms +# are also available at http://genshi.edgewall.org/wiki/License. +# +# This software consists of voluntary contributions made by many +# individuals. For the exact contribution history, see the revision +# history and logs, available at http://genshi.edgewall.org/log/. + +"""Support classes for generating code from abstract syntax trees.""" + +try: + import ast +except ImportError: + from chameleon import ast25 as ast + +import sys +import logging +import weakref +import collections + +node_annotations = weakref.WeakKeyDictionary() + +try: + node_annotations[ast.Name()] = None +except TypeError: + logging.debug( + "Unable to create weak references to AST nodes. " \ + "A lock will be used around compilation loop." + ) + + node_annotations = {} + +__docformat__ = 'restructuredtext en' + + +def annotated(value): + node = load("annotation") + node_annotations[node] = value + return node + + +def parse(source, mode='eval'): + return compile(source, '', mode, ast.PyCF_ONLY_AST) + + +def load(name): + return ast.Name(id=name, ctx=ast.Load()) + + +def store(name): + return ast.Name(id=name, ctx=ast.Store()) + + +def param(name): + return ast.Name(id=name, ctx=ast.Param()) + + +def delete(name): + return ast.Name(id=name, ctx=ast.Del()) + + +def subscript(name, value, ctx): + return ast.Subscript( + value=value, + slice=ast.Index(value=ast.Str(s=name)), + ctx=ctx, + ) + + +def walk_names(target, mode): + for node in ast.walk(target): + if isinstance(node, ast.Name) and \ + isinstance(node.ctx, mode): + yield node.id + + +def iter_fields(node): + """ + Yield a tuple of ``(fieldname, value)`` for each field in ``node._fields`` + that is present on *node*. + """ + for field in node._fields: + try: + yield field, getattr(node, field) + except AttributeError: + pass + + +def iter_child_nodes(node): + """ + Yield all direct child nodes of *node*, that is, all fields that are nodes + and all items of fields that are lists of nodes. + """ + for name, field in iter_fields(node): + if isinstance(field, Node): + yield field + elif isinstance(field, list): + for item in field: + if isinstance(item, Node): + yield item + + +def walk(node): + """ + Recursively yield all descendant nodes in the tree starting at *node* + (including *node* itself), in no specified order. This is useful if you + only want to modify nodes in place and don't care about the context. + """ + todo = collections.deque([node]) + while todo: + node = todo.popleft() + todo.extend(iter_child_nodes(node)) + yield node + + +def copy(source, target): + target.__class__ = source.__class__ + target.__dict__ = source.__dict__ + + +def swap(root, replacement, name): + for node in ast.walk(root): + if (isinstance(node, ast.Name) and + isinstance(node.ctx, ast.Load) and + node.id == name): + assert hasattr(replacement, '_fields') + node_annotations.setdefault(node, replacement) + + +def marker(name): + return ast.Str(s="__%s" % name) + + +class Node(object): + """AST baseclass that gives us a convenient initialization + method. We explicitly declare and use the ``_fields`` attribute.""" + + _fields = () + + def __init__(self, *args, **kwargs): + assert isinstance(self._fields, tuple) + self.__dict__.update(kwargs) + for name, value in zip(self._fields, args): + setattr(self, name, value) + + def __repr__(self): + """Poor man's single-line pretty printer.""" + + name = type(self).__name__ + return '<%s%s at %x>' % ( + name, + "".join(" %s=%r" % (name, getattr(self, name, "\"?\"")) + for name in self._fields), + id(self) + ) + + def extract(self, condition): + result = [] + for node in walk(self): + if condition(node): + result.append(node) + + return result + + +class Builtin(Node): + """Represents a Python builtin. + + Used when a builtin is used internally by the compiler, to avoid + clashing with a user assignment (e.g. ``help`` is a builtin, but + also commonly assigned in templates). + """ + + _fields = "id", "ctx" + + ctx = ast.Load() + + +class Symbol(Node): + """Represents an importable symbol.""" + + _fields = "value", + + +class Static(Node): + """Represents a static value.""" + + _fields = "value", "name" + + name = None + + +class Comment(Node): + _fields = "text", "space", "stmt" + + stmt = None + space = "" + + +class TokenRef(Node): + """Represents a source-code token reference.""" + + _fields = "pos", "length" + + +class ASTCodeGenerator(object): + """General purpose base class for AST transformations. + + Every visitor method can be overridden to return an AST node that has been + altered or replaced in some way. + """ + + def __init__(self, tree): + self.lines_info = [] + self.line_info = [] + self.lines = [] + self.line = "" + self.last = None + self.indent = 0 + self.blame_stack = [] + self.visit(tree) + + if self.line.strip(): + self._new_line() + + self.line = None + self.line_info = None + + # strip trivial lines + self.code = "\n".join( + line.strip() and line or "" + for line in self.lines + ) + + def _change_indent(self, delta): + self.indent += delta + + def _new_line(self): + if self.line is not None: + self.lines.append(self.line) + self.lines_info.append(self.line_info) + self.line = ' ' * 4 * self.indent + if len(self.blame_stack) == 0: + self.line_info = [] + self.last = None + else: + self.line_info = [(0, self.blame_stack[-1],)] + self.last = self.blame_stack[-1] + + def _write(self, s): + if len(s) == 0: + return + if len(self.blame_stack) == 0: + if self.last is not None: + self.last = None + self.line_info.append((len(self.line), self.last)) + else: + if self.last != self.blame_stack[-1]: + self.last = self.blame_stack[-1] + self.line_info.append((len(self.line), self.last)) + self.line += s + + def flush(self): + if self.line: + self._new_line() + + def visit(self, node): + if node is None: + return None + if type(node) is tuple: + return tuple([self.visit(n) for n in node]) + try: + self.blame_stack.append((node.lineno, node.col_offset,)) + info = True + except AttributeError: + info = False + visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None) + if visitor is None: + raise Exception('No handler for ``%s`` (%s).' % ( + node.__class__.__name__, repr(node))) + ret = visitor(node) + if info: + self.blame_stack.pop() + return ret + + def visit_Module(self, node): + for n in node.body: + self.visit(n) + visit_Interactive = visit_Module + visit_Suite = visit_Module + + def visit_Expression(self, node): + return self.visit(node.body) + + # arguments = (expr* args, identifier? vararg, + # identifier? kwarg, expr* defaults) + def visit_arguments(self, node): + first = True + no_default_count = len(node.args) - len(node.defaults) + for i, arg in enumerate(node.args): + if not first: + self._write(', ') + else: + first = False + self.visit(arg) + if i >= no_default_count: + self._write('=') + self.visit(node.defaults[i - no_default_count]) + if getattr(node, 'vararg', None): + if not first: + self._write(', ') + else: + first = False + self._write('*' + node.vararg) + if getattr(node, 'kwarg', None): + if not first: + self._write(', ') + else: + first = False + self._write('**' + node.kwarg) + + def visit_arg(self, node): + self._write(node.arg) + + # FunctionDef(identifier name, arguments args, + # stmt* body, expr* decorators) + def visit_FunctionDef(self, node): + self._new_line() + for decorator in getattr(node, 'decorator_list', ()): + self._new_line() + self._write('@') + self.visit(decorator) + self._new_line() + self._write('def ' + node.name + '(') + self.visit(node.args) + self._write('):') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + # ClassDef(identifier name, expr* bases, stmt* body) + def visit_ClassDef(self, node): + self._new_line() + self._write('class ' + node.name) + if node.bases: + self._write('(') + self.visit(node.bases[0]) + for base in node.bases[1:]: + self._write(', ') + self.visit(base) + self._write(')') + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + # Return(expr? value) + def visit_Return(self, node): + self._new_line() + self._write('return') + if getattr(node, 'value', None): + self._write(' ') + self.visit(node.value) + + # Delete(expr* targets) + def visit_Delete(self, node): + self._new_line() + self._write('del ') + self.visit(node.targets[0]) + for target in node.targets[1:]: + self._write(', ') + self.visit(target) + + # Assign(expr* targets, expr value) + def visit_Assign(self, node): + self._new_line() + for target in node.targets: + self.visit(target) + self._write(' = ') + self.visit(node.value) + + # AugAssign(expr target, operator op, expr value) + def visit_AugAssign(self, node): + self._new_line() + self.visit(node.target) + self._write(' ' + self.binary_operators[node.op.__class__] + '= ') + self.visit(node.value) + + # Print(expr? dest, expr* values, bool nl) + def visit_Print(self, node): + self._new_line() + self._write('print') + if getattr(node, 'dest', None): + self._write(' >> ') + self.visit(node.dest) + if getattr(node, 'values', None): + self._write(', ') + else: + self._write(' ') + if getattr(node, 'values', None): + self.visit(node.values[0]) + for value in node.values[1:]: + self._write(', ') + self.visit(value) + if not node.nl: + self._write(',') + + # For(expr target, expr iter, stmt* body, stmt* orelse) + def visit_For(self, node): + self._new_line() + self._write('for ') + self.visit(node.target) + self._write(' in ') + self.visit(node.iter) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'orelse', None): + self._new_line() + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # While(expr test, stmt* body, stmt* orelse) + def visit_While(self, node): + self._new_line() + self._write('while ') + self.visit(node.test) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'orelse', None): + self._new_line() + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # If(expr test, stmt* body, stmt* orelse) + def visit_If(self, node): + self._new_line() + self._write('if ') + self.visit(node.test) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'orelse', None): + self._new_line() + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # With(expr context_expr, expr? optional_vars, stmt* body) + def visit_With(self, node): + self._new_line() + self._write('with ') + self.visit(node.context_expr) + if getattr(node, 'optional_vars', None): + self._write(' as ') + self.visit(node.optional_vars) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + # Raise(expr? type, expr? inst, expr? tback) + def visit_Raise(self, node): + self._new_line() + self._write('raise') + if not getattr(node, "type", None): + exc = getattr(node, "exc", None) + if exc is None: + return + self._write(' ') + return self.visit(exc) + self._write(' ') + self.visit(node.type) + if not node.inst: + return + self._write(', ') + self.visit(node.inst) + if not node.tback: + return + self._write(', ') + self.visit(node.tback) + + # Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) + def visit_Try(self, node): + self._new_line() + self._write('try:') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'handlers', None): + for handler in node.handlers: + self.visit(handler) + self._new_line() + + if getattr(node, 'orelse', None): + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + if getattr(node, 'finalbody', None): + self._new_line() + self._write('finally:') + self._change_indent(1) + for statement in node.finalbody: + self.visit(statement) + self._change_indent(-1) + + # TryExcept(stmt* body, excepthandler* handlers, stmt* orelse) + def visit_TryExcept(self, node): + self._new_line() + self._write('try:') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + if getattr(node, 'handlers', None): + for handler in node.handlers: + self.visit(handler) + self._new_line() + if getattr(node, 'orelse', None): + self._write('else:') + self._change_indent(1) + for statement in node.orelse: + self.visit(statement) + self._change_indent(-1) + + # excepthandler = (expr? type, expr? name, stmt* body) + def visit_ExceptHandler(self, node): + self._new_line() + self._write('except') + if getattr(node, 'type', None): + self._write(' ') + self.visit(node.type) + if getattr(node, 'name', None): + if sys.version_info[0] == 2: + assert getattr(node, 'type', None) + self._write(', ') + else: + self._write(' as ') + self.visit(node.name) + self._write(':') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + visit_excepthandler = visit_ExceptHandler + + # TryFinally(stmt* body, stmt* finalbody) + def visit_TryFinally(self, node): + self._new_line() + self._write('try:') + self._change_indent(1) + for statement in node.body: + self.visit(statement) + self._change_indent(-1) + + if getattr(node, 'finalbody', None): + self._new_line() + self._write('finally:') + self._change_indent(1) + for statement in node.finalbody: + self.visit(statement) + self._change_indent(-1) + + # Assert(expr test, expr? msg) + def visit_Assert(self, node): + self._new_line() + self._write('assert ') + self.visit(node.test) + if getattr(node, 'msg', None): + self._write(', ') + self.visit(node.msg) + + def visit_alias(self, node): + self._write(node.name) + if getattr(node, 'asname', None): + self._write(' as ') + self._write(node.asname) + + # Import(alias* names) + def visit_Import(self, node): + self._new_line() + self._write('import ') + self.visit(node.names[0]) + for name in node.names[1:]: + self._write(', ') + self.visit(name) + + # ImportFrom(identifier module, alias* names, int? level) + def visit_ImportFrom(self, node): + self._new_line() + self._write('from ') + if node.level: + self._write('.' * node.level) + self._write(node.module) + self._write(' import ') + self.visit(node.names[0]) + for name in node.names[1:]: + self._write(', ') + self.visit(name) + + # Exec(expr body, expr? globals, expr? locals) + def visit_Exec(self, node): + self._new_line() + self._write('exec ') + self.visit(node.body) + if not node.globals: + return + self._write(', ') + self.visit(node.globals) + if not node.locals: + return + self._write(', ') + self.visit(node.locals) + + # Global(identifier* names) + def visit_Global(self, node): + self._new_line() + self._write('global ') + self.visit(node.names[0]) + for name in node.names[1:]: + self._write(', ') + self.visit(name) + + # Expr(expr value) + def visit_Expr(self, node): + self._new_line() + self.visit(node.value) + + # Pass + def visit_Pass(self, node): + self._new_line() + self._write('pass') + + # Break + def visit_Break(self, node): + self._new_line() + self._write('break') + + # Continue + def visit_Continue(self, node): + self._new_line() + self._write('continue') + + ### EXPRESSIONS + def with_parens(f): + def _f(self, node): + self._write('(') + f(self, node) + self._write(')') + return _f + + bool_operators = {ast.And: 'and', ast.Or: 'or'} + + # BoolOp(boolop op, expr* values) + @with_parens + def visit_BoolOp(self, node): + joiner = ' ' + self.bool_operators[node.op.__class__] + ' ' + self.visit(node.values[0]) + for value in node.values[1:]: + self._write(joiner) + self.visit(value) + + binary_operators = { + ast.Add: '+', + ast.Sub: '-', + ast.Mult: '*', + ast.Div: '/', + ast.Mod: '%', + ast.Pow: '**', + ast.LShift: '<<', + ast.RShift: '>>', + ast.BitOr: '|', + ast.BitXor: '^', + ast.BitAnd: '&', + ast.FloorDiv: '//' + } + + # BinOp(expr left, operator op, expr right) + @with_parens + def visit_BinOp(self, node): + self.visit(node.left) + self._write(' ' + self.binary_operators[node.op.__class__] + ' ') + self.visit(node.right) + + unary_operators = { + ast.Invert: '~', + ast.Not: 'not', + ast.UAdd: '+', + ast.USub: '-', + } + + # UnaryOp(unaryop op, expr operand) + def visit_UnaryOp(self, node): + self._write(self.unary_operators[node.op.__class__] + ' ') + self.visit(node.operand) + + # Lambda(arguments args, expr body) + @with_parens + def visit_Lambda(self, node): + self._write('lambda ') + self.visit(node.args) + self._write(': ') + self.visit(node.body) + + # IfExp(expr test, expr body, expr orelse) + @with_parens + def visit_IfExp(self, node): + self.visit(node.body) + self._write(' if ') + self.visit(node.test) + self._write(' else ') + self.visit(node.orelse) + + # Dict(expr* keys, expr* values) + def visit_Dict(self, node): + self._write('{') + for key, value in zip(node.keys, node.values): + self.visit(key) + self._write(': ') + self.visit(value) + self._write(', ') + self._write('}') + + def visit_Set(self, node): + self._write('{') + elts = list(node.elts) + last = elts.pop() + for elt in elts: + self.visit(elt) + self._write(', ') + self.visit(last) + self._write('}') + + # ListComp(expr elt, comprehension* generators) + def visit_ListComp(self, node): + self._write('[') + self.visit(node.elt) + for generator in node.generators: + # comprehension = (expr target, expr iter, expr* ifs) + self._write(' for ') + self.visit(generator.target) + self._write(' in ') + self.visit(generator.iter) + for ifexpr in generator.ifs: + self._write(' if ') + self.visit(ifexpr) + self._write(']') + + # GeneratorExp(expr elt, comprehension* generators) + def visit_GeneratorExp(self, node): + self._write('(') + self.visit(node.elt) + for generator in node.generators: + # comprehension = (expr target, expr iter, expr* ifs) + self._write(' for ') + self.visit(generator.target) + self._write(' in ') + self.visit(generator.iter) + for ifexpr in generator.ifs: + self._write(' if ') + self.visit(ifexpr) + self._write(')') + + # Yield(expr? value) + def visit_Yield(self, node): + self._write('yield') + if getattr(node, 'value', None): + self._write(' ') + self.visit(node.value) + + comparison_operators = { + ast.Eq: '==', + ast.NotEq: '!=', + ast.Lt: '<', + ast.LtE: '<=', + ast.Gt: '>', + ast.GtE: '>=', + ast.Is: 'is', + ast.IsNot: 'is not', + ast.In: 'in', + ast.NotIn: 'not in', + } + + # Compare(expr left, cmpop* ops, expr* comparators) + @with_parens + def visit_Compare(self, node): + self.visit(node.left) + for op, comparator in zip(node.ops, node.comparators): + self._write(' ' + self.comparison_operators[op.__class__] + ' ') + self.visit(comparator) + + # Call(expr func, expr* args, keyword* keywords, + # expr? starargs, expr? kwargs) + def visit_Call(self, node): + self.visit(node.func) + self._write('(') + first = True + for arg in node.args: + if not first: + self._write(', ') + first = False + self.visit(arg) + + for keyword in node.keywords: + if not first: + self._write(', ') + first = False + # keyword = (identifier arg, expr value) + if keyword.arg is not None: + self._write(keyword.arg) + self._write('=') + else: + self._write('**') + self.visit(keyword.value) + # Attribute removed in Python 3.5 + if getattr(node, 'starargs', None): + if not first: + self._write(', ') + first = False + self._write('*') + self.visit(node.starargs) + + # Attribute removed in Python 3.5 + if getattr(node, 'kwargs', None): + if not first: + self._write(', ') + first = False + self._write('**') + self.visit(node.kwargs) + self._write(')') + + # Repr(expr value) + def visit_Repr(self, node): + self._write('`') + self.visit(node.value) + self._write('`') + + # Constant(object value) + def visit_Constant(self, node): + if node.value is Ellipsis: + self._write('...') + else: + self._write(repr(node.value)) + + # Num(object n) + def visit_Num(self, node): + self._write(repr(node.n)) + + # Str(string s) + def visit_Str(self, node): + self._write(repr(node.s)) + + def visit_Ellipsis(self, node): + self._write('...') + + # Attribute(expr value, identifier attr, expr_context ctx) + def visit_Attribute(self, node): + self.visit(node.value) + self._write('.') + self._write(node.attr) + + # Subscript(expr value, slice slice, expr_context ctx) + def visit_Subscript(self, node): + self.visit(node.value) + self._write('[') + if isinstance(node.slice, ast.Tuple) and node.slice.elts: + self.visit(node.slice.elts[0]) + if len(node.slice.elts) == 1: + self._write(', ') + else: + for dim in node.slice.elts[1:]: + self._write(', ') + self.visit(dim) + else: + self.visit(node.slice) + self._write(']') + + # Slice(expr? lower, expr? upper, expr? step) + def visit_Slice(self, node): + if getattr(node, 'lower', None) is not None: + self.visit(node.lower) + self._write(':') + if getattr(node, 'upper', None) is not None: + self.visit(node.upper) + if getattr(node, 'step', None) is not None: + self._write(':') + self.visit(node.step) + + # Index(expr value) + def visit_Index(self, node): + self.visit(node.value) + + # ExtSlice(slice* dims) + def visit_ExtSlice(self, node): + self.visit(node.dims[0]) + if len(node.dims) == 1: + self._write(', ') + else: + for dim in node.dims[1:]: + self._write(', ') + self.visit(dim) + + # Starred(expr value, expr_context ctx) + def visit_Starred(self, node): + self._write('*') + self.visit(node.value) + + # Name(identifier id, expr_context ctx) + def visit_Name(self, node): + self._write(node.id) + + # List(expr* elts, expr_context ctx) + def visit_List(self, node): + self._write('[') + for elt in node.elts: + self.visit(elt) + self._write(', ') + self._write(']') + + # Tuple(expr *elts, expr_context ctx) + def visit_Tuple(self, node): + self._write('(') + for elt in node.elts: + self.visit(elt) + self._write(', ') + self._write(')') + + # NameConstant(singleton value) + def visit_NameConstant(self, node): + self._write(str(node.value)) + +class AnnotationAwareVisitor(ast.NodeVisitor): + def visit(self, node): + annotation = node_annotations.get(node) + if annotation is not None: + assert hasattr(annotation, '_fields') + node = annotation + + super(AnnotationAwareVisitor, self).visit(node) + + def apply_transform(self, node): + if node not in node_annotations: + result = self.transform(node) + if result is not None and result is not node: + node_annotations[node] = result + + +class NameLookupRewriteVisitor(AnnotationAwareVisitor): + def __init__(self, transform): + self.transform = transform + self.transformed = set() + self.scopes = [set()] + + def __call__(self, node): + self.visit(node) + return self.transformed + + def visit_arg(self, node): + scope = self.scopes[-1] + scope.add(node.arg) + + def visit_Name(self, node): + scope = self.scopes[-1] + if isinstance(node.ctx, ast.Param): + scope.add(node.id) + elif node.id not in scope: + self.transformed.add(node.id) + self.apply_transform(node) + + def visit_FunctionDef(self, node): + self.scopes[-1].add(node.name) + + def visit_alias(self, node): + name = node.asname if node.asname is not None else node.name + self.scopes[-1].add(name) + + def visit_Lambda(self, node): + self.scopes.append(set()) + try: + self.visit(node.args) + self.visit(node.body) + finally: + self.scopes.pop() + + +class ItemLookupOnAttributeErrorVisitor(AnnotationAwareVisitor): + def __init__(self, transform): + self.transform = transform + + def visit_Attribute(self, node): + self.generic_visit(node) + self.apply_transform(node) diff --git a/src/chameleon/benchmark.py b/src/chameleon/benchmark.py new file mode 100644 index 0000000..e08835e --- /dev/null +++ b/src/chameleon/benchmark.py @@ -0,0 +1,478 @@ +import unittest +import time +import os +import re +from .utils import text_ + +re_amp = re.compile(r'&(?!([A-Za-z]+|#[0-9]+);)') + +BIGTABLE_ZPT = """\ +<table xmlns="http://www.w3.org/1999/xhtml" +xmlns:tal="http://xml.zope.org/namespaces/tal"> +<tr tal:repeat="row python: options['table']"> +<td tal:repeat="c python: row.values()"> +<span tal:define="d python: c + 1" +tal:attributes="class python: 'column-' + str(d)" +tal:content="python: d" /> +</td> +</tr> +</table>""" + +MANY_STRINGS_ZPT = """\ +<table xmlns="http://www.w3.org/1999/xhtml" +xmlns:tal="http://xml.zope.org/namespaces/tal"> +<tr tal:repeat="i python: xrange(1000)"> +<td tal:content="string: number ${i}" /> +</tr> +</table> +""" + +HELLO_WORLD_ZPT = """\ +<html xmlns="http://www.w3.org/1999/xhtml" +xmlns:tal="http://xml.zope.org/namespaces/tal"> +<body> +<h1>Hello, world!</h1> +</body> +</html> +""" + +I18N_ZPT = """\ +<html xmlns="http://www.w3.org/1999/xhtml" +xmlns:tal="http://xml.zope.org/namespaces/tal" +xmlns:i18n="http://xml.zope.org/namespaces/i18n"> + <body> + <div tal:repeat="i python: xrange(10)"> + <div i18n:translate=""> + Hello world! + </div> + <div i18n:translate="hello_world"> + Hello world! + </div> + <div i18n:translate=""> + <sup>Hello world!</sup> + </div> + </div> + </body> +</html> +""" + + +def benchmark(title): + def decorator(f): + def wrapper(*args): + print( + "==========================\n " \ + "%s\n==========================" % \ + title) + return f(*args) + return wrapper + return decorator + + +def timing(func, *args, **kwargs): + t1 = t2 = time.time() + i = 0 + while t2 - t1 < 3: + func(**kwargs) + func(**kwargs) + func(**kwargs) + func(**kwargs) + i += 4 + t2 = time.time() + return float(10 * (t2 - t1)) / i + + +START = 0 +END = 1 +TAG = 2 + + +def yield_tokens(table=None): + index = [] + tag = index.append + _re_amp = re_amp + tag(START) + yield "<", "html", "", ">\n" + for r in table: + tag(START) + yield "<", "tr", "", ">\n" + + for c in r.values(): + d = c + 1 + tag(START) + yield "<", "td", "", ">\n" + + _tmp5 = d + if not isinstance(_tmp5, unicode): + _tmp5 = str(_tmp5) + if ('&' in _tmp5): + if (';' in _tmp5): + _tmp5 = _re_amp.sub('&', _tmp5) + else: + _tmp5 = _tmp5.replace('&', '&') + if ('<' in _tmp5): + _tmp5 = _tmp5.replace('<', '<') + if ('>' in _tmp5): + _tmp5 = _tmp5.replace('>', '>') + if ('"' in _tmp5): + _tmp5 = _tmp5.replace('"', '"') + _tmp5 = "column-%s" % _tmp5 + + _tmp = d + if (_tmp.__class__ not in (str, unicode, int, float, )): + raise + if (_tmp is not None): + if not isinstance(_tmp, unicode): + _tmp = str(_tmp) + if ('&' in _tmp): + if (';' in _tmp): + _tmp = _re_amp.sub('&', _tmp) + else: + _tmp = _tmp.replace('&', '&') + if ('<' in _tmp): + _tmp = _tmp.replace('<', '<') + if ('>' in _tmp): + _tmp = _tmp.replace('>', '>') + tag(START) + + t = ["classicism"] + + yield "<", "span", " ", t[0], '="', _tmp5, '"', ">\n" + tag(END) + yield "</", "span", ">\n" + tag(END) + yield "</", "td", ">\n" + tag(END) + yield "</", "tr", ">\n" + tag(END) + yield "</", "html", ">\n" + + +def yield_tokens_dict_version(**kwargs): + index = [] + tag = index.append + _re_amp = re_amp + tag(START) + yield "<", "html", "", ">\n" + + for r in kwargs['table']: + kwargs['r'] = r + tag(START) + yield "<", "tr", "", ">\n" + + for c in kwargs['r'].values(): + kwargs['d'] = c + 1 + tag(START) + yield "<", "td", "", ">\n" + + _tmp5 = kwargs['d'] + if not isinstance(_tmp5, unicode): + _tmp5 = str(_tmp5) + if ('&' in _tmp5): + if (';' in _tmp5): + _tmp5 = _re_amp.sub('&', _tmp5) + else: + _tmp5 = _tmp5.replace('&', '&') + if ('<' in _tmp5): + _tmp5 = _tmp5.replace('<', '<') + if ('>' in _tmp5): + _tmp5 = _tmp5.replace('>', '>') + if ('"' in _tmp5): + _tmp5 = _tmp5.replace('"', '"') + _tmp5 = "column-%s" % _tmp5 + + _tmp = kwargs['d'] + if (_tmp.__class__ not in (str, unicode, int, float, )): + raise + if (_tmp is not None): + if not isinstance(_tmp, unicode): + _tmp = str(_tmp) + if ('&' in _tmp): + if (';' in _tmp): + _tmp = _re_amp.sub('&', _tmp) + else: + _tmp = _tmp.replace('&', '&') + if ('<' in _tmp): + _tmp = _tmp.replace('<', '<') + if ('>' in _tmp): + _tmp = _tmp.replace('>', '>') + tag(START) + + t = ["classicism"] + + yield "<", "span", " ", t[0], '="', _tmp5, '"', ">\n" + tag(END) + yield "</", "span", ">\n" + tag(END) + yield "</", "td", ">\n" + tag(END) + yield "</", "tr", ">\n" + tag(END) + yield "</", "html", ">\n" + + +def yield_stream(table=None): + _re_amp = re_amp + yield START, ("html", "", "\n"), None + for r in table: + yield START, ("tr", "", "\n"), None + + for c in r.values(): + d = c + 1 + yield START, ("td", "", "\n"), None + + _tmp5 = d + if not isinstance(_tmp5, unicode): + _tmp5 = str(_tmp5) + if ('&' in _tmp5): + if (';' in _tmp5): + _tmp5 = _re_amp.sub('&', _tmp5) + else: + _tmp5 = _tmp5.replace('&', '&') + if ('<' in _tmp5): + _tmp5 = _tmp5.replace('<', '<') + if ('>' in _tmp5): + _tmp5 = _tmp5.replace('>', '>') + if ('"' in _tmp5): + _tmp5 = _tmp5.replace('"', '"') + _tmp5 = "column-%s" % _tmp5 + + _tmp = d + if (_tmp.__class__ not in (str, unicode, int, float, )): + raise + if (_tmp is not None): + if not isinstance(_tmp, unicode): + _tmp = str(_tmp) + if ('&' in _tmp): + if (';' in _tmp): + _tmp = _re_amp.sub('&', _tmp) + else: + _tmp = _tmp.replace('&', '&') + if ('<' in _tmp): + _tmp = _tmp.replace('<', '<') + if ('>' in _tmp): + _tmp = _tmp.replace('>', '>') + yield START, ("span", "", _tmp, " ", "class", _tmp5), None + + yield END, ("span", "", "\n"), None + yield END, ("td", "", "\n"), None + yield END, ("tr", "", "\n"), None + yield END, ("html", "", "\n"), None + +from itertools import chain + + +def bigtable_python_tokens(table=None, renderer=None): + iterable = renderer(table=table) + stream = chain(*iterable) + return "".join(stream) + + +def bigtable_python_stream(table=None, renderer=None): + stream = renderer(table=table) + return "".join(stream_output(stream)) + + +def bigtable_python_stream_with_filter(table=None, renderer=None): + stream = renderer(table=table) + return "".join(stream_output(uppercase_filter(stream))) + + +def uppercase_filter(stream): + for kind, data, pos in stream: + if kind is START: + data = (data[0], data[1], data[2].upper(),) + data[3:] + elif kind is END: + data = (data[0], data[1], data[2].upper()) + elif kind is TAG: + raise NotImplemented + yield kind, data, pos + + +def stream_output(stream): + for kind, data, pos in stream: + if kind is START: + tag = data[0] + yield "<%s" % tag + l = len(data) + + # optimize for common cases + if l == 3: + pass + elif l == 6: + yield '%s%s="%s"' % (data[3], data[4], data[5]) + else: + i = 3 + while i < l: + yield '%s%s="%s"' % (data[i], data[i + 1], data[i + 2]) + i += 3 + yield "%s>%s" % (data[1], data[2]) + elif kind is END: + yield "</%s%s>%s" % data + elif kind is TAG: + raise NotImplemented + + +class Benchmarks(unittest.TestCase): + table = [dict(a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9, j=10) \ + for x in range(1000)] + + def setUp(self): + # set up i18n component + from zope.i18n import translate + from zope.i18n.interfaces import INegotiator + from zope.i18n.interfaces import ITranslationDomain + from zope.i18n.negotiator import Negotiator + from zope.i18n.simpletranslationdomain import SimpleTranslationDomain + from zope.i18n.tests.test_negotiator import Env + from zope.tales.tales import Context + + self.env = Env(('klingon', 'da', 'en', 'fr', 'no')) + + class ZopeI18NContext(Context): + + def translate(self, msgid, domain=None, context=None, + mapping=None, default=None): + context = self.vars['options']['env'] + return translate(msgid, domain, mapping, + context=context, default=default) + + def _getContext(self, contexts=None, **kwcontexts): + if contexts is not None: + if kwcontexts: + kwcontexts.update(contexts) + else: + kwcontexts = contexts + return ZopeI18NContext(self, kwcontexts) + + def _pt_getEngineContext(namespace): + self = namespace['template'] + engine = self.pt_getEngine() + return _getContext(engine, namespace) + + import zope.component + zope.component.provideUtility(Negotiator(), INegotiator) + catalog = SimpleTranslationDomain('domain') + zope.component.provideUtility(catalog, ITranslationDomain, 'domain') + self.files = os.path.abspath(os.path.join(__file__, '..', 'input')) + + @staticmethod + def _chameleon(body, **kwargs): + from .zpt.template import PageTemplate + return PageTemplate(body, **kwargs) + + @staticmethod + def _zope(body): + from zope.pagetemplate.pagetemplatefile import PageTemplate + template = PageTemplate() + template.pt_edit(body, 'text/xhtml') + return template + + @benchmark(text_("BIGTABLE [python]")) + def test_bigtable(self): + options = {'table': self.table} + + t_chameleon = timing(self._chameleon(BIGTABLE_ZPT), options=options) + print("chameleon: %7.2f" % t_chameleon) + + t_chameleon_utf8 = timing( + self._chameleon(BIGTABLE_ZPT, encoding='utf-8'), options=options) + print("chameleon (utf-8): %7.2f" % t_chameleon_utf8) + + t_tokens = timing( + bigtable_python_tokens, table=self.table, renderer=yield_tokens) + print("token: %7.2f" % t_tokens) + + t_tokens_dict_version = timing( + bigtable_python_tokens, table=self.table, + renderer=yield_tokens_dict_version) + print("token (dict): %7.2f" % t_tokens_dict_version) + + t_stream = timing( + bigtable_python_stream, table=self.table, renderer=yield_stream) + print("stream: %7.2f" % t_stream) + + t_zope = timing(self._zope(BIGTABLE_ZPT), table=self.table) + print("zope.pagetemplate: %7.2f" % t_zope) + print(" %7.1fX" % (t_zope / t_chameleon)) + + print("--------------------------") + print("check: %d vs %d" % ( + len(self._chameleon(BIGTABLE_ZPT)(options=options)), + len(self._zope(BIGTABLE_ZPT)(table=self.table)))) + print("--------------------------") + + @benchmark(text_("MANY STRINGS [python]")) + def test_many_strings(self): + t_chameleon = timing(self._chameleon(MANY_STRINGS_ZPT)) + print("chameleon: %7.2f" % t_chameleon) + t_zope = timing(self._zope(MANY_STRINGS_ZPT)) + print("zope.pagetemplate: %7.2f" % t_zope) + print(" %7.1fX" % (t_zope / t_chameleon)) + + print("--------------------------") + print("check: %d vs %d" % ( + len(self._chameleon(MANY_STRINGS_ZPT)()), + len(self._zope(MANY_STRINGS_ZPT)()))) + print("--------------------------") + + @benchmark(text_("HELLO WORLD")) + def test_hello_world(self): + t_chameleon = timing(self._chameleon(HELLO_WORLD_ZPT)) * 1000 + print("chameleon: %7.2f" % t_chameleon) + t_zope = timing(self._zope(HELLO_WORLD_ZPT)) * 1000 + print("zope.pagetemplate: %7.2f" % t_zope) + print(" %7.1fX" % (t_zope / t_chameleon)) + + print("--------------------------") + print("check: %d vs %d" % ( + len(self._chameleon(HELLO_WORLD_ZPT)()), + len(self._zope(HELLO_WORLD_ZPT)()))) + print("--------------------------") + + @benchmark(text_("I18N")) + def test_i18n(self): + from zope.i18n import translate + t_chameleon = timing( + self._chameleon(I18N_ZPT), + translate=translate, + language="klingon") * 1000 + print("chameleon: %7.2f" % t_chameleon) + t_zope = timing(self._zope(I18N_ZPT), env=self.env) * 1000 + print("zope.pagetemplate: %7.2f" % t_zope) + print(" %7.1fX" % (t_zope / t_chameleon)) + + @benchmark(text_("COMPILATION")) + def test_compilation(self): + template = self._chameleon(HELLO_WORLD_ZPT) + + def chameleon_cook_and_render(template=template): + template.cook(HELLO_WORLD_ZPT) + template() + + t_chameleon = timing(chameleon_cook_and_render) * 1000 + print("chameleon: %7.2f" % t_chameleon) + + template = self._zope(HELLO_WORLD_ZPT) + + def zope_cook_and_render(templte=template): + template._cook() + template() + + t_zope = timing(zope_cook_and_render) * 1000 + print("zope.pagetemplate: %7.2f" % t_zope) + print(" %0.3fX" % (t_zope / t_chameleon)) + + +def start(): + result = unittest.TestResult() + test = unittest.makeSuite(Benchmarks) + test.run(result) + + for error in result.errors: + print("Error in %s...\n" % error[0]) + print(error[1]) + + for failure in result.failures: + print("Failure in %s...\n" % failure[0]) + print(failure[1]) diff --git a/src/chameleon/codegen.py b/src/chameleon/codegen.py new file mode 100644 index 0000000..6cac1a9 --- /dev/null +++ b/src/chameleon/codegen.py @@ -0,0 +1,234 @@ +try: + import ast +except ImportError: + from chameleon import ast25 as ast + +import inspect +import textwrap +import types +import copy + +try: + import __builtin__ as builtins +except ImportError: + import builtins + +reverse_builtin_map = {} +for name, value in builtins.__dict__.items(): + try: + hash(value) + except TypeError: + continue + + reverse_builtin_map[value] = name + +try: + basestring +except NameError: + basestring = str + +from .astutil import ASTCodeGenerator +from .astutil import load +from .astutil import store +from .astutil import parse +from .astutil import Builtin +from .astutil import Symbol +from .astutil import node_annotations + +from .exc import CompilationError + + +try: + NATIVE_NUMBERS = int, float, long, bool +except NameError: + NATIVE_NUMBERS = int, float, bool + + +def template(source, mode='exec', is_func=False, func_args=(), func_defaults=(), **kw): + def wrapper(*vargs, **kwargs): + symbols = dict(zip(args, vargs + defaults)) + symbols.update(kwargs) + + class Visitor(ast.NodeVisitor): + def visit_FunctionDef(self, node): + self.generic_visit(node) + + name = symbols.get(node.name, self) + if name is not self: + node_annotations[node] = ast.FunctionDef( + name=name, + args=node.args, + body=node.body, + decorator_list=getattr(node, "decorator_list", []), + ) + + def visit_Name(self, node): + value = symbols.get(node.id, self) + if value is not self: + if isinstance(value, basestring): + value = load(value) + if isinstance(value, type) or value in reverse_builtin_map: + name = reverse_builtin_map.get(value) + if name is not None: + value = Builtin(name) + else: + value = Symbol(value) + + assert node not in node_annotations + assert hasattr(value, '_fields') + node_annotations[node] = value + + expr = parse(textwrap.dedent(source), mode=mode) + + Visitor().visit(expr) + return expr.body + + assert isinstance(source, basestring) + defaults = func_defaults + args = func_args + if is_func: + return wrapper + else: + return wrapper(**kw) + + +class TemplateCodeGenerator(ASTCodeGenerator): + """Extends the standard Python code generator class with handlers + for the helper node classes: + + - Symbol (an importable value) + - Static (value that can be made global) + - Builtin (from the builtins module) + - Marker (short-hand for a unique static object) + + """ + + names = () + + def __init__(self, tree, source=None): + self.imports = {} + self.defines = {} + self.markers = {} + self.source = source + self.tokens = [] + + # Generate code + super(TemplateCodeGenerator, self).__init__(tree) + + def visit_Module(self, node): + super(TemplateCodeGenerator, self).visit_Module(node) + + # Make sure we terminate the line printer + self.flush() + + # Clear lines array for import visits + body = self.lines + self.lines = [] + + while self.defines: + name, node = self.defines.popitem() + assignment = ast.Assign(targets=[store(name)], value=node) + self.visit(assignment) + + # Make sure we terminate the line printer + self.flush() + + # Clear lines array for import visits + defines = self.lines + self.lines = [] + + while self.imports: + value, node = self.imports.popitem() + + if isinstance(value, types.ModuleType): + stmt = ast.Import( + names=[ast.alias(name=value.__name__, asname=node.id)]) + elif hasattr(value, '__name__'): + path = reverse_builtin_map.get(value) + if path is None: + path = value.__module__ + name = value.__name__ + stmt = ast.ImportFrom( + module=path, + names=[ast.alias(name=name, asname=node.id)], + level=0, + ) + else: + raise TypeError(value) + + self.visit(stmt) + + # Clear last import + self.flush() + + # Stich together lines + self.lines += defines + body + + def define(self, name, node): + assert node is not None + value = self.defines.get(name) + + if value is node: + pass + elif value is None: + self.defines[name] = node + else: + raise CompilationError( + "Duplicate symbol name for define.", name) + + return load(name) + + def require(self, value): + if value is None: + return load("None") + + if isinstance(value, NATIVE_NUMBERS): + return ast.Num(value) + + node = self.imports.get(value) + if node is None: + # we come up with a unique symbol based on the class name + name = "_%s" % getattr(value, '__name__', str(value)).\ + rsplit('.', 1)[-1] + node = load(name) + self.imports[value] = store(node.id) + + return node + + def visit(self, node): + annotation = node_annotations.get(node) + if annotation is None: + super(TemplateCodeGenerator, self).visit(node) + else: + self.visit(annotation) + + def visit_Comment(self, node): + if node.stmt is None: + self._new_line() + else: + self.visit(node.stmt) + + for line in node.text.replace('\r', '\n').split('\n'): + self._new_line() + self._write("%s#%s" % (node.space, line)) + + def visit_Builtin(self, node): + name = load(node.id) + self.visit(name) + + def visit_Symbol(self, node): + node = self.require(node.value) + self.visit(node) + + def visit_Static(self, node): + if node.name is None: + name = "_static_%s" % str(id(node.value)).replace('-', '_') + else: + name = node.name + + node = self.define(name, node.value) + self.visit(node) + + def visit_TokenRef(self, node): + self.tokens.append((node.pos, node.length)) + super(TemplateCodeGenerator, self).visit(ast.Num(n=node.pos)) diff --git a/src/chameleon/compiler.py b/src/chameleon/compiler.py new file mode 100644 index 0000000..5a3569b --- /dev/null +++ b/src/chameleon/compiler.py @@ -0,0 +1,1749 @@ +import re +import sys +import itertools +import logging +import threading +import functools +import collections +import pickle +import textwrap + +from .astutil import load +from .astutil import store +from .astutil import param +from .astutil import swap +from .astutil import subscript +from .astutil import node_annotations +from .astutil import annotated +from .astutil import NameLookupRewriteVisitor +from .astutil import Comment +from .astutil import Symbol +from .astutil import Builtin +from .astutil import Static +from .astutil import TokenRef + +from .codegen import TemplateCodeGenerator +from .codegen import template + +from .tal import ErrorInfo +from .tal import NAME +from .i18n import simple_translate + +from .nodes import Text +from .nodes import Value +from .nodes import Substitution +from .nodes import Assignment +from .nodes import Module +from .nodes import Context + +from .tokenize import Token +from .config import DEBUG_MODE +from .exc import TranslationError +from .exc import ExpressionError +from .parser import groupdict + +from .utils import DebuggingOutputStream +from .utils import char2entity +from .utils import ListDictProxy +from .utils import native_string +from .utils import byte_string +from .utils import string_type +from .utils import unicode_string +from .utils import version +from .utils import ast +from .utils import safe_native +from .utils import builtins +from .utils import decode_htmlentities + +if version >= (3, 0, 0): + long = int + +log = logging.getLogger('chameleon.compiler') + +COMPILER_INTERNALS_OR_DISALLOWED = set([ + "econtext", + "rcontext", + "str", + "int", + "float", + "long", + "len", + "None", + "True", + "False", + "RuntimeError", + ]) + + +RE_MANGLE = re.compile(r'[^\w_]') +RE_NAME = re.compile('^%s$' % NAME) + +if DEBUG_MODE: + LIST = template("cls()", cls=DebuggingOutputStream, mode="eval") +else: + LIST = template("[]", mode="eval") + + +def identifier(prefix, suffix=None): + return "__%s_%s" % (prefix, mangle(suffix or id(prefix))) + + +def mangle(string): + return RE_MANGLE.sub( + '_', unicode_string(string) + ).replace('\n', '').replace('-', '_') + + +def load_econtext(name): + return template("getitem(KEY)", KEY=ast.Str(s=name), mode="eval") + + +def store_econtext(name): + name = native_string(name) + return subscript(name, load("econtext"), ast.Store()) + + +def store_rcontext(name): + name = native_string(name) + return subscript(name, load("rcontext"), ast.Store()) + + +def set_token(stmts, token): + pos = getattr(token, "pos", 0) + body = template("__token = pos", pos=TokenRef(pos, len(token))) + return body + stmts + + +def eval_token(token): + try: + line, column = token.location + filename = token.filename + except AttributeError: + line, column = 0, 0 + filename = "<string>" + + string = safe_native(token) + + return template( + "(string, line, col)", + string=ast.Str(s=string), + line=ast.Num(n=line), + col=ast.Num(n=column), + mode="eval" + ) + + +emit_node = template(is_func=True, func_args=('node',), source=r""" + __append(node)""") + + +emit_node_if_non_trivial = template(is_func=True, func_args=('node',), + source=r""" + if node is not None: + __append(node) +""") + + +emit_bool = template(is_func=True, + func_args=('target', 's', 'default_marker', 'default'), + func_defaults=(None, None), source=r""" + if target is default_marker: + target = default + elif target: + target = s + else: + target = None""") + + +emit_convert = template(is_func=True, + func_args=('target', 'encoded', 'str', 'long', 'type', + 'default_marker', 'default'), + func_defaults=(byte_string, unicode_string, long, type, + None), + source=r""" + if target is None: + pass + elif target is default_marker: + target = default + else: + __tt = type(target) + + if __tt is int or __tt is float or __tt is long: + target = str(target) + elif __tt is encoded: + target = decode(target) + elif __tt is not str: + try: + target = target.__html__ + except AttributeError: + __converted = convert(target) + target = str(target) if target is __converted else __converted + else: + target = target()""") + + +emit_func_convert = template(is_func=True, + func_args=('func', 'encoded', 'str','long','type'), + func_defaults=(byte_string, unicode_string, long, + type), + source=r""" + def func(target): + if target is None: + return + + __tt = type(target) + + if __tt is int or __tt is float or __tt is long: + target = str(target) + + elif __tt is encoded: + target = decode(target) + + elif __tt is not str: + try: + target = target.__html__ + except AttributeError: + __converted = convert(target) + target = str(target) if target is __converted else __converted + else: + target = target() + + return target""") + + +emit_translate = template(is_func=True, + func_args=('target', 'msgid', 'target_language', + 'default'), + func_defaults=(None,), + source=r""" + target = translate(msgid, default=default, domain=__i18n_domain, + context=__i18n_context, + target_language=target_language)""") + + +emit_func_convert_and_escape = template( + is_func=True, + func_args=('func', 'str', 'long', 'type', 'encoded'), + func_defaults=(unicode_string, long, type, byte_string,), + source=r""" + def func(target, quote, quote_entity, default, default_marker): + if target is None: + return + + if target is default_marker: + return default + + __tt = type(target) + + if __tt is int or __tt is float or __tt is long: + target = str(target) + else: + if __tt is encoded: + target = decode(target) + elif __tt is not str: + try: + target = target.__html__ + except: + __converted = convert(target) + target = str(target) if target is __converted \ + else __converted + else: + return target() + + if target is not None: + try: + escape = __re_needs_escape(target) is not None + except TypeError: + pass + else: + if escape: + # Character escape + if '&' in target: + target = target.replace('&', '&') + if '<' in target: + target = target.replace('<', '<') + if '>' in target: + target = target.replace('>', '>') + if quote is not None and quote in target: + target = target.replace(quote, quote_entity) + + return target""") + + +class Interpolator(object): + braces_required_regex = re.compile( + r'(\$)?\$({(?P<expression>.*)})', + re.DOTALL) + + braces_optional_regex = re.compile( + r'(\$)?\$({(?P<expression>.*)}|(?P<variable>[A-Za-z][A-Za-z0-9_]*))', + re.DOTALL) + + def __init__(self, expression, braces_required, translate=False, + decode_htmlentities=False): + self.expression = expression + self.regex = self.braces_required_regex if braces_required else \ + self.braces_optional_regex + self.translate = translate + self.decode_htmlentities = decode_htmlentities + + def __call__(self, name, engine): + """The strategy is to find possible expression strings and + call the ``validate`` function of the parser to validate. + + For every possible starting point, the longest possible + expression is tried first, then the second longest and so + forth. + + Example 1: + + ${'expressions use the ${<expression>} format'} + + The entire expression is attempted first and it is also the + only one that validates. + + Example 2: + + ${'Hello'} ${'world!'} + + Validation of the longest possible expression (the entire + string) will fail, while the second round of attempts, + ``${'Hello'}`` and ``${'world!'}`` respectively, validate. + + """ + + body = [] + nodes = [] + text = self.expression + + expr_map = {} + translate = self.translate + + while text: + matched = text + m = self.regex.search(matched) + if m is None: + text = text.replace('$$', '$') + nodes.append(ast.Str(s=text)) + break + + part = text[:m.start()] + text = text[m.start():] + + skip = text.startswith('$$') + if skip: + part = part + '$' + + if part: + part = part.replace('$$', '$') + node = ast.Str(s=part) + nodes.append(node) + + if skip: + text = text[2:] + continue + + if not body: + target = name + else: + target = store("%s_%d" % (name.id, text.pos)) + + while True: + d = groupdict(m, matched) + string = d["expression"] or d.get("variable") or "" + + if self.decode_htmlentities: + string = decode_htmlentities(string) + + if string: + try: + compiler = engine.parse(string) + body += compiler.assign_text(target) + except ExpressionError: + matched = matched[m.start():m.end() - 1] + m = self.regex.search(matched) + if m is None: + raise + + continue + else: + s = m.group() + assign = ast.Assign(targets=[target], value=ast.Str(s=s)) + body += [assign] + + break + + # If one or more expressions are not simple names, we + # disable translation. + if RE_NAME.match(string) is None: + translate = False + + # if this is the first expression, use the provided + # assignment name; otherwise, generate one (here based + # on the string position) + node = load(target.id) + nodes.append(node) + + expr_map[node] = safe_native(string) + + text = text[len(m.group()):] + + if len(nodes) == 1: + target = nodes[0] + + if translate and isinstance(target, ast.Str): + target = template( + "translate(msgid, domain=__i18n_domain, context=__i18n_context, target_language=target_language)", + msgid=target, mode="eval", + target_language=load("target_language"), + ) + else: + if translate: + formatting_string = "" + keys = [] + values = [] + + for node in nodes: + if isinstance(node, ast.Str): + formatting_string += node.s + else: + string = expr_map[node] + formatting_string += "${%s}" % string + keys.append(ast.Str(s=string)) + values.append(node) + + target = template( + "translate(msgid, mapping=mapping, domain=__i18n_domain, context=__i18n_context, target_language=target_language)", + msgid=ast.Str(s=formatting_string), + target_language=load("target_language"), + mapping=ast.Dict(keys=keys, values=values), + mode="eval" + ) + else: + nodes = [ + node if isinstance(node, ast.Str) else + template( + "NODE if NODE is not None else ''", + NODE=node, mode="eval" + ) + for node in nodes + ] + + target = ast.BinOp( + left=ast.Str(s="%s" * len(nodes)), + op=ast.Mod(), + right=ast.Tuple(elts=nodes, ctx=ast.Load())) + + body += [ast.Assign(targets=[name], value=target)] + return body + + +class ExpressionEngine(object): + """Expression engine. + + This test demonstrates how to configure and invoke the engine. + + >>> from chameleon import tales + >>> parser = tales.ExpressionParser({ + ... 'python': tales.PythonExpr, + ... 'not': tales.NotExpr, + ... 'exists': tales.ExistsExpr, + ... 'string': tales.StringExpr, + ... }, 'python') + + >>> engine = ExpressionEngine(parser) + + An expression evaluation function: + + >>> eval = lambda expression: tales.test( + ... tales.IdentityExpr(expression), engine) + + We have provided 'python' as the default expression type. This + means that when no prefix is given, the expression is evaluated as + a Python expression: + + >>> eval('not False') + True + + Note that the ``type`` prefixes bind left. If ``not`` and + ``exits`` are two expression type prefixes, consider the + following:: + + >>> eval('not: exists: int(None)') + True + + The pipe operator binds right. In the following example, but + arguments are evaluated against ``not: exists: ``. + + >>> eval('not: exists: help') + False + """ + + supported_char_escape_set = set(('&', '<', '>')) + + def __init__(self, parser, char_escape=(), + default=None, default_marker=None): + self._parser = parser + self._char_escape = char_escape + self._default = default + self._default_marker = default_marker + + def __call__(self, string, target): + # BBB: This method is deprecated. Instead, a call should first + # be made to ``parse`` and then one of the assignment methods + # ("value" or "text"). + + compiler = self.parse(string) + return compiler(string, target) + + def parse(self, string, handle_errors=True, char_escape=None): + expression = self._parser(string) + compiler = self.get_compiler(expression, string, handle_errors, char_escape) + return ExpressionCompiler(compiler, self) + + def get_compiler(self, expression, string, handle_errors, char_escape): + if char_escape is None: + char_escape = self._char_escape + def compiler(target, engine, result_type=None, *args): + stmts = expression(target, engine) + + if result_type is not None: + method = getattr(self, '_convert_%s' % result_type) + steps = method(target, char_escape, *args) + stmts.extend(steps) + + if handle_errors: + return set_token(stmts, string.strip()) + + return stmts + + return compiler + + def _convert_bool(self, target, char_escape, s): + """Converts value given by ``target`` to a string ``s`` if the + target is a true value, otherwise ``None``. + """ + + return emit_bool( + target, ast.Str(s=s), + default=self._default, + default_marker=self._default_marker + ) + + def _convert_structure(self, target, char_escape): + """Converts value given by ``target`` to structure output.""" + + return emit_convert( + target, + default=self._default, + default_marker=self._default_marker, + ) + + def _convert_text(self, target, char_escape): + """Converts value given by ``target`` to text.""" + + if not char_escape: + return self._convert_structure(target, char_escape) + + # This is a cop-out - we really only support a very select + # set of escape characters + other = set(char_escape) - self.supported_char_escape_set + + if other: + for supported in '"', '\'', '': + if supported in char_escape: + quote = supported + break + else: + raise RuntimeError( + "Unsupported escape set: %s." % repr(char_escape) + ) + else: + quote = '\0' + + entity = char2entity(quote or '\0') + + return template( + "TARGET = __quote(TARGET, QUOTE, Q_ENTITY, DEFAULT, MARKER)", + TARGET=target, + QUOTE=ast.Str(s=quote), + Q_ENTITY=ast.Str(s=entity), + DEFAULT=self._default, + MARKER=self._default_marker, + ) + + +class ExpressionCompiler(object): + def __init__(self, compiler, engine): + self.compiler = compiler + self.engine = engine + + def assign_bool(self, target, s): + return self.compiler(target, self.engine, "bool", s) + + def assign_text(self, target): + return self.compiler(target, self.engine, "text") + + def assign_value(self, target): + return self.compiler(target, self.engine) + + +class ExpressionEvaluator(object): + """Evaluates dynamic expression. + + This is not particularly efficient, but supported for legacy + applications. + + >>> from chameleon import tales + >>> parser = tales.ExpressionParser({'python': tales.PythonExpr}, 'python') + >>> engine = functools.partial(ExpressionEngine, parser) + + >>> evaluate = ExpressionEvaluator(engine, { + ... 'foo': 'bar', + ... }) + + The evaluation function is passed the local and remote context, + the expression type and finally the expression. + + >>> evaluate({'boo': 'baz'}, {}, 'python', 'foo + boo') + 'barbaz' + + The cache is now primed: + + >>> evaluate({'boo': 'baz'}, {}, 'python', 'foo + boo') + 'barbaz' + + Note that the call method supports currying of the expression + argument: + + >>> python = evaluate({'boo': 'baz'}, {}, 'python') + >>> python('foo + boo') + 'barbaz' + + """ + + __slots__ = "_engine", "_cache", "_names", "_builtins" + + def __init__(self, engine, builtins): + self._engine = engine + self._names, self._builtins = zip(*builtins.items()) + self._cache = {} + + def __call__(self, econtext, rcontext, expression_type, string=None): + if string is None: + return functools.partial( + self.__call__, econtext, rcontext, expression_type + ) + + expression = "%s:%s" % (expression_type, string) + + try: + evaluate = self._cache[expression] + except KeyError: + assignment = Assignment(["_result"], expression, True) + module = Module("evaluate", Context(assignment)) + + compiler = Compiler( + self._engine, module, "<string>", string, + ('econtext', 'rcontext') + self._names + ) + + env = {} + exec(compiler.code, env) + evaluate = self._cache[expression] = env["evaluate"] + + evaluate(econtext, rcontext, *self._builtins) + return econtext['_result'] + + +class NameTransform(object): + """ + >>> nt = NameTransform( + ... set(('foo', 'bar', )), {'boo': 'boz'}, + ... ('econtext', ), + ... ) + + >>> def test(node): + ... rewritten = nt(node) + ... module = ast.Module([ast.fix_missing_locations(rewritten)]) + ... codegen = TemplateCodeGenerator(module) + ... return codegen.code + + Any odd name: + + >>> test(load('frobnitz')) + "getitem('frobnitz')" + + A 'builtin' name will first be looked up via ``get`` allowing fall + back to the global builtin value: + + >>> test(load('foo')) + "get('foo', foo)" + + Internal names (with two leading underscores) are left alone: + + >>> test(load('__internal')) + '__internal' + + Compiler internals or disallowed names: + + >>> test(load('econtext')) + 'econtext' + + Aliased names: + + >>> test(load('boo')) + 'boz' + + """ + + def __init__(self, builtins, aliases, internals): + self.builtins = builtins + self.aliases = aliases + self.internals = internals + + def __call__(self, node): + name = node.id + + # Don't rewrite names that begin with an underscore; they are + # internal and can be assumed to be locally defined. This + # policy really should be part of the template program, not + # defined here in the compiler. + if name.startswith('__') or name in self.internals: + return node + + if isinstance(node.ctx, ast.Store): + return store_econtext(name) + + aliased = self.aliases.get(name) + if aliased is not None: + return load(aliased) + + # If the name is a Python global, first try acquiring it from + # the dynamic context, then fall back to the global. + if name in self.builtins: + return template( + "get(key, name)", + mode="eval", + key=ast.Str(s=name), + name=load(name), + ) + + # Otherwise, simply acquire it from the dynamic context. + return load_econtext(name) + + +class ExpressionTransform(object): + """Internal wrapper to transform expression nodes into assignment + statements. + + The node input may use the provided expression engine, but other + expression node types are supported such as ``Builtin`` which + simply resolves a built-in name. + + Used internally be the compiler. + """ + + loads_symbol = Symbol(pickle.loads) + + def __init__(self, engine_factory, cache, visitor, strict=True): + self.engine_factory = engine_factory + self.cache = cache + self.strict = strict + self.visitor = visitor + + def __call__(self, expression, target): + if isinstance(target, string_type): + target = store(target) + + try: + stmts = self.translate(expression, target) + except ExpressionError: + if self.strict: + raise + + exc = sys.exc_info()[1] + p = pickle.dumps(exc, -1) + + stmts = template( + "__exc = loads(p)", loads=self.loads_symbol, p=ast.Str(s=p) + ) + + stmts += set_token([ast.Raise(exc=load("__exc"))], exc.token) + + # Apply visitor to each statement + for stmt in stmts: + self.visitor(stmt) + + return stmts + + def translate(self, expression, target): + if isinstance(target, string_type): + target = store(target) + + cached = self.cache.get(expression) + + if cached is not None: + stmts = [ast.Assign(targets=[target], value=cached)] + elif isinstance(expression, ast.expr): + stmts = [ast.Assign(targets=[target], value=expression)] + else: + # The engine interface supports simple strings, which + # default to expression nodes + if isinstance(expression, string_type): + expression = Value(expression, True) + + kind = type(expression).__name__ + visitor = getattr(self, "visit_%s" % kind) + stmts = visitor(expression, target) + + # Add comment + target_id = getattr(target, "id", target) + comment = Comment(" %r -> %s" % (expression, target_id)) + stmts.insert(0, comment) + + return stmts + + def visit_Value(self, node, target): + engine = self.engine_factory() + compiler = engine.parse(node.value) + return compiler.assign_value(target) + + def visit_Copy(self, node, target): + return self.translate(node.expression, target) + + def visit_Default(self, node, target): + value = annotated(node.marker) + return [ast.Assign(targets=[target], value=value)] + + def visit_Substitution(self, node, target): + engine = self.engine_factory(default=node.default) + compiler = engine.parse(node.value, char_escape=node.char_escape) + return compiler.assign_text(target) + + def visit_Negate(self, node, target): + return self.translate(node.value, target) + \ + template("TARGET = not TARGET", TARGET=target) + + def visit_Identity(self, node, target): + expression = self.translate(node.expression, "__expression") + value = self.translate(node.value, "__value") + + return expression + value + \ + template("TARGET = __expression is __value", TARGET=target) + + def visit_Equality(self, node, target): + expression = self.translate(node.expression, "__expression") + value = self.translate(node.value, "__value") + + return expression + value + \ + template("TARGET = __expression == __value", TARGET=target) + + def visit_Boolean(self, node, target): + engine = self.engine_factory() + compiler = engine.parse(node.value) + return compiler.assign_bool(target, node.s) + + def visit_Interpolation(self, node, target): + expr = node.value + if isinstance(expr, Substitution): + engine = self.engine_factory( + char_escape=expr.char_escape, + default=expr.default, + ) + elif isinstance(expr, Value): + engine = self.engine_factory() + else: + raise RuntimeError("Bad value: %r." % node.value) + + interpolator = Interpolator( + expr.value, node.braces_required, + translate=node.translation, + decode_htmlentities=True + ) + + compiler = engine.get_compiler( + interpolator, expr.value, True, () + ) + return compiler(target, engine, "text") + + def visit_Translate(self, node, target): + if node.msgid is not None: + msgid = ast.Str(s=node.msgid) + else: + msgid = target + return self.translate(node.node, target) + \ + emit_translate( + target, msgid, "target_language", + default=target + ) + + def visit_Static(self, node, target): + value = annotated(node) + return [ast.Assign(targets=[target], value=value)] + + def visit_Builtin(self, node, target): + value = annotated(node) + return [ast.Assign(targets=[target], value=value)] + + +class Compiler(object): + """Generic compiler class. + + Iterates through nodes and yields Python statements which form a + template program. + """ + + exceptions = NameError, \ + ValueError, \ + AttributeError, \ + LookupError, \ + TypeError + + defaults = { + 'translate': Symbol(simple_translate), + 'decode': Builtin("str"), + 'convert': Builtin("str"), + 'on_error_handler': Builtin("str") + } + + lock = threading.Lock() + + global_builtins = set(builtins.__dict__) + + def __init__(self, engine_factory, node, filename, source, + builtins={}, strict=True): + self._scopes = [set()] + self._expression_cache = {} + self._translations = [] + self._builtins = builtins + self._aliases = [{}] + self._macros = [] + self._current_slot = [] + + internals = COMPILER_INTERNALS_OR_DISALLOWED | \ + set(self.defaults) + + transform = NameTransform( + self.global_builtins | set(builtins), + ListDictProxy(self._aliases), + internals, + ) + + self._visitor = visitor = NameLookupRewriteVisitor(transform) + + self._engine = ExpressionTransform( + engine_factory, + self._expression_cache, + visitor, + strict=strict, + ) + + if isinstance(node_annotations, dict): + self.lock.acquire() + backup = node_annotations.copy() + else: + backup = None + + try: + module = ast.Module([]) + module.body += self.visit(node) + ast.fix_missing_locations(module) + prelude = "__filename = %r\n__default = object()" % filename + generator = TemplateCodeGenerator(module, source) + tokens = [ + Token(source[pos:pos + length], pos, source) + for pos, length in generator.tokens + ] + token_map_def = "__tokens = {" + ", ".join("%d: %r" % ( + token.pos, + (token, ) + token.location + ) for token in tokens) + "}" + finally: + if backup is not None: + node_annotations.clear() + node_annotations.update(backup) + self.lock.release() + + self.code = "\n".join(( + prelude, + token_map_def, + generator.code + )) + + def visit(self, node): + if node is None: + return () + kind = type(node).__name__ + visitor = getattr(self, "visit_%s" % kind) + iterator = visitor(node) + return list(iterator) + + def visit_Sequence(self, node): + for item in node.items: + for stmt in self.visit(item): + yield stmt + + def visit_Element(self, node): + for stmt in self.visit(node.start): + yield stmt + + for stmt in self.visit(node.content): + yield stmt + + if node.end is not None: + for stmt in self.visit(node.end): + yield stmt + + def visit_Module(self, node): + body = [] + + body += template("import re") + body += template("import functools") + body += template("from itertools import chain as __chain") + if version < (3, 0, 0): + body += template("from sys import exc_clear as __exc_clear") + body += template("__marker = object()") + body += template( + r"g_re_amp = re.compile(r'&(?!([A-Za-z]+|#[0-9]+);)')" + ) + body += template( + r"g_re_needs_escape = re.compile(r'[&<>\"\']').search") + + body += template( + r"__re_whitespace = " + r"functools.partial(re.compile('\\s+').sub, ' ')", + ) + + # Visit module content + program = self.visit(node.program) + + body += [ast.FunctionDef( + name=node.name, args=ast.arguments( + args=[param(b) for b in self._builtins], + defaults=(), + ), + body=program + )] + + return body + + def visit_MacroProgram(self, node): + functions = [] + + # Visit defined macros + macros = getattr(node, "macros", ()) + names = [] + for macro in macros: + stmts = self.visit(macro) + function = stmts[-1] + names.append(function.name) + functions += stmts + + # Return function dictionary + functions += [ast.Return(value=ast.Dict( + keys=[ast.Str(s=name) for name in names], + values=[load(name) for name in names], + ))] + + return functions + + def visit_Context(self, node): + return template("getitem = econtext.__getitem__") + \ + template("get = econtext.get") + \ + self.visit(node.node) + + def visit_Macro(self, node): + body = [] + + # Initialization + body += template("__append = __stream.append") + body += template("__re_amp = g_re_amp") + body += template("__token = None") + body += template("__re_needs_escape = g_re_needs_escape") + + body += emit_func_convert("__convert") + body += emit_func_convert_and_escape("__quote") + + # Resolve defaults + for name in self.defaults: + body += template( + "NAME = econtext[KEY]", + NAME=name, KEY=ast.Str(s="__" + name) + ) + + # Internal set of defined slots + self._slots = set() + + # Visit macro body + nodes = itertools.chain(*tuple(map(self.visit, node.body))) + + # Slot resolution + for name in self._slots: + body += template( + "try: NAME = econtext[KEY].pop()\n" + "except: NAME = None", + KEY=ast.Str(s=name), NAME=store(name)) + + exc = template( + "exc_info()[1]", exc_info=Symbol(sys.exc_info), mode="eval" + ) + + exc_handler = template( + "if pos is not None: rcontext.setdefault('__error__', [])." + "append(token + (__filename, exc, ))", + exc=exc, + token=template("__tokens[pos]", pos="__token", mode="eval"), + pos="__token" + ) + template("raise") + + # Wrap visited nodes in try-except error handler. + body += [ + ast.TryExcept( + body=nodes, + handlers=[ast.ExceptHandler(body=exc_handler)] + ) + ] + + function_name = "render" if node.name is None else \ + "render_%s" % mangle(node.name) + + function = ast.FunctionDef( + name=function_name, args=ast.arguments( + args=[ + param("__stream"), + param("econtext"), + param("rcontext"), + param("__i18n_domain"), + param("__i18n_context"), + ], + defaults=[load("None"), load("None")], + ), + body=body + ) + + yield function + + def visit_Text(self, node): + return emit_node(ast.Str(s=node.value)) + + def visit_Domain(self, node): + backup = "__previous_i18n_domain_%s" % mangle(id(node)) + return template("BACKUP = __i18n_domain", BACKUP=backup) + \ + template("__i18n_domain = NAME", NAME=ast.Str(s=node.name)) + \ + self.visit(node.node) + \ + template("__i18n_domain = BACKUP", BACKUP=backup) + + def visit_TxContext(self, node): + backup = "__previous_i18n_context_%s" % mangle(id(node)) + return template("BACKUP = __i18n_context", BACKUP=backup) + \ + template("__i18n_context = NAME", NAME=ast.Str(s=node.name)) + \ + self.visit(node.node) + \ + template("__i18n_context = BACKUP", BACKUP=backup) + + def visit_OnError(self, node): + body = [] + + fallback = identifier("__fallback") + body += template("fallback = len(__stream)", fallback=fallback) + + self._enter_assignment((node.name, )) + fallback_body = self.visit(node.fallback) + self._leave_assignment((node.name, )) + + error_assignment = template( + "econtext[key] = cls(__exc, __tokens[__token][1:3])\n" + "if handler is not None: handler(__exc)", + cls=ErrorInfo, + handler=load("on_error_handler"), + key=ast.Str(s=node.name), + ) + + body += [ast.TryExcept( + body=self.visit(node.node), + handlers=[ast.ExceptHandler( + type=ast.Tuple(elts=[Builtin("Exception")], ctx=ast.Load()), + name=store("__exc"), + body=(error_assignment + \ + template("del __stream[fallback:]", fallback=fallback) + \ + fallback_body + ), + )] + )] + + return body + + def visit_Content(self, node): + name = "__content" + body = self._engine(node.expression, store(name)) + + if node.translate: + body += emit_translate( + name, name, load_econtext("target_language") + ) + + if node.char_escape: + body += template( + "NAME=__quote(NAME, None, '\255', None, None)", + NAME=name, + ) + else: + body += template("NAME = __convert(NAME)", NAME=name) + + body += template("if NAME is not None: __append(NAME)", NAME=name) + + return body + + def visit_Interpolation(self, node): + name = identifier("content") + return self._engine(node, name) + \ + emit_node_if_non_trivial(name) + + def visit_Alias(self, node): + assert len(node.names) == 1 + name = node.names[0] + target = self._aliases[-1][name] = identifier(name, id(node)) + return self._engine(node.expression, target) + + def visit_Assignment(self, node): + for name in node.names: + if name in COMPILER_INTERNALS_OR_DISALLOWED: + raise TranslationError( + "Name disallowed by compiler.", name + ) + + if name.startswith('__'): + raise TranslationError( + "Name disallowed by compiler (double underscore).", + name + ) + + assignment = self._engine(node.expression, store("__value")) + + if len(node.names) != 1: + target = ast.Tuple( + elts=[store_econtext(name) for name in node.names], + ctx=ast.Store(), + ) + else: + target = store_econtext(node.names[0]) + + assignment.append(ast.Assign(targets=[target], value=load("__value"))) + + for name in node.names: + if not node.local: + assignment += template( + "rcontext[KEY] = __value", KEY=ast.Str(s=native_string(name)) + ) + + return assignment + + def visit_Define(self, node): + scope = set(self._scopes[-1]) + self._scopes.append(scope) + self._aliases.append(self._aliases[-1].copy()) + + for assignment in node.assignments: + if assignment.local: + for stmt in self._enter_assignment(assignment.names): + yield stmt + + for stmt in self.visit(assignment): + yield stmt + + for stmt in self.visit(node.node): + yield stmt + + for assignment in node.assignments: + if assignment.local: + for stmt in self._leave_assignment(assignment.names): + yield stmt + + self._scopes.pop() + self._aliases.pop() + + def visit_Omit(self, node): + return self.visit_Condition(node) + + def visit_Condition(self, node): + target = "__condition" + assignment = self._engine(node.expression, target) + + assert assignment + + for stmt in assignment: + yield stmt + + body = self.visit(node.node) or [ast.Pass()] + + orelse = getattr(node, "orelse", None) + if orelse is not None: + orelse = self.visit(orelse) + + test = load(target) + + yield ast.If(test=test, body=body, orelse=orelse) + + def visit_Translate(self, node): + """Translation. + + Visit items and assign output to a default value. + + Finally, compile a translation expression and use either + result or default. + """ + + body = [] + + # Track the blocks of this translation + self._translations.append(set()) + + # Prepare new stream + append = identifier("append", id(node)) + stream = identifier("stream", id(node)) + body += template("s = new_list", s=stream, new_list=LIST) + \ + template("a = s.append", a=append, s=stream) + + # Visit body to generate the message body + code = self.visit(node.node) + swap(ast.Suite(body=code), load(append), "__append") + swap(ast.Suite(body=code), load(stream), "__stream") + body += code + + # Reduce white space and assign as message id + msgid = identifier("msgid", id(node)) + body += template( + "msgid = __re_whitespace(''.join(stream)).strip()", + msgid=msgid, stream=stream + ) + + default = msgid + + # Compute translation block mapping if applicable + names = self._translations[-1] + if names: + keys = [] + values = [] + + for name in names: + stream, append = self._get_translation_identifiers(name) + keys.append(ast.Str(s=name)) + values.append(load(stream)) + + # Initialize value + body.insert( + 0, ast.Assign( + targets=[store(stream)], + value=ast.Str(s=native_string("")))) + + mapping = ast.Dict(keys=keys, values=values) + else: + mapping = None + + # if this translation node has a name, use it as the message id + if node.msgid: + msgid = ast.Str(s=node.msgid) + + # emit the translation expression + body += template( + "if msgid: __append(translate(" + "msgid, mapping=mapping, default=default, domain=__i18n_domain, context=__i18n_context, target_language=target_language))", + msgid=msgid, default=default, mapping=mapping, + target_language=load_econtext("target_language") + ) + + # pop away translation block reference + self._translations.pop() + + return body + + def visit_Start(self, node): + try: + line, column = node.prefix.location + except AttributeError: + line, column = 0, 0 + + yield Comment( + " %s%s ... (%d:%d)\n" + " --------------------------------------------------------" % ( + node.prefix, node.name, line, column)) + + if node.attributes: + for stmt in emit_node(ast.Str(s=node.prefix + node.name)): + yield stmt + + for stmt in self.visit(node.attributes): + yield stmt + + for stmt in emit_node(ast.Str(s=node.suffix)): + yield stmt + else: + for stmt in emit_node( + ast.Str(s=node.prefix + node.name + node.suffix)): + yield stmt + + def visit_End(self, node): + for stmt in emit_node(ast.Str( + s=node.prefix + node.name + node.space + node.suffix)): + yield stmt + + def visit_Attribute(self, node): + attr_format = (node.space + node.name + node.eq + + node.quote + "%s" + node.quote) + + filter_args = list(map(self._engine.cache.get, node.filters)) + + filter_condition = template( + "NAME not in CHAIN", + NAME=ast.Str(s=node.name), + CHAIN=ast.Call( + func=load("__chain"), + args=filter_args, + keywords=[], + starargs=None, + kwargs=None, + ), + mode="eval" + ) + + # Static attributes are just outputted directly + if isinstance(node.expression, ast.Str): + s = attr_format % node.expression.s + if node.filters: + return template( + "if C: __append(S)", C=filter_condition, S=ast.Str(s=s) + ) + else: + return template("__append(S)", S=ast.Str(s=s)) + + target = identifier("attr", node.name) + body = self._engine(node.expression, store(target)) + + condition = template("TARGET is not None", TARGET=target, mode="eval") + + if node.filters: + condition = ast.BoolOp( + values=[condition, filter_condition], + op=ast.And(), + ) + + return body + template( + "if CONDITION: __append(FORMAT % TARGET)", + FORMAT=ast.Str(s=attr_format), + TARGET=target, + CONDITION=condition, + ) + + def visit_DictAttributes(self, node): + target = identifier("attr", id(node)) + body = self._engine(node.expression, store(target)) + + exclude = Static(template( + "set(LIST)", LIST=ast.List( + elts=[ast.Str(s=name) for name in node.exclude], + ctx=ast.Load(), + ), mode="eval" + )) + + body += template( + "for name, value in TARGET.items():\n " + "if name not in EXCLUDE and value is not None: __append(" + "' ' + name + '=' + QUOTE + " + "QUOTE_FUNC(value, QUOTE, QUOTE_ENTITY, None, None) + QUOTE" + ")", + TARGET=target, + EXCLUDE=exclude, + QUOTE_FUNC="__quote", + QUOTE=ast.Str(s=node.quote), + QUOTE_ENTITY=ast.Str(s=char2entity(node.quote or '\0')), + ) + + return body + + def visit_Cache(self, node): + body = [] + + for expression in node.expressions: + name = identifier("cache", id(expression)) + target = store(name) + + # Skip re-evaluation + if self._expression_cache.get(expression): + continue + + body += self._engine(expression, target) + self._expression_cache[expression] = target + + body += self.visit(node.node) + + return body + + def visit_Cancel(self, node): + body = [] + + for expression in node.expressions: + name = identifier("cache", id(expression)) + target = store(name) + + if not self._expression_cache.get(expression): + continue + + body.append(ast.Assign([target], load("None"))) + + body += self.visit(node.node) + + return body + + def visit_UseInternalMacro(self, node): + if node.name is None: + render = "render" + else: + render = "render_%s" % mangle(node.name) + token_reset = template("__token = None") + return token_reset + template( + "f(__stream, econtext.copy(), rcontext, __i18n_domain)", + f=render) + \ + template("econtext.update(rcontext)") + + def visit_DefineSlot(self, node): + name = "__slot_%s" % mangle(node.name) + body = self.visit(node.node) + + self._slots.add(name) + + orelse = template( + "SLOT(__stream, econtext.copy(), rcontext)", + SLOT=name) + test = ast.Compare( + left=load(name), + ops=[ast.Is()], + comparators=[load("None")] + ) + + return [ + ast.If(test=test, body=body or [ast.Pass()], orelse=orelse) + ] + + def visit_Name(self, node): + """Translation name.""" + + if not self._translations: + raise TranslationError( + "Not allowed outside of translation.", node.name) + + if node.name in self._translations[-1]: + raise TranslationError( + "Duplicate translation name: %s.", node.name) + + self._translations[-1].add(node.name) + body = [] + + # prepare new stream + stream, append = self._get_translation_identifiers(node.name) + body += template("s = new_list", s=stream, new_list=LIST) + \ + template("a = s.append", a=append, s=stream) + + # generate code + code = self.visit(node.node) + swap(ast.Suite(body=code), load(append), "__append") + body += code + + # output msgid + text = Text('${%s}' % node.name) + body += self.visit(text) + + # Concatenate stream + body += template("stream = ''.join(stream)", stream=stream) + + return body + + def visit_CodeBlock(self, node): + stmts = template(textwrap.dedent(node.source.strip('\n'))) + + for stmt in stmts: + self._visitor(stmt) + + return set_token(stmts, node.source) + + def visit_UseExternalMacro(self, node): + self._macros.append(node.extend) + + callbacks = [] + for slot in node.slots: + key = "__slot_%s" % mangle(slot.name) + fun = "__fill_%s" % mangle(slot.name) + + self._current_slot.append(slot.name) + + body = template("getitem = econtext.__getitem__") + \ + template("get = econtext.get") + \ + self.visit(slot.node) + + assert self._current_slot.pop() == slot.name + + callbacks.append( + ast.FunctionDef( + name=fun, + args=ast.arguments( + args=[ + param("__stream"), + param("econtext"), + param("rcontext"), + param("__i18n_domain"), + param("__i18n_context"), + ], + defaults=[load("__i18n_domain"), load("__i18n_context")], + ), + body=body or [ast.Pass()], + )) + + key = ast.Str(s=key) + + assignment = template( + "_slots = econtext[KEY] = DEQUE((NAME,))", + KEY=key, NAME=fun, DEQUE=Symbol(collections.deque), + ) + + if node.extend: + append = template("_slots.appendleft(NAME)", NAME=fun) + + assignment = [ast.TryExcept( + body=template("_slots = getitem(KEY)", KEY=key), + handlers=[ast.ExceptHandler(body=assignment)], + orelse=append, + )] + + callbacks.extend(assignment) + + assert self._macros.pop() == node.extend + + assignment = self._engine(node.expression, store("__macro")) + + return ( + callbacks + + assignment + + set_token( + template("__m = __macro.include"), + node.expression.value + ) + + template( + "__m(__stream, econtext.copy(), " + "rcontext, __i18n_domain)" + ) + + template("econtext.update(rcontext)") + ) + + def visit_Repeat(self, node): + # Used for loop variable definition and restore + self._scopes.append(set()) + + # Variable assignment and repeat key for single- and + # multi-variable repeat clause + if node.local: + contexts = "econtext", + else: + contexts = "econtext", "rcontext" + + for name in node.names: + if name in COMPILER_INTERNALS_OR_DISALLOWED: + raise TranslationError( + "Name disallowed by compiler.", name + ) + + if len(node.names) > 1: + targets = [ + ast.Tuple(elts=[ + subscript(native_string(name), load(context), ast.Store()) + for name in node.names], ctx=ast.Store()) + for context in contexts + ] + + key = ast.Tuple( + elts=[ast.Str(s=name) for name in node.names], + ctx=ast.Load()) + else: + name = node.names[0] + targets = [ + subscript(native_string(name), load(context), ast.Store()) + for context in contexts + ] + + key = ast.Str(s=node.names[0]) + + index = identifier("__index", id(node)) + assignment = [ast.Assign(targets=targets, value=load("__item"))] + + # Make repeat assignment in outer loop + names = node.names + local = node.local + + outer = self._engine(node.expression, store("__iterator")) + + if local: + outer[:] = list(self._enter_assignment(names)) + outer + + outer += template( + "__iterator, INDEX = getitem('repeat')(key, __iterator)", + key=key, INDEX=index + ) + + # Set a trivial default value for each name assigned to make + # sure we assign a value even if the iteration is empty + outer += [ast.Assign( + targets=[store_econtext(name) + for name in node.names], + value=load("None")) + ] + + # Compute inner body + inner = self.visit(node.node) + + # After each iteration, decrease the index + inner += template("index -= 1", index=index) + + # For items up to N - 1, emit repeat whitespace + inner += template( + "if INDEX > 0: __append(WHITESPACE)", + INDEX=index, WHITESPACE=ast.Str(s=node.whitespace) + ) + + # Main repeat loop + outer += [ast.For( + target=store("__item"), + iter=load("__iterator"), + body=assignment + inner, + )] + + # Finally, clean up assignment if it's local + if outer: + outer += self._leave_assignment(names) + + self._scopes.pop() + + return outer + + def _get_translation_identifiers(self, name): + assert self._translations + prefix = str(id(self._translations[-1])).replace('-', '_') + stream = identifier("stream_%s" % prefix, name) + append = identifier("append_%s" % prefix, name) + return stream, append + + def _enter_assignment(self, names): + for name in names: + for stmt in template( + "BACKUP = get(KEY, __marker)", + BACKUP=identifier("backup_%s" % name, id(names)), + KEY=ast.Str(s=native_string(name)), + ): + yield stmt + + def _leave_assignment(self, names): + for name in names: + for stmt in template( + "if BACKUP is __marker: del econtext[KEY]\n" + "else: econtext[KEY] = BACKUP", + BACKUP=identifier("backup_%s" % name, id(names)), + KEY=ast.Str(s=native_string(name)), + ): + yield stmt diff --git a/src/chameleon/config.py b/src/chameleon/config.py new file mode 100644 index 0000000..bf4d541 --- /dev/null +++ b/src/chameleon/config.py @@ -0,0 +1,55 @@ +import os +import logging + +log = logging.getLogger('chameleon.config') +environment = dict( + (k[10:], v) for (k, v) in ( + ((j.lower(), x) for (j, x) in os.environ.items())) + if k.startswith('chameleon_') +) + +# Define which values are read as true +TRUE = ('y', 'yes', 't', 'true', 'on', '1') + +# If eager parsing is enabled, templates are parsed upon +# instantiation, rather than when first called upon; this mode is +# useful for verifying validity of templates across a project +EAGER_PARSING = environment.pop('eager', 'false') +EAGER_PARSING = EAGER_PARSING.lower() in TRUE + +# Debug mode is mostly useful for debugging the template engine +# itself. When enabled, generated source code is written to disk to +# ease step-debugging and some log levels are lowered to increase +# output. Also, the generated source code is available in the +# ``source`` attribute of the template instance if compilation +# succeeded. +DEBUG_MODE = environment.pop('debug', 'false') +DEBUG_MODE = DEBUG_MODE.lower() in TRUE + +# If a cache directory is specified, template source code will be +# persisted on disk and reloaded between sessions +path = environment.pop('cache', None) +if path is not None: + CACHE_DIRECTORY = os.path.abspath(path) + if not os.path.exists(CACHE_DIRECTORY): + raise ValueError( + "Cache directory does not exist: %s." % CACHE_DIRECTORY + ) + log.info("directory cache: %s." % CACHE_DIRECTORY) +else: + CACHE_DIRECTORY = None + +# When auto-reload is enabled, templates are reloaded on file change. +AUTO_RELOAD = environment.pop('reload', 'false') +AUTO_RELOAD = AUTO_RELOAD.lower() in TRUE + +for key in environment: + log.warning( + "unknown environment variable set: \"CHAMELEON_%s\"." % key.upper() + ) + +# This is the slice length of the expression displayed in the +# formatted exception string +SOURCE_EXPRESSION_MARKER_LENGTH = 60 + + diff --git a/src/chameleon/exc.py b/src/chameleon/exc.py new file mode 100644 index 0000000..6b88b67 --- /dev/null +++ b/src/chameleon/exc.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- + +import traceback + +from .utils import create_formatted_exception +from .utils import format_kwargs +from .utils import safe_native +from .tokenize import Token +from .config import SOURCE_EXPRESSION_MARKER_LENGTH as LENGTH + + +def compute_source_marker(line, column, expression, size): + """Computes source marker location string. + + >>> def test(l, c, e, s): + ... s, marker = compute_source_marker(l, c, e, s) + ... out = s + '\\n' + marker + ... + ... # Replace dot with middle-dot to work around doctest ellipsis + ... print(out.replace('...', '···')) + + >>> test('foo bar', 4, 'bar', 7) + foo bar + ^^^ + + >>> test('foo ${bar}', 4, 'bar', 10) + foo ${bar} + ^^^ + + >>> test(' foo bar', 6, 'bar', 6) + ··· oo bar + ^^^ + + >>> test(' foo bar baz ', 6, 'bar', 6) + ··· o bar ··· + ^^^ + + The entire expression is always shown, even if ``size`` does not + accomodate for it. + + >>> test(' foo bar baz ', 6, 'bar baz', 10) + ··· oo bar baz + ^^^^^^^ + + >>> test(' foo bar', 10, 'bar', 5) + ··· o bar + ^^^ + + >>> test(' foo bar', 10, 'boo', 5) + ··· o bar + ^ + + """ + + s = line.lstrip() + column -= len(line) - len(s) + s = s.rstrip() + + try: + i = s[column:].index(expression) + except ValueError: + # If we can't find the expression + # (this shouldn't happen), simply + # use a standard size marker + marker = "^" + else: + column += i + marker = "^" * len(expression) + + if len(expression) > size: + offset = column + size = len(expression) + else: + window = (size - len(expression)) / 2.0 + offset = column - window + offset -= min(3, max(0, column + window + len(expression) - len(s))) + offset = int(offset) + + if offset > 0: + s = s[offset:] + r = s.lstrip() + d = len(s) - len(r) + s = "... " + r + column += 4 - d + column -= offset + + # This also adds to the displayed length + size += 4 + + if len(s) > size: + s = s[:size].rstrip() + " ..." + + return s, column * " " + marker + + +def iter_source_marker_lines(source, expression, line, column): + for i, l in enumerate(source): + if i + 1 != line: + continue + + s, marker = compute_source_marker( + l, column, expression, LENGTH + ) + + yield " - Source: %s" % s + yield " %s" % marker + break + + +def ellipsify(string, limit): + if len(string) > limit: + return "... " + string[-(limit - 4):] + + return string + + +class RenderError(Exception): + """An error raised during rendering. + + This class is used as a mixin which is added to the original + exception. + """ + + +class TemplateError(Exception): + """An error raised by Chameleon. + + >>> from chameleon.tokenize import Token + >>> token = Token('token') + >>> message = 'message' + + Make sure the exceptions can be copied: + + >>> from copy import copy + >>> copy(TemplateError(message, token)) + TemplateError('message', 'token') + + And pickle/unpickled: + + >>> from pickle import dumps, loads + >>> loads(dumps(TemplateError(message, token), -1)) + TemplateError('message', 'token') + + """ + + def __init__(self, msg, token): + if not isinstance(token, Token): + token = Token(token, 0) + + Exception.__init__(self, msg, token) + + def __copy__(self): + inst = Exception.__new__(type(self)) + inst.args = self.args + return inst + + def __str__(self): + text = "%s\n\n" % self.args[0] + text += " - String: \"%s\"" % safe_native(self.token) + + if self.filename: + text += "\n" + text += " - Filename: %s" % self.filename + + line, column = self.location + text += "\n" + text += " - Location: (line %d: col %d)" % (line, column) + + if line and column: + if self.token.source: + lines = iter_source_marker_lines( + self.token.source.splitlines(), + self.token, line, column + ) + elif self.filename and not self.filename.startswith('<'): + try: + f = open(self.filename, 'r') + except IOError: + pass + else: + it = iter_source_marker_lines( + iter(f), self.token, line, column + ) + try: + lines = list(lines) + finally: + f.close() + else: + lines = () + + # Prepend newlines. + for line in lines: + text += "\n" + line + + return text + + def __repr__(self): + try: + return "%s('%s', '%s')" % ( + self.__class__.__name__, self.args[0], safe_native(self.token) + ) + except AttributeError: + return object.__repr__(self) + + @property + def token(self): + return self.args[1] + + @property + def filename(self): + return self.token.filename + + @property + def location(self): + return self.token.location + + @property + def offset(self): + return getattr(self.token, "pos", 0) + + +class ParseError(TemplateError): + """An error occurred during parsing. + + Indicates an error on the structural level. + """ + + +class CompilationError(TemplateError): + """An error occurred during compilation. + + Indicates a general compilation error. + """ + + +class TranslationError(TemplateError): + """An error occurred during translation. + + Indicates a general translation error. + """ + + +class LanguageError(CompilationError): + """Language syntax error. + + Indicates a syntactical error due to incorrect usage of the + template language. + """ + + +class ExpressionError(LanguageError): + """An error occurred compiling an expression. + + Indicates a syntactical error in an expression. + """ + + +class ExceptionFormatter(object): + def __init__(self, errors, econtext, rcontext): + kwargs = rcontext.copy() + kwargs.update(econtext) + + for name in tuple(kwargs): + if name.startswith('__'): + del kwargs[name] + + self._errors = errors + self._kwargs = kwargs + + def __call__(self): + # Format keyword arguments; consecutive arguments are indented + # for readability + try: + formatted = format_kwargs(self._kwargs) + except: + # the ``pprint.pformat`` method calls the representation + # method of the arguments; this may fail and since we're + # already in an exception handler, there's no point in + # pursuing this further + formatted = () + + for index, string in enumerate(formatted[1:]): + formatted[index + 1] = " " * 15 + string + + out = [] + + for error in self._errors: + expression, line, column, filename, exc = error + + if isinstance(exc, UnicodeDecodeError): + string = safe_native(exc.object) + s, marker = compute_source_marker( + string, exc.start, string[exc.start:exc.end], LENGTH + ) + + out.append(" - Stream: %s" % s) + out.append(" %s" % marker) + + _filename = ellipsify(filename, 60) if filename else "<string>" + + out.append(" - Expression: \"%s\"" % expression) + out.append(" - Filename: %s" % _filename) + out.append(" - Location: (line %d: col %d)" % (line, column)) + + if filename and not filename.startswith('<') and line and column: + try: + f = open(filename, 'r') + except IOError: + pass + else: + lines = iter_source_marker_lines( + iter(f), expression, line, column + ) + try: + out.extend(lines) + finally: + f.close() + + out.append(" - Arguments: %s" % "\n".join(formatted)) + + if isinstance(exc.__str__, ExceptionFormatter): + # This is a nested error that has already been wrapped + # We must unwrap it before trying to format it to prevent + # recursion + exc = create_formatted_exception(exc, type(exc), exc._original__str__) + formatted = traceback.format_exception_only(type(exc), exc)[-1] + formatted_class = "%s:" % type(exc).__name__ + + if formatted.startswith(formatted_class): + formatted = formatted[len(formatted_class):].lstrip() + + return "\n".join(map(safe_native, [formatted] + out)) diff --git a/src/chameleon/i18n.py b/src/chameleon/i18n.py new file mode 100644 index 0000000..b7c318a --- /dev/null +++ b/src/chameleon/i18n.py @@ -0,0 +1,131 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Foundation 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. +# +############################################################################## + +import re + +from .exc import CompilationError +from .utils import unicode_string + +NAME_RE = r"[a-zA-Z][-a-zA-Z0-9_]*" + +WHITELIST = frozenset([ + "translate", + "domain", + "context", + "target", + "source", + "attributes", + "data", + "name", + "mode", + "xmlns", + "xml", + "comment", + "ignore", + "ignore-attributes", + ]) + +_interp_regex = re.compile(r'(?<!\$)(\$(?:(%(n)s)|{(%(n)s)}))' + % ({'n': NAME_RE})) + + +try: # pragma: no cover + str = unicode +except NameError: + pass + +# BBB: The ``fast_translate`` function here is kept for backwards +# compatibility reasons. Do not use! + +try: # pragma: no cover + from zope.i18n import interpolate + from zope.i18n import translate + from zope.i18nmessageid import Message +except ImportError: # pragma: no cover + pass +else: # pragma: no cover + def fast_translate(msgid, domain=None, mapping=None, context=None, + target_language=None, default=None): + if msgid is None: + return + + if target_language is not None or context is not None: + result = translate( + msgid, domain=domain, mapping=mapping, context=context, + target_language=target_language, default=default) + if result != msgid: + return result + + if isinstance(msgid, Message): + default = msgid.default + mapping = msgid.mapping + + if default is None: + default = str(msgid) + + if not isinstance(default, basestring): + return default + + return interpolate(default, mapping) + + +def simple_translate(msgid, domain=None, mapping=None, context=None, + target_language=None, default=None): + if default is None: + default = getattr(msgid, "default", msgid) + + if mapping is None: + mapping = getattr(msgid, "mapping", None) + + if mapping: + def replace(match): + whole, param1, param2 = match.groups() + return unicode_string(mapping.get(param1 or param2, whole)) + return _interp_regex.sub(replace, default) + + return default + + +def parse_attributes(attrs, xml=True): + d = {} + + # filter out empty items, eg: + # i18n:attributes="value msgid; name msgid2;" + # would result in 3 items where the last one is empty + attrs = [spec for spec in attrs.split(";") if spec] + + for spec in attrs: + if ',' in spec: + raise CompilationError( + "Attribute must not contain comma. Use semicolon to " + "list multiple attributes", spec + ) + parts = spec.split() + if len(parts) == 2: + attr, msgid = parts + elif len(parts) == 1: + attr = parts[0] + msgid = None + else: + raise CompilationError( + "Illegal i18n:attributes specification.", spec) + if not xml: + attr = attr.lower() + attr = attr.strip() + if attr in d: + raise CompilationError( + "Attribute may only be specified once in i18n:attributes", attr) + d[attr] = msgid + + return d diff --git a/src/chameleon/interfaces.py b/src/chameleon/interfaces.py new file mode 100644 index 0000000..85ff6bd --- /dev/null +++ b/src/chameleon/interfaces.py @@ -0,0 +1,102 @@ +from zope.interface import Interface +from zope.interface import Attribute + + +class ITALExpressionErrorInfo(Interface): + + type = Attribute("type", + "The exception class.") + + value = Attribute("value", + "The exception instance.") + + lineno = Attribute("lineno", + "The line number the error occurred on in the source.") + + offset = Attribute("offset", + "The character offset at which the error occurred.") + + +class ITALIterator(Interface): # pragma: no cover + """A TAL iterator + + Not to be confused with a Python iterator. + """ + + def next(): + """Advance to the next value in the iteration, if possible + + Return a true value if it was possible to advance and return + a false value otherwise. + """ + + +class ITALESIterator(ITALIterator): # pragma: no cover + """TAL Iterator provided by TALES + + Values of this iterator are assigned to items in the repeat namespace. + + For example, with a TAL statement like: tal:repeat="item items", + an iterator will be assigned to "repeat/item". The iterator + provides a number of handy methods useful in writing TAL loops. + + The results are undefined of calling any of the methods except + 'length' before the first iteration. + """ + + def index(): + """Return the position (starting with "0") within the iteration + """ + + def number(): + """Return the position (starting with "1") within the iteration + """ + + def even(): + """Return whether the current position is even + """ + + def odd(): + """Return whether the current position is odd + """ + + def parity(): + """Return 'odd' or 'even' depending on the position's parity + + Useful for assigning CSS class names to table rows. + """ + + def start(): + """Return whether the current position is the first position + """ + + def end(): + """Return whether the current position is the last position + """ + + def letter(): + """Return the position (starting with "a") within the iteration + """ + + def Letter(): + """Return the position (starting with "A") within the iteration + """ + + def roman(): + """Return the position (starting with "i") within the iteration + """ + + def Roman(): + """Return the position (starting with "I") within the iteration + """ + + def item(): + """Return the item at the current position + """ + + def length(): + """Return the length of the sequence + + Note that this may fail if the TAL iterator was created on a Python + iterator. + """ diff --git a/src/chameleon/loader.py b/src/chameleon/loader.py new file mode 100644 index 0000000..dd161a6 --- /dev/null +++ b/src/chameleon/loader.py @@ -0,0 +1,192 @@ +import functools +import logging +import os +import py_compile +import shutil +import sys +import tempfile +import warnings +import pkg_resources + +try: + from importlib.machinery import SourceFileLoader + from threading import RLock + lock = RLock() + acquire_lock = lock.acquire + release_lock = lock.release + del lock +except ImportError: + from imp import acquire_lock, release_lock, load_source + + class SourceFileLoader: + def __init__(self, base, filename): + self.base = base + self.filename = filename + + def load_module(self): + try: + acquire_lock() + assert self.base not in sys.modules + with open(self.filename, 'rb') as f: + return load_source(self.base, self.filename, f) + finally: + release_lock() + +log = logging.getLogger('chameleon.loader') + +from .utils import string_type +from .utils import encode_string + + +def cache(func): + def load(self, *args, **kwargs): + template = self.registry.get(args) + if template is None: + self.registry[args] = template = func(self, *args, **kwargs) + return template + return load + + +def abspath_from_asset_spec(spec): + pname, filename = spec.split(':', 1) + return pkg_resources.resource_filename(pname, filename) + +if os.name == "nt": + def abspath_from_asset_spec(spec, f=abspath_from_asset_spec): + if spec[1] == ":": + return spec + return f(spec) + + +class TemplateLoader(object): + """Template loader class. + + To load templates using relative filenames, pass a sequence of + paths (or a single path) as ``search_path``. + + To apply a default filename extension to inputs which do not have + an extension already (i.e. no dot), provide this as + ``default_extension`` (e.g. ``'.pt'``). + + Additional keyword-arguments will be passed on to the template + constructor. + """ + + default_extension = None + + def __init__(self, search_path=None, default_extension=None, **kwargs): + if search_path is None: + search_path = [] + if isinstance(search_path, string_type): + search_path = [search_path] + if default_extension is not None: + self.default_extension = ".%s" % default_extension.lstrip('.') + self.search_path = search_path + self.registry = {} + self.kwargs = kwargs + + @cache + def load(self, spec, cls=None): + if cls is None: + raise ValueError("Unbound template loader.") + + spec = spec.strip() + + if self.default_extension is not None and '.' not in spec: + spec += self.default_extension + + if ':' in spec: + spec = abspath_from_asset_spec(spec) + + if not os.path.isabs(spec): + for path in self.search_path: + path = os.path.join(path, spec) + if os.path.exists(path): + spec = path + break + else: + raise ValueError("Template not found: %s." % spec) + + return cls(spec, search_path=self.search_path, **self.kwargs) + + def bind(self, cls): + return functools.partial(self.load, cls=cls) + + +class MemoryLoader(object): + def build(self, source, filename): + code = compile(source, filename, 'exec') + env = {} + exec(code, env) + return env + + def get(self, name): + return None + + +class ModuleLoader(object): + def __init__(self, path, remove=False): + self.path = path + self.remove = remove + + def __del__(self, shutil=shutil): + if not self.remove: + return + try: + shutil.rmtree(self.path) + except: + warnings.warn("Could not clean up temporary file path: %s" % (self.path,)) + + def get(self, filename): + path = os.path.join(self.path, filename) + if os.path.exists(path): + log.debug("loading module from cache: %s." % filename) + base, ext = os.path.splitext(filename) + return self._load(base, path) + else: + log.debug('cache miss: %s' % filename) + + def build(self, source, filename): + acquire_lock() + try: + d = self.get(filename) + if d is not None: + return d + + base, ext = os.path.splitext(filename) + name = os.path.join(self.path, base + ".py") + + log.debug("writing source to disk (%d bytes)." % len(source)) + fd, fn = tempfile.mkstemp(prefix=base, suffix='.tmp', dir=self.path) + temp = os.fdopen(fd, 'wb') + encoded = source.encode('utf-8') + header = encode_string("# -*- coding: utf-8 -*-" + "\n") + + try: + try: + temp.write(header) + temp.write(encoded) + finally: + temp.close() + except: + os.remove(fn) + raise + + os.rename(fn, name) + log.debug("compiling %s into byte-code..." % filename) + py_compile.compile(name) + + return self._load(base, name) + finally: + release_lock() + + def _load(self, base, filename): + acquire_lock() + try: + module = sys.modules.get(base) + if module is None: + module = SourceFileLoader(base, filename).load_module() + finally: + release_lock() + + return module.__dict__ diff --git a/src/chameleon/metal.py b/src/chameleon/metal.py new file mode 100644 index 0000000..2b2b41d --- /dev/null +++ b/src/chameleon/metal.py @@ -0,0 +1,23 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Foundation 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. +# +############################################################################## + +WHITELIST = frozenset([ + "define-macro", + "extend-macro", + "use-macro", + "define-slot", + "fill-slot", + "xmlns", + "xml" + ]) diff --git a/src/chameleon/namespaces.py b/src/chameleon/namespaces.py new file mode 100644 index 0000000..467963b --- /dev/null +++ b/src/chameleon/namespaces.py @@ -0,0 +1,9 @@ +XML_NS = "http://www.w3.org/XML/1998/namespace" +XMLNS_NS = "http://www.w3.org/2000/xmlns/" +XHTML_NS = "http://www.w3.org/1999/xhtml" +TAL_NS = "http://xml.zope.org/namespaces/tal" +META_NS = "http://xml.zope.org/namespaces/meta" +METAL_NS = "http://xml.zope.org/namespaces/metal" +XI_NS = "http://www.w3.org/2001/XInclude" +I18N_NS = "http://xml.zope.org/namespaces/i18n" +PY_NS = "http://genshi.edgewall.org/" diff --git a/src/chameleon/nodes.py b/src/chameleon/nodes.py new file mode 100644 index 0000000..59ad9da --- /dev/null +++ b/src/chameleon/nodes.py @@ -0,0 +1,233 @@ +from .astutil import Node + + +class UseExternalMacro(Node): + """Extend external macro.""" + + _fields = "expression", "slots", "extend" + + +class Sequence(Node): + """Element sequence.""" + + _fields = "items", + + def __nonzero__(self): + return bool(self.items) + + +class Content(Node): + """Content substitution.""" + + _fields = "expression", "char_escape", "translate" + + +class Default(Node): + """Represents a default value.""" + + _fields = "marker", + + +class CodeBlock(Node): + _fields = "source", + + +class Value(Node): + """Expression object value.""" + + _fields = "value", + + def __repr__(self): + try: + line, column = self.value.location + except AttributeError: + line, column = 0, 0 + + return "<%s %r (%d:%d)>" % ( + type(self).__name__, self.value, line, column + ) + + +class Substitution(Value): + """Expression value for text substitution.""" + + _fields = "value", "char_escape", "default" + + default = None + + +class Boolean(Value): + _fields = "value", "s" + + +class Negate(Node): + """Wraps an expression with a negation.""" + + _fields = "value", + + +class Element(Node): + """XML element.""" + + _fields = "start", "end", "content" + + +class DictAttributes(Node): + """Element attributes from one or more Python dicts.""" + + _fields = "expression", "char_escape", "quote", "exclude" + + +class Attribute(Node): + """Element attribute.""" + + _fields = "name", "expression", "quote", "eq", "space", "filters" + + +class Start(Node): + """Start-tag.""" + + _fields = "name", "prefix", "suffix", "attributes" + + +class End(Node): + """End-tag.""" + + _fields = "name", "space", "prefix", "suffix" + + +class Condition(Node): + """Node visited only if some condition holds.""" + + _fields = "expression", "node", "orelse" + + +class Identity(Node): + """Condition expression that is true on identity.""" + + _fields = "expression", "value" + + +class Equality(Node): + """Condition expression that is true on equality.""" + + _fields = "expression", "value" + + +class Cache(Node): + """Cache (evaluate only once) the value of ``expression`` inside + ``node``. + """ + + _fields = "expressions", "node" + + +class Cancel(Cache): + pass + + +class Copy(Node): + _fields = "expression", + + +class Assignment(Node): + """Variable assignment.""" + + _fields = "names", "expression", "local" + + +class Alias(Assignment): + """Alias assignment. + + Note that ``expression`` should be a cached or global value. + """ + + local = False + + +class Define(Node): + """Variable definition in scope.""" + + _fields = "assignments", "node" + + +class Repeat(Assignment): + """Iterate over provided assignment and repeat body.""" + + _fields = "names", "expression", "local", "whitespace", "node" + + +class Macro(Node): + """Macro definition.""" + + _fields = "name", "body" + + +class Program(Node): + _fields = "name", "body" + + +class Module(Node): + _fields = "name", "program", + + +class Context(Node): + _fields = "node", + + +class Text(Node): + """Static text output.""" + + _fields = "value", + + +class Interpolation(Node): + """String interpolation output.""" + + _fields = "value", "braces_required", "translation" + + +class Translate(Node): + """Translate node.""" + + _fields = "msgid", "node" + + +class Name(Node): + """Translation name.""" + + _fields = "name", "node" + + +class Domain(Node): + """Update translation domain.""" + + _fields = "name", "node" + + +class TxContext(Node): + """Update translation context.""" + + _fields = "name", "node" + + +class OnError(Node): + _fields = "fallback", "name", "node" + + +class UseInternalMacro(Node): + """Use internal macro (defined inside same program).""" + + _fields = "name", + + +class FillSlot(Node): + """Fill a macro slot.""" + + _fields = "name", "node" + + +class DefineSlot(Node): + """Define a macro slot.""" + + _fields = "name", "node" diff --git a/src/chameleon/parser.py b/src/chameleon/parser.py new file mode 100644 index 0000000..b0d62a3 --- /dev/null +++ b/src/chameleon/parser.py @@ -0,0 +1,258 @@ +import re +import logging + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict + +from .exc import ParseError +from .namespaces import XML_NS +from .tokenize import Token + +match_double_hyphen = re.compile(r'--(?!(-)*>)') +match_tag_prefix_and_name = re.compile( + r'^(?P<prefix></?)(?P<name>([^:\n\r ]+:)?[^ \n\t\r>/]+)' + r'(?P<suffix>(?P<space>\s*)/?>)?', + re.UNICODE | re.DOTALL) +match_single_attribute = re.compile( + r'(?P<space>\s+)(?!\d)' + r'(?P<name>[^ =/>\n\t\r]+)' + r'((?P<eq>\s*=\s*)' + r'((?P<quote>[\'"])(?P<value>.*?)(?P=quote)|' + r'(?P<alt_value>[^\s\'">/]+))|' + r'(?P<simple_value>(?![ \\n\\t\\r]*=)))', + re.UNICODE | re.DOTALL) +match_comment = re.compile( + r'^<!--(?P<text>.*)-->$', re.DOTALL) +match_cdata = re.compile( + r'^<!\[CDATA\[(?P<text>.*)\]>$', re.DOTALL) +match_declaration = re.compile( + r'^<!(?P<text>[^>]+)>$', re.DOTALL) +match_processing_instruction = re.compile( + r'^<\?(?P<name>\w+)(?P<text>.*?)\?>', re.DOTALL) +match_xml_declaration = re.compile(r'^<\?xml(?=[ /])', re.DOTALL) + +log = logging.getLogger('chameleon.parser') + + +def substitute(regex, repl, token): + if not isinstance(token, Token): + token = Token(token) + + return Token( + regex.sub(repl, token), + token.pos, + token.source, + token.filename + ) + + +def groups(m, token): + result = [] + for i, group in enumerate(m.groups()): + if group is not None: + j, k = m.span(i + 1) + group = token[j:k] + + result.append(group) + + return tuple(result) + + +def groupdict(m, token): + d = m.groupdict() + for name, value in d.items(): + if value is not None: + i, j = m.span(name) + d[name] = token[i:j] + + return d + + +def match_tag(token, regex=match_tag_prefix_and_name): + m = regex.match(token) + d = groupdict(m, token) + + end = m.end() + token = token[end:] + + attrs = d['attrs'] = [] + for m in match_single_attribute.finditer(token): + attr = groupdict(m, token) + alt_value = attr.pop('alt_value', None) + if alt_value is not None: + attr['value'] = alt_value + attr['quote'] = '' + simple_value = attr.pop('simple_value', None) + if simple_value is not None: + attr['quote'] = '' + attr['value'] = '' + attr['eq'] = '' + attrs.append(attr) + d['suffix'] = token[m.end():] + + return d + + +def parse_tag(token, namespace, restricted_namespace): + node = match_tag(token) + + update_namespace(node['attrs'], namespace) + + if ':' in node['name']: + prefix = node['name'].split(':')[0] + else: + prefix = None + + default = node['namespace'] = namespace.get(prefix, XML_NS) + + node['ns_attrs'] = unpack_attributes( + node['attrs'], namespace, default, restricted_namespace + ) + + node['ns_map'] = namespace + + return node + + +def update_namespace(attributes, namespace): + # possibly update namespaces; we do this in a separate step + # because this assignment is irrespective of order + for attribute in attributes: + name = attribute['name'] + value = attribute['value'] + if name == 'xmlns': + namespace[None] = value + elif name.startswith('xmlns:'): + namespace[name[6:]] = value + + +def unpack_attributes(attributes, namespace, default, restricted_namespace): + namespaced = OrderedDict() + + for index, attribute in enumerate(attributes): + name = attribute['name'] + value = attribute['value'] + + if ':' in name: + prefix = name.split(':')[0] + name = name[len(prefix) + 1:] + try: + ns = namespace[prefix] + except KeyError: + if restricted_namespace: + raise KeyError( + "Undefined namespace prefix: %s." % prefix) + else: + ns = default + else: + ns = default + namespaced[ns, name] = value + + return namespaced + + +def identify(string): + if string.startswith("<"): + if string.startswith("<!--"): + m = match_double_hyphen.search(string[4:]) + if m is not None: + raise ParseError( + "The string '--' is not allowed in a comment.", + string[4 + m.start():4 + m.end()] + ) + return "comment" + if string.startswith("<![CDATA["): + return "cdata" + if string.startswith("<!"): + return "declaration" + if string.startswith("<?xml"): + return "xml_declaration" + if string.startswith("<?"): + return "processing_instruction" + if string.startswith("</"): + return "end_tag" + if string.endswith("/>"): + return "empty_tag" + if string.endswith(">"): + return "start_tag" + return "error" + return "text" + + +class ElementParser(object): + """Parses tokens into elements.""" + + def __init__(self, stream, default_namespaces, restricted_namespace=True): + self.stream = stream + self.queue = [] + self.index = [] + self.namespaces = [default_namespaces.copy()] + self.restricted_namespace = restricted_namespace + + def __iter__(self): + for token in self.stream: + item = self.parse(token) + self.queue.append(item) + + return iter(self.queue) + + def parse(self, token): + kind = identify(token) + visitor = getattr(self, "visit_%s" % kind, self.visit_default) + return visitor(kind, token) + + def visit_comment(self, kind, token): + return "comment", (token, ) + + def visit_cdata(self, kind, token): + return "cdata", (token, ) + + def visit_default(self, kind, token): + return "default", (token, ) + + def visit_processing_instruction(self, kind, token): + m = match_processing_instruction.match(token) + if m is None: + return self.visit_default(kind, token) + + return "processing_instruction", (groupdict(m, token), ) + + def visit_text(self, kind, token): + return kind, (token, ) + + def visit_start_tag(self, kind, token): + namespace = self.namespaces[-1].copy() + self.namespaces.append(namespace) + node = parse_tag(token, namespace, self.restricted_namespace) + self.index.append((node['name'], len(self.queue))) + return kind, (node, ) + + def visit_end_tag(self, kind, token): + try: + namespace = self.namespaces.pop() + except IndexError: + raise ParseError("Unexpected end tag.", token) + + node = parse_tag(token, namespace, self.restricted_namespace) + + while self.index: + name, pos = self.index.pop() + if name == node['name']: + start, = self.queue.pop(pos)[1] + children = self.queue[pos:] + del self.queue[pos:] + break + else: + raise ParseError("Unexpected end tag.", token) + + return "element", (start, node, children) + + def visit_empty_tag(self, kind, token): + namespace = self.namespaces[-1].copy() + node = parse_tag(token, namespace, self.restricted_namespace) + return "element", (node, None, []) + + def visit_xml_declaration(self, kind, token): + return self.visit_empty_tag(kind, token) diff --git a/src/chameleon/program.py b/src/chameleon/program.py new file mode 100644 index 0000000..b165bc6 --- /dev/null +++ b/src/chameleon/program.py @@ -0,0 +1,41 @@ +try: + str = unicode +except NameError: + long = int + +from .tokenize import iter_xml +from .tokenize import iter_text +from .parser import ElementParser +from .namespaces import XML_NS +from .namespaces import XMLNS_NS + + +class ElementProgram(object): + DEFAULT_NAMESPACES = { + 'xmlns': XMLNS_NS, + 'xml': XML_NS, + } + + tokenizers = { + 'xml': iter_xml, + 'text': iter_text, + } + + restricted_namespace = True + + def __init__(self, source, mode="xml", filename=None, tokenizer=None): + if tokenizer is None: + tokenizer = self.tokenizers[mode] + tokens = tokenizer(source, filename) + parser = ElementParser(tokens, self.DEFAULT_NAMESPACES, self.restricted_namespace) + + self.body = [] + + for kind, args in parser: + node = self.visit(kind, args) + if node is not None: + self.body.append(node) + + def visit(self, kind, args): + visitor = getattr(self, "visit_%s" % kind) + return visitor(*args) diff --git a/src/chameleon/py25.py b/src/chameleon/py25.py new file mode 100644 index 0000000..681f82d --- /dev/null +++ b/src/chameleon/py25.py @@ -0,0 +1,36 @@ +import sys + +def lookup_attr(obj, key): + try: + return getattr(obj, key) + except AttributeError: + exc = sys.exc_info()[1] + try: + get = obj.__getitem__ + except AttributeError: + raise exc + try: + return get(key) + except KeyError: + raise exc + +def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + +exec_("""def raise_with_traceback(exc, tb): + raise type(exc), exc, tb +""") + + +def next(iter): + return iter.next() diff --git a/src/chameleon/py26.py b/src/chameleon/py26.py new file mode 100644 index 0000000..d42a6ea --- /dev/null +++ b/src/chameleon/py26.py @@ -0,0 +1,15 @@ +import sys + +def lookup_attr(obj, key): + try: + return getattr(obj, key) + except AttributeError: + exc = sys.exc_info()[1] + try: + get = obj.__getitem__ + except AttributeError: + raise exc + try: + return get(key) + except KeyError: + raise exc diff --git a/src/chameleon/tal.py b/src/chameleon/tal.py new file mode 100755 index 0000000..c0788bf --- /dev/null +++ b/src/chameleon/tal.py @@ -0,0 +1,497 @@ +############################################################################## +# +# Copyright (c) 2001, 2002 Zope Foundation 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. +# +############################################################################## + +import re +import copy + +from .exc import LanguageError +from .utils import descriptorint +from .utils import descriptorstr +from .namespaces import XMLNS_NS +from .parser import groups + + +try: + next +except NameError: + from chameleon.py25 import next + +try: + # optional library: `zope.interface` + from chameleon import interfaces + import zope.interface +except ImportError: + interfaces = None + + +NAME = r"[a-zA-Z_][-a-zA-Z0-9_]*" +DEFINE_RE = re.compile(r"(?s)\s*(?:(global|local)\s+)?" + + r"(%s|\(%s(?:,\s*%s)*\))\s+(.*)\Z" % (NAME, NAME, NAME), + re.UNICODE) +SUBST_RE = re.compile(r"\s*(?:(text|structure)\s+)?(.*)\Z", re.S | re.UNICODE) +ATTR_RE = re.compile(r"\s*([^\s{}'\"]+)\s+([^\s].*)\Z", re.S | re.UNICODE) + +ENTITY_RE = re.compile(r'(&(#?)(x?)(\d{1,5}|\w{1,8});)') + +WHITELIST = frozenset([ + "define", + "comment", + "condition", + "content", + "replace", + "repeat", + "attributes", + "on-error", + "omit-tag", + "script", + "switch", + "case", + "xmlns", + "xml" + ]) + + +def split_parts(arg): + # Break in pieces at undoubled semicolons and + # change double semicolons to singles: + i = 0 + while i < len(arg): + m = ENTITY_RE.search(arg[i:]) + if m is None: + break + arg = arg[:i + m.end()] + ';' + arg[i + m.end():] + i += m.end() + + arg = arg.replace(";;", "\0") + parts = arg.split(';') + parts = [p.replace("\0", ";") for p in parts] + if len(parts) > 1 and not parts[-1].strip(): + del parts[-1] # It ended in a semicolon + + return parts + + +def parse_attributes(clause): + attrs = [] + seen = set() + for part in split_parts(clause): + m = ATTR_RE.match(part) + if not m: + name, expr = None, part.strip() + else: + name, expr = groups(m, part) + + if name in seen: + raise LanguageError( + "Duplicate attribute name in attributes.", part) + + seen.add(name) + attrs.append((name, expr)) + + return attrs + + +def parse_substitution(clause): + m = SUBST_RE.match(clause) + if m is None: + raise LanguageError( + "Invalid content substitution syntax.", clause) + + key, expression = groups(m, clause) + if not key: + key = "text" + + return key, expression + + +def parse_defines(clause): + """ + Parses a tal:define value. + + # Basic syntax, implicit local + >>> parse_defines('hello lovely') + [('local', ('hello',), 'lovely')] + + # Explicit local + >>> parse_defines('local hello lovely') + [('local', ('hello',), 'lovely')] + + # With global + >>> parse_defines('global hello lovely') + [('global', ('hello',), 'lovely')] + + # Multiple expressions + >>> parse_defines('hello lovely; tea time') + [('local', ('hello',), 'lovely'), ('local', ('tea',), 'time')] + + # With multiple names + >>> parse_defines('(hello, howdy) lovely') + [('local', ['hello', 'howdy'], 'lovely')] + + # With unicode whitespace + >>> try: + ... s = '\xc2\xa0hello lovely'.decode('utf-8') + ... except AttributeError: + ... s = '\xa0hello lovely' + >>> from chameleon.utils import unicode_string + >>> parse_defines(s) == [ + ... ('local', ('hello',), 'lovely') + ... ] + True + + """ + defines = [] + for part in split_parts(clause): + m = DEFINE_RE.match(part) + if m is None: + raise LanguageError("Invalid define syntax", part) + context, name, expr = groups(m, part) + context = context or "local" + + if name.startswith('('): + names = [n.strip() for n in name.strip('()').split(',')] + else: + names = (name,) + + defines.append((context, names, expr)) + + return defines + + +def prepare_attributes(attrs, dyn_attributes, i18n_attributes, + ns_attributes, drop_ns): + drop = set([attribute['name'] for attribute, (ns, value) + in zip(attrs, ns_attributes) + if ns in drop_ns or ( + ns == XMLNS_NS and + attribute['value'] in drop_ns + ) + ]) + + attributes = [] + normalized = {} + computed = [] + + for attribute in attrs: + name = attribute['name'] + + if name in drop: + continue + + attributes.append(( + name, + attribute['value'], + attribute['quote'], + attribute['space'], + attribute['eq'], + None, + )) + + normalized[name.lower()] = len(attributes) - 1 + + for name, expr in dyn_attributes: + index = normalized.get(name.lower()) if name else None + + if index is not None: + _, text, quote, space, eq, _ = attributes[index] + add = attributes.__setitem__ + else: + text = None + quote = '"' + space = " " + eq = "=" + index = len(attributes) + add = attributes.insert + if name is not None: + normalized[name.lower()] = len(attributes) - 1 + + attribute = name, text, quote, space, eq, expr + add(index, attribute) + + for name in i18n_attributes: + attr = name.lower() + if attr not in normalized: + attributes.append((name, name, '"', " ", "=", None)) + normalized[attr] = len(attributes) - 1 + + return attributes + + +class RepeatItem(object): + __slots__ = "length", "_iterator" + + __allow_access_to_unprotected_subobjects__ = True + + def __init__(self, iterator, length): + self.length = length + self._iterator = iterator + + def __iter__(self): + return self._iterator + + try: + iter(()).__len__ + except AttributeError: + @descriptorint + def index(self): + try: + remaining = self._iterator.__length_hint__() + except AttributeError: + remaining = len(tuple(copy.copy(self._iterator))) + return self.length - remaining - 1 + else: + @descriptorint + def index(self): + remaining = self._iterator.__len__() + return self.length - remaining - 1 + + @descriptorint + def start(self): + return self.index == 0 + + @descriptorint + def end(self): + return self.index == self.length - 1 + + @descriptorint + def number(self): + return self.index + 1 + + @descriptorstr + def odd(self): + """Returns a true value if the item index is odd. + + >>> it = RepeatItem(iter(("apple", "pear")), 2) + + >>> next(it._iterator) + 'apple' + >>> it.odd() + '' + + >>> next(it._iterator) + 'pear' + >>> it.odd() + 'odd' + """ + + return self.index % 2 == 1 and 'odd' or '' + + @descriptorstr + def even(self): + """Returns a true value if the item index is even. + + >>> it = RepeatItem(iter(("apple", "pear")), 2) + + >>> next(it._iterator) + 'apple' + >>> it.even() + 'even' + + >>> next(it._iterator) + 'pear' + >>> it.even() + '' + """ + + return self.index % 2 == 0 and 'even' or '' + + @descriptorstr + def parity(self): + """Return 'odd' or 'even' depending on the position's parity + + Useful for assigning CSS class names to table rows. + """ + + return self.index % 2 == 0 and 'even' or 'odd' + + def next(self): + raise NotImplementedError( + "Method not implemented (can't update local variable).") + + def _letter(self, base=ord('a'), radix=26): + """Get the iterator position as a lower-case letter + + >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) + >>> next(it._iterator) + 'apple' + >>> it.letter() + 'a' + >>> next(it._iterator) + 'pear' + >>> it.letter() + 'b' + >>> next(it._iterator) + 'orange' + >>> it.letter() + 'c' + """ + + index = self.index + if index < 0: + raise TypeError("No iteration position") + s = "" + while 1: + index, off = divmod(index, radix) + s = chr(base + off) + s + if not index: + return s + + letter = descriptorstr(_letter) + + @descriptorstr + def Letter(self): + """Get the iterator position as an upper-case letter + + >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) + >>> next(it._iterator) + 'apple' + >>> it.Letter() + 'A' + >>> next(it._iterator) + 'pear' + >>> it.Letter() + 'B' + >>> next(it._iterator) + 'orange' + >>> it.Letter() + 'C' + """ + + return self._letter(base=ord('A')) + + @descriptorstr + def Roman(self, rnvalues=( + (1000, 'M'), (900, 'CM'), (500, 'D'), (400, 'CD'), + (100, 'C'), (90, 'XC'), (50, 'L'), (40, 'XL'), + (10, 'X'), (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I'))): + """Get the iterator position as an upper-case roman numeral + + >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) + >>> next(it._iterator) + 'apple' + >>> it.Roman() + 'I' + >>> next(it._iterator) + 'pear' + >>> it.Roman() + 'II' + >>> next(it._iterator) + 'orange' + >>> it.Roman() + 'III' + """ + + n = self.index + 1 + s = "" + for v, r in rnvalues: + rct, n = divmod(n, v) + s = s + r * rct + return s + + @descriptorstr + def roman(self): + """Get the iterator position as a lower-case roman numeral + + >>> it = RepeatItem(iter(("apple", "pear", "orange")), 3) + >>> next(it._iterator) + 'apple' + >>> it.roman() + 'i' + >>> next(it._iterator) + 'pear' + >>> it.roman() + 'ii' + >>> next(it._iterator) + 'orange' + >>> it.roman() + 'iii' + """ + + return self.Roman().lower() + + +if interfaces is not None: + zope.interface.classImplements(RepeatItem, interfaces.ITALESIterator) + + +class RepeatDict(dict): + """Repeat dictionary implementation. + + >>> repeat = RepeatDict({}) + >>> iterator, length = repeat('numbers', range(5)) + >>> length + 5 + + >>> repeat['numbers'] + <chameleon.tal.RepeatItem object at ...> + + >>> repeat.numbers + <chameleon.tal.RepeatItem object at ...> + + >>> getattr(repeat, 'missing_key', None) is None + True + + >>> try: + ... from chameleon import interfaces + ... interfaces.ITALESIterator(repeat,None) is None + ... except ImportError: + ... True + ... + True + """ + + __slots__ = "__setitem__", "__getitem__" + + def __init__(self, d): + self.__setitem__ = d.__setitem__ + self.__getitem__ = d.__getitem__ + + def __getattr__(self,key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + + def __call__(self, key, iterable): + """We coerce the iterable to a tuple and return an iterator + after registering it in the repeat dictionary.""" + + iterable = list(iterable) if iterable is not None else () + + length = len(iterable) + iterator = iter(iterable) + + # Insert as repeat item + self[key] = RepeatItem(iterator, length) + + return iterator, length + + +class ErrorInfo(object): + """Information about an exception passed to an on-error handler.""" + + 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] + + +if interfaces is not None: + zope.interface.classImplements(ErrorInfo, interfaces.ITALExpressionErrorInfo) diff --git a/src/chameleon/tales.py b/src/chameleon/tales.py new file mode 100644 index 0000000..e8c710c --- /dev/null +++ b/src/chameleon/tales.py @@ -0,0 +1,598 @@ +import re +import sys + +from .astutil import parse +from .astutil import store +from .astutil import load +from .astutil import ItemLookupOnAttributeErrorVisitor +from .codegen import TemplateCodeGenerator +from .codegen import template +from .codegen import reverse_builtin_map +from .astutil import Builtin +from .astutil import Symbol +from .exc import ExpressionError +from .utils import resolve_dotted +from .utils import Markup +from .utils import ast +from .tokenize import Token +from .parser import substitute +from .compiler import Interpolator + +try: + from .py26 import lookup_attr +except SyntaxError: + from .py25 import lookup_attr + + +split_parts = re.compile(r'(?<!\\)\|') +match_prefix = re.compile(r'^\s*([a-z][a-z0-9\-_]*):').match +re_continuation = re.compile(r'\\\s*$', re.MULTILINE) + +try: + from __builtin__ import basestring +except ImportError: + basestring = str + +exc_clear = getattr(sys, "exc_clear", None) + + +def resolve_global(value): + name = reverse_builtin_map.get(value) + if name is not None: + return Builtin(name) + + return Symbol(value) + + +def test(expression, engine=None, **env): + if engine is None: + engine = SimpleEngine() + + body = expression(store("result"), engine) + module = ast.Module(body) + module = ast.fix_missing_locations(module) + env['rcontext'] = {} + if exc_clear is not None: + env['__exc_clear'] = exc_clear + source = TemplateCodeGenerator(module).code + code = compile(source, '<string>', 'exec') + exec(code, env) + result = env["result"] + + if isinstance(result, basestring): + result = str(result) + + return result + + +def transform_attribute(node): + return template( + "lookup(object, name)", + lookup=Symbol(lookup_attr), + object=node.value, + name=ast.Str(s=node.attr), + mode="eval" + ) + + +class TalesExpr(object): + """Base class. + + This class helps implementations for the Template Attribute + Language Expression Syntax (TALES). + + The syntax evaluates one or more expressions, separated by '|' + (pipe). The first expression that succeeds, is returned. + + Expression: + + expression := (type ':')? line ('|' expression)? + line := .* + + Expression lines may not contain the pipe character unless + escaped. It has a special meaning: + + If the expression to the left of the pipe fails (raises one of the + exceptions listed in ``catch_exceptions``), evaluation proceeds to + the expression(s) on the right. + + Subclasses must implement ``translate`` which assigns a value for + a given expression. + + >>> class PythonPipeExpr(TalesExpr): + ... def translate(self, expression, target): + ... compiler = PythonExpr(expression) + ... return compiler(target, None) + + >>> test(PythonPipeExpr('foo | bar | 42')) + 42 + + >>> test(PythonPipeExpr('foo|42')) + 42 + """ + + exceptions = NameError, \ + ValueError, \ + AttributeError, \ + LookupError, \ + TypeError + + ignore_prefix = True + + def __init__(self, expression): + self.expression = expression + + def __call__(self, target, engine): + remaining = self.expression + assignments = [] + + while remaining: + if self.ignore_prefix and match_prefix(remaining) is not None: + compiler = engine.parse(remaining) + assignment = compiler.assign_value(target) + remaining = "" + else: + for m in split_parts.finditer(remaining): + expression = remaining[:m.start()] + remaining = remaining[m.end():] + break + else: + expression = remaining + remaining = "" + + expression = expression.replace('\\|', '|') + assignment = self.translate_proxy(engine, expression, target) + assignments.append(assignment) + + if not assignments: + if not remaining: + raise ExpressionError("No input:", remaining) + + assignments.append( + self.translate_proxy(engine, remaining, target) + ) + + for i, assignment in enumerate(reversed(assignments)): + if i == 0: + body = assignment + else: + body = [ast.TryExcept( + body=assignment, + handlers=[ast.ExceptHandler( + type=ast.Tuple( + elts=map(resolve_global, self.exceptions), + ctx=ast.Load()), + name=None, + body=body if exc_clear is None else body + [ + ast.Expr( + ast.Call( + func=load("__exc_clear"), + args=[], + keywords=[], + starargs=None, + kwargs=None, + ) + ) + ], + )], + )] + + return body + + def translate_proxy(self, engine, *args): + """Default implementation delegates to ``translate`` method.""" + + return self.translate(*args) + + def translate(self, expression, target): + """Return statements that assign a value to ``target``.""" + + raise NotImplementedError( + "Must be implemented by a subclass.") + + +class PathExpr(TalesExpr): + """Path expression compiler. + + Syntax:: + + PathExpr ::= Path [ '|' Path ]* + Path ::= variable [ '/' URL_Segment ]* + variable ::= Name + + For example:: + + request/cookies/oatmeal + nothing + here/some-file 2001_02.html.tar.gz/foo + root/to/branch | default + + When a path expression is evaluated, it attempts to traverse + each path, from left to right, until it succeeds or runs out of + paths. To traverse a path, it first fetches the object stored in + the variable. For each path segment, it traverses from the current + object to the subobject named by the path segment. + + Once a path has been successfully traversed, the resulting object + is the value of the expression. If it is a callable object, such + as a method or class, it is called. + + The semantics of traversal (and what it means to be callable) are + implementation-dependent (see the ``translate`` method). + """ + + def translate(self, expression, target): + raise NotImplementedError( + "Path expressions are not yet implemented. " + "It's unclear whether a general implementation " + "can be devised.") + + +class PythonExpr(TalesExpr): + r"""Python expression compiler. + + >>> test(PythonExpr('2 + 2')) + 4 + + The Python expression is a TALES expression. That means we can use + the pipe operator: + + >>> test(PythonExpr('foo | 2 + 2 | 5')) + 4 + + To include a pipe character, use a backslash escape sequence: + + >>> test(PythonExpr(r'"\|"')) + '|' + """ + + transform = ItemLookupOnAttributeErrorVisitor(transform_attribute) + + def parse(self, string): + return parse(string, 'eval').body + + def translate(self, expression, target): + # Strip spaces + string = expression.strip() + + # Conver line continuations to newlines + string = substitute(re_continuation, '\n', string) + + # Convert newlines to spaces + string = string.replace('\n', ' ') + + try: + value = self.parse(string) + except SyntaxError: + exc = sys.exc_info()[1] + raise ExpressionError(exc.msg, string) + + # Transform attribute lookups to allow fallback to item lookup + self.transform.visit(value) + + return [ast.Assign(targets=[target], value=value)] + + +class ImportExpr(object): + re_dotted = re.compile(r'^[A-Za-z.]+$') + + def __init__(self, expression): + self.expression = expression + + def __call__(self, target, engine): + string = self.expression.strip().replace('\n', ' ') + value = template( + "RESOLVE(NAME)", + RESOLVE=Symbol(resolve_dotted), + NAME=ast.Str(s=string), + mode="eval", + ) + return [ast.Assign(targets=[target], value=value)] + + +class NotExpr(object): + """Negates the expression. + + >>> engine = SimpleEngine(PythonExpr) + + >>> test(NotExpr('False'), engine) + True + >>> test(NotExpr('True'), engine) + False + """ + + def __init__(self, expression): + self.expression = expression + + def __call__(self, target, engine): + compiler = engine.parse(self.expression) + body = compiler.assign_value(target) + return body + template("target = not target", target=target) + + +class StructureExpr(object): + """Wraps the expression result as 'structure'. + + >>> engine = SimpleEngine(PythonExpr) + + >>> test(StructureExpr('\"<tt>foo</tt>\"'), engine) + '<tt>foo</tt>' + """ + + wrapper_class = Symbol(Markup) + + def __init__(self, expression): + self.expression = expression + + def __call__(self, target, engine): + compiler = engine.parse(self.expression) + body = compiler.assign_value(target) + return body + template( + "target = wrapper(target)", + target=target, + wrapper=self.wrapper_class + ) + + +class IdentityExpr(object): + """Identity expression. + + Exists to demonstrate the interface. + + >>> test(IdentityExpr('42')) + 42 + """ + + def __init__(self, expression): + self.expression = expression + + def __call__(self, target, engine): + compiler = engine.parse(self.expression) + return compiler.assign_value(target) + + +class StringExpr(object): + """Similar to the built-in ``string.Template``, but uses an + + expression engine to support pluggable string substitution + expressions. + + Expr string: + + string := (text | substitution) (string)? + substitution := ('$' variable | '${' expression '}') + text := .* + + In other words, an expression string can contain multiple + substitutions. The text- and substitution parts will be + concatenated back into a string. + + >>> test(StringExpr('Hello ${name}!'), name='world') + 'Hello world!' + + In the default configuration, braces may be omitted if the + expression is an identifier. + + >>> test(StringExpr('Hello $name!'), name='world') + 'Hello world!' + + The ``braces_required`` flag changes this setting: + + >>> test(StringExpr('Hello $name!', True)) + 'Hello $name!' + + To avoid interpolation, use two dollar symbols. Note that only a + single symbol will appear in the output. + + >>> test(StringExpr('$${name}')) + '${name}' + + In previous versions, it was possible to escape using a regular + backslash coding, but this is no longer supported. + + >>> test(StringExpr(r'\${name}'), name='Hello world!') + '\\\\Hello world!' + + Multiple interpolations in one: + + >>> test(StringExpr("Hello ${'a'}${'b'}${'c'}!")) + 'Hello abc!' + + Here's a more involved example taken from a javascript source: + + >>> result = test(StringExpr(\"\"\" + ... function($$, oid) { + ... $('#' + oid).autocomplete({source: ${'source'}}); + ... } + ... \"\"\")) + + >>> 'source: source' in result + True + + As noted previously, the double-dollar escape also affects + non-interpolation expressions. + + >>> 'function($, oid)' in result + True + + >>> test(StringExpr('test ${1}${2}')) + 'test 12' + + >>> test(StringExpr('test $${1}${2}')) + 'test ${1}2' + + >>> test(StringExpr('test $$')) + 'test $' + + >>> test(StringExpr('$$.ajax(...)')) + '$.ajax(...)' + + >>> test(StringExpr('test $$ ${1}')) + 'test $ 1' + + In the above examples, the expression is evaluated using the + dummy engine which just returns the input as a string. + + As an example, we'll implement an expression engine which + instead counts the number of characters in the expresion and + returns an integer result. + + >>> class engine: + ... @staticmethod + ... def parse(expression, char_escape=None): + ... class compiler: + ... @staticmethod + ... def assign_text(target): + ... return [ + ... ast.Assign( + ... targets=[target], + ... value=ast.Num(n=len(expression)) + ... )] + ... + ... return compiler + + This will demonstrate how the string expression coerces the + input to a string. + + >>> expr = StringExpr( + ... 'There are ${hello world} characters in \"hello world\"') + + We evaluate the expression using the new engine: + + >>> test(expr, engine) + 'There are 11 characters in \"hello world\"' + """ + + def __init__(self, expression, braces_required=False): + # The code relies on the expression being a token string + if not isinstance(expression, Token): + expression = Token(expression, 0) + + self.translator = Interpolator(expression, braces_required) + + def __call__(self, name, engine): + return self.translator(name, engine) + + +class ProxyExpr(TalesExpr): + braces_required = False + + def __init__(self, name, expression, ignore_prefix=True): + super(ProxyExpr, self).__init__(expression) + self.ignore_prefix = ignore_prefix + self.name = name + + def translate_proxy(self, engine, expression, target): + translator = Interpolator(expression, self.braces_required) + assignment = translator(target, engine) + + return assignment + [ + ast.Assign(targets=[target], value=ast.Call( + func=load(self.name), + args=[target], + keywords=[], + starargs=None, + kwargs=None + )) + ] + + +class ExistsExpr(object): + """Boolean wrapper. + + Return 0 if the expression results in an exception, otherwise 1. + + As a means to generate exceptions, we set up an expression engine + which evaluates the provided expression using Python: + + >>> engine = SimpleEngine(PythonExpr) + + >>> test(ExistsExpr('int(0)'), engine) + 1 + >>> test(ExistsExpr('int(None)'), engine) + 0 + + """ + + exceptions = AttributeError, LookupError, TypeError, NameError, KeyError + + def __init__(self, expression): + self.expression = expression + + def __call__(self, target, engine): + ignore = store("_ignore") + compiler = engine.parse(self.expression, False) + body = compiler.assign_value(ignore) + + classes = map(resolve_global, self.exceptions) + + return [ + ast.TryExcept( + body=body, + handlers=[ast.ExceptHandler( + type=ast.Tuple(elts=classes, ctx=ast.Load()), + name=None, + body=template("target = 0", target=target), + )], + orelse=template("target = 1", target=target) + ) + ] + + +class ExpressionParser(object): + def __init__(self, factories, default): + self.factories = factories + self.default = default + + def __call__(self, expression): + m = match_prefix(expression) + if m is not None: + prefix = m.group(1) + expression = expression[m.end():] + else: + prefix = self.default + + try: + factory = self.factories[prefix] + except KeyError: + exc = sys.exc_info()[1] + raise LookupError( + "Unknown expression type: %s." % str(exc) + ) + + return factory(expression) + + +class SimpleEngine(object): + expression = PythonExpr + + def __init__(self, expression=None): + if expression is not None: + self.expression = expression + + def parse(self, string, handle_errors=False, char_escape=None): + compiler = self.expression(string) + return SimpleCompiler(compiler, self) + + +class SimpleCompiler(object): + def __init__(self, compiler, engine): + self.compiler = compiler + self.engine = engine + + def assign_text(self, target): + """Assign expression string as a text value.""" + + return self._assign_value_and_coerce(target, "str") + + def assign_value(self, target): + """Assign expression string as object value.""" + + return self.compiler(target, self.engine) + + def _assign_value_and_coerce(self, target, builtin): + return self.assign_value(target) + template( + "target = builtin(target)", + target=target, + builtin=builtin + ) diff --git a/src/chameleon/template.py b/src/chameleon/template.py new file mode 100644 index 0000000..1cbcc07 --- /dev/null +++ b/src/chameleon/template.py @@ -0,0 +1,356 @@ +from __future__ import with_statement + +import os +import sys +import hashlib +import logging +import tempfile +import inspect + +try: + RecursionError +except NameError: + RecursionError = RuntimeError + +def get_package_versions(): + try: + import pkg_resources + except ImportError: + logging.info("Setuptools not installed. Unable to determine version.") + return [] + + versions = dict() + for path in sys.path: + for distribution in pkg_resources.find_distributions(path): + if distribution.has_version(): + versions.setdefault( + distribution.project_name, + distribution.version, + ) + + return sorted(versions.items()) + + +pkg_digest = hashlib.sha1(__name__.encode('utf-8')) +for name, version in get_package_versions(): + pkg_digest.update(name.encode('utf-8')) + pkg_digest.update(version.encode('utf-8')) + + +from .exc import RenderError +from .exc import TemplateError +from .exc import ExceptionFormatter +from .compiler import Compiler +from .config import DEBUG_MODE +from .config import AUTO_RELOAD +from .config import EAGER_PARSING +from .config import CACHE_DIRECTORY +from .loader import ModuleLoader +from .loader import MemoryLoader +from .nodes import Module +from .utils import DebuggingOutputStream +from .utils import Scope +from .utils import join +from .utils import mangle +from .utils import create_formatted_exception +from .utils import read_bytes +from .utils import raise_with_traceback +from .utils import byte_string + + +log = logging.getLogger('chameleon.template') + + +def _make_module_loader(): + remove = False + if CACHE_DIRECTORY: + path = CACHE_DIRECTORY + else: + path = tempfile.mkdtemp() + remove = True + + return ModuleLoader(path, remove) + + +class BaseTemplate(object): + """Template base class. + + Takes a string input which must be one of the following: + + - a unicode string (or string on Python 3); + - a utf-8 encoded byte string; + - a byte string for an XML document that defines an encoding + in the document premamble; + - an HTML document that specifies the encoding via the META tag. + + Note that the template input is decoded, parsed and compiled on + initialization. + """ + + default_encoding = "utf-8" + + # This attribute is strictly informational in this template class + # and is used in exception formatting. It may be set on + # initialization using the optional ``filename`` keyword argument. + filename = '<string>' + + _cooked = False + + if DEBUG_MODE or CACHE_DIRECTORY: + loader = _make_module_loader() + else: + loader = MemoryLoader() + + if DEBUG_MODE: + output_stream_factory = DebuggingOutputStream + else: + output_stream_factory = list + + debug = DEBUG_MODE + + # The ``builtins`` dictionary can be used by a template class to + # add symbols which may not be redefined and which are (cheaply) + # available in the template variable scope + builtins = {} + + # The ``builtins`` dictionary is updated with this dictionary at + # cook time. Note that it can be provided at class initialization + # using the ``extra_builtins`` keyword argument. + extra_builtins = {} + + # Expression engine must be provided by subclass + engine = None + + # When ``strict`` is set, expressions must be valid at compile + # time. When not set, this is only required at evaluation time. + strict = True + + def __init__(self, body=None, **config): + self.__dict__.update(config) + + if body is not None: + self.write(body) + + # This is only necessary if the ``debug`` flag was passed as a + # keyword argument + if self.__dict__.get('debug') is True: + self.loader = _make_module_loader() + + def __call__(self, **kwargs): + return self.render(**kwargs) + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.filename) + + @property + def keep_body(self): + # By default, we only save the template body if we're + # in debugging mode (to save memory). + return self.__dict__.get('keep_body', DEBUG_MODE) + + @property + def keep_source(self): + # By default, we only save the generated source code if we're + # in debugging mode (to save memory). + return self.__dict__.get('keep_source', DEBUG_MODE) + + def cook(self, body): + builtins_dict = self.builtins.copy() + builtins_dict.update(self.extra_builtins) + names, builtins = zip(*sorted(builtins_dict.items())) + digest = self.digest(body, names) + program = self._cook(body, digest, names) + + initialize = program['initialize'] + functions = initialize(*builtins) + + for name, function in functions.items(): + setattr(self, "_" + name, function) + + self._cooked = True + + if self.keep_body: + self.body = body + + def cook_check(self): + assert self._cooked + + def parse(self, body): + raise NotImplementedError("Must be implemented by subclass.") + + def render(self, **__kw): + econtext = Scope(__kw) + rcontext = {} + self.cook_check() + stream = self.output_stream_factory() + try: + self._render(stream, econtext, rcontext) + except RecursionError: + raise + except: + cls, exc, tb = sys.exc_info() + errors = rcontext.get('__error__') + if errors: + formatter = exc.__str__ + if isinstance(formatter, ExceptionFormatter): + if errors is not formatter._errors: + formatter._errors.extend(errors) + raise + + formatter = ExceptionFormatter(errors, econtext, rcontext) + + try: + exc = create_formatted_exception( + exc, cls, formatter, RenderError + ) + except TypeError: + pass + + raise_with_traceback(exc, tb) + + raise + + return join(stream) + + def write(self, body): + if isinstance(body, byte_string): + body, encoding, content_type = read_bytes( + body, self.default_encoding + ) + else: + content_type = body.startswith('<?xml') + encoding = None + + self.content_type = content_type + self.content_encoding = encoding + + self.cook(body) + + def _get_module_name(self, name): + return "%s.py" % name + + def _cook(self, body, name, builtins): + filename = self._get_module_name(name) + cooked = self.loader.get(filename) + if cooked is None: + try: + source = self._compile(body, builtins) + if self.debug: + source = "# template: %s\n#\n%s" % ( + self.filename, source) + if self.keep_source: + self.source = source + cooked = self.loader.build(source, filename) + except TemplateError: + exc = sys.exc_info()[1] + exc.token.filename = self.filename + raise + elif self.keep_source: + module = sys.modules.get(cooked.get('__name__')) + if module is not None: + self.source = inspect.getsource(module) + else: + self.source = None + return cooked + + def digest(self, body, names): + class_name = type(self).__name__.encode('utf-8') + sha = pkg_digest.copy() + sha.update(body.encode('utf-8', 'ignore')) + sha.update(class_name) + digest = sha.hexdigest() + + if self.filename is not BaseTemplate.filename: + digest = os.path.splitext(self.filename)[0] + '-' + digest + + return digest + + def _compile(self, body, builtins): + program = self.parse(body) + module = Module("initialize", program) + compiler = Compiler( + self.engine, module, self.filename, body, + builtins, strict=self.strict + ) + return compiler.code + + +class BaseTemplateFile(BaseTemplate): + """File-based template base class. + + Relative path names are supported only when a template loader is + provided as the ``loader`` parameter. + """ + + # Auto reload is not enabled by default because it's a significant + # performance hit + auto_reload = AUTO_RELOAD + + def __init__(self, filename, auto_reload=None, **config): + # Normalize filename + filename = os.path.abspath( + os.path.normpath(os.path.expanduser(filename)) + ) + + self.filename = filename + + # Override reload setting only if value is provided explicitly + if auto_reload is not None: + self.auto_reload = auto_reload + + super(BaseTemplateFile, self).__init__(**config) + + if EAGER_PARSING: + self.cook_check() + + def cook_check(self): + if self.auto_reload: + mtime = self.mtime() + + if mtime != self._v_last_read: + self._v_last_read = mtime + self._cooked = False + + if self._cooked is False: + body = self.read() + log.debug("cooking %r (%d bytes)..." % (self.filename, len(body))) + self.cook(body) + + def mtime(self): + try: + return os.path.getmtime(self.filename) + except (IOError, OSError): + return 0 + + def read(self): + with open(self.filename, "rb") as f: + data = f.read() + + body, encoding, content_type = read_bytes( + data, self.default_encoding + ) + + # In non-XML mode, we support various platform-specific line + # endings and convert them to the UNIX newline character + if content_type != "text/xml" and '\r' in body: + body = body.replace('\r\n', '\n').replace('\r', '\n') + + self.content_type = content_type + self.content_encoding = encoding + + return body + + def _get_module_name(self, name): + filename = os.path.basename(self.filename) + mangled = mangle(filename) + return "%s_%s.py" % (mangled, name) + + def _get_filename(self): + return self.__dict__.get('filename') + + def _set_filename(self, filename): + self.__dict__['filename'] = filename + self._v_last_read = None + self._cooked = False + + filename = property(_get_filename, _set_filename) diff --git a/src/chameleon/tests/__init__.py b/src/chameleon/tests/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/src/chameleon/tests/__init__.py @@ -0,0 +1 @@ +# diff --git a/src/chameleon/tests/inputs/001-interpolation.txt b/src/chameleon/tests/inputs/001-interpolation.txt new file mode 100644 index 0000000..b2550bc --- /dev/null +++ b/src/chameleon/tests/inputs/001-interpolation.txt @@ -0,0 +1 @@ +${'<Hello world>'}<&> diff --git a/src/chameleon/tests/inputs/001-variable-scope.html b/src/chameleon/tests/inputs/001-variable-scope.html new file mode 100644 index 0000000..c66087a --- /dev/null +++ b/src/chameleon/tests/inputs/001-variable-scope.html @@ -0,0 +1,7 @@ +<html> + <body py:with="text 'Hello world!'"> + ${text} + $text + </body> + ${text | 'Goodbye world!'} +</html> diff --git a/src/chameleon/tests/inputs/001-variable-scope.pt b/src/chameleon/tests/inputs/001-variable-scope.pt new file mode 100644 index 0000000..e46ee35 --- /dev/null +++ b/src/chameleon/tests/inputs/001-variable-scope.pt @@ -0,0 +1,11 @@ +<html> + <body tal:define="text 'Hello world!'"> + ${text} + </body> + <tal:check condition="exists: text"> + bad + </tal:check> + <tal:check condition="not: exists: text"> + ok + </tal:check> +</html> diff --git a/src/chameleon/tests/inputs/001.xml b/src/chameleon/tests/inputs/001.xml new file mode 100644 index 0000000..7fbef49 --- /dev/null +++ b/src/chameleon/tests/inputs/001.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/002-repeat-scope.pt b/src/chameleon/tests/inputs/002-repeat-scope.pt new file mode 100644 index 0000000..e9a8a1a --- /dev/null +++ b/src/chameleon/tests/inputs/002-repeat-scope.pt @@ -0,0 +1,8 @@ +<html> + <body> + <div tal:repeat="text ('Hello', 'Goodbye')"> + <span tal:repeat="char ('!', '.')">${text}${char}</span> + </div> + <tal:check condition="not: exists: text">ok</tal:check> + </body> +</html> diff --git a/src/chameleon/tests/inputs/002.xml b/src/chameleon/tests/inputs/002.xml new file mode 100644 index 0000000..2e3f1d8 --- /dev/null +++ b/src/chameleon/tests/inputs/002.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc ></doc>
diff --git a/src/chameleon/tests/inputs/003-content.pt b/src/chameleon/tests/inputs/003-content.pt new file mode 100644 index 0000000..9865fc2 --- /dev/null +++ b/src/chameleon/tests/inputs/003-content.pt @@ -0,0 +1,17 @@ +<html> + <body> + <div tal:content="'Hello world!'" /> + <div tal:content="'Hello world!'" />1 + 2<div tal:content="'Hello world!'" /> + <div tal:content="'Hello world!'" />3 + <div tal:content="'Hello world!'">4</div>5 + 6<div tal:content="'Hello world!'"></div> + <div tal:content="1" /> + <div tal:content="1.0" /> + <div tal:content="True" /> + <div tal:content="False" /> + <div tal:content="0" /> + <div tal:content="None" /> + <div tal:replace="content" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/003.xml b/src/chameleon/tests/inputs/003.xml new file mode 100644 index 0000000..c841b81 --- /dev/null +++ b/src/chameleon/tests/inputs/003.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc >
diff --git a/src/chameleon/tests/inputs/004-attributes.pt b/src/chameleon/tests/inputs/004-attributes.pt new file mode 100644 index 0000000..d31ba26 --- /dev/null +++ b/src/chameleon/tests/inputs/004-attributes.pt @@ -0,0 +1,25 @@ +<html> + <body> + <span tal:attributes="class 'hello'" /> + <span class="goodbye" tal:attributes="class 'hello'" /> + <span CLASS="goodbye" tal:attributes="class 'hello'" /> + <span data-æøå="goodbye" tal:attributes="data-æøå 'hello'" /> + <span tal:attributes="class None" /> + <span a="1" b="2" c="3" d=4 tal:attributes="a None"></span> + <span a="1" b="2" c="3" tal:attributes="b None" /> + <span a="1" b="2" c="3" tal:attributes="c None" /> + <span a="1" b="2" c="3" tal:attributes="b None; c None" /> + <span a="1" b="2" c="3" tal:attributes="b string:;;" /> + <span a="1" b="2" c="3" tal:attributes="b string:&" /> + <span class="hello" tal:attributes="class 'goodbye'" /> + <span class="hello" tal:attributes="class '"goodbye"'" /> + <span class="hello" tal:attributes="class '\'goodbye\''" /> + <span class='hello' tal:attributes="class '\'goodbye\''" /> + <span tal:attributes="{'class': 'goodbye'}" /> + <span class="hello" tal:attributes="{'class': 'goodbye'}" /> + <span a="1" class="hello" tal:attributes="{'class': 'goodbye'}" /> + <span tal:attributes="{'class': '"goodbye"'}" /> + <span tal:attributes="class 'hello'; {'class': '"goodbye"'}" /> + <span tal:attributes="{'class': '"goodbye"'}; class 'hello'" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/004.xml b/src/chameleon/tests/inputs/004.xml new file mode 100644 index 0000000..a9c5756 --- /dev/null +++ b/src/chameleon/tests/inputs/004.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+]>
+<doc a1="v1"></doc>
diff --git a/src/chameleon/tests/inputs/005-default.pt b/src/chameleon/tests/inputs/005-default.pt new file mode 100644 index 0000000..6e0f833 --- /dev/null +++ b/src/chameleon/tests/inputs/005-default.pt @@ -0,0 +1,12 @@ +<html> + <body> + <img class="default" tal:attributes="class default" /> + <img tal:attributes="class default" /> + <span tal:content="default">Default</span> + <span tal:content="True">Default</span> + <span tal:content="False">Default</span> + <span tal:content="default"> + <em>${'Computed default'}</em> + </span> + </body> +</html> diff --git a/src/chameleon/tests/inputs/005.xml b/src/chameleon/tests/inputs/005.xml new file mode 100644 index 0000000..b069efe --- /dev/null +++ b/src/chameleon/tests/inputs/005.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+]>
+<doc a1 = "v1"></doc>
diff --git a/src/chameleon/tests/inputs/006-attribute-interpolation.pt b/src/chameleon/tests/inputs/006-attribute-interpolation.pt new file mode 100644 index 0000000..b45f8c3 --- /dev/null +++ b/src/chameleon/tests/inputs/006-attribute-interpolation.pt @@ -0,0 +1,9 @@ +<html> + <body class="ltr" tal:define="hash string:#"> + <img src="${'#'}" alt="copyright (c) ${2010}" /> + <img src="" alt="copyright (c) ${2010}" tal:attributes="src string:$hash" /> + <img src="" alt="copyright (c) ${2010}" tal:attributes="src string:${hash}" /> + <img src="${None}" alt="$ignored" /> + <img src="" alt="${'%stype \'str\'%s' % (chr(60), chr(62))}" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/006.xml b/src/chameleon/tests/inputs/006.xml new file mode 100644 index 0000000..39a3463 --- /dev/null +++ b/src/chameleon/tests/inputs/006.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+]>
+<doc a1='v1'></doc>
diff --git a/src/chameleon/tests/inputs/007-content-interpolation.pt b/src/chameleon/tests/inputs/007-content-interpolation.pt new file mode 100644 index 0000000..c82ce97 --- /dev/null +++ b/src/chameleon/tests/inputs/007-content-interpolation.pt @@ -0,0 +1,21 @@ +<html> + <body> + ${'Hello world!'} + ${literal} + ${structure: literal.s} + ${"%stype 'str'%s" % (chr(60), chr(62))} + && + <script> + $$(document).ready(function(){ + imagecropping.init_editor(); + }); + </script> + ${None} + ${None or + 'Hello world'} + $leftalone + <div>${None}</div> + <div>${1 < 2 and 'Hello world' or None}</div> + <div>${} is ignored.</div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/007.xml b/src/chameleon/tests/inputs/007.xml new file mode 100644 index 0000000..cc3dc53 --- /dev/null +++ b/src/chameleon/tests/inputs/007.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc> </doc>
diff --git a/src/chameleon/tests/inputs/008-builtins.pt b/src/chameleon/tests/inputs/008-builtins.pt new file mode 100644 index 0000000..46d5297 --- /dev/null +++ b/src/chameleon/tests/inputs/008-builtins.pt @@ -0,0 +1,12 @@ +<html> + <body> + ${attrs} + ${nothing} + <div tal:attributes="class string:dynamic" class="static"> + ${attrs['class']} + </div> + <div tal:define="nothing string:nothing"> + ${nothing} + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/008.xml b/src/chameleon/tests/inputs/008.xml new file mode 100644 index 0000000..b3370eb --- /dev/null +++ b/src/chameleon/tests/inputs/008.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>&<>"'</doc>
diff --git a/src/chameleon/tests/inputs/009-literals.pt b/src/chameleon/tests/inputs/009-literals.pt new file mode 100644 index 0000000..ea9c8e6 --- /dev/null +++ b/src/chameleon/tests/inputs/009-literals.pt @@ -0,0 +1,5 @@ +<html> + <body> + ${literal} + </body> +</html> diff --git a/src/chameleon/tests/inputs/009.xml b/src/chameleon/tests/inputs/009.xml new file mode 100644 index 0000000..0fa183e --- /dev/null +++ b/src/chameleon/tests/inputs/009.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc> </doc>
diff --git a/src/chameleon/tests/inputs/010-structure.pt b/src/chameleon/tests/inputs/010-structure.pt new file mode 100644 index 0000000..7958919 --- /dev/null +++ b/src/chameleon/tests/inputs/010-structure.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div tal:content="text string:1 < 2" /> + <div tal:content="structure string:2 < 3, 2&3, 2<3, 2>3" /> + <div tal:content="structure string:3 ${'<'} 4" /> + <div tal:content="structure '%d < %d' % (4, 5)" /> + <div tal:replace="structure content" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/010.xml b/src/chameleon/tests/inputs/010.xml new file mode 100644 index 0000000..eb64d18 --- /dev/null +++ b/src/chameleon/tests/inputs/010.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+]>
+<doc a1="v1" ></doc>
diff --git a/src/chameleon/tests/inputs/011-messages.pt b/src/chameleon/tests/inputs/011-messages.pt new file mode 100644 index 0000000..bf3ffbe --- /dev/null +++ b/src/chameleon/tests/inputs/011-messages.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div tal:content="text message" /> + <div tal:content="structure message" /> + <div tal:content="text string:${message}" /> + <div tal:content="structure string:${message}" /> + ${message} + </body> +</html> diff --git a/src/chameleon/tests/inputs/011.xml b/src/chameleon/tests/inputs/011.xml new file mode 100644 index 0000000..4cac44b --- /dev/null +++ b/src/chameleon/tests/inputs/011.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED a2 CDATA #IMPLIED>
+]>
+<doc a1="v1" a2="v2"></doc>
diff --git a/src/chameleon/tests/inputs/012-translation.pt b/src/chameleon/tests/inputs/012-translation.pt new file mode 100644 index 0000000..138fbd9 --- /dev/null +++ b/src/chameleon/tests/inputs/012-translation.pt @@ -0,0 +1,22 @@ +<html> + <body> + <div i18n:translate=""></div> + <div i18n:translate=""> + Hello world! + </div> + <div i18n:translate="hello_world"> + Hello world! + </div> + <div i18n:translate=""> + <sup>Hello world!</sup> + </div> + <div i18n:translate=""> + Hello <em i18n:name="first">${'world'}</em>! + Goodbye <em i18n:name="second">${'planet'}</em>! + </div> + <div i18n:translate="hello_goodbye"> + Hello <em i18n:name="first">${'world'}</em>! + Goodbye <em i18n:name="second">${'planet'}</em>! + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/012.xml b/src/chameleon/tests/inputs/012.xml new file mode 100644 index 0000000..6ce2a3e --- /dev/null +++ b/src/chameleon/tests/inputs/012.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc : CDATA #IMPLIED>
+]>
+<doc :="v1"></doc>
diff --git a/src/chameleon/tests/inputs/013-repeat-nested.pt b/src/chameleon/tests/inputs/013-repeat-nested.pt new file mode 100644 index 0000000..e93357e --- /dev/null +++ b/src/chameleon/tests/inputs/013-repeat-nested.pt @@ -0,0 +1,11 @@ +<html> + <body> + <table> + <tr tal:repeat="i (1,2)"> + <td tal:repeat="j (1,2)"> + [${i},${j}] + </td> + </tr> + </table> + </body> +</html> diff --git a/src/chameleon/tests/inputs/013.xml b/src/chameleon/tests/inputs/013.xml new file mode 100644 index 0000000..2f4aae4 --- /dev/null +++ b/src/chameleon/tests/inputs/013.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc _.-0123456789 CDATA #IMPLIED>
+]>
+<doc _.-0123456789="v1"></doc>
diff --git a/src/chameleon/tests/inputs/014-repeat-nested-similar.pt b/src/chameleon/tests/inputs/014-repeat-nested-similar.pt new file mode 100644 index 0000000..d656579 --- /dev/null +++ b/src/chameleon/tests/inputs/014-repeat-nested-similar.pt @@ -0,0 +1,7 @@ +<html> + <body> + <span tal:repeat="i (3,4)"> + <span tal:repeat="j (3,4)">[${i},${j}]</span> + </span> + </body> +</html> diff --git a/src/chameleon/tests/inputs/014.xml b/src/chameleon/tests/inputs/014.xml new file mode 100644 index 0000000..47f1f72 --- /dev/null +++ b/src/chameleon/tests/inputs/014.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc abcdefghijklmnopqrstuvwxyz CDATA #IMPLIED>
+]>
+<doc abcdefghijklmnopqrstuvwxyz="v1"></doc>
diff --git a/src/chameleon/tests/inputs/015-translation-nested.pt b/src/chameleon/tests/inputs/015-translation-nested.pt new file mode 100644 index 0000000..6776201 --- /dev/null +++ b/src/chameleon/tests/inputs/015-translation-nested.pt @@ -0,0 +1,10 @@ +<html> + <body> + <div i18n:translate=""> + Price: + <span i18n:name="price" i18n:translate=""> + Per kilo <em i18n:name="amount">${12.5}</em> + </span> + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/015.xml b/src/chameleon/tests/inputs/015.xml new file mode 100644 index 0000000..861df8a --- /dev/null +++ b/src/chameleon/tests/inputs/015.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc ABCDEFGHIJKLMNOPQRSTUVWXYZ CDATA #IMPLIED>
+]>
+<doc ABCDEFGHIJKLMNOPQRSTUVWXYZ="v1"></doc>
diff --git a/src/chameleon/tests/inputs/016-explicit-translation.pt b/src/chameleon/tests/inputs/016-explicit-translation.pt new file mode 100644 index 0000000..52f8278 --- /dev/null +++ b/src/chameleon/tests/inputs/016-explicit-translation.pt @@ -0,0 +1,11 @@ +<html> + <body> + <div i18n:translate="" tal:content="string:Hello world!"> + Hello world! + </div> + <img alt="${'Hello world!'}" i18n:attributes="alt" /> + <img alt="${'Hello world!'}" i18n:attributes="alt hello_world" /> + <img tal:attributes="alt 'Hello world!'" i18n:attributes="alt" /> + <img tal:attributes="alt 'Hello world!'" i18n:attributes="alt hello_world" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/016.xml b/src/chameleon/tests/inputs/016.xml new file mode 100644 index 0000000..66b1973 --- /dev/null +++ b/src/chameleon/tests/inputs/016.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><?pi?></doc>
diff --git a/src/chameleon/tests/inputs/017-omit-tag.pt b/src/chameleon/tests/inputs/017-omit-tag.pt new file mode 100644 index 0000000..6a25141 --- /dev/null +++ b/src/chameleon/tests/inputs/017-omit-tag.pt @@ -0,0 +1,12 @@ +<html> + <body> + <div tal:omit-tag="">Hello world!</div> + <div tal:omit-tag="">1 + Hello world! + 2</div>3 + 4<div tal:omit-tag="True">Hello world!</div> + <div tal:omit-tag="False">Hello world!</div> + <div class="omitted" tal:omit-tag="True">Hello world!</div> + <div class="${'omitted'}" tal:omit-tag="True">Hello world!</div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/017.xml b/src/chameleon/tests/inputs/017.xml new file mode 100644 index 0000000..827ba96 --- /dev/null +++ b/src/chameleon/tests/inputs/017.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><?pi some data ? > <??></doc>
diff --git a/src/chameleon/tests/inputs/018-translation-nested-dynamic.pt b/src/chameleon/tests/inputs/018-translation-nested-dynamic.pt new file mode 100644 index 0000000..7753c75 --- /dev/null +++ b/src/chameleon/tests/inputs/018-translation-nested-dynamic.pt @@ -0,0 +1,13 @@ +<div xmlns="http://www.w3.org/1999/xhtml" + xmlns:i18n="http://xml.zope.org/namespaces/i18n"> + <div i18n:translate="" tal:omit-tag=""> + <span i18n:name="monthname" + i18n:translate="" + tal:content="'october'" + tal:omit-tag="">monthname</span> + <span i18n:name="year" + i18n:translate="" + tal:content="1982" + tal:omit-tag="">year</span> + </div> +</div> diff --git a/src/chameleon/tests/inputs/018.xml b/src/chameleon/tests/inputs/018.xml new file mode 100644 index 0000000..4570903 --- /dev/null +++ b/src/chameleon/tests/inputs/018.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><![CDATA[<foo>]]></doc>
diff --git a/src/chameleon/tests/inputs/019-replace.pt b/src/chameleon/tests/inputs/019-replace.pt new file mode 100644 index 0000000..173a4fb --- /dev/null +++ b/src/chameleon/tests/inputs/019-replace.pt @@ -0,0 +1,13 @@ +<html> + <body> + <div tal:replace="'Hello world!'" /> + <div tal:replace="'Hello world!'" />1 + 2<div tal:replace="'Hello world!'" /> + <div tal:replace="'Hello world!'" />3 + <div tal:replace="'Hello world!'">4</div>5 + 6<div tal:replace="'Hello world!'"></div> + <div tal:replace="1" /> + <div tal:replace="1.0" /> + <div tal:replace="True" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/019.xml b/src/chameleon/tests/inputs/019.xml new file mode 100644 index 0000000..3e6b74c --- /dev/null +++ b/src/chameleon/tests/inputs/019.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><![CDATA[<&]]></doc>
diff --git a/src/chameleon/tests/inputs/020-on-error.pt b/src/chameleon/tests/inputs/020-on-error.pt new file mode 100644 index 0000000..b151109 --- /dev/null +++ b/src/chameleon/tests/inputs/020-on-error.pt @@ -0,0 +1,10 @@ +<html> + <body> + <div id="test" tal:attributes="class python: 'abc' + 2" tal:on-error="nothing" /> + <div tal:on-error="string:${type(error.value).__name__} thrown at ${error.lineno}:${error.offset}."> + <div tal:content="undefined" /> + </div> + <div tal:replace="undefined" tal:on-error="nothing" /> + <div tal:content="undefined" tal:on-error="nothing" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/020.xml b/src/chameleon/tests/inputs/020.xml new file mode 100644 index 0000000..f749551 --- /dev/null +++ b/src/chameleon/tests/inputs/020.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><![CDATA[<&]>]]]></doc>
diff --git a/src/chameleon/tests/inputs/021-translation-domain.pt b/src/chameleon/tests/inputs/021-translation-domain.pt new file mode 100644 index 0000000..4d40d10 --- /dev/null +++ b/src/chameleon/tests/inputs/021-translation-domain.pt @@ -0,0 +1,16 @@ +<html> + <body i18n:domain="old"> + <div i18n:domain="new" i18n:translate=""> + Hello world! + </div> + <div i18n:translate=""> + Hello world! + </div> + <div class="test" i18n:domain="new" i18n:attributes="class"> + Hello world! + </div> + <div class="test" i18n:domain="new" i18n:attributes="class test_msgid"> + Hello world! + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/021.xml b/src/chameleon/tests/inputs/021.xml new file mode 100644 index 0000000..13dda8c --- /dev/null +++ b/src/chameleon/tests/inputs/021.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><!-- a comment --></doc>
diff --git a/src/chameleon/tests/inputs/022-switch.pt b/src/chameleon/tests/inputs/022-switch.pt new file mode 100644 index 0000000..b616aa3 --- /dev/null +++ b/src/chameleon/tests/inputs/022-switch.pt @@ -0,0 +1,21 @@ +<html> + <body> + <div tal:switch="True"> + <span tal:case="False">bad</span> + <span tal:case="True">ok</span> + <span tal:case="True">ok</span> + <span tal:case="default">bad</span> + <span tal:case="True">bad</span> + ${default|string:ok} + </div> + <div tal:switch="True"> + <span tal:case="False">bad</span> + <span tal:case="default">ok</span> + </div> + <div tal:switch="3"> + <span tal:case="1">bad</span> + <span tal:case="2">bad</span> + <span tal:case="default">ok</span> + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/022.xml b/src/chameleon/tests/inputs/022.xml new file mode 100644 index 0000000..41d300e --- /dev/null +++ b/src/chameleon/tests/inputs/022.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><!-- a comment ->--></doc>
diff --git a/src/chameleon/tests/inputs/023-condition.pt b/src/chameleon/tests/inputs/023-condition.pt new file mode 100644 index 0000000..b7661d5 --- /dev/null +++ b/src/chameleon/tests/inputs/023-condition.pt @@ -0,0 +1,6 @@ +<html> + <body tal:condition="True"> + <span tal:define="selector False" tal:condition="selector">bad</span> + <span tal:condition="True">ok</span> + </body> +</html> diff --git a/src/chameleon/tests/inputs/023.xml b/src/chameleon/tests/inputs/023.xml new file mode 100644 index 0000000..3837b83 --- /dev/null +++ b/src/chameleon/tests/inputs/023.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e "">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/024-namespace-elements.pt b/src/chameleon/tests/inputs/024-namespace-elements.pt new file mode 100644 index 0000000..9c29e6c --- /dev/null +++ b/src/chameleon/tests/inputs/024-namespace-elements.pt @@ -0,0 +1,16 @@ +<html> + <body> + <tal:first> + <tal:second> + ${'first'} + </tal:second> + second + </tal:first> + <tal:block condition="True"> + ok + </tal:block> + <tal:block condition="False"> + bad + </tal:block> + </body> +</html> diff --git a/src/chameleon/tests/inputs/024.xml b/src/chameleon/tests/inputs/024.xml new file mode 100644 index 0000000..b0655c6 --- /dev/null +++ b/src/chameleon/tests/inputs/024.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (foo)>
+<!ELEMENT foo (#PCDATA)>
+<!ENTITY e "<foo></foo>">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/025-repeat-whitespace.pt b/src/chameleon/tests/inputs/025-repeat-whitespace.pt new file mode 100644 index 0000000..3f448b4 --- /dev/null +++ b/src/chameleon/tests/inputs/025-repeat-whitespace.pt @@ -0,0 +1,15 @@ +<html> + <body> + <ul> + <li tal:repeat="i (1, 2, 3)" tal:content="i" /> + <tal:item repeat="i (1, 2, 3)"><li tal:content="i" /></tal:item> + <span tal:omit-tag="" tal:repeat="j (1, 2, 3)"><li tal:content="j" /></span> + <tal:count> + <tal:count-loop repeat="count (1, 2, 3)"> + <span tal:replace="count" + /><tal:comma condition="not repeat['count'].end">,</tal:comma> + </tal:count-loop> + </tal:count>. + </ul> + </body> +</html> diff --git a/src/chameleon/tests/inputs/025.xml b/src/chameleon/tests/inputs/025.xml new file mode 100644 index 0000000..ed01f36 --- /dev/null +++ b/src/chameleon/tests/inputs/025.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (foo*)>
+<!ELEMENT foo (#PCDATA)>
+]>
+<doc><foo/><foo></foo></doc>
diff --git a/src/chameleon/tests/inputs/026-repeat-variable.pt b/src/chameleon/tests/inputs/026-repeat-variable.pt new file mode 100644 index 0000000..c4bf48a --- /dev/null +++ b/src/chameleon/tests/inputs/026-repeat-variable.pt @@ -0,0 +1,13 @@ +<div xmlns="http://www.w3.org/1999/xhtml" + xmlns:tal="http://xml.zope.org/namespaces/tal"> + <ul> + <li tal:attributes="class repeat['i'].even()+repeat['i'].odd()" name="${i}-${repeat.i.index}" tal:repeat="i range(3)"><span tal:replace="i" /></li> + </ul> + <ul> + <li tal:attributes="class repeat['i'].even+repeat['i'].odd" + tal:repeat="i range(3)"><span tal:replace="i" /></li> + </ul> + <ul> + <li tal:repeat="i range(3)"><span tal:condition="repeat['i'].even" tal:replace="repeat['i'].even" /><span tal:condition="repeat['i'].odd" tal:replace="repeat['i'].odd" /></li> + </ul> +</div> diff --git a/src/chameleon/tests/inputs/026.xml b/src/chameleon/tests/inputs/026.xml new file mode 100644 index 0000000..1ba033c --- /dev/null +++ b/src/chameleon/tests/inputs/026.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (foo*)>
+<!ELEMENT foo EMPTY>
+]>
+<doc><foo/><foo></foo></doc>
diff --git a/src/chameleon/tests/inputs/027-attribute-replacement.pt b/src/chameleon/tests/inputs/027-attribute-replacement.pt new file mode 100644 index 0000000..aaadf2e --- /dev/null +++ b/src/chameleon/tests/inputs/027-attribute-replacement.pt @@ -0,0 +1,11 @@ +<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 + default; 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> +</div> diff --git a/src/chameleon/tests/inputs/027.xml b/src/chameleon/tests/inputs/027.xml new file mode 100644 index 0000000..ee02439 --- /dev/null +++ b/src/chameleon/tests/inputs/027.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (foo*)>
+<!ELEMENT foo ANY>
+]>
+<doc><foo/><foo></foo></doc>
diff --git a/src/chameleon/tests/inputs/028-attribute-toggle.pt b/src/chameleon/tests/inputs/028-attribute-toggle.pt new file mode 100644 index 0000000..60981e0 --- /dev/null +++ b/src/chameleon/tests/inputs/028-attribute-toggle.pt @@ -0,0 +1,6 @@ +<div xmlns="http://www.w3.org/1999/xhtml" + xmlns:tal="http://xml.zope.org/namespaces/tal"> + <option tal:attributes="selected True"></option> + <option tal:attributes="selected False"></option> + <option tal:attributes="selected None"></option> +</div> diff --git a/src/chameleon/tests/inputs/028.xml b/src/chameleon/tests/inputs/028.xml new file mode 100644 index 0000000..3d95747 --- /dev/null +++ b/src/chameleon/tests/inputs/028.xml @@ -0,0 +1,5 @@ +<?xml version="1.0"?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/029-attribute-ordering.pt b/src/chameleon/tests/inputs/029-attribute-ordering.pt new file mode 100644 index 0000000..d49a3ce --- /dev/null +++ b/src/chameleon/tests/inputs/029-attribute-ordering.pt @@ -0,0 +1,5 @@ +<div xmlns="http://www.w3.org/1999/xhtml" + xmlns:tal="http://xml.zope.org/namespaces/tal"> + <a rel="self" href="http://repoze.org" id="link-id" + tal:attributes="href 'http://python.org'" /> +</div> diff --git a/src/chameleon/tests/inputs/029.xml b/src/chameleon/tests/inputs/029.xml new file mode 100644 index 0000000..909f6ff --- /dev/null +++ b/src/chameleon/tests/inputs/029.xml @@ -0,0 +1,5 @@ +<?xml version='1.0'?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/030-repeat-tuples.pt b/src/chameleon/tests/inputs/030-repeat-tuples.pt new file mode 100644 index 0000000..90c7b5a --- /dev/null +++ b/src/chameleon/tests/inputs/030-repeat-tuples.pt @@ -0,0 +1,7 @@ +<html> + <body> + <div tal:repeat="(i, j) ((1, 2), (3, 4))"> + ${repeat['i', 'j'].number}, ${i}, ${j} + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/030.xml b/src/chameleon/tests/inputs/030.xml new file mode 100644 index 0000000..3a7ddaa --- /dev/null +++ b/src/chameleon/tests/inputs/030.xml @@ -0,0 +1,5 @@ +<?xml version = "1.0"?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/031-namespace-with-tal.pt b/src/chameleon/tests/inputs/031-namespace-with-tal.pt new file mode 100644 index 0000000..fa46c40 --- /dev/null +++ b/src/chameleon/tests/inputs/031-namespace-with-tal.pt @@ -0,0 +1,7 @@ +<div> + <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> diff --git a/src/chameleon/tests/inputs/031.xml b/src/chameleon/tests/inputs/031.xml new file mode 100644 index 0000000..a58e058 --- /dev/null +++ b/src/chameleon/tests/inputs/031.xml @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding="UTF-8"?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/032-master-template.pt b/src/chameleon/tests/inputs/032-master-template.pt new file mode 100644 index 0000000..01743f9 --- /dev/null +++ b/src/chameleon/tests/inputs/032-master-template.pt @@ -0,0 +1,20 @@ +<html i18n:domain="master" metal:define-macro="main" tal:define="content nothing"> + <head> + <title metal:define-slot="title" + metal:define-macro="title" + tal:define="has_title exists: title" + tal:content="title if has_title else default">Master template</title> + </head> + <body> + <div id="content"> + <metal:content define-slot="content"> + <!-- content here --> + </metal:content> + </div> + <div id="footer"> + <metal:footer define-slot="body-footer" tal:content="nothing"> + <!-- footer here --> + </metal:footer> + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/032.xml b/src/chameleon/tests/inputs/032.xml new file mode 100644 index 0000000..be55c8d --- /dev/null +++ b/src/chameleon/tests/inputs/032.xml @@ -0,0 +1,5 @@ +<?xml version='1.0' standalone='yes'?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/033-use-macro-trivial.pt b/src/chameleon/tests/inputs/033-use-macro-trivial.pt new file mode 100644 index 0000000..de4524f --- /dev/null +++ b/src/chameleon/tests/inputs/033-use-macro-trivial.pt @@ -0,0 +1 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']" /> diff --git a/src/chameleon/tests/inputs/033.xml b/src/chameleon/tests/inputs/033.xml new file mode 100644 index 0000000..a3f9053 --- /dev/null +++ b/src/chameleon/tests/inputs/033.xml @@ -0,0 +1,5 @@ +<?xml version='1.0' encoding="UTF-8" standalone='yes'?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/034-use-template-as-macro.pt b/src/chameleon/tests/inputs/034-use-template-as-macro.pt new file mode 100644 index 0000000..29315ea --- /dev/null +++ b/src/chameleon/tests/inputs/034-use-template-as-macro.pt @@ -0,0 +1 @@ +<html metal:use-macro="load('032-master-template.pt')" />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/034.xml b/src/chameleon/tests/inputs/034.xml new file mode 100644 index 0000000..7d52f31 --- /dev/null +++ b/src/chameleon/tests/inputs/034.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc/>
diff --git a/src/chameleon/tests/inputs/035-use-macro-with-fill-slot.pt b/src/chameleon/tests/inputs/035-use-macro-with-fill-slot.pt new file mode 100644 index 0000000..f5ff63e --- /dev/null +++ b/src/chameleon/tests/inputs/035-use-macro-with-fill-slot.pt @@ -0,0 +1,5 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']"> + <title metal:fill-slot="title" tal:define="kind 'New'"> + ${kind} title + </title> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/035.xml b/src/chameleon/tests/inputs/035.xml new file mode 100644 index 0000000..f109a8b --- /dev/null +++ b/src/chameleon/tests/inputs/035.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc />
diff --git a/src/chameleon/tests/inputs/036-use-macro-inherits-dynamic-scope.pt b/src/chameleon/tests/inputs/036-use-macro-inherits-dynamic-scope.pt new file mode 100644 index 0000000..525912a --- /dev/null +++ b/src/chameleon/tests/inputs/036-use-macro-inherits-dynamic-scope.pt @@ -0,0 +1,2 @@ +<html tal:define="title string:New title" + metal:use-macro="load('032-master-template.pt').macros['main']" /> diff --git a/src/chameleon/tests/inputs/036.xml b/src/chameleon/tests/inputs/036.xml new file mode 100644 index 0000000..8ab2b3f --- /dev/null +++ b/src/chameleon/tests/inputs/036.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
+<?pi data?>
diff --git a/src/chameleon/tests/inputs/037-use-macro-local-variable-scope.pt b/src/chameleon/tests/inputs/037-use-macro-local-variable-scope.pt new file mode 100644 index 0000000..7eb1c80 --- /dev/null +++ b/src/chameleon/tests/inputs/037-use-macro-local-variable-scope.pt @@ -0,0 +1,5 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']" > + <metal:content metal:fill-slot="content"> + <span tal:condition="content is None">ok</span> + </metal:content> +</html> diff --git a/src/chameleon/tests/inputs/037.xml b/src/chameleon/tests/inputs/037.xml new file mode 100644 index 0000000..f9b2113 --- /dev/null +++ b/src/chameleon/tests/inputs/037.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
+<!-- comment -->
+
diff --git a/src/chameleon/tests/inputs/038-use-macro-globals.pt b/src/chameleon/tests/inputs/038-use-macro-globals.pt new file mode 100644 index 0000000..be72046 --- /dev/null +++ b/src/chameleon/tests/inputs/038-use-macro-globals.pt @@ -0,0 +1,6 @@ +<metal:globals use-macro="load('039-globals.pt')" /> +<html> + <body> + <span tal:condition="content is None">ok</span> + </body> +</html> diff --git a/src/chameleon/tests/inputs/038.xml b/src/chameleon/tests/inputs/038.xml new file mode 100644 index 0000000..d14f41b --- /dev/null +++ b/src/chameleon/tests/inputs/038.xml @@ -0,0 +1,6 @@ +<!-- comment -->
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
+
diff --git a/src/chameleon/tests/inputs/039-globals.pt b/src/chameleon/tests/inputs/039-globals.pt new file mode 100644 index 0000000..315ff86 --- /dev/null +++ b/src/chameleon/tests/inputs/039-globals.pt @@ -0,0 +1 @@ +<tal:globals define="global content None" />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/039.xml b/src/chameleon/tests/inputs/039.xml new file mode 100644 index 0000000..0897316 --- /dev/null +++ b/src/chameleon/tests/inputs/039.xml @@ -0,0 +1,5 @@ +<?pi data?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/040-macro-using-template-symbol.pt b/src/chameleon/tests/inputs/040-macro-using-template-symbol.pt new file mode 100644 index 0000000..866215b --- /dev/null +++ b/src/chameleon/tests/inputs/040-macro-using-template-symbol.pt @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:tal="http://xml.zope.org/namespaces/tal" + xmlns:metal="http://xml.zope.org/namespaces/metal" + metal:define-macro="master" + template-macros="${' '.join(template.macros.names)}" + macros="${' '.join(macros.names)}"> + <metal:block tal:define="foo 'foo'"> + ${foo} + <div metal:define-slot="content" tal:replace="None"> + <!-- will be replaced --> + </div> + <span template-macros="${' '.join(template.macros.names)}" + macros="${' '.join(macros.names)}"> + <!-- demonstrate difference between + `template` and `macros` symbol --> + </span> + </metal:block> +</html> diff --git a/src/chameleon/tests/inputs/040.xml b/src/chameleon/tests/inputs/040.xml new file mode 100644 index 0000000..12c419b --- /dev/null +++ b/src/chameleon/tests/inputs/040.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+]>
+<doc a1=""<&>'"></doc>
diff --git a/src/chameleon/tests/inputs/041-translate-nested-names.pt b/src/chameleon/tests/inputs/041-translate-nested-names.pt new file mode 100644 index 0000000..33f26b5 --- /dev/null +++ b/src/chameleon/tests/inputs/041-translate-nested-names.pt @@ -0,0 +1,22 @@ +<html> + <body> + <div i18n:translate=""> + Hello + <tal:nested condition="True"> + <span i18n:name="world">world!</span> + </tal:nested> + </div> + <div i18n:translate="hello"> + Hello + <tal:nested condition="True"> + <span i18n:name="world">world!</span> + </tal:nested> + </div> + <div i18n:translate=""> + Goodbye + <tal:nested condition="False"> + <span i18n:name="world">world!</span> + </tal:nested> + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/041.xml b/src/chameleon/tests/inputs/041.xml new file mode 100644 index 0000000..a59f536 --- /dev/null +++ b/src/chameleon/tests/inputs/041.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+]>
+<doc a1="A"></doc>
diff --git a/src/chameleon/tests/inputs/042-use-macro-fill-footer.pt b/src/chameleon/tests/inputs/042-use-macro-fill-footer.pt new file mode 100644 index 0000000..0c0464f --- /dev/null +++ b/src/chameleon/tests/inputs/042-use-macro-fill-footer.pt @@ -0,0 +1,3 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']"> + <metal:footer fill-slot="body-footer">New footer</metal:footer> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/042.xml b/src/chameleon/tests/inputs/042.xml new file mode 100644 index 0000000..5d7c650 --- /dev/null +++ b/src/chameleon/tests/inputs/042.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>A</doc>
diff --git a/src/chameleon/tests/inputs/043-macro-nested-dynamic-vars.pt b/src/chameleon/tests/inputs/043-macro-nested-dynamic-vars.pt new file mode 100644 index 0000000..5a2f15c --- /dev/null +++ b/src/chameleon/tests/inputs/043-macro-nested-dynamic-vars.pt @@ -0,0 +1,19 @@ +<html> + <body> + <tal:macros condition="0"> + + <metal:outer define-macro="outer"> + <metal:inner use-macro="template.macros['inner']" /> + </metal:outer> + + <metal:inner define-macro="inner"> + ${title} + </metal:inner> + + </tal:macros> + + <div tal:define="title string:My title" + metal:use-macro="template.macros['outer']" + /> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/043.xml b/src/chameleon/tests/inputs/043.xml new file mode 100644 index 0000000..a8095df --- /dev/null +++ b/src/chameleon/tests/inputs/043.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ATTLIST doc a1 CDATA #IMPLIED>
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc a1="foo
+bar"></doc>
diff --git a/src/chameleon/tests/inputs/044-tuple-define.pt b/src/chameleon/tests/inputs/044-tuple-define.pt new file mode 100644 index 0000000..35dbd65 --- /dev/null +++ b/src/chameleon/tests/inputs/044-tuple-define.pt @@ -0,0 +1,5 @@ +<html> + <body tal:define="(a, b) 'a', 'b'"> + ${a}, ${b} + </body> +</html> diff --git a/src/chameleon/tests/inputs/044.xml b/src/chameleon/tests/inputs/044.xml new file mode 100644 index 0000000..bee1d23 --- /dev/null +++ b/src/chameleon/tests/inputs/044.xml @@ -0,0 +1,10 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (e*)>
+<!ELEMENT e EMPTY>
+<!ATTLIST e a1 CDATA "v1" a2 CDATA "v2" a3 CDATA #IMPLIED>
+]>
+<doc>
+<e a3="v3"/>
+<e a1="w1"/>
+<e a2="w2" a3="v3"/>
+</doc>
diff --git a/src/chameleon/tests/inputs/045-namespaces.pt b/src/chameleon/tests/inputs/045-namespaces.pt new file mode 100644 index 0000000..c247e92 --- /dev/null +++ b/src/chameleon/tests/inputs/045-namespaces.pt @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE application [ + <!ENTITY nbsp "\ "> +]> +<application xmlns="http://research.sun.com/wadl/2006/10" + xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd" + xmlns:wadl="http://research.sun.com/wadl/2006/10" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:tal="http://xml.zope.org/namespaces/tal"> + <resources tal:define="path '/'"> + <resource path="${path}">ZZZ YYY XXX</resource> + </resources> +</application> diff --git a/src/chameleon/tests/inputs/045.xml b/src/chameleon/tests/inputs/045.xml new file mode 100644 index 0000000..e2567f5 --- /dev/null +++ b/src/chameleon/tests/inputs/045.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA "v1">
+<!ATTLIST doc a1 CDATA "z1">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/046-extend-macro.pt b/src/chameleon/tests/inputs/046-extend-macro.pt new file mode 100644 index 0000000..487fbb3 --- /dev/null +++ b/src/chameleon/tests/inputs/046-extend-macro.pt @@ -0,0 +1,6 @@ +<html metal:define-macro="extended" + metal:extend-macro="load('032-master-template.pt').macros['main']"> + <metal:footer fill-slot="body-footer"> + <span metal:define-slot="kind">New</span> footer + </metal:footer> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/046.xml b/src/chameleon/tests/inputs/046.xml new file mode 100644 index 0000000..c50a284 --- /dev/null +++ b/src/chameleon/tests/inputs/046.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA "v1">
+<!ATTLIST doc a2 CDATA "v2">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/047-use-extended-macro.pt b/src/chameleon/tests/inputs/047-use-extended-macro.pt new file mode 100644 index 0000000..39529dd --- /dev/null +++ b/src/chameleon/tests/inputs/047-use-extended-macro.pt @@ -0,0 +1,3 @@ +<html metal:use-macro="load('046-extend-macro.pt').macros['extended']"> + <em metal:fill-slot="kind">Extended</em> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/047.xml b/src/chameleon/tests/inputs/047.xml new file mode 100644 index 0000000..a4c688c --- /dev/null +++ b/src/chameleon/tests/inputs/047.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>X
+Y</doc>
diff --git a/src/chameleon/tests/inputs/048-use-extended-macro-fill-original.pt b/src/chameleon/tests/inputs/048-use-extended-macro-fill-original.pt new file mode 100644 index 0000000..46d7ae3 --- /dev/null +++ b/src/chameleon/tests/inputs/048-use-extended-macro-fill-original.pt @@ -0,0 +1,5 @@ +<html metal:use-macro="load('046-extend-macro.pt').macros['extended']"> + <metal:footer fill-slot="body-footer"> + Extended footer + </metal:footer> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/048.xml b/src/chameleon/tests/inputs/048.xml new file mode 100644 index 0000000..c6b2ded --- /dev/null +++ b/src/chameleon/tests/inputs/048.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>]</doc>
diff --git a/src/chameleon/tests/inputs/049-entities-in-attributes.pt b/src/chameleon/tests/inputs/049-entities-in-attributes.pt new file mode 100644 index 0000000..387e1fb --- /dev/null +++ b/src/chameleon/tests/inputs/049-entities-in-attributes.pt @@ -0,0 +1,11 @@ +<html> + <body> + <pre tal:content="string:amp=&amp; lt=&lt;" /> + <pre tal:content="structure string:amp=&amp; lt=&lt;" /> + <script tal:replace="structure string:<script />" /> + <script tal:replace="string:<script />" /> + <script tal:replace="string:${'<'}script /${'>'}" /> + <script tal:replace="structure string:${'<'}script /${'>'}" /> + <img alt="1 < 2: ${1 < 2}" /> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/049.xml b/src/chameleon/tests/inputs/049.xml Binary files differnew file mode 100644 index 0000000..c3cc797 --- /dev/null +++ b/src/chameleon/tests/inputs/049.xml diff --git a/src/chameleon/tests/inputs/050-define-macro-and-use-not-extend.pt b/src/chameleon/tests/inputs/050-define-macro-and-use-not-extend.pt new file mode 100644 index 0000000..35ab70b --- /dev/null +++ b/src/chameleon/tests/inputs/050-define-macro-and-use-not-extend.pt @@ -0,0 +1,6 @@ +<html metal:define-macro="main" + metal:use-macro="load('032-master-template.pt').macros['main']"> + <metal:content fill-slot="content"> + Default content + </metal:content> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/050.xml b/src/chameleon/tests/inputs/050.xml Binary files differnew file mode 100644 index 0000000..12303b1 --- /dev/null +++ b/src/chameleon/tests/inputs/050.xml diff --git a/src/chameleon/tests/inputs/051-use-non-extended-macro.pt b/src/chameleon/tests/inputs/051-use-non-extended-macro.pt new file mode 100644 index 0000000..f8d10c3 --- /dev/null +++ b/src/chameleon/tests/inputs/051-use-non-extended-macro.pt @@ -0,0 +1,5 @@ +<html metal:use-macro="load('050-define-macro-and-use-not-extend.pt').macros['main']"> + <metal:content fill-slot="content"> + <span>Filled content</span> + </metal:content> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/051.xml b/src/chameleon/tests/inputs/051.xml Binary files differnew file mode 100644 index 0000000..7ae8f6c --- /dev/null +++ b/src/chameleon/tests/inputs/051.xml diff --git a/src/chameleon/tests/inputs/052-i18n-domain-inside-filled-slot.pt b/src/chameleon/tests/inputs/052-i18n-domain-inside-filled-slot.pt new file mode 100644 index 0000000..fadd5af --- /dev/null +++ b/src/chameleon/tests/inputs/052-i18n-domain-inside-filled-slot.pt @@ -0,0 +1,8 @@ +<html metal:define-macro="main" + metal:use-macro="load('032-master-template.pt').macros['main']"> + <metal:content fill-slot="content"> + <span i18n:domain="mydomain" i18n:translate=""> + Translated content + </span> + </metal:content> +</html> diff --git a/src/chameleon/tests/inputs/052.xml b/src/chameleon/tests/inputs/052.xml new file mode 100644 index 0000000..3f33a4c --- /dev/null +++ b/src/chameleon/tests/inputs/052.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>ð€€ô¿½</doc>
diff --git a/src/chameleon/tests/inputs/053-special-characters-in-attributes.pt b/src/chameleon/tests/inputs/053-special-characters-in-attributes.pt new file mode 100644 index 0000000..149e273 --- /dev/null +++ b/src/chameleon/tests/inputs/053-special-characters-in-attributes.pt @@ -0,0 +1,6 @@ +<html> + <body> + <a tal:attributes="href string:@@view">test</a> + <a href="${'@@'}view">test</a> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/053.xml b/src/chameleon/tests/inputs/053.xml new file mode 100644 index 0000000..0d88f28 --- /dev/null +++ b/src/chameleon/tests/inputs/053.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ENTITY e "<e/>">
+<!ELEMENT doc (e)>
+<!ELEMENT e EMPTY>
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/054-import-expression.pt b/src/chameleon/tests/inputs/054-import-expression.pt new file mode 100644 index 0000000..a0b2bcc --- /dev/null +++ b/src/chameleon/tests/inputs/054-import-expression.pt @@ -0,0 +1,3 @@ +<div tal:define="datetime import: datetime.datetime"> + ${datetime(1984, 12, 31).isoformat()} +</div>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/054.xml b/src/chameleon/tests/inputs/054.xml new file mode 100644 index 0000000..5d1c88b --- /dev/null +++ b/src/chameleon/tests/inputs/054.xml @@ -0,0 +1,10 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+
+
+<doc
+></doc
+>
+
+
diff --git a/src/chameleon/tests/inputs/055-attribute-fallback-to-dict-lookup.pt b/src/chameleon/tests/inputs/055-attribute-fallback-to-dict-lookup.pt new file mode 100644 index 0000000..32b9147 --- /dev/null +++ b/src/chameleon/tests/inputs/055-attribute-fallback-to-dict-lookup.pt @@ -0,0 +1,4 @@ +<div tal:define="obj {'foo': 'bar', 'boo': {'bar': 'baz'}}"> + ${obj.foo} + ${obj.boo.bar} +</div> diff --git a/src/chameleon/tests/inputs/055.xml b/src/chameleon/tests/inputs/055.xml new file mode 100644 index 0000000..da0292c --- /dev/null +++ b/src/chameleon/tests/inputs/055.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<?pi data?>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/056-comment-attribute.pt b/src/chameleon/tests/inputs/056-comment-attribute.pt new file mode 100644 index 0000000..e4f3289 --- /dev/null +++ b/src/chameleon/tests/inputs/056-comment-attribute.pt @@ -0,0 +1,7 @@ +<html> + <body> + <tal:first comment="ignored"> + Namespace tag + </tal:first> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/056.xml b/src/chameleon/tests/inputs/056.xml new file mode 100644 index 0000000..144871b --- /dev/null +++ b/src/chameleon/tests/inputs/056.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>A</doc>
diff --git a/src/chameleon/tests/inputs/057-order.pt b/src/chameleon/tests/inputs/057-order.pt new file mode 100644 index 0000000..7c05574 --- /dev/null +++ b/src/chameleon/tests/inputs/057-order.pt @@ -0,0 +1,8 @@ +<html> + <body> + <div tal:condition="False" tal:repeat="item bad_input" /> + <div tal:define="sequence 1, 2, 3" + tal:repeat="item sequence" + tal:replace="item" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/057.xml b/src/chameleon/tests/inputs/057.xml new file mode 100644 index 0000000..c1ac849 --- /dev/null +++ b/src/chameleon/tests/inputs/057.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (a*)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/058-script.pt b/src/chameleon/tests/inputs/058-script.pt new file mode 100644 index 0000000..01fef61 --- /dev/null +++ b/src/chameleon/tests/inputs/058-script.pt @@ -0,0 +1,16 @@ +<html> + <head> + <script tal:define="field {'oid': 1}; values 'values'; options 'options'"> + deform.addCallback( + '${field.oid}', + function (oid) { + $('#' + oid).autocomplete({source: ${values}}); + $('#' + oid).autocomplete("option", ${options}); + } + ); + </script> + </head> + <body> + <!-- Form --> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/058.xml b/src/chameleon/tests/inputs/058.xml new file mode 100644 index 0000000..2ff23b2 --- /dev/null +++ b/src/chameleon/tests/inputs/058.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ATTLIST doc a1 NMTOKENS #IMPLIED>
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc a1=" 1 2 "></doc>
diff --git a/src/chameleon/tests/inputs/059-embedded-javascript.pt b/src/chameleon/tests/inputs/059-embedded-javascript.pt new file mode 100644 index 0000000..689b989 --- /dev/null +++ b/src/chameleon/tests/inputs/059-embedded-javascript.pt @@ -0,0 +1,6 @@ +<html> + <body> + <span onclick="alert(true && false);">test</span> + <span onclick="alert(${str(True).lower()} && ${str(False).lower()});">test</span> + </body> +</html> diff --git a/src/chameleon/tests/inputs/059.xml b/src/chameleon/tests/inputs/059.xml new file mode 100644 index 0000000..2171480 --- /dev/null +++ b/src/chameleon/tests/inputs/059.xml @@ -0,0 +1,10 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (e*)>
+<!ELEMENT e EMPTY>
+<!ATTLIST e a1 CDATA #IMPLIED a2 CDATA #IMPLIED a3 CDATA #IMPLIED>
+]>
+<doc>
+<e a1="v1" a2="v2" a3="v3"/>
+<e a1="w1" a2="v2"/>
+<e a1="v1" a2="w2" a3="v3"/>
+</doc>
diff --git a/src/chameleon/tests/inputs/060-macro-with-multiple-same-slots.pt b/src/chameleon/tests/inputs/060-macro-with-multiple-same-slots.pt new file mode 100644 index 0000000..c3d6614 --- /dev/null +++ b/src/chameleon/tests/inputs/060-macro-with-multiple-same-slots.pt @@ -0,0 +1,8 @@ +<html metal:define-macro="main"> + <head> + <title><metal:title define-slot="title">Untitled</metal:title></title> + </head> + <body> + <h1><metal:title define-slot="title">Untitled</metal:title></h1> + </body> +</html> diff --git a/src/chameleon/tests/inputs/060.xml b/src/chameleon/tests/inputs/060.xml new file mode 100644 index 0000000..6cd6b43 --- /dev/null +++ b/src/chameleon/tests/inputs/060.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>X Y</doc>
diff --git a/src/chameleon/tests/inputs/061-fill-one-slot-but-two-defined.pt b/src/chameleon/tests/inputs/061-fill-one-slot-but-two-defined.pt new file mode 100644 index 0000000..a93aa43 --- /dev/null +++ b/src/chameleon/tests/inputs/061-fill-one-slot-but-two-defined.pt @@ -0,0 +1,3 @@ +<html metal:use-macro="load('060-macro-with-multiple-same-slots.pt').macros['main']"> + <metal:title fill-slot="title">My document</metal:title> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/061.xml b/src/chameleon/tests/inputs/061.xml new file mode 100644 index 0000000..bbdc152 --- /dev/null +++ b/src/chameleon/tests/inputs/061.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>£</doc>
diff --git a/src/chameleon/tests/inputs/062-comments-and-expressions.pt b/src/chameleon/tests/inputs/062-comments-and-expressions.pt new file mode 100644 index 0000000..76a980b --- /dev/null +++ b/src/chameleon/tests/inputs/062-comments-and-expressions.pt @@ -0,0 +1,27 @@ +<div> + <!--! ${dropped} --> +</div> + +<div> + <!--? ${left alone} --> +</div> + +<div> + <!-- ${'not left alone'} --> +</div> + +<div meta:interpolation="true"> + <!-- ${'not left alone'} --> +</div> + +<div meta:interpolation="false"> + <!-- ${left alone} --> +</div> + +<!--[if IE ${6}]> + Special instructions for IE 6 here +<![endif]--> + +<!--?[if IE 6]> + ${left alone} +<![endif]-->
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/062.xml b/src/chameleon/tests/inputs/062.xml new file mode 100644 index 0000000..f4ba530 --- /dev/null +++ b/src/chameleon/tests/inputs/062.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>เจมส์</doc>
diff --git a/src/chameleon/tests/inputs/063-continuation.pt b/src/chameleon/tests/inputs/063-continuation.pt new file mode 100644 index 0000000..02e9303 --- /dev/null +++ b/src/chameleon/tests/inputs/063-continuation.pt @@ -0,0 +1,4 @@ +<div tal:define="foo 1 + \ + 1"> + ${foo} +</div>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/063.xml b/src/chameleon/tests/inputs/063.xml new file mode 100644 index 0000000..9668f2d --- /dev/null +++ b/src/chameleon/tests/inputs/063.xml @@ -0,0 +1,4 @@ +<!DOCTYPE เจมส์ [
+<!ELEMENT เจมส์ (#PCDATA)>
+]>
+<เจมส์></เจมส์>
diff --git a/src/chameleon/tests/inputs/064-tags-and-special-characters.pt b/src/chameleon/tests/inputs/064-tags-and-special-characters.pt new file mode 100644 index 0000000..66746c3 --- /dev/null +++ b/src/chameleon/tests/inputs/064-tags-and-special-characters.pt @@ -0,0 +1,4 @@ +<tal:block> + <div id="test" class="${'test'}"> + </div> +</tal:block>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/064.xml b/src/chameleon/tests/inputs/064.xml new file mode 100644 index 0000000..74a97aa --- /dev/null +++ b/src/chameleon/tests/inputs/064.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>𐀀􏿽</doc>
diff --git a/src/chameleon/tests/inputs/065-use-macro-in-fill.pt b/src/chameleon/tests/inputs/065-use-macro-in-fill.pt new file mode 100644 index 0000000..540c31c --- /dev/null +++ b/src/chameleon/tests/inputs/065-use-macro-in-fill.pt @@ -0,0 +1,6 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']"> + <title tal:define="title string:Title" + metal:fill-slot="title" + metal:use-macro="load('032-master-template.pt').macros['title']" /> + <div metal:fill-slot="content">Content</div> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/065.xml b/src/chameleon/tests/inputs/065.xml new file mode 100644 index 0000000..f708f2b --- /dev/null +++ b/src/chameleon/tests/inputs/065.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ENTITY e "<">
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/066-load-expression.pt b/src/chameleon/tests/inputs/066-load-expression.pt new file mode 100644 index 0000000..747ddeb --- /dev/null +++ b/src/chameleon/tests/inputs/066-load-expression.pt @@ -0,0 +1 @@ +<html tal:define="hello_world load: hello_world.pt" metal:use-macro="hello_world" /> diff --git a/src/chameleon/tests/inputs/066.xml b/src/chameleon/tests/inputs/066.xml new file mode 100644 index 0000000..a27340b --- /dev/null +++ b/src/chameleon/tests/inputs/066.xml @@ -0,0 +1,7 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA #IMPLIED>
+<!-- 34 is double quote -->
+<!ENTITY e1 """>
+]>
+<doc a1="&e1;"></doc>
diff --git a/src/chameleon/tests/inputs/067-attribute-decode.pt b/src/chameleon/tests/inputs/067-attribute-decode.pt new file mode 100644 index 0000000..bed3f82 --- /dev/null +++ b/src/chameleon/tests/inputs/067-attribute-decode.pt @@ -0,0 +1,6 @@ +<html> + <body> + <img src="#" tal:attributes="class 1 > 0 and 'up' or 0 < 1 and 'down';" /> + <img src="#" tal:attributes="class 0 > 1 and 'up' or 0 < 1 and 'down';" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/067.xml b/src/chameleon/tests/inputs/067.xml new file mode 100644 index 0000000..a0ccf77 --- /dev/null +++ b/src/chameleon/tests/inputs/067.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc> </doc>
diff --git a/src/chameleon/tests/inputs/068-less-than-greater-than-in-attributes.pt b/src/chameleon/tests/inputs/068-less-than-greater-than-in-attributes.pt new file mode 100644 index 0000000..1c5f34d --- /dev/null +++ b/src/chameleon/tests/inputs/068-less-than-greater-than-in-attributes.pt @@ -0,0 +1,8 @@ +<html> + <body> + <span tal:content="string:0 < 1 or 0 > 1" /> + <span tal:content="structure string:0 < 1 or 0 > 1" /> + <span class="0 < 1 or 0 > 1" /> + <span>0 < 1 or 0 > 1</span> + </body> +</html> diff --git a/src/chameleon/tests/inputs/068.xml b/src/chameleon/tests/inputs/068.xml new file mode 100644 index 0000000..8ed806b --- /dev/null +++ b/src/chameleon/tests/inputs/068.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e " ">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/069-translation-domain-and-macro.pt b/src/chameleon/tests/inputs/069-translation-domain-and-macro.pt new file mode 100644 index 0000000..180e6f7 --- /dev/null +++ b/src/chameleon/tests/inputs/069-translation-domain-and-macro.pt @@ -0,0 +1,3 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']"> + <title metal:fill-slot="title" i18n:domain="test" i18n:translate="title">Title</title> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/069.xml b/src/chameleon/tests/inputs/069.xml new file mode 100644 index 0000000..2437f60 --- /dev/null +++ b/src/chameleon/tests/inputs/069.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!NOTATION n PUBLIC "whatever">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/070-translation-domain-and-use-macro.pt b/src/chameleon/tests/inputs/070-translation-domain-and-use-macro.pt new file mode 100644 index 0000000..9bd8486 --- /dev/null +++ b/src/chameleon/tests/inputs/070-translation-domain-and-use-macro.pt @@ -0,0 +1,3 @@ +<html i18n:domain="test" metal:use-macro="load('032-master-template.pt').macros['main']"> + <title metal:fill-slot="title" i18n:translate="title">Title</title> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/070.xml b/src/chameleon/tests/inputs/070.xml new file mode 100644 index 0000000..eef097d --- /dev/null +++ b/src/chameleon/tests/inputs/070.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ENTITY % e "<!ELEMENT doc (#PCDATA)>">
+%e;
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/071-html-attribute-defaults.pt b/src/chameleon/tests/inputs/071-html-attribute-defaults.pt new file mode 100644 index 0000000..6eb992e --- /dev/null +++ b/src/chameleon/tests/inputs/071-html-attribute-defaults.pt @@ -0,0 +1,12 @@ +<html> + <body> + <input type="input" tal:attributes="checked True" /> + <input type="input" tal:attributes="checked False" /> + <input type="input" tal:attributes="checked None" /> + <input type="input" checked="checked" tal:attributes="checked default" /> + <input type="input" checked tal:attributes="checked default" /> + <input type="input" checked checked="${default if True else None}" /> + <input type="input" tal:attributes="checked default" /> + <input type="input" class="field" tal:attributes="class True" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/071.xml b/src/chameleon/tests/inputs/071.xml new file mode 100644 index 0000000..ebfba23 --- /dev/null +++ b/src/chameleon/tests/inputs/071.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a ID #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/072-repeat-interpolation.pt b/src/chameleon/tests/inputs/072-repeat-interpolation.pt new file mode 100644 index 0000000..56c1144 --- /dev/null +++ b/src/chameleon/tests/inputs/072-repeat-interpolation.pt @@ -0,0 +1,13 @@ +<html> + <body> + <ul> + <li tal:repeat="i range(1, 4)" class="${'odd' if i % 2 != 0 else default}">${i}</li> + </ul> + <ul> + <li tal:repeat="i range(1, 4)" class="${'odd' if i % 2 != 0 else None}">${i}</li> + </ul> + <ul> + <li tal:repeat="i range(1, 4)" class="${'odd' if i % 2 != 0 else False}">${i}</li> + </ul> + </body> +</html> diff --git a/src/chameleon/tests/inputs/072.xml b/src/chameleon/tests/inputs/072.xml new file mode 100644 index 0000000..6ef39dc --- /dev/null +++ b/src/chameleon/tests/inputs/072.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a IDREF #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/073-utf8-encoded.pt b/src/chameleon/tests/inputs/073-utf8-encoded.pt new file mode 100644 index 0000000..c20dc30 --- /dev/null +++ b/src/chameleon/tests/inputs/073-utf8-encoded.pt @@ -0,0 +1,5 @@ +<?xml version="1.0" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>${'my title'} — ${'my site'}</title></head> +<body></body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/073.xml b/src/chameleon/tests/inputs/073.xml new file mode 100644 index 0000000..217476d --- /dev/null +++ b/src/chameleon/tests/inputs/073.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a IDREFS #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/074-encoded-template.pt b/src/chameleon/tests/inputs/074-encoded-template.pt new file mode 100644 index 0000000..3a48662 --- /dev/null +++ b/src/chameleon/tests/inputs/074-encoded-template.pt @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="windows-1251" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>${'my title'} — ${'my site'}</title></head> +<body></body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/074.xml b/src/chameleon/tests/inputs/074.xml new file mode 100644 index 0000000..8b2354f --- /dev/null +++ b/src/chameleon/tests/inputs/074.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a ENTITY #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/075-nested-macros.pt b/src/chameleon/tests/inputs/075-nested-macros.pt new file mode 100644 index 0000000..cb0635f --- /dev/null +++ b/src/chameleon/tests/inputs/075-nested-macros.pt @@ -0,0 +1,11 @@ +<metal:page define-macro="master"> + <tal:block metal:use-macro="load('032-master-template.pt').macros['main']"> + + <metal:block fill-slot="content"> + <metal:override define-slot="content"> + foo + </metal:override> + </metal:block> + + </tal:block> +</metal:page> diff --git a/src/chameleon/tests/inputs/075.xml b/src/chameleon/tests/inputs/075.xml new file mode 100644 index 0000000..33c0124 --- /dev/null +++ b/src/chameleon/tests/inputs/075.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a ENTITIES #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/076-nested-macro-override.pt b/src/chameleon/tests/inputs/076-nested-macro-override.pt new file mode 100644 index 0000000..63655a1 --- /dev/null +++ b/src/chameleon/tests/inputs/076-nested-macro-override.pt @@ -0,0 +1,3 @@ +<metal:page use-macro="load('075-nested-macros.pt').macros['master']"> + <metal:block fill-slot="content">bar</metal:block> +</metal:page> diff --git a/src/chameleon/tests/inputs/076.xml b/src/chameleon/tests/inputs/076.xml new file mode 100644 index 0000000..65b731c --- /dev/null +++ b/src/chameleon/tests/inputs/076.xml @@ -0,0 +1,7 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a NOTATION (n1|n2) #IMPLIED>
+<!NOTATION n1 SYSTEM "http://www.w3.org/">
+<!NOTATION n2 SYSTEM "http://www.w3.org/">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/077-i18n-attributes.pt b/src/chameleon/tests/inputs/077-i18n-attributes.pt new file mode 100644 index 0000000..f0efbe3 --- /dev/null +++ b/src/chameleon/tests/inputs/077-i18n-attributes.pt @@ -0,0 +1 @@ +<input type="hidden" id="uploadify_label_file_title" i18n:domain="test" i18n:attributes="value Title" />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/077.xml b/src/chameleon/tests/inputs/077.xml new file mode 100644 index 0000000..e5f301e --- /dev/null +++ b/src/chameleon/tests/inputs/077.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a (1|2) #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/078-tags-and-newlines.pt b/src/chameleon/tests/inputs/078-tags-and-newlines.pt new file mode 100644 index 0000000..961e509 --- /dev/null +++ b/src/chameleon/tests/inputs/078-tags-and-newlines.pt @@ -0,0 +1,23 @@ +<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:tal="http://xml.zope.org/namespaces/tal"
+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
+ tal:omit-tag=""
+ tal:define="id 'foo'">
+ <body>
+ <span id="toDataContainer"
+ tal:attributes="id string:${id}-toDataContainer">
+ <script type="text/javascript" tal:content="string:
+ copyDataForSubmit('${id}');">
+ // initial copying of field "field.to" --> "field"
+ copyDataForSubmit("<i tal:replace="${id}"/>");
+ </script>
+ </span>
+ <tal:block
+ repeat="value python: (1, 2, 3)"
+ ><span class="selected-option"
+ tal:content="value"
+ /><tal:block condition="not:repeat.value.end">, </tal:block
+ ></tal:block
+>
+ </body>
+</html>
diff --git a/src/chameleon/tests/inputs/078.xml b/src/chameleon/tests/inputs/078.xml new file mode 100644 index 0000000..b31f40f --- /dev/null +++ b/src/chameleon/tests/inputs/078.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #REQUIRED>
+]>
+<doc a="v"></doc>
diff --git a/src/chameleon/tests/inputs/079-implicit-i18n.pt b/src/chameleon/tests/inputs/079-implicit-i18n.pt new file mode 100644 index 0000000..da29289 --- /dev/null +++ b/src/chameleon/tests/inputs/079-implicit-i18n.pt @@ -0,0 +1,16 @@ +<html tal:define="request dict(site_url='http://host')"> + <head> + <title>Welcome</title> + </head> + <body> + <h1>Welcome</h1> + <span>An edge case: ${.</span> + <img tal:define="site_url request.site_url" alt="Site logo" href="${site_url}/logo.png" /> + <img alt="Site logo" href="${request.site_url}/logo.png" /> + <div id="content"> + boo foo. + <br /> + bar. + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/079.xml b/src/chameleon/tests/inputs/079.xml new file mode 100644 index 0000000..a3290d6 --- /dev/null +++ b/src/chameleon/tests/inputs/079.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #FIXED "v">
+]>
+<doc a="v"></doc>
diff --git a/src/chameleon/tests/inputs/080-xmlns-namespace-on-tal.pt b/src/chameleon/tests/inputs/080-xmlns-namespace-on-tal.pt new file mode 100644 index 0000000..3123c46 --- /dev/null +++ b/src/chameleon/tests/inputs/080-xmlns-namespace-on-tal.pt @@ -0,0 +1,6 @@ +<tal:component xmlns="http://www.w3.org/1999/xhtml" + xmlns:i18n="http://xml.zope.org/namespaces/i18n" + xmlns:metal="http://xml.zope.org/namespaces/metal" + xmlns:tal="http://xml.zope.org/namespaces/tal"> + Hello world +</tal:component> diff --git a/src/chameleon/tests/inputs/080.xml b/src/chameleon/tests/inputs/080.xml new file mode 100644 index 0000000..3208fa9 --- /dev/null +++ b/src/chameleon/tests/inputs/080.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #FIXED "v">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/081-load-spec.pt b/src/chameleon/tests/inputs/081-load-spec.pt new file mode 100644 index 0000000..388f761 --- /dev/null +++ b/src/chameleon/tests/inputs/081-load-spec.pt @@ -0,0 +1 @@ +<html metal:use-macro="load: chameleon:tests/inputs/hello_world.pt" />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/081.xml b/src/chameleon/tests/inputs/081.xml new file mode 100644 index 0000000..51ee1a3 --- /dev/null +++ b/src/chameleon/tests/inputs/081.xml @@ -0,0 +1,7 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (a, b, c)>
+<!ELEMENT a (a?)>
+<!ELEMENT b (b*)>
+<!ELEMENT c (a | b)+>
+]>
+<doc><a/><b/><c><a/></c></doc>
diff --git a/src/chameleon/tests/inputs/082-load-spec-computed.pt b/src/chameleon/tests/inputs/082-load-spec-computed.pt new file mode 100644 index 0000000..6d78526 --- /dev/null +++ b/src/chameleon/tests/inputs/082-load-spec-computed.pt @@ -0,0 +1 @@ +<html tal:define="name 'hello_world'" metal:use-macro="load: chameleon:tests/inputs/${name}.pt" />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/082.xml b/src/chameleon/tests/inputs/082.xml new file mode 100644 index 0000000..d5245ac --- /dev/null +++ b/src/chameleon/tests/inputs/082.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ENTITY % e SYSTEM "e.dtd">
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/083-template-dict-to-macro.pt b/src/chameleon/tests/inputs/083-template-dict-to-macro.pt new file mode 100644 index 0000000..6bc3af4 --- /dev/null +++ b/src/chameleon/tests/inputs/083-template-dict-to-macro.pt @@ -0,0 +1,2 @@ +<html tal:define="template load: 032-master-template.pt" + metal:use-macro="template['main']" />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/083.xml b/src/chameleon/tests/inputs/083.xml new file mode 100644 index 0000000..937cfc0 --- /dev/null +++ b/src/chameleon/tests/inputs/083.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ENTITY % e PUBLIC 'whatever' "e.dtd">
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/084-interpolation-in-cdata.pt b/src/chameleon/tests/inputs/084-interpolation-in-cdata.pt new file mode 100644 index 0000000..391d719 --- /dev/null +++ b/src/chameleon/tests/inputs/084-interpolation-in-cdata.pt @@ -0,0 +1,9 @@ +<html> + <head> + <script> + <![CDATA[ + alert("${'Hello world!'}"); + ]]> + </script> + </head> +</html> diff --git a/src/chameleon/tests/inputs/084.xml b/src/chameleon/tests/inputs/084.xml new file mode 100644 index 0000000..8276076 --- /dev/null +++ b/src/chameleon/tests/inputs/084.xml @@ -0,0 +1 @@ +<!DOCTYPE doc [<!ELEMENT doc (#PCDATA)>]><doc></doc>
diff --git a/src/chameleon/tests/inputs/085-nested-translation.pt b/src/chameleon/tests/inputs/085-nested-translation.pt new file mode 100644 index 0000000..35aa5da --- /dev/null +++ b/src/chameleon/tests/inputs/085-nested-translation.pt @@ -0,0 +1,11 @@ +<html tal:define="request dict(site_url='http://host')" i18n:domain="new"> + <head> + <title>Welcome</title> + </head> + <body> + <h1>Welcome</h1> + <p i18n:translate=""> + <a i18n:name="click_here" i18n:translate="" href="${request.site_url}">Click here</a> to continue. + </p> + </body> +</html> diff --git a/src/chameleon/tests/inputs/085.xml b/src/chameleon/tests/inputs/085.xml new file mode 100644 index 0000000..cf5834f --- /dev/null +++ b/src/chameleon/tests/inputs/085.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY % e "<foo>">
+<!ENTITY e "">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/086-self-closing.pt b/src/chameleon/tests/inputs/086-self-closing.pt new file mode 100644 index 0000000..fe74962 --- /dev/null +++ b/src/chameleon/tests/inputs/086-self-closing.pt @@ -0,0 +1,10 @@ +<html metal:use-macro="load('032-master-template.pt').macros['main']"> + <body> + <div metal:fill-slot="content"> + <div tal:condition="True"> + <a href="#">Chart</a> + <div id="chart-info"/> + </div> + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/086.xml b/src/chameleon/tests/inputs/086.xml new file mode 100644 index 0000000..bbc3080 --- /dev/null +++ b/src/chameleon/tests/inputs/086.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e "">
+<!ENTITY e "<foo>">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/087-code-blocks.pt b/src/chameleon/tests/inputs/087-code-blocks.pt new file mode 100644 index 0000000..24e7d15 --- /dev/null +++ b/src/chameleon/tests/inputs/087-code-blocks.pt @@ -0,0 +1,28 @@ +<?python + foo = [1, 2, 3] ?> + +<ul> + <li tal:repeat="f foo" tal:content="f" /> +</ul> + +<?python + foo = [1, 2, 3] + boo = [4, 5, 6] +?> + +<ul> + <li tal:repeat="(f, g) zip(foo, boo)" tal:content="f + g" /> +</ul> + +<div> + <?python numbers = map(str, range(1, 10)) ?> + Please input a number from the range ${", ".join(numbers)}. +</div> + +<div> + <?python + def function(i): + return i + 1 + ?> + 41 + 1 = ${function(41)}. +</div>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/087.xml b/src/chameleon/tests/inputs/087.xml new file mode 100644 index 0000000..34797a6 --- /dev/null +++ b/src/chameleon/tests/inputs/087.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ENTITY e "<foo/>">
+<!ELEMENT doc (foo)>
+<!ELEMENT foo EMPTY>
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/088-python-newlines.pt b/src/chameleon/tests/inputs/088-python-newlines.pt new file mode 100644 index 0000000..dbb7ad0 --- /dev/null +++ b/src/chameleon/tests/inputs/088-python-newlines.pt @@ -0,0 +1,2 @@ +<span tal:replace="python: ', '.join(( + 'a', 'b', 'c'))" /> diff --git a/src/chameleon/tests/inputs/088.xml b/src/chameleon/tests/inputs/088.xml new file mode 100644 index 0000000..f97d968 --- /dev/null +++ b/src/chameleon/tests/inputs/088.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e "<foo>">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/089-load-fallback.pt b/src/chameleon/tests/inputs/089-load-fallback.pt new file mode 100644 index 0000000..189027f --- /dev/null +++ b/src/chameleon/tests/inputs/089-load-fallback.pt @@ -0,0 +1,3 @@ +<html metal:use-macro="load: tests/missing.pt | + chameleon:tests/inputs/hello_world.pt" + />
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/089.xml b/src/chameleon/tests/inputs/089.xml new file mode 100644 index 0000000..2d80c8f --- /dev/null +++ b/src/chameleon/tests/inputs/089.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ENTITY e "𐀀􏿽">
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/090-tuple-expression.pt b/src/chameleon/tests/inputs/090-tuple-expression.pt new file mode 100644 index 0000000..89e276d --- /dev/null +++ b/src/chameleon/tests/inputs/090-tuple-expression.pt @@ -0,0 +1,8 @@ +<ul tal:define="items ('main', 'Welcome'), + ('listings', 'Listings'), + ('about', 'About'), + ('contact', 'Contact')"> + <li tal:repeat="(route, title) items"> + <a href="#${route}" id="${route}nav">${title}</a> + </li> +</ul> diff --git a/src/chameleon/tests/inputs/090.xml b/src/chameleon/tests/inputs/090.xml new file mode 100644 index 0000000..c392c96 --- /dev/null +++ b/src/chameleon/tests/inputs/090.xml @@ -0,0 +1,7 @@ +<!DOCTYPE doc [
+<!ATTLIST e a NOTATION (n) #IMPLIED>
+<!ELEMENT doc (e)*>
+<!ELEMENT e (#PCDATA)>
+<!NOTATION n PUBLIC "whatever">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/091-repeat-none.pt b/src/chameleon/tests/inputs/091-repeat-none.pt new file mode 100644 index 0000000..e912962 --- /dev/null +++ b/src/chameleon/tests/inputs/091-repeat-none.pt @@ -0,0 +1,5 @@ +<html> + <body> + <div tal:repeat="i None">error</div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/091.xml b/src/chameleon/tests/inputs/091.xml new file mode 100644 index 0000000..7343d0f --- /dev/null +++ b/src/chameleon/tests/inputs/091.xml @@ -0,0 +1,7 @@ +<!DOCTYPE doc [
+<!NOTATION n SYSTEM "http://www.w3.org/">
+<!ENTITY e SYSTEM "http://www.w3.org/" NDATA n>
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a ENTITY "e">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/092.xml b/src/chameleon/tests/inputs/092.xml new file mode 100644 index 0000000..627b74e --- /dev/null +++ b/src/chameleon/tests/inputs/092.xml @@ -0,0 +1,10 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (a)*>
+<!ELEMENT a EMPTY>
+]>
+<doc>
+<a/>
+ <a/> <a/>
+
+
+</doc>
diff --git a/src/chameleon/tests/inputs/093.xml b/src/chameleon/tests/inputs/093.xml new file mode 100644 index 0000000..968acb6 --- /dev/null +++ b/src/chameleon/tests/inputs/093.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [ +<!ELEMENT doc (#PCDATA)> +]> +<doc>
+
</doc>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/094.xml b/src/chameleon/tests/inputs/094.xml new file mode 100644 index 0000000..5726e7d --- /dev/null +++ b/src/chameleon/tests/inputs/094.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ENTITY % e "foo">
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a1 CDATA "%e;">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/095.xml b/src/chameleon/tests/inputs/095.xml new file mode 100644 index 0000000..1fe6959 --- /dev/null +++ b/src/chameleon/tests/inputs/095.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ATTLIST doc a1 CDATA #IMPLIED>
+<!ATTLIST doc a1 NMTOKENS #IMPLIED>
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc a1="1 2"></doc>
diff --git a/src/chameleon/tests/inputs/096.xml b/src/chameleon/tests/inputs/096.xml new file mode 100644 index 0000000..a6f8f43 --- /dev/null +++ b/src/chameleon/tests/inputs/096.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ATTLIST doc a1 NMTOKENS " 1 2 ">
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/097.xml b/src/chameleon/tests/inputs/097.xml new file mode 100644 index 0000000..c606afa --- /dev/null +++ b/src/chameleon/tests/inputs/097.xml @@ -0,0 +1,8 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY % e SYSTEM "097.ent">
+<!ATTLIST doc a1 CDATA "v1">
+%e;
+<!ATTLIST doc a2 CDATA "v2">
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/098.xml b/src/chameleon/tests/inputs/098.xml new file mode 100644 index 0000000..33a64ce --- /dev/null +++ b/src/chameleon/tests/inputs/098.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><?pi x
+y?></doc>
diff --git a/src/chameleon/tests/inputs/099.xml b/src/chameleon/tests/inputs/099.xml new file mode 100644 index 0000000..1b7214a --- /dev/null +++ b/src/chameleon/tests/inputs/099.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/100.xml b/src/chameleon/tests/inputs/100.xml new file mode 100644 index 0000000..5b839e7 --- /dev/null +++ b/src/chameleon/tests/inputs/100.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ENTITY e PUBLIC ";!*#@$_%" "100.xml">
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/101-unclosed-tags.html b/src/chameleon/tests/inputs/101-unclosed-tags.html new file mode 100644 index 0000000..03230ec --- /dev/null +++ b/src/chameleon/tests/inputs/101-unclosed-tags.html @@ -0,0 +1,5 @@ +<html> + <body> + <h1><br><br>Hello world</h1> + </body> +</html> diff --git a/src/chameleon/tests/inputs/101.xml b/src/chameleon/tests/inputs/101.xml new file mode 100644 index 0000000..f464484 --- /dev/null +++ b/src/chameleon/tests/inputs/101.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e """>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/102-unquoted-attributes.html b/src/chameleon/tests/inputs/102-unquoted-attributes.html new file mode 100644 index 0000000..ac6400d --- /dev/null +++ b/src/chameleon/tests/inputs/102-unquoted-attributes.html @@ -0,0 +1,5 @@ +<html> + <body> + <h1 class=title align=center>Hello world</h1> + </body> +</html> diff --git a/src/chameleon/tests/inputs/102.xml b/src/chameleon/tests/inputs/102.xml new file mode 100644 index 0000000..f239ff5 --- /dev/null +++ b/src/chameleon/tests/inputs/102.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="""></doc>
diff --git a/src/chameleon/tests/inputs/103-simple-attribute.html b/src/chameleon/tests/inputs/103-simple-attribute.html new file mode 100644 index 0000000..a8a4000 --- /dev/null +++ b/src/chameleon/tests/inputs/103-simple-attribute.html @@ -0,0 +1,8 @@ +<html> + <body> + <select name="foo2" multiple style="visibility: hidden"> + <option value="1">this should</option> + <option value="2">remain hidden right?</option> + </select> + </body> +</html> diff --git a/src/chameleon/tests/inputs/103.xml b/src/chameleon/tests/inputs/103.xml new file mode 100644 index 0000000..1dbbd5b --- /dev/null +++ b/src/chameleon/tests/inputs/103.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><doc></doc>
diff --git a/src/chameleon/tests/inputs/104.xml b/src/chameleon/tests/inputs/104.xml new file mode 100644 index 0000000..666f43d --- /dev/null +++ b/src/chameleon/tests/inputs/104.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="x y"></doc>
diff --git a/src/chameleon/tests/inputs/105.xml b/src/chameleon/tests/inputs/105.xml new file mode 100644 index 0000000..6b3af2b --- /dev/null +++ b/src/chameleon/tests/inputs/105.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="x	y"></doc>
diff --git a/src/chameleon/tests/inputs/106.xml b/src/chameleon/tests/inputs/106.xml new file mode 100644 index 0000000..8757c0a --- /dev/null +++ b/src/chameleon/tests/inputs/106.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="x y"></doc>
diff --git a/src/chameleon/tests/inputs/107.xml b/src/chameleon/tests/inputs/107.xml new file mode 100644 index 0000000..3d2c256 --- /dev/null +++ b/src/chameleon/tests/inputs/107.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="x y"></doc>
diff --git a/src/chameleon/tests/inputs/108.xml b/src/chameleon/tests/inputs/108.xml new file mode 100644 index 0000000..e919bf2 --- /dev/null +++ b/src/chameleon/tests/inputs/108.xml @@ -0,0 +1,7 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e "
+">
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="x&e;y"></doc>
diff --git a/src/chameleon/tests/inputs/109.xml b/src/chameleon/tests/inputs/109.xml new file mode 100644 index 0000000..33fa38e --- /dev/null +++ b/src/chameleon/tests/inputs/109.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a=""></doc>
diff --git a/src/chameleon/tests/inputs/110.xml b/src/chameleon/tests/inputs/110.xml new file mode 100644 index 0000000..0c61c65 --- /dev/null +++ b/src/chameleon/tests/inputs/110.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e " ">
+<!ATTLIST doc a CDATA #IMPLIED>
+]>
+<doc a="x&e;y"></doc>
diff --git a/src/chameleon/tests/inputs/111.xml b/src/chameleon/tests/inputs/111.xml new file mode 100644 index 0000000..cb56f26 --- /dev/null +++ b/src/chameleon/tests/inputs/111.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST doc a NMTOKENS #IMPLIED>
+]>
+<doc a=" x  y "></doc>
diff --git a/src/chameleon/tests/inputs/112.xml b/src/chameleon/tests/inputs/112.xml new file mode 100644 index 0000000..27b6a4c --- /dev/null +++ b/src/chameleon/tests/inputs/112.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (a | b)>
+<!ELEMENT a (#PCDATA)>
+]>
+<doc><a></a></doc>
diff --git a/src/chameleon/tests/inputs/113.xml b/src/chameleon/tests/inputs/113.xml new file mode 100644 index 0000000..d2edd0f --- /dev/null +++ b/src/chameleon/tests/inputs/113.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ATTLIST e a CDATA #IMPLIED>
+]>
+<doc></doc>
diff --git a/src/chameleon/tests/inputs/114.xml b/src/chameleon/tests/inputs/114.xml new file mode 100644 index 0000000..52e2070 --- /dev/null +++ b/src/chameleon/tests/inputs/114.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e "<![CDATA[&foo;]]>">
+]>
+<doc>&e;</doc>
diff --git a/src/chameleon/tests/inputs/115.xml b/src/chameleon/tests/inputs/115.xml new file mode 100644 index 0000000..d939a67 --- /dev/null +++ b/src/chameleon/tests/inputs/115.xml @@ -0,0 +1,6 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY e1 "&e2;">
+<!ENTITY e2 "v">
+]>
+<doc>&e1;</doc>
diff --git a/src/chameleon/tests/inputs/116.xml b/src/chameleon/tests/inputs/116.xml new file mode 100644 index 0000000..55ab496 --- /dev/null +++ b/src/chameleon/tests/inputs/116.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+]>
+<doc><![CDATA[
+]]></doc>
diff --git a/src/chameleon/tests/inputs/117.xml b/src/chameleon/tests/inputs/117.xml new file mode 100644 index 0000000..e4f02b1 --- /dev/null +++ b/src/chameleon/tests/inputs/117.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY rsqb "]">
+]>
+<doc>]</doc>
diff --git a/src/chameleon/tests/inputs/118.xml b/src/chameleon/tests/inputs/118.xml new file mode 100644 index 0000000..fba6c44 --- /dev/null +++ b/src/chameleon/tests/inputs/118.xml @@ -0,0 +1,5 @@ +<!DOCTYPE doc [
+<!ELEMENT doc (#PCDATA)>
+<!ENTITY rsqb "]]">
+]>
+<doc>]</doc>
diff --git a/src/chameleon/tests/inputs/119.xml b/src/chameleon/tests/inputs/119.xml new file mode 100644 index 0000000..876e747 --- /dev/null +++ b/src/chameleon/tests/inputs/119.xml @@ -0,0 +1,4 @@ +<!DOCTYPE doc [
+<!ELEMENT doc ANY>
+]>
+<doc><!-- -á --></doc>
diff --git a/src/chameleon/tests/inputs/120-translation-context.pt b/src/chameleon/tests/inputs/120-translation-context.pt new file mode 100644 index 0000000..c3f5a52 --- /dev/null +++ b/src/chameleon/tests/inputs/120-translation-context.pt @@ -0,0 +1,13 @@ +<html> + <body> + <div i18n:translate=""> + Hello world! + </div> + <button i18n:context="button" i18n:translate=""> + Hello world! + </button> + <div i18n:domain="new" i18n:context="navigation"> + <a i18n:translate="">Tab</a> + </div> + </body> +</html> diff --git a/src/chameleon/tests/inputs/121-translation-comment.pt b/src/chameleon/tests/inputs/121-translation-comment.pt new file mode 100644 index 0000000..52afa1b --- /dev/null +++ b/src/chameleon/tests/inputs/121-translation-comment.pt @@ -0,0 +1,7 @@ +<html> + <body> + <h1 i18n:translate="" i18n:comment="Site title"> + Hello world! + </h1> + </body> +</html> diff --git a/src/chameleon/tests/inputs/122-translation-ignore.pt b/src/chameleon/tests/inputs/122-translation-ignore.pt new file mode 100644 index 0000000..8a3d864 --- /dev/null +++ b/src/chameleon/tests/inputs/122-translation-ignore.pt @@ -0,0 +1,10 @@ +<html> + <body> + <h1 i18n:ignore="true">Hello world!</h1> + <a href="http://www.python.org" + title="Python" + i18n:ignore-attributes="title"> + Python is a programming language. + </a> + </body> +</html> diff --git a/src/chameleon/tests/inputs/123-html5-data-attributes.pt b/src/chameleon/tests/inputs/123-html5-data-attributes.pt new file mode 100644 index 0000000..e6e6575 --- /dev/null +++ b/src/chameleon/tests/inputs/123-html5-data-attributes.pt @@ -0,0 +1,5 @@ +<html> + <body> + <div foo="bar" data-tal-content="'Hello world!'" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/124-translation-target.pt b/src/chameleon/tests/inputs/124-translation-target.pt new file mode 100644 index 0000000..14c455a --- /dev/null +++ b/src/chameleon/tests/inputs/124-translation-target.pt @@ -0,0 +1,6 @@ +<html> + <body> + <h1 i18n:translate="" i18n:target="string:fr">Hello world!</h1> + <p i18n:translate="" i18n:target="default">It's a big world.</p> + </body> +</html> diff --git a/src/chameleon/tests/inputs/125-macro-translation-ordering.pt b/src/chameleon/tests/inputs/125-macro-translation-ordering.pt new file mode 100644 index 0000000..9f89ac5 --- /dev/null +++ b/src/chameleon/tests/inputs/125-macro-translation-ordering.pt @@ -0,0 +1,12 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" + xmlns:tal="http://xml.zope.org/namespaces/tal" + xmlns:metal="http://xml.zope.org/namespaces/metal" + xmlns:i18n="http://xml.zope.org/namespaces/i18n" + lang="en"> + <body> + <div i18n:translate=""> + <h1>h1</h1> + <h2 metal:define-macro="amacro">h2</h2> + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/126-define-escaping.pt b/src/chameleon/tests/inputs/126-define-escaping.pt new file mode 100644 index 0000000..4a2271e --- /dev/null +++ b/src/chameleon/tests/inputs/126-define-escaping.pt @@ -0,0 +1,10 @@ +<html> + <body tal:define="s1 string:&; + s2 python:'&'"> + ${s1} + ${s2} + ${structure: s1}amp; + ${structure: s2}amp; + <a tal:attributes="href string:localhost?a=1b${s1}2" /> + </body> +</html> diff --git a/src/chameleon/tests/inputs/238-macroname.pt b/src/chameleon/tests/inputs/238-macroname.pt new file mode 100644 index 0000000..781c901 --- /dev/null +++ b/src/chameleon/tests/inputs/238-macroname.pt @@ -0,0 +1,7 @@ +<span metal:define-macro="page" tal:omit-tag=""> + <h1 metal:define-slot="name" tal:omit-tag="" /> +</span> + +<span metal:use-macro="macros['page']"> + <h1 metal:fill-slot="name" tal:content="macroname">name</h1> +</span> diff --git a/src/chameleon/tests/inputs/greeting.pt b/src/chameleon/tests/inputs/greeting.pt new file mode 100644 index 0000000..f54d9c1 --- /dev/null +++ b/src/chameleon/tests/inputs/greeting.pt @@ -0,0 +1 @@ +<div>Hello, ${name | 'undefined'}.</div> diff --git a/src/chameleon/tests/inputs/hello_world.pt b/src/chameleon/tests/inputs/hello_world.pt new file mode 100644 index 0000000..fa9360e --- /dev/null +++ b/src/chameleon/tests/inputs/hello_world.pt @@ -0,0 +1,5 @@ +<html> + <body> + ${'Hello world!'} + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/inputs/hello_world.txt b/src/chameleon/tests/inputs/hello_world.txt new file mode 100644 index 0000000..61ef312 --- /dev/null +++ b/src/chameleon/tests/inputs/hello_world.txt @@ -0,0 +1 @@ +${'Hello world!'} diff --git a/src/chameleon/tests/inputs/multinode-implicit-i18n.pt b/src/chameleon/tests/inputs/multinode-implicit-i18n.pt new file mode 100644 index 0000000..050752b --- /dev/null +++ b/src/chameleon/tests/inputs/multinode-implicit-i18n.pt @@ -0,0 +1,3 @@ +<html> + <body>Foo ${message}</body> +</html> diff --git a/src/chameleon/tests/outputs/001.html b/src/chameleon/tests/outputs/001.html new file mode 100644 index 0000000..0318013 --- /dev/null +++ b/src/chameleon/tests/outputs/001.html @@ -0,0 +1,7 @@ +<html> + <body> + Hello world! + Hello world! + </body> + Goodbye world! +</html> diff --git a/src/chameleon/tests/outputs/001.pt b/src/chameleon/tests/outputs/001.pt new file mode 100644 index 0000000..fff27d1 --- /dev/null +++ b/src/chameleon/tests/outputs/001.pt @@ -0,0 +1,9 @@ +<html> + <body> + Hello world! + </body> + + + ok + +</html> diff --git a/src/chameleon/tests/outputs/001.txt b/src/chameleon/tests/outputs/001.txt new file mode 100644 index 0000000..ec73032 --- /dev/null +++ b/src/chameleon/tests/outputs/001.txt @@ -0,0 +1 @@ +<Hello world><&> diff --git a/src/chameleon/tests/outputs/002.pt b/src/chameleon/tests/outputs/002.pt new file mode 100644 index 0000000..599f3d6 --- /dev/null +++ b/src/chameleon/tests/outputs/002.pt @@ -0,0 +1,13 @@ +<html> + <body> + <div> + <span>Hello!</span> + <span>Hello.</span> + </div> + <div> + <span>Goodbye!</span> + <span>Goodbye.</span> + </div> + ok + </body> +</html> diff --git a/src/chameleon/tests/outputs/003.pt b/src/chameleon/tests/outputs/003.pt new file mode 100644 index 0000000..05ac901 --- /dev/null +++ b/src/chameleon/tests/outputs/003.pt @@ -0,0 +1,17 @@ +<html> + <body> + <div>Hello world!</div> + <div>Hello world!</div>1 + 2<div>Hello world!</div> + <div>Hello world!</div>3 + <div>Hello world!</div>5 + 6<div>Hello world!</div> + <div>1</div> + <div>1.0</div> + <div>True</div> + <div>False</div> + <div>0</div> + <div></div> + <div>Hello world!</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/004.pt b/src/chameleon/tests/outputs/004.pt new file mode 100644 index 0000000..debe775 --- /dev/null +++ b/src/chameleon/tests/outputs/004.pt @@ -0,0 +1,25 @@ +<html> + <body> + <span class="hello" /> + <span class="hello" /> + <span class="hello" /> + <span data-æøå="hello" /> + <span /> + <span b="2" c="3" d=4></span> + <span a="1" c="3" /> + <span a="1" b="2" /> + <span a="1" /> + <span a="1" b=";" c="3" /> + <span a="1" b="&" c="3" /> + <span class="goodbye" /> + <span class=""goodbye"" /> + <span class="'goodbye'" /> + <span class=''goodbye'' /> + <span class="goodbye" /> + <span class="goodbye" /> + <span a="1" class="goodbye" /> + <span class=""goodbye"" /> + <span class=""goodbye"" /> + <span class="hello" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/005.pt b/src/chameleon/tests/outputs/005.pt new file mode 100644 index 0000000..b60fef6 --- /dev/null +++ b/src/chameleon/tests/outputs/005.pt @@ -0,0 +1,12 @@ +<html> + <body> + <img class="default" /> + <img /> + <span>Default</span> + <span>True</span> + <span>False</span> + <span> + <em>Computed default</em> + </span> + </body> +</html> diff --git a/src/chameleon/tests/outputs/006.pt b/src/chameleon/tests/outputs/006.pt new file mode 100644 index 0000000..fe4fef5 --- /dev/null +++ b/src/chameleon/tests/outputs/006.pt @@ -0,0 +1,9 @@ +<html> + <body class="ltr"> + <img src="#" alt="copyright (c) 2010" /> + <img src="#" alt="copyright (c) 2010" /> + <img src="#" alt="copyright (c) 2010" /> + <img alt="$ignored" /> + <img src="" alt="<type 'str'>" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/007.pt b/src/chameleon/tests/outputs/007.pt new file mode 100644 index 0000000..5e8783a --- /dev/null +++ b/src/chameleon/tests/outputs/007.pt @@ -0,0 +1,20 @@ +<html> + <body> + Hello world! + <div>Hello world!</div> + <div>Hello world!</div> + <type 'str'> + && + <script> + $(document).ready(function(){ + imagecropping.init_editor(); + }); + </script> + + Hello world + $leftalone + <div></div> + <div>Hello world</div> + <div>${} is ignored.</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/008.pt b/src/chameleon/tests/outputs/008.pt new file mode 100644 index 0000000..6c7bf08 --- /dev/null +++ b/src/chameleon/tests/outputs/008.pt @@ -0,0 +1,12 @@ +<html> + <body> + {} + + <div class="dynamic"> + static + </div> + <div> + nothing + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/009.pt b/src/chameleon/tests/outputs/009.pt new file mode 100644 index 0000000..91a4e97 --- /dev/null +++ b/src/chameleon/tests/outputs/009.pt @@ -0,0 +1,5 @@ +<html> + <body> + <div>Hello world!</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/010.pt b/src/chameleon/tests/outputs/010.pt new file mode 100644 index 0000000..bd72f1b --- /dev/null +++ b/src/chameleon/tests/outputs/010.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>1 < 2</div> + <div>2 < 3, 2&3, 2<3, 2>3</div> + <div>3 < 4</div> + <div>4 < 5</div> + <div>Hello world!</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/011-en.pt b/src/chameleon/tests/outputs/011-en.pt new file mode 100644 index 0000000..2e06201 --- /dev/null +++ b/src/chameleon/tests/outputs/011-en.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>Message ('message' translation into 'en')</div> + <div>Message ('message' translation into 'en')</div> + <div>Message ('message' translation into 'en')</div> + <div>Message ('message' translation into 'en')</div> + Message ('message' translation into 'en') + </body> +</html> diff --git a/src/chameleon/tests/outputs/011.pt b/src/chameleon/tests/outputs/011.pt new file mode 100644 index 0000000..aa1d317 --- /dev/null +++ b/src/chameleon/tests/outputs/011.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>Message</div> + <div>Message</div> + <div>Message</div> + <div>Message</div> + Message + </body> +</html> diff --git a/src/chameleon/tests/outputs/012-en.pt b/src/chameleon/tests/outputs/012-en.pt new file mode 100644 index 0000000..0ce5bdc --- /dev/null +++ b/src/chameleon/tests/outputs/012-en.pt @@ -0,0 +1,10 @@ +<html> + <body> + <div></div> + <div>Hello world! ('Hello world!' translation into 'en')</div> + <div>Hello world! ('hello_world' translation into 'en')</div> + <div><sup>Hello world!</sup> ('<sup>Hello world!</sup>' translation into 'en')</div> + <div>Hello <em>world</em>! Goodbye <em>planet</em>! ('Hello ${first}! Goodbye ${second}!' translation into 'en')</div> + <div>Hello <em>world</em>! Goodbye <em>planet</em>! ('hello_goodbye' translation into 'en')</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/012.pt b/src/chameleon/tests/outputs/012.pt new file mode 100644 index 0000000..482e678 --- /dev/null +++ b/src/chameleon/tests/outputs/012.pt @@ -0,0 +1,10 @@ +<html> + <body> + <div></div> + <div>Hello world!</div> + <div>Hello world!</div> + <div><sup>Hello world!</sup></div> + <div>Hello <em>world</em>! Goodbye <em>planet</em>!</div> + <div>Hello <em>world</em>! Goodbye <em>planet</em>!</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/013.pt b/src/chameleon/tests/outputs/013.pt new file mode 100644 index 0000000..1bdbbc6 --- /dev/null +++ b/src/chameleon/tests/outputs/013.pt @@ -0,0 +1,22 @@ +<html> + <body> + <table> + <tr> + <td> + [1,1] + </td> + <td> + [1,2] + </td> + </tr> + <tr> + <td> + [2,1] + </td> + <td> + [2,2] + </td> + </tr> + </table> + </body> +</html> diff --git a/src/chameleon/tests/outputs/014.pt b/src/chameleon/tests/outputs/014.pt new file mode 100644 index 0000000..07b8268 --- /dev/null +++ b/src/chameleon/tests/outputs/014.pt @@ -0,0 +1,12 @@ +<html> + <body> + <span> + <span>[3,3]</span> + <span>[3,4]</span> + </span> + <span> + <span>[4,3]</span> + <span>[4,4]</span> + </span> + </body> +</html> diff --git a/src/chameleon/tests/outputs/015-en.pt b/src/chameleon/tests/outputs/015-en.pt new file mode 100644 index 0000000..a3b82c6 --- /dev/null +++ b/src/chameleon/tests/outputs/015-en.pt @@ -0,0 +1,5 @@ +<html> + <body> + <div>Price: <span>Per kilo <em>12.5</em> ('Per kilo ${amount}' translation into 'en')</span> ('Price: ${price}' translation into 'en')</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/015.pt b/src/chameleon/tests/outputs/015.pt new file mode 100644 index 0000000..877b20f --- /dev/null +++ b/src/chameleon/tests/outputs/015.pt @@ -0,0 +1,5 @@ +<html> + <body> + <div>Price: <span>Per kilo <em>12.5</em></span></div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/016-en.pt b/src/chameleon/tests/outputs/016-en.pt new file mode 100644 index 0000000..b1a2a45 --- /dev/null +++ b/src/chameleon/tests/outputs/016-en.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>Hello world! ('Hello world!' translation into 'en')</div> + <img alt="Hello world! ('Hello world!' translation into 'en')" /> + <img alt="Hello world! ('hello_world' translation into 'en')" /> + <img alt="Hello world! ('Hello world!' translation into 'en')" /> + <img alt="Hello world! ('hello_world' translation into 'en')" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/016.pt b/src/chameleon/tests/outputs/016.pt new file mode 100644 index 0000000..e02e7bb --- /dev/null +++ b/src/chameleon/tests/outputs/016.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>Hello world!</div> + <img alt="Hello world!" /> + <img alt="Hello world!" /> + <img alt="Hello world!" /> + <img alt="Hello world!" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/017.pt b/src/chameleon/tests/outputs/017.pt new file mode 100644 index 0000000..735ee02 --- /dev/null +++ b/src/chameleon/tests/outputs/017.pt @@ -0,0 +1,12 @@ +<html> + <body> + Hello world! + 1 + Hello world! + 23 + 4Hello world! + <div>Hello world!</div> + Hello world! + Hello world! + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/018-en.pt b/src/chameleon/tests/outputs/018-en.pt new file mode 100644 index 0000000..f083de6 --- /dev/null +++ b/src/chameleon/tests/outputs/018-en.pt @@ -0,0 +1,3 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + october ('october' translation into 'en') 1982 ('1982' translation into 'en') ('${monthname} ${year}' translation into 'en') +</div> diff --git a/src/chameleon/tests/outputs/018.pt b/src/chameleon/tests/outputs/018.pt new file mode 100644 index 0000000..a58c185 --- /dev/null +++ b/src/chameleon/tests/outputs/018.pt @@ -0,0 +1,3 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + october 1982 +</div> diff --git a/src/chameleon/tests/outputs/019.pt b/src/chameleon/tests/outputs/019.pt new file mode 100644 index 0000000..03ba430 --- /dev/null +++ b/src/chameleon/tests/outputs/019.pt @@ -0,0 +1,13 @@ +<html> + <body> + Hello world! + Hello world!1 + 2Hello world! + Hello world!3 + Hello world!5 + 6Hello world! + 1 + 1.0 + True + </body> +</html> diff --git a/src/chameleon/tests/outputs/020.pt b/src/chameleon/tests/outputs/020.pt new file mode 100644 index 0000000..d84f201 --- /dev/null +++ b/src/chameleon/tests/outputs/020.pt @@ -0,0 +1,8 @@ +<html> + <body> + <div id="test"></div> + <div>NameError thrown at 5:24.</div> + <div></div> + <div></div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/021-en.pt b/src/chameleon/tests/outputs/021-en.pt new file mode 100644 index 0000000..41725da --- /dev/null +++ b/src/chameleon/tests/outputs/021-en.pt @@ -0,0 +1,12 @@ +<html> + <body> + <div>Hello world! ('Hello world!' translation into 'en' with domain 'new')</div> + <div>Hello world! ('Hello world!' translation into 'en' with domain 'old')</div> + <div class="test ('test' translation into 'en' with domain 'new')"> + Hello world! + </div> + <div class="test ('test_msgid' translation into 'en' with domain 'new')"> + Hello world! + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/021.pt b/src/chameleon/tests/outputs/021.pt new file mode 100644 index 0000000..fbfc68f --- /dev/null +++ b/src/chameleon/tests/outputs/021.pt @@ -0,0 +1,12 @@ +<html> + <body> + <div>Hello world!</div> + <div>Hello world!</div> + <div class="test"> + Hello world! + </div> + <div class="test"> + Hello world! + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/022.pt b/src/chameleon/tests/outputs/022.pt new file mode 100644 index 0000000..e7f5a23 --- /dev/null +++ b/src/chameleon/tests/outputs/022.pt @@ -0,0 +1,21 @@ +<html> + <body> + <div> + + <span>ok</span> + + + + ok + </div> + <div> + + <span>ok</span> + </div> + <div> + + + <span>ok</span> + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/023.pt b/src/chameleon/tests/outputs/023.pt new file mode 100644 index 0000000..49f6af9 --- /dev/null +++ b/src/chameleon/tests/outputs/023.pt @@ -0,0 +1,6 @@ +<html> + <body> + + <span>ok</span> + </body> +</html> diff --git a/src/chameleon/tests/outputs/024.pt b/src/chameleon/tests/outputs/024.pt new file mode 100644 index 0000000..68e12bf --- /dev/null +++ b/src/chameleon/tests/outputs/024.pt @@ -0,0 +1,14 @@ +<html> + <body> + + + first + + second + + + ok + + + </body> +</html> diff --git a/src/chameleon/tests/outputs/025.pt b/src/chameleon/tests/outputs/025.pt new file mode 100644 index 0000000..d1acac8 --- /dev/null +++ b/src/chameleon/tests/outputs/025.pt @@ -0,0 +1,22 @@ +<html> + <body> + <ul> + <li>1</li> + <li>2</li> + <li>3</li> + <li>1</li><li>2</li><li>3</li> + <li>1</li> + <li>2</li> + <li>3</li> + + + 1, + + 2, + + 3 + + . + </ul> + </body> +</html> diff --git a/src/chameleon/tests/outputs/026.pt b/src/chameleon/tests/outputs/026.pt new file mode 100644 index 0000000..8252f85 --- /dev/null +++ b/src/chameleon/tests/outputs/026.pt @@ -0,0 +1,17 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + <ul> + <li name="0-0" class="even">0</li> + <li name="1-1" class="odd">1</li> + <li name="2-2" class="even">2</li> + </ul> + <ul> + <li class="even">0</li> + <li class="odd">1</li> + <li class="even">2</li> + </ul> + <ul> + <li>even</li> + <li>odd</li> + <li>even</li> + </ul> +</div> diff --git a/src/chameleon/tests/outputs/027.pt b/src/chameleon/tests/outputs/027.pt new file mode 100644 index 0000000..4af28a7 --- /dev/null +++ b/src/chameleon/tests/outputs/027.pt @@ -0,0 +1,7 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + <span id="test" + class="defabcdummy" + onClick="alert();" style="hij">abcghi</span> + Hello World! + Hello World! +</div> diff --git a/src/chameleon/tests/outputs/028.pt b/src/chameleon/tests/outputs/028.pt new file mode 100644 index 0000000..61e7f31 --- /dev/null +++ b/src/chameleon/tests/outputs/028.pt @@ -0,0 +1,5 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + <option selected="True"></option> + <option></option> + <option></option> +</div> diff --git a/src/chameleon/tests/outputs/029.pt b/src/chameleon/tests/outputs/029.pt new file mode 100644 index 0000000..71f54ea --- /dev/null +++ b/src/chameleon/tests/outputs/029.pt @@ -0,0 +1,3 @@ +<div xmlns="http://www.w3.org/1999/xhtml"> + <a rel="self" href="http://python.org" id="link-id" /> +</div> diff --git a/src/chameleon/tests/outputs/030.pt b/src/chameleon/tests/outputs/030.pt new file mode 100644 index 0000000..8e39450 --- /dev/null +++ b/src/chameleon/tests/outputs/030.pt @@ -0,0 +1,10 @@ +<html> + <body> + <div> + 1, 1, 2 + </div> + <div> + 2, 3, 4 + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/031.pt b/src/chameleon/tests/outputs/031.pt new file mode 100644 index 0000000..dd6f9d3 --- /dev/null +++ b/src/chameleon/tests/outputs/031.pt @@ -0,0 +1,7 @@ +<div> + Hello World! + Hello World! + Hello World! + 012 + True +</div> diff --git a/src/chameleon/tests/outputs/032.pt b/src/chameleon/tests/outputs/032.pt new file mode 100644 index 0000000..817b301 --- /dev/null +++ b/src/chameleon/tests/outputs/032.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/033.pt b/src/chameleon/tests/outputs/033.pt new file mode 100644 index 0000000..817b301 --- /dev/null +++ b/src/chameleon/tests/outputs/033.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/034.pt b/src/chameleon/tests/outputs/034.pt new file mode 100644 index 0000000..817b301 --- /dev/null +++ b/src/chameleon/tests/outputs/034.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/035.pt b/src/chameleon/tests/outputs/035.pt new file mode 100644 index 0000000..1acdf7c --- /dev/null +++ b/src/chameleon/tests/outputs/035.pt @@ -0,0 +1,17 @@ +<html> + <head> + <title> + New title + </title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/036.pt b/src/chameleon/tests/outputs/036.pt new file mode 100644 index 0000000..6eb5cc0 --- /dev/null +++ b/src/chameleon/tests/outputs/036.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>New title</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/037.pt b/src/chameleon/tests/outputs/037.pt new file mode 100644 index 0000000..e278467 --- /dev/null +++ b/src/chameleon/tests/outputs/037.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <span>ok</span> + + </div> + <div id="footer"> + + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/038.pt b/src/chameleon/tests/outputs/038.pt new file mode 100644 index 0000000..ba1af40 --- /dev/null +++ b/src/chameleon/tests/outputs/038.pt @@ -0,0 +1,6 @@ + +<html> + <body> + <span>ok</span> + </body> +</html> diff --git a/src/chameleon/tests/outputs/039.pt b/src/chameleon/tests/outputs/039.pt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/chameleon/tests/outputs/039.pt diff --git a/src/chameleon/tests/outputs/040.pt b/src/chameleon/tests/outputs/040.pt new file mode 100644 index 0000000..773fda0 --- /dev/null +++ b/src/chameleon/tests/outputs/040.pt @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" + template-macros="master" + macros="master"> + + foo + + <span template-macros="master" + macros="master"> + <!-- demonstrate difference between + `template` and `macros` symbol --> + </span> + +</html> diff --git a/src/chameleon/tests/outputs/041.pt b/src/chameleon/tests/outputs/041.pt new file mode 100644 index 0000000..38902c6 --- /dev/null +++ b/src/chameleon/tests/outputs/041.pt @@ -0,0 +1,7 @@ +<html> + <body> + <div>Hello <span>world!</span></div> + <div>Hello <span>world!</span></div> + <div>Goodbye</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/042.pt b/src/chameleon/tests/outputs/042.pt new file mode 100644 index 0000000..c1faeed --- /dev/null +++ b/src/chameleon/tests/outputs/042.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + New footer + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/043.pt b/src/chameleon/tests/outputs/043.pt new file mode 100644 index 0000000..3dee3f3 --- /dev/null +++ b/src/chameleon/tests/outputs/043.pt @@ -0,0 +1,11 @@ +<html> + <body> + + + + + My title + + + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/044.pt b/src/chameleon/tests/outputs/044.pt new file mode 100644 index 0000000..192be69 --- /dev/null +++ b/src/chameleon/tests/outputs/044.pt @@ -0,0 +1,5 @@ +<html> + <body> + a, b + </body> +</html> diff --git a/src/chameleon/tests/outputs/045.pt b/src/chameleon/tests/outputs/045.pt new file mode 100644 index 0000000..8afde05 --- /dev/null +++ b/src/chameleon/tests/outputs/045.pt @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE application [ + <!ENTITY nbsp "\ "> +]> +<application xmlns="http://research.sun.com/wadl/2006/10" + xsi:schemaLocation="http://research.sun.com/wadl/2006/10/wadl.xsd" + xmlns:wadl="http://research.sun.com/wadl/2006/10" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <resources> + <resource path="/">ZZZ YYY XXX</resource> + </resources> +</application> diff --git a/src/chameleon/tests/outputs/046.pt b/src/chameleon/tests/outputs/046.pt new file mode 100644 index 0000000..666fbb2 --- /dev/null +++ b/src/chameleon/tests/outputs/046.pt @@ -0,0 +1,17 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + <span>New</span> footer + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/047.pt b/src/chameleon/tests/outputs/047.pt new file mode 100644 index 0000000..1074b11 --- /dev/null +++ b/src/chameleon/tests/outputs/047.pt @@ -0,0 +1,17 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + <em>Extended</em> footer + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/048.pt b/src/chameleon/tests/outputs/048.pt new file mode 100644 index 0000000..27f6b18 --- /dev/null +++ b/src/chameleon/tests/outputs/048.pt @@ -0,0 +1,17 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + Extended footer + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/049.pt b/src/chameleon/tests/outputs/049.pt new file mode 100644 index 0000000..612f38d --- /dev/null +++ b/src/chameleon/tests/outputs/049.pt @@ -0,0 +1,11 @@ +<html> + <body> + <pre>amp=&amp; lt=&lt;</pre> + <pre>amp=& lt=<</pre> + <script /> + <script /> + <script /> + <script /> + <img alt="1 < 2: True" /> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/050.pt b/src/chameleon/tests/outputs/050.pt new file mode 100644 index 0000000..6141cd5 --- /dev/null +++ b/src/chameleon/tests/outputs/050.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + Default content + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/051.pt b/src/chameleon/tests/outputs/051.pt new file mode 100644 index 0000000..6141cd5 --- /dev/null +++ b/src/chameleon/tests/outputs/051.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + Default content + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/052.pt b/src/chameleon/tests/outputs/052.pt new file mode 100644 index 0000000..6bd85ad --- /dev/null +++ b/src/chameleon/tests/outputs/052.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <span>Translated content</span> + + </div> + <div id="footer"> + + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/053.pt b/src/chameleon/tests/outputs/053.pt new file mode 100644 index 0000000..7133ff0 --- /dev/null +++ b/src/chameleon/tests/outputs/053.pt @@ -0,0 +1,6 @@ +<html> + <body> + <a href="@@view">test</a> + <a href="@@view">test</a> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/054.pt b/src/chameleon/tests/outputs/054.pt new file mode 100644 index 0000000..9644613 --- /dev/null +++ b/src/chameleon/tests/outputs/054.pt @@ -0,0 +1,3 @@ +<div> + 1984-12-31T00:00:00 +</div>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/055.pt b/src/chameleon/tests/outputs/055.pt new file mode 100644 index 0000000..2cdbc4b --- /dev/null +++ b/src/chameleon/tests/outputs/055.pt @@ -0,0 +1,4 @@ +<div> + bar + baz +</div> diff --git a/src/chameleon/tests/outputs/056.pt b/src/chameleon/tests/outputs/056.pt new file mode 100644 index 0000000..2d20b13 --- /dev/null +++ b/src/chameleon/tests/outputs/056.pt @@ -0,0 +1,7 @@ +<html> + <body> + + Namespace tag + + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/057.pt b/src/chameleon/tests/outputs/057.pt new file mode 100644 index 0000000..9099fe3 --- /dev/null +++ b/src/chameleon/tests/outputs/057.pt @@ -0,0 +1,8 @@ +<html> + <body> + + 1 + 2 + 3 + </body> +</html> diff --git a/src/chameleon/tests/outputs/058.pt b/src/chameleon/tests/outputs/058.pt new file mode 100644 index 0000000..d51cf5a --- /dev/null +++ b/src/chameleon/tests/outputs/058.pt @@ -0,0 +1,16 @@ +<html> + <head> + <script> + deform.addCallback( + '1', + function (oid) { + $('#' + oid).autocomplete({source: values}); + $('#' + oid).autocomplete("option", options); + } + ); + </script> + </head> + <body> + <!-- Form --> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/059.pt b/src/chameleon/tests/outputs/059.pt new file mode 100644 index 0000000..3a2d216 --- /dev/null +++ b/src/chameleon/tests/outputs/059.pt @@ -0,0 +1,6 @@ +<html> + <body> + <span onclick="alert(true && false);">test</span> + <span onclick="alert(true && false);">test</span> + </body> +</html> diff --git a/src/chameleon/tests/outputs/060.pt b/src/chameleon/tests/outputs/060.pt new file mode 100644 index 0000000..ec5c045 --- /dev/null +++ b/src/chameleon/tests/outputs/060.pt @@ -0,0 +1,8 @@ +<html> + <head> + <title>Untitled</title> + </head> + <body> + <h1>Untitled</h1> + </body> +</html> diff --git a/src/chameleon/tests/outputs/061.pt b/src/chameleon/tests/outputs/061.pt new file mode 100644 index 0000000..fc34ac7 --- /dev/null +++ b/src/chameleon/tests/outputs/061.pt @@ -0,0 +1,8 @@ +<html> + <head> + <title>My document</title> + </head> + <body> + <h1>My document</h1> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/062.pt b/src/chameleon/tests/outputs/062.pt new file mode 100644 index 0000000..9e01acc --- /dev/null +++ b/src/chameleon/tests/outputs/062.pt @@ -0,0 +1,27 @@ +<div> + +</div> + +<div> + <!-- ${left alone} --> +</div> + +<div> + <!-- not left alone --> +</div> + +<div> + <!-- not left alone --> +</div> + +<div> + <!-- ${left alone} --> +</div> + +<!--[if IE 6]> + Special instructions for IE 6 here +<![endif]--> + +<!--[if IE 6]> + ${left alone} +<![endif]-->
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/063.pt b/src/chameleon/tests/outputs/063.pt new file mode 100644 index 0000000..ce20563 --- /dev/null +++ b/src/chameleon/tests/outputs/063.pt @@ -0,0 +1,3 @@ +<div> + 2 +</div>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/064.pt b/src/chameleon/tests/outputs/064.pt new file mode 100644 index 0000000..2f83961 --- /dev/null +++ b/src/chameleon/tests/outputs/064.pt @@ -0,0 +1,3 @@ + + <div id="test" class="test"> + </div> diff --git a/src/chameleon/tests/outputs/065.pt b/src/chameleon/tests/outputs/065.pt new file mode 100644 index 0000000..66d8886 --- /dev/null +++ b/src/chameleon/tests/outputs/065.pt @@ -0,0 +1,13 @@ +<html> + <head> + <title>Title</title> + </head> + <body> + <div id="content"> + <div>Content</div> + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/066.pt b/src/chameleon/tests/outputs/066.pt new file mode 100644 index 0000000..c3352aa --- /dev/null +++ b/src/chameleon/tests/outputs/066.pt @@ -0,0 +1,5 @@ +<html> + <body> + Hello world! + </body> +</html> diff --git a/src/chameleon/tests/outputs/067.pt b/src/chameleon/tests/outputs/067.pt new file mode 100644 index 0000000..84d379c --- /dev/null +++ b/src/chameleon/tests/outputs/067.pt @@ -0,0 +1,6 @@ +<html> + <body> + <img src="#" class="up" /> + <img src="#" class="down" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/068.pt b/src/chameleon/tests/outputs/068.pt new file mode 100644 index 0000000..fdde265 --- /dev/null +++ b/src/chameleon/tests/outputs/068.pt @@ -0,0 +1,8 @@ +<html> + <body> + <span>0 < 1 or 0 > 1</span> + <span>0 < 1 or 0 > 1</span> + <span class="0 < 1 or 0 > 1" /> + <span>0 < 1 or 0 > 1</span> + </body> +</html> diff --git a/src/chameleon/tests/outputs/069-en.pt b/src/chameleon/tests/outputs/069-en.pt new file mode 100644 index 0000000..acadf4e --- /dev/null +++ b/src/chameleon/tests/outputs/069-en.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Title ('title' translation into 'en' with domain 'test')</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/069.pt b/src/chameleon/tests/outputs/069.pt new file mode 100644 index 0000000..1a564e0 --- /dev/null +++ b/src/chameleon/tests/outputs/069.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Title</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/070-en.pt b/src/chameleon/tests/outputs/070-en.pt new file mode 100644 index 0000000..acadf4e --- /dev/null +++ b/src/chameleon/tests/outputs/070-en.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Title ('title' translation into 'en' with domain 'test')</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/070.pt b/src/chameleon/tests/outputs/070.pt new file mode 100644 index 0000000..1a564e0 --- /dev/null +++ b/src/chameleon/tests/outputs/070.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Title</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/071.pt b/src/chameleon/tests/outputs/071.pt new file mode 100644 index 0000000..d9b8a05 --- /dev/null +++ b/src/chameleon/tests/outputs/071.pt @@ -0,0 +1,12 @@ +<html> + <body> + <input type="input" checked="True" /> + <input type="input" /> + <input type="input" /> + <input type="input" checked="checked" /> + <input type="input" checked /> + <input type="input" checked /> + <input type="input" /> + <input type="input" class="True" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/072.pt b/src/chameleon/tests/outputs/072.pt new file mode 100644 index 0000000..b59ee72 --- /dev/null +++ b/src/chameleon/tests/outputs/072.pt @@ -0,0 +1,19 @@ +<html> + <body> + <ul> + <li class="odd">1</li> + <li>2</li> + <li class="odd">3</li> + </ul> + <ul> + <li class="odd">1</li> + <li>2</li> + <li class="odd">3</li> + </ul> + <ul> + <li class="odd">1</li> + <li>2</li> + <li class="odd">3</li> + </ul> + </body> +</html> diff --git a/src/chameleon/tests/outputs/073.pt b/src/chameleon/tests/outputs/073.pt new file mode 100644 index 0000000..5054129 --- /dev/null +++ b/src/chameleon/tests/outputs/073.pt @@ -0,0 +1,5 @@ +<?xml version="1.0" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>my title — my site</title></head> +<body></body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/074.pt b/src/chameleon/tests/outputs/074.pt new file mode 100644 index 0000000..40a44d8 --- /dev/null +++ b/src/chameleon/tests/outputs/074.pt @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="windows-1251" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head><title>my title — my site</title></head> +<body></body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/075.pt b/src/chameleon/tests/outputs/075.pt new file mode 100644 index 0000000..e6f18d9 --- /dev/null +++ b/src/chameleon/tests/outputs/075.pt @@ -0,0 +1,19 @@ + + <html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + + foo + + + </div> + <div id="footer"> + + </div> + </body> +</html> + diff --git a/src/chameleon/tests/outputs/076.pt b/src/chameleon/tests/outputs/076.pt new file mode 100644 index 0000000..6397767 --- /dev/null +++ b/src/chameleon/tests/outputs/076.pt @@ -0,0 +1,17 @@ + + <html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> +<BLANKLINE> + bar +<BLANKLINE> + </div> + <div id="footer"> +<BLANKLINE> + </div> + </body> +</html> + diff --git a/src/chameleon/tests/outputs/077-en.pt b/src/chameleon/tests/outputs/077-en.pt new file mode 100644 index 0000000..ac74ae1 --- /dev/null +++ b/src/chameleon/tests/outputs/077-en.pt @@ -0,0 +1 @@ +<input type="hidden" id="uploadify_label_file_title" value="value ('Title' translation into 'en' with domain 'test')" />
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/077.pt b/src/chameleon/tests/outputs/077.pt new file mode 100644 index 0000000..53fca7d --- /dev/null +++ b/src/chameleon/tests/outputs/077.pt @@ -0,0 +1 @@ +<input type="hidden" id="uploadify_label_file_title" value="value" />
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/078.pt b/src/chameleon/tests/outputs/078.pt new file mode 100644 index 0000000..3503501 --- /dev/null +++ b/src/chameleon/tests/outputs/078.pt @@ -0,0 +1,9 @@ + + <body> + <span id="foo-toDataContainer"> + <script type="text/javascript"> + copyDataForSubmit('foo');</script> + </span> + <span class="selected-option">1</span>, <span class="selected-option">2</span>, <span class="selected-option">3</span> + </body> + diff --git a/src/chameleon/tests/outputs/079-en.pt b/src/chameleon/tests/outputs/079-en.pt new file mode 100644 index 0000000..680183a --- /dev/null +++ b/src/chameleon/tests/outputs/079-en.pt @@ -0,0 +1,16 @@ +<html> + <head> + <title>Welcome ('Welcome' translation into 'en')</title> + </head> + <body> + <h1>Welcome ('Welcome' translation into 'en')</h1> + <span>An edge case: ${. ('An edge case: ${.' translation into 'en')</span> + <img alt="Site logo ('Site logo' translation into 'en')" href="http://host/logo.png" /> + <img alt="Site logo ('Site logo' translation into 'en')" href="http://host/logo.png" /> + <div id="content"> + boo foo. ('boo foo.' translation into 'en') + <br /> + bar. ('bar.' translation into 'en') + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/079.pt b/src/chameleon/tests/outputs/079.pt new file mode 100644 index 0000000..1a05467 --- /dev/null +++ b/src/chameleon/tests/outputs/079.pt @@ -0,0 +1,16 @@ +<html> + <head> + <title>Welcome</title> + </head> + <body> + <h1>Welcome</h1> + <span>An edge case: ${.</span> + <img alt="Site logo" href="http://host/logo.png" /> + <img alt="Site logo" href="http://host/logo.png" /> + <div id="content"> + boo foo. + <br /> + bar. + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/080.pt b/src/chameleon/tests/outputs/080.pt new file mode 100644 index 0000000..a15dba4 --- /dev/null +++ b/src/chameleon/tests/outputs/080.pt @@ -0,0 +1,3 @@ + + Hello world + diff --git a/src/chameleon/tests/outputs/081.pt b/src/chameleon/tests/outputs/081.pt new file mode 100644 index 0000000..9b5e558 --- /dev/null +++ b/src/chameleon/tests/outputs/081.pt @@ -0,0 +1,5 @@ +<html> + <body> + Hello world! + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/082.pt b/src/chameleon/tests/outputs/082.pt new file mode 100644 index 0000000..9b5e558 --- /dev/null +++ b/src/chameleon/tests/outputs/082.pt @@ -0,0 +1,5 @@ +<html> + <body> + Hello world! + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/083.pt b/src/chameleon/tests/outputs/083.pt new file mode 100644 index 0000000..a673741 --- /dev/null +++ b/src/chameleon/tests/outputs/083.pt @@ -0,0 +1,15 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + + <!-- content here --> + + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/084.pt b/src/chameleon/tests/outputs/084.pt new file mode 100644 index 0000000..362cb8a --- /dev/null +++ b/src/chameleon/tests/outputs/084.pt @@ -0,0 +1,9 @@ +<html> + <head> + <script> + <![CDATA[ + alert("Hello world!"); + ]]> + </script> + </head> +</html> diff --git a/src/chameleon/tests/outputs/085-en.pt b/src/chameleon/tests/outputs/085-en.pt new file mode 100644 index 0000000..2b72251 --- /dev/null +++ b/src/chameleon/tests/outputs/085-en.pt @@ -0,0 +1,9 @@ +<html> + <head> + <title>Welcome</title> + </head> + <body> + <h1>Welcome</h1> + <p><a href="http://host">Click here ('Click here' translation into 'en' with domain 'new')</a> to continue. ('${click_here} to continue.' translation into 'en' with domain 'new')</p> + </body> +</html> diff --git a/src/chameleon/tests/outputs/085.pt b/src/chameleon/tests/outputs/085.pt new file mode 100644 index 0000000..b9cdcad --- /dev/null +++ b/src/chameleon/tests/outputs/085.pt @@ -0,0 +1,9 @@ +<html> + <head> + <title>Welcome</title> + </head> + <body> + <h1>Welcome</h1> + <p><a href="http://host">Click here</a> to continue.</p> + </body> +</html> diff --git a/src/chameleon/tests/outputs/086.pt b/src/chameleon/tests/outputs/086.pt new file mode 100644 index 0000000..d39fde4 --- /dev/null +++ b/src/chameleon/tests/outputs/086.pt @@ -0,0 +1,18 @@ +<html> + <head> + <title>Master template</title> + </head> + <body> + <div id="content"> + <div> + <div> + <a href="#">Chart</a> + <div id="chart-info"/> + </div> + </div> + </div> + <div id="footer"> + + </div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/087.pt b/src/chameleon/tests/outputs/087.pt new file mode 100644 index 0000000..791d1c5 --- /dev/null +++ b/src/chameleon/tests/outputs/087.pt @@ -0,0 +1,25 @@ + + +<ul> + <li>1</li> + <li>2</li> + <li>3</li> +</ul> + + + +<ul> + <li>5</li> + <li>7</li> + <li>9</li> +</ul> + +<div> + + Please input a number from the range 1, 2, 3, 4, 5, 6, 7, 8, 9. +</div> + +<div> + + 41 + 1 = 42. +</div>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/088.pt b/src/chameleon/tests/outputs/088.pt new file mode 100644 index 0000000..d901c21 --- /dev/null +++ b/src/chameleon/tests/outputs/088.pt @@ -0,0 +1 @@ +a, b, c diff --git a/src/chameleon/tests/outputs/089.pt b/src/chameleon/tests/outputs/089.pt new file mode 100644 index 0000000..9b5e558 --- /dev/null +++ b/src/chameleon/tests/outputs/089.pt @@ -0,0 +1,5 @@ +<html> + <body> + Hello world! + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/090.pt b/src/chameleon/tests/outputs/090.pt new file mode 100644 index 0000000..ba98574 --- /dev/null +++ b/src/chameleon/tests/outputs/090.pt @@ -0,0 +1,14 @@ +<ul> + <li> + <a href="#main" id="mainnav">Welcome</a> + </li> + <li> + <a href="#listings" id="listingsnav">Listings</a> + </li> + <li> + <a href="#about" id="aboutnav">About</a> + </li> + <li> + <a href="#contact" id="contactnav">Contact</a> + </li> +</ul> diff --git a/src/chameleon/tests/outputs/091.pt b/src/chameleon/tests/outputs/091.pt new file mode 100644 index 0000000..e435ac7 --- /dev/null +++ b/src/chameleon/tests/outputs/091.pt @@ -0,0 +1,5 @@ +<html> + <body> + + </body> +</html> diff --git a/src/chameleon/tests/outputs/101.html b/src/chameleon/tests/outputs/101.html new file mode 100644 index 0000000..03230ec --- /dev/null +++ b/src/chameleon/tests/outputs/101.html @@ -0,0 +1,5 @@ +<html> + <body> + <h1><br><br>Hello world</h1> + </body> +</html> diff --git a/src/chameleon/tests/outputs/102.html b/src/chameleon/tests/outputs/102.html new file mode 100644 index 0000000..ac6400d --- /dev/null +++ b/src/chameleon/tests/outputs/102.html @@ -0,0 +1,5 @@ +<html> + <body> + <h1 class=title align=center>Hello world</h1> + </body> +</html> diff --git a/src/chameleon/tests/outputs/103.html b/src/chameleon/tests/outputs/103.html new file mode 100644 index 0000000..a8a4000 --- /dev/null +++ b/src/chameleon/tests/outputs/103.html @@ -0,0 +1,8 @@ +<html> + <body> + <select name="foo2" multiple style="visibility: hidden"> + <option value="1">this should</option> + <option value="2">remain hidden right?</option> + </select> + </body> +</html> diff --git a/src/chameleon/tests/outputs/120-en.pt b/src/chameleon/tests/outputs/120-en.pt new file mode 100644 index 0000000..11a949c --- /dev/null +++ b/src/chameleon/tests/outputs/120-en.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>Hello world! ('Hello world!' translation into 'en')</div> + <button>Hello world! ('Hello world!' translation into 'en', context 'button')</button> + <div> + <a>Tab ('Tab' translation into 'en' with domain 'new', context 'navigation')</a> + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/120.pt b/src/chameleon/tests/outputs/120.pt new file mode 100644 index 0000000..14f8e8b --- /dev/null +++ b/src/chameleon/tests/outputs/120.pt @@ -0,0 +1,9 @@ +<html> + <body> + <div>Hello world!</div> + <button>Hello world!</button> + <div> + <a>Tab</a> + </div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/121.pt b/src/chameleon/tests/outputs/121.pt new file mode 100644 index 0000000..ab766de --- /dev/null +++ b/src/chameleon/tests/outputs/121.pt @@ -0,0 +1,5 @@ +<html> + <body> + <h1>Hello world!</h1> + </body> +</html> diff --git a/src/chameleon/tests/outputs/122.pt b/src/chameleon/tests/outputs/122.pt new file mode 100644 index 0000000..1aeda2f --- /dev/null +++ b/src/chameleon/tests/outputs/122.pt @@ -0,0 +1,9 @@ +<html> + <body> + <h1>Hello world!</h1> + <a href="http://www.python.org" + title="Python"> + Python is a programming language. + </a> + </body> +</html> diff --git a/src/chameleon/tests/outputs/123.pt b/src/chameleon/tests/outputs/123.pt new file mode 100644 index 0000000..ed05144 --- /dev/null +++ b/src/chameleon/tests/outputs/123.pt @@ -0,0 +1,5 @@ +<html> + <body> + <div foo="bar">Hello world!</div> + </body> +</html> diff --git a/src/chameleon/tests/outputs/124-en.pt b/src/chameleon/tests/outputs/124-en.pt new file mode 100644 index 0000000..ee69555 --- /dev/null +++ b/src/chameleon/tests/outputs/124-en.pt @@ -0,0 +1,6 @@ +<html> + <body> + <h1>Hello world! ('Hello world!' translation into 'fr')</h1> + <p>It's a big world. ('It's a big world.' translation into 'en')</p> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/124.pt b/src/chameleon/tests/outputs/124.pt new file mode 100644 index 0000000..cb2740d --- /dev/null +++ b/src/chameleon/tests/outputs/124.pt @@ -0,0 +1,6 @@ +<html> + <body> + <h1>Hello world! ('Hello world!' translation into 'fr')</h1> + <p>It's a big world.</p> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/125.pt b/src/chameleon/tests/outputs/125.pt new file mode 100644 index 0000000..3e703e3 --- /dev/null +++ b/src/chameleon/tests/outputs/125.pt @@ -0,0 +1,6 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" + lang="en"> + <body> + <div><h1>h1</h1> <h2>h2</h2></div> + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/126.pt b/src/chameleon/tests/outputs/126.pt new file mode 100644 index 0000000..e831547 --- /dev/null +++ b/src/chameleon/tests/outputs/126.pt @@ -0,0 +1,9 @@ +<html> + <body> + & + & + & + & + <a href="localhost?a=1b&2" /> + </body> +</html> diff --git a/src/chameleon/tests/outputs/238.pt b/src/chameleon/tests/outputs/238.pt new file mode 100644 index 0000000..b967fb2 --- /dev/null +++ b/src/chameleon/tests/outputs/238.pt @@ -0,0 +1,7 @@ + + + + + + <h1>macros['page']</h1> + diff --git a/src/chameleon/tests/outputs/greeting.pt b/src/chameleon/tests/outputs/greeting.pt new file mode 100644 index 0000000..0261e30 --- /dev/null +++ b/src/chameleon/tests/outputs/greeting.pt @@ -0,0 +1 @@ +<div>Hello, undefined.</div> diff --git a/src/chameleon/tests/outputs/hello_world.pt b/src/chameleon/tests/outputs/hello_world.pt new file mode 100644 index 0000000..9b5e558 --- /dev/null +++ b/src/chameleon/tests/outputs/hello_world.pt @@ -0,0 +1,5 @@ +<html> + <body> + Hello world! + </body> +</html>
\ No newline at end of file diff --git a/src/chameleon/tests/outputs/hello_world.txt b/src/chameleon/tests/outputs/hello_world.txt new file mode 100644 index 0000000..cd08755 --- /dev/null +++ b/src/chameleon/tests/outputs/hello_world.txt @@ -0,0 +1 @@ +Hello world! diff --git a/src/chameleon/tests/outputs/multinode-en.pt b/src/chameleon/tests/outputs/multinode-en.pt new file mode 100644 index 0000000..298dad8 --- /dev/null +++ b/src/chameleon/tests/outputs/multinode-en.pt @@ -0,0 +1,3 @@ +<html> + <body>Foo Message ('message' translation into 'en') ('Foo ${message}' translation into 'en')</body> +</html> diff --git a/src/chameleon/tests/outputs/multinode.pt b/src/chameleon/tests/outputs/multinode.pt new file mode 100644 index 0000000..289d0f6 --- /dev/null +++ b/src/chameleon/tests/outputs/multinode.pt @@ -0,0 +1,3 @@ +<html> + <body>Foo Message</body> +</html> diff --git a/src/chameleon/tests/test_doctests.py b/src/chameleon/tests/test_doctests.py new file mode 100644 index 0000000..c84c200 --- /dev/null +++ b/src/chameleon/tests/test_doctests.py @@ -0,0 +1,40 @@ +import unittest +import doctest + +OPTIONFLAGS = (doctest.ELLIPSIS | + doctest.REPORT_ONLY_FIRST_FAILURE) + + +class DoctestCase(unittest.TestCase): + def __new__(self, test): + return getattr(self, test)() + + @classmethod + def test_tal(cls): + from chameleon import tal + return doctest.DocTestSuite( + tal, optionflags=OPTIONFLAGS) + + @classmethod + def test_tales(cls): + from chameleon import tales + return doctest.DocTestSuite( + tales, optionflags=OPTIONFLAGS) + + @classmethod + def test_utils(cls): + from chameleon import utils + return doctest.DocTestSuite( + utils, optionflags=OPTIONFLAGS) + + @classmethod + def test_exc(cls): + from chameleon import exc + return doctest.DocTestSuite( + exc, optionflags=OPTIONFLAGS) + + @classmethod + def test_compiler(cls): + from chameleon import compiler + return doctest.DocTestSuite( + compiler, optionflags=OPTIONFLAGS) diff --git a/src/chameleon/tests/test_exc.py b/src/chameleon/tests/test_exc.py new file mode 100644 index 0000000..ddb0b6d --- /dev/null +++ b/src/chameleon/tests/test_exc.py @@ -0,0 +1,13 @@ +from unittest import TestCase + +class TestTemplateError(TestCase): + + def test_keep_token_location_info(self): + # tokens should not lose information when passed to a TemplateError + from chameleon import exc, tokenize, utils + token = tokenize.Token('stuff', 5, 'more\nstuff', 'mystuff.txt') + error = exc.TemplateError('message', token) + s = str(error) + self.assertTrue( + '- Location: (line 2: col 0)' in s, + 'No location data found\n%s' % s) diff --git a/src/chameleon/tests/test_loader.py b/src/chameleon/tests/test_loader.py new file mode 100644 index 0000000..def2170 --- /dev/null +++ b/src/chameleon/tests/test_loader.py @@ -0,0 +1,110 @@ +import unittest + + +class LoadTests: + def _makeOne(self, search_path=None, **kwargs): + klass = self._getTargetClass() + return klass(search_path, **kwargs) + + def _getTargetClass(self): + from chameleon.loader import TemplateLoader + return TemplateLoader + + def test_load_relative(self): + import os + here = os.path.join(os.path.dirname(__file__), "inputs") + loader = self._makeOne(search_path=[here]) + result = self._load(loader, 'hello_world.pt') + self.assertEqual(result.filename, os.path.join(here, 'hello_world.pt')) + + def test_consecutive_loads(self): + import os + here = os.path.join(os.path.dirname(__file__), "inputs") + loader = self._makeOne(search_path=[here]) + + self.assertTrue( + self._load(loader, 'hello_world.pt') is \ + self._load(loader, 'hello_world.pt')) + + def test_load_relative_badpath_in_searchpath(self): + import os + here = os.path.join(os.path.dirname(__file__), "inputs") + loader = self._makeOne(search_path=[os.path.join(here, 'none'), here]) + result = self._load(loader, 'hello_world.pt') + self.assertEqual(result.filename, os.path.join(here, 'hello_world.pt')) + + def test_load_abs(self): + import os + here = os.path.join(os.path.dirname(__file__), "inputs") + loader = self._makeOne() + abs = os.path.join(here, 'hello_world.pt') + result = self._load(loader, abs) + self.assertEqual(result.filename, abs) + + +class LoadPageTests(unittest.TestCase, LoadTests): + def _load(self, loader, filename): + from chameleon.zpt import template + return loader.load(filename, template.PageTemplateFile) + + +class ModuleLoadTests(unittest.TestCase): + def _makeOne(self, *args, **kwargs): + from chameleon.loader import ModuleLoader + return ModuleLoader(*args, **kwargs) + + def test_build(self): + import tempfile + path = tempfile.mkdtemp() + loader = self._makeOne(path) + source = "def function(): return %r" % "\xc3\xa6\xc3\xb8\xc3\xa5" + try: + source = source.decode('utf-8') + except AttributeError: + import sys + self.assertTrue(sys.version_info[0] > 2) + + module = loader.build(source, "test.xml") + result1 = module['function']() + d = {} + code = compile(source, 'test.py', 'exec') + exec(code, d) + result2 = d['function']() + self.assertEqual(result1, result2) + + import os + self.assertTrue("test.py" in os.listdir(path)) + + import shutil + shutil.rmtree(path) + + +class ZPTLoadTests(unittest.TestCase): + def _makeOne(self, *args, **kwargs): + import os + here = os.path.join(os.path.dirname(__file__), "inputs") + from chameleon.zpt import loader + return loader.TemplateLoader(here, **kwargs) + + def test_load_xml(self): + loader = self._makeOne() + template = loader.load("hello_world.pt", "xml") + from chameleon.zpt.template import PageTemplateFile + self.assertTrue(isinstance(template, PageTemplateFile)) + + def test_load_text(self): + loader = self._makeOne() + template = loader.load("hello_world.txt", "text") + from chameleon.zpt.template import PageTextTemplateFile + self.assertTrue(isinstance(template, PageTextTemplateFile)) + + def test_load_getitem_gets_xml_file(self): + loader = self._makeOne() + template = loader["hello_world.pt"] + from chameleon.zpt.template import PageTemplateFile + self.assertTrue(isinstance(template, PageTemplateFile)) + + +def test_suite(): + import sys + return unittest.findTestCases(sys.modules[__name__]) diff --git a/src/chameleon/tests/test_parser.py b/src/chameleon/tests/test_parser.py new file mode 100644 index 0000000..5161038 --- /dev/null +++ b/src/chameleon/tests/test_parser.py @@ -0,0 +1,103 @@ +from __future__ import with_statement + +import sys + +from unittest import TestCase + +from ..namespaces import XML_NS +from ..namespaces import XMLNS_NS +from ..namespaces import PY_NS + + +class ParserTest(TestCase): + def test_comment_double_hyphen_parsing(self): + from ..parser import match_double_hyphen + + self.assertFalse(match_double_hyphen.match('->')) + self.assertFalse(match_double_hyphen.match('-->')) + self.assertFalse(match_double_hyphen.match('--->')) + self.assertFalse(match_double_hyphen.match('---->')) + self.assertFalse(match_double_hyphen.match('- >')) + + self.assertTrue(match_double_hyphen.match('-- >')) + + def test_sample_files(self): + import os + import traceback + path = os.path.join(os.path.dirname(__file__), "inputs") + for filename in os.listdir(path): + if not filename.endswith('.html'): + continue + + with open(os.path.join(path, filename), 'rb') as f: + source = f.read() + + from ..utils import read_encoded + try: + want = read_encoded(source) + except UnicodeDecodeError: + exc = sys.exc_info()[1] + self.fail("%s - %s" % (exc, filename)) + + from ..tokenize import iter_xml + from ..parser import ElementParser + try: + tokens = iter_xml(want) + parser = ElementParser(tokens, { + 'xmlns': XMLNS_NS, + 'xml': XML_NS, + 'py': PY_NS, + }) + elements = tuple(parser) + except: + self.fail(traceback.format_exc()) + + output = [] + + def render(kind, args): + if kind == 'element': + # start tag + tag, end, children = args + output.append("%(prefix)s%(name)s" % tag) + + for attr in tag['attrs']: + output.append( + "%(space)s%(name)s%(eq)s%(quote)s%(value)s%(quote)s" % \ + attr + ) + + output.append("%(suffix)s" % tag) + + # children + for item in children: + render(*item) + + # end tag + output.append( + "%(prefix)s%(name)s%(space)s%(suffix)s" % end + ) + elif kind == 'text': + text = args[0] + output.append(text) + elif kind == 'start_tag': + node = args[0] + output.append( + "%(prefix)s%(name)s%(space)s%(suffix)s" % node + ) + else: + raise RuntimeError("Not implemented: %s." % kind) + + for kind, args in elements: + render(kind, args) + + got = "".join(output) + + from doctest import OutputChecker + checker = OutputChecker() + + if checker.check_output(want, got, 0) is False: + from doctest import Example + example = Example(f.name, want) + diff = checker.output_difference( + example, got, 0) + self.fail("(%s) - \n%s" % (f.name, diff)) diff --git a/src/chameleon/tests/test_sniffing.py b/src/chameleon/tests/test_sniffing.py new file mode 100644 index 0000000..203138c --- /dev/null +++ b/src/chameleon/tests/test_sniffing.py @@ -0,0 +1,124 @@ +from __future__ import with_statement + +import os +import unittest +import tempfile +import shutil + +from chameleon.utils import unicode_string +from chameleon.utils import encode_string + + +class TypeSniffingTestCase(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.mkdtemp(prefix='chameleon-tests') + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def _get_temporary_file(self): + filename = os.path.join(self.tempdir, 'template.py') + assert not os.path.exists(filename) + f = open(filename, 'w') + f.flush() + f.close() + return filename + + def get_template(self, text): + fn = self._get_temporary_file() + + with open(fn, 'wb') as tmpfile: + tmpfile.write(text) + + from chameleon.template import BaseTemplateFile + + class DummyTemplateFile(BaseTemplateFile): + def cook(self, body): + self.body = body + + template = DummyTemplateFile(fn) + template.cook_check() + return template + + def check_content_type(self, text, expected_type): + from chameleon.utils import read_bytes + content_type = read_bytes(text, 'ascii')[2] + self.assertEqual(content_type, expected_type) + + def test_xml_encoding(self): + from chameleon.utils import xml_prefixes + + document1 = unicode_string( + "<?xml version='1.0' encoding='ascii'?><doc/>" + ) + document2 = unicode_string( + "<?xml\tversion='1.0' encoding='ascii'?><doc/>" + ) + + for bom, encoding in xml_prefixes: + try: + "".encode(encoding) + except LookupError: + # System does not support this encoding + continue + + self.check_content_type(document1.encode(encoding), "text/xml") + self.check_content_type(document2.encode(encoding), "text/xml") + + HTML_PUBLIC_ID = "-//W3C//DTD HTML 4.01 Transitional//EN" + HTML_SYSTEM_ID = "http://www.w3.org/TR/html4/loose.dtd" + + # Couldn't find the code that handles this... yet. + # def test_sniffer_html_ascii(self): + # self.check_content_type( + # "<!DOCTYPE html [ SYSTEM '%s' ]><html></html>" + # % self.HTML_SYSTEM_ID, + # "text/html") + # self.check_content_type( + # "<html><head><title>sample document</title></head></html>", + # "text/html") + + # TODO: This reflects a case that simply isn't handled by the + # sniffer; there are many, but it gets it right more often than + # before. + def donttest_sniffer_xml_simple(self): + self.check_content_type("<doc><element/></doc>", "text/xml") + + def test_html_default_encoding(self): + body = encode_string( + '<html><head><title>' \ + '\xc3\x90\xc2\xa2\xc3\x90\xc2\xb5' \ + '\xc3\x91\xc2\x81\xc3\x91\xc2\x82' \ + '</title></head></html>') + + template = self.get_template(body) + self.assertEqual(template.body, body.decode('utf-8')) + + def test_html_encoding_by_meta(self): + body = encode_string( + '<html><head><title>' \ + '\xc3\x92\xc3\xa5\xc3\xb1\xc3\xb2' \ + '</title><meta http-equiv="Content-Type"' \ + ' content="text/html; charset=windows-1251"/>' \ + "</head></html>") + + template = self.get_template(body) + self.assertEqual(template.body, body.decode('windows-1251')) + + def test_xhtml(self): + body = encode_string( + '<html><head><title>' \ + '\xc3\x92\xc3\xa5\xc3\xb1\xc3\xb2' \ + '</title><meta http-equiv="Content-Type"' \ + ' content="text/html; charset=windows-1251"/>' \ + "</head></html>") + + template = self.get_template(body) + self.assertEqual(template.body, body.decode('windows-1251')) + + +def test_suite(): + return unittest.makeSuite(TypeSniffingTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") diff --git a/src/chameleon/tests/test_templates.py b/src/chameleon/tests/test_templates.py new file mode 100644 index 0000000..9283654 --- /dev/null +++ b/src/chameleon/tests/test_templates.py @@ -0,0 +1,795 @@ +# -*- coding: utf-8 -*- + +from __future__ import with_statement + +import re +import os +import sys +import shutil +import tempfile + +from functools import wraps +from functools import partial + +try: + from unittest2 import TestCase +except ImportError: + from unittest import TestCase + +try: + RecursionError +except NameError: + RecursionError = RuntimeError + +from chameleon.utils import byte_string +from chameleon.exc import RenderError + + +class Message(object): + def __str__(self): + return "message" + + +class ImportTestCase(TestCase): + def test_pagetemplates(self): + from chameleon import PageTemplate + from chameleon import PageTemplateFile + from chameleon import PageTemplateLoader + + def test_pagetexttemplates(self): + from chameleon import PageTextTemplate + from chameleon import PageTextTemplateFile + + +class TemplateFileTestCase(TestCase): + @property + def _class(self): + from chameleon.template import BaseTemplateFile + + class TestTemplateFile(BaseTemplateFile): + cook_count = 0 + + def cook(self, body): + self.cook_count += 1 + self._cooked = True + + return TestTemplateFile + + def setUp(self): + self.tempdir = tempfile.mkdtemp(prefix='chameleon-tests') + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def _get_temporary_file(self): + filename = os.path.join(self.tempdir, 'template.py') + assert not os.path.exists(filename) + f = open(filename, 'w') + f.flush() + f.close() + return filename + + def test_cook_check(self): + fn = self._get_temporary_file() + template = self._class(fn) + template.cook_check() + self.assertEqual(template.cook_count, 1) + + def test_auto_reload(self): + fn = self._get_temporary_file() + + # set time in past + os.utime(fn, (0, 0)) + + template = self._class(fn, auto_reload=True) + template.cook_check() + + # a second cook check makes no difference + template.cook_check() + self.assertEqual(template.cook_count, 1) + + # set current time on file + os.utime(fn, None) + + # file is reloaded + template.cook_check() + self.assertEqual(template.cook_count, 2) + + def test_relative_is_expanded_to_cwd(self): + template = self._class("___does_not_exist___") + try: + template.cook_check() + except IOError: + exc = sys.exc_info()[1] + self.assertEqual( + os.getcwd(), + os.path.dirname(exc.filename) + ) + else: + self.fail("Expected OSError.") + + +class RenderTestCase(TestCase): + root = os.path.dirname(__file__) + + def find_files(self, ext): + inputs = os.path.join(self.root, "inputs") + outputs = os.path.join(self.root, "outputs") + for filename in sorted(os.listdir(inputs)): + name, extension = os.path.splitext(filename) + if extension != ext: + continue + path = os.path.join(inputs, filename) + + # if there's no output file, treat document as static and + # expect intput equal to output + import glob + globbed = tuple(glob.iglob(os.path.join( + outputs, "%s*%s" % (name.split('-', 1)[0], ext)))) + + if not globbed: + self.fail("Missing output for: %s." % name) + + for output in globbed: + name, ext = os.path.splitext(output) + basename = os.path.basename(name) + if '-' in basename: + language = basename.split('-')[1] + else: + language = None + + yield path, output, language + + +class ZopePageTemplatesTest(RenderTestCase): + @property + def from_string(body): + from ..zpt.template import PageTemplate + return partial(PageTemplate, keep_source=True) + + @property + def from_file(body): + from ..zpt.template import PageTemplateFile + return partial(PageTemplateFile, keep_source=True) + + def template(body): + def decorator(func): + @wraps(func) + def wrapper(self): + template = self.from_string(body) + return func(self, template) + + return wrapper + return decorator + + def error(body): + def decorator(func): + @wraps(func) + def wrapper(self): + from chameleon.exc import TemplateError + try: + template = self.from_string(body) + except TemplateError: + exc = sys.exc_info()[1] + return func(self, body, exc) + else: + self.fail("Expected exception.") + + return wrapper + return decorator + + def test_syntax_error_in_strict_mode(self): + from chameleon.exc import ExpressionError + + self.assertRaises( + ExpressionError, + self.from_string, + """<tal:block replace='bad /// ' />""", + strict=True + ) + + def test_syntax_error_in_non_strict_mode(self): + from chameleon.exc import ExpressionError + + body = """<tal:block replace='bad /// ' />""" + template = self.from_string(body, strict=False) + + try: + template() + except ExpressionError: + exc = sys.exc_info()[1] + self.assertTrue(body[exc.offset:].startswith('bad ///')) + else: + self.fail("Expected exception") + + def test_exists_error_leak(self): + body = '''\ + <?xml version="1.0"?> + <root> + <one tal:condition="exists: var_does_not_exists" /> + <two tal:attributes="my_attr dict()['key-does-not-exist']" /> + </root>''' + template = self.from_string(body, strict=False) + try: + template() + except RenderError: + exc = sys.exc_info()[1] + self.assertNotIn('var_does_not_exists', str(exc)) + else: + self.fail("Expected exception") + + def test_sys_exc_info_is_clear_after_pipe(self): + body = """<div tal:content="y|nothing"></div><span tal:content="error()" />""" + template = self.from_string(body, strict=False) + + got = template.render(error=sys.exc_info) + self.assertTrue('<span>(None, None, None)</span>' in got) + + def test_render_macro_include_subtemplate_containing_error(self): + macro_inner = self.from_string('''<two tal:attributes="my_attr dict()['key-does-not-exist']" />''') + macro_wrap = self.from_string('''<one tal:content="macro_inner()" />''') + template = self.from_string( + ''' + <tal:defs define="translate string:"> + <span i18n:translate="">foo</span> + <metal:macro use-macro="macro" /> + </tal:defs> + ''') + try: + result = template(macro=macro_wrap, macro_inner=macro_inner) + except RenderError: + exc = sys.exc_info()[1] + self.assertTrue(isinstance(exc, KeyError)) + self.assertIn(''''key-does-not-exist' + + - Expression: "dict()['key-does-not-exist']" + - Filename: <string> + - Location: (line 1: col 29) + - Expression: "macro_inner()" + - Filename: <string> + - Location: (line 1: col 18) + - Expression: "macro" + - Filename: <string> + - Location: (line 4: col 38) +''', str(exc)) + else: + self.fail("Expected exception") + + def test_render_error_macro_include(self): + body = """<metal:block use-macro='"bad"' />""" + template = self.from_string(body, strict=False) + + try: + template() + except RenderError: + exc = sys.exc_info()[1] + self.assertTrue(isinstance(exc, AttributeError)) + self.assertIn('bad', str(exc)) + else: + self.fail("Expected exception") + + @error("""<tal:dummy attributes=\"dummy 'dummy'\" />""") + def test_attributes_on_tal_tag_fails(self, body, exc): + self.assertTrue(body[exc.offset:].startswith('dummy')) + + @error("""<tal:dummy i18n:attributes=\"foo, bar\" />""") + def test_i18n_attributes_with_non_identifiers(self, body, exc): + self.assertTrue(body[exc.offset:].startswith('foo,')) + + @error("""<tal:dummy repeat=\"key,value mydict.items()\">""") + def test_repeat_syntax_error_message(self, body, exc): + self.assertTrue(body[exc.offset:].startswith('key,value')) + + @error('''<tal:dummy><p i18n:translate="mymsgid"> + <span i18n:name="repeat"/><span i18n:name="repeat"/> + </p></tal:dummy>''') + def test_repeat_i18n_name_error(self, body, exc): + self.assertTrue(body[exc.offset:].startswith('repeat'), body[exc.offset:]) + + @error('''<tal:dummy> + <span i18n:name="not_in_translation"/> + </tal:dummy>''') + def test_i18n_name_not_in_translation_error(self, body, exc): + self.assertTrue(body[exc.offset:].startswith('not_in_translation')) + + def test_encoded(self): + filename = '074-encoded-template.pt' + with open(os.path.join(self.root, 'inputs', filename), 'rb') as f: + body = f.read() + + self.from_string(body) + + def test_utf8_encoded(self): + filename = '073-utf8-encoded.pt' + with open(os.path.join(self.root, 'inputs', filename), 'rb') as f: + body = f.read() + + self.from_string(body) + + def test_recursion_error(self): + template = self.from_string( + '<div metal:define-macro="main" ' + 'metal:use-macro="template.macros.main" />' + ) + self.assertRaises(RecursionError, template) + try: + template() + except RecursionError as exc: + self.assertFalse(isinstance(exc, RenderError)) + + def test_unicode_decode_error(self): + template = self.from_file( + os.path.join(self.root, 'inputs', 'greeting.pt') + ) + + string = native = "the artist formerly known as ƤŗÃƞĆě" + try: + string = string.decode('utf-8') + except AttributeError: + pass + + class name: + @staticmethod + def __html__(): + # This raises a decoding exception + string.encode('utf-8').decode('ascii') + + self.fail("Expected exception raised.") + + try: + template(name=name) + except UnicodeDecodeError: + exc = sys.exc_info()[1] + formatted = str(exc) + + # There's a marker under the expression that has the + # unicode decode error + self.assertTrue('^^^^^' in formatted) + self.assertTrue(native in formatted) + else: + self.fail("expected error") + + def test_custom_encoding_for_str_or_bytes_in_content(self): + string = '<div>ТеÑÑ‚${text}</div>' + try: + string = string.decode('utf-8') + except AttributeError: + pass + + template = self.from_string(string, encoding="windows-1251") + + text = 'ТеÑÑ‚' + + try: + text = text.decode('utf-8') + except AttributeError: + pass + + rendered = template(text=text.encode('windows-1251')) + + self.assertEqual( + rendered, + string.replace('${text}', text) + ) + + def test_custom_encoding_for_str_or_bytes_in_attributes(self): + string = '<img tal="ТеÑÑ‚${text}" />' + try: + string = string.decode('utf-8') + except AttributeError: + pass + + template = self.from_string(string, encoding="windows-1251") + + text = 'ТеÑÑ‚' + + try: + text = text.decode('utf-8') + except AttributeError: + pass + + rendered = template(text=text.encode('windows-1251')) + + self.assertEqual( + rendered, + string.replace('${text}', text) + ) + + def test_null_translate_function(self): + template = self.from_string('${test}', translate=None) + rendered = template(test=object()) + self.assertTrue('object' in rendered) + + def test_on_error_handler(self): + exc = [] + handler = exc.append + template = self.from_string( + '<tal:block on-error="string:error">${test}</tal:block>', + on_error_handler=handler + ) + rendered = template() + self.assertEqual(len(exc), 1) + self.assertEqual(exc[0].__class__.__name__, "NameError") + + def test_object_substitution_coerce_to_str(self): + template = self.from_string('${test}', translate=None) + + class dummy(object): + def __repr__(inst): + self.fail("call not expected") + + def __str__(inst): + return '<dummy>' + + rendered = template(test=dummy()) + self.assertEqual(rendered, '<dummy>') + + def test_repr(self): + template = self.from_file( + os.path.join(self.root, 'inputs', 'hello_world.pt') + ) + self.assertTrue(template.filename in repr(template)) + + def test_underscore_variable(self): + template = self.from_string( + "<div tal:define=\"_dummy 'foo'\">${_dummy}</div>" + ) + self.assertTrue(template(), "<div>foo</div>") + + def test_trim_attribute_space(self): + document = '''<div + class="document" + id="test" + tal:attributes="class string:${default} test" + />''' + + result1 = self.from_string( + document)() + + result2 = self.from_string( + document, trim_attribute_space=True)() + + self.assertEqual(result1.count(" "), 49) + self.assertEqual(result2.count(" "), 4) + self.assertTrue(" />" in result1) + self.assertTrue(" />" in result2) + + def test_exception(self): + from traceback import format_exception_only + + template = self.from_string( + "<div tal:define=\"dummy foo\">${dummy}</div>" + ) + try: + template() + except Exception as exc: + self.assertIn(RenderError, type(exc).__bases__) + exc = sys.exc_info()[1] + formatted = str(exc) + self.assertFalse('NameError:' in formatted) + self.assertTrue('foo' in formatted) + self.assertTrue('(line 1: col 23)' in formatted) + + formatted_exc = "\n".join(format_exception_only(type(exc), exc)) + self.assertTrue('NameError: foo' in formatted_exc) + else: + self.fail("expected error") + + def test_create_formatted_exception(self): + from chameleon.utils import create_formatted_exception + + exc = create_formatted_exception(NameError('foo'), NameError, str) + self.assertEqual(exc.args, ('foo', )) + + class MyNameError(NameError): + def __init__(self, boo): + NameError.__init__(self, boo) + self.bar = boo + + exc = create_formatted_exception(MyNameError('foo'), MyNameError, str) + self.assertEqual(exc.args, ('foo', )) + self.assertEqual(exc.bar, 'foo') + + def test_create_formatted_exception_no_subclass(self): + from chameleon.utils import create_formatted_exception + + class DifficultMetaClass(type): + def __init__(self, class_name, bases, namespace): + if not bases == (BaseException, ): + raise TypeError(bases) + + Difficult = DifficultMetaClass('Difficult', (BaseException, ), {'args': ()}) + + exc = create_formatted_exception(Difficult(), Difficult, str) + self.assertEqual(exc.args, ()) + + def test_error_handler_makes_safe_copy(self): + calls = [] + + class TestException(Exception): + def __init__(self, *args, **kwargs): + calls.append((args, kwargs)) + + def _render(stream, econtext, rcontext): + exc = TestException('foo', bar='baz') + rcontext['__error__'] = ('expression', 1, 42, 'test.pt', exc), + raise exc + + template = self.from_string("") + template._render = _render + try: + template() + except TestException: + self.assertEqual(calls, [(('foo', ), {'bar': 'baz'})]) + exc = sys.exc_info()[1] + formatted = str(exc) + self.assertTrue('TestException' in formatted) + self.assertTrue('"expression"' in formatted) + self.assertTrue('(line 1: col 42)' in formatted) + else: + self.fail("unexpected error") + + def test_double_underscore_variable(self): + from chameleon.exc import TranslationError + self.assertRaises( + TranslationError, self.from_string, + "<div tal:define=\"__dummy 'foo'\">${__dummy}</div>", + ) + + def test_compiler_internals_are_disallowed(self): + from chameleon.compiler import COMPILER_INTERNALS_OR_DISALLOWED + from chameleon.exc import TranslationError + + for name in COMPILER_INTERNALS_OR_DISALLOWED: + body = "<d tal:define=\"%s 'foo'\">${%s}</d>" % (name, name) + self.assertRaises(TranslationError, self.from_string, body) + + def test_simple_translate_mapping(self): + template = self.from_string( + '<div i18n:translate="">' + '<span i18n:name="name">foo</span>' + '</div>') + + self.assertEqual(template(), '<div><span>foo</span></div>') + + def test_translate_is_not_an_internal(self): + macro = self.from_string('<span i18n:translate="">bar</span>') + template = self.from_string( + ''' + <tal:defs define="translate string:"> + <span i18n:translate="">foo</span> + <metal:macro use-macro="macro" /> + </tal:defs> + ''') + + result = template(macro=macro) + self.assertTrue('foo' in result) + self.assertTrue('foo' in result) + + def test_literal_false(self): + template = self.from_string( + '<input type="input" tal:attributes="checked False" />' + '<input type="input" tal:attributes="checked True" />' + '<input type="input" tal:attributes="checked None" />' + '<input type="input" tal:attributes="checked default" />', + literal_false=True, + ) + + self.assertEqual( + template(), + '<input type="input" checked="False" />' + '<input type="input" checked="True" />' + '<input type="input" />' + '<input type="input" />', + template.source + ) + + def test_boolean_attributes(self): + template = self.from_string( + '<input type="input" tal:attributes="checked False" />' + '<input type="input" tal:attributes="checked True" />' + '<input type="input" tal:attributes="checked None" />' + '<input type="input" tal:attributes="checked \'\'" />' + '<input type="input" tal:attributes="checked default" />' + '<input type="input" checked="checked" tal:attributes="checked default" />', + boolean_attributes=set(['checked']) + ) + + self.assertEqual( + template(), + '<input type="input" />' + '<input type="input" checked="checked" />' + '<input type="input" />' + '<input type="input" />' + '<input type="input" />' + '<input type="input" checked="checked" />', + template.source + ) + + def test_default_debug_flag(self): + from chameleon.config import DEBUG_MODE + template = self.from_file( + os.path.join(self.root, 'inputs', 'hello_world.pt'), + ) + self.assertEqual(template.debug, DEBUG_MODE) + self.assertTrue('debug' not in template.__dict__) + + def test_debug_flag_on_string(self): + from chameleon.loader import ModuleLoader + + with open(os.path.join(self.root, 'inputs', 'hello_world.pt')) as f: + source = f.read() + + template = self.from_string(source, debug=True) + + self.assertTrue(template.debug) + self.assertTrue(isinstance(template.loader, ModuleLoader)) + + def test_debug_flag_on_file(self): + from chameleon.loader import ModuleLoader + template = self.from_file( + os.path.join(self.root, 'inputs', 'hello_world.pt'), + debug=True, + ) + self.assertTrue(template.debug) + self.assertTrue(isinstance(template.loader, ModuleLoader)) + + def test_tag_mismatch(self): + from chameleon.exc import ParseError + + try: + self.from_string(""" + <div metal:use-macro="layout"> + <div metal:fill-slot="name"></dav> + </div> + """) + except ParseError: + exc = sys.exc_info()[1] + self.assertTrue("</dav>" in str(exc)) + else: + self.fail("Expected error.") + + +class ZopeTemplatesTestSuite(RenderTestCase): + def setUp(self): + self.temp_path = temp_path = tempfile.mkdtemp() + + @self.addCleanup + def cleanup(path=temp_path): + shutil.rmtree(path) + + def test_pt_files(self): + from ..zpt.template import PageTemplateFile + + class Literal(object): + def __init__(self, s): + self.s = s + + def __html__(self): + return self.s + + def __str__(self): + raise RuntimeError( + "%r is a literal." % self.s) + + from chameleon.loader import TemplateLoader + loader = TemplateLoader(os.path.join(self.root, "inputs")) + + self.execute( + ".pt", PageTemplateFile, + literal=Literal("<div>Hello world!</div>"), + content="<div>Hello world!</div>", + message=Message(), + load=loader.bind(PageTemplateFile), + ) + + def test_txt_files(self): + from ..zpt.template import PageTextTemplateFile + self.execute(".txt", PageTextTemplateFile) + + def execute(self, ext, factory, **kwargs): + def translate(msgid, domain=None, mapping=None, context=None, + target_language=None, default=None): + if default is None: + default = str(msgid) + + if isinstance(msgid, Message): + default = "Message" + + if mapping: + default = re.sub(r'\${([a-z_]+)}', r'%(\1)s', default) % \ + mapping + + if target_language is None: + return default + + if domain is None: + with_domain = "" + else: + with_domain = " with domain '%s'" % domain + + if context is None: + with_context = "" + else: + with_context = ", context '%s'" % context + + stripped = default.rstrip('\n ') + return "%s ('%s' translation into '%s'%s%s)%s" % ( + stripped, msgid, target_language, with_domain, with_context, + default[len(stripped):] + ) + + for input_path, output_path, language in self.find_files(ext): + # Make friendly title so we can locate the generated + # source when debugging + self.shortDescription = lambda: input_path + + # When input path contains the string 'implicit-i18n', we + # enable "implicit translation". + implicit_i18n = 'implicit-i18n' in input_path + implicit_i18n_attrs = ("alt", "title") if implicit_i18n else () + + enable_data_attributes = 'data-attributes' in input_path + + template = factory( + input_path, + keep_source=True, + strict=False, + implicit_i18n_translate=implicit_i18n, + implicit_i18n_attributes=implicit_i18n_attrs, + enable_data_attributes=enable_data_attributes, + ) + + params = kwargs.copy() + params.update({ + 'translate': translate, + 'target_language': language, + }) + + template.cook_check() + + try: + got = template.render(**params) + except: + import traceback + e = traceback.format_exc() + self.fail("%s\n\n Example source:\n\n%s" % (e, "\n".join( + ["%#03.d%s" % (lineno + 1, line and " " + line or "") + for (lineno, line) in + enumerate(template.source.split( + '\n'))]))) + + if isinstance(got, byte_string): + got = got.decode('utf-8') + + from doctest import OutputChecker + checker = OutputChecker() + + if not os.path.exists(output_path): + output = template.body + else: + with open(output_path, 'rb') as f: + output = f.read() + + from chameleon.utils import read_xml_encoding + from chameleon.utils import detect_encoding + + if template.content_type == 'text/xml': + encoding = read_xml_encoding(output) or \ + template.default_encoding + else: + content_type, encoding = detect_encoding( + output, template.default_encoding) + + # Newline normalization across platforms + want = '\n'.join(output.decode(encoding).splitlines()) + got = '\n'.join(got.splitlines()) + + if checker.check_output(want, got, 0) is False: + from doctest import Example + example = Example(input_path, want) + diff = checker.output_difference( + example, got, 0) + self.fail("(%s) - \n%s\n\nCode:\n%s" % ( + input_path, diff.rstrip('\n'), + template.source.encode('utf-8'))) diff --git a/src/chameleon/tests/test_tokenizer.py b/src/chameleon/tests/test_tokenizer.py new file mode 100644 index 0000000..f00181e --- /dev/null +++ b/src/chameleon/tests/test_tokenizer.py @@ -0,0 +1,47 @@ +import sys + +from unittest import TestCase + + +class TokenizerTest(TestCase): + def test_sample_files(self): + import os + import traceback + path = os.path.join(os.path.dirname(__file__), "inputs") + for filename in os.listdir(path): + if not filename.endswith('.xml'): + continue + f = open(os.path.join(path, filename), 'rb') + source = f.read() + f.close() + + from ..utils import read_encoded + try: + want = read_encoded(source) + except UnicodeDecodeError: + exc = sys.exc_info()[1] + self.fail("%s - %s" % (exc, filename)) + + from ..tokenize import iter_xml + try: + tokens = iter_xml(want) + got = "".join(tokens) + except: + self.fail(traceback.format_exc()) + + from doctest import OutputChecker + checker = OutputChecker() + + if checker.check_output(want, got, 0) is False: + from doctest import Example + example = Example(f.name, want) + diff = checker.output_difference( + example, got, 0) + self.fail("(%s) - \n%s" % (f.name, diff)) + + def test_token(self): + from chameleon.tokenize import Token + token = Token("abc", 1) + + self.assertTrue(isinstance(token[1:], Token)) + self.assertEqual(token[1:].pos, 2) diff --git a/src/chameleon/tokenize.py b/src/chameleon/tokenize.py new file mode 100644 index 0000000..bec3c7b --- /dev/null +++ b/src/chameleon/tokenize.py @@ -0,0 +1,144 @@ +# http://code.activestate.com/recipes/65125-xml-lexing-shallow-parsing/ +# by Paul Prescod +# licensed under the PSF License +# +# modified to capture all non-overlapping parts of tokens + +import re + +try: + str = unicode +except NameError: + pass + +class recollector: + def __init__(self): + self.res = {} + + def add(self, name, reg ): + re.compile(reg) # check that it is valid + self.res[name] = reg % self.res + +collector = recollector() +a = collector.add + +a("TextSE", "[^<]+") +a("UntilHyphen", "[^-]*-") +a("Until2Hyphens", "%(UntilHyphen)s(?:[^-]%(UntilHyphen)s)*-") +a("CommentCE", "%(Until2Hyphens)s>?") +a("UntilRSBs", "[^\\]]*](?:[^\\]]+])*]+") +a("CDATA_CE", "%(UntilRSBs)s(?:[^\\]>]%(UntilRSBs)s)*>" ) +a("S", "[ \\n\\t\\r]+") +a("Simple", "[^\"'>/]+") +a("NameStrt", "[A-Za-z_:@]|[^\\x00-\\x7F]") +a("NameChar", "[A-Za-z0-9_:.-]|[^\\x00-\\x7F]") +a("Name", "(?:%(NameStrt)s)(?:%(NameChar)s)*") +a("QuoteSE", "\"[^\"]*\"|'[^']*'") +a("DT_IdentSE" , "%(S)s%(Name)s(?:%(S)s(?:%(Name)s|%(QuoteSE)s))*" ) +a("MarkupDeclCE" , "(?:[^\\]\"'><]+|%(QuoteSE)s)*>" ) +a("S1", "[\\n\\r\\t ]") +a("UntilQMs", "[^?]*\\?+") +a("PI_Tail" , "\\?>|%(S1)s%(UntilQMs)s(?:[^>?]%(UntilQMs)s)*>" ) +a("DT_ItemSE", + "<(?:!(?:--%(Until2Hyphens)s>|[^-]%(MarkupDeclCE)s)|" + "\\?%(Name)s(?:%(PI_Tail)s))|%%%(Name)s;|%(S)s" +) +a("DocTypeCE" , +"%(DT_IdentSE)s(?:%(S)s)?(?:\\[(?:%(DT_ItemSE)s)*](?:%(S)s)?)?>?" ) +a("DeclCE", + "--(?:%(CommentCE)s)?|\\[CDATA\\[(?:%(CDATA_CE)s)?|" + "DOCTYPE(?:%(DocTypeCE)s)?") +a("PI_CE", "%(Name)s(?:%(PI_Tail)s)?") +a("EndTagCE", "%(Name)s(?:%(S)s)?>?") +a("AttValSE", r"\"[^\"]*\"|'[^']*'|[^\s=<>`]+") +a("ElemTagCE", + "(%(Name)s)(?:(%(S)s)(%(Name)s)(((?:%(S)s)?=(?:%(S)s)?)" + "(?:%(AttValSE)s|%(Simple)s)|(?!(?:%(S)s)?=)))*(?:%(S)s)?(/?>)?") +a("MarkupSPE", + "<(?:!(?:%(DeclCE)s)?|" + "\\?(?:%(PI_CE)s)?|/(?:%(EndTagCE)s)?|(?:%(ElemTagCE)s)?)") +a("XML_SPE", "%(TextSE)s|%(MarkupSPE)s") +a("XML_MARKUP_ONLY_SPE", "%(MarkupSPE)s") +a("ElemTagSPE", "<|%(Name)s") + +re_xml_spe = re.compile(collector.res['XML_SPE']) +re_markup_only_spe = re.compile(collector.res['XML_MARKUP_ONLY_SPE']) + + +def iter_xml(body, filename=None): + for match in re_xml_spe.finditer(body): + string = match.group() + pos = match.start() + yield Token(string, pos, body, filename) + + +def iter_text(body, filename=None): + yield Token(body, 0, body, filename) + + +class Token(str): + __slots__ = "pos", "source", "filename" + + def __new__(cls, string, pos=0, source=None, filename=None): + inst = str.__new__(cls, string) + inst.pos = pos + inst.source = source + inst.filename = filename or "" + return inst + + def __getslice__(self, i, j): + slice = str.__getslice__(self, i, j) + return Token(slice, self.pos + i, self.source, self.filename) + + def __getitem__(self, index): + s = str.__getitem__(self, index) + if isinstance(index, slice): + return Token( + s, self.pos + (index.start or 0), self.source, self.filename) + return s + + def __add__(self, other): + if other is None: + return self + + return Token( + str.__add__(self, other), self.pos, self.source, self.filename) + + def __eq__(self, other): + return str.__eq__(self, other) + + def __hash__(self): + return str.__hash__(self) + + def replace(self, *args): + s = str.replace(self, *args) + return Token(s, self.pos, self.source, self.filename) + + def split(self, *args): + l = str.split(self, *args) + pos = self.pos + for i, s in enumerate(l): + l[i] = Token(s, pos, self.source, self.filename) + pos += len(s) + return l + + def strip(self, *args): + return self.lstrip(*args).rstrip(*args) + + def lstrip(self, *args): + s = str.lstrip(self, *args) + return Token( + s, self.pos + len(self) - len(s), self.source, self.filename) + + def rstrip(self, *args): + s = str.rstrip(self, *args) + return Token(s, self.pos, self.source, self.filename) + + @property + def location(self): + if self.source is None: + return 0, self.pos + + body = self.source[:self.pos] + line = body.count('\n') + return line + 1, self.pos - body.rfind('\n', 0) - 1 diff --git a/src/chameleon/utils.py b/src/chameleon/utils.py new file mode 100644 index 0000000..df921ae --- /dev/null +++ b/src/chameleon/utils.py @@ -0,0 +1,438 @@ +import os +import re +import sys +import codecs +import logging + +from copy import copy + +version = sys.version_info[:3] + +try: + import ast as _ast +except ImportError: + from chameleon import ast25 as _ast + + +class ASTProxy(object): + aliases = { + # Python 3.3 + 'TryExcept': 'Try', + 'TryFinally': 'Try', + } + + def __getattr__(self, name): + if name.startswith('__'): + raise AttributeError(name) + return _ast.__dict__.get(name) or getattr(_ast, self.aliases[name]) + + +ast = ASTProxy() + +log = logging.getLogger('chameleon.utils') + +# Python 2 +if version < (3, 0, 0): + import htmlentitydefs + import __builtin__ as builtins + + from .py25 import raise_with_traceback + + chr = unichr + native_string = str + decode_string = unicode + encode_string = str + unicode_string = unicode + string_type = basestring + byte_string = str + + def safe_native(s, encoding='utf-8'): + if not isinstance(s, unicode): + s = decode_string(s, encoding, 'replace') + + return s.encode(encoding) + +# Python 3 +else: + from html import entities as htmlentitydefs + import builtins + + byte_string = bytes + string_type = str + native_string = str + decode_string = bytes.decode + encode_string = lambda s: bytes(s, 'utf-8') + unicode_string = str + + def safe_native(s, encoding='utf-8'): + if not isinstance(s, str): + s = decode_string(s, encoding, 'replace') + + return s + + def raise_with_traceback(exc, tb): + exc.__traceback__ = tb + raise exc + +def text_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``byte_string``, return + ``s.decode(encoding, errors)``, otherwise return ``s``""" + if isinstance(s, byte_string): + return s.decode(encoding, errors) + return s + +entity_re = re.compile(r'&(#?)(x?)(\d{1,5}|\w{1,8});') + +module_cache = {} + +xml_prefixes = ( + (codecs.BOM_UTF8, 'utf-8-sig'), + (codecs.BOM_UTF16_BE, 'utf-16-be'), + (codecs.BOM_UTF16_LE, 'utf-16-le'), + (codecs.BOM_UTF16, 'utf-16'), + (codecs.BOM_UTF32_BE, 'utf-32-be'), + (codecs.BOM_UTF32_LE, 'utf-32-le'), + (codecs.BOM_UTF32, 'utf-32'), + ) + + +def _has_encoding(encoding): + try: + "".encode(encoding) + except LookupError: + return False + else: + return True + + +# Precomputed prefix table +_xml_prefixes = tuple( + (bom, str('<?xml').encode(encoding), encoding) + for bom, encoding in reversed(xml_prefixes) + if _has_encoding(encoding) + ) + +_xml_decl = encode_string("<?xml") + +RE_META = re.compile( + r'\s*<meta\s+http-equiv=["\']?Content-Type["\']?' + r'\s+content=["\']?([^;]+);\s*charset=([^"\']+)["\']?\s*/?\s*>\s*', + re.IGNORECASE + ) + +RE_ENCODING = re.compile( + r'encoding\s*=\s*(?:"|\')(?P<encoding>[\w\-]+)(?:"|\')'.encode('ascii'), + re.IGNORECASE + ) + + +def read_encoded(data): + return read_bytes(data, "utf-8")[0] + + +def read_bytes(body, default_encoding): + for bom, prefix, encoding in _xml_prefixes: + if body.startswith(bom): + document = body.decode(encoding) + return document, encoding, \ + "text/xml" if document.startswith("<?xml") else None + + if prefix != encode_string('<?xml') and body.startswith(prefix): + return body.decode(encoding), encoding, "text/xml" + + if body.startswith(_xml_decl): + content_type = "text/xml" + + encoding = read_xml_encoding(body) or default_encoding + else: + content_type, encoding = detect_encoding(body, default_encoding) + + return body.decode(encoding), encoding, content_type + + +def detect_encoding(body, default_encoding): + if not isinstance(body, str): + body = body.decode('ascii', 'ignore') + + match = RE_META.search(body) + if match is not None: + return match.groups() + + return None, default_encoding + + +def read_xml_encoding(body): + if body.startswith('<?xml'.encode('ascii')): + match = RE_ENCODING.search(body) + if match is not None: + return match.group('encoding').decode('ascii') + + +def mangle(filename): + """Mangles template filename into top-level Python module name. + + >>> mangle('hello_world.pt') + 'hello_world' + + >>> mangle('foo.bar.baz.pt') + 'foo_bar_baz' + + >>> mangle('foo-bar-baz.pt') + 'foo_bar_baz' + + """ + + base, ext = os.path.splitext(filename) + return base.replace('.', '_').replace('-', '_') + + +def char2entity(c): + cp = ord(c) + name = htmlentitydefs.codepoint2name.get(cp) + return '&%s;' % name if name is not None else '&#%d;' % cp + + +def substitute_entity(match, n2cp=htmlentitydefs.name2codepoint): + ent = match.group(3) + + if match.group(1) == "#": + if match.group(2) == '': + return chr(int(ent)) + elif match.group(2) == 'x': + return chr(int('0x' + ent, 16)) + else: + cp = n2cp.get(ent) + + if cp: + return chr(cp) + else: + return match.group() + + +def create_formatted_exception(exc, cls, formatter, base=Exception): + try: + try: + new = type(cls.__name__, (cls, base), { + '__str__': formatter, + '_original__str__': exc.__str__, + '__new__': BaseException.__new__, + '__module__': cls.__module__, + }) + except TypeError: + new = cls + + try: + inst = BaseException.__new__(new) + except TypeError: + inst = cls.__new__(new) + + BaseException.__init__(inst, *exc.args) + inst.__dict__ = exc.__dict__ + + return inst + except ValueError: + name = type(exc).__name__ + log.warn("Unable to copy exception of type '%s'." % name) + raise TypeError(exc) + + +def unescape(string): + for name in ('lt', 'gt', 'quot'): + cp = htmlentitydefs.name2codepoint[name] + string = string.replace('&%s;' % name, chr(cp)) + + return string + + +_concat = unicode_string("").join + + +def join(stream): + """Concatenate stream. + + >>> print(join(('Hello', ' ', 'world'))) + Hello world + + >>> join(('Hello', 0)) + Traceback (most recent call last): + ... + TypeError: ... expected ... + + """ + + try: + return _concat(stream) + except: + # Loop through stream and coerce each element into unicode; + # this should raise an exception + for element in stream: + unicode_string(element) + + # In case it didn't, re-raise the original exception + raise + + +def decode_htmlentities(string): + """ + >>> native_string(decode_htmlentities('&amp;')) + '&' + + """ + + decoded = entity_re.subn(substitute_entity, string)[0] + + # preserve input token data + return string.replace(string, decoded) + + +# Taken from zope.dottedname +def _resolve_dotted(name, module=None): + name = name.split('.') + if not name[0]: + if module is None: + raise ValueError("relative name without base module") + module = module.split('.') + name.pop(0) + while not name[0]: + module.pop() + name.pop(0) + name = module + name + + used = name.pop(0) + found = __import__(used) + for n in name: + used += '.' + n + try: + found = getattr(found, n) + except AttributeError: + __import__(used) + found = getattr(found, n) + + return found + + +def resolve_dotted(dotted): + if not dotted in module_cache: + resolved = _resolve_dotted(dotted) + module_cache[dotted] = resolved + return module_cache[dotted] + + +def limit_string(s, max_length=53): + if len(s) > max_length: + return s[:max_length - 3] + '...' + + return s + + +def format_kwargs(kwargs): + items = [] + for name, value in kwargs.items(): + if isinstance(value, string_type): + short = limit_string(value) + items.append((name, short.replace('\n', '\\n'))) + elif isinstance(value, (int, float)): + items.append((name, value)) + elif isinstance(value, dict): + items.append((name, '{...} (%d)' % len(value))) + else: + items.append((name, + "<%s %s at %s>" % ( + type(value).__name__, + getattr(value, '__name__', "-"), + hex(abs(id(value)))))) + + return ["%s: %s" % item for item in items] + + +class callablestr(str): + __slots__ = () + + def __call__(self): + return self + + +class callableint(int): + __slots__ = () + + def __call__(self): + return self + + +class descriptorstr(object): + __slots__ = "function", "__name__" + + def __init__(self, function): + self.function = function + self.__name__ = function.__name__ + + def __get__(self, context, cls): + return callablestr(self.function(context)) + + +class descriptorint(object): + __slots__ = "function", "__name__" + + def __init__(self, function): + self.function = function + self.__name__ = function.__name__ + + def __get__(self, context, cls): + return callableint(self.function(context)) + + +class DebuggingOutputStream(list): + def append(self, value): + if not isinstance(value, string_type): + raise TypeError(value) + + unicode_string(value) + list.append(self, value) + + +class Scope(dict): + set_local = setLocal = dict.__setitem__ + + __slots__ = "set_global", + + def __new__(cls, *args): + inst = dict.__new__(cls, *args) + inst.set_global = inst.__setitem__ + return inst + + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + raise NameError(key) + + @property + def vars(self): + return self + + def copy(self): + inst = Scope(self) + inst.set_global = self.set_global + return inst + + +class ListDictProxy(object): + def __init__(self, l): + self._l = l + + def get(self, key): + return self._l[-1].get(key) + + +class Markup(unicode_string): + """Wraps a string to always render as structure. + + >>> Markup('<br />') + s'<br />' + """ + + def __html__(self): + return unicode_string(self) + + def __repr__(self): + return "s'%s'" % self diff --git a/src/chameleon/zpt/__init__.py b/src/chameleon/zpt/__init__.py new file mode 100644 index 0000000..792d600 --- /dev/null +++ b/src/chameleon/zpt/__init__.py @@ -0,0 +1 @@ +# diff --git a/src/chameleon/zpt/loader.py b/src/chameleon/zpt/loader.py new file mode 100644 index 0000000..ea0e0fd --- /dev/null +++ b/src/chameleon/zpt/loader.py @@ -0,0 +1,30 @@ +from chameleon.loader import TemplateLoader as BaseLoader +from chameleon.zpt import template + + +class TemplateLoader(BaseLoader): + formats = { + "xml": template.PageTemplateFile, + "text": template.PageTextTemplateFile, + } + + default_format = "xml" + + def __init__(self, *args, **kwargs): + formats = kwargs.pop('formats', None) + if formats is not None: + self.formats = formats + + super(TemplateLoader, self).__init__(*args, **kwargs) + + def load(self, filename, format=None): + """Load and return a template file. + + The format parameter determines will parse the file. Valid + options are `xml` and `text`. + """ + + cls = self.formats[format or self.default_format] + return super(TemplateLoader, self).load(filename, cls) + + __getitem__ = load diff --git a/src/chameleon/zpt/program.py b/src/chameleon/zpt/program.py new file mode 100644 index 0000000..5b8491e --- /dev/null +++ b/src/chameleon/zpt/program.py @@ -0,0 +1,876 @@ +import re + +try: + import ast +except ImportError: + from chameleon import ast25 as ast + +try: + str = unicode +except NameError: + long = int + +from functools import partial +from copy import copy + +from ..program import ElementProgram + +from ..namespaces import XML_NS +from ..namespaces import XMLNS_NS +from ..namespaces import I18N_NS as I18N +from ..namespaces import TAL_NS as TAL +from ..namespaces import METAL_NS as METAL +from ..namespaces import META_NS as META + +from ..astutil import Static +from ..astutil import parse +from ..astutil import marker + +from .. import tal +from .. import metal +from .. import i18n +from .. import nodes + +from ..exc import LanguageError +from ..exc import ParseError +from ..exc import CompilationError + +from ..utils import decode_htmlentities + +try: + str = unicode +except NameError: + long = int + + +missing = object() + +re_trim = re.compile(r'($\s+|\s+^)', re.MULTILINE) + +EMPTY_DICT = Static(ast.Dict(keys=[], values=[])) + + +def skip(node): + return node + + +def wrap(node, *wrappers): + for wrapper in reversed(wrappers): + node = wrapper(node) + return node + + +def validate_attributes(attributes, namespace, whitelist): + for ns, name in attributes: + if ns == namespace and name not in whitelist: + raise CompilationError( + "Bad attribute for namespace '%s'" % ns, name + ) + + +def convert_data_attributes(ns_attrs, attrs, namespaces): + d = 0 + for i, attr in list(enumerate(attrs)): + name = attr['name'] + if name.startswith('data-'): + name = name[5:] + if '-' not in name: + continue + prefix, name = name.split('-', 1) + ns_attrs[namespaces[prefix], name] = attr['value'] + attrs.pop(i - d) + d += 1 + + +class MacroProgram(ElementProgram): + """Visitor class that generates a program for the ZPT language.""" + + DEFAULT_NAMESPACES = { + 'xmlns': XMLNS_NS, + 'xml': XML_NS, + 'tal': TAL, + 'metal': METAL, + 'i18n': I18N, + 'meta': META, + } + + DROP_NS = TAL, METAL, I18N, META + + VARIABLE_BLACKLIST = "default", "repeat", "nothing", \ + "convert", "decode", "translate" + + _interpolation_enabled = True + _whitespace = "\n" + _last = "" + + # Macro name (always trivial for a macro program) + name = None + + # This default marker value has the semantics that if an + # expression evaluates to that value, the expression default value + # is returned. For an attribute, if there is no default, this + # means that the attribute is dropped. + default_marker = None + + # Escape mode (true value means XML-escape) + escape = True + + # Attributes which should have boolean behavior (on true, the + # value takes the attribute name, on false, the attribute is + # dropped) + boolean_attributes = set() + + # If provided, this should be a set of attributes for implicit + # translation. Any attribute whose name is included in the set + # will be translated even without explicit markup. Note that all + # values should be lowercase strings. + implicit_i18n_attributes = set() + + # If set, text will be translated even without explicit markup. + implicit_i18n_translate = False + + # If set, additional attribute whitespace will be stripped. + trim_attribute_space = False + + # If set, data attributes can be used instead of namespace + # attributes, e.g. "data-tal-content" instead of "tal:content". + enable_data_attributes = False + + # If set, XML namespaces are restricted to the list of those + # defined and used by the page template language. + restricted_namespace = True + + def __init__(self, *args, **kwargs): + # Internal array for switch statements + self._switches = [] + + # Internal array for current use macro level + self._use_macro = [] + + # Internal array for current interpolation status + self._interpolation = [True] + + # Internal dictionary of macro definitions + self._macros = {} + + # Apply default values from **kwargs to self + self._pop_defaults( + kwargs, + 'boolean_attributes', + 'default_marker', + 'escape', + 'implicit_i18n_translate', + 'implicit_i18n_attributes', + 'trim_attribute_space', + 'enable_data_attributes', + 'restricted_namespace', + ) + + super(MacroProgram, self).__init__(*args, **kwargs) + + @property + def macros(self): + macros = list(self._macros.items()) + macros.append((None, nodes.Sequence(self.body))) + + return tuple( + nodes.Macro(name, [nodes.Context(node)]) + for name, node in macros + ) + + def visit_default(self, node): + return nodes.Text(node) + + def visit_element(self, start, end, children): + ns = start['ns_attrs'] + attrs = start['attrs'] + + if self.enable_data_attributes: + attrs = list(attrs) + convert_data_attributes(ns, attrs, start['ns_map']) + + for (prefix, attr), encoded in tuple(ns.items()): + if prefix == TAL or prefix == METAL: + ns[prefix, attr] = decode_htmlentities(encoded) + + # Validate namespace attributes + validate_attributes(ns, TAL, tal.WHITELIST) + validate_attributes(ns, METAL, metal.WHITELIST) + validate_attributes(ns, I18N, i18n.WHITELIST) + + # Check attributes for language errors + self._check_attributes(start['namespace'], ns) + + # Remember whitespace for item repetition + if self._last is not None: + self._whitespace = "\n" + " " * len(self._last.rsplit('\n', 1)[-1]) + + # Set element-local whitespace + whitespace = self._whitespace + + # Set up switch + try: + clause = ns[TAL, 'switch'] + except KeyError: + switch = None + else: + value = nodes.Value(clause) + switch = value, nodes.Copy(value) + + self._switches.append(switch) + + body = [] + + # Include macro + use_macro = ns.get((METAL, 'use-macro')) + extend_macro = ns.get((METAL, 'extend-macro')) + if use_macro or extend_macro: + omit = True + slots = [] + self._use_macro.append(slots) + + if use_macro: + inner = nodes.UseExternalMacro( + nodes.Value(use_macro), slots, False + ) + macro_name = use_macro + else: + inner = nodes.UseExternalMacro( + nodes.Value(extend_macro), slots, True + ) + macro_name = extend_macro + + # While the macro executes, it should have access to the name it was + # called with as 'macroname'. Splitting on / mirrors zope.tal and is a + # concession to the path expression syntax. + macro_name = macro_name.rsplit('/', 1)[-1] + inner = nodes.Define( + [nodes.Assignment(["macroname"], Static(ast.Str(macro_name)), True)], + inner, + ) + # -or- include tag + else: + content = nodes.Sequence(body) + + # tal:content + try: + clause = ns[TAL, 'content'] + except KeyError: + pass + else: + key, value = tal.parse_substitution(clause) + translate = ns.get((I18N, 'translate')) == '' + content = self._make_content_node( + value, content, key, translate, + ) + + if end is None: + # Make sure start-tag has opening suffix. + start['suffix'] = ">" + + # Explicitly set end-tag. + end = { + 'prefix': '</', + 'name': start['name'], + 'space': '', + 'suffix': '>' + } + + # i18n:translate + try: + clause = ns[I18N, 'translate'] + except KeyError: + pass + else: + dynamic = ns.get((TAL, 'content')) or ns.get((TAL, 'replace')) + + if not dynamic: + content = nodes.Translate(clause, content) + + # tal:attributes + try: + clause = ns[TAL, 'attributes'] + except KeyError: + TAL_ATTRIBUTES = [] + else: + TAL_ATTRIBUTES = tal.parse_attributes(clause) + + # i18n:attributes + try: + clause = ns[I18N, 'attributes'] + except KeyError: + I18N_ATTRIBUTES = {} + else: + I18N_ATTRIBUTES = i18n.parse_attributes(clause) + + # Prepare attributes from TAL language + prepared = tal.prepare_attributes( + attrs, TAL_ATTRIBUTES, + I18N_ATTRIBUTES, ns, self.DROP_NS + ) + + # Create attribute nodes + STATIC_ATTRIBUTES = self._create_static_attributes(prepared) + ATTRIBUTES = self._create_attributes_nodes( + prepared, I18N_ATTRIBUTES, STATIC_ATTRIBUTES, + ) + + # Start- and end nodes + start_tag = nodes.Start( + start['name'], + self._maybe_trim(start['prefix']), + self._maybe_trim(start['suffix']), + ATTRIBUTES + ) + + end_tag = nodes.End( + end['name'], + end['space'], + self._maybe_trim(end['prefix']), + self._maybe_trim(end['suffix']), + ) if end is not None else None + + # tal:omit-tag + try: + clause = ns[TAL, 'omit-tag'] + except KeyError: + omit = False + else: + clause = clause.strip() + + if clause == "": + omit = True + else: + expression = nodes.Negate(nodes.Value(clause)) + omit = expression + + # Wrap start- and end-tags in condition + start_tag = nodes.Condition(expression, start_tag) + + if end_tag is not None: + end_tag = nodes.Condition(expression, end_tag) + + if omit is True or start['namespace'] in self.DROP_NS: + inner = content + else: + inner = nodes.Element( + start_tag, + end_tag, + content, + ) + + # Assign static attributes dictionary to "attrs" value + inner = nodes.Define( + [nodes.Alias(["attrs"], STATIC_ATTRIBUTES or EMPTY_DICT)], + inner, + ) + + if omit is not False: + inner = nodes.Cache([omit], inner) + + # tal:replace + try: + clause = ns[TAL, 'replace'] + except KeyError: + pass + else: + key, value = tal.parse_substitution(clause) + translate = ns.get((I18N, 'translate')) == '' + inner = self._make_content_node( + value, inner, key, translate + ) + + # metal:define-slot + try: + clause = ns[METAL, 'define-slot'] + except KeyError: + DEFINE_SLOT = skip + else: + DEFINE_SLOT = partial(nodes.DefineSlot, clause) + + # tal:define + try: + clause = ns[TAL, 'define'] + except KeyError: + defines = [] + else: + defines = tal.parse_defines(clause) + + if defines is None: + raise ParseError("Invalid define syntax.", clause) + + # i18n:target + try: + target_language = ns[I18N, 'target'] + except KeyError: + pass + else: + # The name "default" is an alias for the target language + # variable. We simply replace it. + target_language = target_language.replace( + 'default', 'target_language' + ) + + defines.append( + ('local', ("target_language", ), target_language) + ) + + if defines: + DEFINE = partial( + nodes.Define, + [nodes.Assignment( + names, nodes.Value(expr), context == "local") + for (context, names, expr) in defines], + ) + else: + DEFINE = skip + + # tal:case + try: + clause = ns[TAL, 'case'] + except KeyError: + CASE = skip + else: + value = nodes.Value(clause) + for switch in reversed(self._switches): + if switch is not None: + break + else: + raise LanguageError( + "Must define switch on a parent element.", clause + ) + + CASE = lambda node: nodes.Define( + [nodes.Alias(["default"], switch[1], False)], + nodes.Condition( + nodes.Equality(switch[0], value), + nodes.Cancel([switch[0]], node), + )) + + # tal:repeat + try: + clause = ns[TAL, 'repeat'] + except KeyError: + REPEAT = skip + else: + defines = tal.parse_defines(clause) + assert len(defines) == 1 + context, names, expr = defines[0] + + expression = nodes.Value(expr) + + if start['namespace'] == TAL: + self._last = None + self._whitespace = whitespace.lstrip('\n') + whitespace = "" + + REPEAT = partial( + nodes.Repeat, + names, + expression, + context == "local", + whitespace + ) + + # tal:condition + try: + clause = ns[TAL, 'condition'] + except KeyError: + CONDITION = skip + else: + expression = nodes.Value(clause) + CONDITION = partial(nodes.Condition, expression) + + # tal:switch + if switch is None: + SWITCH = skip + else: + SWITCH = partial(nodes.Cache, list(switch)) + + # i18n:domain + try: + clause = ns[I18N, 'domain'] + except KeyError: + DOMAIN = skip + else: + DOMAIN = partial(nodes.Domain, clause) + + # i18n:context + try: + clause = ns[I18N, 'context'] + except KeyError: + CONTEXT = skip + else: + CONTEXT = partial(nodes.TxContext, clause) + + # i18n:name + try: + clause = ns[I18N, 'name'] + except KeyError: + NAME = skip + else: + if not clause.strip(): + NAME = skip + else: + NAME = partial(nodes.Name, clause) + + # The "slot" node next is the first node level that can serve + # as a macro slot + slot = wrap( + inner, + DEFINE_SLOT, + DEFINE, + CASE, + CONDITION, + REPEAT, + SWITCH, + DOMAIN, + CONTEXT, + ) + + # metal:fill-slot + try: + clause = ns[METAL, 'fill-slot'] + except KeyError: + pass + else: + if not clause.strip(): + raise LanguageError( + "Must provide a non-trivial string for metal:fill-slot.", + clause + ) + + index = -(1 + int(bool(use_macro or extend_macro))) + + try: + slots = self._use_macro[index] + except IndexError: + raise LanguageError( + "Cannot use metal:fill-slot without metal:use-macro.", + clause + ) + + slots = self._use_macro[index] + slots.append(nodes.FillSlot(clause, slot)) + + # metal:define-macro + try: + clause = ns[METAL, 'define-macro'] + except KeyError: + pass + else: + if ns.get((METAL, 'fill-slot')) is not None: + raise LanguageError( + "Can't have 'fill-slot' and 'define-macro' " + "on the same element.", + clause + ) + + self._macros[clause] = slot + slot = nodes.UseInternalMacro(clause) + + slot = wrap( + slot, + NAME + ) + + # tal:on-error + try: + clause = ns[TAL, 'on-error'] + except KeyError: + ON_ERROR = skip + else: + key, value = tal.parse_substitution(clause) + translate = ns.get((I18N, 'translate')) == '' + fallback = self._make_content_node( + value, None, key, translate, + ) + + if omit is False and start['namespace'] not in self.DROP_NS: + start_tag = copy(start_tag) + + start_tag.attributes = nodes.Sequence( + start_tag.attributes.extract( + lambda attribute: + isinstance(attribute, nodes.Attribute) and + isinstance(attribute.expression, ast.Str) + ) + ) + + if end_tag is None: + # Make sure start-tag has opening suffix. We don't + # allow self-closing element here. + start_tag.suffix = ">" + + # Explicitly set end-tag. + end_tag = nodes.End(start_tag.name, '', '</', '>',) + + fallback = nodes.Element( + start_tag, + end_tag, + fallback, + ) + + ON_ERROR = partial(nodes.OnError, fallback, 'error') + + clause = ns.get((META, 'interpolation')) + if clause in ('false', 'off'): + INTERPOLATION = False + elif clause in ('true', 'on'): + INTERPOLATION = True + elif clause is None: + INTERPOLATION = self._interpolation[-1] + else: + raise LanguageError("Bad interpolation setting.", clause) + + self._interpolation.append(INTERPOLATION) + + # Visit content body + for child in children: + body.append(self.visit(*child)) + + self._switches.pop() + self._interpolation.pop() + + if use_macro: + self._use_macro.pop() + + return wrap( + slot, + ON_ERROR + ) + + def visit_start_tag(self, start): + return self.visit_element(start, None, []) + + def visit_cdata(self, node): + if not self._interpolation[-1] or not '${' in node: + return nodes.Text(node) + + expr = nodes.Substitution(node, ()) + return nodes.Interpolation(expr, True, False) + + def visit_comment(self, node): + if node.startswith('<!--!'): + return + + if node.startswith('<!--?'): + return nodes.Text('<!--' + node.lstrip('<!-?')) + + if not self._interpolation[-1] or not '${' in node: + return nodes.Text(node) + + char_escape = ('&', '<', '>') if self.escape else () + expression = nodes.Substitution(node[4:-3], char_escape) + + return nodes.Sequence( + [nodes.Text(node[:4]), + nodes.Interpolation(expression, True, False), + nodes.Text(node[-3:]) + ]) + + def visit_processing_instruction(self, node): + if node['name'] != 'python': + text = '<?' + node['name'] + node['text'] + '?>' + return self.visit_text(text) + + return nodes.CodeBlock(node['text']) + + def visit_text(self, node): + self._last = node + + translation = self.implicit_i18n_translate + + if self._interpolation[-1] and '${' in node: + char_escape = ('&', '<', '>') if self.escape else () + expression = nodes.Substitution(node, char_escape) + return nodes.Interpolation(expression, True, translation) + + node = node.replace('$$', '$') + + if not translation: + return nodes.Text(node) + + match = re.search(r'(\s*)(.*\S)(\s*)', node, flags=re.DOTALL) + if match is not None: + prefix, text, suffix = match.groups() + normalized = re.sub(r'\s+', ' ', text) + return nodes.Sequence([ + nodes.Text(prefix), + nodes.Translate(normalized, nodes.Text(normalized), None), + nodes.Text(suffix), + ]) + else: + return nodes.Text(node) + + def _pop_defaults(self, kwargs, *attributes): + for attribute in attributes: + default = getattr(self, attribute) + value = kwargs.pop(attribute, default) + setattr(self, attribute, value) + + def _check_attributes(self, namespace, ns): + if namespace in self.DROP_NS and ns.get((TAL, 'attributes')): + raise LanguageError( + "Dynamic attributes not allowed on elements of " + "the namespace: %s." % namespace, + ns[TAL, 'attributes'], + ) + + script = ns.get((TAL, 'script')) + if script is not None: + raise LanguageError( + "The script attribute is unsupported.", script) + + tal_content = ns.get((TAL, 'content')) + if tal_content and ns.get((TAL, 'replace')): + raise LanguageError( + "You cannot use tal:content and tal:replace at the same time.", + tal_content + ) + + if tal_content and ns.get((I18N, 'translate')): + raise LanguageError( + "You cannot use tal:content with non-trivial i18n:translate.", + tal_content + ) + + def _make_content_node(self, expression, default, key, translate): + value = nodes.Value(expression) + char_escape = ('&', '<', '>') if key == 'text' else () + content = nodes.Content(value, char_escape, translate) + + if default is not None: + content = nodes.Condition( + nodes.Identity(value, marker("default")), + default, + content, + ) + + # Cache expression to avoid duplicate evaluation + content = nodes.Cache([value], content) + + # Define local marker "default" + content = nodes.Define( + [nodes.Alias(["default"], marker("default"))], + content + ) + + return content + + def _create_attributes_nodes(self, prepared, I18N_ATTRIBUTES, STATIC): + attributes = [] + + names = [attr[0] for attr in prepared] + filtering = [[]] + + for i, (name, text, quote, space, eq, expr) in enumerate(prepared): + implicit_i18n = ( + name is not None and + name.lower() in self.implicit_i18n_attributes + ) + + char_escape = ('&', '<', '>', quote) + + # Use a provided default text as the default marker + # (aliased to the name ``default``), otherwise use the + # program's default marker value. + if text is not None: + default_marker = ast.Str(s=text) + else: + default_marker = self.default_marker + + msgid = I18N_ATTRIBUTES.get(name, missing) + + # If (by heuristic) ``text`` contains one or more + # interpolation expressions, apply interpolation + # substitution to the text + if expr is None and text is not None and '${' in text: + expr = nodes.Substitution(text, char_escape) + translation = implicit_i18n and msgid is missing + value = nodes.Interpolation(expr, True, translation) + default_marker = self.default_marker + + # If the expression is non-trivial, the attribute is + # dynamic (computed). + elif expr is not None: + if name is None: + expression = nodes.Value(decode_htmlentities(expr)) + value = nodes.DictAttributes( + expression, ('&', '<', '>', '"'), '"', + set(filter(None, names[i:])) + ) + for fs in filtering: + fs.append(expression) + filtering.append([]) + elif name in self.boolean_attributes: + value = nodes.Boolean(expr, name) + else: + if text is not None: + default = default_marker + else: + default = None + + value = nodes.Substitution( + decode_htmlentities(expr), + char_escape, + default + ) + + # Otherwise, it's a static attribute. We don't include it + # here if there's one or more "computed" attributes + # (dynamic, from one or more dict values). + else: + value = ast.Str(s=text) + if msgid is missing and implicit_i18n: + msgid = text + + if name is not None: + # If translation is required, wrap in a translation + # clause + if msgid is not missing: + value = nodes.Translate(msgid, value) + + space = self._maybe_trim(space) + fs = filtering[-1] + attribute = nodes.Attribute(name, value, quote, eq, space, fs) + + if not isinstance(value, ast.Str): + # Always define a ``default`` alias for non-static + # expressions. + attribute = nodes.Define( + [nodes.Alias(["default"], default_marker)], + attribute, + ) + + value = attribute + + attributes.append(value) + + result = nodes.Sequence(attributes) + + fs = filtering[0] + if fs: + return nodes.Cache(fs, result) + + return result + + def _create_static_attributes(self, prepared): + static_attrs = {} + + for name, text, quote, space, eq, expr in prepared: + if name is None: + continue + + static_attrs[name] = text if text is not None else expr + + if not static_attrs: + return + + return Static(parse(repr(static_attrs)).body) + + def _maybe_trim(self, string): + if self.trim_attribute_space: + return re_trim.sub(" ", string) + + return string diff --git a/src/chameleon/zpt/template.py b/src/chameleon/zpt/template.py new file mode 100644 index 0000000..973d449 --- /dev/null +++ b/src/chameleon/zpt/template.py @@ -0,0 +1,479 @@ +try: + import ast +except ImportError: + from chameleon import ast25 as ast + +from functools import partial +from os.path import dirname +from hashlib import md5 + +from ..i18n import simple_translate +from ..tales import PythonExpr +from ..tales import StringExpr +from ..tales import NotExpr +from ..tales import ExistsExpr +from ..tales import ImportExpr +from ..tales import ProxyExpr +from ..tales import StructureExpr +from ..tales import ExpressionParser + +from ..tal import RepeatDict + +from ..template import BaseTemplate +from ..template import BaseTemplateFile +from ..compiler import ExpressionEngine +from ..loader import TemplateLoader +from ..astutil import Builtin +from ..utils import decode_string +from ..utils import string_type +from ..utils import unicode_string + +from .program import MacroProgram + +try: + bytes +except NameError: + bytes = str + + +class PageTemplate(BaseTemplate): + """Constructor for the page template language. + + Takes a string input as the only positional argument:: + + template = PageTemplate("<div>Hello, ${name}.</div>") + + Configuration (keyword arguments): + + ``auto_reload`` + + Enables automatic reload of templates. This is mostly useful + in a development mode since it takes a significant performance + hit. + + ``default_expression`` + + Set the default expression type. The default setting is + ``python``. + + ``encoding`` + + The default text substitution value is a unicode string on + Python 2 or simply string on Python 3. + + Pass an encoding to allow encoded byte string input + (e.g. UTF-8). + + ``literal_false`` + + Attributes are not dropped for a value of ``False``. Instead, + the value is coerced to a string. + + This setting exists to provide compatibility with the + reference implementation. + + ``boolean_attributes`` + + Attributes included in this set are treated as booleans: if a + true value is provided, the attribute value is the attribute + name, e.g.:: + + boolean_attributes = {"selected"} + + If we insert an attribute with the name "selected" and + provide a true value, the attribute will be rendered:: + + selected="selected" + + If a false attribute is provided (including the empty string), + the attribute is dropped. + + The special return value ``default`` drops or inserts the + attribute based on the value element attribute value. + + ``translate`` + + Use this option to set a translation function. + + Example:: + + def translate(msgid, domain=None, mapping=None, default=None, context=None): + ... + return translation + + Note that if ``target_language`` is provided at render time, + the translation function must support this argument. + + ``implicit_i18n_translate`` + + Enables implicit translation for text appearing inside + elements. Default setting is ``False``. + + While implicit translation does work for text that includes + expression interpolation, each expression must be simply a + variable name (e.g. ``${foo}``); otherwise, the text will not + be marked for translation. + + ``implicit_i18n_attributes`` + + Any attribute contained in this set will be marked for + implicit translation. Each entry must be a lowercase string. + + Example:: + + implicit_i18n_attributes = set(['alt', 'title']) + + ``on_error_handler`` + + This is an optional exception handler that is invoked during the + "on-error" fallback block. + + ``strict`` + + Enabled by default. If disabled, expressions are only required + to be valid at evaluation time. + + This setting exists to provide compatibility with the + reference implementation which compiles expressions at + evaluation time. + + ``trim_attribute_space`` + + If set, additional attribute whitespace will be stripped. + + ``restricted_namespace`` + + True by default. If set False, ignored all namespace except chameleon default namespaces. It will be useful working with attributes based javascript template renderer like VueJS. + + Example: + + <div v-bind:id="dynamicId"></div> + <button v-on:click="greet">Greet</button> + <button @click="greet">Greet</button> + + ``tokenizer`` + + None by default. If provided, this tokenizer is used instead of the default + (which is selected based on the template mode parameter.) + + Output is unicode on Python 2 and string on Python 3. + + """ + + expression_types = { + 'python': PythonExpr, + 'string': StringExpr, + 'not': NotExpr, + 'exists': ExistsExpr, + 'import': ImportExpr, + 'structure': StructureExpr, + } + + default_expression = 'python' + + translate = staticmethod(simple_translate) + + encoding = None + + boolean_attributes = set() + + literal_false = False + + mode = "xml" + + implicit_i18n_translate = False + + implicit_i18n_attributes = set() + + trim_attribute_space = False + + enable_data_attributes = False + + on_error_handler = None + + restricted_namespace = True + + tokenizer = None + + def __init__(self, body, **config): + self.macros = Macros(self) + super(PageTemplate, self).__init__(body, **config) + + def __getitem__(self, name): + return self.macros[name] + + @property + def builtins(self): + return self._builtins() + + @property + def engine(self): + if self.literal_false: + default_marker = Builtin("__default") + else: + default_marker = Builtin("False") + + return partial( + ExpressionEngine, + self.expression_parser, + default_marker=default_marker, + ) + + @property + def expression_parser(self): + return ExpressionParser(self.expression_types, self.default_expression) + + def parse(self, body): + if self.literal_false: + default_marker = Builtin("__default") + else: + default_marker = Builtin("False") + + return MacroProgram( + body, self.mode, self.filename, + escape=True if self.mode == "xml" else False, + default_marker=default_marker, + boolean_attributes=self.boolean_attributes, + implicit_i18n_translate=self.implicit_i18n_translate, + implicit_i18n_attributes=self.implicit_i18n_attributes, + trim_attribute_space=self.trim_attribute_space, + enable_data_attributes=self.enable_data_attributes, + restricted_namespace=self.restricted_namespace, + tokenizer=self.tokenizer + ) + + def render(self, encoding=None, **_kw): + """Render template to string. + + If providd, the ``encoding`` argument overrides the template + default value. + + Additional keyword arguments are passed as template variables. + + In addition, some also have a special meaning: + + ``translate`` + + This keyword argument will override the default template + translate function. + + ``target_language`` + + This will be used as the default argument to the translate + function if no `i18n:target` value is provided. + + If not provided, the `translate` function will need to + negotiate a language based on the provided context. + """ + + translate = _kw.get('translate') + if translate is not None: + has_translate = True + else: + has_translate = False + translate = self.translate + + # This should not be necessary, but we include it for + # backward compatibility. + if translate is None: + translate = type(self).translate + + encoding = encoding if encoding is not None else self.encoding + if encoding is not None: + def translate(msgid, txl=translate, encoding=encoding, **kwargs): + if isinstance(msgid, bytes): + msgid = decode_string(msgid, encoding) + return txl(msgid, **kwargs) + + def decode(inst, encoding=encoding): + return decode_string(inst, encoding, 'ignore') + else: + decode = decode_string + + target_language = _kw.get('target_language') + + setdefault = _kw.setdefault + setdefault("__translate", translate) + setdefault("__convert", + partial(translate, target_language=target_language)) + setdefault("__decode", decode) + setdefault("target_language", None) + setdefault("__on_error_handler", self.on_error_handler) + + # Make sure we have a repeat dictionary + if 'repeat' not in _kw: _kw['repeat'] = RepeatDict({}) + + return super(PageTemplate, self).render(**_kw) + + def include(self, *args, **kwargs): + self.cook_check() + self._render(*args, **kwargs) + + def digest(self, body, names): + hex = super(PageTemplate, self).digest(body, names) + if isinstance(hex, unicode_string): + hex = hex.encode('utf-8') + digest = md5(hex) + digest.update(';'.join(names).encode('utf-8')) + + for attr in ( + 'trim_attribute_space', + 'implicit_i18n_translate', + 'literal_false', + 'strict' + ): + v = getattr(self, attr) + digest.update( + (";%s=%s" % (attr, str(v))).encode('ascii') + ) + + return digest.hexdigest() + + def _builtins(self): + return { + 'template': self, + 'macros': self.macros, + 'nothing': None, + } + + +class PageTemplateFile(PageTemplate, BaseTemplateFile): + """File-based constructor. + + Takes a string input as the only positional argument:: + + template = PageTemplateFile(absolute_path) + + Note that the file-based template class comes with the expression + type ``load`` which loads templates relative to the provided + filename. + + Below are listed the configuration arguments specific to + file-based templates; see the string-based template class for + general options and documentation: + + Configuration (keyword arguments): + + ``loader_class`` + + The provided class will be used to create the template loader + object. The default implementation supports relative and + absolute path specs. + + The class must accept keyword arguments ``search_path`` + (sequence of paths to search for relative a path spec) and + ``default_extension`` (if provided, this should be added to + any path spec). + + ``prepend_relative_search_path`` + + Inserts the path relative to the provided template file path + into the template search path. + + The default setting is ``True``. + + ``search_path`` + + If provided, this is used as the search path for the ``load:`` + expression. It must be a string or an iterable yielding a + sequence of strings. + + """ + + expression_types = PageTemplate.expression_types.copy() + expression_types['load'] = partial( + ProxyExpr, '__loader', + ignore_prefix=False + ) + + prepend_relative_search_path = True + + def __init__(self, filename, search_path=None, loader_class=TemplateLoader, + **config): + super(PageTemplateFile, self).__init__(filename, **config) + + if search_path is None: + search_path = [] + else: + if isinstance(search_path, string_type): + search_path = [search_path] + else: + search_path = list(search_path) + + # If the flag is set (this is the default), prepend the path + # relative to the template file to the search path + if self.prepend_relative_search_path: + path = dirname(self.filename) + search_path.insert(0, path) + + loader = loader_class(search_path=search_path, **config) + template_class = type(self) + + # Bind relative template loader instance to the same template + # class, providing the same keyword arguments. + self._loader = loader.bind(template_class) + + def _builtins(self): + d = super(PageTemplateFile, self)._builtins() + d['__loader'] = self._loader + return d + + +class PageTextTemplate(PageTemplate): + """Text-based template class. + + Takes a non-XML input:: + + template = PageTextTemplate("Hello, ${name}.") + + This is similar to the standard library class ``string.Template``, + but uses the expression engine to substitute variables. + """ + + mode = "text" + + +class PageTextTemplateFile(PageTemplateFile): + """File-based constructor.""" + + mode = "text" + + def render(self, **vars): + result = super(PageTextTemplateFile, self).render(**vars) + return result.encode(self.encoding or 'utf-8') + + +class Macro(object): + __slots__ = "include", + + def __init__(self, render): + self.include = render + + +class Macros(object): + __slots__ = "template", + + def __init__(self, template): + self.template = template + + def __getitem__(self, name): + name = name.replace('-', '_') + self.template.cook_check() + + try: + function = getattr(self.template, "_render_%s" % name) + except AttributeError: + raise KeyError( + "Macro does not exist: '%s'." % name) + + return Macro(function) + + @property + def names(self): + self.template.cook_check() + + result = [] + for name in self.template.__dict__: + if name.startswith('_render_'): + result.append(name[8:]) + return result |