From 916a568e9fd45b2298aad1064cbaea3ac9b8a47c Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Sun, 9 Aug 2026 17:36:12 -0700 Subject: [PATCH] fix(site-builder): address Task 25 review findings on

x

`; + } + + test('~7000 levels of nested @media does not crash purifyHtml (was: RangeError: Maximum call stack size exceeded)', () => { + const code = buildDeeplyNestedMedia(7000); + expect(() => purifyHtml(code)).not.toThrow(); + const out = purifyHtml(code); + expect(out).toContain('

x

'); + expect(out).toContain('@media'); + }); + + test('a scope class + wrapper is still produced for the pathological input (best-effort, not a silent no-op)', () => { + const code = buildDeeplyNestedMedia(7000); + const out = purifyHtml(code); + expect(out).toMatch(/^
/); + }); +}); + +describe('purifyHtml -- review finding: idempotent over its own prior output', () => { + test('running purifyHtml() twice (customer pastes previously-published output into a fresh block) does not nest a second wrapper', () => { + const code = '

Hi

'; + const once = purifyHtml(code); + const twice = purifyHtml(once); + expect(twice).toBe(once); + // Specifically: no second wrapper div, no double-prefixed selector. + expect((twice.match(/
{ + const code = '

Hi

'; + const gen1 = purifyHtml(code); + const gen2 = purifyHtml(gen1); + const gen3 = purifyHtml(gen2); + expect(gen3).toBe(gen1); + }); +}); diff --git a/craft/src/components/basic/HtmlBlock.tsx b/craft/src/components/basic/HtmlBlock.tsx index 1215c5a..b99129e 100644 --- a/craft/src/components/basic/HtmlBlock.tsx +++ b/craft/src/components/basic/HtmlBlock.tsx @@ -78,6 +78,21 @@ const PURIFY_CONFIG = { // DOMPurify runs, so it can only match inside this block's own wrapper // element. See the FORCE_BODY comment below and scopeStyleBlocks() for // why allowing the tag alone is not sufficient. + // + // Review note (Task 25 follow-up, documented not fixed): DOMPurify's + // SAFE_FOR_XML default (on unless a caller explicitly disables it, + // which PURIFY_CONFIG does not) silently drops an ENTIRE ` -- a very plausible - // paste, style-before-markup is a common snippet shape -- would vanish - // with no error anywhere, despite ` -- a very +// plausible paste, style-before-markup is a common snippet shape -- would +// vanish with no error anywhere, despite ...
` wrapper. Without this check, + * `scopeStyleBlocks` would hash the NEW `code` to a NEW scope class, fail + * to recognise the embedded selectors as already scoped (they're prefixed + * for the OLD class, not the new one `scopeCss`'s own idempotency guard + * checks against), and nest a second wrapper div around the first while + * re-prefixing every selector under the new class on top of the old one. + * + * Detects "the sanitized content IS ALREADY exactly one of our own scoped + * wrappers": a single root element, a
, whose class matches our own + * naming convention, and whose `