feat(f7.1): render augmented mermaid diff in changed-block branch (#22)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
*/
|
*/
|
||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import { diffArrays, diffWords } from "diff";
|
import { diffArrays, diffWords } from "diff";
|
||||||
|
import { diffMermaid } from "./mermaidDiff";
|
||||||
|
|
||||||
export type BlockType = "prose" | "code" | "mermaid";
|
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);
|
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 =
|
||||||
|
'<div class="cw-mermaid-legend">' +
|
||||||
|
'<span class="cw-leg cw-leg-add">added</span>' +
|
||||||
|
'<span class="cw-leg cw-leg-chg">changed</span>' +
|
||||||
|
'<span class="cw-leg cw-leg-rem">removed</span>' +
|
||||||
|
"</div>";
|
||||||
|
|
||||||
/** Build a markdown string with inline <ins>/<del> from a word-level prose diff. */
|
/** Build a markdown string with inline <ins>/<del> from a word-level prose diff. */
|
||||||
function wordMergedMarkdown(beforeRaw: string, afterRaw: string): string {
|
function wordMergedMarkdown(beforeRaw: string, afterRaw: string): string {
|
||||||
return diffWords(beforeRaw, afterRaw)
|
return diffWords(beforeRaw, afterRaw)
|
||||||
@@ -256,8 +275,19 @@ function renderOp(op: BlockOp, render: (src: string) => string): string {
|
|||||||
case "changed":
|
case "changed":
|
||||||
cls = "cw-changed";
|
cls = "cw-changed";
|
||||||
if (op.atomic) {
|
if (op.atomic) {
|
||||||
inner = safe(op.block.raw); // the NEW block, whole (INV-23)
|
const curBody = op.block.type === "mermaid" ? mermaidFenceBody(op.block.raw) : null;
|
||||||
badge = '<span class="cw-badge">changed</span>';
|
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 = '<span class="cw-badge">changed</span>';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
inner = safe(wordMergedMarkdown(op.before.raw, op.block.raw));
|
inner = safe(wordMergedMarkdown(op.before.raw, op.block.raw));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,14 +99,16 @@ describe("renderTrackChanges", () => {
|
|||||||
expect(html).toContain("cw-badge");
|
expect(html).toContain("cw-badge");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('emits <pre class="mermaid"> for a mermaid fence + a changed badge', () => {
|
it('emits <pre class="mermaid"> for a changed flowchart, augmented intra-diagram (#22)', () => {
|
||||||
const html = renderTrackChanges(
|
const html = renderTrackChanges(
|
||||||
"```mermaid\nflowchart LR\n a-->b\n```\n",
|
"```mermaid\nflowchart LR\n a-->b\n```\n",
|
||||||
"```mermaid\nflowchart LR\n a-->c\n```\n",
|
"```mermaid\nflowchart LR\n a-->c\n```\n",
|
||||||
);
|
);
|
||||||
expect(html).toContain('<pre class="mermaid">');
|
expect(html).toContain('<pre class="mermaid">');
|
||||||
expect(html).toContain("a-->c"); // current source, escaped
|
expect(html).toContain("a-->c"); // current source preserved as prefix, escaped
|
||||||
expect(html).toContain("cw-badge");
|
// 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('<span class="cw-badge">changed</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unchanged doc renders with no marks", () => {
|
it("unchanged doc renders with no marks", () => {
|
||||||
@@ -232,3 +234,33 @@ describe("renderAuthorship", () => {
|
|||||||
expect(renderAuthorship(text, spans)).toBe(renderAuthorship(text, spans));
|
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('<span class="cw-badge">changed</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
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('<span class="cw-badge">changed</span>');
|
||||||
|
expect(html).not.toContain("classDef cwAdded");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user