Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/saxparse.py: 98%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2turbohtml.saxparse: DOM-less, event-driven HTML parsing.
4Where :func:`turbohtml.parse` hands back a document you navigate, :func:`sax_parse` fires a callback for each
5construct the parser builds -- a start tag, an end tag, a run of text, a comment, the doctype -- and hands back
6nothing. The events reflect the fully WHATWG-correct construction (implied ``<html>``/``<head>``/``<body>``, foster
7parenting out of tables, the adoption agency, and the rest), so you see the tree the parser *built*, not the raw token
8soup; and no per-node Python object is ever created, so a one-pass extraction over a large document never pays for a
9document-sized object graph and keeps only what your handler keeps. This is the event-driven model Python's
10:class:`html.parser.HTMLParser`, expat, and htmlparser2 offer, with a spec-correct tree builder behind it and the
11tokenization, construction, and walk all in C.
13The one thing this is *not* is a way to parse a document larger than memory: a spec-correct tree builder cannot run in
14space proportional to the open-element depth, because the adoption agency and text coalescing retroactively move and
15merge nodes the walk has already emitted, so the working tree is retained until the parse ends (then freed in one
16shot). The win over :func:`turbohtml.parse` is the object graph you never build and the tree you never keep; the win
17over :class:`html.parser.HTMLParser` is speed and a tree the standard library's raw callbacks never reconstruct.
19Two shapes share one C walk. Subclass :class:`SaxHandler`, override the events you care about, and pass an instance to
20:func:`sax_parse` for the push (callback) form; or iterate :func:`iter_events` for the pull form, a stream of typed
21:class:`StartElement`/:class:`EndElement`/:class:`Characters`/:class:`Comment`/:class:`Doctype`/
22:class:`ProcessingInstruction` records. WHATWG HTML processing instructions expose their target and data separately;
23the reserved ``xml`` and ``xml-stylesheet`` targets remain comments.
24"""
26from __future__ import annotations
28from typing import TYPE_CHECKING, Final, NamedTuple, cast
30from ._html import _sax_dispatch, _sax_events
32if TYPE_CHECKING:
33 from collections.abc import Callable, Iterator
35__all__ = [
36 "Characters",
37 "Comment",
38 "Doctype",
39 "EndElement",
40 "ProcessingInstruction",
41 "SaxEvent",
42 "SaxHandler",
43 "StartElement",
44 "iter_events",
45 "sax_parse",
46]
49class SaxHandler:
50 """
51 Base class for a :func:`sax_parse` callback object.
53 Every method is a no-op by default; subclass and override only the events you need, the way you subclass
54 :class:`html.parser.HTMLParser`.
55 """
57 def start_element(self, tag: str, attrs: tuple[tuple[str, str | None], ...]) -> None:
58 """Handle a start tag ``tag`` with its ``attrs`` (each a ``(name, value)`` pair, value None if valueless)."""
60 def end_element(self, tag: str) -> None:
61 """Handle the end of element ``tag`` (fired for every element, including empty and void ones)."""
63 def characters(self, data: str) -> None:
64 """Handle a run of character data ``data``."""
66 def comment(self, data: str) -> None:
67 """Handle a comment with body ``data``."""
69 def doctype(self, name: str, public_id: str | None, system_id: str | None) -> None:
70 """Handle the document type declaration; ``public_id``/``system_id`` are None when the source omits them."""
72 def processing_instruction(self, target: str, data: str) -> None:
73 """Receive non-reserved instructions after the parser separates their target and data."""
76class StartElement(NamedTuple):
77 """A start-tag event."""
79 tag: str
80 """the element's tag name."""
81 attrs: tuple[tuple[str, str | None], ...]
82 """the attributes, each a ``(name, value)`` pair with ``value`` None for a valueless attribute."""
85class EndElement(NamedTuple):
86 """An end-tag event, fired for every element the parser closes."""
88 tag: str
89 """the element's tag name."""
92class Characters(NamedTuple):
93 """A run of character data."""
95 data: str
96 """the text."""
99class Comment(NamedTuple):
100 """A comment event."""
102 data: str
103 """the comment body, between ``<!--`` and ``-->``."""
106class Doctype(NamedTuple):
107 """A document type declaration event."""
109 name: str
110 """the document type name, e.g. ``html``."""
111 public_id: str | None
112 """the public identifier, or None when the source omits it."""
113 system_id: str | None
114 """the system identifier, or None when the source omits it."""
117class ProcessingInstruction(NamedTuple):
118 """An HTML instruction whose target is not reserved for XML."""
120 target: str
121 """the ASCII target, preserving source case."""
122 data: str
123 """the text after leading whitespace and before the closing delimiter."""
126SaxEvent = StartElement | EndElement | Characters | Comment | Doctype | ProcessingInstruction
127"""The union of every event :func:`iter_events` yields."""
129_EVENTS: Final[tuple[Callable[..., SaxEvent], ...]] = (
130 StartElement,
131 EndElement,
132 Characters,
133 Comment,
134 Doctype,
135 ProcessingInstruction,
136)
139def iter_events(html: str) -> Iterator[SaxEvent]:
140 """
141 Parse ``html`` and yield its SAX events as typed records, one at a time, without building a document.
143 :param html: the markup to parse.
144 :returns: an iterator of :class:`StartElement`/:class:`EndElement`/:class:`Characters`/:class:`Comment`/
145 :class:`Doctype`/:class:`ProcessingInstruction` records in document order.
146 :raises TypeError: if ``html`` is not a str.
147 """
148 for event in _sax_events(html):
149 yield _EVENTS[cast("int", event[0]) - 1](*event[1:])
152def sax_parse(html: str, handler: SaxHandler) -> None:
153 """
154 Parse ``html``, calling ``handler``'s methods for each event, and retain no tree.
156 :param html: the markup to parse.
157 :param handler: a :class:`SaxHandler` (or subclass) whose overridden methods receive the events.
158 :raises TypeError: if ``html`` is not a str.
159 """
160 _sax_dispatch(html, handler)