############### 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 ``