diff --git a/src/trackChangesModel.ts b/src/trackChangesModel.ts index 1eb1765..ffb44bc 100644 --- a/src/trackChangesModel.ts +++ b/src/trackChangesModel.ts @@ -10,6 +10,7 @@ */ import MarkdownIt from "markdown-it"; import { diffArrays, diffWords } from "diff"; +import { diffMermaid } from "./mermaidDiff"; export type BlockType = "prose" | "code" | "mermaid"; @@ -204,6 +205,24 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => { return defaultFence(tokens, idx, options, env, self); }; +/** Inner source of a ```mermaid fence (drops the fence lines), or null. */ +function mermaidFenceBody(raw: string): string | null { + const lines = raw.split(/\r?\n/); + const open = lines[0]?.match(/^(\s*)(`{3,}|~{3,})\s*(\w+)?/); + if (!open || open[3]?.toLowerCase() !== "mermaid") return null; + const body = lines.slice(1); + if (body.length && /^(\s*)(`{3,}|~{3,})/.test(body[body.length - 1])) body.pop(); + return body.join("\n"); +} + +/** F7.1 (#22): legend shown beneath an intra-diagram-diffed mermaid block. */ +const MERMAID_LEGEND = + '
' + + 'added' + + 'changed' + + 'removed' + + "
"; + /** Build a markdown string with inline / from a word-level prose diff. */ function wordMergedMarkdown(beforeRaw: string, afterRaw: string): string { return diffWords(beforeRaw, afterRaw) @@ -256,8 +275,19 @@ function renderOp(op: BlockOp, render: (src: string) => string): string { case "changed": cls = "cw-changed"; if (op.atomic) { - inner = safe(op.block.raw); // the NEW block, whole (INV-23) - badge = 'changed'; + const curBody = op.block.type === "mermaid" ? mermaidFenceBody(op.block.raw) : null; + const beforeBody = op.before.type === "mermaid" ? mermaidFenceBody(op.before.raw) : null; + const md = + curBody !== null && beforeBody !== null + ? diffMermaid(beforeBody, curBody) + : { kind: "fallback" as const }; + if (md.kind === "augmented") { + // Re-render the augmented diagram as a mermaid fence; add a legend, drop the badge (INV-29). + inner = safe("```mermaid\n" + md.source + "\n```") + MERMAID_LEGEND; + } else { + inner = safe(op.block.raw); // the NEW block, whole (INV-23 / INV-30 fallback) + badge = 'changed'; + } } else { inner = safe(wordMergedMarkdown(op.before.raw, op.block.raw)); } diff --git a/test/trackChangesModel.test.ts b/test/trackChangesModel.test.ts index ddfc255..0297ee1 100644 --- a/test/trackChangesModel.test.ts +++ b/test/trackChangesModel.test.ts @@ -99,14 +99,16 @@ describe("renderTrackChanges", () => { expect(html).toContain("cw-badge"); }); - it('emits
 for a mermaid fence + a changed badge', () => {
+  it('emits 
 for a changed flowchart, augmented intra-diagram (#22)', () => {
     const html = renderTrackChanges(
       "```mermaid\nflowchart LR\n a-->b\n```\n",
       "```mermaid\nflowchart LR\n a-->c\n```\n",
     );
     expect(html).toContain('
');
-    expect(html).toContain("a-->c"); // current source, escaped
-    expect(html).toContain("cw-badge");
+    expect(html).toContain("a-->c"); // current source preserved as prefix, escaped
+    // a changed flowchart is now diffed in place (INV-29): styling, not a whole-block badge
+    expect(html).toContain("classDef cwAdded");
+    expect(html).not.toContain('changed');
   });
 
   it("unchanged doc renders with no marks", () => {
@@ -232,3 +234,33 @@ describe("renderAuthorship", () => {
     expect(renderAuthorship(text, spans)).toBe(renderAuthorship(text, spans));
   });
 });
+
+import { renderTrackChanges as rtc2 } from "../src/trackChangesModel";
+
+describe("renderTrackChanges — intra-diagram mermaid (#22)", () => {
+  it("augments a changed flowchart with styling directives (INV-29)", () => {
+    const base = "```mermaid\nflowchart LR\n A-->B\n```\n";
+    const cur = "```mermaid\nflowchart LR\n A-->B\n B-->C\n```\n";
+    const html = rtc2(base, cur);
+    expect(html).toContain("classDef cwAdded");
+    expect(html).toMatch(/class\s+C\s+cwAdded/);
+    expect(html).toContain('class="cw-mermaid-legend"');
+    // no single whole-block "changed" badge when we augmented
+    expect(html).not.toContain('changed');
+  });
+
+  it("augments a changed sequence diagram", () => {
+    const base = "```mermaid\nsequenceDiagram\n A->>B: hi\n```\n";
+    const cur = "```mermaid\nsequenceDiagram\n A->>B: hi\n B->>C: on\n```\n";
+    const html = rtc2(base, cur);
+    expect(html).toContain("rect rgb(46, 160, 67)");
+  });
+
+  it("falls back to the v1 badge for an unsupported diagram type (INV-30)", () => {
+    const base = "```mermaid\nclassDiagram\n class A\n```\n";
+    const cur = "```mermaid\nclassDiagram\n class B\n```\n";
+    const html = rtc2(base, cur);
+    expect(html).toContain('changed');
+    expect(html).not.toContain("classDef cwAdded");
+  });
+});