Sanitizing without the element
The pipeline behind <safe-fragment> is available as plain functions, for template systems and component runtimes that need a
sanitizer hook. Same pipeline, same guarantees, same SanitizationReport; the element is just one consumer of it.
import { sanitizeToFragment, sanitizeToFragmentSync, preloadSanitizer } from "@johnhenry/safe-fragment";
const { fragment, report } = await sanitizeToFragment(untrustedHtml, { profile: "article-v1" });target.replaceChildren(fragment); // a detached, profile-conformant DocumentFragment
await preloadSanitizer(); // once, at startupconst sync = sanitizeToFragmentSync(untrustedHtml, { profile: "article-v1" });The result’s fragment is detached and fully profile-conformant: insert it with append or replaceChildren. It is never touched
by an unsafe sink.
sanitizeToFragment(html, options)
Section titled “sanitizeToFragment(html, options)”Returns Promise<{ fragment, report }>. Throws a SafeFragmentError on failure; it never returns a partially sanitized result.
Options (SanitizeToFragmentOptions):
| Option | Meaning |
|---|---|
profile |
Required. Name of a registered profile. There is no default. A missing or empty value throws INVALID_PROFILE; an unregistered one throws UNKNOWN_PROFILE. |
document |
The document whose realm should be used (for example a pop-out’s). Defaults to the ambient document; without any DOM it throws UNSUPPORTED_ENVIRONMENT. |
maxInputLength |
Largest accepted input in UTF-16 code units (default 1,000,000). Longer input throws SOURCE_TOO_LARGE. |
baseUrl |
Base URL that protocol-relative URLs inherit their scheme from. Defaults to document.baseURI. |
idPolicy |
"prefix" (default) rewrites every id (and reference) to user-content-<id>. "keep-in-shadow" keeps ids as written and is safe only if you insert the returned fragment into a shadow root: in light DOM or the document a kept id can clobber window and document properties. Nothing here can check where you insert it, so that guarantee is yours. Any other value throws INVALID_OPTION. See Ids inside a shadow root. |
loadDOMPurify |
DOMPurify loader for this call; see loadDOMPurify below. |
The report lists what both the engine and enforceProfile removed (removedElements, removedAttributes, rewrittenUrls);
outputLength is an approximation. It lists only genuinely removed nodes: DOMPurify’s own scaffolding (the <remove> sentinel and the <body>
wrapper) is not reported, so a benign input reports nothing on either engine (safe-fragment#9,
081f92c). The native engine’s report cannot list what it strips unconditionally (<script>, <iframe>, on* handlers, javascript: URLs);
DOMPurify’s does (ADR 0007).
sanitizeToFragmentSync(html, options)
Section titled “sanitizeToFragmentSync(html, options)”The synchronous variant. It works only when no loading is needed: the native Sanitizer API exists, or DOMPurify was already
prepared (await preloadSanitizer(), or an earlier async call). Otherwise it throws SANITIZER_NOT_READY. It fails closed: it
never degrades to an unsanitized or lesser path. It takes the same options except that loadDOMPurify is ignored, because a
synchronous call cannot load anything.
Trap: on Safari (no setHTML) the sync variant throws SANITIZER_NOT_READY until DOMPurify is ready. Call
await preloadSanitizer() at startup.
preloadSanitizer(options?)
Section titled “preloadSanitizer(options?)”Prepares the sanitizer so the synchronous API works and the first render does not pay the DOMPurify load. Resolves with the
engine that will be used, "native" or "dompurify". It rejects with SANITIZER_UNAVAILABLE (whose message says how to fix it)
if DOMPurify is needed but cannot be loaded, and with UNSUPPORTED_ENVIRONMENT where there is no document.
| Option | Meaning |
|---|---|
document |
The document whose window the sanitizer is prepared for. Defaults to the ambient document. |
loadDOMPurify |
Supplies DOMPurify for pages with no bundler or import map. Also becomes the app-wide loader. |
engine |
"auto" (default) does nothing when the native Sanitizer API exists. "dompurify" loads and instantiates DOMPurify regardless, so the sync API works even if you force the fallback. |
One DOMPurify instance is created per window and reused, so a Trusted Types dompurify policy is registered once; see
Trusted Types and CSP.
loadDOMPurify
Section titled “loadDOMPurify”loadDOMPurify is an option, not a function export. It is accepted by registerSafeFragment, preloadSanitizer and
sanitizeToFragment, and supplies the DOMPurify factory for pages with no bundler and no import map:
import { registerSafeFragment } from "@johnhenry/safe-fragment";
registerSafeFragment({ loadDOMPurify: () => import("https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.es.mjs").then((m) => m.default),});It has the type DOMPurifyLoader: () => Promise<DOMPurifyFactory>, where a DOMPurifyFactory is createDOMPurify, the callable
default export of the dompurify module ((window) => instance). Without it, the bare specifier dompurify must resolve through
a bundler or an import map; if it does not, you get SANITIZER_UNAVAILABLE. Use the exact dompurify version the release
pins: profile output stability depends on it (Getting started).
Passing loadDOMPurify to registerSafeFragment or preloadSanitizer also sets it as the app-wide loader.
Engines
Section titled “Engines”mode: "text" profiles (plain-text-v1) skip the pipeline entirely: the string becomes a single Text node and no HTML parser
is invoked. mode: "html" profiles use the native Sanitizer API (Element.prototype.setHTML) when it exists and otherwise a
locked-down DOMPurify, then the shared enforceProfile() pass runs identically after either. See the
Security model and ADR 0002.