Edgewall Software

source: branches/stable/0.5.x/genshi/builder.py

Last change on this file was 865, checked in by cmlenz, 15 years ago

Updated copyright years.

  • Property svn:eol-style set to native
File size: 11.5 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2008 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
71try:
72    set
73except NameError:
74    from sets import Set as set
75
76from genshi.core import Attrs, Markup, Namespace, QName, Stream, \
77                        START, END, TEXT
78
79__all__ = ['Fragment', 'Element', 'ElementFactory', 'tag']
80__docformat__ = 'restructuredtext en'
81
82
83class Fragment(object):
84    """Represents a markup fragment, which is basically just a list of element
85    or text nodes.
86    """
87    __slots__ = ['children']
88
89    def __init__(self):
90        """Create a new fragment."""
91        self.children = []
92
93    def __add__(self, other):
94        return Fragment()(self, other)
95
96    def __call__(self, *args):
97        """Append any positional arguments as child nodes.
98       
99        :see: `append`
100        """
101        map(self.append, args)
102        return self
103
104    def __iter__(self):
105        return self._generate()
106
107    def __repr__(self):
108        return '<%s>' % self.__class__.__name__
109
110    def __str__(self):
111        return str(self.generate())
112
113    def __unicode__(self):
114        return unicode(self.generate())
115
116    def __html__(self):
117        return Markup(self.generate())
118
119    def append(self, node):
120        """Append an element or string as child node.
121       
122        :param node: the node to append; can be an `Element`, `Fragment`, or a
123                     `Stream`, or a Python string or number
124        """
125        if isinstance(node, (Stream, Element, basestring, int, float, long)):
126            # For objects of a known/primitive type, we avoid the check for
127            # whether it is iterable for better performance
128            self.children.append(node)
129        elif isinstance(node, Fragment):
130            self.children.extend(node.children)
131        elif node is not None:
132            try:
133                map(self.append, iter(node))
134            except TypeError:
135                self.children.append(node)
136
137    def _generate(self):
138        for child in self.children:
139            if isinstance(child, Fragment):
140                for event in child._generate():
141                    yield event
142            elif isinstance(child, Stream):
143                for event in child:
144                    yield event
145            else:
146                if not isinstance(child, basestring):
147                    child = unicode(child)
148                yield TEXT, child, (None, -1, -1)
149
150    def generate(self):
151        """Return a markup event stream for the fragment.
152       
153        :rtype: `Stream`
154        """
155        return Stream(self._generate())
156
157
158def _kwargs_to_attrs(kwargs):
159    attrs = []
160    names = set()
161    for name, value in kwargs.items():
162        name = name.rstrip('_').replace('_', '-')
163        if value is not None and name not in names:
164            attrs.append((QName(name), unicode(value)))
165            names.add(name)
166    return Attrs(attrs)
167
168
169class Element(Fragment):
170    """Simple XML output generator based on the builder pattern.
171
172    Construct XML elements by passing the tag name to the constructor:
173
174    >>> print Element('strong')
175    <strong/>
176
177    Attributes can be specified using keyword arguments. The values of the
178    arguments will be converted to strings and any special XML characters
179    escaped:
180
181    >>> print Element('textarea', rows=10, cols=60)
182    <textarea rows="10" cols="60"/>
183    >>> print Element('span', title='1 < 2')
184    <span title="1 &lt; 2"/>
185    >>> print Element('span', title='"baz"')
186    <span title="&#34;baz&#34;"/>
187
188    The " character is escaped using a numerical entity.
189    The order in which attributes are rendered is undefined.
190
191    If an attribute value evaluates to `None`, that attribute is not included
192    in the output:
193
194    >>> print Element('a', name=None)
195    <a/>
196
197    Attribute names that conflict with Python keywords can be specified by
198    appending an underscore:
199
200    >>> print Element('div', class_='warning')
201    <div class="warning"/>
202
203    Nested elements can be added to an element using item access notation.
204    The call notation can also be used for this and for adding attributes
205    using keyword arguments, as one would do in the constructor.
206
207    >>> print Element('ul')(Element('li'), Element('li'))
208    <ul><li/><li/></ul>
209    >>> print Element('a')('Label')
210    <a>Label</a>
211    >>> print Element('a')('Label', href="target")
212    <a href="target">Label</a>
213
214    Text nodes can be nested in an element by adding strings instead of
215    elements. Any special characters in the strings are escaped automatically:
216
217    >>> print Element('em')('Hello world')
218    <em>Hello world</em>
219    >>> print Element('em')(42)
220    <em>42</em>
221    >>> print Element('em')('1 < 2')
222    <em>1 &lt; 2</em>
223
224    This technique also allows mixed content:
225
226    >>> print Element('p')('Hello ', Element('b')('world'))
227    <p>Hello <b>world</b></p>
228
229    Quotes are not escaped inside text nodes:
230    >>> print Element('p')('"Hello"')
231    <p>"Hello"</p>
232
233    Elements can also be combined with other elements or strings using the
234    addition operator, which results in a `Fragment` object that contains the
235    operands:
236   
237    >>> print Element('br') + 'some text' + Element('br')
238    <br/>some text<br/>
239   
240    Elements with a namespace can be generated using the `Namespace` and/or
241    `QName` classes:
242   
243    >>> from genshi.core import Namespace
244    >>> xhtml = Namespace('http://www.w3.org/1999/xhtml')
245    >>> print Element(xhtml.html, lang='en')
246    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
247    """
248    __slots__ = ['tag', 'attrib']
249
250    def __init__(self, tag_, **attrib):
251        Fragment.__init__(self)
252        self.tag = QName(tag_)
253        self.attrib = _kwargs_to_attrs(attrib)
254
255    def __call__(self, *args, **kwargs):
256        """Append any positional arguments as child nodes, and keyword arguments
257        as attributes.
258       
259        :return: the element itself so that calls can be chained
260        :rtype: `Element`
261        :see: `Fragment.append`
262        """
263        self.attrib |= _kwargs_to_attrs(kwargs)
264        Fragment.__call__(self, *args)
265        return self
266
267    def __repr__(self):
268        return '<%s "%s">' % (self.__class__.__name__, self.tag)
269
270    def _generate(self):
271        yield START, (self.tag, self.attrib), (None, -1, -1)
272        for kind, data, pos in Fragment._generate(self):
273            yield kind, data, pos
274        yield END, self.tag, (None, -1, -1)
275
276    def generate(self):
277        """Return a markup event stream for the fragment.
278       
279        :rtype: `Stream`
280        """
281        return Stream(self._generate())
282
283
284class ElementFactory(object):
285    """Factory for `Element` objects.
286   
287    A new element is created simply by accessing a correspondingly named
288    attribute of the factory object:
289   
290    >>> factory = ElementFactory()
291    >>> print factory.foo
292    <foo/>
293    >>> print factory.foo(id=2)
294    <foo id="2"/>
295   
296    Markup fragments (lists of nodes without a parent element) can be created
297    by calling the factory:
298   
299    >>> print factory('Hello, ', factory.em('world'), '!')
300    Hello, <em>world</em>!
301   
302    A factory can also be bound to a specific namespace:
303   
304    >>> factory = ElementFactory('http://www.w3.org/1999/xhtml')
305    >>> print factory.html(lang="en")
306    <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
307   
308    The namespace for a specific element can be altered on an existing factory
309    by specifying the new namespace using item access:
310   
311    >>> factory = ElementFactory()
312    >>> print factory.html(factory['http://www.w3.org/2000/svg'].g(id=3))
313    <html><g xmlns="http://www.w3.org/2000/svg" id="3"/></html>
314   
315    Usually, the `ElementFactory` class is not be used directly. Rather, the
316    `tag` instance should be used to create elements.
317    """
318
319    def __init__(self, namespace=None):
320        """Create the factory, optionally bound to the given namespace.
321       
322        :param namespace: the namespace URI for any created elements, or `None`
323                          for no namespace
324        """
325        if namespace and not isinstance(namespace, Namespace):
326            namespace = Namespace(namespace)
327        self.namespace = namespace
328
329    def __call__(self, *args):
330        """Create a fragment that has the given positional arguments as child
331        nodes.
332
333        :return: the created `Fragment`
334        :rtype: `Fragment`
335        """
336        return Fragment()(*args)
337
338    def __getitem__(self, namespace):
339        """Return a new factory that is bound to the specified namespace.
340       
341        :param namespace: the namespace URI or `Namespace` object
342        :return: an `ElementFactory` that produces elements bound to the given
343                 namespace
344        :rtype: `ElementFactory`
345        """
346        return ElementFactory(namespace)
347
348    def __getattr__(self, name):
349        """Create an `Element` with the given name.
350       
351        :param name: the tag name of the element to create
352        :return: an `Element` with the specified name
353        :rtype: `Element`
354        """
355        return Element(self.namespace and self.namespace[name] or name)
356
357
358tag = ElementFactory()
359"""Global `ElementFactory` bound to the default namespace.
360
361:type: `ElementFactory`
362"""
Note: See TracBrowser for help on using the repository browser.