Edgewall Software

source: tags/0.3.1/genshi/output.py

Last change on this file was 299, checked in by cmlenz, 17 years ago

Fixed EOL style.

  • Property svn:eol-style set to native
File size: 17.9 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 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"""This module provides different kinds of serialization methods for XML event
15streams.
16"""
17
18from itertools import chain
19try:
20    frozenset
21except NameError:
22    from sets import ImmutableSet as frozenset
23import re
24
25from genshi.core import escape, Markup, Namespace, QName, StreamEventKind
26from genshi.core import DOCTYPE, START, END, START_NS, TEXT, START_CDATA, \
27                        END_CDATA, PI, COMMENT, XML_NAMESPACE
28
29__all__ = ['DocType', 'XMLSerializer', 'XHTMLSerializer', 'HTMLSerializer',
30           'TextSerializer']
31
32
33class DocType(object):
34    """Defines a number of commonly used DOCTYPE declarations as constants."""
35
36    HTML_STRICT = ('html', '-//W3C//DTD HTML 4.01//EN',
37                   'http://www.w3.org/TR/html4/strict.dtd')
38    HTML_TRANSITIONAL = ('html', '-//W3C//DTD HTML 4.01 Transitional//EN',
39                         'http://www.w3.org/TR/html4/loose.dtd')
40    HTML = HTML_STRICT
41
42    XHTML_STRICT = ('html', '-//W3C//DTD XHTML 1.0 Strict//EN',
43                    'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd')
44    XHTML_TRANSITIONAL = ('html', '-//W3C//DTD XHTML 1.0 Transitional//EN',
45                          'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd')
46    XHTML = XHTML_STRICT
47
48
49class XMLSerializer(object):
50    """Produces XML text from an event stream.
51   
52    >>> from genshi.builder import tag
53    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
54    >>> print ''.join(XMLSerializer()(elem.generate()))
55    <div><a href="foo"/><br/><hr noshade="True"/></div>
56    """
57
58    _PRESERVE_SPACE = frozenset()
59
60    def __init__(self, doctype=None, strip_whitespace=True):
61        """Initialize the XML serializer.
62       
63        @param doctype: a `(name, pubid, sysid)` tuple that represents the
64            DOCTYPE declaration that should be included at the top of the
65            generated output
66        @param strip_whitespace: whether extraneous whitespace should be
67            stripped from the output
68        """
69        self.preamble = []
70        if doctype:
71            self.preamble.append((DOCTYPE, doctype, (None, -1, -1)))
72        self.filters = [EmptyTagFilter()]
73        if strip_whitespace:
74            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
75
76    def __call__(self, stream):
77        ns_attrib = []
78        ns_mapping = {XML_NAMESPACE.uri: 'xml'}
79        have_doctype = False
80        in_cdata = False
81
82        stream = chain(self.preamble, stream)
83        for filter_ in self.filters:
84            stream = filter_(stream)
85        for kind, data, pos in stream:
86
87            if kind is START or kind is EMPTY:
88                tag, attrib = data
89
90                tagname = tag.localname
91                namespace = tag.namespace
92                if namespace:
93                    if namespace in ns_mapping:
94                        prefix = ns_mapping[namespace]
95                        if prefix:
96                            tagname = '%s:%s' % (prefix, tagname)
97                    else:
98                        ns_attrib.append((QName('xmlns'), namespace))
99                buf = ['<', tagname]
100
101                for attr, value in attrib + ns_attrib:
102                    attrname = attr.localname
103                    if attr.namespace:
104                        prefix = ns_mapping.get(attr.namespace)
105                        if prefix:
106                            attrname = '%s:%s' % (prefix, attrname)
107                    buf += [' ', attrname, '="', escape(value), '"']
108                ns_attrib = []
109
110                if kind is EMPTY:
111                    buf += ['/>']
112                else:
113                    buf += ['>']
114
115                yield Markup(''.join(buf))
116
117            elif kind is END:
118                tag = data
119                tagname = tag.localname
120                if tag.namespace:
121                    prefix = ns_mapping.get(tag.namespace)
122                    if prefix:
123                        tagname = '%s:%s' % (prefix, tag.localname)
124                yield Markup('</%s>' % tagname)
125
126            elif kind is TEXT:
127                if in_cdata:
128                    yield data
129                else:
130                    yield escape(data, quotes=False)
131
132            elif kind is COMMENT:
133                yield Markup('<!--%s-->' % data)
134
135            elif kind is DOCTYPE and not have_doctype:
136                name, pubid, sysid = data
137                buf = ['<!DOCTYPE %s']
138                if pubid:
139                    buf += [' PUBLIC "%s"']
140                elif sysid:
141                    buf += [' SYSTEM']
142                if sysid:
143                    buf += [' "%s"']
144                buf += ['>\n']
145                yield Markup(''.join(buf), *filter(None, data))
146                have_doctype = True
147
148            elif kind is START_NS:
149                prefix, uri = data
150                if uri not in ns_mapping:
151                    ns_mapping[uri] = prefix
152                    if not prefix:
153                        ns_attrib.append((QName('xmlns'), uri))
154                    else:
155                        ns_attrib.append((QName('xmlns:%s' % prefix), uri))
156
157            elif kind is START_CDATA:
158                yield Markup('<![CDATA[')
159                in_cdata = True
160
161            elif kind is END_CDATA:
162                yield Markup(']]>')
163                in_cdata = False
164
165            elif kind is PI:
166                yield Markup('<?%s %s?>' % data)
167
168
169class XHTMLSerializer(XMLSerializer):
170    """Produces XHTML text from an event stream.
171   
172    >>> from genshi.builder import tag
173    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
174    >>> print ''.join(XHTMLSerializer()(elem.generate()))
175    <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
176    """
177
178    NAMESPACE = Namespace('http://www.w3.org/1999/xhtml')
179
180    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
181                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
182                              'param'])
183    _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
184                                'defer', 'disabled', 'ismap', 'multiple',
185                                'nohref', 'noresize', 'noshade', 'nowrap'])
186    _PRESERVE_SPACE = frozenset([QName('pre'), QName('textarea')])
187
188    def __call__(self, stream):
189        namespace = self.NAMESPACE
190        ns_attrib = []
191        ns_mapping = {XML_NAMESPACE.uri: 'xml'}
192        boolean_attrs = self._BOOLEAN_ATTRS
193        empty_elems = self._EMPTY_ELEMS
194        have_doctype = False
195        in_cdata = False
196
197        stream = chain(self.preamble, stream)
198        for filter_ in self.filters:
199            stream = filter_(stream)
200        for kind, data, pos in stream:
201
202            if kind is START or kind is EMPTY:
203                tag, attrib = data
204
205                tagname = tag.localname
206                tagns = tag.namespace
207                if tagns:
208                    if tagns in ns_mapping:
209                        prefix = ns_mapping[tagns]
210                        if prefix:
211                            tagname = '%s:%s' % (prefix, tagname)
212                    else:
213                        ns_attrib.append((QName('xmlns'), tagns))
214                buf = ['<', tagname]
215
216                for attr, value in attrib + ns_attrib:
217                    attrname = attr.localname
218                    if attr.namespace:
219                        prefix = ns_mapping.get(attr.namespace)
220                        if prefix:
221                            attrname = '%s:%s' % (prefix, attrname)
222                    if attrname in boolean_attrs:
223                        if value:
224                            buf += [' ', attrname, '="', attrname, '"']
225                    else:
226                        buf += [' ', attrname, '="', escape(value), '"']
227                ns_attrib = []
228
229                if kind is EMPTY:
230                    if (tagns and tagns != namespace.uri) \
231                            or tag.localname in empty_elems:
232                        buf += [' />']
233                    else:
234                        buf += ['></%s>' % tagname]
235                else:
236                    buf += ['>']
237
238                yield Markup(''.join(buf))
239
240            elif kind is END:
241                tag = data
242                tagname = tag.localname
243                if tag.namespace:
244                    prefix = ns_mapping.get(tag.namespace)
245                    if prefix:
246                        tagname = '%s:%s' % (prefix, tagname)
247                yield Markup('</%s>' % tagname)
248
249            elif kind is TEXT:
250                if in_cdata:
251                    yield data
252                else:
253                    yield escape(data, quotes=False)
254
255            elif kind is COMMENT:
256                yield Markup('<!--%s-->' % data)
257
258            elif kind is DOCTYPE and not have_doctype:
259                name, pubid, sysid = data
260                buf = ['<!DOCTYPE %s']
261                if pubid:
262                    buf += [' PUBLIC "%s"']
263                elif sysid:
264                    buf += [' SYSTEM']
265                if sysid:
266                    buf += [' "%s"']
267                buf += ['>\n']
268                yield Markup(''.join(buf), *filter(None, data))
269                have_doctype = True
270
271            elif kind is START_NS:
272                prefix, uri = data
273                if uri not in ns_mapping:
274                    ns_mapping[uri] = prefix
275                    if not prefix:
276                        ns_attrib.append((QName('xmlns'), uri))
277                    else:
278                        ns_attrib.append((QName('xmlns:%s' % prefix), uri))
279
280            elif kind is START_CDATA:
281                yield Markup('<![CDATA[')
282                in_cdata = True
283
284            elif kind is END_CDATA:
285                yield Markup(']]>')
286                in_cdata = False
287
288            elif kind is PI:
289                yield Markup('<?%s %s?>' % data)
290
291
292class HTMLSerializer(XHTMLSerializer):
293    """Produces HTML text from an event stream.
294   
295    >>> from genshi.builder import tag
296    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
297    >>> print ''.join(HTMLSerializer()(elem.generate()))
298    <div><a href="foo"></a><br><hr noshade></div>
299    """
300
301    _NOESCAPE_ELEMS = frozenset([QName('script'), QName('style')])
302
303    def __init__(self, doctype=None, strip_whitespace=True):
304        """Initialize the HTML serializer.
305       
306        @param doctype: a `(name, pubid, sysid)` tuple that represents the
307            DOCTYPE declaration that should be included at the top of the
308            generated output
309        @param strip_whitespace: whether extraneous whitespace should be
310            stripped from the output
311        """
312        super(HTMLSerializer, self).__init__(doctype, False)
313        if strip_whitespace:
314            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
315                                                 self._NOESCAPE_ELEMS, True))
316
317    def __call__(self, stream):
318        namespace = self.NAMESPACE
319        ns_mapping = {}
320        boolean_attrs = self._BOOLEAN_ATTRS
321        empty_elems = self._EMPTY_ELEMS
322        noescape_elems = self._NOESCAPE_ELEMS
323        have_doctype = False
324        noescape = False
325
326        stream = chain(self.preamble, stream)
327        for filter_ in self.filters:
328            stream = filter_(stream)
329        for kind, data, pos in stream:
330
331            if kind is START or kind is EMPTY:
332                tag, attrib = data
333                if not tag.namespace or tag in namespace:
334                    tagname = tag.localname
335                    buf = ['<', tagname]
336
337                    for attr, value in attrib:
338                        attrname = attr.localname
339                        if not attr.namespace or attr in namespace:
340                            if attrname in boolean_attrs:
341                                if value:
342                                    buf += [' ', attrname]
343                            else:
344                                buf += [' ', attrname, '="', escape(value), '"']
345
346                    buf += ['>']
347
348                    if kind is EMPTY:
349                        if tagname not in empty_elems:
350                            buf += ['</%s>' % tagname]
351
352                    yield Markup(''.join(buf))
353
354                    if tagname in noescape_elems:
355                        noescape = True
356
357            elif kind is END:
358                tag = data
359                if not tag.namespace or tag in namespace:
360                    yield Markup('</%s>' % tag.localname)
361
362                noescape = False
363
364            elif kind is TEXT:
365                if noescape:
366                    yield data
367                else:
368                    yield escape(data, quotes=False)
369
370            elif kind is COMMENT:
371                yield Markup('<!--%s-->' % data)
372
373            elif kind is DOCTYPE and not have_doctype:
374                name, pubid, sysid = data
375                buf = ['<!DOCTYPE %s']
376                if pubid:
377                    buf += [' PUBLIC "%s"']
378                elif sysid:
379                    buf += [' SYSTEM']
380                if sysid:
381                    buf += [' "%s"']
382                buf += ['>\n']
383                yield Markup(''.join(buf), *filter(None, data))
384                have_doctype = True
385
386            elif kind is START_NS and data[1] not in ns_mapping:
387                ns_mapping[data[1]] = data[0]
388
389            elif kind is PI:
390                yield Markup('<?%s %s?>' % data)
391
392
393class TextSerializer(object):
394    """Produces plain text from an event stream.
395   
396    Only text events are included in the output. Unlike the other serializer,
397    special XML characters are not escaped:
398   
399    >>> from genshi.builder import tag
400    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
401    >>> print elem
402    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
403    >>> print ''.join(TextSerializer()(elem.generate()))
404    <Hello!>
405
406    If text events contain literal markup (instances of the `Markup` class),
407    tags or entities are stripped from the output:
408   
409    >>> elem = tag.div(Markup('<a href="foo">Hello!</a><br/>'))
410    >>> print elem
411    <div><a href="foo">Hello!</a><br/></div>
412    >>> print ''.join(TextSerializer()(elem.generate()))
413    Hello!
414    """
415
416    def __call__(self, stream):
417        for kind, data, pos in stream:
418            if kind is TEXT:
419                if type(data) is Markup:
420                    data = data.striptags().stripentities()
421                yield unicode(data)
422
423
424class EmptyTagFilter(object):
425    """Combines `START` and `STOP` events into `EMPTY` events for elements that
426    have no contents.
427    """
428
429    EMPTY = StreamEventKind('EMPTY')
430
431    def __call__(self, stream):
432        prev = (None, None, None)
433        for kind, data, pos in stream:
434            if prev[0] is START:
435                if kind is END:
436                    prev = EMPTY, prev[1], prev[2]
437                    yield prev
438                    continue
439                else:
440                    yield prev
441            if kind is not START:
442                yield kind, data, pos
443            prev = kind, data, pos
444
445
446EMPTY = EmptyTagFilter.EMPTY
447
448
449class WhitespaceFilter(object):
450    """A filter that removes extraneous ignorable white space from the
451    stream."""
452
453    def __init__(self, preserve=None, noescape=None, escape_cdata=False):
454        """Initialize the filter.
455       
456        @param preserve: a set or sequence of tag names for which white-space
457            should be ignored.
458        @param noescape: a set or sequence of tag names for which text content
459            should not be escaped
460       
461        Both the `preserve` and `noescape` sets are expected to refer to
462        elements that cannot contain further child elements.
463        """
464        if preserve is None:
465            preserve = []
466        self.preserve = frozenset(preserve)
467        if noescape is None:
468            noescape = []
469        self.noescape = frozenset(noescape)
470        self.escape_cdata = escape_cdata
471
472    def __call__(self, stream, ctxt=None, space=XML_NAMESPACE['space'],
473                 trim_trailing_space=re.compile('[ \t]+(?=\n)').sub,
474                 collapse_lines=re.compile('\n{2,}').sub):
475        mjoin = Markup('').join
476        preserve_elems = self.preserve
477        preserve = False
478        noescape_elems = self.noescape
479        noescape = False
480        escape_cdata = self.escape_cdata
481
482        textbuf = []
483        push_text = textbuf.append
484        pop_text = textbuf.pop
485        for kind, data, pos in chain(stream, [(None, None, None)]):
486            if kind is TEXT:
487                if noescape:
488                    data = Markup(data)
489                push_text(data)
490            else:
491                if textbuf:
492                    if len(textbuf) > 1:
493                        text = mjoin(textbuf, escape_quotes=False)
494                        del textbuf[:]
495                    else:
496                        text = escape(pop_text(), quotes=False)
497                    if not preserve:
498                        text = collapse_lines('\n', trim_trailing_space('', text))
499                    yield TEXT, Markup(text), pos
500
501                if kind is START:
502                    tag, attrib = data
503                    if not preserve and (tag in preserve_elems or
504                                         attrib.get(space) == 'preserve'):
505                        preserve = True
506                    if not noescape and tag in noescape_elems:
507                        noescape = True
508
509                elif kind is END:
510                    preserve = noescape = False
511
512                elif kind is START_CDATA and not escape_cdata:
513                    noescape = True
514
515                elif kind is END_CDATA and not escape_cdata:
516                    noescape = False
517
518                if kind:
519                    yield kind, data, pos
Note: See TracBrowser for help on using the repository browser.