/** * Sanitization SSOT for the unified markdown engine. * * Layered defense (order matters, see engine.tsx): * 1. `escapeUnknownHtmlTags` — TEXT pre-pass. Escapes ``s outside the * effective allowlist so LLM-emitted pseudo-tags (``, ``) * never reach React as unknown elements (React 19 crash guard). * NOT a security boundary. * 2. `rehype-raw` parses remaining raw HTML into HAST. * 3. `rehypeSanitize` with `buildSanitizeSchema(...)` — the audited * allow-list boundary (hast-util-sanitize) with a schema extended to * exactly what our surfaces need. * 4. `rehypeStripUnsafe` — custom strip pass kept as defense-in-depth * (srcset candidate scanning, iframe[srcdoc], belt-and-suspenders if * the schema is ever loosened). * * COUPLED-ALLOWLIST INVARIANT (tested in __tests__/sanitize-invariant.test.ts): * the two effective tag lists are EQUAL (case-insensitively), both computed * AFTER merging `extraAllowedHtmlTags`. Both directions matter: * - pre-pass ⊆ sanitizer: the pre-pass must never admit a raw tag the * sanitizer then silently drops. * - sanitizer ⊆ pre-pass: the pre-pass must never ESCAPE a tag the * sanitizer would happily keep. This direction was broken before * 2026-07: `strike` (and every other `defaultSchema`-only tag) survived * the sanitizer but was escaped to `<strike>` source text by the * pre-pass, so legacy authored markup regressed to visible tag soup. * Both lists are now derived from the SINGLE `effectiveTagList()` below — * never fork them. * * ONE documented exception, and it is CONTENT-dependent rather than * list-level (so the invariant test still holds as an equality of tag SETS): * an UNCLOSED RAWTEXT/RCDATA opener (`` `` is one `inlineCode` node, so * that `` is a code sample and not a closer — yet this scan (and * `PROTECTED_SPAN_RE`, whose body class is `[^\n]`) was strictly PER LINE, so * the mask never saw the span, the closer stayed visible in the haystack, * `hasLaterCloser` returned true, and a prose `` — proving the closer is a code sample * — beside a live textarea swallowing the prose). This is the shape that is not * a CONTAINER at all, so no container sweep could ever have reached it. * * A code span CANNOT cross a paragraph break, so the scan unit is a maximal run * of non-blank lines. Blank lines still terminate a segment, which keeps the * fail-CLOSED direction (an unterminated opener consumes at most its own * paragraph, never the rest of the document) and keeps the bound linear — the * `suffMax` / cursor structure is unchanged, `\n` is simply an ordinary * character inside a segment. * * `PROTECTED_SPAN_RE` (the CARVE) is deliberately left per-line: it rounds the * other way, so at worst a multi-line code sample renders as escaped text. */ declare function findInlineCodeRanges(source: string): Array<[number, number]>; /** Exported for the differential fuzz against the retired regex. */ export declare const __findInlineCodeRangesForTest: typeof findInlineCodeRanges; /** * Build the haystack `hasLaterCloser` searches: a LENGTH-PRESERVING lowercased * copy of the document with every region that cannot contain a REAL closing * tag blanked to spaces. * * Why this exists: the escaping pass carefully carves code out, but the * closer search used to run over the RAW document. So a `` sitting * inside a code fence, an inline-code span, or another tag's attribute string * satisfied "is closed later", the prose opener was left LIVE, and parse5's * RAWTEXT span swallowed the rest of the message anyway — the whole fix was * one code sample away from being bypassed, which is exactly what an LLM * answer about HTML looks like. * * Masking (rather than deleting) keeps every index identical to the original * string, so the caller's offset arithmetic is unchanged. THE LENGTH * INVARIANT IS LOAD-BEARING — see `foldAsciiCase`. * * CARVE DECISION (deliberate, do not "unify"): these tracker-derived regions * are NOT fed to the escaping carve, even though that would stop an authored * EOF-terminated fence body from rendering as literal `<their>`. * * The genuine asymmetry is the EOF-TERMINATED fence, and only that one. The * tracker protects an unclosed opener all the way to end of input, so a single * stray ``` line — mid-stream, or inside an open raw-HTML block where a ``` * line is content rather than a fence — would carve the ENTIRE remainder of the * document out of the escaping pass. `PROTECTED_SPAN_RE` protects nothing at * all there (it only recognizes a fence CLOSED by a same-marker run), so its * failure mode is bounded: a code sample renders as escaped text. In the carve * an over-detected region is a region that is NOT escaped — a fail-OPEN, i.e. * exactly the swallow this module exists to prevent — so the materially larger * fail-open surface decides it. * * SHARED over-detection (e.g. a ``` line inside an HTML block — `
`, * `
`, `
` — where CommonMark says the line is HTML content, not a * fence) was previously dismissed here as "not an argument either way". THAT * WAS WRONG: it is precisely the residual fail-open. The intersection guard * below only reconciles DISAGREEMENT, so when BOTH engines open the same bogus * fence the guard is a no-op and a live `