1"""
2Sanitize untrusted HTML against an allowlist, the way bleach.clean did.
3
4bleach is the last maintained Python sanitizer and it is end of life, deprecated for sitting on the unmaintained
5html5lib; turbohtml owns a maintained WHATWG tree builder, so this rebuilds the sanitizer on it. The model is the one
6every safe sanitizer converged on: parse the input into a real tree, drop everything not on the allowlist while walking
7it, and serialize once. There is no serialize-then-reparse round trip, which is the step that lets mutation XSS slip
8back in, and a :class:`Policy` baseline removes ``<script>``, event-handler attributes, and ``javascript:`` URLs even
9when a caller's allowlist would admit them, so a no-argument :func:`sanitize` is safe by construction.
10
11This module is the policy facade only; the walk that keeps, escapes, strips, or removes each node runs in C
12(``turbohtml._html._sanitize``), so the safety baseline lives below the configurable surface.
13"""
14
15from __future__ import annotations
16
17from dataclasses import dataclass, field
18from enum import Enum
19from itertools import starmap
20from types import MappingProxyType
21from typing import TYPE_CHECKING, Final, TypeVar, overload
22
23from turbohtml._html import _sanitize, _sanitize_policy
24
25if TYPE_CHECKING:
26 import re
27 from collections.abc import Callable, Mapping, Sequence
28
29 from turbohtml._html import Node
30
31
32_NODE_T = TypeVar("_NODE_T", bound="Node")
33
34
35@dataclass(frozen=True, slots=True)
36class Removed:
37 """
38 One item a policy dropped, the shape :meth:`Sanitizer.sanitize_report` returns.
39
40 :param tag: the tag name of the dropped element, or of the element a dropped attribute was on.
41 :param attribute: the dropped attribute's name, or ``None`` when the element itself was dropped.
42 """
43
44 tag: str
45 attribute: str | None = None
46
47
48@dataclass(frozen=True, slots=True)
49class Transform:
50 """
51 A rename rule for ``Policy.transform_tags``, sanitize-html's ``simpleTransform``.
52
53 Mapping a source tag to a bare string renames it; mapping it to a ``Transform`` renames it and adds attributes. The
54 added attributes are not trusted: they join the element's own attributes and go through the same allowlist and
55 URL/style scrubbing, so a transform cannot add an attribute the policy would otherwise strip.
56
57 :param tag: the target tag name to rename to.
58 :param attributes: attribute name/value pairs to add to the renamed element (added if absent, overwritten if
59 present).
60 """
61
62 tag: str
63 attributes: Mapping[str, str] = field(default_factory=dict)
64
65
66class OnDisallowed(Enum):
67 """
68 What to do with an element the policy does not allow.
69
70 ``ESCAPE`` renders the tag as visible text (``<x>`` becomes ``<x>``), the safe bleach default that keeps the
71 content readable; ``STRIP`` drops the tag but keeps its children; ``REMOVE`` drops the tag and its whole subtree.
72 """
73
74 ESCAPE = 0
75 STRIP = 1
76 REMOVE = 2
77
78
79#: bleach's default allowed tags, the migration baseline.
80DEFAULT_TAGS: Final = frozenset({
81 "a",
82 "abbr",
83 "acronym",
84 "b",
85 "blockquote",
86 "code",
87 "em",
88 "i",
89 "li",
90 "ol",
91 "strong",
92 "ul",
93})
94#: bleach's default allowed attributes, keyed by tag (``"*"`` would match every tag).
95DEFAULT_ATTRIBUTES: Final[Mapping[str, frozenset[str]]] = MappingProxyType({
96 "a": frozenset({"href", "title"}),
97 "abbr": frozenset({"title"}),
98 "acronym": frozenset({"title"}),
99})
100#: bleach's default URL schemes.
101DEFAULT_SCHEMES: Final = frozenset({"http", "https", "mailto"})
102#: bleach's default allowed CSS properties (CSS 2.1 safe set plus SVG paint), for scrubbing a ``style`` attribute.
103DEFAULT_CSS_PROPERTIES: Final = frozenset({
104 "azimuth", "background-color", "border-bottom-color", "border-collapse", "border-color", "border-left-color",
105 "border-right-color", "border-top-color", "clear", "color", "cursor", "direction", "display", "elevation",
106 "float", "font", "font-family", "font-size", "font-style", "font-variant", "font-weight", "height",
107 "letter-spacing", "line-height", "overflow", "pause", "pause-after", "pause-before", "pitch", "pitch-range",
108 "richness", "speak", "speak-header", "speak-numeral", "speak-punctuation", "speech-rate", "stress", "text-align",
109 "text-decoration", "text-indent", "unicode-bidi", "vertical-align", "voice-family", "volume", "white-space",
110 "width", "fill", "fill-opacity", "fill-rule", "stroke", "stroke-width", "stroke-linecap", "stroke-linejoin",
111 "stroke-opacity",
112}) # fmt: skip
113
114# A roomier set for the relaxed preset: typical user-generated content (headings, tables, images, figures).
115_RELAXED_TAGS: Final = DEFAULT_TAGS | {
116 "p", "br", "hr", "span", "div", "pre", "h1", "h2", "h3", "h4", "h5", "h6",
117 "dl", "dt", "dd", "sub", "sup", "del", "ins", "mark", "small", "u", "s", "q", "cite", "img",
118 "figure", "figcaption", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col",
119} # fmt: skip
120_RELAXED_ATTRIBUTES: Final[Mapping[str, frozenset[str]]] = MappingProxyType({
121 "*": frozenset({"title", "lang", "dir"}),
122 "a": frozenset({"href", "title", "name"}),
123 "img": frozenset({"src", "alt", "title", "width", "height"}),
124 "td": frozenset({"colspan", "rowspan"}),
125 "th": frozenset({"colspan", "rowspan", "scope"}),
126 "col": frozenset({"span"}),
127 "colgroup": frozenset({"span"}),
128})
129
130
131@dataclass(frozen=True)
132class Policy:
133 r"""
134 An immutable, thread-safe description of what sanitizing keeps.
135
136 Build one and reuse it across threads. Whatever a policy allows, a non-configurable baseline still removes the
137 unsafe element set, event-handler attributes, and dangerous URL schemes, so every policy is safe by construction.
138
139 :param tags: the allowed element set; any tag outside it is handled per ``on_disallowed_tag``.
140 :param attributes: allowed attribute names keyed by tag (``"*"`` as the key matches every tag, and ``"*"`` inside a
141 set allows every name).
142 :param url_schemes: the allowlist for URL-bearing attributes such as ``href`` and ``src``, and for the redirect URL
143 in a ``<meta http-equiv="refresh">`` ``content``; ``javascript:`` stays dropped even when listed.
144 :param allow_relative_urls: keep relative (scheme-less) URLs, which carry no scheme to check. A value whose scheme
145 holds a non-ASCII or control code point, such as ``ftp\xa0://``, is relative under the WHATWG URL parser.
146 :param allow_fragment_urls: keep fragment-only URLs even when relative URLs are disallowed.
147 :param on_disallowed_tag: how to treat a tag not in ``tags`` (:class:`OnDisallowed`: escape, strip, or remove).
148 :param strip_comments: drop HTML comments from the output.
149 :param add_link_rel: ``rel`` tokens forced onto every kept ``<a href>`` (e.g. ``noopener``).
150 :param attribute_predicate: an optional predicate called once per attribute before allowlist and safety checks,
151 in source order, including transform-added attributes. False drops an attribute; True cannot bypass safety.
152 Attributes added by ``set_attributes`` do not pass through this predicate.
153 :param attribute_filter: an optional rewrite for each surviving attribute, returning a replacement value or
154 ``None`` to drop it. The replacement passes through the safety checks before serialization.
155 :param set_attributes: attribute values forced onto every kept instance of a tag (added if absent, overwritten if
156 present); unlike ``attribute_filter``, this can add attributes that were not there. Added values pass through
157 the same URL, CSS, template, value, media-host, and named-property checks as parsed attributes.
158 :param remove_with_content: disallowed tags whose whole subtree is dropped (e.g. ``script``/``style``) rather than
159 escaped or stripped, so their text never leaks into the output.
160 :param css_properties: the CSS property allowlist. A kept ``style`` attribute and, when ``style`` is in ``tags``,
161 the ``<style>`` element's stylesheet body are both scrubbed against it: any declaration whose property name is
162 not in the set (or whose value smuggles ``expression()`` or a ``url()`` with a disallowed scheme) is dropped.
163 The non-configurable baseline also drops ``behavior`` and ``-moz-binding``. CSS escapes are decoded before
164 these checks, while function-like text inside strings and comments remains inert. Selectors and block nesting
165 remain.
166 :param attribute_prefixes: allow any attribute whose name starts with one of these prefixes (e.g. ``"data-"`` for
167 every ``data-*``), on top of the exact-name and ``"*"`` matches in ``attributes``.
168 :param attribute_values: restrict a kept attribute to literal values, keyed ``{tag: {attribute: allowed_values}}``;
169 a surviving attribute whose value is outside its set is dropped. This narrows an attribute ``attributes``
170 already admits and cannot admit a new one.
171 :param transform_tags: rename rules applied before the allowlist, keyed by source tag (sanitize-html's
172 ``transformTags``). A value that is a plain string renames the element to it; a :class:`Transform` renames it
173 and adds attributes. The rename runs first, then the renamed element is re-checked from scratch, so a transform
174 decides an element's *name* but never its safety: the allowlist still governs the target tag (mapping to
175 ``script`` still drops it), and any added attributes are scrubbed like the element's own. Only HTML elements are
176 transformed, matched by tag name. Empty (the default) renames nothing.
177 :param allowed_styles: a per-property value allowlist for the ``style`` attribute, keyed
178 ``{tag: {property: [pattern, ...]}}`` with ``"*"`` as a tag matching every element (sanitize-html's
179 ``allowedStyles``). A declaration survives only when its property is listed for the element's tag or ``"*"``
180 (their pattern lists union) and its value matches one of the patterns (an unanchored :func:`re.search`).
181 Patterns are strings or precompiled :class:`re.Pattern`. This narrows on top of ``css_properties`` and the
182 non-configurable dangerous-value baseline: a property this admits is still dropped unless ``css_properties``
183 lists it too, and ``expression()`` or a ``url()`` with a disallowed scheme is dropped even if a pattern would
184 match it. Empty (the default) leaves ``css_properties`` as the only ``style`` filter.
185 :param media_hosts: allowed hosts for an embedded-media ``src`` (``audio``, ``video``, ``source``, ``track``); a
186 ``src`` whose URL host is not one of these lowercase entries is dropped. Empty means no host restriction.
187 :param strip_template_markers: collapse template-engine expressions (``{{ }}``, ``${ }``, ``<% %>``) in kept text
188 and attribute values to a single space, so sanitized output cannot re-inject when a template engine (Angular,
189 Vue, Mustache, EJS, ERB) later renders it. DOMPurify's ``SAFE_FOR_TEMPLATES``; off by default.
190 :param isolate_named_props: prefix every kept ``id`` and ``name`` value with ``user-content-`` so it cannot shadow
191 a built-in ``document`` or form property through named access (DOM clobbering: ``<input name="attributes">``
192 makes ``form.attributes`` resolve to the input, ``<img name="body">`` hides ``document.body``). The prefix is
193 left alone when already present, so re-sanitizing is a fixpoint. DOMPurify's ``SANITIZE_NAMED_PROPS``; off by
194 default.
195 :param custom_element_check: a predicate that keeps an unlisted custom element (a hyphenated HTML tag such as
196 ``my-widget`` or ``x-card``) without naming it in ``tags``, DOMPurify's
197 ``CUSTOM_ELEMENT_HANDLING.tagNameCheck``. Given the element's lowercased tag name, return true to keep it (pass
198 ``re.compile(r"x-.*").search`` to drive it from a regex). Only basic custom-element names reach it (the reserved
199 ``annotation-xml``/``font-face`` family never does), and the safety baseline still escapes unsafe tags and
200 scrubs ``on*``/URL/style attributes on whatever it keeps. ``None`` (the default) escapes every unlisted tag as
201 before.
202 :param custom_attribute_check: a predicate that keeps an unlisted attribute on a kept custom element, DOMPurify's
203 ``CUSTOM_ELEMENT_HANDLING.attributeNameCheck``. Given ``(tag, attribute_name)`` return true to keep it; the
204 ``on*``, URL-scheme, and ``style`` scrubbing still apply, so this widens the name allowlist, never the safety
205 baseline. ``None`` (the default) keeps only the attributes ``attributes`` already admits.
206 :param allow_customized_builtins: keep an ``is`` attribute whose value passes ``custom_element_check``, so a
207 customized built-in element (``<button is="my-button">``) survives, DOMPurify's
208 ``allowCustomizedBuiltInElements``. Off by default, and inert without ``custom_element_check``.
209 :param allow_html: keep HTML-namespace elements; turning it off drops the whole HTML namespace, DOMPurify's
210 ``USE_PROFILES.html``. Independent of the tag allowlist, so it composes with ``allow_svg``/``allow_mathml`` to
211 select which content languages a policy admits.
212 :param allow_svg: keep SVG-namespace elements, DOMPurify's ``USE_PROFILES.svg``. Off drops every SVG element even
213 when its tag is in ``tags``. The baseline rejects SVG animation elements because they can assign event handlers
214 or script URLs at runtime; an allowlist cannot keep them.
215 :param allow_mathml: keep MathML-namespace elements, DOMPurify's ``USE_PROFILES.mathMl``. Off drops every MathML
216 element even when its tag is in ``tags``.
217 :param xml: emit well-formed XML/XHTML instead of HTML, DOMPurify's ``RETURN_DOM`` served through the XML
218 serializer. Every kept empty element self-closes (``<br/>``), foreign SVG and MathML subtrees carry their
219 namespace declarations, text and attribute values follow the XML escaping rules, and a kept comment or a
220 stray control character is neutralized so the result reparses through :func:`turbohtml.parse_xml`. Use it to
221 clean an XHTML dialect (Reportlab's RML, ePub content) whose consumer rejects HTML's bare ``<br>``. Off (the
222 default) keeps the HTML serialization.
223 """
224
225 tags: frozenset[str] = DEFAULT_TAGS
226 # default_factory, not a plain default: dataclasses before 3.12 reject a MappingProxyType default as "mutable"
227 attributes: Mapping[str, frozenset[str]] = field(default_factory=lambda: DEFAULT_ATTRIBUTES)
228 url_schemes: frozenset[str] = DEFAULT_SCHEMES
229 allow_relative_urls: bool = True
230 allow_fragment_urls: bool = False
231 on_disallowed_tag: OnDisallowed = OnDisallowed.ESCAPE
232 strip_comments: bool = True
233 add_link_rel: frozenset[str] = frozenset()
234 attribute_filter: Callable[[str, str, str], str | None] | None = None
235 set_attributes: Mapping[str, Mapping[str, str]] = field(default_factory=dict)
236 remove_with_content: frozenset[str] = frozenset()
237 css_properties: frozenset[str] = DEFAULT_CSS_PROPERTIES
238 attribute_prefixes: frozenset[str] = frozenset()
239 attribute_values: Mapping[str, Mapping[str, frozenset[str]]] = field(default_factory=dict)
240 media_hosts: frozenset[str] = frozenset()
241 strip_template_markers: bool = False
242 allowed_styles: Mapping[str, Mapping[str, Sequence[str | re.Pattern[str]]]] = field(default_factory=dict)
243 transform_tags: Mapping[str, Transform | str] = field(default_factory=dict)
244 isolate_named_props: bool = False
245 custom_element_check: Callable[[str], bool] | None = None
246 custom_attribute_check: Callable[[str, str], bool] | None = None
247 allow_customized_builtins: bool = False
248 allow_html: bool = True
249 allow_svg: bool = True
250 allow_mathml: bool = True
251 xml: bool = False
252 attribute_predicate: Callable[[str, str, str], bool] | None = None
253 _bleach_url_policy: bool = False
254
255 @classmethod
256 def strict(cls) -> Policy:
257 """
258 Allow no markup at all: every tag is escaped to text and every attribute dropped.
259
260 :returns: the strict policy.
261 """
262 return cls(tags=frozenset(), attributes=MappingProxyType({}))
263
264 @classmethod
265 def basic(cls) -> Policy:
266 """
267 Allow bleach's default 12-tag set, for migration parity.
268
269 :returns: the basic policy.
270 """
271 return cls()
272
273 @classmethod
274 def relaxed(cls) -> Policy:
275 """
276 Allow the richer set typical user-generated content needs: headings, tables, images, and figures.
277
278 :returns: the relaxed policy.
279 """
280 return cls(
281 tags=frozenset(_RELAXED_TAGS),
282 attributes=_RELAXED_ATTRIBUTES,
283 add_link_rel=frozenset({"noopener", "noreferrer"}),
284 )
285
286
287class Sanitizer:
288 """
289 A reusable sanitizer; build it once from a :class:`Policy` and call :meth:`sanitize` from any thread.
290
291 :param options: the policy to enforce; None uses bleach's default allowlist.
292 """
293
294 def __init__(self, options: Policy | None = None) -> None:
295 """Compile a policy into the form the C walk consumes."""
296 self.policy = options if options is not None else Policy()
297 (
298 self._attributes,
299 self._link_rel,
300 self._set_attributes,
301 self._attribute_values,
302 self._allowed_styles,
303 self._transform_tags,
304 ) = _sanitize_policy(
305 self.policy.attributes,
306 self.policy.add_link_rel,
307 self.policy.set_attributes,
308 self.policy.attribute_values,
309 self.policy.allowed_styles,
310 self.policy.transform_tags,
311 Transform,
312 )
313
314 def sanitize(self, html: str | Node) -> str:
315 """
316 Sanitize an HTML fragment.
317
318 :param html: the untrusted HTML fragment, or an already parsed node whose subtree to sanitize (see
319 :meth:`sanitize_node`), which skips the parse.
320 :returns: the sanitized, safe HTML.
321 :raises TypeError: if a set-typed policy field (``tags``, ``url_schemes``, ``remove_with_content``,
322 ``css_properties``, ``attribute_prefixes``, ``media_hosts``) holds a value that is not a set or frozenset,
323 or ``attribute_prefixes`` contains a non-string.
324 :raises ValueError: if ``attribute_prefixes`` contains an empty string, which would match every attribute.
325 """
326 return self._render(self._filter(html, None))
327
328 def sanitize_report(self, html: str | Node) -> tuple[str, list[Removed]]:
329 """
330 Sanitize a fragment and report what the policy dropped, the way DOMPurify populates ``DOMPurify.removed``.
331
332 Every disallowed element (removed, stripped, or escaped) and every stripped attribute becomes one
333 :class:`Removed` record, in the order the walk reached it, so a caller can log or tune a policy against
334 evidence instead of guessing.
335
336 :param html: the untrusted HTML fragment, or an already parsed node (see :meth:`sanitize_node`).
337 :returns: the sanitized HTML paired with the list of dropped items.
338 :raises TypeError: like :meth:`sanitize`, on a mistyped set-valued policy field.
339 :raises ValueError: like :meth:`sanitize`, on an empty ``attribute_prefixes`` entry.
340 """
341 removed: list[tuple[str, str | None]] = []
342 html_out = self._render(self._filter(html, removed))
343 return html_out, list(starmap(Removed, removed))
344
345 def sanitize_node(self, node: _NODE_T) -> _NODE_T:
346 """
347 Sanitize an already parsed subtree and return the sanitized copy as a tree.
348
349 This is the tree-to-tree form for a pipeline that parses once, runs several transforms, and serializes once,
350 the way DOMPurify's ``RETURN_DOM`` hands back a DOM instead of a string. The node itself is the kept context,
351 like the fragment root of :meth:`sanitize`: the policy applies to its descendants, never to the node. So pass
352 the ``body`` of a document to clean what it holds, or a ``Document`` to have the policy judge the ``html``
353 element itself (a ``bleach``-style allowlist then needs ``html``, ``head`` and ``body`` listed). The copy lives
354 in its own tree and inherits the source's XML flag; the source is left untouched.
355
356 :param node: the element or document whose subtree to sanitize.
357 :returns: a new node of the same kind holding the sanitized subtree.
358 :raises TypeError: if ``node`` is not a node, or on a mistyped set-valued policy field.
359 :raises ValueError: like :meth:`sanitize`, on an empty ``attribute_prefixes`` entry.
360 """
361 return self._filter(_node_only(node), None)
362
363 def sanitize_report_node(self, node: _NODE_T) -> tuple[_NODE_T, list[Removed]]:
364 """
365 Sanitize an already parsed subtree and report what the policy dropped.
366
367 :meth:`sanitize_node` and :meth:`sanitize_report` combined: the copy comes back as a tree, the drops as
368 records.
369
370 :param node: the element or document whose subtree to sanitize.
371 :returns: the sanitized copy paired with one :class:`Removed` record per dropped element or attribute.
372 :raises TypeError: if ``node`` is not a node, or on a mistyped set-valued policy field.
373 :raises ValueError: like :meth:`sanitize`, on an empty ``attribute_prefixes`` entry.
374 """
375 removed: list[tuple[str, str | None]] = []
376 copy = self._filter(_node_only(node), removed)
377 return copy, list(starmap(Removed, removed))
378
379 def _render(self, root: Node) -> str:
380 """Serialize the sanitized root's children as XML when the policy asks, else as HTML."""
381 return root.inner_xml if self.policy.xml else root.inner_html
382
383 @overload
384 def _filter(self, html: _NODE_T, removed: list[tuple[str, str | None]] | None) -> _NODE_T: ...
385
386 @overload
387 def _filter(self, html: str, removed: list[tuple[str, str | None]] | None) -> Node: ...
388
389 def _filter(self, html: str | Node, removed: list[tuple[str, str | None]] | None) -> Node:
390 """Run the C walk over a freshly parsed fragment or a copy of a node, appending drops to ``removed``."""
391 policy = self.policy
392 return _sanitize(
393 html,
394 policy.tags,
395 self._attributes,
396 policy.url_schemes,
397 policy.allow_relative_urls,
398 policy.allow_fragment_urls,
399 policy.on_disallowed_tag.value,
400 policy.strip_comments,
401 self._link_rel,
402 policy.attribute_filter,
403 self._set_attributes,
404 policy.remove_with_content,
405 policy.css_properties,
406 policy.attribute_prefixes,
407 self._attribute_values,
408 policy.media_hosts,
409 policy.strip_template_markers,
410 removed,
411 self._allowed_styles,
412 self._transform_tags,
413 policy.isolate_named_props,
414 policy.custom_element_check,
415 policy.custom_attribute_check,
416 policy.allow_customized_builtins,
417 policy.allow_html,
418 policy.allow_svg,
419 policy.allow_mathml,
420 policy.attribute_predicate,
421 policy._bleach_url_policy, # ruff:ignore[private-member-access] # bridge the migration-only C option
422 policy.xml, # the XML serializer round-trips through parse_xml, so the HTML parse-stability pass is skipped
423 )
424
425
426def _node_only(node: _NODE_T) -> _NODE_T:
427 """Reject a str where a parsed node is required, so a caller who meant the string form gets told which one."""
428 if isinstance(node, str):
429 msg = "sanitize_node takes a parsed node; pass a str to sanitize instead"
430 raise TypeError(msg)
431 return node
432
433
434def sanitize(html: str | Node, options: Policy | None = None) -> str:
435 """
436 Sanitize an HTML fragment against a policy.
437
438 :param html: the untrusted HTML fragment, or an already parsed node whose subtree to sanitize (see
439 :func:`sanitize_node`), which skips the parse.
440 :param options: the policy to enforce; None uses bleach's default allowlist.
441 :returns: the sanitized, safe HTML.
442 :raises TypeError: if a set-typed policy field (``tags``, ``url_schemes``, ``remove_with_content``,
443 ``css_properties``, ``attribute_prefixes``, ``media_hosts``) holds a value that is not a set or frozenset, or
444 ``attribute_prefixes`` contains a non-string.
445 :raises ValueError: if ``attribute_prefixes`` contains an empty string, which would match every attribute.
446 """
447 return Sanitizer(options).sanitize(html)
448
449
450def sanitize_report(html: str | Node, options: Policy | None = None) -> tuple[str, list[Removed]]:
451 """
452 Sanitize a fragment and report what the policy dropped, like DOMPurify's ``DOMPurify.removed``.
453
454 :param html: the untrusted HTML fragment, or an already parsed node (see :func:`sanitize_node`).
455 :param options: the policy to enforce; None uses bleach's default allowlist.
456 :returns: the sanitized HTML paired with one :class:`Removed` record per dropped element or attribute.
457 :raises TypeError: like :func:`sanitize`, on a mistyped set-valued policy field.
458 :raises ValueError: like :func:`sanitize`, on an empty ``attribute_prefixes`` entry.
459 """
460 return Sanitizer(options).sanitize_report(html)
461
462
463def sanitize_node(node: _NODE_T, options: Policy | None = None) -> _NODE_T:
464 """
465 Sanitize an already parsed subtree against a policy and return the sanitized copy as a tree.
466
467 The tree-to-tree form for a pipeline that parses once and serializes once; see
468 :meth:`Sanitizer.sanitize_node` for what the node means and what comes back.
469
470 :param node: the element or document whose subtree to sanitize.
471 :param options: the policy to enforce; None uses bleach's default allowlist.
472 :returns: a new node of the same kind holding the sanitized subtree.
473 :raises TypeError: if ``node`` is not a node, or on a mistyped set-valued policy field.
474 :raises ValueError: like :func:`sanitize`, on an empty ``attribute_prefixes`` entry.
475 """
476 return Sanitizer(options).sanitize_node(node)
477
478
479def sanitize_report_node(node: _NODE_T, options: Policy | None = None) -> tuple[_NODE_T, list[Removed]]:
480 """
481 Sanitize an already parsed subtree and report what the policy dropped.
482
483 :param node: the element or document whose subtree to sanitize.
484 :param options: the policy to enforce; None uses bleach's default allowlist.
485 :returns: the sanitized copy paired with one :class:`Removed` record per dropped element or attribute.
486 :raises TypeError: if ``node`` is not a node, or on a mistyped set-valued policy field.
487 :raises ValueError: like :func:`sanitize`, on an empty ``attribute_prefixes`` entry.
488 """
489 return Sanitizer(options).sanitize_report_node(node)
490
491
492__all__ = [
493 "DEFAULT_ATTRIBUTES",
494 "DEFAULT_CSS_PROPERTIES",
495 "DEFAULT_SCHEMES",
496 "DEFAULT_TAGS",
497 "OnDisallowed",
498 "Policy",
499 "Removed",
500 "Sanitizer",
501 "Transform",
502 "sanitize",
503 "sanitize_node",
504 "sanitize_report",
505 "sanitize_report_node",
506]