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>

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