From markupsafe¶
markupsafe is the safe-string library behind Jinja2, WTForms, and Werkzeug. It ships a Markup string subclass and an escape function so a template
engine can interpolate untrusted values into HTML without escaping trusted markup twice. Its scope is deliberately
narrow: HTML escaping and a safe-string type, with a small C accelerator for the escape itself. It carries no parser and
no DOM; striptags and unescape are regex scans over the string.
turbohtml.migration.markupsafe reimplements that whole surface – Markup, escape, escape_silent,
soft_str, and EscapeFormatter – fully type annotated, with the escape and every Markup operation backed by
turbohtml’s C extension. Because turbohtml already has a WHATWG tokenizer and HTML5 reference table, striptags and
unescape run on that real parser rather than a regex, so they resolve references and tag boundaries the regex scan
can miss.
turbohtml vs markupsafe¶
Dimension |
turbohtml |
markupsafe |
|---|---|---|
Scope |
Safe-string escaping as one module of a full HTML parser/serializer stack |
Focused HTML escaping and a |
Feature breadth |
Full markupsafe surface plus tokenizer-backed |
|
Performance |
C escape 2-3x faster on the small clean strings autoescape interpolates (see below) |
C-accelerated |
Typing |
Fully annotated, ships type stubs |
Typed (ships |
Dependencies |
Single self-contained package, C extension bundled |
Single self-contained package, optional C speedup |
Maintenance |
Active, part of the turbohtml project |
Active, maintained by the Pallets team |
Feature overlap¶
The public surface ports 1:1 – the names, signatures, and escaping semantics match, so a Jinja2, WTForms, or Werkzeug project only changes the import:
Markup– thestrsubclass, including the__html__protocol, the composing operators (+,%,*), and the full text-returningstrmethod surface kept asMarkup.escape()– returns aMarkup, honors__html__, leaves an existingMarkupuntouched.escape_silent()– likeescapebut mapsNoneto an emptyMarkup.soft_str()– coerces tostrwithout escaping.EscapeFormatter– the subclassablestring.Formatterbehindformat(), cooperating throughsuper()for a template sandbox.striptags()andunescape().
What turbohtml adds¶
The escape and every
Markupoperation run in C, so autoescaping stays a single C call and the small-string escape runs 2-3x faster than markupsafe’s own C escape.striptags()parses with the WHATWG tokenizer instead of scanning for<, so a comment containing<cannot end tag removal early and references resolve during stripping.unescape()uses the full HTML5 named-reference table, resolving references the regex scan can miss.Everything else turbohtml is: the same
escapeand tokenizer primitives back parsing, serialization, and the selector engine, so a project that adopts the safe-string also gets a parser under one name-per-concept API.
What markupsafe has that turbohtml does not¶
Drop-in package identity. turbohtml does not register itself as the
markupsafedistribution, so it will not transparently replace the installed package for code that doesimport markupsafe. Adoption is an explicit per-project import swap. No equivalent: change the import line.The ``soft_unicode`` alias was removed in markupsafe 3.0 and is absent here too; use
soft_str().
Performance¶
operation |
turbohtml |
|
|---|---|---|
markupsafe-compatible escape — clean (8 B) |
64.2 ns |
203 ns (3.2x) |
markupsafe-compatible escape — clean (32 B) |
70.6 ns |
223 ns (3.2x) |
markupsafe-compatible escape — clean (256 B) |
134 ns |
477 ns (3.6x) |
markupsafe-compatible escape — name with ‘ and & |
88.4 ns |
230 ns (2.6x) |
markupsafe-compatible escape — escape-heavy markup |
153 ns |
365 ns (2.4x) |
Markup operations — striptags |
1.21 µs |
2.37 µs (2.0x) |
Markup operations — unescape |
223 ns |
1.04 µs (4.7x) |
Markup operations — format (escapes operands) |
1.84 µs |
2.09 µs (1.2x) |
Markup operations — join (escapes operands) |
671 ns |
1.22 µs (1.9x) |
The escape runs two to three times faster than markupsafe’s own C escape on the small, mostly-clean strings a template
engine interpolates under autoescape, and the other Markup operations stay ahead too – striptags and
unescape run on turbohtml’s tokenizer and HTML5 reference resolution rather than markupsafe’s regex scan, and the
composing operations (format, join) escape each untrusted operand through the same C escape.
How to migrate¶
A Jinja2, WTForms, or Werkzeug project changes only the import line:
# markupsafe
from markupsafe import Markup, escape, escape_silent, soft_str, EscapeFormatter
# turbohtml
from turbohtml.migration.markupsafe import (
Markup,
escape,
escape_silent,
soft_str,
EscapeFormatter,
)
turbohtml |
|
|---|---|
|
|
|
escape returns a Markup with the same numeric quote references markupsafe
emits, honors the __html__ protocol, and leaves an existing Markup untouched. Markup overrides the full
str method surface, so a value that flows through a template filter such as upper or replace stays a
Markup and autoescaping does not escape it a second time. The operations that combine text (+, %,
format(), join(), replace,
…) escape their untrusted operands:
from turbohtml.migration.markupsafe import Markup, escape, escape_silent
print(escape('<a href="x">Tom & Jerry</a>'))
print(Markup("<b>{}</b>").format("<i>"))
print(Markup("<b>safe</b>").upper()) # str methods keep the Markup, so it is not re-escaped
print(escape_silent(None) == Markup(""))
<a href="x">Tom & Jerry</a>
<b><i></b>
<B>SAFE</B>
True
Gotchas and pitfalls¶
Two methods are upgrades rather than reimplementations:
striptags()andunescape()run on turbohtml’s tokenizer and HTML5 reference resolution. They resolve references markupsafe’s regex stripping can miss and treat comments as real nodes, so the plain text a page produces can differ where the input contains comments, malformed tags, or named references.striptagscollapses runs of whitespace to single spaces, matching markupsafe; do not rely on the exact incidental whitespace of either implementation.The
soft_unicodealias that markupsafe 3.0 removed is absent here too; usesoft_str().turbohtml does not register itself as
markupsafe, so adoption stays an explicit per-project import rather than a transparent replacement of the installed package.