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

19 statements  

1""" 

2turbohtml.rewrite: a DOM-less, single-pass HTML rewriter. 

3 

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. 

9 

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. 

13 

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""" 

20 

21from __future__ import annotations 

22 

23from typing import TYPE_CHECKING, Protocol 

24 

25from ._html import _rewrite, _RewriteHandle 

26 

27if TYPE_CHECKING: 

28 from collections.abc import Iterable 

29 

30__all__ = [ 

31 "CommentHandler", 

32 "DoctypeHandler", 

33 "Element", 

34 "ElementHandler", 

35 "TextHandler", 

36 "rewrite", 

37] 

38 

39Element = _RewriteHandle 

40"""The node handle a rewrite handler receives. 

41 

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. 

47 

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). 

51 

52The rewrite writes an edited element's start tag again from its attributes and escapes ``&``, ``"``, ``<`` and ``>`` in 

53each value. 

54 

55The handle is valid only for the duration of the handler call; stashing it and using it afterwards raises 

56:class:`RuntimeError`. 

57 

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""" 

61 

62 

63class ElementHandler(Protocol): 

64 """A callback invoked with the matched :class:`Element` for each element a selector matches.""" 

65 

66 def __call__(self, element: Element, /) -> None: 

67 """Edit the matched ``element`` in place.""" 

68 

69 

70class TextHandler(Protocol): 

71 """A callback invoked with each run of character data as an :class:`Element` handle of text kind.""" 

72 

73 def __call__(self, text: Element, /) -> None: 

74 """Edit the ``text`` run in place.""" 

75 

76 

77class CommentHandler(Protocol): 

78 """A callback invoked with each comment as an :class:`Element` handle of comment kind.""" 

79 

80 def __call__(self, comment: Element, /) -> None: 

81 """Edit the ``comment`` in place.""" 

82 

83 

84class DoctypeHandler(Protocol): 

85 """A callback invoked with the document type declaration as an :class:`Element` handle of doctype kind.""" 

86 

87 def __call__(self, doctype: Element, /) -> None: 

88 """Edit the ``doctype`` in place.""" 

89 

90 

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. 

101 

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)