F7.1: intra-diagram mermaid diffing (#22) #28
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* mermaidDiff — F7.1 (#22) dispatcher. Detects a mermaid diagram's type and
|
||||
* routes a baseline→current diff to the matching pure differ, which re-emits the
|
||||
* CURRENT diagram source augmented with mermaid styling directives (INV-29). Any
|
||||
* unsupported type or parser surprise degrades to the v1 whole-block badge
|
||||
* (INV-30) — this function never throws. vscode-free, DOM-free, deterministic.
|
||||
*/
|
||||
import { diffFlowchart } from "./mermaidFlowchartDiff";
|
||||
import { diffSequence } from "./mermaidSequenceDiff";
|
||||
|
||||
/** Theme-neutral colors baked into emitted mermaid source (source can't read CSS vars). */
|
||||
export const CW_COLORS = {
|
||||
added: "#2ea043",
|
||||
changed: "#d29922",
|
||||
removed: "#808080",
|
||||
} as const;
|
||||
|
||||
export type DiagramType = "flowchart" | "sequence" | "other";
|
||||
|
||||
export interface MermaidDiffAugmented {
|
||||
kind: "augmented";
|
||||
source: string;
|
||||
}
|
||||
export interface MermaidDiffFallback {
|
||||
kind: "fallback";
|
||||
}
|
||||
export type MermaidDiffResult = MermaidDiffAugmented | MermaidDiffFallback;
|
||||
|
||||
export function detectDiagramType(source: string): DiagramType {
|
||||
for (const line of source.split(/\r?\n/)) {
|
||||
const t = line.trim();
|
||||
if (t === "" || t.startsWith("%%")) continue;
|
||||
if (/^(flowchart|graph)\b/.test(t)) return "flowchart";
|
||||
if (/^sequenceDiagram\b/.test(t)) return "sequence";
|
||||
return "other";
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function diffMermaid(beforeSrc: string, currentSrc: string): MermaidDiffResult {
|
||||
const type = detectDiagramType(currentSrc);
|
||||
try {
|
||||
if (type === "flowchart") return { kind: "augmented", source: diffFlowchart(beforeSrc, currentSrc) };
|
||||
if (type === "sequence") return { kind: "augmented", source: diffSequence(beforeSrc, currentSrc) };
|
||||
} catch {
|
||||
return { kind: "fallback" };
|
||||
}
|
||||
return { kind: "fallback" };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// stub, replaced in Tasks 2-3
|
||||
export function diffFlowchart(_beforeSrc: string, currentSrc: string): string {
|
||||
return currentSrc;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// stub, replaced in Tasks 4-5
|
||||
export function diffSequence(_beforeSrc: string, currentSrc: string): string {
|
||||
return currentSrc;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { detectDiagramType, diffMermaid } from "../src/mermaidDiff";
|
||||
|
||||
describe("detectDiagramType", () => {
|
||||
it("detects flowchart from `flowchart` / `graph` headers", () => {
|
||||
expect(detectDiagramType("flowchart LR\n a-->b")).toBe("flowchart");
|
||||
expect(detectDiagramType("graph TD\n a-->b")).toBe("flowchart");
|
||||
expect(detectDiagramType(" \n%% c\ngraph TD")).toBe("flowchart");
|
||||
});
|
||||
it("detects sequence from `sequenceDiagram`", () => {
|
||||
expect(detectDiagramType("sequenceDiagram\n A->>B: hi")).toBe("sequence");
|
||||
});
|
||||
it("returns `other` for unsupported types", () => {
|
||||
expect(detectDiagramType("classDiagram\n class A")).toBe("other");
|
||||
expect(detectDiagramType("stateDiagram-v2\n s1 --> s2")).toBe("other");
|
||||
expect(detectDiagramType("")).toBe("other");
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffMermaid", () => {
|
||||
it("falls back for an unsupported diagram type", () => {
|
||||
expect(diffMermaid("classDiagram\n class A", "classDiagram\n class B")).toEqual({ kind: "fallback" });
|
||||
});
|
||||
it("falls back (never throws) when the differ throws", () => {
|
||||
// A deliberately malformed flowchart still degrades gracefully.
|
||||
const r = diffMermaid("flowchart LR", "flowchart LR\n a -->");
|
||||
expect(["augmented", "fallback"]).toContain(r.kind);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user