Edgewall Software

source: tags/0.6.1/genshi/builder.py

Last change on this file was 1083, checked in by cmlenz, 14 years ago

A bit of cleanup of the Markup Python implementation.

  • Property svn:eol-style set to native
File size: 11.5 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2009 Edgewall Software
4# All rights reserved.
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://genshi.edgewall.org/wiki/License.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://genshi.edgewall.org/log/.
13
14"""Support for programmatically generating markup streams from Python code using
15a very simple syntax. The main entry point to this module is the `tag` object
16(which is actually an instance of the ``ElementFactory`` class). You should
17rarely (if ever) need to directly import and use any of the other classes in
18this module.
19
20Elements can be created using the `tag` object using attribute access. For
21example:
22
23>>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.')
24>>> doc
25<Element "p">
26
27This produces an `Element` instance which can be further modified to add child
28nodes and attributes. This is done by "calling" the element: positional
29arguments are added as child nodes (alternatively, the `Element.append` method
30can be used for that purpose), whereas keywords arguments are added as
31attributes:
32
33>>> doc(tag.br)
34<Element "p">
35>>> print(doc)
36<p>Some text and <a href="http://example.org/">a link</a>.<br/></p>
37
38If an attribute name collides with a Python keyword, simply append an underscore
39to the name:
40
41>>> doc(class_='intro')
42<Element "p">
43>>> print(doc)
44<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
45
46As shown above, an `Element` can easily be directly rendered to XML text by
47printing it or using the Python ``str()`` function. This is basically a
48shortcut for converting the `Element` to a stream and serializing that
49stream:
50
51>>> stream = doc.generate()
52>>> stream #doctest: +ELLIPSIS
53<genshi.core.Stream object at ...>
54>>> print(stream)
55<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
56
57
58The `tag` object also allows creating "fragments", which are basically lists
59of nodes (elements or text) that don't have a parent element. This can be useful
60for creating snippets of markup that are attached to a parent element later (for
61example in a template). Fragments are created by calling the `tag` object, which
62returns an object of type `Fragment`:
63
64>>> fragment = tag('Hello, ', tag.em('world'), '!')
65>>> fragment
66<Fragment>
67>>> print(fragment)
68Hello, <em>world</em>!
69"""
70
71from genshi.core import Attrs, Markup, Namespace, QName, Stream, \
72                        START, END, TEXT
73
74__all__ = ['Fragment', 'Element', 'ElementFactory', 'tag']
75__docformat__ = 'restructuredtext en'
76
77
78class Fragment(object):
79    """Represents a markup fragment, which is basically just a list of element
80    or text nodes.
81    """
82    __slots__ = ['children']
83
84    def __init__(self):
85        """Create a new fragment."""
86        self.children = []
87
88    def __add__(self, other):
89        return Fragment()(self, other)
90
91    def __call__(self, *args):
92        """Append any positional arguments as child nodes.
93       
94        :see: `append`
95        """
96        for arg in args:
97            self.append(arg)
98        return self
99
100    def __iter__(self):
101        return self._generate()
102
103    def __repr__(self):
104        return '<%s>' % type(self).__name__
105
106    def __str__(self):
107        return str(self.generate())
108
109    def __unicode__(self):
110        return unicode(self.generate())
111
112    def __html__(self):
113        return Markup(self.generate())
114
115    def append(self, node):
116        """Append an element or string as child node.
117       
118        :param node: the node to append; can be an `Element`, `Fragment`, or a
119                     `Stream`, or a Python string or number
120        """
121        if isinstance(node, (Stream, Element, basestring, int, float, long)):
122            # For objects of a known/primitive type, we avoid the check for
123            # whether it is iterable for better performance
124            self.children.append(node)
125        elif isinstance(node, Fragment):
126            self.children.extend(node.children)
127        elif node is not None:
128            try:
129                for child in node:
130                    self.append(child)
131            except TypeError:
132                self.children.append(node)
133
134    def _generate(self):
135        for child in self.children:
136            if isinstance(child, Fragment):
137                for event in child._generate():
138                    yield event
139            elif isinstance(child, Stream):
140                for event in child:
141                    yield event
142            else:
143                if not isinstance(child, basestring):
144                    child = unicode(child)
145                yield TEXT, child, (None, -1, -1)
146
147    def generate(self):
148        """Return a markup event stream for the fragment.
149       
150        :rtype: `Stream`
151        """
152        return Stream(self._generate())
153
154
155def _kwargs_to_attrs(kwargs):
156    attrs = []
157    names = set()
158    for name, value in kwargs.items():
159        name = name.rstrip('_').replace('_', '-')
160        if value is not None and name not in names:
161            attrs.append((QName(name), unicode(value)))
162            names.add(name)
163    return Attrs(attrs)
164
165
166class Element(Fragment):
167    """Simple XML output generator based on the builder pattern.
168
169    Construct XML elements by passing the tag name to the constructor:
170
171    >>> print(Element('strong'))
172    <strong/>
173
174    Attributes can be specified using keyword arguments. The values of the
175    arguments will be converted to strings and any special XML characters
176    escaped:
177
178    >>> print(Element('textarea', rows=10, cols=60))
179    <textarea rows="10" cols="60"/>
180    >>> print(Element('span', title='1 < 2'))
181    <span title="1 &lt; 2"/>
182    >>> print(Element('span', title='"baz"'))
183    <span title="&#34;baz&#34;"/>
184
185    The " character is escaped using a numerical entity.
186    The order in which attributes are rendered is undefined.
187
188    If an attribute value evaluates to `None`, that attribute is not included
189    in the output:
190
191    >>> print(Element('a', name=None))
192    <a/>
193
194    Attribute names that conflict with Python keywords can be specified by
195    appending an underscore:
196
197    >>> print(Element('div', class_='warning'))
198    <div class="warning"/>
199
200    Nested elements can be added to an element using item access notation.
201    The call notation can also be used for this and for adding attributes
202    using keyword arguments, as one would do in the constructor.
203
204    >>> print(Element('ul')(Element('li'), Element('li')))
205    <ul><li/><li/></ul>
206    >>> print(Element('a')('Label'))
207    <a>Label</a>
208    >>> print(Element('a')('Label', href="target"))
209    <a href="target">Label</a>
210
211    Text nodes can be nested in an element by adding strings instead of
212    elements. Any special characters in the strings are escaped automatically:
213
214    >>> print(Element('em')('Hello world'))
215    <em>Hello world</em>
216    >>> print(Element('em')(42))
217    <em>42</em>
218    >>> print(Element('em')('1 < 2'))
219    <em>1 &lt; 2</em>
220
221    This technique also allows mixed content:
222
223    >>> print(Element('p')('Hello ', Element('b')('world')))
224    <p>Hello <b>world</b></p>
225
226    Quotes are not escaped inside text nodes:
227    >>> print(Element('p')('"Hello"'))
228    <p>"Hello"</p>
229
230    Elements can also be combined with other elements or strings using the
231    addition operator, which results in a `Fragment` object that contains the
232    operands:
233   
234    >>> print(Element('br') + 'some text' + Element('br'))
235    <br/>some text<br/>
236   
237    Elements with a namespace can be generated using the `Namespace` and/or
238    `QName` classes:
239   
240    >>> from genshi.core import Namespace
241    >>> xhtml = Namespace('http://www.w3.org/1999/xhtml')
242    >>> print(Element(xhtml.html, lang='en'))
243    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
244    """
245    __slots__ = ['tag', 'attrib']
246
247    def __init__(self, tag_, **attrib):
248        Fragment.__init__(self)
249        self.tag = QName(tag_)
250        self.attrib = _kwargs_to_attrs(attrib)
251
252    def __call__(self, *args, **kwargs):
253        """Append any positional arguments as child nodes, and keyword arguments
254        as attributes.
255       
256        :return: the element itself so that calls can be chained
257        :rtype: `Element`
258        :see: `Fragment.append`
259        """
260        self.attrib |= _kwargs_to_attrs(kwargs)
261        Fragment.__call__(self, *args)
262        return self
263
264    def __repr__(self):
265        return '<%s "%s">' % (type(self).__name__, self.tag)
266
267    def _generate(self):
268        yield START, (self.tag, self.attrib), (None, -1, -1)
269        for kind, data, pos in Fragment._generate(self):
270            yield kind, data, pos
271        yield END, self.tag, (None, -1, -1)
272
273    def generate(self):
274        """Return a markup event stream for the fragment.
275       
276        :rtype: `Stream`
277        """
278        return Stream(self._generate())
279
280
281class ElementFactory(object):
282    """Factory for `Element` objects.
283   
284    A new element is created simply by accessing a correspondingly named
285    attribute of the factory object:
286   
287    >>> factory = ElementFactory()
288    >>> print(factory.foo)
289    <foo/>
290    >>> print(factory.foo(id=2))
291    <foo id="2"/>
292   
293    Markup fragments (lists of nodes without a parent element) can be created
294    by calling the factory:
295   
296    >>> print(factory('Hello, ', factory.em('world'), '!'))
297    Hello, <em>world</em>!
298   
299    A factory can also be bound to a specific namespace:
300   
301    >>> factory = ElementFactory('http://www.w3.org/1999/xhtml')
302    >>> print(factory.html(lang="en"))
303    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
304   
305    The namespace for a specific element can be altered on an existing factory
306    by specifying the new namespace using item access:
307   
308    >>> factory = ElementFactory()
309    >>> print(factory.html(factory['http://www.w3.org/2000/svg'].g(id=3)))
310    <html><g xmlns="http://www.w3.org/2000/svg" id="3"/></html>
311   
312    Usually, the `ElementFactory` class is not be used directly. Rather, the
313    `tag` instance should be used to create elements.
314    """
315
316    def __init__(self, namespace=None):
317        """Create the factory, optionally bound to the given namespace.
318       
319        :param namespace: the namespace URI for any created elements, or `None`
320                          for no namespace
321        """
322        if namespace and not isinstance(namespace, Namespace):
323            namespace = Namespace(namespace)
324        self.namespace = namespace
325
326    def __call__(self, *args):
327        """Create a fragment that has the given positional arguments as child
328        nodes.
329
330        :return: the created `Fragment`
331        :rtype: `Fragment`
332        """
333        return Fragment()(*args)
334
335    def __getitem__(self, namespace):
336        """Return a new factory that is bound to the specified namespace.
337       
338        :param namespace: the namespace URI or `Namespace` object
339        :return: an `ElementFactory` that produces elements bound to the given
340                 namespace
341        :rtype: `ElementFactory`
342        """
343        return ElementFactory(namespace)
344
345    def __getattr__(self, name):
346        """Create an `Element` with the given name.
347       
348        :param name: the tag name of the element to create
349        :return: an `Element` with the specified name
350        :rtype: `Element`
351        """
352        return Element(self.namespace and self.namespace[name] or name)
353
354
355tag = ElementFactory()
356"""Global `ElementFactory` bound to the default namespace.
357
358:type: `ElementFactory`
359"""
Note: See TracBrowser for help on using the repository browser.