############# From bleach ############# .. package-meta:: bleach mozilla/bleach `bleach `_ was the standard Python HTML allowlist sanitizer and linkifier, built on html5lib. Two jobs live in one library: ``bleach.clean`` strips markup down to an allowed set of tags, attributes, and URL schemes so user-supplied HTML is safe to render, and ``bleach.linkify`` scans plain text for URLs and wraps them in ```` tags. It powered comment fields, wikis, and message bodies across the Django ecosystem for a decade. bleach reached end of life with no maintained successor. turbohtml covers both jobs from its ``turbohtml.clean`` module: ``bleach.clean`` maps to the allowlist :class:`~turbohtml.clean.Sanitizer` (with a drop-in :func:`turbohtml.migration.bleach.clean` shim), and ``bleach.linkify`` maps to :func:`turbohtml.clean.linkify`. Both run their filtering in C, ship full type annotations, and take a frozen, thread-safe configuration. ********************* turbohtml vs bleach ********************* .. list-table:: :header-rows: 1 :widths: 22 39 39 - - Dimension - turbohtml - bleach - - Scope - Full WHATWG parser, serializer, sanitizer, linkifier, selectors - Sanitize and linkify only, over html5lib - - Feature breadth - Escape/strip/remove per tag, value-rewriting attribute filter, forced attributes, regenerable IANA TLD table - Allow/strip tags, bool attribute callback, ``css_sanitizer`` for style scrubbing - - Performance - Filtering and link scan in C - Pure-Python over html5lib - - Typing - Fully annotated, ``py.typed`` - Untyped - - Dependencies - None (self-contained C extension) - html5lib, plus tinycss2 for CSS sanitizing - - Maintenance - Active - End of life, no successor Feature overlap =============== The shared surface ports one-to-one: - ``bleach.clean(text, tags=, attributes=, protocols=, strip=, strip_comments=)`` -> :func:`turbohtml.migration.bleach.clean` (same signature) or a native :class:`~turbohtml.clean.Policy` + :class:`~turbohtml.clean.Sanitizer`. - ``bleach.linkify(text, ...)`` and the reusable ``Linker`` -> :func:`turbohtml.clean.linkify` and :class:`turbohtml.clean.Linker`. - The ``nofollow`` and ``target_blank`` callbacks and ``DEFAULT_CALLBACKS`` keep their names in :mod:`turbohtml.clean`. - bleach's default allowed tags, attributes, and URL schemes are the turbohtml migration baseline (``DEFAULT_TAGS``, ``DEFAULT_ATTRIBUTES``, ``DEFAULT_SCHEMES``), so an unconfigured ``clean`` behaves the same. What turbohtml adds =================== - A frozen, thread-safe :class:`~turbohtml.clean.Policy`, where sharing a configured ``bleach.Cleaner`` across threads was a documented footgun. - An :class:`~turbohtml.clean.OnDisallowed` enum that names escape, strip, and remove, where bleach overloaded the two booleans ``strip`` and ``strip_comments``. - An ``attribute_filter`` that returns a replacement value or ``None`` to drop, where bleach's attribute callable only returned a bool. - ``set_attributes`` to force attributes (for example ``rel="noopener"``) onto every kept instance of a tag, which bleach could only approximate through a linkify callback. - A safety baseline that removes ``")) .. testoutput:: <p>Hi link</p><script>evil()</script> For new code prefer the native :class:`~turbohtml.clean.Policy`/:class:`~turbohtml.clean.Sanitizer` API: a frozen, thread-safe policy, an :class:`~turbohtml.clean.OnDisallowed` enum that names escape, strip, and remove where bleach overloaded two booleans, and an ``attribute_filter`` that rewrites or drops a value where bleach's callable only returned a bool. Linkifying ========== The entry points keep bleach's names, so the import changes and the common case is identical: .. code-block:: python # bleach from bleach import linkify from bleach.linkifier import Linker, DEFAULT_CALLBACKS from bleach.callbacks import nofollow, target_blank # turbohtml from turbohtml.clean import linkify, Linker, Linkify, DEFAULT_CALLBACKS, nofollow, target_blank .. list-table:: :header-rows: 1 :widths: 50 50 - - `bleach `__ - turbohtml - - ``linkify(text, ...)`` - :func:`turbohtml.clean.linkify` - - ``Linker(...)`` - :class:`turbohtml.clean.Linker` - - ``DEFAULT_CALLBACKS`` - ``DEFAULT_CALLBACKS`` - - ``nofollow``, ``target_blank`` - :func:`turbohtml.clean.nofollow`, :func:`turbohtml.clean.target_blank` - - callback ``(attrs, new)`` with ``(namespace, name)`` keys - a single :class:`~turbohtml.clean.LinkCandidate` (``url``, ``text``, ``attrs``) - - ``new`` flag - ``LinkCandidate.existing`` (inverted) - - ``protocols=`` - ``Linkify.schemes`` ``linkify(text, Linkify(callbacks=..., skip_tags=..., parse_email=...))``, the reusable :class:`~turbohtml.clean.Linker`, and the ``nofollow``/``target_blank`` defaults work as before: the six knobs are now fields of a frozen :class:`~turbohtml.clean.Linkify` config. Only custom callbacks change shape. bleach passed ``(attrs, new)`` where ``attrs`` was keyed by ``(namespace, name)`` tuples with a ``"_text"`` pseudo-key for the text; turbohtml passes a single :class:`~turbohtml.clean.LinkCandidate` with plain ``url``, ``text``, and ``attrs`` (a ``dict[str, str]``), and a callback returns it to keep the link or ``None`` to leave the text bare. bleach's ``new`` flag becomes ``LinkCandidate.existing`` (inverted: ``new=True`` is ``existing=False``). Porting a callback means reading fields instead of tuple keys: .. testcode:: from turbohtml.clean import LinkCandidate, Linkify, linkify def shorten(link: LinkCandidate) -> LinkCandidate | None: link.text = link.url.removeprefix("https://").removeprefix("http://") return link print(linkify("read https://example.com/page", Linkify(callbacks=[shorten]))) .. testoutput:: read example.com/page bleach's ``protocols`` maps to the ``Linkify.schemes`` field, which restricts the explicit URL schemes that autolink, and bleach's custom-TLD support maps to ``Linkify.extra_tlds``, on top of a current IANA table you can regenerate where bleach shipped a frozen list. A bare domain such as ``example.com`` still links only when its last label is a known TLD. ********************** Gotchas and pitfalls ********************** - turbohtml's safety baseline (``