Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/turbohtml/rewrite.py: 95%
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.rewrite: a DOM-less, single-pass HTML rewriter.
4Where :func:`turbohtml.parse` builds a document you navigate and mutate, :func:`rewrite` transforms markup on
5the wire without ever building a tree. It streams the WHATWG tokenizer over the input, tracks only the stack of
6currently-open elements, and calls a handler for each element a CSS selector matches (and, if you ask, for each
7run of text, each comment, and the doctype). A handler edits the node in place -- set an attribute, insert markup
8before or after it, replace its inner content, or drop it -- and the edited output is emitted incrementally.
10Memory is proportional to the open-element depth plus whatever a handler chooses to buffer, never to the document
11size, so a multi-megabyte page rewrites in a fixed working set. This is the model Cloudflare's lol-html popularized;
12turbohtml reuses its own native tokenizer and CSS selector engine behind it.
14Because the pass never looks ahead, only the selectors decidable from an element and its ancestors stream: type,
15universal, id, class, and attribute selectors, the descendant (space) and child (``>``) combinators, ``:root``,
16and :is()/:where()/:not() over that subset. The sibling combinators (``+``, ``~``), the positional and structural
17pseudo-classes (``:nth-child``, ``:last-of-type``, ``:only-child``, ``:empty``), and ``:has()`` need content the
18stream has not seen yet, so a selector using one raises :class:`~turbohtml.SelectorSyntaxError`.
19"""
21from __future__ import annotations
23from typing import TYPE_CHECKING, Protocol
25from ._html import _rewrite, _RewriteHandle
27if TYPE_CHECKING:
28 from collections.abc import Iterable
30__all__ = [
31 "CommentHandler",
32 "DoctypeHandler",
33 "Element",
34 "ElementHandler",
35 "TextHandler",
36 "rewrite",
37]
39Element = _RewriteHandle
40"""The node handle a rewrite handler receives.
42The same handle backs every handler kind; which members apply depends on :attr:`~Element.kind`. An element handle
43exposes :attr:`~Element.tag`, :attr:`~Element.attrs`, and the attribute and content edits; a text or comment handle
44exposes :attr:`~Element.text` and :meth:`~Element.set_text`; a doctype handle exposes :attr:`~Element.name`,
45:attr:`~Element.public_id`, and :attr:`~Element.system_id`. Insertion and removal (:meth:`~Element.before`,
46:meth:`~Element.after`, :meth:`~Element.replace`, :meth:`~Element.remove`) apply to every kind.
48Text content is HTML-escaped, except inside ``script``, ``style`` and the other raw-text elements, where a browser
49decodes no character references: text written there is kept as is, so ``a < b`` stays a valid script. The rewrite
50then raises :class:`ValueError` if that text would end the element early (``</script``, or ``<!--`` in a script).
52The rewrite writes an edited element's start tag again from its attributes and escapes ``&``, ``"``, ``<`` and ``>`` in
53each value.
55The handle is valid only for the duration of the handler call; stashing it and using it afterwards raises
56:class:`RuntimeError`.
58:meth:`~Element.set_attribute` raises :class:`ValueError` for a name the DOM API rejects, and :meth:`~Element.set_text`
59raises it for comment text that would end the comment early.
60"""
63class ElementHandler(Protocol):
64 """A callback invoked with the matched :class:`Element` for each element a selector matches."""
66 def __call__(self, element: Element, /) -> None:
67 """Edit the matched ``element`` in place."""
70class TextHandler(Protocol):
71 """A callback invoked with each run of character data as an :class:`Element` handle of text kind."""
73 def __call__(self, text: Element, /) -> None:
74 """Edit the ``text`` run in place."""
77class CommentHandler(Protocol):
78 """A callback invoked with each comment as an :class:`Element` handle of comment kind."""
80 def __call__(self, comment: Element, /) -> None:
81 """Edit the ``comment`` in place."""
84class DoctypeHandler(Protocol):
85 """A callback invoked with the document type declaration as an :class:`Element` handle of doctype kind."""
87 def __call__(self, doctype: Element, /) -> None:
88 """Edit the ``doctype`` in place."""
91def rewrite(
92 html: str,
93 *,
94 elements: Iterable[tuple[str, ElementHandler]] = (),
95 text: TextHandler | None = None,
96 comments: CommentHandler | None = None,
97 doctype: DoctypeHandler | None = None,
98) -> str:
99 """
100 Rewrite ``html`` in a single streaming pass and return the transformed markup.
102 :param html: the markup to rewrite.
103 :param elements: an iterable of ``(selector, handler)`` pairs; ``handler`` is called with the matched
104 :class:`Element` for each element the streamable CSS ``selector`` matches, in document order. When several
105 selectors match one element their handlers run in the order given.
106 :param text: called with each run of character data (a text-kind :class:`Element`); None skips text.
107 :param comments: called with each comment (a comment-kind :class:`Element`); None skips comments.
108 :param doctype: called with the document type declaration (a doctype-kind :class:`Element`); None skips it.
109 :returns: the rewritten markup. An untouched construct is reproduced verbatim, so a rewrite that edits nothing
110 returns the input unchanged (character references and original quoting are preserved).
111 :raises TypeError: if ``html`` is not a str.
112 :raises ValueError: if text a handler wrote into a raw-text element such as ``script`` or ``style`` would end
113 that element before its end tag.
114 :raises SelectorSyntaxError: if a selector is malformed or uses a construct the stream cannot match (a sibling
115 combinator, a positional or structural pseudo-class, or ``:has()``).
116 """
117 return _rewrite(html, tuple(elements), text, comments, doctype)