# HG changeset patch # User cmlenz # Date 1180718507 0 # Node ID 0742f421caba747967488b1f8fd443931162b959 # Parent 869b7885a516483c1916628d771fe4bbfce7b1f3 Merged revisions 487-603 via svnmerge from http://svn.edgewall.org/repos/genshi/trunk diff --git a/COPYING b/COPYING --- a/COPYING +++ b/COPYING @@ -1,4 +1,4 @@ -Copyright (C) 2006 Edgewall Software +Copyright (C) 2006-2007 Edgewall Software All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/ChangeLog b/ChangeLog --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,54 @@ +Version 0.5 +http://svn.edgewall.org/repos/genshi/tags/0.5.0/ +(?, from branches/stable/0.5.x) + + * Added #include directive for text templates (ticket #115). + + +Version 0.4.2 +http://svn.edgewall.org/repos/genshi/tags/0.4.2/ +(?, from branches/stable/0.4.x) + + * The `doctype` parameter of the markup serializers now also accepts the "name" + of the doctype as string, in addition to the `(name, pubid, sysid)` tuple. + * The I18n filter was not replacing the original attributes with the + translation, but instead adding a second attribute with the same name. + + +Version 0.4.1 +http://svn.edgewall.org/repos/genshi/tags/0.4.1/ +(May 21 2007, from branches/stable/0.4.x) + + * Fix incorrect reference to translation function in the I18N filter. + * The `ET()` function now correctly handles attributes with a namespace. + * XML declarations are now processed internally, as well as written to the + output when XML serialization is used (ticket #111). + * Added the functions `encode()` and `get_serializer()` to the `genshi.output` + module, which provide a lower-level API to the functionality previously only + available through `Stream.render()` and `Stream.serialize()`. + * The `DocType` class now has a `get(name)` function that returns a `DOCTYPE` + tuple for a given string. + * Added frameset variants to the `DocType` constants for HTML 4.01 and XHTML + 1.0. + * Improved I18n extraction for pluralizable messages: for any translation + function with multiple string arguments (such as ``ngettext``), a single + item with a tuple of strings is yielded, instead an item for each string + argument. + * The `HTMLFormFiller` stream filter no longer alters form elements for which + the data element contains no corresponding item. + * Code in `` processing instructions no longer gets the special + treatment as Python code in template expressions, i.e. item and attribute + access are no longer interchangeable (which was broken in a number of ways + anyway, see ticket #113). This change does not affect expressions. + * Numerous fixes for the execution of Python code in `` processing + instructions (tickets #113 and #114). + * The `py:def` (and `#def`) directive now supports "star args" (i.e. `*args` + and `**kwargs`) in the function declaration (ticket #116). + + Version 0.4 http://svn.edgewall.org/repos/genshi/tags/0.4.0/ -(?, from branches/stable/0.4.x) +(Apr 16 2007, from branches/stable/0.4.x) * New example applications for CherryPy and web.py. * The template loader now uses a LRU cache to limit the number of cached @@ -30,7 +78,39 @@ * `MarkupTemplate`s can now be instantiated from markup streams, in addition to strings and file-like objects (ticket #69). * Improve handling of incorrectly nested tags in the HTML parser. - * Template includes can you be nested inside fallback content. + * Template includes can now be nested inside fallback content. + * Expressions can now contain dict literals (ticket #37). + * It is now possible to have one or more escaped dollar signs in front of a + full expression (ticket #92). + * The `Markup` class is now available by default in template expressions + (ticket #67). + * The handling of namespace declarations in XML/XHTML output has been improved. + * The `Attrs` class no longer automatically wraps all attribute names in + `QName` objects. This is now the responsibility of whoever is instantiating + `Attrs` objects (for example, stream filters and generators). + * Python code blocks are now supported using the `` processing + instruction (ticket #84). + * The way errors in template expressions are handled can now be configured. The + option `LenientLookup` provides the same forgiving mode used in previous + Genshi versions, while `StrictLookup` raises exceptions when undefined + variables or members are accessed. The lenient mode is still the default in + this version, but that may change in the future. (ticket #88) + * If a variable is not necessarily defined at the top level of the template + data, the new built-in functions `defined(key)` and `value_of(key, default)` + can be used so that the template also works in strict lookup mode. These + functions were previously only available when using Genshi via the template + engine plugin (for compatibility with Kid). + * `style` attributes are no longer allowed by the `HTMLSanitizer` by default. + If they are explicitly added to the set of safe attributes, any unicode + escapes in the attribute value are now handled properly. + * Namespace declarations on conditional elements (for example using a `py:if` + directive`) are no longer moved to the following element when the element + originally carrying the declaration is removed from the stream (ticket #107). + * Added basic built-in support for internationalizing templates by providing + a new `Translator` class that can both extract localizable strings from a + stream, and replace those strings with their localizations at render time. + The code for this was largely taken from previous work done by Matt Good + and David Fraser. Version 0.3.6 diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,4 @@ exclude doc/2000ft.graffle -exclude doc/docutils.conf recursive-exclude doc/logo.lineform * -exclude doc/Makefile +include doc/api/*.* include doc/*.html diff --git a/UPGRADE.txt b/UPGRADE.txt --- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -4,19 +4,23 @@ Upgrading from Genshi 0.3.x to 0.4.x ------------------------------------ -The `genshi.template` module has been refactored into a package with -multiple modules. While code using the normal templating APIs should -continue to work without problems, you should make sure to remove any -leftover traces of the `template.py` file on the installation path. -This is not necessary when Genshi was installed as a Python egg. +The modules ``genshi.filters`` and ``genshi.template`` have been +refactored into packages containing multiple modules. While code using +the regular APIs should continue to work without problems, you should +make sure to remove any leftover traces of the ``template.py`` file on +the installation path. This is not necessary when Genshi was installed +as a Python egg. Results of evaluating template expressions are no longer implicitly called if they are callable. If you have been using that feature, you will need to add the parenthesis to actually call the function. Instances of `genshi.core.Attrs` are now immutable. Filters -manipulating the attributes in a stream may need to be updated. See -the documentation of the `Attrs` class for more information. +manipulating the attributes in a stream may need to be updated. Also, +the `Attrs` class no longer automatically wraps all attribute names +in `QName` objects, so users of the `Attrs` class need to do this +themselves. See the documentation of the `Attrs` class for more +information. Upgrading from Markup @@ -40,7 +44,7 @@ has been renamed to: - `markup.template.MarkupTemplate` + `genshi.template.MarkupTemplate` If you've been using the Template class directly, you'll need to update your code (a simple find/replace should do--the API itself diff --git a/doc/2000ft.graffle b/doc/2000ft.graffle --- a/doc/2000ft.graffle +++ b/doc/2000ft.graffle @@ -56,7 +56,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -91,7 +91,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -126,7 +126,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -161,7 +161,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -196,7 +196,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -273,7 +273,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -347,7 +347,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -423,7 +423,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -569,12 +569,12 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural -\f0\fs22 \cf0 Whitespace Filter} +\f0\fs22 \cf0 HTML Sanitizer} @@ -604,12 +604,12 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural -\f0\fs22 \cf0 XInclude Filter} +\f0\fs22 \cf0 HTML Form Filler} @@ -639,12 +639,12 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural -\f0\fs22 \cf0 HTML Sanitizer} +\f0\fs22 \cf0 I18N Translator} @@ -720,7 +720,7 @@ Align 2 Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural @@ -760,7 +760,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -795,7 +795,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -830,7 +830,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -911,7 +911,7 @@ Align 2 Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural @@ -946,7 +946,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -981,7 +981,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -1062,7 +1062,7 @@ Align 2 Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural @@ -1141,7 +1141,7 @@ Text Text - {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf410 + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf420 {\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;} {\colortbl;\red255\green255\blue255;} \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural @@ -1249,7 +1249,7 @@ ModificationDate - 2006-09-12 18:13:56 +0200 + 2007-04-13 15:15:25 +0200 Modifier Christopher Lenz NotesVisible @@ -1325,7 +1325,7 @@ ShowStatusBar VisibleRegion - {{-121, 0}, {765, 470}} + {{-121, 0}, {765, 446}} Zoom 1 diff --git a/doc/2000ft.png b/doc/2000ft.png index 3c6dc53915076b2fd6225b9798325463a3e7399d..d0209916dd84c14e852056736dffa991c1cc1506 GIT binary patch literal 28698 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 S+j_ADkoqVuRweQ&=>Gv;WFyl6 diff --git a/doc/builder.txt b/doc/builder.txt deleted file mode 100644 --- a/doc/builder.txt +++ /dev/null @@ -1,72 +0,0 @@ -.. -*- mode: rst; encoding: utf-8 -*- - -================================== -Generating Markup Programmatically -================================== - -Genshi provides a ``builder`` module which lets you generate markup from Python -code using a very simple syntax. The main entry point to the ``builder`` module -is the ``tag`` object (which is actually an instance of the ``ElementFactory`` -class). You should rarely (if ever) need to directly import and use any of the -other classes in the ``builder`` module. - - -.. contents:: Contents - :depth: 2 -.. sectnum:: - - -Creating Elements -================= - -Elements can be created through the `tag` object using attribute access, for -example:: - - >>> from genshi.builder import tag - >>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.') - >>> doc - - -This produces an ``Element`` instance which can be further modified to add child -nodes and attributes. This is done by “calling” the element: positional -arguments are added as child nodes (alternatively, the ``append`` method can be -used for that purpose), whereas keywords arguments are added as attributes:: - - >>> doc(tag.br) - - >>> print doc -

