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

48 statements  

1""" 

2turbohtml.saxparse: DOM-less, event-driven HTML parsing. 

3 

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. 

12 

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. 

18 

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

25 

26from __future__ import annotations 

27 

28from typing import TYPE_CHECKING, Final, NamedTuple, cast 

29 

30from ._html import _sax_dispatch, _sax_events 

31 

32if TYPE_CHECKING: 

33 from collections.abc import Callable, Iterator 

34 

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] 

47 

48 

49class SaxHandler: 

50 """ 

51 Base class for a :func:`sax_parse` callback object. 

52 

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

56 

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

59 

60 def end_element(self, tag: str) -> None: 

61 """Handle the end of element ``tag`` (fired for every element, including empty and void ones).""" 

62 

63 def characters(self, data: str) -> None: 

64 """Handle a run of character data ``data``.""" 

65 

66 def comment(self, data: str) -> None: 

67 """Handle a comment with body ``data``.""" 

68 

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

71 

72 def processing_instruction(self, target: str, data: str) -> None: 

73 """Receive non-reserved instructions after the parser separates their target and data.""" 

74 

75 

76class StartElement(NamedTuple): 

77 """A start-tag event.""" 

78 

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

83 

84 

85class EndElement(NamedTuple): 

86 """An end-tag event, fired for every element the parser closes.""" 

87 

88 tag: str 

89 """the element's tag name.""" 

90 

91 

92class Characters(NamedTuple): 

93 """A run of character data.""" 

94 

95 data: str 

96 """the text.""" 

97 

98 

99class Comment(NamedTuple): 

100 """A comment event.""" 

101 

102 data: str 

103 """the comment body, between ``<!--`` and ``-->``.""" 

104 

105 

106class Doctype(NamedTuple): 

107 """A document type declaration event.""" 

108 

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

115 

116 

117class ProcessingInstruction(NamedTuple): 

118 """An HTML instruction whose target is not reserved for XML.""" 

119 

120 target: str 

121 """the ASCII target, preserving source case.""" 

122 data: str 

123 """the text after leading whitespace and before the closing delimiter.""" 

124 

125 

126SaxEvent = StartElement | EndElement | Characters | Comment | Doctype | ProcessingInstruction 

127"""The union of every event :func:`iter_events` yields.""" 

128 

129_EVENTS: Final[tuple[Callable[..., SaxEvent], ...]] = ( 

130 StartElement, 

131 EndElement, 

132 Characters, 

133 Comment, 

134 Doctype, 

135 ProcessingInstruction, 

136) 

137 

138 

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. 

142 

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:]) 

150 

151 

152def sax_parse(html: str, handler: SaxHandler) -> None: 

153 """ 

154 Parse ``html``, calling ``handler``'s methods for each event, and retain no tree. 

155 

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)