feat(f7): block splitter for the render engine (#21)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* trackChangesModel — F7 pure, vscode-free render engine (spec §6.2, INV-22/23).
|
||||||
|
*
|
||||||
|
* `renderTrackChanges(baselineText, currentText)` returns the annotated HTML body
|
||||||
|
* for the preview webview: a block-level diff (LCS over normalized blocks) with
|
||||||
|
* word-level `<ins>`/`<del>` refinement inside changed PROSE blocks, and CODE /
|
||||||
|
* MERMAID fences kept ATOMIC (INV-23 — never word-refined, never partially
|
||||||
|
* rendered). Deterministic: same inputs → identical HTML (INV-22). No vscode, no
|
||||||
|
* DOM — `markdown-it` and `diff` are libraries; mermaid runs later in the webview.
|
||||||
|
*/
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import { diffArrays, diffWords } from "diff";
|
||||||
|
|
||||||
|
export type BlockType = "prose" | "code" | "mermaid";
|
||||||
|
|
||||||
|
export interface Block {
|
||||||
|
/** Verbatim source of the block (no surrounding blank lines). */
|
||||||
|
raw: string;
|
||||||
|
/** Normalized match key: lowercased, whitespace-collapsed. */
|
||||||
|
key: string;
|
||||||
|
type: BlockType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize(raw: string): string {
|
||||||
|
return raw.trim().replace(/\s+/g, " ").toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBlock(raw: string, type: BlockType): Block {
|
||||||
|
return { raw, key: normalize(raw), type };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Split markdown into top-level blocks; fenced code/mermaid stay whole. */
|
||||||
|
export function splitBlocks(text: string): Block[] {
|
||||||
|
const lines = text.split(/\r?\n/);
|
||||||
|
const blocks: Block[] = [];
|
||||||
|
let buf: string[] = [];
|
||||||
|
const flushProse = () => {
|
||||||
|
const raw = buf.join("\n");
|
||||||
|
if (raw.trim()) blocks.push(makeBlock(raw, "prose"));
|
||||||
|
buf = [];
|
||||||
|
};
|
||||||
|
let i = 0;
|
||||||
|
while (i < lines.length) {
|
||||||
|
const line = lines[i];
|
||||||
|
const fence = line.match(/^(\s*)(`{3,}|~{3,})(.*)$/);
|
||||||
|
if (fence) {
|
||||||
|
flushProse();
|
||||||
|
const marker = fence[2][0]; // ` or ~
|
||||||
|
const info = fence[3].trim().split(/\s+/)[0].toLowerCase();
|
||||||
|
const fenceLines = [line];
|
||||||
|
i++;
|
||||||
|
while (i < lines.length) {
|
||||||
|
fenceLines.push(lines[i]);
|
||||||
|
const closed = lines[i].trim().startsWith(marker.repeat(3));
|
||||||
|
i++;
|
||||||
|
if (closed) break;
|
||||||
|
}
|
||||||
|
blocks.push(makeBlock(fenceLines.join("\n"), info === "mermaid" ? "mermaid" : "code"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.trim() === "") {
|
||||||
|
flushProse();
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buf.push(line);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
flushProse();
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { splitBlocks } from "../src/trackChangesModel";
|
||||||
|
|
||||||
|
describe("splitBlocks", () => {
|
||||||
|
it("splits prose paragraphs on blank lines, dropping empties", () => {
|
||||||
|
const blocks = splitBlocks("# Heading\n\nFirst para.\n\nSecond para.\n");
|
||||||
|
expect(blocks.map((b) => b.type)).toEqual(["prose", "prose", "prose"]);
|
||||||
|
expect(blocks[0].raw).toBe("# Heading");
|
||||||
|
expect(blocks[2].raw).toBe("Second para.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a fenced code block whole and tags it `code`", () => {
|
||||||
|
const text = "Intro.\n\n```ts\nconst a = 1;\n\nconst b = 2;\n```\n\nOutro.\n";
|
||||||
|
const blocks = splitBlocks(text);
|
||||||
|
expect(blocks.map((b) => b.type)).toEqual(["prose", "code", "prose"]);
|
||||||
|
expect(blocks[1].raw).toBe("```ts\nconst a = 1;\n\nconst b = 2;\n```");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tags a mermaid fence `mermaid`", () => {
|
||||||
|
const blocks = splitBlocks("```mermaid\nflowchart LR\n a-->b\n```\n");
|
||||||
|
expect(blocks).toHaveLength(1);
|
||||||
|
expect(blocks[0].type).toBe("mermaid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalizes the key (whitespace-collapsed) for matching", () => {
|
||||||
|
const blocks = splitBlocks("Hello world\n");
|
||||||
|
expect(blocks[0].key).toBe("hello world");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user