Edgewall Software

source: trunk/genshi/input.py @ 1212

Last change on this file since 1212 was 1212, checked in by hodgestar, 11 years ago

Remove unused isinstance checks.

  • Property svn:eol-style set to native
File size: 16.1 KB
RevLine 
[2]1# -*- coding: utf-8 -*-
2#
[1077]3# Copyright (C) 2006-2009 Edgewall Software
[2]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
[287]8# are also available at http://genshi.edgewall.org/wiki/License.
[2]9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
[287]12# history and logs, available at http://genshi.edgewall.org/log/.
[2]13
[517]14"""Support for constructing markup streams from files, strings, or other
15sources.
16"""
17
[185]18from itertools import chain
[1189]19import codecs
[1082]20import htmlentitydefs as entities
21import HTMLParser as html
[2]22from xml.parsers import expat
23
[361]24from genshi.core import Attrs, QName, Stream, stripentities
[1082]25from genshi.core import START, END, XML_DECL, DOCTYPE, TEXT, START_NS, \
26                        END_NS, START_CDATA, END_CDATA, PI, COMMENT
[1157]27from genshi.compat import StringIO, BytesIO
[2]28
[1157]29
[358]30__all__ = ['ET', 'ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML']
[517]31__docformat__ = 'restructuredtext en'
[2]32
[1082]33
[358]34def ET(element):
[525]35    """Convert a given ElementTree element to a markup stream.
36   
37    :param element: an ElementTree element
38    :return: a markup stream
39    """
[358]40    tag_name = QName(element.tag.lstrip('{'))
[554]41    attrs = Attrs([(QName(attr.lstrip('{')), value)
42                   for attr, value in element.items()])
[185]43
[358]44    yield START, (tag_name, attrs), (None, -1, -1)
45    if element.text:
46        yield TEXT, element.text, (None, -1, -1)
47    for child in element.getchildren():
48        for item in ET(child):
49            yield item
50    yield END, tag_name, (None, -1, -1)
51    if element.tail:
52        yield TEXT, element.tail, (None, -1, -1)
53
54
[22]55class ParseError(Exception):
56    """Exception raised when fatal syntax errors are found in the input being
[525]57    parsed.
58    """
[22]59
[514]60    def __init__(self, message, filename=None, lineno=-1, offset=-1):
[525]61        """Exception initializer.
62       
63        :param message: the error message from the parser
64        :param filename: the path to the file that was parsed
65        :param lineno: the number of the line on which the error was encountered
66        :param offset: the column number where the error was encountered
67        """
[514]68        self.msg = message
69        if filename:
[526]70            message += ', in ' + filename
[22]71        Exception.__init__(self, message)
[514]72        self.filename = filename or '<string>'
[22]73        self.lineno = lineno
74        self.offset = offset
75
76
[2]77class XMLParser(object):
78    """Generator-based XML parser based on roughly equivalent code in
[27]79    Kid/ElementTree.
80   
81    The parsing is initiated by iterating over the parser object:
82   
83    >>> parser = XMLParser(StringIO('<root id="2"><child>Foo</child></root>'))
84    >>> for kind, data, pos in parser:
[1076]85    ...     print('%s %s' % (kind, data))
[1080]86    START (QName('root'), Attrs([(QName('id'), u'2')]))
87    START (QName('child'), Attrs())
[27]88    TEXT Foo
89    END child
90    END root
91    """
[2]92
[361]93    _entitydefs = ['<!ENTITY %s "&#%d;">' % (name, value) for name, value in
[1079]94                   entities.name2codepoint.items()]
[1157]95    _external_dtd = u'\n'.join(_entitydefs).encode('utf-8')
[361]96
[390]97    def __init__(self, source, filename=None, encoding=None):
[385]98        """Initialize the parser for the given XML input.
[27]99       
[517]100        :param source: the XML text as a file-like object
101        :param filename: the name of the file, if appropriate
102        :param encoding: the encoding of the file; if not specified, the
103                         encoding is assumed to be ASCII, UTF-8, or UTF-16, or
104                         whatever the encoding specified in the XML declaration
105                         (if any)
[27]106        """
[2]107        self.source = source
[22]108        self.filename = filename
[2]109
110        # Setup the Expat parser
[390]111        parser = expat.ParserCreate(encoding, '}')
[2]112        parser.buffer_text = True
[1157]113        # Python 3 does not have returns_unicode
114        if hasattr(parser, 'returns_unicode'):
115            parser.returns_unicode = True
[202]116        parser.ordered_attributes = True
117
[2]118        parser.StartElementHandler = self._handle_start
119        parser.EndElementHandler = self._handle_end
120        parser.CharacterDataHandler = self._handle_data
121        parser.StartDoctypeDeclHandler = self._handle_doctype
122        parser.StartNamespaceDeclHandler = self._handle_start_ns
123        parser.EndNamespaceDeclHandler = self._handle_end_ns
[184]124        parser.StartCdataSectionHandler = self._handle_start_cdata
125        parser.EndCdataSectionHandler = self._handle_end_cdata
[2]126        parser.ProcessingInstructionHandler = self._handle_pi
[558]127        parser.XmlDeclHandler = self._handle_xml_decl
[2]128        parser.CommentHandler = self._handle_comment
[259]129
130        # Tell Expat that we'll handle non-XML entities ourselves
131        # (in _handle_other)
[2]132        parser.DefaultHandler = self._handle_other
[361]133        parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_ALWAYS)
[259]134        parser.UseForeignDTD()
[361]135        parser.ExternalEntityRefHandler = self._build_foreign
[2]136
137        self.expat = parser
[22]138        self._queue = []
[2]139
[185]140    def parse(self):
[525]141        """Generator that parses the XML source, yielding markup events.
142       
143        :return: a markup event stream
144        :raises ParseError: if the XML text is not well formed
145        """
[185]146        def _generate():
147            try:
148                bufsize = 4 * 1024 # 4K
149                done = False
150                while 1:
151                    while not done and len(self._queue) == 0:
152                        data = self.source.read(bufsize)
[1157]153                        if not data: # end of data
[185]154                            if hasattr(self, 'expat'):
155                                self.expat.Parse('', True)
156                                del self.expat # get rid of circular references
157                            done = True
158                        else:
[257]159                            if isinstance(data, unicode):
160                                data = data.encode('utf-8')
[185]161                            self.expat.Parse(data, False)
162                    for event in self._queue:
163                        yield event
164                    self._queue = []
165                    if done:
166                        break
167            except expat.ExpatError, e:
168                msg = str(e)
169                raise ParseError(msg, self.filename, e.lineno, e.offset)
[187]170        return Stream(_generate()).filter(_coalesce)
[185]171
[2]172    def __iter__(self):
[185]173        return iter(self.parse())
[2]174
[361]175    def _build_foreign(self, context, base, sysid, pubid):
176        parser = self.expat.ExternalEntityParserCreate(context)
[1157]177        parser.ParseFile(BytesIO(self._external_dtd))
[361]178        return 1
179
[184]180    def _enqueue(self, kind, data=None, pos=None):
[27]181        if pos is None:
182            pos = self._getpos()
[185]183        if kind is TEXT:
[171]184            # Expat reports the *end* of the text event as current position. We
185            # try to fix that up here as much as possible. Unfortunately, the
186            # offset is only valid for single-line text. For multi-line text,
187            # it is apparently not possible to determine at what offset it
188            # started
189            if '\n' in data:
190                lines = data.splitlines()
191                lineno = pos[1] - len(lines) + 1
192                offset = -1
193            else:
194                lineno = pos[1]
195                offset = pos[2] - len(data)
196            pos = (pos[0], lineno, offset)
[27]197        self._queue.append((kind, data, pos))
198
[2]199    def _getpos_unknown(self):
[171]200        return (self.filename, -1, -1)
[2]201
[22]202    def _getpos(self):
[171]203        return (self.filename, self.expat.CurrentLineNumber,
[22]204                self.expat.CurrentColumnNumber)
[2]205
206    def _handle_start(self, tag, attrib):
[495]207        attrs = Attrs([(QName(name), value) for name, value in
208                       zip(*[iter(attrib)] * 2)])
209        self._enqueue(START, (QName(tag), attrs))
[2]210
211    def _handle_end(self, tag):
[185]212        self._enqueue(END, QName(tag))
[2]213
214    def _handle_data(self, text):
[185]215        self._enqueue(TEXT, text)
[2]216
[558]217    def _handle_xml_decl(self, version, encoding, standalone):
218        self._enqueue(XML_DECL, (version, encoding, standalone))
219
[2]220    def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
[185]221        self._enqueue(DOCTYPE, (name, pubid, sysid))
[2]222
223    def _handle_start_ns(self, prefix, uri):
[185]224        self._enqueue(START_NS, (prefix or '', uri))
[2]225
226    def _handle_end_ns(self, prefix):
[185]227        self._enqueue(END_NS, prefix or '')
[2]228
[184]229    def _handle_start_cdata(self):
[185]230        self._enqueue(START_CDATA)
[184]231
232    def _handle_end_cdata(self):
[185]233        self._enqueue(END_CDATA)
[184]234
[2]235    def _handle_pi(self, target, data):
[185]236        self._enqueue(PI, (target, data))
[2]237
238    def _handle_comment(self, text):
[185]239        self._enqueue(COMMENT, text)
[2]240
241    def _handle_other(self, text):
242        if text.startswith('&'):
243            # deal with undefined entities
244            try:
[1079]245                text = unichr(entities.name2codepoint[text[1:-1]])
[185]246                self._enqueue(TEXT, text)
[2]247            except KeyError:
[259]248                filename, lineno, offset = self._getpos()
249                error = expat.error('undefined entity "%s": line %d, column %d'
250                                    % (text, lineno, offset))
251                error.code = expat.errors.XML_ERROR_UNDEFINED_ENTITY
252                error.lineno = lineno
253                error.offset = offset
254                raise error
[2]255
256
257def XML(text):
[525]258    """Parse the given XML source and return a markup stream.
259   
260    Unlike with `XMLParser`, the returned stream is reusable, meaning it can be
261    iterated over multiple times:
262   
263    >>> xml = XML('<doc><elem>Foo</elem><elem>Bar</elem></doc>')
[1076]264    >>> print(xml)
[525]265    <doc><elem>Foo</elem><elem>Bar</elem></doc>
[1076]266    >>> print(xml.select('elem'))
[525]267    <elem>Foo</elem><elem>Bar</elem>
[1076]268    >>> print(xml.select('elem/text()'))
[525]269    FooBar
270   
271    :param text: the XML source
272    :return: the parsed XML event stream
273    :raises ParseError: if the XML text is not well-formed
274    """
[1082]275    return Stream(list(XMLParser(StringIO(text))))
[2]276
277
[22]278class HTMLParser(html.HTMLParser, object):
[2]279    """Parser for HTML input based on the Python `HTMLParser` module.
280   
281    This class provides the same interface for generating stream events as
282    `XMLParser`, and attempts to automatically balance tags.
[27]283   
284    The parsing is initiated by iterating over the parser object:
285   
[1157]286    >>> parser = HTMLParser(BytesIO(u'<UL compact><LI>Foo</UL>'.encode('utf-8')), encoding='utf-8')
[27]287    >>> for kind, data, pos in parser:
[1076]288    ...     print('%s %s' % (kind, data))
[1080]289    START (QName('ul'), Attrs([(QName('compact'), u'compact')]))
290    START (QName('li'), Attrs())
[27]291    TEXT Foo
292    END li
293    END ul
[2]294    """
295
296    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
297                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
298                              'param'])
299
[1157]300    def __init__(self, source, filename=None, encoding=None):
[385]301        """Initialize the parser for the given HTML input.
302       
[517]303        :param source: the HTML text as a file-like object
304        :param filename: the name of the file, if known
305        :param filename: encoding of the file; ignored if the input is unicode
[385]306        """
[2]307        html.HTMLParser.__init__(self)
308        self.source = source
[22]309        self.filename = filename
[385]310        self.encoding = encoding
[22]311        self._queue = []
[2]312        self._open_tags = []
313
[185]314    def parse(self):
[525]315        """Generator that parses the HTML source, yielding markup events.
316       
317        :return: a markup event stream
318        :raises ParseError: if the HTML text is not well formed
319        """
[185]320        def _generate():
[1189]321            if self.encoding:
322                reader = codecs.getreader(self.encoding)
323                source = reader(self.source)
324            else:
325                source = self.source
[185]326            try:
327                bufsize = 4 * 1024 # 4K
328                done = False
329                while 1:
330                    while not done and len(self._queue) == 0:
[1189]331                        data = source.read(bufsize)
[1157]332                        if not data: # end of data
[185]333                            self.close()
334                            done = True
335                        else:
[1157]336                            if not isinstance(data, unicode):
[1189]337                                raise UnicodeError("source returned bytes, but no encoding specified")
[185]338                            self.feed(data)
339                    for kind, data, pos in self._queue:
340                        yield kind, data, pos
341                    self._queue = []
342                    if done:
343                        open_tags = self._open_tags
344                        open_tags.reverse()
345                        for tag in open_tags:
346                            yield END, QName(tag), pos
347                        break
348            except html.HTMLParseError, e:
349                msg = '%s: line %d, column %d' % (e.msg, e.lineno, e.offset)
350                raise ParseError(msg, self.filename, e.lineno, e.offset)
[187]351        return Stream(_generate()).filter(_coalesce)
[185]352
[2]353    def __iter__(self):
[185]354        return iter(self.parse())
[2]355
[27]356    def _enqueue(self, kind, data, pos=None):
357        if pos is None:
358            pos = self._getpos()
359        self._queue.append((kind, data, pos))
360
[22]361    def _getpos(self):
362        lineno, column = self.getpos()
363        return (self.filename, lineno, column)
364
[2]365    def handle_starttag(self, tag, attrib):
[27]366        fixed_attrib = []
367        for name, value in attrib: # Fixup minimized attributes
368            if value is None:
[1212]369                value = name
[495]370            fixed_attrib.append((QName(name), stripentities(value)))
[27]371
[227]372        self._enqueue(START, (QName(tag), Attrs(fixed_attrib)))
[2]373        if tag in self._EMPTY_ELEMS:
[185]374            self._enqueue(END, QName(tag))
[2]375        else:
376            self._open_tags.append(tag)
377
378    def handle_endtag(self, tag):
379        if tag not in self._EMPTY_ELEMS:
380            while self._open_tags:
381                open_tag = self._open_tags.pop()
[460]382                self._enqueue(END, QName(open_tag))
[2]383                if open_tag.lower() == tag.lower():
384                    break
385
386    def handle_data(self, text):
[185]387        self._enqueue(TEXT, text)
[2]388
389    def handle_charref(self, name):
[515]390        if name.lower().startswith('x'):
391            text = unichr(int(name[1:], 16))
392        else:
393            text = unichr(int(name))
[185]394        self._enqueue(TEXT, text)
[2]395
396    def handle_entityref(self, name):
[185]397        try:
[1079]398            text = unichr(entities.name2codepoint[name])
[185]399        except KeyError:
400            text = '&%s;' % name
401        self._enqueue(TEXT, text)
[2]402
403    def handle_pi(self, data):
[458]404        target, data = data.split(None, 1)
405        if data.endswith('?'):
406            data = data[:-1]
[185]407        self._enqueue(PI, (target.strip(), data.strip()))
[2]408
409    def handle_comment(self, text):
[185]410        self._enqueue(COMMENT, text)
[2]411
412
[1157]413def HTML(text, encoding=None):
[525]414    """Parse the given HTML source and return a markup stream.
415   
416    Unlike with `HTMLParser`, the returned stream is reusable, meaning it can be
417    iterated over multiple times:
418   
[1157]419    >>> html = HTML('<body><h1>Foo</h1></body>', encoding='utf-8')
[1076]420    >>> print(html)
[525]421    <body><h1>Foo</h1></body>
[1076]422    >>> print(html.select('h1'))
[525]423    <h1>Foo</h1>
[1076]424    >>> print(html.select('h1/text()'))
[525]425    Foo
426   
427    :param text: the HTML source
428    :return: the parsed XML event stream
429    :raises ParseError: if the HTML text is not well-formed, and error recovery
430                        fails
431    """
[1157]432    if isinstance(text, unicode):
[1189]433        # If it's unicode text the encoding should be set to None.
434        # The option to pass in an incorrect encoding is for ease
435        # of writing doctests that work in both Python 2.x and 3.x.
436        return Stream(list(HTMLParser(StringIO(text), encoding=None)))
[1157]437    return Stream(list(HTMLParser(BytesIO(text), encoding=encoding)))
[185]438
[1082]439
[187]440def _coalesce(stream):
[185]441    """Coalesces adjacent TEXT events into a single event."""
[187]442    textbuf = []
443    textpos = None
444    for kind, data, pos in chain(stream, [(None, None, None)]):
445        if kind is TEXT:
446            textbuf.append(data)
447            if textpos is None:
448                textpos = pos
449        else:
450            if textbuf:
[1075]451                yield TEXT, ''.join(textbuf), textpos
[187]452                del textbuf[:]
453                textpos = None
454            if kind:
455                yield kind, data, pos
Note: See TracBrowser for help on using the repository browser.