diff --git a/src/services/graph-visualize-html.ts b/src/services/graph-visualize-html.ts index 6356c79..4aaf1d5 100644 --- a/src/services/graph-visualize-html.ts +++ b/src/services/graph-visualize-html.ts @@ -281,16 +281,20 @@ export async function buildInteractiveGraphHtml(opts: InteractiveHtmlOptions): P ? `${files.length} files · ${fileEdges.length} edges · ${sym.symbolCount ?? 0} symbols · ${sym.edgeCount ?? 0} calls` : `${files.length} files · ${fileEdges.length} edges`; + // Use function replacers for asset/data injection: string replacements + // would interpret `$&`, `$'`, `` $` ``, and `$$` in the replacement, which + // corrupts vendored bundles (cytoscape.min.js and dagre.min.js both contain + // a regex-escape idiom of the form `\$&`). const html = assets.template .replace(/\{\{TITLE\}\}/g, escapeHtml(`${opts.projectName} — SocratiCode graph`)) .replace(/\{\{PROJECT_NAME\}\}/g, escapeHtml(opts.projectName)) .replace(/\{\{STATS\}\}/g, escapeHtml(statsLine)) - .replace("{{STYLES}}", assets.styles) - .replace("{{CYTOSCAPE}}", assets.cytoscape) - .replace("{{DAGRE}}", assets.dagre) - .replace("{{CYTOSCAPE_DAGRE}}", assets.cytoscapeDagre) - .replace("{{DATA_JSON}}", toEmbeddedJson(data)) - .replace("{{APP}}", assets.app); + .replace("{{STYLES}}", () => assets.styles) + .replace("{{CYTOSCAPE}}", () => assets.cytoscape) + .replace("{{DAGRE}}", () => assets.dagre) + .replace("{{CYTOSCAPE_DAGRE}}", () => assets.cytoscapeDagre) + .replace("{{DATA_JSON}}", () => toEmbeddedJson(data)) + .replace("{{APP}}", () => assets.app); logger.info("Built interactive graph HTML", { project: opts.projectName, diff --git a/tests/unit/graph-visualize-html.test.ts b/tests/unit/graph-visualize-html.test.ts index 3e5abc0..b5da649 100644 --- a/tests/unit/graph-visualize-html.test.ts +++ b/tests/unit/graph-visualize-html.test.ts @@ -105,6 +105,20 @@ describe("graph-visualize-html", () => { expect(html).toContain('"symbolMode":"capped"'); }); + it("inlines vendored Cytoscape/Dagre bundles byte-for-byte (no $-token interpretation)", async () => { + // Regression for a `String.prototype.replace(string, string)` pitfall: a `$&` + // inside the replacement is interpreted as the match, corrupting any asset + // (or embedded data) that contains `$&`, `$'`, `` $` ``, or `$$`. + const { html } = await buildInteractiveGraphHtml({ + projectPath: "/proj", projectName: "bytes", projectId: "p1", graph: SAMPLE_GRAPH, + }); + // If the bug returns, `\$&` becomes `\{{CYTOSCAPE}}` / `\{{DAGRE}}` in the output. + expect(html.includes("\\{{CYTOSCAPE}}")).toBe(false); + expect(html.includes("\\{{DAGRE}}")).toBe(false); + // And the original token must survive verbatim from both bundles. + expect(html.includes("\\$&")).toBe(true); + }); + it("marks edges that belong to a cycle as cyclic:true", async () => { const cyclic: CodeGraph = { nodes: [