mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 04:03:05 +00:00
fix(markdown): retain diagram identity and reference scope
This commit is contained in:
@@ -1,40 +1,54 @@
|
||||
import { fromMarkdown } from "mdast-util-from-markdown";
|
||||
import type { RootContent } from "mdast";
|
||||
import type { Definition, RootContent } from "mdast";
|
||||
|
||||
interface Segment {
|
||||
kind: "markdown" | "mermaid";
|
||||
offset: number;
|
||||
id: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
/** Native enriched-markdown has no fenced-block override. Only diagrams leave its engine. */
|
||||
export function splitMermaidFences(markdown: string): Segment[] {
|
||||
if (!/mermaid/i.test(markdown)) return [{ kind: "markdown", offset: 0, source: markdown }];
|
||||
if (!/mermaid/i.test(markdown)) return [{ kind: "markdown", id: "0", source: markdown }];
|
||||
const segments: Segment[] = [];
|
||||
const definitions: Definition[] = [];
|
||||
let cursor = 0;
|
||||
function visit(node: RootContent): void {
|
||||
if (node.type === "definition") definitions.push(node);
|
||||
if (node.type === "code" && node.lang?.toLowerCase() === "mermaid" && node.position) {
|
||||
const start = node.position.start.offset!;
|
||||
const end = node.position.end.offset!;
|
||||
if (start > cursor)
|
||||
segments.push({ kind: "markdown", offset: cursor, source: markdown.slice(cursor, start) });
|
||||
segments.push({ kind: "mermaid", offset: start, source: node.value });
|
||||
segments.push({
|
||||
kind: "markdown",
|
||||
id: String(segments.length),
|
||||
source: markdown.slice(cursor, start),
|
||||
});
|
||||
segments.push({ kind: "mermaid", id: String(segments.length), source: node.value });
|
||||
cursor = end;
|
||||
} else if ("children" in node) node.children.forEach(visit);
|
||||
}
|
||||
const tree = fromMarkdown(markdown);
|
||||
tree.children.forEach(visit);
|
||||
if (cursor < markdown.length || segments.length === 0)
|
||||
segments.push({ kind: "markdown", offset: cursor, source: markdown.slice(cursor) });
|
||||
segments.push({
|
||||
kind: "markdown",
|
||||
id: String(segments.length),
|
||||
source: markdown.slice(cursor),
|
||||
});
|
||||
// Reference links keep their document-wide scope on both sides of a diagram.
|
||||
if (segments.some((segment) => segment.kind === "mermaid")) {
|
||||
const definitions = tree.children
|
||||
.filter((node) => node.type === "definition")
|
||||
.map((node) => markdown.slice(node.position!.start.offset, node.position!.end.offset))
|
||||
const referenceSource = definitions
|
||||
.map((node) => {
|
||||
const label = node.identifier.replace(/[\\[\]]/g, "\\$&");
|
||||
const url = node.url.replace(/[\\<>]/g, "\\$&");
|
||||
const title = node.title ? ` "${node.title.replace(/[\\"]/g, "\\$&")}"` : "";
|
||||
return `[${label}]: <${url}>${title}`;
|
||||
})
|
||||
.join("\n");
|
||||
if (definitions)
|
||||
if (referenceSource)
|
||||
for (const segment of segments) {
|
||||
if (segment.kind === "markdown") segment.source += `\n\n${definitions}`;
|
||||
if (segment.kind === "markdown") segment.source += `\n\n${referenceSource}`;
|
||||
}
|
||||
}
|
||||
return segments;
|
||||
|
||||
@@ -85,12 +85,12 @@ it("extracts only Mermaid fences for the native diagram host", async () => {
|
||||
const source =
|
||||
"First.\n\n```ts\nconst n = 1;\n```\n\n```mermaid\ngraph TD\nA-->B\n```\n\n> Last.";
|
||||
expect(splitMermaidFences(source)).toEqual([
|
||||
{ kind: "markdown", offset: 0, source: "First.\n\n```ts\nconst n = 1;\n```\n\n" },
|
||||
{ kind: "mermaid", offset: 32, source: "graph TD\nA-->B" },
|
||||
{ kind: "markdown", offset: 61, source: "\n\n> Last." },
|
||||
{ kind: "markdown", id: "0", source: "First.\n\n```ts\nconst n = 1;\n```\n\n" },
|
||||
{ kind: "mermaid", id: "1", source: "graph TD\nA-->B" },
|
||||
{ kind: "markdown", id: "2", source: "\n\n> Last." },
|
||||
]);
|
||||
expect(splitMermaidFences("```md\n```mermaid\n```")).toEqual([
|
||||
{ kind: "markdown", offset: 0, source: "```md\n```mermaid\n```" },
|
||||
{ kind: "markdown", id: "0", source: "```md\n```mermaid\n```" },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -100,12 +100,31 @@ it("keeps reference links available across native Mermaid boundaries", async ()
|
||||
"[Before][file]\n\n```mermaid\ngraph TD\nA-->B\n```\n\n[After][file]\n\n[file]: src/file.ts",
|
||||
);
|
||||
for (const part of parts.filter((candidate) => candidate.kind === "markdown"))
|
||||
expect(part.source).toContain("[file]: src/file.ts");
|
||||
expect(part.source).toContain("[file]: <src/file.ts>");
|
||||
});
|
||||
|
||||
it("recognizes mixed-case Mermaid fence info and incomplete streaming fences", async () => {
|
||||
const { splitMermaidFences } = await import("./fence/mermaid/segments");
|
||||
expect(splitMermaidFences("```Mermaid title\ngraph TD\nA-->B")).toEqual([
|
||||
{ kind: "mermaid", offset: 0, source: "graph TD\nA-->B" },
|
||||
{ kind: "mermaid", id: "0", source: "graph TD\nA-->B" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("carries reference definitions nested in quotes across a diagram", async () => {
|
||||
const { splitMermaidFences } = await import("./fence/mermaid/segments");
|
||||
const parts = splitMermaidFences(
|
||||
"[open][file]\n\n```mermaid\ngraph TD\nA-->B\n```\n\n> [file]: src/file.ts",
|
||||
);
|
||||
expect(parts[0].source).toContain("[file]: <src/file.ts>");
|
||||
});
|
||||
|
||||
it("keeps diagram identity when an earlier workspace image resolves", async () => {
|
||||
const { splitMermaidFences } = await import("./fence/mermaid/segments");
|
||||
const source = "\n\n```mermaid\ngraph TD\nA-->B\n```";
|
||||
const before = splitMermaidFences(source).find((part) => part.kind === "mermaid");
|
||||
const after = splitMermaidFences(
|
||||
source.replace("a.png", "file:///long/cache/path/image.png"),
|
||||
).find((part) => part.kind === "mermaid");
|
||||
expect(before).toHaveProperty("id");
|
||||
expect(after?.id).toBe(before?.id);
|
||||
});
|
||||
|
||||
@@ -158,17 +158,17 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({
|
||||
const closeImage = useStableEvent(() => setImage(null));
|
||||
const segments = useMemo(
|
||||
() =>
|
||||
isWeb ? [{ kind: "markdown", offset: 0, source: markdown }] : splitMermaidFences(markdown),
|
||||
isWeb ? [{ kind: "markdown", id: "0", source: markdown }] : splitMermaidFences(markdown),
|
||||
[markdown],
|
||||
);
|
||||
const content = (
|
||||
<MermaidStreamingContext.Provider value={streaming}>
|
||||
{segments.map((segment) =>
|
||||
segment.kind === "mermaid" ? (
|
||||
<MermaidFence key={segment.offset} code={segment.source} />
|
||||
<MermaidFence key={`${segment.kind}:${segment.id}`} code={segment.source} />
|
||||
) : (
|
||||
<ThemedMarkdown
|
||||
key={segment.offset}
|
||||
key={`${segment.kind}:${segment.id}`}
|
||||
markdown={segment.source}
|
||||
containerStyle={isWeb ? WEB_CONTAINER : undefined}
|
||||
uniProps={compact ? compactStyle : regularStyle}
|
||||
|
||||
Reference in New Issue
Block a user