Clean

Clean untrusted or raw HTML: sanitize it against an allowlist and rewrite bare URLs into links. Sanitizing is a successor to bleach.clean – build a Policy (or take a preset), then sanitize; a non-overridable baseline removes scripting elements, event-handler attributes, and javascript: URLs regardless of the policy. Linkifying is a successor to bleach.linkify – it finds URLs and email addresses and wraps them in <a> links, HTML-aware so it never links inside an existing <a>, a raw-text element, or a caller’s skip_tags.

turbohtml.clean.sanitize(html, options=None)[source]

Sanitize an HTML fragment against a policy.

Parameters:
  • html (str) – the untrusted HTML fragment.

  • options (Policy | None) – the policy to enforce; None uses bleach’s default allowlist.

Return type:

str

Returns:

the sanitized, safe HTML.

Raises:
  • TypeError – if a set-typed policy field (tags, url_schemes, remove_with_content, css_properties, attribute_prefixes, media_hosts) holds a value that is not a set or frozenset, or attribute_prefixes contains a non-string.

  • ValueError – if attribute_prefixes contains an empty string, which would match every attribute.

sanitize_report() sanitizes and also returns what the policy dropped, one Removed record per removed element or stripped attribute, the way DOMPurify populates DOMPurify.removed.

turbohtml.clean.sanitize_report(html, options=None)[source]

Sanitize a fragment and report what the policy dropped, like DOMPurify’s DOMPurify.removed.

Parameters:
  • html (str) – the untrusted HTML fragment.

  • options (Policy | None) – the policy to enforce; None uses bleach’s default allowlist.

Return type:

tuple[str, list[Removed]]

Returns:

the sanitized HTML paired with one Removed record per dropped element or attribute.

Raises:
class turbohtml.clean.Removed(tag, attribute=None)[source]

One item a policy dropped, the shape Sanitizer.sanitize_report() returns.

Parameters:
  • tag (str) – the tag name of the dropped element, or of the element a dropped attribute was on.

  • attribute (str | None) – the dropped attribute’s name, or None when the element itself was dropped.

class turbohtml.clean.Sanitizer(options=None)[source]

A reusable sanitizer; build it once from a Policy and call sanitize() from any thread.

Parameters:

options (Policy | None) – the policy to enforce; None uses bleach’s default allowlist.

sanitize(html)[source]

Sanitize an HTML fragment.

Parameters:

html (str) – the untrusted HTML fragment.

Return type:

str

Returns:

the sanitized, safe HTML.

Raises:
  • TypeError – if a set-typed policy field (tags, url_schemes, remove_with_content, css_properties, attribute_prefixes, media_hosts) holds a value that is not a set or frozenset, or attribute_prefixes contains a non-string.

  • ValueError – if attribute_prefixes contains an empty string, which would match every attribute.

sanitize_report(html)[source]

Sanitize a fragment and report what the policy dropped, the way DOMPurify populates DOMPurify.removed.

Every disallowed element (removed, stripped, or escaped) and every stripped attribute becomes one Removed record, in the order the walk reached it, so a caller can log or tune a policy against evidence instead of guessing.

Parameters:

html (str) – the untrusted HTML fragment.

Return type:

tuple[str, list[Removed]]

Returns:

the sanitized HTML paired with the list of dropped items.

