From 7fe47160e9daeec27da048ccc9834e0fff47d539 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Wed, 7 Oct 2026 13:16:17 -0700 Subject: [PATCH] fix(export): keep injected attrs out of the WHP-FORM marker comment injectAttrs() skipped leading \n
z
'; + expect(injectAttrs(html, ' data-z')).toBe( + ' \n
z
', + ); + }); + test('comment containing ">" is skipped as a unit', () => { + expect(injectAttrs('

t

', ' data-z')).toBe('

t

'); + }); + test('bare comment with nothing after is left alone', () => { + expect(injectAttrs('', ' data-z')).toBe(''); + }); + test('void tag after a comment still gets attrs before the slash', () => { + expect(injectAttrs('', ' data-z')).toBe(''); + }); +}); diff --git a/craft/src/utils/html-export.ts b/craft/src/utils/html-export.ts index f40e78e..91f39ff 100644 --- a/craft/src/utils/html-export.ts +++ b/craft/src/utils/html-export.ts @@ -70,13 +70,21 @@ function buildDataAttrs(props: Record): string { * the attrs before it instead, yielding well-formed ``. * Non-void tags (no trailing `/`) are unaffected. * - * Leading `` elements (components such as ButtonLink and - * ColumnLayout emit a scoped style block before their element) are skipped, - * so the attrs land on the component's real element, not on the `` elements (components such as + * ButtonLink and ColumnLayout emit a scoped style block before their element) + * and `` comments (relay forms -- ContactForm, SubscribeForm, + * FormContainer -- emit the `` marker before their element) + * are skipped, in any order and repeated, so the attrs land on the + * component's first REAL element. Injecting into the marker comment would + * break it (`…thankyou=""-- data-wsb-r="…">`): the browser swallows the form + * into an unterminated comment, and the publish step refuses the page. */ export function injectAttrs(html: string, attrs: string): string { if (!attrs) return html; - const prefixLen = leadingStyleLength(html); + const prefixLen = leadingNonElementLength(html); + // Nothing but style blocks / comments: there is no element to carry the + // attrs, and injecting into a comment would corrupt it -- leave it alone. + if (prefixLen > 0 && prefixLen >= html.length) return html; if (prefixLen > 0) return html.slice(0, prefixLen) + injectAttrs(html.slice(prefixLen), attrs); // Find the first > of the opening tag and inject before it const idx = html.indexOf('>'); @@ -94,18 +102,18 @@ export function injectAttrs(html: string, attrs: string): string { return html.slice(0, idx) + attrs + html.slice(idx); } -/** Length of any run of `` elements (plus whitespace) at the - * very start of `html`; 0 when it does not start with one. Only skips when - * something follows the style block(s) -- a bare style block is left alone. */ -function leadingStyleLength(html: string): number { - const re = /^\s*]*>[\s\S]*?<\/style>\s*/i; +/** Length of any run of `` elements and `` comments + * (plus whitespace), in any order, at the very start of `html`; 0 when it + * does not start with one. Equals `html.length` when nothing follows. */ +function leadingNonElementLength(html: string): number { + const re = /^\s*(?:]*>[\s\S]*?<\/style>|)\s*/i; let len = 0; for (;;) { const m = re.exec(html.slice(len)); if (!m) break; len += m[0].length; } - return len < html.length ? len : 0; + return len; } /**