Skip to content

Commit bc03a5a

Browse files
fix(signal-processing): don't rewrite <body in script/style content
The body-tag auto-annotation pass (`<body([^>]*)>` → `<body$1 data-stx-auto>`) ran a global-but-first-match regex over the entire rendered output. If any embedded `<script>` block contained a regex literal like `/<body[^>]*>/` (common in HTML-cleaning libraries — ts-medium-editor's paste extension does exactly this), the rewrite landed inside the JS code instead of the document body. The corrupted output looked like [/^[\s\S]*<body[^ data-stx-auto>]*>\s*|.../g, ""] which JS rejects with `Invalid regular expression: Range out of order in character class` (the `-` chars inside `data-stx-auto` become broken character ranges like `t-s`). The page-level script fails to load, scopes don't register, downstream components don't hydrate. Two changes: - Detect the body tag using a script/style-stripped copy of the output, so embedded JS regex literals don't masquerade as a body tag. - When the rewrite is warranted, locate the actual body offset in the original output by scanning past every <script>/<style> block, and only rewrite at that anchored position.
1 parent 89e233f commit bc03a5a

1 file changed

Lines changed: 35 additions & 5 deletions

File tree

packages/stx/src/signal-processing.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -953,13 +953,43 @@ export async function processSignals(template: string, options: StxOptions, file
953953
}
954954

955955
// If no setup code but has signals syntax, add data-stx-auto to body for auto-processing
956-
// Check if body already has data-stx attribute (not just any occurrence in the template)
957-
const bodyMatch = output.match(/<body([^>]*)>/i)
956+
// Check if body already has data-stx attribute (not just any occurrence in the template).
957+
// Crucially, scan for `<body` only outside of <script> and <style> blocks — embedded
958+
// JS often contains regex literals like `/<body[^>]*>/` and corrupting those into
959+
// `<body data-stx-auto[^>]*>` breaks the entire page (the malformed character class
960+
// throws "Invalid regular expression: Range out of order in character class").
961+
const stripped = output.replace(/<script\b[\s\S]*?<\/script>/gi, '').replace(/<style\b[\s\S]*?<\/style>/gi, '')
962+
const bodyMatch = stripped.match(/<body([^>]*)>/i)
958963
const bodyHasDataStx = bodyMatch && /data-stx/.test(bodyMatch[1])
959964

960-
if (!setupCode && !bodyHasDataStx) {
961-
if (output.includes('<body') && bodyMatch && !/data-stx-auto/.test(bodyMatch[1])) {
962-
output = output.replace(/<body([^>]*)>/, '<body$1 data-stx-auto>')
965+
if (!setupCode && !bodyHasDataStx && bodyMatch && !/data-stx-auto/.test(bodyMatch[1])) {
966+
// Locate the actual body tag in the original output to replace. Find the
967+
// first <body in `stripped`, then find the equivalent position in `output`
968+
// by walking forward and skipping over <script>/<style> regions, so we
969+
// don't replace a regex literal inside a script.
970+
const bodyTagRe = /<body[^>]*>/i
971+
const scriptOrStyleRe = /<(script|style)\b[\s\S]*?<\/\1>/gi
972+
const skipRanges: Array<[number, number]> = []
973+
let m: RegExpExecArray | null
974+
while ((m = scriptOrStyleRe.exec(output)) !== null)
975+
skipRanges.push([m.index, m.index + m[0].length])
976+
977+
let searchFrom = 0
978+
let bodyIdx = -1
979+
while (searchFrom < output.length) {
980+
bodyTagRe.lastIndex = searchFrom
981+
const hit = bodyTagRe.exec(output.slice(searchFrom))
982+
if (!hit) break
983+
const absIdx = searchFrom + hit.index
984+
const insideSkip = skipRanges.some(([s, e]) => absIdx >= s && absIdx < e)
985+
if (!insideSkip) { bodyIdx = absIdx; break }
986+
searchFrom = absIdx + hit[0].length
987+
}
988+
989+
if (bodyIdx !== -1) {
990+
const before = output.slice(0, bodyIdx)
991+
const after = output.slice(bodyIdx).replace(bodyTagRe, m => m.replace(/^<body([^>]*)>$/i, '<body$1 data-stx-auto>'))
992+
output = before + after
963993
}
964994
}
965995

0 commit comments

Comments
 (0)