Edgewall Software

Ticket #108: html5_support.diff

File html5_support.diff, 8.9 kB (added by tbroyer, 19 months ago)

Adds HTML5 parsing and serialization support

  • genshi/__init__.py

     
    2626    pass 
    2727 
    2828from genshi.core import * 
    29 from genshi.input import ParseError, XML, HTML 
     29from genshi.input import ParseError, XML, HTML, HTML5 
  • genshi/input.py

     
    2525import htmlentitydefs 
    2626from StringIO import StringIO 
    2727 
    28 from genshi.core import Attrs, QName, Stream, stripentities 
     28from genshi.core import Attrs, Namespace, QName, Stream, stripentities 
    2929from genshi.core import DOCTYPE, START, END, START_NS, END_NS, TEXT, \ 
    3030                        START_CDATA, END_CDATA, PI, COMMENT 
    3131 
    32 __all__ = ['ET', 'ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML'] 
     32__all__ = ['ET', 'ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML', 'HTML5Parser', 'HTML5'] 
    3333__docformat__ = 'restructuredtext en' 
    3434 
    3535def ET(element): 
     
    426426    """ 
    427427    return Stream(list(HTMLParser(StringIO(text), encoding=encoding))) 
    428428 
     429class HTML5Parser(object): 
     430    """Parser for HTML input based on `html5lib`. 
     431     
     432    This class provides the same interface for generating stream events as 
     433    `XMLParser`. 
     434     
     435    The parsing is initiated by iterating over the parser object: 
     436     
     437    >>> parser = HTML5Parser(StringIO('<UL compact><LI>Foo</UL>')) 
     438    >>> for kind, data, pos in parser: 
     439    ...     print kind, repr(data) 
     440    START (QName(u'html'), Attrs()) 
     441    START (QName(u'head'), Attrs()) 
     442    END QName(u'head') 
     443    START (QName(u'body'), Attrs()) 
     444    START (QName(u'ul'), Attrs([(QName(u'compact'), '')])) 
     445    START (QName(u'li'), Attrs()) 
     446    TEXT u'Foo' 
     447    END QName(u'li') 
     448    END QName(u'ul') 
     449    END QName(u'body') 
     450    END QName(u'html') 
     451    """ 
     452     
     453    html = Namespace('http://www.w3.org/1999/xhtml') 
     454 
     455    def __init__(self, source, filename=None, encoding=None, innerHTML=False): 
     456        """Initialize the parser for the given HTML input. 
     457         
     458        :param source: the HTML text as a file-like object 
     459        :param filename: the name of the file, if known 
     460        :param encoding: encoding of the file; ignored if the input is unicode 
     461        :param innerHTML: are we parsing in innerHTML mode (innerHTML=True is not yet supported by html5lib) 
     462        """ 
     463        self.source = source 
     464        self.filename = filename 
     465        self.encoding = encoding 
     466        self.innerHTML = innerHTML 
     467        import html5lib 
     468        self.parser = html5lib.HTMLParser() 
     469 
     470    def parse(self): 
     471        """Generator that parses the HTML source, yielding markup events. 
     472         
     473        :return: a markup event stream 
     474        """ 
     475        # TODO: Add some basic namespace support, e.g. convert known prefixes (py:, svg:, mathml:, smil:) to QNames 
     476        document = self.parser.parse(self.source, encoding=self.encoding, innerHTML=self.innerHTML) 
     477        return self._generate(document) 
     478 
     479    def __iter__(self): 
     480        return iter(self.parse()) 
     481     
     482    def _generate(self, element): 
     483        from html5lib.treebuilders.simpletree import Document, DocumentType, CommentNode, TextNode 
     484 
     485        pos = (self.filename, -1, -1) 
     486 
     487        if isinstance(element, Document): 
     488            for child in element.childNodes: 
     489                for kind, data, pos in self._generate(child): 
     490                    yield kind, data, pos 
     491 
     492        elif isinstance(element, DocumentType): 
     493            yield DOCTYPE, (element.name, None, None), pos 
     494 
     495        elif isinstance(element, CommentNode): 
     496            yield COMMENT, element.data, pos 
     497 
     498        elif isinstance(element, TextNode): 
     499            yield TEXT, element.value, pos 
     500 
     501        else: # Element 
     502            tag_name = self.html[element.name] 
     503            attrs = Attrs([(self.html[attr], value) for attr, value in element.attributes.iteritems()]) 
     504            yield START, (tag_name, attrs), pos 
     505            for child in element.childNodes: 
     506                for kind, data, pos in self._generate(child): 
     507                    yield kind, data, pos 
     508            yield END, tag_name, pos 
     509 
     510 
     511def HTML5(text, encoding=None, strict=False, innerHTML=False): 
     512    """Parse the given HTML source and return a markup stream. 
     513     
     514    Unlike with `HTML5Parser`, the returned stream is reusable, meaning it can be 
     515    iterated over multiple times: 
     516     
     517    >>> html = HTML5('<body><h1>Foo</h1></body>') 
     518    >>> print html 
     519    <html xmlns="http://www.w3.org/1999/xhtml"><head/><body><h1>Foo</h1></body></html> 
     520    >>> print html.select('body/h1') 
     521    <h1 xmlns="http://www.w3.org/1999/xhtml">Foo</h1> 
     522    >>> print html.select('body/h1/text()') 
     523    Foo 
     524     
     525    :param text: the HTML source 
     526    :return: the parsed XML event stream 
     527    """ 
     528    return Stream(list(HTML5Parser(StringIO(text), encoding=encoding))) 
     529 
    429530def _coalesce(stream): 
    430531    """Coalesces adjacent TEXT events into a single event.""" 
    431532    textbuf = [] 
  • genshi/output.py

     
    2727                        START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE 
    2828 
    2929__all__ = ['DocType', 'XMLSerializer', 'XHTMLSerializer', 'HTMLSerializer', 
    30            'TextSerializer'] 
     30           'TextSerializer', 'HTML5Serializer'] 
    3131__docformat__ = 'restructuredtext en' 
    3232 
    3333 
     
    5353        'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' 
    5454    ) 
    5555    XHTML = XHTML_STRICT 
     56     
     57    HTML5 = ('html', None, None) 
    5658 
    5759 
    5860class XMLSerializer(object): 
     
    321323                yield Markup('<?%s %s?>' % data) 
    322324 
    323325 
     326class HTML5Serializer(object): 
     327    _NOESCAPE_ELEMS = frozenset(['style', 'script', 'xmp', 'iframe', 'noembed', 
     328                                  'noframes', 'noscript']) 
     329 
     330    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'bgsound', 'br', 
     331                              'col', 'embed', 'frame', 'hr', 'img', 'input', 
     332                              'link', 'meta', 'param', 'spacer', 'wbr']) 
     333 
     334    def __init__(self, doctype=DocType.HTML5): 
     335        self.preamble = [] 
     336        if doctype: 
     337            self.preamble.append((DOCTYPE, doctype, (None, -1, -1))) 
     338        self.filters = [EmptyTagFilter(), NamespaceStripper('http://www.w3.org/1999/xhtml')] 
     339     
     340    def __call__(self, stream): 
     341        empty_elems = self._EMPTY_ELEMS 
     342        noescape_elems = self._NOESCAPE_ELEMS 
     343        have_doctype = False 
     344        noescape = None 
     345        skip_content = None 
     346        depth = 0 
     347 
     348        stream = chain(self.preamble, stream) 
     349        for filter_ in self.filters: 
     350            stream = filter_(stream) 
     351        for kind, data, pos in stream: 
     352 
     353            if kind is START or kind is EMPTY: 
     354                if kind is START: 
     355                    depth += 1 
     356                tag, attrib = data 
     357                buf = ['<', tag.lower()] 
     358                for attr, value in attrib: 
     359                    buf += [' ', attr.lower(), '="', self.escape(value), '"'] 
     360                buf.append('>') 
     361                if kind is EMPTY: 
     362                    if tag not in empty_elems: 
     363                        buf.append('</%s>' % tag) 
     364                yield Markup(u''.join(buf)) 
     365                if tag in noescape_elems: 
     366                    noescape = depth 
     367                if tag in empty_elems: 
     368                    skip_content = depth 
     369 
     370            elif kind is END: 
     371                yield Markup('</%s>' % data) 
     372                if noescape == depth: 
     373                    noescape = None 
     374                if skip_content == depth: 
     375                    skip_content = None 
     376                depth -= 1 
     377 
     378            elif kind is TEXT: 
     379                if noescape: 
     380                    yield data 
     381                else: 
     382                    yield self.escape(data) 
     383 
     384            elif kind is COMMENT: 
     385                yield Markup('<!-%s-->' % data) 
     386 
     387            elif kind is DOCTYPE and not have_doctype: 
     388                name, pubid, sysid = data 
     389                buf = ['<!DOCTYPE %s'] 
     390                if pubid: 
     391                    buf.append(' PUBLIC "%s"') 
     392                elif sysid: 
     393                    buf.append(' SYSTEM') 
     394                if sysid: 
     395                    buf.append(' "%s"') 
     396                buf.append('>\n') 
     397                yield Markup(u''.join(buf), *filter(None, data)) 
     398                have_doctype = True 
     399 
     400            elif kind is PI: 
     401                # This is not valid HTML5 but looks like an SGML PI 
     402                yield Markup('<?%s %s>' % data) 
     403 
     404    def escape(text): 
     405        return unicode(text).replace('&', '&amp;') \ 
     406                             .replace('<', '&lt;') \ 
     407                             .replace('>', '&gt;') \ 
     408                             .replace('"', '&quot;') 
     409    escape = staticmethod(escape) 
     410 
     411 
    324412class TextSerializer(object): 
    325413    """Produces plain text from an event stream. 
    326414