mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
fix(visualize): use function replacers so vendored assets containing $& survive intact
String.prototype.replace with a string replacement argument interprets
`$&`, `$'`, `` $` ``, and `$$` as special tokens (match, post-match,
pre-match, literal-$). The vendored bundles contain the classic
regex-escape idiom `"\\$&"` (`cytoscape.min.js` and `dagre.min.js` each
have one), which the asset-injection pipeline was corrupting into e.g.
`"\\{{CYTOSCAPE}}"` in the generated HTML. The visible symptom was a
broken Cytoscape search-box escape path whenever a user typed a regex
metacharacter into the live-search input.
Fix: convert the six asset/data replacements in buildInteractiveGraphHtml
to function-replacer form (`() => assets.cytoscape`), which uses the
returned string literally with no `$`-token interpretation. Same class
of bug would have affected embedded JSON had a file path or symbol name
contained `$&` — now neutralised for the whole chain.
Regression test added: asserts the `\\$&` idiom survives verbatim in the
rendered HTML and that neither `\\{{CYTOSCAPE}}` nor `\\{{DAGRE}}`
appears. A single `expect(html).toContain('"\\\\$&"')` catches the bug.
Quality gates (all green):
- Biome lint: clean
- TypeScript (tsc --noEmit): clean
- Unit tests: 686/686 (was 685; +1 regression test)
- CodeRabbit: No findings
- Snyk: 0 issues
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user