Parse HTML into a tree¶
Turn markup into a navigable tree with turbohtml.parse(): parse a whole document, feed one arriving in chunks, or
parse a fragment in a given context.
Parse a document arriving in chunks¶
When a document arrives over a stream you do not have to buffer the whole thing before parsing. Feed each chunk to an
turbohtml.IncrementalParser and call close() for the finished Document; the parser
holds only the bytes it has not yet consumed, never the whole source, and the result is identical to parsing the joined
string with turbohtml.parse():
parser = turbohtml.IncrementalParser()
for chunk in ("<ul><li>on", "e<li>two</", "ul>"):
parser.feed(chunk)
document = parser.close()
print([item.text for item in document.find_all("li")])
['one', 'two']
feed also accepts bytes: a chunk is decoded with the parser’s encoding (utf-8 by default), and a
multi-byte character split across two chunks is held back until the rest of its bytes arrive. As a context manager the
parser releases its work-in-progress when the block exits, so you can stop early without leaking the partial parse:
with turbohtml.IncrementalParser(encoding="utf-8") as parser:
parser.feed(b"<p>caf")
parser.feed("é</p>".encode())
document = parser.close()
print(document.find("p").text)
café
Inspect the parse errors of a document¶
turbohtml.parse() recovers from malformed markup the way a browser does and records each WHATWG parse error it
recovered from on errors. Each ParseError carries the spec code and
the source position (1-based line, 0-based col); a well-formed document yields an empty list:
import turbohtml
document = turbohtml.parse("<a b b>")
for error in document.errors:
print(f"{error.code} at {error.line}:{error.col}")
duplicate-attribute at 1:6
To fail instead of recover (in a linter or a strict ingest pipeline), pass strict=True and catch
HTMLParseError, whose error attribute is the first ParseError:
try:
turbohtml.parse("<!DOCTYPE", strict=True)
except turbohtml.HTMLParseError as exception:
print(exception.error.code)
eof-in-doctype
Parse an HTML fragment¶
To parse markup that belongs inside a specific element (a table row, an SVG subtree), use
turbohtml.parse_fragment() with the context tag. It returns that context Element with the
parsed nodes as its children, applying the same insertion rules the element would impose in a full document:
row = turbohtml.parse_fragment("<td>a<td>b", "tr")
print([cell.text for cell in row.find_all("td")])
print(row.html)
['a', 'b']
<tr><td>a</td><td>b</td></tr>
Reproduce the scripting-enabled tree¶
By default turbohtml parses with the WHATWG scripting flag off, so <noscript> content is markup you can navigate:
document = turbohtml.parse("<noscript><a href='/no-js'>plain link</a></noscript>")
print(document.find("a").text)
plain link
A scripting browser instead treats <noscript> as a raw-text element – its content is one text run, never parsed as
markup. Pass scripting=True to build that tree; the inner tags become literal text and serialize back unescaped:
document = turbohtml.parse("<noscript><a href='/no-js'>plain link</a></noscript>", scripting=True)
noscript = document.find("noscript")
print(noscript.text)
print(noscript.html)
<a href='/no-js'>plain link</a>
<noscript><a href='/no-js'>plain link</a></noscript>
Find where an element came from¶
Every parsed element records where its start tag began, so an error report or linter can point back at the source. Read
source_line (1-based), source_col (0-based), or the
position pair, the same convention as html.parser.HTMLParser.getpos() and
lxml’s sourceline:
doc = turbohtml.parse("<ul>\n <li>first</li>\n <li>second</li>\n</ul>")
for item in doc.find_all("li"):
print(item.text, item.position)
first (2, 2)
second (3, 2)
An element with no place in the source (a node built by hand, or an implied html/head/body) reads None.
Pass positions=False to turbohtml.parse() to skip the tracking entirely (a small memory and speed saving),
after which every accessor reads None:
print(turbohtml.parse("<p>x</p>", positions=False).find("p").source_line)
print(turbohtml.Element("div").position)
None
None
Slice the source of every tag and attribute¶
For a linter, a formatter, or a source-mapping tool, the start-tag line and column are not enough: you need the exact
span of the tag, of its closing tag, and of each attribute. Pass source_locations=True to turbohtml.parse()
and read source_location, the SourceLocation record parse5 exposes as
sourceCodeLocationInfo. Every span is a SourceSpan of start/end line, column, and code-point
offset, and the half-open [start_offset, end_offset) slices the construct straight out of the source:
source = '<a href="/x" title="home">go</a>'
link = turbohtml.parse(source, source_locations=True).find("a")
location = link.source_location
start, end = location.start_tag, location.end_tag
print(source[start.start_offset : start.end_offset])
print(source[end.start_offset : end.end_offset])
for name, span in location.attrs.items():
print(name, "->", source[span.start_offset : span.end_offset])
<a href="/x" title="home">
</a>
href -> href="/x"
title -> title="home"
An element the source never closed – a void element, a self-closed one, or one closed implicitly or at end of input –
has end_tag None. Tracking is off by default; source_locations=True implies positions=True, so
source_line and position stay available beside the spans.