Skip to content

Commit 8a7df6a

Browse files
fix(@langchain/core): replace drawMermaidPng with drawMermaidImage (#8923)
1 parent 0092a79 commit 8a7df6a

File tree

2 files changed

+252
-3
lines changed

2 files changed

+252
-3
lines changed

langchain-core/src/runnables/graph_mermaid.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,15 +172,50 @@ export function drawMermaid(
172172
}
173173

174174
/**
175-
* Renders Mermaid graph using the Mermaid.INK API.
175+
* @deprecated Use `drawMermaidImage` instead.
176176
*/
177177
export async function drawMermaidPng(
178178
mermaidSyntax: string,
179179
config?: {
180180
backgroundColor?: string;
181181
}
182182
) {
183-
let { backgroundColor = "white" } = config ?? {};
183+
return drawMermaidImage(mermaidSyntax, {
184+
...config,
185+
imageType: "png",
186+
});
187+
}
188+
189+
/**
190+
* Renders Mermaid graph using the Mermaid.INK API.
191+
*
192+
* @example
193+
* ```javascript
194+
* const image = await drawMermaidImage(mermaidSyntax, {
195+
* backgroundColor: "white",
196+
* imageType: "png",
197+
* });
198+
* fs.writeFileSync("image.png", image);
199+
* ```
200+
*
201+
* @param mermaidSyntax - The Mermaid syntax to render.
202+
* @param config - The configuration for the image.
203+
* @returns The image as a Blob.
204+
*/
205+
export async function drawMermaidImage(
206+
mermaidSyntax: string,
207+
config?: {
208+
/**
209+
* The type of image to render.
210+
* @default "png"
211+
*/
212+
imageType?: "png" | "jpeg" | "webp";
213+
backgroundColor?: string;
214+
}
215+
) {
216+
let backgroundColor = config?.backgroundColor ?? "white";
217+
const imageType = config?.imageType ?? "png";
218+
184219
// Use btoa for compatibility, assume ASCII
185220
const mermaidSyntaxEncoded = btoa(mermaidSyntax);
186221
// Check if the background color is a hexadecimal color code using regex
@@ -190,7 +225,7 @@ export async function drawMermaidPng(
190225
backgroundColor = `!${backgroundColor}`;
191226
}
192227
}
193-
const imageUrl = `https://mermaid.ink/img/${mermaidSyntaxEncoded}?bgColor=${backgroundColor}`;
228+
const imageUrl = `https://mermaid.ink/img/${mermaidSyntaxEncoded}?bgColor=${backgroundColor}&type=${imageType}`;
194229
const res = await fetch(imageUrl);
195230
if (!res.ok) {
196231
throw new Error(
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import {
2+
describe,
3+
test,
4+
expect,
5+
jest,
6+
beforeEach,
7+
afterEach,
8+
} from "@jest/globals";
9+
import { drawMermaidImage } from "../graph_mermaid.js";
10+
11+
// Mock global fetch
12+
const mockFetch = jest.fn() as jest.MockedFunction<typeof fetch>;
13+
global.fetch = mockFetch;
14+
15+
describe("drawMermaidImage", () => {
16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
});
19+
20+
afterEach(() => {
21+
jest.restoreAllMocks();
22+
});
23+
24+
test("should render a basic Mermaid graph as PNG", async () => {
25+
const mockBlob = new Blob(["mock image data"], { type: "image/png" });
26+
mockFetch.mockResolvedValueOnce({
27+
ok: true,
28+
blob: async () => mockBlob,
29+
} as Response);
30+
31+
const mermaidSyntax = "graph TD; A --> B;";
32+
const result = await drawMermaidImage(mermaidSyntax);
33+
34+
expect(result).toBe(mockBlob);
35+
expect(mockFetch).toHaveBeenCalledTimes(1);
36+
37+
// Check the URL construction
38+
const expectedEncodedSyntax = btoa(mermaidSyntax);
39+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=png`;
40+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
41+
});
42+
43+
test("should handle different image types", async () => {
44+
const mockBlob = new Blob(["mock image data"], { type: "image/jpeg" });
45+
mockFetch.mockResolvedValueOnce({
46+
ok: true,
47+
blob: async () => mockBlob,
48+
} as Response);
49+
50+
const mermaidSyntax = "graph LR; Start --> End;";
51+
const result = await drawMermaidImage(mermaidSyntax, {
52+
imageType: "jpeg",
53+
});
54+
55+
expect(result).toBe(mockBlob);
56+
const expectedEncodedSyntax = btoa(mermaidSyntax);
57+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=jpeg`;
58+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
59+
});
60+
61+
test("should handle webp image type", async () => {
62+
const mockBlob = new Blob(["mock image data"], { type: "image/webp" });
63+
mockFetch.mockResolvedValueOnce({
64+
ok: true,
65+
blob: async () => mockBlob,
66+
} as Response);
67+
68+
const mermaidSyntax = "graph TB; X --> Y;";
69+
const result = await drawMermaidImage(mermaidSyntax, {
70+
imageType: "webp",
71+
});
72+
73+
expect(result).toBe(mockBlob);
74+
const expectedEncodedSyntax = btoa(mermaidSyntax);
75+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=webp`;
76+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
77+
});
78+
79+
test("should handle hex color backgrounds", async () => {
80+
const mockBlob = new Blob(["mock image data"], { type: "image/png" });
81+
mockFetch.mockResolvedValueOnce({
82+
ok: true,
83+
blob: async () => mockBlob,
84+
} as Response);
85+
86+
const mermaidSyntax = "flowchart TD; A --> B;";
87+
const result = await drawMermaidImage(mermaidSyntax, {
88+
backgroundColor: "#FF5733",
89+
});
90+
91+
expect(result).toBe(mockBlob);
92+
const expectedEncodedSyntax = btoa(mermaidSyntax);
93+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=#FF5733&type=png`;
94+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
95+
});
96+
97+
test("should handle short hex color backgrounds", async () => {
98+
const mockBlob = new Blob(["mock image data"], { type: "image/png" });
99+
mockFetch.mockResolvedValueOnce({
100+
ok: true,
101+
blob: async () => mockBlob,
102+
} as Response);
103+
104+
const mermaidSyntax = "flowchart TD; A --> B;";
105+
const result = await drawMermaidImage(mermaidSyntax, {
106+
backgroundColor: "#FFF",
107+
});
108+
109+
expect(result).toBe(mockBlob);
110+
const expectedEncodedSyntax = btoa(mermaidSyntax);
111+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=#FFF&type=png`;
112+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
113+
});
114+
115+
test("should handle named color backgrounds", async () => {
116+
const mockBlob = new Blob(["mock image data"], { type: "image/png" });
117+
mockFetch.mockResolvedValueOnce({
118+
ok: true,
119+
blob: async () => mockBlob,
120+
} as Response);
121+
122+
const mermaidSyntax = "classDiagram; class A; class B;";
123+
const result = await drawMermaidImage(mermaidSyntax, {
124+
backgroundColor: "transparent",
125+
});
126+
127+
expect(result).toBe(mockBlob);
128+
const expectedEncodedSyntax = btoa(mermaidSyntax);
129+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!transparent&type=png`;
130+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
131+
});
132+
133+
test("should throw error when API returns non-OK response", async () => {
134+
mockFetch.mockResolvedValueOnce({
135+
ok: false,
136+
status: 400,
137+
statusText: "Bad Request",
138+
} as Response);
139+
140+
const mermaidSyntax = "invalid syntax";
141+
142+
await expect(drawMermaidImage(mermaidSyntax)).rejects.toThrow(
143+
"Failed to render the graph using the Mermaid.INK API.\nStatus code: 400\nStatus text: Bad Request"
144+
);
145+
146+
expect(mockFetch).toHaveBeenCalledTimes(1);
147+
});
148+
149+
test("should handle network errors", async () => {
150+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
151+
152+
const mermaidSyntax = "graph TD; A --> B;";
153+
154+
await expect(drawMermaidImage(mermaidSyntax)).rejects.toThrow(
155+
"Network error"
156+
);
157+
158+
expect(mockFetch).toHaveBeenCalledTimes(1);
159+
});
160+
161+
test("should properly encode complex Mermaid syntax", async () => {
162+
const mockBlob = new Blob(["mock image data"], { type: "image/png" });
163+
mockFetch.mockResolvedValueOnce({
164+
ok: true,
165+
blob: async () => mockBlob,
166+
} as Response);
167+
168+
const complexSyntax = `graph TD;
169+
A[Christmas] -->|Get money| B(Go shopping)
170+
B --> C{Let me think}
171+
C -->|One| D[Laptop]
172+
C -->|Two| E[iPhone]
173+
C -->|Three| F[fa:fa-car Car]`;
174+
175+
const result = await drawMermaidImage(complexSyntax);
176+
177+
expect(result).toBe(mockBlob);
178+
const expectedEncodedSyntax = btoa(complexSyntax);
179+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=png`;
180+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
181+
});
182+
183+
test("should handle undefined background color", async () => {
184+
const mockBlob = new Blob(["mock image data"], { type: "image/png" });
185+
mockFetch.mockResolvedValueOnce({
186+
ok: true,
187+
blob: async () => mockBlob,
188+
} as Response);
189+
190+
const mermaidSyntax = "graph TD; A --> B;";
191+
const result = await drawMermaidImage(mermaidSyntax, {
192+
backgroundColor: undefined,
193+
});
194+
195+
expect(result).toBe(mockBlob);
196+
const expectedEncodedSyntax = btoa(mermaidSyntax);
197+
const expectedUrl = `https://mermaid.ink/img/${expectedEncodedSyntax}?bgColor=!white&type=png`;
198+
expect(mockFetch).toHaveBeenCalledWith(expectedUrl);
199+
});
200+
201+
test("should handle server error with different status codes", async () => {
202+
mockFetch.mockResolvedValueOnce({
203+
ok: false,
204+
status: 500,
205+
statusText: "Internal Server Error",
206+
} as Response);
207+
208+
const mermaidSyntax = "graph TD; A --> B;";
209+
210+
await expect(drawMermaidImage(mermaidSyntax)).rejects.toThrow(
211+
"Failed to render the graph using the Mermaid.INK API.\nStatus code: 500\nStatus text: Internal Server Error"
212+
);
213+
});
214+
});

0 commit comments

Comments
 (0)