Some text and a link.

- -If an attribute name collides with a Python keyword, simply append an underscore -to the name:: - - >>> doc(class_='intro') - - >>> print doc -

Some text and a link.

- -As shown above, an ``Element`` can easily be directly rendered to XML text by -printing it or using the Python ``str()`` function. This is basically a -shortcut for converting the ``Element`` to a stream and serializing that -stream:: - - >>> stream = doc.generate() - >>> stream - - >>> print stream -

Some text and a link.

- - -Creating Fragments -================== - -The ``tag`` object also allows creating “fragments”, which are basically lists -of nodes (elements or text) that don't have a parent element. This can be useful -for creating snippets of markup that are attached to a parent element later (for -example in a template). Fragments are created by calling the ``tag`` object:: - - >>> fragment = tag('Hello, ', tag.em('world'), '!') - >>> fragment - - >>> print fragment - Hello, world! diff --git a/doc/epydoc.conf b/doc/epydoc.conf new file mode 100644 --- /dev/null +++ b/doc/epydoc.conf @@ -0,0 +1,24 @@ +[epydoc] + +name: Documentation Index +url: ../index.html +modules: genshi +verbosity: 1 + +# Extraction +docformat: restructuredtext +parse: yes +introspect: yes +exclude: .*\.tests.* +inheritance: listed +private: no +imports: no +include-log: no + +# HTML output +output: html +target: doc/api/ +css: doc/style/epydoc.css +top: genshi +frames: no +sourcecode: no diff --git a/doc/filters.txt b/doc/filters.txt new file mode 100644 --- /dev/null +++ b/doc/filters.txt @@ -0,0 +1,132 @@ +.. -*- mode: rst; encoding: utf-8 -*- + +============== +Stream Filters +============== + +`Markup Streams`_ showed how to write filters and how they are applied to +markup streams. This page describes the features of the various filters that +come with Genshi itself. + +.. _`Markup Streams`: streams.html + +.. contents:: Contents + :depth: 1 +.. sectnum:: + + +HTML Form Filler +================ + +The filter ``genshi.filters.HTMLFormFiller`` can automatically populate an HTML +form from values provided as a simple dictionary. When using thi filter, you can +basically omit any ``value``, ``selected``, or ``checked`` attributes from form +controls in your templates, and let the filter do all that work for you. + +``HTMLFormFiller`` takes a dictionary of data to populate the form with, where +the keys should match the names of form elements, and the values determine the +values of those controls. For example:: + + >>> from genshi.filters import HTMLFormFiller + >>> from genshi.template import MarkupTemplate + >>> template = MarkupTemplate("""
+ ...

+ ...
+ ...
+ ... + ...

+ ...
""") + >>> filler = HTMLFormFiller(data=dict(username='john', remember=True)) + >>> print template.generate() | filler +
+

+
+
+ +

+
+ +.. note:: This processing is done without in any way reparsing the template + output. As any stream filter it operates after the template output is + generated but *before* that output is actually serialized. + +The filter will of course also handle radio buttons as well as `` +

""") | HTMLFormFiller() + self.assertEquals("""

+ +

""") | HTMLFormFiller(data={'foo': 'bar'}) + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_textarea_multi_value(self): + html = HTML("""

+ +

""") | HTMLFormFiller(data={'foo': ['bar']}) + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_input_checkbox_no_value(self): + html = HTML("""

+ +

""") | HTMLFormFiller() + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_input_checkbox_single_value_auto(self): + html = HTML("""

+ +

""") + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': ''}))) + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': 'on'}))) + + def test_fill_input_checkbox_single_value_defined(self): + html = HTML("""

+ +

""") + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': '1'}))) + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': '2'}))) + + def test_fill_input_checkbox_multi_value_auto(self): + html = HTML("""

+ +

""") + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': []}))) + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': ['on']}))) + + def test_fill_input_checkbox_multi_value_defined(self): + html = HTML("""

+ +

""") + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': ['1']}))) + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': ['2']}))) + + def test_fill_input_radio_no_value(self): + html = HTML("""

+ +

""") | HTMLFormFiller() + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_input_radio_single_value(self): + html = HTML("""

+ +

""") + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': '1'}))) + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': '2'}))) + + def test_fill_input_radio_multi_value(self): + html = HTML("""

+ +

""") + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': ['1']}))) + self.assertEquals("""

