fix(coding-agent/export): prevented HTML template $ substitution expansions
- Switched template CSS/JS inlining replacements to callback form in generation scripts to avoid `$` replacement expansion semantics. - Updated HTML export generation to use callback-based replacements for theme variables and session data injection to prevent accidental `$` substitution parsing. - Added regression tests for the inlined script ensuring literal `$'` regex tokens remain intact, no closing HTML tags are injected, and the script parses via `new Function`.
This commit is contained in:
@@ -18,10 +18,11 @@ const minifiedCss = css
|
||||
.replace(/\s*([{}:;,])\s*/g, "$1")
|
||||
.trim();
|
||||
|
||||
// Inline everything
|
||||
// Inline everything; use function replacements so `$'`, `$&`, `$$`, etc. inside
|
||||
// the embedded CSS/JS are not interpreted as substitution patterns.
|
||||
const template = html
|
||||
.replace("<template-css/>", `<style>${minifiedCss}</style>`)
|
||||
.replace("<template-js/>", `<script>${js}</script>`);
|
||||
.replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
|
||||
.replace("<template-js/>", () => `<script>${js}</script>`);
|
||||
|
||||
// Write generated file
|
||||
const output = `// Auto-generated by scripts/generate-template.ts - DO NOT EDIT
|
||||
|
||||
@@ -103,9 +103,12 @@ async function generateHtml(sessionData: SessionData, themeName?: string): Promi
|
||||
const themeVars = await generateThemeVars(themeName);
|
||||
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64();
|
||||
|
||||
return TEMPLATE.replace("<theme-vars/>", `<style>:root { ${themeVars} }</style>`).replace(
|
||||
// Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the
|
||||
// substituted CSS/base64 are not interpreted as substitution patterns
|
||||
// (see https://mdn.io/String.replace).
|
||||
return TEMPLATE.replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`).replace(
|
||||
"{{SESSION_DATA}}",
|
||||
sessionDataBase64,
|
||||
() => sessionDataBase64,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,8 +17,9 @@ export async function getTemplate(): Promise<string> {
|
||||
.replace(/\s*([{}:;,])\s*/g, "$1")
|
||||
.trim();
|
||||
|
||||
// Inline everything
|
||||
// Inline everything; use function replacements so `$'`, `$&`, `$$`, etc.
|
||||
// inside the embedded CSS/JS are not interpreted as substitution patterns.
|
||||
return html
|
||||
.replace("<template-css/>", `<style>${minifiedCss}</style>`)
|
||||
.replace("<template-js/>", `<script>${js}</script>`);
|
||||
.replace("<template-css/>", () => `<style>${minifiedCss}</style>`)
|
||||
.replace("<template-js/>", () => `<script>${js}</script>`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { TEMPLATE } from "../../src/export/html/template.generated";
|
||||
|
||||
// Regression: `String.prototype.replace(string, string)` treats `$'`, `$&`,
|
||||
// `$$`, `$n`, etc. as substitution patterns. The inlined `<script>` body now
|
||||
// contains JS regex literals like `'\\s*Cell\\b\\s*(.*)$'` whose trailing `$'`
|
||||
// would be expanded to "the text after `<template-js/>`" (i.e. `</body></html>`)
|
||||
// if the replacement is a plain string instead of a function. That spliced the
|
||||
// closing HTML tags into the middle of a regex string and produced
|
||||
// `Uncaught SyntaxError: Invalid or unexpected token` at runtime.
|
||||
// The fix is to pass the replacement as a function in
|
||||
// scripts/generate-template.ts (and the mirror in template.macro.ts).
|
||||
describe("HTML export template script inlining", () => {
|
||||
function extractScript(): string {
|
||||
const match = TEMPLATE.match(/<script>([\s\S]*?)<\/script>\s*<\/body>/);
|
||||
if (!match) throw new Error("inlined <script> block not found in TEMPLATE");
|
||||
return match[1];
|
||||
}
|
||||
|
||||
it("preserves the literal `$'` regex anchor inside the inlined script", () => {
|
||||
const script = extractScript();
|
||||
// The eval-cell parser must still contain the raw `(.*)$'` and `End\\b.*$'`
|
||||
// regex sources — these are exactly the substrings that trigger the bug
|
||||
// when the replacement is treated as a substitution template.
|
||||
expect(script).toContain("\\\\s*Cell\\\\b\\\\s*(.*)$', 'i'");
|
||||
expect(script).toContain("\\\\s*End\\\\b.*$', 'i'");
|
||||
});
|
||||
|
||||
it("does not splice closing HTML tags into the inlined script", () => {
|
||||
const script = extractScript();
|
||||
expect(script).not.toMatch(/<\/body>/i);
|
||||
expect(script).not.toMatch(/<\/html>/i);
|
||||
});
|
||||
|
||||
it("produces a syntactically valid inlined script", () => {
|
||||
const script = extractScript();
|
||||
// `new Function(body)` parses without executing. Throws SyntaxError on
|
||||
// the spliced-tag corruption the substitution-pattern bug produces.
|
||||
expect(() => new Function(script)).not.toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user