From sanitize-html¶
sanitize-html is the standard Node HTML sanitizer: you declare the
tags, attributes, URL schemes, and CSS properties you trust, and everything else is stripped. Its distinguishing feature
is transformTags – a map that renames an element (and optionally rewrites its attributes) during the sanitize pass,
with a simpleTransform(tagName, attribs, merge) helper for the common rename-and-add case. Projects that ran
sanitization in a Node service, or in a Python service shelling out to Node, reach for it because bleach and its
successors historically had no equivalent rename step.
turbohtml’s turbohtml.clean sanitizer covers the same allowlist surface behind a frozen, thread-safe
Policy, and Policy.transform_tags is the direct port of transformTags: it renames HTML
elements during the same single C walk, with Transform playing the role of
simpleTransform. Moving the sanitize step into Python drops the Node subprocess, and the second language with it.
turbohtml vs sanitize-html¶
Dimension |
turbohtml |
sanitize-html |
|---|---|---|
Runtime |
Python, filtering in a C extension |
Node.js, filtering in JavaScript over htmlparser2 |
Rename step |
|
|
Rename safety |
Renamed element re-checked against the allowlist and the unconditional baseline |
Renamed element re-checked against |
Configuration |
One frozen |
A plain options object per call |
Typing |
Fully annotated, |
TypeScript definitions |
Dependencies |
None (self-contained C extension) |
htmlparser2, and its transitive tree |
Feature overlap¶
The transform surface ports one-to-one:
transformTags: { b: 'strong' }(rename) ->transform_tags={"b": "strong"}.transformTags: { center: sanitizeHtml.simpleTransform('div', { class: 'x' }) }(rename and add attributes) ->transform_tags={"center": Transform("div", {"class": "x"})}.allowedTags/allowedAttributes->Policy.tags/Policy.attributes.allowedSchemes->Policy.url_schemes;allowedStyles->Policy.allowed_styles.
Both apply the rename before the allowlist, so a transform decides an element’s name but never its safety: mapping a tag
to script still drops it, and an attribute added by a transform is scrubbed like the element’s own. turbohtml keeps
that guarantee under an unconditional baseline – on* handlers, scripting elements, and javascript: URLs are
removed regardless of the policy – so a transform can smuggle neither a disallowed tag nor an unscrubbed attribute.
The one shape difference: transformTags also accepts an arbitrary callback returning { tagName, attribs, text },
and a '*' entry that runs on every tag. transform_tags is a declarative per-tag map (a string or a
Transform); it renames and adds attributes without running caller code mid-walk, and per-tag
rules cover the presentational-tag modernization simpleTransform was built for. Only HTML elements are transformed.
Porting a transform¶
A sanitize-html config that modernizes legacy presentational tags:
const clean = sanitizeHtml(dirty, {
allowedTags: ["strong", "em", "div"],
allowedAttributes: { div: ["class"] },
transformTags: {
b: "strong",
i: "em",
center: sanitizeHtml.simpleTransform("div", { class: "legacy" }),
},
});
ports to:
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": "legacy"}),
},
)
print(sanitize("<center><b>bold</b> and <i>italic</i></center>", policy))
<div class="legacy"><strong>bold</strong> and <em>italic</em></div>
Performance¶
turbohtml renames in the same C walk that sanitizes, so a transform adds no extra pass. The table times both libraries end-to-end on a document dense in the presentational tags a transform rewrites; the sanitize-html figure is its Node runner over stdin, the cost a Python service pays to reach it as a subprocess.
input |
turbohtml |
|
|---|---|---|
legacy 4 KiB |
54 µs |
56.5 ms (1046x) |