diff --git a/src/mermaidDiff.ts b/src/mermaidDiff.ts new file mode 100644 index 0000000..35b7eef --- /dev/null +++ b/src/mermaidDiff.ts @@ -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" }; +} diff --git a/src/mermaidFlowchartDiff.ts b/src/mermaidFlowchartDiff.ts new file mode 100644 index 0000000..fef5f2d --- /dev/null +++ b/src/mermaidFlowchartDiff.ts @@ -0,0 +1,4 @@ +// stub, replaced in Tasks 2-3 +export function diffFlowchart(_beforeSrc: string, currentSrc: string): string { + return currentSrc; +} diff --git a/src/mermaidSequenceDiff.ts b/src/mermaidSequenceDiff.ts new file mode 100644 index 0000000..d24ff04 --- /dev/null +++ b/src/mermaidSequenceDiff.ts @@ -0,0 +1,4 @@ +// stub, replaced in Tasks 4-5 +export function diffSequence(_beforeSrc: string, currentSrc: string): string { + return currentSrc; +} diff --git a/test/mermaidDiff.test.ts b/test/mermaidDiff.test.ts new file mode 100644 index 0000000..f02645d --- /dev/null +++ b/test/mermaidDiff.test.ts @@ -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); + }); +});