###############
Tokenize HTML
###############
Run the WHATWG tokenizer over HTML: port an :class:`python:html.parser.HTMLParser` subclass, or consume the token stream
from :func:`turbohtml.tokenize` directly.
**************************
Migrate from html.parser
**************************
The quickest port keeps your subclass: :class:`turbohtml.migration.stdlib.HTMLParser` is a drop-in base class with the
same ``handle_*`` callbacks and ``feed``/``close`` methods, over the WHATWG-conformant tokenizer. Change the import and
the base class and the handlers fire as before:
.. testcode::
from turbohtml.migration.stdlib import HTMLParser
class LinkCollector(HTMLParser):
def __init__(self):
super().__init__()
self.links = []
def handle_starttag(self, tag, attrs):
if tag == "a":
self.links += [v for n, v in attrs if n == "href" and v]
collector = LinkCollector()
collector.feed('x y')
collector.close()
print(collector.links)
.. testoutput::
['/x', '/y']
It differs from ``html.parser`` only where ``html.parser`` diverges from the WHATWG algorithm: references are always
resolved (so ``handle_entityref``/``handle_charref`` never fire), and a processing instruction or CDATA section reaches
``handle_comment`` rather than ``handle_pi``/``unknown_decl``, because the HTML spec treats both as comments.
If you would rather drop the subclass entirely, turbohtml also exposes the raw token stream.
:class:`python:html.parser.HTMLParser` is callback-driven: you subclass it and override a handler per event. turbohtml
inverts that into a token stream you iterate, which removes the subclass, the mutable handler state, and the
per-callback Python call overhead. A typical parser:
.. code-block:: python
from html.parser import HTMLParser
class LinkCollector(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.links: list[str] = []
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == "a":
self.links.extend(value for name, value in attrs if name == "href" and value)
collector = LinkCollector()
collector.feed(page)
collector.close()
becomes a loop:
.. code-block:: python
import turbohtml
links = [
href
for token in turbohtml.tokenize(page)
if token.type is turbohtml.TokenType.START_TAG and token.tag == "a" and (href := token.attr("href"))
]
The events map one to one:
- ``handle_starttag(tag, attrs)`` → a token with ``type is TokenType.START_TAG``; ``token.tag`` and ``token.attrs``
carry the same lowercased name and decoded ``(name, value)`` pairs, and ``token.attr(name)`` replaces scanning the
list.
- ``handle_endtag(tag)`` → ``TokenType.END_TAG``.
- ``handle_startendtag(tag, attrs)`` → a ``START_TAG`` token with ``self_closing`` true (turbohtml does not emit a
separate event).
- ``handle_data(data)`` → ``TokenType.TEXT``; character references arrive decoded, like ``convert_charrefs=True``, so
there is no ``handle_entityref``/``handle_charref`` pair to implement.
- ``handle_comment(data)`` → ``TokenType.COMMENT``.
- ``handle_decl(decl)`` → ``TokenType.DOCTYPE``, split into ``name``, ``public_id`` and ``system_id`` instead of one raw
string.
- ``self.getpos()`` → ``token.line`` and ``token.col``, the same 1-based-line, 0-based-column convention.
- ``feed()``/``close()`` → the same names on :class:`turbohtml.Tokenizer`; each ``feed()`` returns the tokens that chunk
completed instead of firing callbacks, and a ``with`` block replaces remembering ``close()``.
turbohtml differs from ``html.parser`` wherever ``html.parser`` diverges from the WHATWG algorithm browsers implement:
turbohtml handles the raw-text content models (a ```` inside ``