summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Klose <doko@debian.org>2019-09-05 07:39:45 +0200
committerMatthias Klose <doko@debian.org>2019-09-05 07:39:45 +0200
commitdb3cd9bfd6d279eb6fb61a0588dc0bfc9a4e4639 (patch)
tree54dce106ebe361e51024fa13420057643725b938
Import python-chameleon_3.6.2.orig.tar.gz
[dgit import orig python-chameleon_3.6.2.orig.tar.gz]
-rw-r--r--LICENSE.txt185
-rw-r--r--MANIFEST.in3
-rw-r--r--PKG-INFO1539
-rw-r--r--README.rst25
-rw-r--r--setup.cfg4
-rw-r--r--setup.py71
-rw-r--r--src/Chameleon.egg-info/PKG-INFO1539
-rw-r--r--src/Chameleon.egg-info/SOURCES.txt393
-rw-r--r--src/Chameleon.egg-info/dependency_links.txt1
-rw-r--r--src/Chameleon.egg-info/not-zip-safe1
-rw-r--r--src/Chameleon.egg-info/top_level.txt1
-rw-r--r--src/chameleon/__init__.py6
-rw-r--r--src/chameleon/ast25.py135
-rw-r--r--src/chameleon/astutil.py1019
-rw-r--r--src/chameleon/benchmark.py478
-rw-r--r--src/chameleon/codegen.py234
-rw-r--r--src/chameleon/compiler.py1749
-rw-r--r--src/chameleon/config.py55
-rw-r--r--src/chameleon/exc.py332
-rw-r--r--src/chameleon/i18n.py131
-rw-r--r--src/chameleon/interfaces.py102
-rw-r--r--src/chameleon/loader.py192
-rw-r--r--src/chameleon/metal.py23
-rw-r--r--src/chameleon/namespaces.py9
-rw-r--r--src/chameleon/nodes.py233
-rw-r--r--src/chameleon/parser.py258
-rw-r--r--src/chameleon/program.py41
-rw-r--r--src/chameleon/py25.py36
-rw-r--r--src/chameleon/py26.py15
-rwxr-xr-xsrc/chameleon/tal.py497
-rw-r--r--src/chameleon/tales.py598
-rw-r--r--src/chameleon/template.py356
-rw-r--r--src/chameleon/tests/__init__.py1
-rw-r--r--src/chameleon/tests/inputs/001-interpolation.txt1
-rw-r--r--src/chameleon/tests/inputs/001-variable-scope.html7
-rw-r--r--src/chameleon/tests/inputs/001-variable-scope.pt11
-rw-r--r--src/chameleon/tests/inputs/001.xml4
-rw-r--r--src/chameleon/tests/inputs/002-repeat-scope.pt8
-rw-r--r--src/chameleon/tests/inputs/002.xml4
-rw-r--r--src/chameleon/tests/inputs/003-content.pt17
-rw-r--r--src/chameleon/tests/inputs/003.xml4
-rw-r--r--src/chameleon/tests/inputs/004-attributes.pt25
-rw-r--r--src/chameleon/tests/inputs/004.xml5
-rw-r--r--src/chameleon/tests/inputs/005-default.pt12
-rw-r--r--src/chameleon/tests/inputs/005.xml5
-rw-r--r--src/chameleon/tests/inputs/006-attribute-interpolation.pt9
-rw-r--r--src/chameleon/tests/inputs/006.xml5
-rw-r--r--src/chameleon/tests/inputs/007-content-interpolation.pt21
-rw-r--r--src/chameleon/tests/inputs/007.xml4
-rw-r--r--src/chameleon/tests/inputs/008-builtins.pt12
-rw-r--r--src/chameleon/tests/inputs/008.xml4
-rw-r--r--src/chameleon/tests/inputs/009-literals.pt5
-rw-r--r--src/chameleon/tests/inputs/009.xml4
-rw-r--r--src/chameleon/tests/inputs/010-structure.pt9
-rw-r--r--src/chameleon/tests/inputs/010.xml5
-rw-r--r--src/chameleon/tests/inputs/011-messages.pt9
-rw-r--r--src/chameleon/tests/inputs/011.xml5
-rw-r--r--src/chameleon/tests/inputs/012-translation.pt22
-rw-r--r--src/chameleon/tests/inputs/012.xml5
-rw-r--r--src/chameleon/tests/inputs/013-repeat-nested.pt11
-rw-r--r--src/chameleon/tests/inputs/013.xml5
-rw-r--r--src/chameleon/tests/inputs/014-repeat-nested-similar.pt7
-rw-r--r--src/chameleon/tests/inputs/014.xml5
-rw-r--r--src/chameleon/tests/inputs/015-translation-nested.pt10
-rw-r--r--src/chameleon/tests/inputs/015.xml5
-rw-r--r--src/chameleon/tests/inputs/016-explicit-translation.pt11
-rw-r--r--src/chameleon/tests/inputs/016.xml4
-rw-r--r--src/chameleon/tests/inputs/017-omit-tag.pt12
-rw-r--r--src/chameleon/tests/inputs/017.xml4
-rw-r--r--src/chameleon/tests/inputs/018-translation-nested-dynamic.pt13
-rw-r--r--src/chameleon/tests/inputs/018.xml4
-rw-r--r--src/chameleon/tests/inputs/019-replace.pt13
-rw-r--r--src/chameleon/tests/inputs/019.xml4
-rw-r--r--src/chameleon/tests/inputs/020-on-error.pt10
-rw-r--r--src/chameleon/tests/inputs/020.xml4
-rw-r--r--src/chameleon/tests/inputs/021-translation-domain.pt16
-rw-r--r--src/chameleon/tests/inputs/021.xml4
-rw-r--r--src/chameleon/tests/inputs/022-switch.pt21
-rw-r--r--src/chameleon/tests/inputs/022.xml4
-rw-r--r--src/chameleon/tests/inputs/023-condition.pt6
-rw-r--r--src/chameleon/tests/inputs/023.xml5
-rw-r--r--src/chameleon/tests/inputs/024-namespace-elements.pt16
-rw-r--r--src/chameleon/tests/inputs/024.xml6
-rw-r--r--src/chameleon/tests/inputs/025-repeat-whitespace.pt15
-rw-r--r--src/chameleon/tests/inputs/025.xml5
-rw-r--r--src/chameleon/tests/inputs/026-repeat-variable.pt13
-rw-r--r--src/chameleon/tests/inputs/026.xml5
-rw-r--r--src/chameleon/tests/inputs/027-attribute-replacement.pt11
-rw-r--r--src/chameleon/tests/inputs/027.xml5
-rw-r--r--src/chameleon/tests/inputs/028-attribute-toggle.pt6
-rw-r--r--src/chameleon/tests/inputs/028.xml5
-rw-r--r--src/chameleon/tests/inputs/029-attribute-ordering.pt5
-rw-r--r--src/chameleon/tests/inputs/029.xml5
-rw-r--r--src/chameleon/tests/inputs/030-repeat-tuples.pt7
-rw-r--r--src/chameleon/tests/inputs/030.xml5
-rw-r--r--src/chameleon/tests/inputs/031-namespace-with-tal.pt7
-rw-r--r--src/chameleon/tests/inputs/031.xml5
-rw-r--r--src/chameleon/tests/inputs/032-master-template.pt20
-rw-r--r--src/chameleon/tests/inputs/032.xml5
-rw-r--r--src/chameleon/tests/inputs/033-use-macro-trivial.pt1
-rw-r--r--src/chameleon/tests/inputs/033.xml5
-rw-r--r--src/chameleon/tests/inputs/034-use-template-as-macro.pt1
-rw-r--r--src/chameleon/tests/inputs/034.xml4
-rw-r--r--src/chameleon/tests/inputs/035-use-macro-with-fill-slot.pt5
-rw-r--r--src/chameleon/tests/inputs/035.xml4
-rw-r--r--src/chameleon/tests/inputs/036-use-macro-inherits-dynamic-scope.pt2
-rw-r--r--src/chameleon/tests/inputs/036.xml5
-rw-r--r--src/chameleon/tests/inputs/037-use-macro-local-variable-scope.pt5
-rw-r--r--src/chameleon/tests/inputs/037.xml6
-rw-r--r--src/chameleon/tests/inputs/038-use-macro-globals.pt6
-rw-r--r--src/chameleon/tests/inputs/038.xml6
-rw-r--r--src/chameleon/tests/inputs/039-globals.pt1
-rw-r--r--src/chameleon/tests/inputs/039.xml5
-rw-r--r--src/chameleon/tests/inputs/040-macro-using-template-symbol.pt20
-rw-r--r--src/chameleon/tests/inputs/040.xml5
-rw-r--r--src/chameleon/tests/inputs/041-translate-nested-names.pt22
-rw-r--r--src/chameleon/tests/inputs/041.xml5
-rw-r--r--src/chameleon/tests/inputs/042-use-macro-fill-footer.pt3
-rw-r--r--src/chameleon/tests/inputs/042.xml4
-rw-r--r--src/chameleon/tests/inputs/043-macro-nested-dynamic-vars.pt19
-rw-r--r--src/chameleon/tests/inputs/043.xml6
-rw-r--r--src/chameleon/tests/inputs/044-tuple-define.pt5
-rw-r--r--src/chameleon/tests/inputs/044.xml10
-rw-r--r--src/chameleon/tests/inputs/045-namespaces.pt13
-rw-r--r--src/chameleon/tests/inputs/045.xml6
-rw-r--r--src/chameleon/tests/inputs/046-extend-macro.pt6
-rw-r--r--src/chameleon/tests/inputs/046.xml6
-rw-r--r--src/chameleon/tests/inputs/047-use-extended-macro.pt3
-rw-r--r--src/chameleon/tests/inputs/047.xml5
-rw-r--r--src/chameleon/tests/inputs/048-use-extended-macro-fill-original.pt5
-rw-r--r--src/chameleon/tests/inputs/048.xml4
-rw-r--r--src/chameleon/tests/inputs/049-entities-in-attributes.pt11
-rw-r--r--src/chameleon/tests/inputs/049.xmlbin0 -> 124 bytes
-rw-r--r--src/chameleon/tests/inputs/050-define-macro-and-use-not-extend.pt6
-rw-r--r--src/chameleon/tests/inputs/050.xmlbin0 -> 132 bytes
-rw-r--r--src/chameleon/tests/inputs/051-use-non-extended-macro.pt5
-rw-r--r--src/chameleon/tests/inputs/051.xmlbin0 -> 140 bytes
-rw-r--r--src/chameleon/tests/inputs/052-i18n-domain-inside-filled-slot.pt8
-rw-r--r--src/chameleon/tests/inputs/052.xml4
-rw-r--r--src/chameleon/tests/inputs/053-special-characters-in-attributes.pt6
-rw-r--r--src/chameleon/tests/inputs/053.xml6
-rw-r--r--src/chameleon/tests/inputs/054-import-expression.pt3
-rw-r--r--src/chameleon/tests/inputs/054.xml10
-rw-r--r--src/chameleon/tests/inputs/055-attribute-fallback-to-dict-lookup.pt4
-rw-r--r--src/chameleon/tests/inputs/055.xml5
-rw-r--r--src/chameleon/tests/inputs/056-comment-attribute.pt7
-rw-r--r--src/chameleon/tests/inputs/056.xml4
-rw-r--r--src/chameleon/tests/inputs/057-order.pt8
-rw-r--r--src/chameleon/tests/inputs/057.xml4
-rw-r--r--src/chameleon/tests/inputs/058-script.pt16
-rw-r--r--src/chameleon/tests/inputs/058.xml5
-rw-r--r--src/chameleon/tests/inputs/059-embedded-javascript.pt6
-rw-r--r--src/chameleon/tests/inputs/059.xml10
-rw-r--r--src/chameleon/tests/inputs/060-macro-with-multiple-same-slots.pt8
-rw-r--r--src/chameleon/tests/inputs/060.xml4
-rw-r--r--src/chameleon/tests/inputs/061-fill-one-slot-but-two-defined.pt3
-rw-r--r--src/chameleon/tests/inputs/061.xml4
-rw-r--r--src/chameleon/tests/inputs/062-comments-and-expressions.pt27
-rw-r--r--src/chameleon/tests/inputs/062.xml4
-rw-r--r--src/chameleon/tests/inputs/063-continuation.pt4
-rw-r--r--src/chameleon/tests/inputs/063.xml4
-rw-r--r--src/chameleon/tests/inputs/064-tags-and-special-characters.pt4
-rw-r--r--src/chameleon/tests/inputs/064.xml4
-rw-r--r--src/chameleon/tests/inputs/065-use-macro-in-fill.pt6
-rw-r--r--src/chameleon/tests/inputs/065.xml5
-rw-r--r--src/chameleon/tests/inputs/066-load-expression.pt1
-rw-r--r--src/chameleon/tests/inputs/066.xml7
-rw-r--r--src/chameleon/tests/inputs/067-attribute-decode.pt6
-rw-r--r--src/chameleon/tests/inputs/067.xml4
-rw-r--r--src/chameleon/tests/inputs/068-less-than-greater-than-in-attributes.pt8
-rw-r--r--src/chameleon/tests/inputs/068.xml5
-rw-r--r--src/chameleon/tests/inputs/069-translation-domain-and-macro.pt3
-rw-r--r--src/chameleon/tests/inputs/069.xml5
-rw-r--r--src/chameleon/tests/inputs/070-translation-domain-and-use-macro.pt3
-rw-r--r--src/chameleon/tests/inputs/070.xml5
-rw-r--r--src/chameleon/tests/inputs/071-html-attribute-defaults.pt12
-rw-r--r--src/chameleon/tests/inputs/071.xml5
-rw-r--r--src/chameleon/tests/inputs/072-repeat-interpolation.pt13
-rw-r--r--src/chameleon/tests/inputs/072.xml5
-rw-r--r--src/chameleon/tests/inputs/073-utf8-encoded.pt5
-rw-r--r--src/chameleon/tests/inputs/073.xml5
-rw-r--r--src/chameleon/tests/inputs/074-encoded-template.pt5
-rw-r--r--src/chameleon/tests/inputs/074.xml5
-rw-r--r--src/chameleon/tests/inputs/075-nested-macros.pt11
-rw-r--r--src/chameleon/tests/inputs/075.xml5
-rw-r--r--src/chameleon/tests/inputs/076-nested-macro-override.pt3
-rw-r--r--src/chameleon/tests/inputs/076.xml7
-rw-r--r--src/chameleon/tests/inputs/077-i18n-attributes.pt1
-rw-r--r--src/chameleon/tests/inputs/077.xml5
-rw-r--r--src/chameleon/tests/inputs/078-tags-and-newlines.pt23
-rw-r--r--src/chameleon/tests/inputs/078.xml5
-rw-r--r--src/chameleon/tests/inputs/079-implicit-i18n.pt16
-rw-r--r--src/chameleon/tests/inputs/079.xml5
-rw-r--r--src/chameleon/tests/inputs/080-xmlns-namespace-on-tal.pt6
-rw-r--r--src/chameleon/tests/inputs/080.xml5
-rw-r--r--src/chameleon/tests/inputs/081-load-spec.pt1
-rw-r--r--src/chameleon/tests/inputs/081.xml7
-rw-r--r--src/chameleon/tests/inputs/082-load-spec-computed.pt1
-rw-r--r--src/chameleon/tests/inputs/082.xml5
-rw-r--r--src/chameleon/tests/inputs/083-template-dict-to-macro.pt2
-rw-r--r--src/chameleon/tests/inputs/083.xml5
-rw-r--r--src/chameleon/tests/inputs/084-interpolation-in-cdata.pt9
-rw-r--r--src/chameleon/tests/inputs/084.xml1
-rw-r--r--src/chameleon/tests/inputs/085-nested-translation.pt11
-rw-r--r--src/chameleon/tests/inputs/085.xml6
-rw-r--r--src/chameleon/tests/inputs/086-self-closing.pt10
-rw-r--r--src/chameleon/tests/inputs/086.xml6
-rw-r--r--src/chameleon/tests/inputs/087-code-blocks.pt28
-rw-r--r--src/chameleon/tests/inputs/087.xml6
-rw-r--r--src/chameleon/tests/inputs/088-python-newlines.pt2
-rw-r--r--src/chameleon/tests/inputs/088.xml5
-rw-r--r--src/chameleon/tests/inputs/089-load-fallback.pt3
-rw-r--r--src/chameleon/tests/inputs/089.xml5
-rw-r--r--src/chameleon/tests/inputs/090-tuple-expression.pt8
-rw-r--r--src/chameleon/tests/inputs/090.xml7
-rw-r--r--src/chameleon/tests/inputs/091-repeat-none.pt5
-rw-r--r--src/chameleon/tests/inputs/091.xml7
-rw-r--r--src/chameleon/tests/inputs/092.xml10
-rw-r--r--src/chameleon/tests/inputs/093.xml5
-rw-r--r--src/chameleon/tests/inputs/094.xml6
-rw-r--r--src/chameleon/tests/inputs/095.xml6
-rw-r--r--src/chameleon/tests/inputs/096.xml5
-rw-r--r--src/chameleon/tests/inputs/097.xml8
-rw-r--r--src/chameleon/tests/inputs/098.xml5
-rw-r--r--src/chameleon/tests/inputs/099.xml5
-rw-r--r--src/chameleon/tests/inputs/100.xml5
-rw-r--r--src/chameleon/tests/inputs/101-unclosed-tags.html5
-rw-r--r--src/chameleon/tests/inputs/101.xml5
-rw-r--r--src/chameleon/tests/inputs/102-unquoted-attributes.html5
-rw-r--r--src/chameleon/tests/inputs/102.xml5
-rw-r--r--src/chameleon/tests/inputs/103-simple-attribute.html8
-rw-r--r--src/chameleon/tests/inputs/103.xml4
-rw-r--r--src/chameleon/tests/inputs/104.xml5
-rw-r--r--src/chameleon/tests/inputs/105.xml5
-rw-r--r--src/chameleon/tests/inputs/106.xml5
-rw-r--r--src/chameleon/tests/inputs/107.xml5
-rw-r--r--src/chameleon/tests/inputs/108.xml7
-rw-r--r--src/chameleon/tests/inputs/109.xml5
-rw-r--r--src/chameleon/tests/inputs/110.xml6
-rw-r--r--src/chameleon/tests/inputs/111.xml5
-rw-r--r--src/chameleon/tests/inputs/112.xml5
-rw-r--r--src/chameleon/tests/inputs/113.xml5
-rw-r--r--src/chameleon/tests/inputs/114.xml5
-rw-r--r--src/chameleon/tests/inputs/115.xml6
-rw-r--r--src/chameleon/tests/inputs/116.xml5
-rw-r--r--src/chameleon/tests/inputs/117.xml5
-rw-r--r--src/chameleon/tests/inputs/118.xml5
-rw-r--r--src/chameleon/tests/inputs/119.xml4
-rw-r--r--src/chameleon/tests/inputs/120-translation-context.pt13
-rw-r--r--src/chameleon/tests/inputs/121-translation-comment.pt7
-rw-r--r--src/chameleon/tests/inputs/122-translation-ignore.pt10
-rw-r--r--src/chameleon/tests/inputs/123-html5-data-attributes.pt5
-rw-r--r--src/chameleon/tests/inputs/124-translation-target.pt6
-rw-r--r--src/chameleon/tests/inputs/125-macro-translation-ordering.pt12
-rw-r--r--src/chameleon/tests/inputs/126-define-escaping.pt10
-rw-r--r--src/chameleon/tests/inputs/238-macroname.pt7
-rw-r--r--src/chameleon/tests/inputs/greeting.pt1
-rw-r--r--src/chameleon/tests/inputs/hello_world.pt5
-rw-r--r--src/chameleon/tests/inputs/hello_world.txt1
-rw-r--r--src/chameleon/tests/inputs/multinode-implicit-i18n.pt3
-rw-r--r--src/chameleon/tests/outputs/001.html7
-rw-r--r--src/chameleon/tests/outputs/001.pt9
-rw-r--r--src/chameleon/tests/outputs/001.txt1
-rw-r--r--src/chameleon/tests/outputs/002.pt13
-rw-r--r--src/chameleon/tests/outputs/003.pt17
-rw-r--r--src/chameleon/tests/outputs/004.pt25
-rw-r--r--src/chameleon/tests/outputs/005.pt12
-rw-r--r--src/chameleon/tests/outputs/006.pt9
-rw-r--r--src/chameleon/tests/outputs/007.pt20
-rw-r--r--src/chameleon/tests/outputs/008.pt12
-rw-r--r--src/chameleon/tests/outputs/009.pt5
-rw-r--r--src/chameleon/tests/outputs/010.pt9
-rw-r--r--src/chameleon/tests/outputs/011-en.pt9
-rw-r--r--src/chameleon/tests/outputs/011.pt9
-rw-r--r--src/chameleon/tests/outputs/012-en.pt10
-rw-r--r--src/chameleon/tests/outputs/012.pt10
-rw-r--r--src/chameleon/tests/outputs/013.pt22
-rw-r--r--src/chameleon/tests/outputs/014.pt12
-rw-r--r--src/chameleon/tests/outputs/015-en.pt5
-rw-r--r--src/chameleon/tests/outputs/015.pt5
-rw-r--r--src/chameleon/tests/outputs/016-en.pt9
-rw-r--r--src/chameleon/tests/outputs/016.pt9
-rw-r--r--src/chameleon/tests/outputs/017.pt12
-rw-r--r--src/chameleon/tests/outputs/018-en.pt3
-rw-r--r--src/chameleon/tests/outputs/018.pt3
-rw-r--r--src/chameleon/tests/outputs/019.pt13
-rw-r--r--src/chameleon/tests/outputs/020.pt8
-rw-r--r--src/chameleon/tests/outputs/021-en.pt12
-rw-r--r--src/chameleon/tests/outputs/021.pt12
-rw-r--r--src/chameleon/tests/outputs/022.pt21
-rw-r--r--src/chameleon/tests/outputs/023.pt6
-rw-r--r--src/chameleon/tests/outputs/024.pt14
-rw-r--r--src/chameleon/tests/outputs/025.pt22
-rw-r--r--src/chameleon/tests/outputs/026.pt17
-rw-r--r--src/chameleon/tests/outputs/027.pt7
-rw-r--r--src/chameleon/tests/outputs/028.pt5
-rw-r--r--src/chameleon/tests/outputs/029.pt3
-rw-r--r--src/chameleon/tests/outputs/030.pt10
-rw-r--r--src/chameleon/tests/outputs/031.pt7
-rw-r--r--src/chameleon/tests/outputs/032.pt15
-rw-r--r--src/chameleon/tests/outputs/033.pt15
-rw-r--r--src/chameleon/tests/outputs/034.pt15
-rw-r--r--src/chameleon/tests/outputs/035.pt17
-rw-r--r--src/chameleon/tests/outputs/036.pt15
-rw-r--r--src/chameleon/tests/outputs/037.pt15
-rw-r--r--src/chameleon/tests/outputs/038.pt6
-rw-r--r--src/chameleon/tests/outputs/039.pt0
-rw-r--r--src/chameleon/tests/outputs/040.pt15
-rw-r--r--src/chameleon/tests/outputs/041.pt7
-rw-r--r--src/chameleon/tests/outputs/042.pt15
-rw-r--r--src/chameleon/tests/outputs/043.pt11
-rw-r--r--src/chameleon/tests/outputs/044.pt5
-rw-r--r--src/chameleon/tests/outputs/045.pt12
-rw-r--r--src/chameleon/tests/outputs/046.pt17
-rw-r--r--src/chameleon/tests/outputs/047.pt17
-rw-r--r--src/chameleon/tests/outputs/048.pt17
-rw-r--r--src/chameleon/tests/outputs/049.pt11
-rw-r--r--src/chameleon/tests/outputs/050.pt15
-rw-r--r--src/chameleon/tests/outputs/051.pt15
-rw-r--r--src/chameleon/tests/outputs/052.pt15
-rw-r--r--src/chameleon/tests/outputs/053.pt6
-rw-r--r--src/chameleon/tests/outputs/054.pt3
-rw-r--r--src/chameleon/tests/outputs/055.pt4
-rw-r--r--src/chameleon/tests/outputs/056.pt7
-rw-r--r--src/chameleon/tests/outputs/057.pt8
-rw-r--r--src/chameleon/tests/outputs/058.pt16
-rw-r--r--src/chameleon/tests/outputs/059.pt6
-rw-r--r--src/chameleon/tests/outputs/060.pt8
-rw-r--r--src/chameleon/tests/outputs/061.pt8
-rw-r--r--src/chameleon/tests/outputs/062.pt27
-rw-r--r--src/chameleon/tests/outputs/063.pt3
-rw-r--r--src/chameleon/tests/outputs/064.pt3
-rw-r--r--src/chameleon/tests/outputs/065.pt13
-rw-r--r--src/chameleon/tests/outputs/066.pt5
-rw-r--r--src/chameleon/tests/outputs/067.pt6
-rw-r--r--src/chameleon/tests/outputs/068.pt8
-rw-r--r--src/chameleon/tests/outputs/069-en.pt15
-rw-r--r--src/chameleon/tests/outputs/069.pt15
-rw-r--r--src/chameleon/tests/outputs/070-en.pt15
-rw-r--r--src/chameleon/tests/outputs/070.pt15
-rw-r--r--src/chameleon/tests/outputs/071.pt12
-rw-r--r--src/chameleon/tests/outputs/072.pt19
-rw-r--r--src/chameleon/tests/outputs/073.pt5
-rw-r--r--src/chameleon/tests/outputs/074.pt5
-rw-r--r--src/chameleon/tests/outputs/075.pt19
-rw-r--r--src/chameleon/tests/outputs/076.pt17
-rw-r--r--src/chameleon/tests/outputs/077-en.pt1
-rw-r--r--src/chameleon/tests/outputs/077.pt1
-rw-r--r--src/chameleon/tests/outputs/078.pt9
-rw-r--r--src/chameleon/tests/outputs/079-en.pt16
-rw-r--r--src/chameleon/tests/outputs/079.pt16
-rw-r--r--src/chameleon/tests/outputs/080.pt3
-rw-r--r--src/chameleon/tests/outputs/081.pt5
-rw-r--r--src/chameleon/tests/outputs/082.pt5
-rw-r--r--src/chameleon/tests/outputs/083.pt15
-rw-r--r--src/chameleon/tests/outputs/084.pt9
-rw-r--r--src/chameleon/tests/outputs/085-en.pt9
-rw-r--r--src/chameleon/tests/outputs/085.pt9
-rw-r--r--src/chameleon/tests/outputs/086.pt18
-rw-r--r--src/chameleon/tests/outputs/087.pt25
-rw-r--r--src/chameleon/tests/outputs/088.pt1
-rw-r--r--src/chameleon/tests/outputs/089.pt5
-rw-r--r--src/chameleon/tests/outputs/090.pt14
-rw-r--r--src/chameleon/tests/outputs/091.pt5
-rw-r--r--src/chameleon/tests/outputs/101.html5
-rw-r--r--src/chameleon/tests/outputs/102.html5
-rw-r--r--src/chameleon/tests/outputs/103.html8
-rw-r--r--src/chameleon/tests/outputs/120-en.pt9
-rw-r--r--src/chameleon/tests/outputs/120.pt9
-rw-r--r--src/chameleon/tests/outputs/121.pt5
-rw-r--r--src/chameleon/tests/outputs/122.pt9
-rw-r--r--src/chameleon/tests/outputs/123.pt5
-rw-r--r--src/chameleon/tests/outputs/124-en.pt6
-rw-r--r--src/chameleon/tests/outputs/124.pt6
-rw-r--r--src/chameleon/tests/outputs/125.pt6
-rw-r--r--src/chameleon/tests/outputs/126.pt9
-rw-r--r--src/chameleon/tests/outputs/238.pt7
-rw-r--r--src/chameleon/tests/outputs/greeting.pt1
-rw-r--r--src/chameleon/tests/outputs/hello_world.pt5
-rw-r--r--src/chameleon/tests/outputs/hello_world.txt1
-rw-r--r--src/chameleon/tests/outputs/multinode-en.pt3
-rw-r--r--src/chameleon/tests/outputs/multinode.pt3
-rw-r--r--src/chameleon/tests/test_doctests.py40
-rw-r--r--src/chameleon/tests/test_exc.py13
-rw-r--r--src/chameleon/tests/test_loader.py110
-rw-r--r--src/chameleon/tests/test_parser.py103
-rw-r--r--src/chameleon/tests/test_sniffing.py124
-rw-r--r--src/chameleon/tests/test_templates.py795
-rw-r--r--src/chameleon/tests/test_tokenizer.py47
-rw-r--r--src/chameleon/tokenize.py144
-rw-r--r--src/chameleon/utils.py438
-rw-r--r--src/chameleon/zpt/__init__.py1
-rw-r--r--src/chameleon/zpt/loader.py30
-rw-r--r--src/chameleon/zpt/program.py876
-rw-r--r--src/chameleon/zpt/template.py479
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
+ ``&amp;`` 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
+ ``&amp;`` 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('&amp;', _tmp5)
+ else:
+ _tmp5 = _tmp5.replace('&', '&amp;')
+ if ('<' in _tmp5):
+ _tmp5 = _tmp5.replace('<', '&lt;')
+ if ('>' in _tmp5):
+ _tmp5 = _tmp5.replace('>', '&gt;')
+ if ('"' in _tmp5):
+ _tmp5 = _tmp5.replace('"', '&quot;')
+ _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('&amp;', _tmp)
+ else:
+ _tmp = _tmp.replace('&', '&amp;')
+ if ('<' in _tmp):
+ _tmp = _tmp.replace('<', '&lt;')
+ if ('>' in _tmp):
+ _tmp = _tmp.replace('>', '&gt;')
+ 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('&amp;', _tmp5)
+ else:
+ _tmp5 = _tmp5.replace('&', '&amp;')
+ if ('<' in _tmp5):
+ _tmp5 = _tmp5.replace('<', '&lt;')
+ if ('>' in _tmp5):
+ _tmp5 = _tmp5.replace('>', '&gt;')
+ if ('"' in _tmp5):
+ _tmp5 = _tmp5.replace('"', '&quot;')
+ _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('&amp;', _tmp)
+ else:
+ _tmp = _tmp.replace('&', '&amp;')
+ if ('<' in _tmp):
+ _tmp = _tmp.replace('<', '&lt;')
+ if ('>' in _tmp):
+ _tmp = _tmp.replace('>', '&gt;')
+ 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('&amp;', _tmp5)
+ else:
+ _tmp5 = _tmp5.replace('&', '&amp;')
+ if ('<' in _tmp5):
+ _tmp5 = _tmp5.replace('<', '&lt;')
+ if ('>' in _tmp5):
+ _tmp5 = _tmp5.replace('>', '&gt;')
+ if ('"' in _tmp5):
+ _tmp5 = _tmp5.replace('"', '&quot;')
+ _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('&amp;', _tmp)
+ else:
+ _tmp = _tmp.replace('&', '&amp;')
+ if ('<' in _tmp):
+ _tmp = _tmp.replace('<', '&lt;')
+ if ('>' in _tmp):
+ _tmp = _tmp.replace('>', '&gt;')
+ 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('&', '&amp;')
+ if '<' in target:
+ target = target.replace('<', '&lt;')
+ if '>' in target:
+ target = target.replace('>', '&gt;')
+ 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:&amp;" />
+ <span class="hello" tal:attributes="class 'goodbye'" />
+ <span class="hello" tal:attributes="class '&quot;goodbye&quot;'" />
+ <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': '&quot;goodbye&quot;'}" />
+ <span tal:attributes="class 'hello'; {'class': '&quot;goodbye&quot;'}" />
+ <span tal:attributes="{'class': '&quot;goodbye&quot;'}; 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 &lt; 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>&#32;</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>&amp;&lt;&gt;&quot;&apos;</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>&#x20;</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 &lt; 2" />
+ <div tal:content="structure string:2 &lt; 3, 2&amp;3, 2&lt;3, 2&gt;3" />
+ <div tal:content="structure string:3 ${'&lt;'} 4" />
+ <div tal:content="structure '%d &lt; %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 "&#60;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="&quot;&lt;&amp;&gt;&apos;"></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="&#65;"></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>&#00000000000000000000000000000000065;</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 "\&#160;">
+]>
+<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&nbsp;YYY&nbsp;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;amp; lt=&amp;lt;" />
+ <pre tal:content="structure string:amp=&amp;amp; lt=&amp;lt;" />
+ <script tal:replace="structure string:&lt;script /&gt;" />
+ <script tal:replace="string:&lt;script /&gt;" />
+ <script tal:replace="string:${'&lt;'}script /${'&gt;'}" />
+ <script tal:replace="structure string:${'&lt;'}script /${'&gt;'}" />
+ <img alt="1 &lt; 2: ${1 &lt; 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
new file mode 100644
index 0000000..c3cc797
--- /dev/null
+++ b/src/chameleon/tests/inputs/049.xml
Binary files differ
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
new file mode 100644
index 0000000..12303b1
--- /dev/null
+++ b/src/chameleon/tests/inputs/050.xml
Binary files differ
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
new file mode 100644
index 0000000..7ae8f6c
--- /dev/null
+++ b/src/chameleon/tests/inputs/051.xml
Binary files differ
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>&#x0000000000000000000000000000000000000041;</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&#10;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>&#163;</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>&#xe40;&#xe08;&#xe21;ส์</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>&#x10000;&#x10FFFD;</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 "&#60;">
+<!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 "&#34;">
+]>
+<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 &lt; 1 and 'down';" />
+ <img src="#" tal:attributes="class 0 &gt; 1 and 'up' or 0 &lt; 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>&#13;</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 "&#13;">
+]>
+<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/&#62;">
+<!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 "&lt;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 "&#x10000;&#x10FFFD;&#x10FFFF;">
+<!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 "&#34;">
+]>
+<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="&#34;"></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>&#60;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&#9;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&#10;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&#13;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 "&#13;&#10;">
+<!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="&#32;x&#32;&#32;y&#32;"></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>&rsqb;</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>&rsqb;</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:&amp;;
+ s2 python:'&amp;'">
+ ${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>
+ &lt;div&gt;Hello world!&lt;/div&gt;
+ </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="&amp;" c="3" />
+ <span class="goodbye" />
+ <span class="&quot;goodbye&quot;" />
+ <span class="'goodbye'" />
+ <span class='&#39;goodbye&#39;' />
+ <span class="goodbye" />
+ <span class="goodbye" />
+ <span a="1" class="goodbye" />
+ <span class="&quot;goodbye&quot;" />
+ <span class="&quot;goodbye&quot;" />
+ <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="&lt;type 'str'&gt;" />
+ </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>
+ &lt;type 'str'&gt;
+ &&
+ <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 &lt; 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 "\&#160;">
+]>
+<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&nbsp;YYY&nbsp;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;amp; lt=&amp;lt;</pre>
+ <pre>amp=&amp; lt=&lt;</pre>
+ <script />
+ &lt;script /&gt;
+ &lt;script /&gt;
+ <script />
+ <img alt="1 &lt; 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 &lt; 1 or 0 &gt; 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>
+ &amp;
+ &amp;
+ &amp;
+ &amp;
+ <a href="localhost?a=1b&amp;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, '&lt;dummy&gt;')
+
+ 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;amp;'))
+ '&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