Skip to content

Commit 1349706

Browse files
smorandSébastien MORANDCopilot
authored
feat: add handDrawn look option for sketch-style rendering (#46)
* feat: add handDrawn look option for sketch-style rendering Mermaid v11 supports a 'look' setting that switches diagrams to a sketchy, Excalidraw-style rendering via rough.js. This exposes it as an optional parameter on the MCP tool so users can request that style from their AI assistant without having to embed a frontmatter config block. - Add 'look' field to the Zod schema (enum: classic, handDrawn; default classic) - Pass it through renderMermaid into mermaidConfig - Propagate it to the mermaid.ink payload for svg_url / png_url outputs - Update tool snapshot fixture and add unit tests for createMermaidInkUrl Backwards compatible: default 'classic' preserves current behavior. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: use result.data for argument destructuring in renderDiagram * docs: update createMermaidInkUrl JSDoc to include look in payload --------- Co-authored-by: Sébastien MORAND <sebastien.morand@ibm.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent aef3807 commit 1349706

6 files changed

Lines changed: 76 additions & 5 deletions

File tree

__tests__/tools/mermaid.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
"description": "Theme for the diagram (optional). Default is 'default'.",
1616
"default": "default"
1717
},
18+
"look": {
19+
"type": "string",
20+
"enum": ["classic", "handDrawn"],
21+
"description": "Visual style for the diagram (optional). 'classic' is the default Mermaid look; 'handDrawn' produces a sketchy, Excalidraw-like rendering (Mermaid v11+).",
22+
"default": "classic"
23+
},
1824
"backgroundColor": {
1925
"type": "string",
2026
"description": "Background color for the diagram (optional). Default is 'white'.",

__tests__/utils/mermaidUrl.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { inflateSync } from "node:zlib";
2+
import { describe, expect, it } from "vitest";
3+
import { createMermaidInkUrl } from "../../src/utils/mermaidUrl";
4+
5+
function decodePayload(url: string): {
6+
code: string;
7+
mermaid: { theme: string; look: string };
8+
} {
9+
const encoded = url.split("pako:")[1];
10+
const base64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
11+
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
12+
const buffer = Buffer.from(padded, "base64");
13+
return JSON.parse(inflateSync(buffer).toString("utf-8"));
14+
}
15+
16+
describe("createMermaidInkUrl", () => {
17+
const sample = "graph TD;A-->B;";
18+
19+
it("defaults to classic look when not specified", () => {
20+
const url = createMermaidInkUrl(sample, "img");
21+
expect(url.startsWith("https://mermaid.ink/img/pako:")).toBe(true);
22+
const payload = decodePayload(url);
23+
expect(payload.mermaid.theme).toBe("default");
24+
expect(payload.mermaid.look).toBe("classic");
25+
expect(payload.code).toBe(sample);
26+
});
27+
28+
it("propagates handDrawn look into the mermaid.ink payload", () => {
29+
const url = createMermaidInkUrl(sample, "svg", "forest", "handDrawn");
30+
expect(url.startsWith("https://mermaid.ink/svg/pako:")).toBe(true);
31+
const payload = decodePayload(url);
32+
expect(payload.mermaid.theme).toBe("forest");
33+
expect(payload.mermaid.look).toBe("handDrawn");
34+
});
35+
36+
it("uses the requested variant in the URL path", () => {
37+
const svgUrl = createMermaidInkUrl(sample, "svg");
38+
const imgUrl = createMermaidInkUrl(sample, "img");
39+
expect(svgUrl).toMatch(/^https:\/\/mermaid\.ink\/svg\//);
40+
expect(imgUrl).toMatch(/^https:\/\/mermaid\.ink\/img\//);
41+
});
42+
});

src/server.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,23 @@ function setupToolHandlers(server: McpServer): void {
5959
);
6060
}
6161

62-
const { mermaid, theme, backgroundColor, outputType = "base64" } = args;
62+
const {
63+
mermaid,
64+
theme,
65+
look,
66+
backgroundColor,
67+
outputType = "base64",
68+
} = result.data;
6369
Logger.info(
64-
`Rendering diagram (outputType=${outputType}, theme=${theme ?? "default"})`,
70+
`Rendering diagram (outputType=${outputType}, theme=${theme ?? "default"}, look=${look ?? "classic"})`,
6571
);
6672
const { id, svg, screenshot } = await withRetry(
6773
() =>
6874
renderMermaid(
6975
mermaid as string,
7076
theme as string,
7177
backgroundColor as string,
78+
look as "classic" | "handDrawn" | undefined,
7279
),
7380
{ maxAttempts: 3, delayMs: 500 },
7481
);
@@ -99,6 +106,7 @@ function setupToolHandlers(server: McpServer): void {
99106
mermaid as string,
100107
variant,
101108
(theme as string) || "default",
109+
(look as "classic" | "handDrawn" | undefined) || "classic",
102110
);
103111
return {
104112
content: [

src/tools/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ C-->D;.`)
1515
.describe("Theme for the diagram (optional). Default is 'default'.")
1616
.optional()
1717
.default("default"),
18+
look: z
19+
.enum(["classic", "handDrawn"])
20+
.describe(
21+
"Visual style for the diagram (optional). 'classic' is the default Mermaid look; 'handDrawn' produces a sketchy, Excalidraw-like rendering (Mermaid v11+).",
22+
)
23+
.optional()
24+
.default("classic"),
1825
backgroundColor: z
1926
.string()
2027
.describe(

src/utils/mermaidUrl.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,19 @@ function encodeMermaidToBase64Url(mermaid: string): string {
1515

1616
/**
1717
* Creates a public mermaid.ink URL for the given mermaid definition.
18-
* The payload must be a JSON object `{ code, mermaid: { theme } }` as expected by mermaid.ink.
18+
* The payload is a JSON object `{ code, mermaid: { theme, look } }` as expected by mermaid.ink.
19+
* `look` defaults to `"classic"`; pass `"handDrawn"` for a sketch-style rendering.
1920
*/
2021
export function createMermaidInkUrl(
2122
mermaid: string,
2223
variant: "svg" | "img",
2324
theme = "default",
25+
look: "classic" | "handDrawn" = "classic",
2426
): string {
25-
const payload = JSON.stringify({ code: mermaid, mermaid: { theme } });
27+
const payload = JSON.stringify({
28+
code: mermaid,
29+
mermaid: { theme, look },
30+
});
2631
const encoded = encodeMermaidToBase64Url(payload);
2732
return `https://mermaid.ink/${variant}/pako:${encoded}`;
2833
}

src/utils/render.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export async function renderMermaid(
2020
mermaid: string,
2121
theme = "default",
2222
backgroundColor = "white",
23+
look: "classic" | "handDrawn" = "classic",
2324
): Promise<RenderResult> {
2425
if (!renderer) renderer = createMermaidRenderer();
2526
const cssContent = `svg { background: ${backgroundColor}; }`;
@@ -31,8 +32,10 @@ export async function renderMermaid(
3132
screenshot: true,
3233
css: cssTmpPath,
3334
mermaidConfig: {
34-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
35+
// biome-ignore lint/suspicious/noExplicitAny: Mermaid accepts string theme values here, but the renderer boundary does not expose a precise type for this config property.
3536
theme: theme as any,
37+
// biome-ignore lint/suspicious/noExplicitAny: Mermaid accepts "classic" | "handDrawn" for `look`, but the renderer boundary does not expose a precise type for this config property.
38+
look: look as any,
3639
},
3740
});
3841
const r0 = r[0] as PromiseSettledResult<RenderResult>;

0 commit comments

Comments
 (0)