Raises:
class turbohtml.clean.Policy(tags=frozenset({'a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'strong', 'ul'}), attributes=<factory>, url_schemes=frozenset({'http', 'https', 'mailto'}), allow_relative_urls=True, on_disallowed_tag=OnDisallowed.ESCAPE, strip_comments=True, add_link_rel=frozenset({}), attribute_filter=None, set_attributes=<factory>, remove_with_content=frozenset({}), css_properties=frozenset({'azimuth', 'background-color', 'border-bottom-color', 'border-collapse', 'border-color', 'border-left-color', 'border-right-color', 'border-top-color', 'clear', 'color', 'cursor', 'direction', 'display', 'elevation', 'fill', 'fill-opacity', 'fill-rule', 'float', 'font', 'font-family', 'font-size', 'font-style', 'font-variant', 'font-weight', 'height', 'letter-spacing', 'line-height', 'overflow', 'pause', 'pause-after', 'pause-before', 'pitch', 'pitch-range', 'richness', 'speak', 'speak-header', 'speak-numeral', 'speak-punctuation', 'speech-rate', 'stress', 'stroke', 'stroke-linecap', 'stroke-linejoin', 'stroke-opacity', 'stroke-width', 'text-align', 'text-decoration', 'text-indent', 'unicode-bidi', 'vertical-align', 'voice-family', 'volume', 'white-space', 'width'}), attribute_prefixes=frozenset({}), attribute_values=<factory>, media_hosts=frozenset({}), strip_template_markers=False, allowed_styles=<factory>, transform_tags=<factory>, isolate_named_props=False, custom_element_check=None, custom_attribute_check=None, allow_customized_builtins=False, allow_html=True, allow_svg=True, allow_mathml=True, xml=False)[source]

An immutable, thread-safe description of what sanitizing keeps.

Build one and reuse it across threads. Whatever a policy allows, a non-configurable baseline still removes the unsafe element set, event-handler attributes, and dangerous URL schemes, so every policy is safe by construction.

Parameters:
  • tags (frozenset[str]) – the allowed element set; any tag outside it is handled per on_disallowed_tag.

  • attributes (Mapping[str, frozenset[str]]) – allowed attribute names keyed by tag ("*" as the key matches every tag, and "*" inside a set allows every name).

  • url_schemes (frozenset[str]) – the allowlist for URL-bearing attributes such as href and src.

  • allow_relative_urls (bool) – keep relative (scheme-less) URLs, which carry no scheme to check.

  • on_disallowed_tag (OnDisallowed) – how to treat a tag not in tags (OnDisallowed: escape, strip, or remove).

  • strip_comments (bool) – drop HTML comments from the output.

  • add_link_rel (frozenset[str]) – rel tokens forced onto every kept <a href> (e.g. noopener).

  • attribute_filter (Callable[[str, str, str], str | None] | None) – an optional last word over every surviving attribute, returning a replacement value or None to drop it.

  • set_attributes (Mapping[str, Mapping[str, str]]) – attribute values forced onto every kept instance of a tag (added if absent, overwritten if present); unlike attribute_filter, this can add attributes that were not there.

  • remove_with_content (frozenset[str]) – disallowed tags whose whole subtree is dropped (e.g. script/style) rather than escaped or stripped, so their text never leaks into the output.

  • css_properties (frozenset[str]) – the CSS property allowlist. A kept style attribute and, when style is in tags, the <style> element’s stylesheet body are both scrubbed against it: any declaration whose property name is not in the set (or whose value smuggles expression() or a url() with a disallowed scheme) is dropped, while selectors and block nesting are kept, so dangerous CSS cannot ride in on a kept style.

  • attribute_prefixes (frozenset[str]) – allow any attribute whose name starts with one of these prefixes (e.g. "data-" for every data-*), on top of the exact-name and "*" matches in attributes.

  • attribute_values (Mapping[str, Mapping[str, frozenset[str]]]) – restrict a kept attribute to literal values, keyed {tag: {attribute: allowed_values}}; a surviving attribute whose value is outside its set is dropped. This narrows an attribute attributes already admits and cannot admit a new one.

  • transform_tags (Mapping[str, Transform | str]) – rename rules applied before the allowlist, keyed by source tag (sanitize-html’s transformTags). A value that is a plain string renames the element to it; a Transform renames it and adds attributes. The rename runs first, then the renamed element is re-checked from scratch, so a transform decides an element’s name but never its safety: the allowlist still governs the target tag (mapping to script still drops it), and any added attributes are scrubbed like the element’s own. Only HTML elements are transformed, matched by tag name. Empty (the default) renames nothing.

  • allowed_styles (Mapping[str, Mapping[str, Sequence[str | Pattern[str]]]]) – a per-property value allowlist for the style attribute, keyed {tag: {property: [pattern, ...]}} with "*" as a tag matching every element (sanitize-html’s allowedStyles). A declaration survives only when its property is listed for the element’s tag or "*" (their pattern lists union) and its value matches one of the patterns (an unanchored re.search()). Patterns are strings or precompiled re.Pattern. This narrows on top of css_properties and the non-configurable dangerous-value baseline: a property this admits is still dropped unless css_properties lists it too, and expression() or a url() with a disallowed scheme is dropped even if a pattern would match it. Empty (the default) leaves css_properties as the only style filter.

  • media_hosts (frozenset[str]) – allowed hosts for an embedded-media src (audio, video, source, track); a src whose URL host is not one of these lowercase entries is dropped. Empty means no host restriction.

  • strip_template_markers (bool) – collapse template-engine expressions ({{ }}, ${ }, <% %>) in kept text and attribute values to a single space, so sanitized output cannot re-inject when a template engine (Angular, Vue, Mustache, EJS, ERB) later renders it. DOMPurify’s SAFE_FOR_TEMPLATES; off by default.

  • isolate_named_props (bool) – prefix every kept id and name value with user-content- so it cannot shadow a built-in document or form property through named access (DOM clobbering: <input name="attributes"> makes form.attributes resolve to the input, <img name="body"> hides document.body). The prefix is left alone when already present, so re-sanitizing is a fixpoint. DOMPurify’s SANITIZE_NAMED_PROPS; off by default.

  • custom_element_check (Callable[[str], bool] | None) – a predicate that keeps an unlisted custom element (a hyphenated HTML tag such as my-widget or x-card) without naming it in tags, DOMPurify’s CUSTOM_ELEMENT_HANDLING.tagNameCheck. Given the element’s lowercased tag name, return true to keep it (pass re.compile(r"x-.*").search to drive it from a regex). Only basic custom-element names reach it (the reserved annotation-xml/font-face family never does), and the safety baseline still escapes unsafe tags and scrubs on*/URL/style attributes on whatever it keeps. None (the default) escapes every unlisted tag as before.

  • custom_attribute_check (Callable[[str, str], bool] | None) – a predicate that keeps an unlisted attribute on a kept custom element, DOMPurify’s CUSTOM_ELEMENT_HANDLING.attributeNameCheck. Given (tag, attribute_name) return true to keep it; the on*, URL-scheme, and style scrubbing still apply, so this widens the name allowlist, never the safety baseline. None (the default) keeps only the attributes attributes already admits.

  • allow_customized_builtins (bool) – keep an is attribute whose value passes custom_element_check, so a customized built-in element (<button is="my-button">) survives, DOMPurify’s allowCustomizedBuiltInElements. Off by default, and inert without custom_element_check.

  • allow_html (bool) – keep HTML-namespace elements; turning it off drops the whole HTML namespace, DOMPurify’s USE_PROFILES.html. Independent of the tag allowlist, so it composes with allow_svg/allow_mathml to select which content languages a policy admits.

  • allow_svg (bool) – keep SVG-namespace elements, DOMPurify’s USE_PROFILES.svg. Off drops every SVG element even when its tag is in tags.

  • allow_mathml (bool) – keep MathML-namespace elements, DOMPurify’s USE_PROFILES.mathMl. Off drops every MathML element even when its tag is in tags.

  • xml (bool) – emit well-formed XML/XHTML instead of HTML, DOMPurify’s RETURN_DOM served through the XML serializer. Every kept empty element self-closes (<br/>), foreign SVG and MathML subtrees carry their namespace declarations, text and attribute values follow the XML escaping rules, and a kept comment or a stray control character is neutralized so the result reparses through turbohtml.parse_xml(). Use it to clean an XHTML dialect (Reportlab’s RML, ePub content) whose consumer rejects HTML’s bare <br>. Off (the default) keeps the HTML serialization.

classmethod strict()[source]

Allow no markup at all: every tag is escaped to text and every attribute dropped.

Return type:

Policy

Returns:

the strict policy.

classmethod basic()[source]

Allow bleach’s default 12-tag set, for migration parity.

Return type:

Policy

Returns:

the basic policy.

classmethod relaxed()[source]

Allow the richer set typical user-generated content needs: headings, tables, images, and figures.

Return type:

Policy

Returns:

the relaxed policy.

Policy.css_properties allowlists style property names; Policy.allowed_styles narrows further by value, the way sanitize-html’s allowedStyles does. Key it {tag: {property: [pattern, ...]}} ("*" matches every tag); a style declaration survives only when its property is listed for the element’s tag or "*" and its value matches one of the patterns via an unanchored re.search(). It runs on top of css_properties and the dangerous-value baseline – the property must still be in css_properties, and expression() or a disallowed-scheme url() is dropped even when a pattern would admit it:

from turbohtml.clean import sanitize, Policy

policy = Policy(
    tags=frozenset({"p"}),
    attributes={"p": frozenset({"style"})},
    css_properties=frozenset({"color"}),
    allowed_styles={"*": {"color": [r"^#[0-9a-f]{3,6}$"]}},
)
print(sanitize('<p style="color: #0a0; color: red">ok</p>', policy))
<p style="color: #0a0">ok</p>

Policy.transform_tags renames elements during the same walk, sanitize-html’s transformTags. Key it by source tag: map to a bare string to rename, or to a Transform to rename and add attributes. The rename runs before the allowlist, so the renamed element is re-checked from scratch – a transform decides an element’s name but never its safety. Mapping a tag to script still drops it, and an added attribute is scrubbed like the element’s own, so it must be allowlisted to survive:

from turbohtml.clean import sanitize, Policy, Transform

policy = Policy(
    tags=frozenset({"strong", "div"}),
    attributes={"div": frozenset({"class"})},
    transform_tags={"b": "strong", "center": Transform("div", {"class": "center"})},
)
print(sanitize("<b>bold</b> and <center>middle</center>", policy))
<strong>bold</strong> and <div class="center">middle</div>

Policy.isolate_named_props prefixes every kept id and name value with user-content-, DOMPurify’s SANITIZE_NAMED_PROPS. It stops DOM clobbering – an id or name whose value matches a built-in document or form property shadows that property through named access – by moving the value out of the property namespace. An already-prefixed value is left alone, so re-sanitizing is a fixpoint. The isolation is applied after attribute_filter, so the value a filter returns is the one that gets namespaced:

from turbohtml.clean import sanitize, Policy

policy = Policy(
    tags=frozenset({"input"}),
    attributes={"input": frozenset({"name"})},
    isolate_named_props=True,
)
print(sanitize('<input name="attributes">', policy))
<input name="user-content-attributes">

Policy.custom_element_check keeps an unlisted hyphenated custom element when its matcher admits the tag name, DOMPurify’s CUSTOM_ELEMENT_HANDLING.tagNameCheck; custom_attribute_check does the same for that element’s attributes (attributeNameCheck), and allow_customized_builtins keeps an is attribute naming a custom element. The safety baseline still runs on whatever the matcher keeps, so an on* handler is dropped even here:

from turbohtml.clean import sanitize, Policy

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><x-card data-id="7" onclick="x">c</x-card></p>', policy))
<p><x-card data-id="7">c</x-card></p>

Policy.allow_html, Policy.allow_svg, and Policy.allow_mathml gate each namespace independently, DOMPurify’s USE_PROFILES. All default on; turning one off drops that whole namespace even when its tags are allowlisted, so a policy can keep SVG but not MathML:

policy = Policy(tags=frozenset({"svg", "circle", "math", "mi"}), allow_svg=False)
print(sanitize("<svg><circle></circle></svg><math><mi>x</mi></math>", policy))
&lt;svg&gt;&lt;circle&gt;&lt;/circle&gt;&lt;/svg&gt;<math><mi>x</mi></math>

Policy.xml serializes the cleaned tree as well-formed XML/XHTML rather than HTML. The walk is unchanged, so the policy is exactly as safe; the difference is output syntax – an empty element self-closes, values follow the XML escaping rules, a foreign root declares its namespace, and any comment, control character, or attribute name XML cannot hold is neutralized – so the result reparses through turbohtml.parse_xml(). Use it to feed an XHTML dialect that rejects HTML’s bare <br>:

policy = Policy(tags=frozenset({"p", "br"}), xml=True)
print(sanitize("<p>one<br>two</p>", policy))
<p>one<br/>two</p>
class turbohtml.clean.Transform(tag, attributes=<factory>)[source]

A rename rule for Policy.transform_tags, sanitize-html’s simpleTransform.

Mapping a source tag to a bare string renames it; mapping it to a Transform renames it and adds attributes. The added attributes are not trusted: they join the element’s own attributes and go through the same allowlist and URL/style scrubbing, so a transform cannot add an attribute the policy would otherwise strip.

Parameters:
  • tag (str) – the target tag name to rename to.

  • attributes (Mapping[str, str]) – attribute name/value pairs to add to the renamed element (added if absent, overwritten if present).

class turbohtml.clean.OnDisallowed(*values)[source]

What to do with an element the policy does not allow.

ESCAPE renders the tag as visible text (<x> becomes &lt;x&gt;), the safe bleach default that keeps the content readable; STRIP drops the tag but keeps its children; REMOVE drops the tag and its whole subtree.

The sanitizer ships bleach’s default allowlists as module constants, so a Policy can extend a known baseline instead of enumerating a safe set from scratch.

turbohtml.clean.DEFAULT_TAGS

Build an immutable unordered collection of unique elements.

The tags the default policy keeps: a, abbr, acronym, b, blockquote, code, em, i, li, ol, strong, ul.

turbohtml.clean.DEFAULT_ATTRIBUTES

Read-only proxy of a mapping.

The attributes the default policy keeps, keyed by tag: href and title on a, and title on abbr and acronym.

turbohtml.clean.DEFAULT_SCHEMES

Build an immutable unordered collection of unique elements.

The URL schemes the default policy allows in an href or src: http, https, mailto.

turbohtml.clean.DEFAULT_CSS_PROPERTIES

Build an immutable unordered collection of unique elements.

The CSS properties the default policy keeps when scrubbing a style attribute: the CSS 2.1 safe set plus the SVG paint properties.

Linkifying

A Linkify configuration object carries the knobs: a callback receives each generated LinkCandidate and returns it to keep the link or None to leave the text bare, process_existing runs the callbacks over <a> tags already in the input (a callback reads LinkCandidate.existing to tell the two apart), extra_tlds extends bare-domain detection beyond the built-in IANA table, and schemes sets which explicit-scheme URLs autolink (defaulting to the built-in http/https/ftp set, so a typo scheme or a javascript:// payload stays plain text).

turbohtml.clean.linkify(text, options=None)[source]

Find URLs and email addresses in HTML and wrap them in <a> links, leaving existing markup untouched.

Parameters:
  • text (str) – the HTML to linkify.

  • options (Linkify | None) – the configuration to apply; None uses DEFAULT_CALLBACKS and detects nothing else.

Return type:

str

Returns:

the linkified HTML.

class turbohtml.clean.Linkify(callbacks=(<function nofollow>, ), skip_tags=None, parse_email=False, process_existing=False, extra_tlds=None, schemes=None)[source]

An immutable, thread-safe description of how linkify() and Linker find and rewrite links.

Build one and reuse it across threads.

Parameters:
  • callbacks (Iterable[Callable[[LinkCandidate], LinkCandidate | None]]) – callables run on each detected link to adjust or veto it (defaults to DEFAULT_CALLBACKS).

  • skip_tags (Iterable[str] | None) – tags whose text is left untouched, such as pre and code.

  • parse_email (bool) – also autolink bare email addresses as mailto: links.

  • process_existing (bool) – run the callbacks over <a> tags already present, not only freshly detected links.

  • extra_tlds (Iterable[str] | None) – top-level domains that make a bare domain a link, on top of the built-in IANA table.

  • schemes (Iterable[str] | None) – the exact set of scheme:// URL schemes that autolink; None keeps the built-in http/https/ftp default, so a typo scheme or a javascript:// payload stays plain text. A bare domain is always treated as http and is governed by the TLD table, not schemes.

class turbohtml.clean.Linker(options=None)[source]

A reusable linkifier; build it once from a Linkify configuration and call linkify() per document.

Parameters:

options (Linkify | None) – the configuration to apply; None uses DEFAULT_CALLBACKS and detects nothing else.

linkify(text)[source]

Linkify HTML, leaving everything but eligible text runs untouched.

Parameters:

text (str) – the HTML to linkify.

Return type:

str

Returns:

the linkified HTML.

class turbohtml.clean.LinkCandidate(url, text, attrs=None, *, existing=False)[source]

A link handed to each callback to mutate or veto.

A callback returns the link to keep it, or None to drop the anchor: a detected link stays plain text, an existing one is unwrapped to its contents.

Parameters:
  • url (str) – the link’s href.

  • text (str) – the visible link text the reader sees.

  • attrs (dict[str, str] | None) – extra attributes to put on the <a> (rel, target, class, …).

  • existing (bool) – True when reprocessing an <a> already in the input, False for a freshly detected link.

turbohtml.clean.nofollow(link)[source]

Add rel="nofollow" to a web link so search engines skip it, leaving mailto: and other links alone.

Parameters:

link (LinkCandidate) – the link to adjust.

Return type:

LinkCandidate | None

Returns:

the link, with nofollow added when it is a web link.

turbohtml.clean.target_blank(link)[source]

Open a web link in a new tab, stripping a stale target from a non-web link so it cannot leak through.

Parameters:

link (LinkCandidate) – the link to adjust.

Return type:

LinkCandidate | None

Returns:

the link, with target set on a web link or cleared on a non-web link.

turbohtml.clean.Callback

str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to ‘utf-8’. errors defaults to ‘strict’.

The type of a linkify callback: a callable that takes one LinkCandidate and returns it to keep the link or None to leave the text bare. nofollow() and target_blank() are built-in examples.

turbohtml.clean.DEFAULT_CALLBACKS

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable’s items.

If the argument is a tuple, the return value is the same object.

The callbacks linkify() applies when the caller passes none: (nofollow,), so bare-URL links get rel="nofollow" unless you opt out.

To only locate links in plain text rather than rewrite HTML, use LinkDetector. It returns a LinkSpan for each match and accepts custom tlds and scheme-less schemes.

class turbohtml.clean.LinkDetector(*, emails=True, bare_domains=True, tlds=(), schemes=())[source]

Find the links in plain text, configured once and reused per call.

Unlike Linker, which rewrites HTML, a detector only locates links and hands back LinkSpan objects, leaving the text untouched.

Parameters:
  • emails (bool) – detect bare email addresses.

  • bare_domains (bool) – detect bare domains (example.com) with no explicit scheme.

  • tlds (Iterable[str]) – custom top-level domains accepted for bare-domain matching, on top of the IANA table.

  • schemes (Iterable[str]) – extra schemes to detect, both as scheme-less opaque URLs (tel:, bitcoin:) and as scheme:// authority URLs, on top of the built-in http/https/ftp set; an unregistered scheme such as javascript:// or a typo like hppt:// is not detected.

find(text)[source]

Find every link in a run of text.

Parameters:

text (str) – the text to scan.

Return type:

list[LinkSpan]

Returns:

every link as a LinkSpan, in the order it appears.

Test a run of text for any link, cheaper than find() when only presence matters.

Parameters:

text (str) – the text to scan.

Return type:

bool

Returns:

whether the text contains at least one link.

class turbohtml.clean.LinkSpan(start, end, text, url, is_email)[source]

One URL or email address found in a run of plain text.

Parameters:
  • start (int) – the half-open start offset of the match in the scanned text.

  • end (int) – the half-open end offset of the match in the scanned text.

  • text (str) – the matched substring exactly as it appeared.

  • url (str) – the normalized href (mailto: for an email, http:// for a bare domain, the text itself for a scheme:// or registered scheme-less URL).

  • is_email (bool) – whether the match is an email address.

Minifying

minify() shrinks an HTML document in one call – it parses the input and serializes it through the round-trip-safe Minify layout, so the output reparses to the same tree and minifying is idempotent (minify(minify(x)) == minify(x)). It replaces minify-html and htmlmin. The four transforms (fold insignificant whitespace, omit optional tags, unquote attributes, strip comments) default on; pass a Minify to turn any off.

turbohtml.clean.minify(html, options=None)[source]

Minify an HTML document.

Parse html into a tree and serialize it with the minifying layout, folding insignificant whitespace, omitting the start/end tags the WHATWG rules make optional, dropping redundant attribute quotes, and stripping comments. Each transform is round-trip safe, so the result reparses to the same tree and minifying is idempotent.

Parameters:
  • html (str) – the HTML document to minify.

  • options (Minify | None) – which folds to apply; None engages every transform (the Minify default).

Return type:

str

Returns:

the minified HTML.

The HTML minify layout emits <style> bodies verbatim; to also minify embedded CSS, run minify_css() (below) over a <style> body yourself, which is what minify-html’s minify_css did inline. Passing a JSMinify as Minify(minify_js=...) rewrites inline <script> content, covering minify-html’s minify_js. The doctype is always normalized to <!doctype html> (minify-html’s minify_doctype is implicit), and HTML has no processing instructions to drop (remove_processing_instructions is moot under the WHATWG parser, which reads them as bogus comments).

JS minification

minify_js() minifies a JavaScript string on its own, the jsmin/rjsmin successor. It always folds whitespace, comments, and number literals; a frozen JSMinify toggles the optional mangle (rename local bindings) and fold (constant-fold and eliminate dead code) passes. The same JSMinify passed as Minify(minify_js=...) rewrites inline <script> content during HTML minification.

turbohtml.clean.minify_js(source, options=None, on_error='raise')[source]

Minify a JavaScript source string, returning the shortest equivalent program.

Every transform preserves semantics, so the result evaluates identically to source. options defaults to the full pipeline (fold and mangle).

on_error chooses what happens when the parser cannot handle the script. The default "raise" fails loudly; "passthrough" returns source unchanged instead, the never-fail contract jsmin and rjsmin guarantee – the same verbatim fallback the inline-<script> path already applies so one bad script cannot break serialization.

Parameters:
  • source (str) – the JavaScript source to minify.

  • options (JSMinify | None) – which optional passes to run; None runs the full pipeline.

  • on_error (Literal['raise', 'passthrough']) – "raise" (default) to raise on a script the parser cannot handle, or "passthrough" to return source unchanged.

Return type:

str

Returns:

the minified JavaScript, or source verbatim when it is unparsable and on_error="passthrough".

Raises:
  • TypeError – if source is not a str.

  • ValueError – if on_error is not "raise" or "passthrough", or – when on_error="raise" – if the parser cannot handle the script (a lexical or syntax error, or input nested past the depth limit), with the message naming the construct, its byte offset, and the offending token.

class turbohtml.clean.JSMinify(mangle=True, fold=True)[source]

Which optional JavaScript-minifier passes to run.

Whitespace and numeric-literal minification is unconditional, as is comment removal – except /*! ... */ bang comments and any comment carrying an @license or @preserve annotation, which are kept byte-exact as a leading banner so a license header survives (the same rule the CSS minifier applies). mangle renames local bindings to short names (the bulk of the size win) and fold runs constant folding and dead-code elimination; turning either off keeps that aspect of the source readable (e.g. mangle=False for debuggable output).

Parameters:
  • mangle (bool) – rename local bindings to short names.

  • fold (bool) – constant-fold and eliminate dead code.

Raises:

TypeError – if constructed with an unexpected or extra argument.

CSS minification

Minify CSS the value-safe way: every transform produces output that parses to the same cascade. minify_css() takes a whole stylesheet; minify_css_inline() takes a bare declaration list, the value of an HTML style attribute. Both are value-safe at any baseline; the optional CSSMinify baseline year only bounds how new the output syntax may be.

turbohtml.clean.minify_css(css, options=None)[source]

Minify a full CSS stylesheet.

Parameters:
  • css (str) – the stylesheet source (rules, at-rules, comments).

  • options (CSSMinify | None) – the minification options; defaults to CSSMinify (the most compatible output).

Return type:

str

Returns:

the minified stylesheet.

turbohtml.clean.minify_css_inline(css, options=None)[source]

Minify an inline declaration list, the value of an HTML style attribute.

Use this rather than minify_css() when the source is bare declarations (color:red; margin:0) with no surrounding selector or braces.

Parameters:
  • css (str) – the declaration-list source.

  • options (CSSMinify | None) – the minification options; defaults to CSSMinify (the most compatible output).

Return type:

str

Returns:

the minified declaration list.

class turbohtml.clean.CSSMinify(baseline=None)[source]

Options for minify_css() and minify_css_inline().

baseline is the Baseline year the output may target: the minifier applies a transform whose output syntax reached Baseline in year Y only when baseline >= Y. None (the default) emits only long-interoperable syntax; 2021 additionally merges the shorthands that reached Baseline that year (inset, the flex gap, the two-value overflow). Every year is value-safe – the year bounds only how new the output syntax may be, never the cascade it parses to.

Parameters:

baseline (int | None)

Transformations

Every transformation below preserves the computed value: the output parses to the same cascade as the input on any conformant browser. Each links to the specification that establishes the equivalence.

Numbers and dimensions

  • Drop a leading +, redundant leading and trailing zeros, and switch to e-notation when it is shorter (Syntax 3 §4.3.3, Values 4 §6.1). a{width:+0.50px}a{width:.5px}, a{margin:100000px}a{margin:1e5px}.

  • Lower-case a known unit and drop the unit on a zero <length> (0px0); angle, time, frequency and other dimensions keep their unit, since a bare 0 is a <length> only (Values 4 §5.2). a{margin:0PX}a{margin:0}, while a{transform:rotate(0deg)} is unchanged.

  • Fold a calc() of constant, like-united operands with exact rational arithmetic; a non-combinable or non-terminating result is kept verbatim, and +/- without surrounding whitespace is left untouched (Values 4 §10). a{width:calc(1px + 2px)}a{width:3px}.

Colors

  • Swap a named color and its hex for whichever is shorter, and shorten #rrggbb#rgb and #rrggbbaa#rgba (Color 4 §5.2, §6.1). a{color:#ffffff}a{color:#fff}, a{color:#000080}a{color:navy}.

  • Fold an opaque rgb()/hsl() to hex only when every channel lands on an exact 8-bit value; a fractional channel is kept functional, since rounding it changes the color (Color 4 §15). a{color:rgb(255,0,0)}a{color:red}.

  • Collapse transparent and rgba(0,0,0,0) to #0000, drop an alpha of 1, and use the shorter rgb()/ hsl() alias of rgba()/hsla() (Color 4 §4). a{color:transparent}a{color:#0000}, a{color:rgba(1,2,3,.5)}a{color:rgb(1,2,3,.5)}.

Shorthands

  • Collapse a 1–4 value box shorthand when mirrored edges are equal, and merge the four physical longhands back into the shorthand (Box 3 §6–7, Cascade 5 §2.2). a{margin:1px 1px 1px 1px}a{margin:1px}.

  • Collapse the background, background-position, background-repeat (Backgrounds 3 §3), flex (Flexbox 1 §7.1.1) and font (Fonts 4 §2.7) shorthands to their shortest equivalent form. a{font:bold 12px x}a{font:700 12px x}.

  • Merge flex-direction + flex-wrap into flex-flow, and each Box Alignment axis pair into its place- shorthand – align-content + justify-content into place-content, align-items + justify-items into place-items, and align-self + justify-self into place-self (Box Alignment 3). a{align-items:center;justify-items:center}a{place-items:center}.

  • Merge the four border-*-radius corners into border-radius (Backgrounds 3 §6) and outline-width + outline-style + outline-color into outline (UI 4 §3.1); each resets only its own longhands, and the corner merge stands down for an elliptical corner or a logical sibling. a{outline-width:1px; outline-style:solid;outline-color:red}a{outline:1px solid red}.

Structure and selectors

  • Collapse insignificant whitespace and strip comments, keeping a /*! */ bang comment (Syntax 3 §3.3). /*c*/a{color:red}a{color:red}, while /*! keep */a{color:red}/*!keep*/a{color:red}.

  • Drop a declaration an identically-keyed later one overrides, remove an empty rule or an empty @media/ @supports/@container (an empty @layer or @keyframes is kept, since either is observable), merge two rules with the same selector or an identical body – even across intervening rules, as long as each sets none of the moved properties so the cascade cannot change – and fuse consecutive @media blocks that share a prelude (Cascade 5 §6.4.4). a{}b{c:d}b{c:d}, @media print{} is dropped, and a{color:red}b{margin:0}a{font-size:2px}a{color:red;font-size:2px}b{margin:0}.

  • Lower-case type selectors, trim combinator whitespace, drop a redundant universal * before a subclass, write the four legacy pseudo-elements with one colon (::before:before), and unquote an attribute value that is a valid identifier (Selectors 4 §5–6, Pseudo-Elements 4 §8); a custom-property name keeps its case (Variables 1 §2).

  • Keep a custom-property value byte-exact past edge trimming: §2 defines the value as the literal token stream, var() splices it verbatim, and getComputedStyle().getPropertyValue() reads it back as written, so collapsing its internal whitespace – which the other PyPI minifiers do for a few hundred bytes on custom-property-heavy CSS – is observable and never applied. A{X:1}a{x:1}, [a="Foo"]{x:1}[a=Foo]{x:1}.

  • Rewrite a @keyframes from selector to 0% (Animations 1), and drop the space before and/or after a ) in a media query (Media Queries 4). @keyframes k{from{opacity:0}}@keyframes k{0%{opacity:0}}.

Baseline

Every transform above is value-safe and, by default, applies to every stylesheet – its output syntax has been interoperable for years. A few emit a shorthand whose interop is more recent, so they are gated on the CSSMinify baseline year and stay off unless you target that year or later. Each row lists the year a transform’s output syntax reached Baseline; a transform tagged year Y runs when baseline >= Y.

Baseline year

Transforms enabled

any (None, default)

Every transform in the sections above: numbers, colors, box/flex/place- shorthands, structure, selectors.

2021

Merge top/right/bottom/left into inset, merge the two overflow longhands into overflow, merge row-gap + column-gap into the flex gap, and merge each logical inline/block pair into its shorthand – margin-inline, margin-block, padding-inline, padding-block, inset-inline, inset-block (Logical Properties 1); a logical merge stands down when the physical longhand it aliases is in the rule. At baseline=2021, a{top:0;right:0;bottom:0;left:0}a{inset:0} and a{margin-inline-start:1px;margin-inline-end:2px}a{margin-inline:1px 2px}.

turbohtml.migration.bleach

A drop-in for bleach.clean for projects migrating off bleach. It translates bleach’s arguments onto a Policy; the safety baseline still applies, so an attributes callable cannot re-admit an event handler or a javascript: URL.

turbohtml.migration.bleach.clean(text, tags=None, attributes=None, protocols=None, strip=False, strip_comments=True, css_sanitizer=None)[source]

Sanitize HTML against a bleach-style allowlist, the way bleach.clean did.

Parameters:
  • text (str) – the untrusted HTML.

  • tags (Iterable[str] | None) – the allowed tag names; None uses bleach’s default set.

  • attributes (object) – the allowed attributes as a flat list, a per-tag dict, or a (tag, name, value) predicate; None uses bleach’s default.

  • protocols (Iterable[str] | None) – the allowed URL schemes; None uses bleach’s default.

  • strip (bool) – drop a disallowed tag and keep its children, rather than escaping it to text.

  • strip_comments (bool) – drop HTML comments from the output.

  • css_sanitizer (object) – accepted for signature compatibility; passing one raises NotImplementedError.

Return type:

str

Returns:

the sanitized, safe HTML.