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 > 3 & 2 < 4
By default escape escapes quotation marks too, which you want inside an attribute value:
print(turbohtml.escape('name="O\'Brien"'))
name="O'Brien"
Reverse the process: turn HTML character references back into text:
print(turbohtml.unescape("Tom & Jerry, café"))
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.