Getting started

Go from an empty environment to escaping and unescaping your first HTML.

Install turbohtml from PyPI:

$ pip install turbohtml

Open a Python prompt and escape some text for safe inclusion in an HTML page:

import turbohtml

print(turbohtml.escape("5 > 3 & 2 < 4"))
5 &gt; 3 &amp; 2 &lt; 4

By default escape escapes quotation marks too, which you want inside an attribute value:

print(turbohtml.escape('name="O\'Brien"'))
name=&quot;O&#x27;Brien&quot;

Reverse the process: turn HTML character references back into text:

print(turbohtml.unescape("Tom &amp; Jerry, caf&eacute;"))
Tom & Jerry, café

Stay with the string helpers below, or continue to Tokenizing a document to break whole documents into tokens.

Linkify plain text

One more string-in, string-out helper rounds out the getting-started toolkit: turbohtml.clean.linkify() finds the URLs in a run of text and wraps each one in an anchor, leaving the surrounding characters untouched. It is the quickest way to turn a plain message into clickable HTML:

from turbohtml.clean import linkify

print(linkify("Visit https://example.com today"))
Visit <a href="https://example.com" rel="nofollow">https://example.com</a> today

Every generated link carries rel="nofollow" by default, so untrusted text stays safe to publish.

Normalize Unicode text

One more string helper cleans up Unicode itself. The same character can be typed as one code point or as a base letter plus a combining mark, so "café" need not equal "café" even though they look identical. turbohtml.detect.normalize() folds text to a Unicode normalization form, so the two compare equal:

from turbohtml.detect import normalize

composed = "café"  # e-acute as one code point
decomposed = "café"  # plain e followed by a combining acute accent
print(composed == decomposed)
print(normalize("NFC", composed) == normalize("NFC", decomposed))
False
True

Reach for NFC before you compare or store text; the Handle character encodings guide covers the other three forms.

Sanitize untrusted HTML

When the input is already HTML rather than plain text, clean it against an allowlist with turbohtml.clean.sanitize(). A Policy says what to keep; here it allows a <p> with a style attribute and, through allowed_styles, keeps a color only when it is a hex value. A non-overridable baseline still drops dangerous CSS, so the url(javascript:...) goes even though the property name is allowed:

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: url(javascript:x)">Hi</p>', policy))
<p style="color: #0a0">Hi</p>

Resolve a computed style

When you need the style a browser would apply rather than the raw rules, run the CSS cascade with turbohtml.cssom.computed_style(). It reads the document’s <style> sheets plus each element’s inline style, orders the declarations by importance, the style attribute, specificity, and source order, then fills in inheritance and initial values. Here #intro wins color through !important over its own inline rule, and the nested <em> inherits that color:

import turbohtml
from turbohtml.cssom import computed_style

doc = turbohtml.parse(
    "<html><head><style>"
    "p { color: gray } .lead { font-weight: bold } #intro { color: teal !important }"
    "</style></head><body>"
    "<p class=lead id=intro style='color: red'>Hello <em>world</em></p>"
    "</body></html>"
)
intro = doc.select_one("#intro")
print(computed_style(intro)["color"], computed_style(intro)["font-weight"])
print(computed_style(doc.select_one("em"))["color"])
teal bold
teal

The value is the computed value, not the used value: turbohtml runs no layout, so lengths and percentages come back as written – see The cascade and computed style.

Transform with XSLT

turbohtml can also reshape one document into another with an XSLT 1.0 stylesheet, the job lxml’s etree.XSLT does. A stylesheet is XML, so parse it with turbohtml.parse_xml(), wrap it in turbohtml.transform.Transform, and call it on a source document:

from turbohtml import parse_xml
from turbohtml.transform import Transform

style = parse_xml(
    '<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">'
    '<xsl:output method="html"/>'
    '<xsl:template match="/"><ul>'
    '<xsl:apply-templates select="list/item"/></ul></xsl:template>'
    '<xsl:template match="item"><li><xsl:value-of select="."/></li></xsl:template>'
    "</xsl:stylesheet>"
)
print(Transform(style)(parse_xml("<list><item>one</item><item>two</item></list>")))
<ul><li>one</li><li>two</li></ul>

The transform reuses the XPath engine for every match pattern and select expression; see Transform XML with XSLT for parameters and output methods, and XSLT for how it works.

Validate against a schema

When the input is XML with a contract, check it against an XSD or RELAX NG schema with turbohtml.validate.XMLSchema or RelaxNG. Compile the schema once, then validate a document parsed with turbohtml.parse_xml(); the result carries a valid flag and one ValidationError per violation, each with the /root/child path that located it:

from turbohtml import parse_xml
from turbohtml.validate import XMLSchema

schema = XMLSchema(
    '<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">'
    '<xs:element name="order"><xs:complexType><xs:sequence>'
    '<xs:element name="sku" type="xs:string"/>'
    '<xs:element name="qty" type="xs:positiveInteger"/>'
    "</xs:sequence></xs:complexType></xs:element></xs:schema>"
)
result = schema.validate(parse_xml("<order><sku>A-1</sku><qty>0</qty></order>"))
print(result.valid)
print(result.errors[0].path, result.errors[0].type)
False
/order/qty datatype

With the string helpers in hand, continue to Tokenizing a document to break whole documents into tokens.