Edgewall Software

source: tags/0.3.5/genshi/output.py

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

Ported [425] to 0.3.x.

  • Property svn:eol-style set to native
File size: 18.3 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([
187        QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
188        QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
189    ])
190
191    def __call__(self, stream):
192        namespace = self.NAMESPACE
193        ns_attrib = []
194        ns_mapping = {XML_NAMESPACE.uri: 'xml'}
195        boolean_attrs = self._BOOLEAN_ATTRS
196        empty_elems = self._EMPTY_ELEMS
197        have_doctype = False
198        in_cdata = False
199
200        stream = chain(self.preamble, stream)
201        for filter_ in self.filters:
202            stream = filter_(stream)
203        for kind, data, pos in stream:
204
205            if kind is START or kind is EMPTY:
206                tag, attrib = data
207
208                tagname = tag.localname
209                tagns = tag.namespace
210                if tagns:
211                    if tagns in ns_mapping:
212                        prefix = ns_mapping[tagns]
213                        if prefix:
214                            tagname = '%s:%s' % (prefix, tagname)
215                    else:
216                        ns_attrib.append((QName('xmlns'), tagns))
217                buf = ['<', tagname]
218
219                for attr, value in attrib + ns_attrib:
220                    attrname = attr.localname
221                    if attr.namespace:
222                        prefix = ns_mapping.get(attr.namespace)
223                        if prefix:
224                            attrname = '%s:%s' % (prefix, attrname)
225                    if attrname in boolean_attrs:
226                        if value:
227                            buf += [' ', attrname, '="', attrname, '"']
228                    else:
229                        buf += [' ', attrname, '="', escape(value), '"']
230                ns_attrib = []
231
232                if kind is EMPTY:
233                    if (tagns and tagns != namespace.uri) \
234                            or tag.localname in empty_elems:
235                        buf += [' />']
236                    else:
237                        buf += ['></%s>' % tagname]
238                else:
239                    buf += ['>']
240
241                yield Markup(''.join(buf))
242
243            elif kind is END:
244                tag = data
245                tagname = tag.localname
246                if tag.namespace:
247                    prefix = ns_mapping.get(tag.namespace)
248                    if prefix:
249                        tagname = '%s:%s' % (prefix, tagname)
250                yield Markup('</%s>' % tagname)
251
252            elif kind is TEXT:
253                if in_cdata:
254                    yield data
255                else:
256                    yield escape(data, quotes=False)
257
258            elif kind is COMMENT:
259                yield Markup('<!--%s-->' % data)
260
261            elif kind is DOCTYPE and not have_doctype:
262                name, pubid, sysid = data
263                buf = ['<!DOCTYPE %s']
264                if pubid:
265                    buf += [' PUBLIC "%s"']
266                elif sysid:
267                    buf += [' SYSTEM']
268                if sysid:
269                    buf += [' "%s"']
270                buf += ['>\n']
271                yield Markup(''.join(buf), *filter(None, data))
272                have_doctype = True
273
274            elif kind is START_NS:
275                prefix, uri = data
276                if uri not in ns_mapping:
277                    ns_mapping[uri] = prefix
278                    if not prefix:
279                        ns_attrib.append((QName('xmlns'), uri))
280                    else:
281                        ns_attrib.append((QName('xmlns:%s' % prefix), uri))
282
283            elif kind is START_CDATA:
284                yield Markup('<![CDATA[')
285                in_cdata = True
286
287            elif kind is END_CDATA:
288                yield Markup(']]>')
289                in_cdata = False
290
291            elif kind is PI:
292                yield Markup('<?%s %s?>' % data)
293
294
295class HTMLSerializer(XHTMLSerializer):
296    """Produces HTML text from an event stream.
297   
298    >>> from genshi.builder import tag
299    >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
300    >>> print ''.join(HTMLSerializer()(elem.generate()))
301    <div><a href="foo"></a><br><hr noshade></div>
302    """
303
304    _NOESCAPE_ELEMS = frozenset([QName('script'),
305                                 QName('http://www.w3.org/1999/xhtml}script'),
306                                 QName('style'),
307                                 QName('http://www.w3.org/1999/xhtml}style')])
308
309    def __init__(self, doctype=None, strip_whitespace=True):
310        """Initialize the HTML serializer.
311       
312        @param doctype: a `(name, pubid, sysid)` tuple that represents the
313            DOCTYPE declaration that should be included at the top of the
314            generated output
315        @param strip_whitespace: whether extraneous whitespace should be
316            stripped from the output
317        """
318        super(HTMLSerializer, self).__init__(doctype, False)
319        if strip_whitespace:
320            self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
321                                                 self._NOESCAPE_ELEMS, True))
322
323    def __call__(self, stream):
324        namespace = self.NAMESPACE
325        ns_mapping = {}
326        boolean_attrs = self._BOOLEAN_ATTRS
327        empty_elems = self._EMPTY_ELEMS
328        noescape_elems = self._NOESCAPE_ELEMS
329        have_doctype = False
330        noescape = False
331
332        stream = chain(self.preamble, stream)
333        for filter_ in self.filters:
334            stream = filter_(stream)
335        for kind, data, pos in stream:
336
337            if kind is START or kind is EMPTY:
338                tag, attrib = data
339                if not tag.namespace or tag in namespace:
340                    tagname = tag.localname
341                    buf = ['<', tagname]
342
343                    for attr, value in attrib:
344                        attrname = attr.localname
345                        if not attr.namespace or attr in namespace:
346                            if attrname in boolean_attrs:
347                                if value:
348                                    buf += [' ', attrname]
349                            else:
350                                buf += [' ', attrname, '="', escape(value), '"']
351
352                    buf += ['>']
353
354                    if kind is EMPTY:
355                        if tagname not in empty_elems:
356                            buf += ['</%s>' % tagname]
357
358                    yield Markup(''.join(buf))
359
360                    if tagname in noescape_elems:
361                        noescape = True
362
363            elif kind is END:
364                tag = data
365                if not tag.namespace or tag in namespace:
366                    yield Markup('</%s>' % tag.localname)
367
368                noescape = False
369
370            elif kind is TEXT:
371                if noescape:
372                    yield data
373                else:
374                    yield escape(data, quotes=False)
375
376            elif kind is COMMENT:
377                yield Markup('<!--%s-->' % data)
378
379            elif kind is DOCTYPE and not have_doctype:
380                name, pubid, sysid = data
381                buf = ['<!DOCTYPE %s']
382                if pubid:
383                    buf += [' PUBLIC "%s"']
384                elif sysid:
385                    buf += [' SYSTEM']
386                if sysid:
387                    buf += [' "%s"']
388                buf += ['>\n']
389                yield Markup(''.join(buf), *filter(None, data))
390                have_doctype = True
391
392            elif kind is START_NS and data[1] not in ns_mapping:
393                ns_mapping[data[1]] = data[0]
394
395            elif kind is PI:
396                yield Markup('<?%s %s?>' % data)
397
398
399class TextSerializer(object):
400    """Produces plain text from an event stream.
401   
402    Only text events are included in the output. Unlike the other serializer,
403    special XML characters are not escaped:
404   
405    >>> from genshi.builder import tag
406    >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
407    >>> print elem
408    <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
409    >>> print ''.join(TextSerializer()(elem.generate()))
410    <Hello!>
411
412    If text events contain literal markup (instances of the `Markup` class),
413    tags or entities are stripped from the output:
414   
415    >>> elem = tag.div(Markup('<a href="foo">Hello!</a><br/>'))
416    >>> print elem
417    <div><a href="foo">Hello!</a><br/></div>
418    >>> print ''.join(TextSerializer()(elem.generate()))
419    Hello!
420    """
421
422    def __call__(self, stream):
423        for kind, data, pos in stream:
424            if kind is TEXT:
425                if type(data) is Markup:
426                    data = data.striptags().stripentities()
427                yield unicode(data)
428
429
430class EmptyTagFilter(object):
431    """Combines `START` and `STOP` events into `EMPTY` events for elements that
432    have no contents.
433    """
434
435    EMPTY = StreamEventKind('EMPTY')
436
437    def __call__(self, stream):
438        prev = (None, None, None)
439        for kind, data, pos in stream:
440            if prev[0] is START:
441                if kind is END:
442                    prev = EMPTY, prev[1], prev[2]
443                    yield prev
444                    continue
445                else:
446                    yield prev
447            if kind is not START:
448                yield kind, data, pos
449            prev = kind, data, pos
450
451
452EMPTY = EmptyTagFilter.EMPTY
453
454
455class WhitespaceFilter(object):
456    """A filter that removes extraneous ignorable white space from the
457    stream."""
458
459    def __init__(self, preserve=None, noescape=None, escape_cdata=False):
460        """Initialize the filter.
461       
462        @param preserve: a set or sequence of tag names for which white-space
463            should be ignored.
464        @param noescape: a set or sequence of tag names for which text content
465            should not be escaped
466       
467        The `noescape` set is expected to refer to elements that cannot contain
468        further child elements (such as <style> or <script> in HTML documents).
469        """
470        if preserve is None:
471            preserve = []
472        self.preserve = frozenset(preserve)
473        if noescape is None:
474            noescape = []
475        self.noescape = frozenset(noescape)
476        self.escape_cdata = escape_cdata
477
478    def __call__(self, stream, ctxt=None, space=XML_NAMESPACE['space'],
479                 trim_trailing_space=re.compile('[ \t]+(?=\n)').sub,
480                 collapse_lines=re.compile('\n{2,}').sub):
481        mjoin = Markup('').join
482        preserve_elems = self.preserve
483        preserve = 0
484        noescape_elems = self.noescape
485        noescape = False
486        escape_cdata = self.escape_cdata
487
488        textbuf = []
489        push_text = textbuf.append
490        pop_text = textbuf.pop
491        for kind, data, pos in chain(stream, [(None, None, None)]):
492            if kind is TEXT:
493                if noescape:
494                    data = Markup(data)
495                push_text(data)
496            else:
497                if textbuf:
498                    if len(textbuf) > 1:
499                        text = mjoin(textbuf, escape_quotes=False)
500                        del textbuf[:]
501                    else:
502                        text = escape(pop_text(), quotes=False)
503                    if not preserve:
504                        text = collapse_lines('\n', trim_trailing_space('', text))
505                    yield TEXT, Markup(text), pos
506
507                if kind is START:
508                    tag, attrs = data
509                    if preserve or (tag in preserve_elems or
510                                    attrs.get(space) == 'preserve'):
511                        preserve += 1
512                    if not noescape and tag in noescape_elems:
513                        noescape = True
514
515                elif kind is END:
516                    noescape = False
517                    if preserve:
518                        preserve -= 1
519
520                elif kind is START_CDATA and not escape_cdata:
521                    noescape = True
522
523                elif kind is END_CDATA and not escape_cdata:
524                    noescape = False
525
526                if kind:
527                    yield kind, data, pos
Note: See TracBrowser for help on using the repository browser.