+ +

""", unicode(html | HTMLFormFiller(data={'foo': ['2']}))) + + def test_fill_select_no_value_auto(self): + html = HTML("""

+ +

""") | HTMLFormFiller() + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_select_no_value_defined(self): + html = HTML("""

+ +

""") | HTMLFormFiller() + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_select_single_value_auto(self): + html = HTML("""

+ +

""") | HTMLFormFiller(data={'foo': '1'}) + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_select_single_value_defined(self): + html = HTML("""

+ +

""") | HTMLFormFiller(data={'foo': '1'}) + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_select_multi_value_auto(self): + html = HTML("""

+ +

""") | HTMLFormFiller(data={'foo': ['1', '3']}) + self.assertEquals("""

+ +

""", unicode(html)) + + def test_fill_select_multi_value_defined(self): + html = HTML("""

+ +

""") | HTMLFormFiller(data={'foo': ['1', '3']}) + self.assertEquals("""

+ +

""", unicode(html)) + + +class HTMLSanitizerTestCase(unittest.TestCase): + + def test_sanitize_unchanged(self): + html = HTML('fo
o
') + self.assertEquals(u'fo
o
', + unicode(html | HTMLSanitizer())) + + def test_sanitize_escape_text(self): + html = HTML('fo&') + self.assertEquals(u'fo&', + unicode(html | HTMLSanitizer())) + html = HTML('<foo>') + self.assertEquals(u'<foo>', + unicode(html | HTMLSanitizer())) + + def test_sanitize_entityref_text(self): + html = HTML('foö') + self.assertEquals(u'foö', + unicode(html | HTMLSanitizer())) + + def test_sanitize_escape_attr(self): + html = HTML('
') + self.assertEquals(u'
', + unicode(html | HTMLSanitizer())) + + def test_sanitize_close_empty_tag(self): + html = HTML('fo
o
') + self.assertEquals(u'fo
o
', + unicode(html | HTMLSanitizer())) + + def test_sanitize_invalid_entity(self): + html = HTML('&junk;') + self.assertEquals('&junk;', unicode(html | HTMLSanitizer())) + + def test_sanitize_remove_script_elem(self): + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + self.assertRaises(ParseError, HTML, 'alert("foo")') + self.assertRaises(ParseError, HTML, + '') + + def test_sanitize_remove_onclick_attr(self): + html = HTML('
') + self.assertEquals(u'
', unicode(html | HTMLSanitizer())) + + def test_sanitize_remove_style_scripts(self): + sanitizer = HTMLSanitizer(safe_attrs=HTMLSanitizer.SAFE_ATTRS | set(['style'])) + # Inline style with url() using javascript: scheme + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + # Inline style with url() using javascript: scheme, using control char + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + # Inline style with url() using javascript: scheme, in quotes + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + # IE expressions in CSS not allowed + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + html = HTML('
') + self.assertEquals(u'
', + unicode(html | sanitizer)) + # Inline style with url() using javascript: scheme, using unicode + # escapes + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + html = HTML('
') + self.assertEquals(u'
', unicode(html | sanitizer)) + + def test_sanitize_remove_src_javascript(self): + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + # Case-insensitive protocol matching + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + # Grave accents (not parsed) + self.assertRaises(ParseError, HTML, + '') + # Protocol encoded using UTF-8 numeric entities + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + # Protocol encoded using UTF-8 numeric entities without a semicolon + # (which is allowed because the max number of digits is used) + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + # Protocol encoded using UTF-8 numeric hex entities without a semicolon + # (which is allowed because the max number of digits is used) + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + # Embedded tab character in protocol + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + # Embedded tab character in protocol, but encoded this time + html = HTML('') + self.assertEquals(u'', unicode(html | HTMLSanitizer())) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(doctest.DocTestSuite(HTMLFormFiller.__module__)) + suite.addTest(unittest.makeSuite(HTMLFormFillerTestCase, 'test')) + suite.addTest(unittest.makeSuite(HTMLSanitizerTestCase, 'test')) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/genshi/filters/tests/i18n.py b/genshi/filters/tests/i18n.py new file mode 100644 --- /dev/null +++ b/genshi/filters/tests/i18n.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007 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/. + +import doctest +from StringIO import StringIO +import unittest + +from genshi.template import MarkupTemplate +from genshi.filters.i18n import Translator + + +class TranslatorTestCase(unittest.TestCase): + + def test_extract_plural_form(self): + tmpl = MarkupTemplate(""" + ${ngettext("Singular", "Plural", num)} + """) + translator = Translator() + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((2, 'ngettext', (u'Singular', u'Plural')), messages[0]) + + def test_extract_included_attribute_text(self): + tmpl = MarkupTemplate(""" + + """) + translator = Translator() + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((2, None, u'Foo'), messages[0]) + + def test_extract_attribute_expr(self): + tmpl = MarkupTemplate(""" + + """) + translator = Translator() + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((2, '_', u'Save'), messages[0]) + + def test_extract_non_included_attribute_interpolated(self): + tmpl = MarkupTemplate(""" + Foo + """) + translator = Translator() + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((2, None, u'Foo'), messages[0]) + + def test_extract_text_from_sub(self): + tmpl = MarkupTemplate(""" + Foo + """) + translator = Translator() + messages = list(translator.extract(tmpl.stream)) + self.assertEqual(1, len(messages)) + self.assertEqual((2, None, u'Foo'), messages[0]) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(TranslatorTestCase, 'test')) + suite.addTests(doctest.DocTestSuite(Translator.__module__)) + return suite + +if __name__ == '__main__': + unittest.main(defaultTest='suite') diff --git a/genshi/input.py b/genshi/input.py --- a/genshi/input.py +++ b/genshi/input.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2007 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -11,6 +11,10 @@ # individuals. For the exact contribution history, see the revision # history and logs, available at http://genshi.edgewall.org/log/. +"""Support for constructing markup streams from files, strings, or other +sources. +""" + from itertools import chain from xml.parsers import expat try: @@ -22,15 +26,21 @@ from StringIO import StringIO from genshi.core import Attrs, QName, Stream, stripentities -from genshi.core import DOCTYPE, START, END, START_NS, END_NS, TEXT, \ +from genshi.core import START, END, XML_DECL, DOCTYPE, TEXT, START_NS, END_NS, \ START_CDATA, END_CDATA, PI, COMMENT __all__ = ['ET', 'ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML'] +__docformat__ = 'restructuredtext en' def ET(element): - """Convert a given ElementTree element to a markup stream.""" + """Convert a given ElementTree element to a markup stream. + + :param element: an ElementTree element + :return: a markup stream + """ tag_name = QName(element.tag.lstrip('{')) - attrs = Attrs(element.items()) + attrs = Attrs([(QName(attr.lstrip('{')), value) + for attr, value in element.items()]) yield START, (tag_name, attrs), (None, -1, -1) if element.text: @@ -45,12 +55,22 @@ class ParseError(Exception): """Exception raised when fatal syntax errors are found in the input being - parsed.""" + parsed. + """ - def __init__(self, message, filename='', lineno=-1, offset=-1): + def __init__(self, message, filename=None, lineno=-1, offset=-1): + """Exception initializer. + + :param message: the error message from the parser + :param filename: the path to the file that was parsed + :param lineno: the number of the line on which the error was encountered + :param offset: the column number where the error was encountered + """ + self.msg = message + if filename: + message += ', in ' + filename Exception.__init__(self, message) - self.msg = message - self.filename = filename + self.filename = filename or '' self.lineno = lineno self.offset = offset @@ -78,11 +98,12 @@ def __init__(self, source, filename=None, encoding=None): """Initialize the parser for the given XML input. - @param source: the XML text as a file-like object - @param filename: the name of the file, if appropriate - @param encoding: the encoding of the file; if not specified, the - encoding is assumed to be ASCII, UTF-8, or UTF-16, or whatever the - encoding specified in the XML declaration (if any) + :param source: the XML text as a file-like object + :param filename: the name of the file, if appropriate + :param encoding: the encoding of the file; if not specified, the + encoding is assumed to be ASCII, UTF-8, or UTF-16, or + whatever the encoding specified in the XML declaration + (if any) """ self.source = source self.filename = filename @@ -102,6 +123,7 @@ parser.StartCdataSectionHandler = self._handle_start_cdata parser.EndCdataSectionHandler = self._handle_end_cdata parser.ProcessingInstructionHandler = self._handle_pi + parser.XmlDeclHandler = self._handle_xml_decl parser.CommentHandler = self._handle_comment # Tell Expat that we'll handle non-XML entities ourselves @@ -119,6 +141,11 @@ self._queue = [] def parse(self): + """Generator that parses the XML source, yielding markup events. + + :return: a markup event stream + :raises ParseError: if the XML text is not well formed + """ def _generate(): try: bufsize = 4 * 1024 # 4K @@ -142,8 +169,6 @@ break except expat.ExpatError, e: msg = str(e) - if self.filename: - msg += ', in ' + self.filename raise ParseError(msg, self.filename, e.lineno, e.offset) return Stream(_generate()).filter(_coalesce) @@ -182,7 +207,9 @@ self.expat.CurrentColumnNumber) def _handle_start(self, tag, attrib): - self._enqueue(START, (QName(tag), Attrs(zip(*[iter(attrib)] * 2)))) + attrs = Attrs([(QName(name), value) for name, value in + zip(*[iter(attrib)] * 2)]) + self._enqueue(START, (QName(tag), attrs)) def _handle_end(self, tag): self._enqueue(END, QName(tag)) @@ -190,6 +217,9 @@ def _handle_data(self, text): self._enqueue(TEXT, text) + def _handle_xml_decl(self, version, encoding, standalone): + self._enqueue(XML_DECL, (version, encoding, standalone)) + def _handle_doctype(self, name, sysid, pubid, has_internal_subset): self._enqueue(DOCTYPE, (name, pubid, sysid)) @@ -228,6 +258,23 @@ def XML(text): + """Parse the given XML source and return a markup stream. + + Unlike with `XMLParser`, the returned stream is reusable, meaning it can be + iterated over multiple times: + + >>> xml = XML('FooBar') + >>> print xml + FooBar + >>> print xml.select('elem') + FooBar + >>> print xml.select('elem/text()') + FooBar + + :param text: the XML source + :return: the parsed XML event stream + :raises ParseError: if the XML text is not well-formed + """ return Stream(list(XMLParser(StringIO(text)))) @@ -256,9 +303,9 @@ def __init__(self, source, filename=None, encoding='utf-8'): """Initialize the parser for the given HTML input. - @param source: the HTML text as a file-like object - @param filename: the name of the file, if known - @param filename: encoding of the file; ignored if the input is unicode + :param source: the HTML text as a file-like object + :param filename: the name of the file, if known + :param filename: encoding of the file; ignored if the input is unicode """ html.HTMLParser.__init__(self) self.source = source @@ -268,6 +315,11 @@ self._open_tags = [] def parse(self): + """Generator that parses the HTML source, yielding markup events. + + :return: a markup event stream + :raises ParseError: if the HTML text is not well formed + """ def _generate(): try: bufsize = 4 * 1024 # 4K @@ -291,8 +343,6 @@ break except html.HTMLParseError, e: msg = '%s: line %d, column %d' % (e.msg, e.lineno, e.offset) - if self.filename: - msg += ', in %s' % self.filename raise ParseError(msg, self.filename, e.lineno, e.offset) return Stream(_generate()).filter(_coalesce) @@ -315,7 +365,7 @@ value = unicode(name) elif not isinstance(value, unicode): value = value.decode(self.encoding, 'replace') - fixed_attrib.append((name, stripentities(value))) + fixed_attrib.append((QName(name), stripentities(value))) self._enqueue(START, (QName(tag), Attrs(fixed_attrib))) if tag in self._EMPTY_ELEMS: @@ -337,7 +387,10 @@ self._enqueue(TEXT, text) def handle_charref(self, name): - text = unichr(int(name)) + if name.lower().startswith('x'): + text = unichr(int(name[1:], 16)) + else: + text = unichr(int(name)) self._enqueue(TEXT, text) def handle_entityref(self, name): @@ -358,6 +411,24 @@ def HTML(text, encoding='utf-8'): + """Parse the given HTML source and return a markup stream. + + Unlike with `HTMLParser`, the returned stream is reusable, meaning it can be + iterated over multiple times: + + >>> html = HTML('

