From 262a821ddc3efb815b26afb28db2b7d0f7db7e30 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 18 Oct 2026 20:10:38 -0400 Subject: [PATCH 1/2] fix(comment-scan): scan template text after a markup expression (RIG-5717) A line such as `)}, or email` closes a multi-line markup expression and continues as template prose. The whole region went to the TSX parser, which rejected the trailing text, so the file was unscannable. When the region does not parse, retry with the text after the closing brace split off and scanned as template. Across the 29 orion and compass .astro files, this fixes the one failing file and changes no other residue. Co-authored-by: Matt Wilkinson --- packages/comment-scan/astro.test.ts | 15 ++++++++ packages/comment-scan/astro.ts | 57 +++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/packages/comment-scan/astro.test.ts b/packages/comment-scan/astro.test.ts index 245f86a..1ea0944 100644 --- a/packages/comment-scan/astro.test.ts +++ b/packages/comment-scan/astro.test.ts @@ -176,6 +176,21 @@ describe("scanAstro", () => { ).toBe(true); }); + test("template text after a markup expression's closing brace stays template", () => { + const text = + '

\n\t{url ? (\n\t\t{label}\n\t) : (\n\t\tlabel\n\t)}, or email // not a comment\n\tx.\n

'; + const result = scanAstro(text); + expect(result.ambiguous).toBe(false); + expect(result.residues).toHaveLength(8); + expect(result.residues[5]).toBe("\t)}, or email // not a comment"); + }); + + test("a comment inside an expression with trailing text is still stripped", () => { + const result = scanAstro("

\n{x /* note */}, or\n

"); + expect(result.ambiguous).toBe(false); + expect(result.residues[1]).toBe("{x }, or"); + }); + test("plain brace expressions use the lexical scanner", () => { const result = scanAstro("

{title}

\n

after

"); expect(result.ambiguous).toBe(false); diff --git a/packages/comment-scan/astro.ts b/packages/comment-scan/astro.ts index 86c1050..0ea65e9 100644 --- a/packages/comment-scan/astro.ts +++ b/packages/comment-scan/astro.ts @@ -40,11 +40,14 @@ const MARKUP_EXPR = /\{[^}]*<[A-Za-z/]/; const EXPR_OPEN = /\{[^}]*$/; const MARKUP_LINE = /<[A-Za-z/]/; -// The lines of the `{ … }` region starting at `from`, or null when it does not -// close (or holds no markup, so the scanner handles it). Brace depth is -// counted on the region text, which is exact for the shapes here: a `{` inside -// a string or comment would need the parser this function is selecting. -function markupRegion(lines: readonly string[], from: number): string[] | null { +// The lines of the `{ … }` region starting at `from`, plus the column just past +// the `}` that closes it on the last line, or null when it holds no markup (so +// the scanner handles it). Brace depth is counted on the region text, which is +// exact for the shapes here: a `{` inside a string or comment needs the parser. +function markupRegion( + lines: readonly string[], + from: number, +): { lines: string[]; end: number } | null { const first = lines[from] ?? ""; if (!MARKUP_EXPR.test(first) && !EXPR_OPEN.test(first)) return null; let depth = 0; @@ -54,6 +57,7 @@ function markupRegion(lines: readonly string[], from: number): string[] | null { for (let k = from; k < lines.length; k++) { const line = lines[k] ?? ""; lineComment = false; + let end = line.length; if (k > from && MARKUP_LINE.test(line) && quote === null) markup = true; for (let j = 0; j < line.length; j++) { const ch = line[j] ?? ""; @@ -79,12 +83,37 @@ function markupRegion(lines: readonly string[], from: number): string[] | null { continue; } if (ch === "{") depth++; - else if (ch === "}") depth--; + else if (ch === "}") { + depth--; + if (depth <= 0) end = j + 1; + } } if (depth <= 0 && quote === null) - return markup ? lines.slice(from, k + 1) : null; + return markup ? { lines: lines.slice(from, k + 1), end } : null; } - return markup ? lines.slice(from) : null; + return markup + ? { lines: lines.slice(from), end: (lines.at(-1) ?? "").length } + : null; +} + +// Parses a region as TSX. When the whole region does not parse, retries with +// the text after the closing `}` split off and scanned as template: a line such +// as `)}, or email` ends an expression and continues as prose. +function scanRegion(region: { lines: string[]; end: number }): string[] | null { + const whole = scanJsx(region.lines.join("\n")); + if (!whole.ambiguous && whole.residues.length === region.lines.length) + return whole.residues; + const last = region.lines.length - 1; + const lastLine = region.lines[last] ?? ""; + const tail = lastLine.slice(region.end); + if (tail.trim() === "") return null; + const body = [...region.lines.slice(0, last), lastLine.slice(0, region.end)]; + const parsed = scanJsx(body.join("\n")); + if (parsed.ambiguous || parsed.residues.length !== body.length) return null; + const tailScan = scanLines(HTML, [tail], DEFAULT_STATE); + if (tailScan.underflow || tailScan.state.stack.length > 0) return null; + const joined = `${parsed.residues[last] ?? ""}${tailScan.residues[0] ?? ""}`; + return [...parsed.residues.slice(0, last), joined.replace(/[ \t]+$/, "")]; } type Walk = { @@ -159,14 +188,12 @@ function walkAstro(text: string): Walk { // hand the parser a fragment, and a fragment holding only the JSX half // of the expression falls back to the token scanner, where a `//` in // element text strips the rest of the line. - const parsed = scanJsx(region.join("\n")); + const parsed = scanRegion(region); // A region that does not parse cannot have its comments placed. - if (parsed.ambiguous || parsed.residues.length !== region.length) { - return UNMODELLED; - } - template.push(...region); - residues.push(...parsed.residues); - i += region.length; + if (parsed === null) return UNMODELLED; + template.push(...region.lines); + residues.push(...parsed); + i += region.lines.length; continue; } From b465f6d02959f9ccf8078eb15a53e1f018aa9baf Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 18 Oct 2026 20:22:45 -0400 Subject: [PATCH 2/2] fix(comment-scan): end an Astro markup region at its first closing brace (RIG-5717) Review fix: brace counting continued into the template tail, so a later `{name}` moved the split point and the file went ambiguous again. Co-authored-by: Matt Wilkinson --- packages/comment-scan/astro.test.ts | 6 ++++++ packages/comment-scan/astro.ts | 11 ++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/comment-scan/astro.test.ts b/packages/comment-scan/astro.test.ts index 1ea0944..469c01c 100644 --- a/packages/comment-scan/astro.test.ts +++ b/packages/comment-scan/astro.test.ts @@ -191,6 +191,12 @@ describe("scanAstro", () => { expect(result.residues[1]).toBe("{x }, or"); }); + test("a later expression in the template tail stays template", () => { + const result = scanAstro("

\n{x}, or email {name}\n

"); + expect(result.ambiguous).toBe(false); + expect(result.residues[1]).toBe("{x}, or email {name}"); + }); + test("plain brace expressions use the lexical scanner", () => { const result = scanAstro("

{title}

\n

after

"); expect(result.ambiguous).toBe(false); diff --git a/packages/comment-scan/astro.ts b/packages/comment-scan/astro.ts index 0ea65e9..2e5a038 100644 --- a/packages/comment-scan/astro.ts +++ b/packages/comment-scan/astro.ts @@ -57,7 +57,7 @@ function markupRegion( for (let k = from; k < lines.length; k++) { const line = lines[k] ?? ""; lineComment = false; - let end = line.length; + let end = -1; if (k > from && MARKUP_LINE.test(line) && quote === null) markup = true; for (let j = 0; j < line.length; j++) { const ch = line[j] ?? ""; @@ -85,11 +85,16 @@ function markupRegion( if (ch === "{") depth++; else if (ch === "}") { depth--; - if (depth <= 0) end = j + 1; + if (depth <= 0 && end === -1) end = j + 1; } } if (depth <= 0 && quote === null) - return markup ? { lines: lines.slice(from, k + 1), end } : null; + return markup + ? { + lines: lines.slice(from, k + 1), + end: end === -1 ? line.length : end, + } + : null; } return markup ? { lines: lines.slice(from), end: (lines.at(-1) ?? "").length }