#############################
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 :meth:`~turbohtml.Element.append`; the ``text`` setter fills an element with a single text child:
.. testcode::
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)
.. testoutput::
Tea
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:
.. testcode::
print(turbohtml.Element("input", {"class": ["a", "b"], "disabled": None}).html)
.. testoutput::
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:
.. testcode::
doc = turbohtml.parse("
Duplicate a subtree with :func:`python:copy.deepcopy` (or :mod:`python:pickle`); the clone is a standalone tree you can
edit without touching the original:
.. testcode::
import copy
clone = copy.deepcopy(article)
clone.append(turbohtml.Element("footer"))
print(clone.html == article.html)
.. testoutput::
False
Reshaping is not limited to trees you built. When you clean untrusted markup, the sanitizer can rename tags in the same
pass: a :class:`~turbohtml.clean.Policy` with ``transform_tags`` rewrites a source tag to a target (a
:class:`~turbohtml.clean.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:
.. testcode::
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("
Oldmarkup
", policy))
.. testoutput::
Oldmarkup
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:
.. testcode::
policy = Policy(
tags=frozenset({"form", "input"}),
attributes={"form": frozenset({"id"}), "input": frozenset({"name"})},
isolate_named_props=True,
)
print(sanitize('', policy))
.. testoutput::
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:
.. testcode::
policy = Policy(
tags=frozenset({"p"}),
custom_element_check=lambda tag: tag.startswith("x-"),
custom_attribute_check=lambda _tag, name: name.startswith("data-"),
)
print(sanitize('
see stars
', policy))
.. testoutput::
see stars
When you serialize, set an ``Html`` config's ``layout`` to a :class:`~turbohtml.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 ```` tags stay because real whitespace separates the
items:
.. testcode::
from turbohtml import Html, Minify
page = turbohtml.parse("
To keep a record of the edits you make, register a :class:`~turbohtml.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:
.. testcode::
from turbohtml import Element, MutationObserver
page = turbohtml.parse("
one
")
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])
.. testoutput::
childList ['li']
With a tree you can build and reshape, continue to :doc:`exporting` to turn it back into text.