fix(markdown): preserve first definitions across Mermaid segments

This commit is contained in:
Mohamed Boudra
2026-10-15 17:03:08 +02:00
parent a97f02e16f
commit 30b7b269e9
2 changed files with 29 additions and 5 deletions
@@ -11,10 +11,11 @@ interface Segment {
export function splitMermaidFences(markdown: string): Segment[] {
if (!/mermaid/i.test(markdown)) return [{ kind: "markdown", id: "0", source: markdown }];
const segments: Segment[] = [];
const definitions: Definition[] = [];
const definitions = new Map<string, Definition>();
let cursor = 0;
function visit(node: RootContent): void {
if (node.type === "definition") definitions.push(node);
if (node.type === "definition" && !definitions.has(node.identifier))
definitions.set(node.identifier, node);
if (node.type === "code" && node.lang?.toLowerCase() === "mermaid" && node.position) {
const start = node.position.start.offset!;
const end = node.position.end.offset!;
@@ -36,9 +37,10 @@ export function splitMermaidFences(markdown: string): Segment[] {
id: String(segments.length),
source: markdown.slice(cursor),
});
// Reference links keep their document-wide scope on both sides of a diagram.
// Prepend the document's first definitions after splitting, so fragment-local
// duplicates cannot override them. Original source slices and fence offsets stay intact.
if (segments.some((segment) => segment.kind === "mermaid")) {
const referenceSource = definitions
const referenceSource = [...definitions.values()]
.map((node) => {
const label = node.identifier.replace(/[\\[\]]/g, "\\$&");
const url = node.url.replace(/[\\<>]/g, "\\$&");
@@ -48,7 +50,7 @@ export function splitMermaidFences(markdown: string): Segment[] {
.join("\n");
if (referenceSource)
for (const segment of segments) {
if (segment.kind === "markdown") segment.source += `\n\n${referenceSource}`;
if (segment.kind === "markdown") segment.source = `${referenceSource}\n\n${segment.source}`;
}
}
return segments;
@@ -369,3 +369,25 @@ it("retries transient image decoding failures instead of caching an unavailable
expect(attempts).toBe(2);
client.clear();
});
it("keeps the document's first reference definition across every native Mermaid segment", async () => {
const { fromMarkdown } = await import("mdast-util-from-markdown");
const { splitMermaidFences } = await import("./fence/mermaid/segments");
const before = '[first]: /repo/first.ts "Original"\n\n';
const after = '\n\n[open][first]\n\n[first]: /repo/second.ts "Later"';
const source = `${before}\`\`\`mermaid\ngraph TD\nA-->B\n\`\`\`${after}`;
const firstDefinition = (markdown: string) =>
fromMarkdown(markdown).children.find((node) => node.type === "definition");
const canonical = firstDefinition(source);
const segments = splitMermaidFences(source);
for (const segment of segments.filter((candidate) => candidate.kind === "markdown")) {
expect(firstDefinition(segment.source)).toMatchObject({
identifier: canonical?.identifier,
url: canonical?.url,
title: canonical?.title,
});
}
expect(segments[0].source.endsWith(before)).toBe(true);
expect(segments[2].source.endsWith(after)).toBe(true);
expect(segments[1]).toEqual({ kind: "mermaid", id: "1", source: "graph TD\nA-->B" });
});