Foo

') + >>> print html +

Foo

+ >>> print html.select('h1') +

Foo

+ >>> print html.select('h1/text()') + Foo + + :param text: the HTML source + :return: the parsed XML event stream + :raises ParseError: if the HTML text is not well-formed, and error recovery + fails + """ return Stream(list(HTMLParser(StringIO(text), encoding=encoding))) def _coalesce(stream): diff --git a/genshi/output.py b/genshi/output.py --- a/genshi/output.py +++ b/genshi/output.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2007 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -22,29 +22,116 @@ from sets import ImmutableSet as frozenset import re -from genshi.core import escape, Markup, Namespace, QName, StreamEventKind -from genshi.core import DOCTYPE, START, END, START_NS, TEXT, START_CDATA, \ - END_CDATA, PI, COMMENT, XML_NAMESPACE +from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind +from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \ + START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE -__all__ = ['DocType', 'XMLSerializer', 'XHTMLSerializer', 'HTMLSerializer', - 'TextSerializer'] +__all__ = ['encode', 'get_serializer', 'DocType', 'XMLSerializer', + 'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer'] +__docformat__ = 'restructuredtext en' + +def encode(iterator, method='xml', encoding='utf-8'): + """Encode serializer output into a string. + + :param iterator: the iterator returned from serializing a stream (basically + any iterator that yields unicode objects) + :param method: the serialization method; determines how characters not + representable in the specified encoding are treated + :param encoding: how the output string should be encoded; if set to `None`, + this method returns a `unicode` object + :return: a string or unicode object (depending on the `encoding` parameter) + :since: version 0.4.1 + """ + output = u''.join(list(iterator)) + if encoding is not None: + errors = 'replace' + if method != 'text' and not isinstance(method, TextSerializer): + errors = 'xmlcharrefreplace' + return output.encode(encoding, errors) + return output + +def get_serializer(method='xml', **kwargs): + """Return a serializer object for the given method. + + :param method: the serialization method; can be either "xml", "xhtml", + "html", "text", or a custom serializer class + + Any additional keyword arguments are passed to the serializer, and thus + depend on the `method` parameter value. + + :see: `XMLSerializer`, `XHTMLSerializer`, `HTMLSerializer`, `TextSerializer` + :since: version 0.4.1 + """ + if isinstance(method, basestring): + method = {'xml': XMLSerializer, + 'xhtml': XHTMLSerializer, + 'html': HTMLSerializer, + 'text': TextSerializer}[method.lower()] + return method(**kwargs) class DocType(object): """Defines a number of commonly used DOCTYPE declarations as constants.""" - HTML_STRICT = ('html', '-//W3C//DTD HTML 4.01//EN', - 'http://www.w3.org/TR/html4/strict.dtd') - HTML_TRANSITIONAL = ('html', '-//W3C//DTD HTML 4.01 Transitional//EN', - 'http://www.w3.org/TR/html4/loose.dtd') + HTML_STRICT = ( + 'html', '-//W3C//DTD HTML 4.01//EN', + 'http://www.w3.org/TR/html4/strict.dtd' + ) + HTML_TRANSITIONAL = ( + 'html', '-//W3C//DTD HTML 4.01 Transitional//EN', + 'http://www.w3.org/TR/html4/loose.dtd' + ) + HTML_FRAMESET = ( + 'html', '-//W3C//DTD HTML 4.01 Frameset//EN', + 'http://www.w3.org/TR/html4/frameset.dtd' + ) HTML = HTML_STRICT - XHTML_STRICT = ('html', '-//W3C//DTD XHTML 1.0 Strict//EN', - 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd') - XHTML_TRANSITIONAL = ('html', '-//W3C//DTD XHTML 1.0 Transitional//EN', - 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd') + HTML5 = ('html', None, None) + + XHTML_STRICT = ( + 'html', '-//W3C//DTD XHTML 1.0 Strict//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' + ) + XHTML_TRANSITIONAL = ( + 'html', '-//W3C//DTD XHTML 1.0 Transitional//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' + ) + XHTML_FRAMESET = ( + 'html', '-//W3C//DTD XHTML 1.0 Frameset//EN', + 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd' + ) XHTML = XHTML_STRICT + def get(cls, name): + """Return the ``(name, pubid, sysid)`` tuple of the ``DOCTYPE`` + declaration for the specified name. + + The following names are recognized in this version: + * "html" or "html-strict" for the HTML 4.01 strict DTD + * "html-transitional" for the HTML 4.01 transitional DTD + * "html-transitional" for the HTML 4.01 frameset DTD + * "html5" for the ``DOCTYPE`` proposed for HTML5 + * "xhtml" or "xhtml-strict" for the XHTML 1.0 strict DTD + * "xhtml-transitional" for the XHTML 1.0 transitional DTD + * "xhtml-frameset" for the XHTML 1.0 frameset DTD + + :param name: the name of the ``DOCTYPE`` + :return: the ``(name, pubid, sysid)`` tuple for the requested + ``DOCTYPE``, or ``None`` if the name is not recognized + :since: version 0.4.1 + """ + return { + 'html': cls.HTML, 'html-strict': cls.HTML_STRICT, + 'html-transitional': DocType.HTML_TRANSITIONAL, + 'html-frameset': DocType.HTML_FRAMESET, + 'html5': cls.HTML5, + 'xhtml': cls.XHTML, 'xhtml-strict': cls.XHTML_STRICT, + 'xhtml-transitional': cls.XHTML_TRANSITIONAL, + 'xhtml-frameset': cls.XHTML_FRAMESET, + }.get(name.lower()) + get = classmethod(get) + class XMLSerializer(object): """Produces XML text from an event stream. @@ -57,25 +144,127 @@ _PRESERVE_SPACE = frozenset() - def __init__(self, doctype=None, strip_whitespace=True): + def __init__(self, doctype=None, strip_whitespace=True, + namespace_prefixes=None): """Initialize the XML serializer. - @param doctype: a `(name, pubid, sysid)` tuple that represents the - DOCTYPE declaration that should be included at the top of the - generated output - @param strip_whitespace: whether extraneous whitespace should be - stripped from the output + :param doctype: a ``(name, pubid, sysid)`` tuple that represents the + DOCTYPE declaration that should be included at the top + of the generated output, or the name of a DOCTYPE as + defined in `DocType.get` + :param strip_whitespace: whether extraneous whitespace should be + stripped from the output + :note: Changed in 0.4.2: The `doctype` parameter can now be a string. """ self.preamble = [] if doctype: + if isinstance(doctype, basestring): + doctype = DocType.get(doctype) self.preamble.append((DOCTYPE, doctype, (None, -1, -1))) self.filters = [EmptyTagFilter()] if strip_whitespace: self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE)) + self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes)) def __call__(self, stream): - ns_attrib = [] - ns_mapping = {XML_NAMESPACE.uri: 'xml'} + have_decl = have_doctype = False + in_cdata = False + + stream = chain(self.preamble, stream) + for filter_ in self.filters: + stream = filter_(stream) + for kind, data, pos in stream: + + if kind is START or kind is EMPTY: + tag, attrib = data + buf = ['<', tag] + for attr, value in attrib: + buf += [' ', attr, '="', escape(value), '"'] + buf.append(kind is EMPTY and '/>' or '>') + yield Markup(u''.join(buf)) + + elif kind is END: + yield Markup('' % data) + + elif kind is TEXT: + if in_cdata: + yield data + else: + yield escape(data, quotes=False) + + elif kind is COMMENT: + yield Markup('' % data) + + elif kind is XML_DECL and not have_decl: + version, encoding, standalone = data + buf = ['\n') + yield Markup(u''.join(buf)) + have_decl = True + + elif kind is DOCTYPE and not have_doctype: + name, pubid, sysid = data + buf = ['\n') + yield Markup(u''.join(buf), *filter(None, data)) + have_doctype = True + + elif kind is START_CDATA: + yield Markup('') + in_cdata = False + + elif kind is PI: + yield Markup('' % data) + + +class XHTMLSerializer(XMLSerializer): + """Produces XHTML text from an event stream. + + >>> from genshi.builder import tag + >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True)) + >>> print ''.join(XHTMLSerializer()(elem.generate())) +


+ """ + + _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame', + 'hr', 'img', 'input', 'isindex', 'link', 'meta', + 'param']) + _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare', + 'defer', 'disabled', 'ismap', 'multiple', + 'nohref', 'noresize', 'noshade', 'nowrap']) + _PRESERVE_SPACE = frozenset([ + QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'), + QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea') + ]) + + def __init__(self, doctype=None, strip_whitespace=True, + namespace_prefixes=None): + super(XHTMLSerializer, self).__init__(doctype, False) + self.filters = [EmptyTagFilter()] + if strip_whitespace: + self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE)) + namespace_prefixes = namespace_prefixes or {} + namespace_prefixes['http://www.w3.org/1999/xhtml'] = '' + self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes)) + + def __call__(self, stream): + boolean_attrs = self._BOOLEAN_ATTRS + empty_elems = self._EMPTY_ELEMS have_doctype = False in_cdata = False @@ -86,42 +275,22 @@ if kind is START or kind is EMPTY: tag, attrib = data - - tagname = tag.localname - namespace = tag.namespace - if namespace: - if namespace in ns_mapping: - prefix = ns_mapping[namespace] - if prefix: - tagname = '%s:%s' % (prefix, tagname) + buf = ['<', tag] + for attr, value in attrib: + if attr in boolean_attrs: + value = attr + buf += [' ', attr, '="', escape(value), '"'] + if kind is EMPTY: + if tag in empty_elems: + buf.append(' />') else: - ns_attrib.append((QName('xmlns'), namespace)) - buf = ['<', tagname] - - if ns_attrib: - attrib += tuple(ns_attrib) - for attr, value in attrib: - attrname = attr.localname - attrns = attr.namespace - if attrns: - prefix = ns_mapping.get(attrns) - if prefix: - attrname = '%s:%s' % (prefix, attrname) - buf += [' ', attrname, '="', escape(value), '"'] - ns_attrib = [] - - buf.append(kind is EMPTY and '/>' or '>') - + buf.append('>' % tag) + else: + buf.append('>') yield Markup(u''.join(buf)) elif kind is END: - tag = data - tagname = tag.localname - if tag.namespace: - prefix = ns_mapping.get(tag.namespace) - if prefix: - tagname = '%s:%s' % (prefix, tag.localname) - yield Markup('' % tagname) + yield Markup('' % data) elif kind is TEXT: if in_cdata: @@ -145,144 +314,6 @@ yield Markup(u''.join(buf), *filter(None, data)) have_doctype = True - elif kind is START_NS: - prefix, uri = data - if uri not in ns_mapping: - ns_mapping[uri] = prefix - if not prefix: - ns_attrib.append((QName('xmlns'), uri)) - else: - ns_attrib.append((QName('xmlns:%s' % prefix), uri)) - - elif kind is START_CDATA: - yield Markup('') - in_cdata = False - - elif kind is PI: - yield Markup('' % data) - - -class XHTMLSerializer(XMLSerializer): - """Produces XHTML text from an event stream. - - >>> from genshi.builder import tag - >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True)) - >>> print ''.join(XHTMLSerializer()(elem.generate())) -


- """ - - NAMESPACE = Namespace('http://www.w3.org/1999/xhtml') - - _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame', - 'hr', 'img', 'input', 'isindex', 'link', 'meta', - 'param']) - _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare', - 'defer', 'disabled', 'ismap', 'multiple', - 'nohref', 'noresize', 'noshade', 'nowrap']) - _PRESERVE_SPACE = frozenset([ - QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'), - QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea') - ]) - - def __call__(self, stream): - namespace = self.NAMESPACE - ns_attrib = [] - ns_mapping = {XML_NAMESPACE.uri: 'xml'} - boolean_attrs = self._BOOLEAN_ATTRS - empty_elems = self._EMPTY_ELEMS - have_doctype = False - in_cdata = False - - stream = chain(self.preamble, stream) - for filter_ in self.filters: - stream = filter_(stream) - for kind, data, pos in stream: - - if kind is START or kind is EMPTY: - tag, attrib = data - - tagname = tag.localname - tagns = tag.namespace - if tagns: - if tagns in ns_mapping: - prefix = ns_mapping[tagns] - if prefix: - tagname = '%s:%s' % (prefix, tagname) - else: - ns_attrib.append((QName('xmlns'), tagns)) - buf = ['<', tagname] - - if ns_attrib: - attrib += tuple(ns_attrib) - for attr, value in attrib: - attrname = attr.localname - attrns = attr.namespace - if attrns: - prefix = ns_mapping.get(attrns) - if prefix: - attrname = '%s:%s' % (prefix, attrname) - if attrname in boolean_attrs: - if value: - buf += [' ', attrname, '="', attrname, '"'] - else: - buf += [' ', attrname, '="', escape(value), '"'] - ns_attrib = [] - - if kind is EMPTY: - if (tagns and tagns != namespace.uri) \ - or tagname in empty_elems: - buf.append(' />') - else: - buf.append('>' % tagname) - else: - buf.append('>') - - yield Markup(u''.join(buf)) - - elif kind is END: - tag = data - tagname = tag.localname - if tag.namespace: - prefix = ns_mapping.get(tag.namespace) - if prefix: - tagname = '%s:%s' % (prefix, tagname) - yield Markup('' % tagname) - - elif kind is TEXT: - if in_cdata: - yield data - else: - yield escape(data, quotes=False) - - elif kind is COMMENT: - yield Markup('' % data) - - elif kind is DOCTYPE and not have_doctype: - name, pubid, sysid = data - buf = ['\n') - yield Markup(u''.join(buf), *filter(None, data)) - have_doctype = True - - elif kind is START_NS: - prefix, uri = data - if uri not in ns_mapping: - ns_mapping[uri] = prefix - if not prefix: - ns_attrib.append((QName('xmlns'), uri)) - else: - ns_attrib.append((QName('xmlns:%s' % prefix), uri)) - elif kind is START_CDATA: yield Markup('

""" - _NOESCAPE_ELEMS = frozenset([QName('script'), - QName('http://www.w3.org/1999/xhtml}script'), - QName('style'), - QName('http://www.w3.org/1999/xhtml}style')]) + _NOESCAPE_ELEMS = frozenset([ + QName('script'), QName('http://www.w3.org/1999/xhtml}script'), + QName('style'), QName('http://www.w3.org/1999/xhtml}style') + ]) def __init__(self, doctype=None, strip_whitespace=True): """Initialize the HTML serializer. - @param doctype: a `(name, pubid, sysid)` tuple that represents the - DOCTYPE declaration that should be included at the top of the - generated output - @param strip_whitespace: whether extraneous whitespace should be - stripped from the output + :param doctype: a ``(name, pubid, sysid)`` tuple that represents the + DOCTYPE declaration that should be included at the top + of the generated output + :param strip_whitespace: whether extraneous whitespace should be + stripped from the output """ super(HTMLSerializer, self).__init__(doctype, False) + self.filters = [EmptyTagFilter()] if strip_whitespace: self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE, self._NOESCAPE_ELEMS)) + self.filters.append(NamespaceStripper('http://www.w3.org/1999/xhtml')) def __call__(self, stream): - namespace = self.NAMESPACE - ns_mapping = {} boolean_attrs = self._BOOLEAN_ATTRS empty_elems = self._EMPTY_ELEMS noescape_elems = self._NOESCAPE_ELEMS @@ -339,35 +370,23 @@ if kind is START or kind is EMPTY: tag, attrib = data - if not tag.namespace or tag in namespace: - tagname = tag.localname - buf = ['<', tagname] - - for attr, value in attrib: - attrname = attr.localname - if not attr.namespace or attr in namespace: - if attrname in boolean_attrs: - if value: - buf += [' ', attrname] - else: - buf += [' ', attrname, '="', escape(value), '"'] - - buf.append('>') - - if kind is EMPTY: - if tagname not in empty_elems: - buf.append('' % tagname) - - yield Markup(u''.join(buf)) - - if tagname in noescape_elems: - noescape = True + buf = ['<', tag] + for attr, value in attrib: + if attr in boolean_attrs: + if value: + buf += [' ', attr] + else: + buf += [' ', attr, '="', escape(value), '"'] + buf.append('>') + if kind is EMPTY: + if tag not in empty_elems: + buf.append('' % tag) + yield Markup(u''.join(buf)) + if tag in noescape_elems: + noescape = True elif kind is END: - tag = data - if not tag.namespace or tag in namespace: - yield Markup('' % tag.localname) - + yield Markup('' % data) noescape = False elif kind is TEXT: @@ -392,9 +411,6 @@ yield Markup(u''.join(buf), *filter(None, data)) have_doctype = True - elif kind is START_NS and data[1] not in ns_mapping: - ns_mapping[data[1]] = data[0] - elif kind is PI: yield Markup('' % data) @@ -423,8 +439,9 @@ """ def __call__(self, stream): - for kind, data, pos in stream: - if kind is TEXT: + for event in stream: + if event[0] is TEXT: + data = event[1] if type(data) is Markup: data = data.striptags().stripentities() yield unicode(data) @@ -439,36 +456,206 @@ def __call__(self, stream): prev = (None, None, None) - for kind, data, pos in stream: + for ev in stream: if prev[0] is START: - if kind is END: + if ev[0] is END: prev = EMPTY, prev[1], prev[2] yield prev continue else: yield prev - if kind is not START: - yield kind, data, pos - prev = kind, data, pos + if ev[0] is not START: + yield ev + prev = ev EMPTY = EmptyTagFilter.EMPTY +class NamespaceFlattener(object): + r"""Output stream filter that removes namespace information from the stream, + instead adding namespace attributes and prefixes as needed. + + :param prefixes: optional mapping of namespace URIs to prefixes + + >>> from genshi.input import XML + >>> xml = XML(''' + ... + ... ''') + >>> for kind, data, pos in NamespaceFlattener()(xml): + ... print kind, repr(data) + START (u'doc', Attrs([(u'xmlns', u'NS1'), (u'xmlns:two', u'NS2')])) + TEXT u'\n ' + START (u'two:item', Attrs()) + END u'two:item' + TEXT u'\n' + END u'doc' + """ + + def __init__(self, prefixes=None): + self.prefixes = {XML_NAMESPACE.uri: 'xml'} + if prefixes is not None: + self.prefixes.update(prefixes) + + def __call__(self, stream): + prefixes = dict([(v, [k]) for k, v in self.prefixes.items()]) + namespaces = {XML_NAMESPACE.uri: ['xml']} + def _push_ns(prefix, uri): + namespaces.setdefault(uri, []).append(prefix) + prefixes.setdefault(prefix, []).append(uri) + + ns_attrs = [] + _push_ns_attr = ns_attrs.append + def _make_ns_attr(prefix, uri): + return u'xmlns%s' % (prefix and ':%s' % prefix or ''), uri + + def _gen_prefix(): + val = 0 + while 1: + val += 1 + yield 'ns%d' % val + _gen_prefix = _gen_prefix().next + + for kind, data, pos in stream: + + if kind is START or kind is EMPTY: + tag, attrs = data + + tagname = tag.localname + tagns = tag.namespace + if tagns: + if tagns in namespaces: + prefix = namespaces[tagns][-1] + if prefix: + tagname = u'%s:%s' % (prefix, tagname) + else: + _push_ns_attr((u'xmlns', tagns)) + _push_ns('', tagns) + + new_attrs = [] + for attr, value in attrs: + attrname = attr.localname + attrns = attr.namespace + if attrns: + if attrns not in namespaces: + prefix = _gen_prefix() + _push_ns(prefix, attrns) + _push_ns_attr(('xmlns:%s' % prefix, attrns)) + else: + prefix = namespaces[attrns][-1] + if prefix: + attrname = u'%s:%s' % (prefix, attrname) + new_attrs.append((attrname, value)) + + yield kind, (tagname, Attrs(ns_attrs + new_attrs)), pos + del ns_attrs[:] + + elif kind is END: + tagname = data.localname + tagns = data.namespace + if tagns: + prefix = namespaces[tagns][-1] + if prefix: + tagname = u'%s:%s' % (prefix, tagname) + yield kind, tagname, pos + + elif kind is START_NS: + prefix, uri = data + if uri not in namespaces: + prefix = prefixes.get(uri, [prefix])[-1] + _push_ns_attr(_make_ns_attr(prefix, uri)) + _push_ns(prefix, uri) + + elif kind is END_NS: + if data in prefixes: + uris = prefixes.get(data) + uri = uris.pop() + if not uris: + del prefixes[data] + if uri not in uris or uri != uris[-1]: + uri_prefixes = namespaces[uri] + uri_prefixes.pop() + if not uri_prefixes: + del namespaces[uri] + if ns_attrs: + attr = _make_ns_attr(data, uri) + if attr in ns_attrs: + ns_attrs.remove(attr) + + else: + yield kind, data, pos + + +class NamespaceStripper(object): + r"""Stream filter that removes all namespace information from a stream, and + optionally strips out all tags not in a given namespace. + + :param namespace: the URI of the namespace that should not be stripped. If + not set, only elements with no namespace are included in + the output. + + >>> from genshi.input import XML + >>> xml = XML(''' + ... + ... ''') + >>> for kind, data, pos in NamespaceStripper(Namespace('NS1'))(xml): + ... print kind, repr(data) + START (u'doc', Attrs()) + TEXT u'\n ' + TEXT u'\n' + END u'doc' + """ + + def __init__(self, namespace=None): + if namespace is not None: + self.namespace = Namespace(namespace) + else: + self.namespace = {} + + def __call__(self, stream): + namespace = self.namespace + + for kind, data, pos in stream: + + if kind is START or kind is EMPTY: + tag, attrs = data + if tag.namespace and tag not in namespace: + continue + + new_attrs = [] + for attr, value in attrs: + if not attr.namespace or attr in namespace: + new_attrs.append((attr, value)) + + data = tag.localname, Attrs(new_attrs) + + elif kind is END: + if data.namespace and data not in namespace: + continue + data = data.localname + + elif kind is START_NS or kind is END_NS: + continue + + yield kind, data, pos + + class WhitespaceFilter(object): """A filter that removes extraneous ignorable white space from the - stream.""" + stream. + """ def __init__(self, preserve=None, noescape=None): """Initialize the filter. - @param preserve: a set or sequence of tag names for which white-space - should be preserved - @param noescape: a set or sequence of tag names for which text content - should not be escaped + :param preserve: a set or sequence of tag names for which white-space + should be preserved + :param noescape: a set or sequence of tag names for which text content + should not be escaped The `noescape` set is expected to refer to elements that cannot contain - further child elements (such as """, output) + def test_html5_doctype(self): + stream = HTML('') + output = stream.render(HTMLSerializer, doctype=DocType.HTML5) + self.assertEqual('\n', output) + class EmptyTagFilterTestCase(unittest.TestCase): diff --git a/genshi/tests/path.py b/genshi/tests/path.py --- a/genshi/tests/path.py +++ b/genshi/tests/path.py @@ -451,25 +451,17 @@ path = Path('.[@flag="0"]/*') self.assertEqual('', path.select(xml).render()) - # FIXME: the following two don't work due to a problem in XML serialization: - # attributes that would need a namespace prefix that isn't in the - # prefix map would need to get an artificial prefix, but currently - # don't - # - #def test_attrname_with_namespace(self): - # xml = XML('') - # path = Path('foo[@f:bar]') - # print path - # namespaces = {'f': 'FOO'} - # self.assertEqual('', - # path.select(xml, namespaces=namespaces).render()) - # - #def test_attrwildcard_with_namespace(self): - # xml = XML('') - # path = Path('foo[@f:*]') - # namespaces = {'f': 'FOO'} - # self.assertEqual('', - # path.select(xml, namespaces=namespaces).render()) + def test_attrname_with_namespace(self): + xml = XML('') + path = Path('foo[@f:bar]') + self.assertEqual('', + path.select(xml, namespaces={'f': 'FOO'}).render()) + + def test_attrwildcard_with_namespace(self): + xml = XML('') + path = Path('foo[@f:*]') + self.assertEqual('', + path.select(xml, namespaces={'f': 'FOO'}).render()) def suite(): diff --git a/genshi/util.py b/genshi/util.py --- a/genshi/util.py +++ b/genshi/util.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2006 Edgewall Software +# Copyright (C) 2006-2007 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which @@ -16,6 +16,8 @@ import htmlentitydefs import re +__docformat__ = 'restructuredtext en' + class LRUCache(dict): """A dictionary-like object that stores only a certain number of items, and @@ -137,7 +139,9 @@ def flatten(items): - """Flattens a potentially nested sequence into a flat list: + """Flattens a potentially nested sequence into a flat list. + + :param items: the sequence to flatten >>> flatten((1, 2)) [1, 2] @@ -157,6 +161,21 @@ def plaintext(text, keeplinebreaks=True): """Returns the text as a `unicode` string with all entities and tags removed. + + >>> plaintext('1 < 2') + u'1 < 2' + + The `keeplinebreaks` parameter can be set to ``False`` to replace any line + breaks by simple spaces: + + >>> plaintext('''1 + ... < + ... 2''', keeplinebreaks=False) + u'1 < 2' + + :param text: the text to convert to plain text + :param keeplinebreaks: whether line breaks in the text should be kept intact + :return: the text with tags and entities removed """ text = stripentities(striptags(text)) if not keeplinebreaks: @@ -206,7 +225,7 @@ _STRIPTAGS_RE = re.compile(r'<[^>]*?>') def striptags(text): - """Return a copy of the text with all XML/HTML tags removed. + """Return a copy of the text with any XML/HTML tags removed. >>> striptags('Foo bar') 'Foo bar' @@ -214,5 +233,8 @@ 'Foo' >>> striptags('Foo
') 'Foo' + + :param text: the string to remove tags from + :return: the text with tags removed """ return _STRIPTAGS_RE.sub('', text) diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ from setuptools import setup except ImportError: from distutils.core import setup +import sys class build_doc(Command): @@ -34,7 +35,8 @@ def run(self): from docutils.core import publish_cmdline - conf = os.path.join('doc', 'docutils.conf') + docutils_conf = os.path.join('doc', 'docutils.conf') + epydoc_conf = os.path.join('doc', 'epydoc.conf') for source in glob('doc/*.txt'): dest = os.path.splitext(source)[0] + '.html' @@ -42,7 +44,23 @@ os.path.getmtime(dest) < os.path.getmtime(source): print 'building documentation file %s' % dest publish_cmdline(writer_name='html', - argv=['--config=%s' % conf, source, dest]) + argv=['--config=%s' % docutils_conf, source, + dest]) + + try: + from epydoc import cli + old_argv = sys.argv[1:] + sys.argv[1:] = [ + '--config=%s' % epydoc_conf, + '--no-private', # epydoc bug, not read from config + '--simple-term', + '--verbose' + ] + cli.cli() + sys.argv[1:] = old_argv + + except ImportError: + print 'epydoc not installed, skipping API documentation.' class test_doc(Command): @@ -63,13 +81,13 @@ setup( name = 'Genshi', - version = '0.4', + version = '0.5', description = 'A toolkit for stream-based generation of output for the web', long_description = \ -"""Genshi is a Python library that provides an integrated set of components -for parsing, generating, and processing HTML, XML or other textual content for -output generation on the web. The major feature is a template language, which -is heavily inspired by Kid.""", +"""Genshi is a Python library that provides an integrated set of +components for parsing, generating, and processing HTML, XML or +other textual content for output generation on the web. The major +feature is a template language, which is heavily inspired by Kid.""", author = 'Edgewall Software', author_email = 'info@edgewall.org', license = 'BSD', @@ -90,7 +108,7 @@ 'Topic :: Text Processing :: Markup :: XML' ], keywords = ['python.templating.engines'], - packages = ['genshi', 'genshi.template'], + packages = ['genshi', 'genshi.filters', 'genshi.template'], test_suite = 'genshi.tests.suite', extras_require = {'plugin': ['setuptools>=0.6a2']}, @@ -101,5 +119,5 @@ genshi-text = genshi.template.plugin:TextTemplateEnginePlugin[plugin] """, - cmdclass={'build_doc': build_doc, 'test_doc': test_doc} + cmdclass = {'build_doc': build_doc, 'test_doc': test_doc} )