Building and editing a tree¶
Everything so far read a document that already existed. You can also build one. Construct nodes from their classes and
assemble them with append(); the text setter fills an element with a single text child:
import turbohtml
from turbohtml import Element, Comment
article = turbohtml.Element("article", {"class": "post"})
title = turbohtml.Element("h1")
title.text = "Tea"
article.append(title)
article.append(Comment("draft"))
print(article.html)
<article class="post"><h1>Tea</h1><!--draft--></article>
A list value for a token-list attribute (class, rel, …) joins on a space, and None (or "") sets an
empty attribute, which reads back as the empty string:
print(turbohtml.Element("input", {"class": ["a", "b"], "disabled": None}).html)
<input class="a b" disabled="">
Editing a parsed tree uses the BeautifulSoup vocabulary (insert_before, replace_with, wrap, unwrap,
decompose), and element.attrs is a live mapping you assign to. A node already in a tree moves; a node from
another tree is adopted by copy:
doc = turbohtml.parse("<p>keep <b>bold</b> <span>drop</span></p>")
print(doc.find("b").unwrap())
doc.find("span").decompose()
doc.find("p").attrs["class"] = "lead"
print(doc.find("p").html)
Element('b')
<p class="lead">keep bold </p>
Duplicate a subtree with copy.deepcopy() (or pickle); the clone is a standalone tree you can
edit without touching the original:
import copy
clone = copy.deepcopy(article)
clone.append(turbohtml.Element("footer"))
print(clone.html == article.html)
False
Reshaping is not limited to trees you built. When you clean untrusted markup, the sanitizer can rename tags in the same
pass: a Policy with transform_tags rewrites a source tag to a target (a
Transform also adds attributes). The rename runs before the allowlist, so the result is
re-checked – modernizing legacy tags while the safety baseline still applies:
from turbohtml.clean import sanitize, Policy, Transform
policy = Policy(
tags=frozenset({"strong", "em", "div"}),
attributes={"div": frozenset({"class"})},
transform_tags={"b": "strong", "i": "em", "center": Transform("div", {"class": "center"})},
)
print(sanitize("<center><b>Old</b> <i>markup</i></center>", policy))
<div class="center"><strong>Old</strong> <em>markup</em></div>
The same pass can defuse DOM clobbering. An id or name whose value matches a built-in property
(form.attributes, document.body) shadows it through named access, and the allowlist keeps such an attribute
because it is otherwise ordinary. Turn on isolate_named_props to prefix every kept id and name value with
user-content-, moving it out of the property namespace:
policy = Policy(
tags=frozenset({"form", "input"}),
attributes={"form": frozenset({"id"}), "input": frozenset({"name"})},
isolate_named_props=True,
)
print(sanitize('<form id="settings"><input name="attributes"></form>', policy))
<form id="user-content-settings"><input name="user-content-attributes"></form>
To keep an app’s own custom elements without listing every one, hand the policy a matcher instead of a name set:
custom_element_check decides whether an unlisted hyphenated element survives, and custom_attribute_check which
of its attributes do. The safety baseline is unchanged, so the onclick here is dropped even though the matcher keeps
the element:
policy = Policy(
tags=frozenset({"p"}),
custom_element_check=lambda tag: tag.startswith("x-"),
custom_attribute_check=lambda _tag, name: name.startswith("data-"),
)
print(sanitize('<p>see <x-rating data-stars="5" onclick="steal()">stars</x-rating></p>', policy))
<p>see <x-rating data-stars="5">stars</x-rating></p>
When you serialize, set an Html config’s layout to a Minify to shrink the output without
changing what it means: it folds insignificant whitespace, omits optional tags, unquotes safe attributes, and strips
comments, and the result reparses to the same tree. Here the </li> tags stay because real whitespace separates the
items:
from turbohtml import Html, Minify
page = turbohtml.parse("<ul>\n <li>one</li>\n <li>two</li>\n</ul>")
print(page.find("ul").serialize(Html(layout=Minify())))
<ul> <li>one</li> <li>two</li> </ul>
To keep a record of the edits you make, register a MutationObserver on a node before changing it,
then read the changes back with take_records. Delivery is synchronous – turbohtml has no event loop, so the records
are ready the moment you ask, not on a later microtask:
from turbohtml import Element, MutationObserver
page = turbohtml.parse("<ul><li>one</li></ul>")
listing = page.find("ul")
observer = MutationObserver()
observer.observe(listing, child_list=True)
listing.append(Element("li"))
(record,) = observer.take_records()
print(record.type, [added.tag for added in record.added_nodes])
childList ['li']
With a tree you can build and reshape, continue to Exporting to text to turn it back into text.