Edgewall Software

source: tags/0.3.1/genshi/input.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: 11.4 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
14from itertools import chain
15from xml.parsers import expat
16try:
17    frozenset
18except NameError:
19    from sets import ImmutableSet as frozenset
20import HTMLParser as html
21import htmlentitydefs
22from StringIO import StringIO
23
24from genshi.core import Attrs, QName, Stream
25from genshi.core import DOCTYPE, START, END, START_NS, END_NS, TEXT, \
26                        START_CDATA, END_CDATA, PI, COMMENT
27
28__all__ = ['ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML']
29
30
31class ParseError(Exception):
32    """Exception raised when fatal syntax errors are found in the input being
33    parsed."""
34
35    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
36        Exception.__init__(self, message)
37        self.msg = message
38        self.filename = filename
39        self.lineno = lineno
40        self.offset = offset
41
42
43class XMLParser(object):
44    """Generator-based XML parser based on roughly equivalent code in
45    Kid/ElementTree.
46   
47    The parsing is initiated by iterating over the parser object:
48   
49    >>> parser = XMLParser(StringIO('<root id="2"><child>Foo</child></root>'))
50    >>> for kind, data, pos in parser:
51    ...     print kind, data
52    START (u'root', [(u'id', u'2')])
53    START (u'child', [])
54    TEXT Foo
55    END child
56    END root
57    """
58
59    def __init__(self, source, filename=None):
60        """Initialize the parser for the given XML text.
61       
62        @param source: the XML text as a file-like object
63        @param filename: the name of the file, if appropriate
64        """
65        self.source = source
66        self.filename = filename
67
68        # Setup the Expat parser
69        parser = expat.ParserCreate('utf-8', '}')
70        parser.buffer_text = True
71        parser.returns_unicode = True
72        parser.ordered_attributes = True
73
74        parser.StartElementHandler = self._handle_start
75        parser.EndElementHandler = self._handle_end
76        parser.CharacterDataHandler = self._handle_data
77        parser.StartDoctypeDeclHandler = self._handle_doctype
78        parser.StartNamespaceDeclHandler = self._handle_start_ns
79        parser.EndNamespaceDeclHandler = self._handle_end_ns
80        parser.StartCdataSectionHandler = self._handle_start_cdata
81        parser.EndCdataSectionHandler = self._handle_end_cdata
82        parser.ProcessingInstructionHandler = self._handle_pi
83        parser.CommentHandler = self._handle_comment
84
85        # Tell Expat that we'll handle non-XML entities ourselves
86        # (in _handle_other)
87        parser.DefaultHandler = self._handle_other
88        parser.UseForeignDTD()
89
90        # Location reporting is only support in Python >= 2.4
91        if not hasattr(parser, 'CurrentLineNumber'):
92            self._getpos = self._getpos_unknown
93
94        self.expat = parser
95        self._queue = []
96
97    def parse(self):
98        def _generate():
99            try:
100                bufsize = 4 * 1024 # 4K
101                done = False
102                while 1:
103                    while not done and len(self._queue) == 0:
104                        data = self.source.read(bufsize)
105                        if data == '': # end of data
106                            if hasattr(self, 'expat'):
107                                self.expat.Parse('', True)
108                                del self.expat # get rid of circular references
109                            done = True
110                        else:
111                            if isinstance(data, unicode):
112                                data = data.encode('utf-8')
113                            self.expat.Parse(data, False)
114                    for event in self._queue:
115                        yield event
116                    self._queue = []
117                    if done:
118                        break
119            except expat.ExpatError, e:
120                msg = str(e)
121                if self.filename:
122                    msg += ', in ' + self.filename
123                raise ParseError(msg, self.filename, e.lineno, e.offset)
124        return Stream(_generate()).filter(_coalesce)
125
126    def __iter__(self):
127        return iter(self.parse())
128
129    def _enqueue(self, kind, data=None, pos=None):
130        if pos is None:
131            pos = self._getpos()
132        if kind is TEXT:
133            # Expat reports the *end* of the text event as current position. We
134            # try to fix that up here as much as possible. Unfortunately, the
135            # offset is only valid for single-line text. For multi-line text,
136            # it is apparently not possible to determine at what offset it
137            # started
138            if '\n' in data:
139                lines = data.splitlines()
140                lineno = pos[1] - len(lines) + 1
141                offset = -1
142            else:
143                lineno = pos[1]
144                offset = pos[2] - len(data)
145            pos = (pos[0], lineno, offset)
146        self._queue.append((kind, data, pos))
147
148    def _getpos_unknown(self):
149        return (self.filename, -1, -1)
150
151    def _getpos(self):
152        return (self.filename, self.expat.CurrentLineNumber,
153                self.expat.CurrentColumnNumber)
154
155    def _handle_start(self, tag, attrib):
156        self._enqueue(START, (QName(tag), Attrs(zip(*[iter(attrib)] * 2))))
157
158    def _handle_end(self, tag):
159        self._enqueue(END, QName(tag))
160
161    def _handle_data(self, text):
162        self._enqueue(TEXT, text)
163
164    def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
165        self._enqueue(DOCTYPE, (name, pubid, sysid))
166
167    def _handle_start_ns(self, prefix, uri):
168        self._enqueue(START_NS, (prefix or '', uri))
169
170    def _handle_end_ns(self, prefix):
171        self._enqueue(END_NS, prefix or '')
172
173    def _handle_start_cdata(self):
174        self._enqueue(START_CDATA)
175
176    def _handle_end_cdata(self):
177        self._enqueue(END_CDATA)
178
179    def _handle_pi(self, target, data):
180        self._enqueue(PI, (target, data))
181
182    def _handle_comment(self, text):
183        self._enqueue(COMMENT, text)
184
185    def _handle_other(self, text):
186        if text.startswith('&'):
187            # deal with undefined entities
188            try:
189                text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
190                self._enqueue(TEXT, text)
191            except KeyError:
192                filename, lineno, offset = self._getpos()
193                error = expat.error('undefined entity "%s": line %d, column %d'
194                                    % (text, lineno, offset))
195                error.code = expat.errors.XML_ERROR_UNDEFINED_ENTITY
196                error.lineno = lineno
197                error.offset = offset
198                raise error
199
200
201def XML(text):
202    return Stream(list(XMLParser(StringIO(text))))
203
204
205class HTMLParser(html.HTMLParser, object):
206    """Parser for HTML input based on the Python `HTMLParser` module.
207   
208    This class provides the same interface for generating stream events as
209    `XMLParser`, and attempts to automatically balance tags.
210   
211    The parsing is initiated by iterating over the parser object:
212   
213    >>> parser = HTMLParser(StringIO('<UL compact><LI>Foo</UL>'))
214    >>> for kind, data, pos in parser:
215    ...     print kind, data
216    START (u'ul', [(u'compact', u'compact')])
217    START (u'li', [])
218    TEXT Foo
219    END li
220    END ul
221    """
222
223    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
224                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
225                              'param'])
226
227    def __init__(self, source, filename=None):
228        html.HTMLParser.__init__(self)
229        self.source = source
230        self.filename = filename
231        self._queue = []
232        self._open_tags = []
233
234    def parse(self):
235        def _generate():
236            try:
237                bufsize = 4 * 1024 # 4K
238                done = False
239                while 1:
240                    while not done and len(self._queue) == 0:
241                        data = self.source.read(bufsize)
242                        if data == '': # end of data
243                            self.close()
244                            done = True
245                        else:
246                            self.feed(data)
247                    for kind, data, pos in self._queue:
248                        yield kind, data, pos
249                    self._queue = []
250                    if done:
251                        open_tags = self._open_tags
252                        open_tags.reverse()
253                        for tag in open_tags:
254                            yield END, QName(tag), pos
255                        break
256            except html.HTMLParseError, e:
257                msg = '%s: line %d, column %d' % (e.msg, e.lineno, e.offset)
258                if self.filename:
259                    msg += ', in %s' % self.filename
260                raise ParseError(msg, self.filename, e.lineno, e.offset)
261        return Stream(_generate()).filter(_coalesce)
262
263    def __iter__(self):
264        return iter(self.parse())
265
266    def _enqueue(self, kind, data, pos=None):
267        if pos is None:
268            pos = self._getpos()
269        self._queue.append((kind, data, pos))
270
271    def _getpos(self):
272        lineno, column = self.getpos()
273        return (self.filename, lineno, column)
274
275    def handle_starttag(self, tag, attrib):
276        fixed_attrib = []
277        for name, value in attrib: # Fixup minimized attributes
278            if value is None:
279                value = name
280            fixed_attrib.append((name, unicode(value)))
281
282        self._enqueue(START, (QName(tag), Attrs(fixed_attrib)))
283        if tag in self._EMPTY_ELEMS:
284            self._enqueue(END, QName(tag))
285        else:
286            self._open_tags.append(tag)
287
288    def handle_endtag(self, tag):
289        if tag not in self._EMPTY_ELEMS:
290            while self._open_tags:
291                open_tag = self._open_tags.pop()
292                if open_tag.lower() == tag.lower():
293                    break
294                self._enqueue(END, QName(open_tag))
295            self._enqueue(END, QName(tag))
296
297    def handle_data(self, text):
298        self._enqueue(TEXT, text)
299
300    def handle_charref(self, name):
301        text = unichr(int(name))
302        self._enqueue(TEXT, text)
303
304    def handle_entityref(self, name):
305        try:
306            text = unichr(htmlentitydefs.name2codepoint[name])
307        except KeyError:
308            text = '&%s;' % name
309        self._enqueue(TEXT, text)
310
311    def handle_pi(self, data):
312        target, data = data.split(maxsplit=1)
313        data = data.rstrip('?')
314        self._enqueue(PI, (target.strip(), data.strip()))
315
316    def handle_comment(self, text):
317        self._enqueue(COMMENT, text)
318
319
320def HTML(text):
321    return Stream(list(HTMLParser(StringIO(text))))
322
323def _coalesce(stream):
324    """Coalesces adjacent TEXT events into a single event."""
325    textbuf = []
326    textpos = None
327    for kind, data, pos in chain(stream, [(None, None, None)]):
328        if kind is TEXT:
329            textbuf.append(data)
330            if textpos is None:
331                textpos = pos
332        else:
333            if textbuf:
334                yield TEXT, u''.join(textbuf), textpos
335                del textbuf[:]
336                textpos = None
337            if kind:
338                yield kind, data, pos
Note: See TracBrowser for help on using the repository browser.