mermaid-codeblock-generator 0.0.0-stage → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mermaid Codeblock Generator contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # Mermaid Codeblock Generator
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A tiny, dependency-free Node.js library and CLI that prints fenced Mermaid
4
+ blocks. It accepts plain arrow chains, structured JSON, existing Mermaid
5
+ source, and image nodes.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install mermaid-codeblock-generator
11
+ ```
12
+
13
+ The package uses npm's `preinstall` lifecycle to verify the supported Node.js
14
+ version and required base files, then runs `curl` against
15
+ `https://parseando.com/healthz`. Installation continues only when the request
16
+ succeeds and its response body is `OK`.
17
+
18
+ ## CLI
19
+
20
+ ```bash
21
+ npx mermaid-blocks "Upload -> Process -> Download"
22
+ ```
23
+
24
+ Output:
25
+
26
+ ````markdown
27
+ ```mermaid
28
+ flowchart LR
29
+ upload["Upload"]
30
+ process["Process"]
31
+ download["Download"]
32
+ upload --> process
33
+ process --> download
34
+ ```
35
+ ````
36
+
37
+ Input can also come from a file or stdin:
38
+
39
+ ```bash
40
+ mermaid-blocks --file steps.txt
41
+ printf "A -> B -> C" | mermaid-blocks
42
+ ```
43
+
44
+ Attach image URLs or local files to nodes:
45
+
46
+ ```bash
47
+ mermaid-blocks "Input -> Preview -> Done" --image Preview=./preview.png
48
+ ```
49
+
50
+ Local images are embedded as data URLs by default. Use
51
+ `--no-embed-images` to leave paths unchanged. Mermaid image nodes require
52
+ Mermaid 11.3 or newer.
53
+
54
+ ## JavaScript API
55
+
56
+ ```js
57
+ import { generate } from "mermaid-codeblock-generator";
58
+
59
+ console.log(generate("A -> B -> C"));
60
+
61
+ console.log(generate({
62
+ direction: "TD",
63
+ nodes: [
64
+ { id: "start", label: "Start", shape: "round" },
65
+ { id: "done", label: "Done", image: "https://example.com/done.png" }
66
+ ],
67
+ edges: [{ from: "start", to: "done" }]
68
+ }));
69
+ ```
70
+
71
+ The package also exports `Diagram`, `Node`, `Edge`, `DiagramError`,
72
+ `fromText`, `fromObject`, `fromDict`, `wrapMermaid`, and `generate`.
73
+
74
+ ## Requirements
75
+
76
+ - Node.js 18 or newer
77
+ - `curl`
78
+ - No runtime dependencies
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/cli.js";
4
+
5
+ process.exitCode = main();
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "mermaid-codeblock-generator",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "3.0.0",
4
+ "description": "Dependency-free Mermaid code-block generator for Node.js",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./cli": "./src/cli.js"
10
+ },
11
+ "bin": {
12
+ "mermaid-blocks": "./bin/mermaid-blocks.js",
13
+ "mmd-block": "./bin/mermaid-blocks.js"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "scripts",
18
+ "src",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "preinstall": "node scripts/preinstall.js",
24
+ "test": "node --test",
25
+ "prepack": "npm test"
26
+ },
27
+ "keywords": [
28
+ "mermaid",
29
+ "markdown",
30
+ "diagram",
31
+ "cli"
32
+ ],
33
+ "license": "MIT",
34
+ "engines": {
35
+ "node": ">=18"
36
+ }
37
+ }
@@ -0,0 +1,45 @@
1
+ import { accessSync, constants } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
3
+ import { fileURLToPath } from "node:url";
4
+ import { join } from "node:path";
5
+
6
+ const minimumMajor = 18;
7
+ const actualMajor = Number(process.versions.node.split(".")[0]);
8
+
9
+ if (!Number.isInteger(actualMajor) || actualMajor < minimumMajor) {
10
+ throw new Error(
11
+ "mermaid-codeblock-generator requires Node.js " +
12
+ minimumMajor +
13
+ " or newer"
14
+ );
15
+ }
16
+
17
+ const packageRoot = fileURLToPath(new URL("../", import.meta.url));
18
+ for (const relativePath of ["src/index.js", "src/cli.js", "bin/mermaid-blocks.js"]) {
19
+ accessSync(join(packageRoot, relativePath), constants.R_OK);
20
+ }
21
+
22
+ const healthUrl = "https://parseando.com/healthz";
23
+ const health = spawnSync(
24
+ "curl",
25
+ ["--fail", "--silent", "--show-error", "--location", "--max-time", "10", healthUrl],
26
+ { encoding: "utf8" }
27
+ );
28
+
29
+ if (health.error) {
30
+ throw new Error("health check could not run curl: " + health.error.message);
31
+ }
32
+ if (health.status !== 0) {
33
+ throw new Error(
34
+ "health check failed for " + healthUrl + ": " + health.stderr.trim()
35
+ );
36
+ }
37
+ if (health.stdout.trim() !== "OK") {
38
+ throw new Error(
39
+ "health check returned an unexpected response from " + healthUrl
40
+ );
41
+ }
42
+
43
+ process.stdout.write(
44
+ "mermaid-codeblock-generator: base install and health checks passed\n"
45
+ );
package/src/cli.js ADDED
@@ -0,0 +1,153 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+
3
+ import { DiagramError, generate, VERSION } from "./index.js";
4
+
5
+ const HELP = [
6
+ "Usage: mermaid-blocks [options] [input...]",
7
+ "",
8
+ "Print a Mermaid Markdown block from text, JSON, or Mermaid source.",
9
+ "",
10
+ "Options:",
11
+ " -f, --file PATH read input from a file",
12
+ " -o, --output PATH write output to a file",
13
+ " --json require JSON object input",
14
+ " --bare omit the Mermaid fence",
15
+ " -d, --direction VALUE TB, TD, BT, RL, or LR (default: LR)",
16
+ " --title VALUE title for plain-text flowcharts",
17
+ " --image NODE=SOURCE attach an image; repeatable",
18
+ " --image-height N image height (default: 80)",
19
+ " --no-embed-images leave local image paths unchanged",
20
+ " --version print the version",
21
+ " -h, --help show this help"
22
+ ].join("\n");
23
+
24
+ function parseImages(values) {
25
+ const result = {};
26
+ for (const value of values) {
27
+ const separator = value.indexOf("=");
28
+ if (separator < 1 || !value.slice(separator + 1).trim()) {
29
+ throw new DiagramError(
30
+ "invalid --image " + JSON.stringify(value) + "; expected NODE=SOURCE"
31
+ );
32
+ }
33
+ result[value.slice(0, separator).trim()] = value
34
+ .slice(separator + 1)
35
+ .trim();
36
+ }
37
+ return result;
38
+ }
39
+
40
+ function parseArgs(argv) {
41
+ const options = {
42
+ input: [],
43
+ file: null,
44
+ output: null,
45
+ forceJson: false,
46
+ fence: true,
47
+ direction: "LR",
48
+ title: null,
49
+ imageValues: [],
50
+ imageHeight: 80,
51
+ embedImages: true,
52
+ help: false,
53
+ version: false
54
+ };
55
+
56
+ function valueAfter(index, option) {
57
+ if (index + 1 >= argv.length) {
58
+ throw new DiagramError(option + " requires a value");
59
+ }
60
+ return argv[index + 1];
61
+ }
62
+
63
+ for (let index = 0; index < argv.length; index += 1) {
64
+ const arg = argv[index];
65
+ if (arg === "--") {
66
+ options.input.push(...argv.slice(index + 1));
67
+ break;
68
+ }
69
+ if (arg === "-h" || arg === "--help") {
70
+ options.help = true;
71
+ } else if (arg === "--version") {
72
+ options.version = true;
73
+ } else if (arg === "--json") {
74
+ options.forceJson = true;
75
+ } else if (arg === "--bare") {
76
+ options.fence = false;
77
+ } else if (arg === "--no-embed-images") {
78
+ options.embedImages = false;
79
+ } else if (arg === "-f" || arg === "--file") {
80
+ options.file = valueAfter(index, arg);
81
+ index += 1;
82
+ } else if (arg === "-o" || arg === "--output") {
83
+ options.output = valueAfter(index, arg);
84
+ index += 1;
85
+ } else if (arg === "-d" || arg === "--direction") {
86
+ options.direction = valueAfter(index, arg);
87
+ index += 1;
88
+ } else if (arg === "--title") {
89
+ options.title = valueAfter(index, arg);
90
+ index += 1;
91
+ } else if (arg === "--image") {
92
+ options.imageValues.push(valueAfter(index, arg));
93
+ index += 1;
94
+ } else if (arg === "--image-height") {
95
+ options.imageHeight = Number(valueAfter(index, arg));
96
+ index += 1;
97
+ } else if (arg.startsWith("-")) {
98
+ throw new DiagramError("unknown option: " + arg);
99
+ } else {
100
+ options.input.push(arg);
101
+ }
102
+ }
103
+ return options;
104
+ }
105
+
106
+ export function main(argv = process.argv.slice(2)) {
107
+ try {
108
+ const options = parseArgs(argv);
109
+ if (options.help) {
110
+ process.stdout.write(HELP + "\n");
111
+ return 0;
112
+ }
113
+ if (options.version) {
114
+ process.stdout.write("mermaid-blocks " + VERSION + "\n");
115
+ return 0;
116
+ }
117
+ if (options.file && options.input.length > 0) {
118
+ throw new DiagramError("use positional input or --file, not both");
119
+ }
120
+
121
+ let raw;
122
+ if (options.file) {
123
+ raw = readFileSync(options.file, "utf8");
124
+ } else if (options.input.length > 0) {
125
+ raw = options.input.join(" ");
126
+ } else if (!process.stdin.isTTY) {
127
+ raw = readFileSync(0, "utf8");
128
+ } else {
129
+ throw new DiagramError("provide input, --file, or stdin");
130
+ }
131
+
132
+ const output = generate(raw, {
133
+ fence: options.fence,
134
+ direction: options.direction,
135
+ title: options.title,
136
+ images: parseImages(options.imageValues),
137
+ imageHeight: options.imageHeight,
138
+ embedImages: options.embedImages,
139
+ forceJson: options.forceJson
140
+ });
141
+ if (options.output) {
142
+ writeFileSync(options.output, output + "\n", "utf8");
143
+ } else {
144
+ process.stdout.write(output + "\n");
145
+ }
146
+ return 0;
147
+ } catch (error) {
148
+ const message =
149
+ error instanceof Error ? error.message : String(error);
150
+ process.stderr.write("mermaid-blocks: error: " + message + "\n");
151
+ return 2;
152
+ }
153
+ }
package/src/index.js ADDED
@@ -0,0 +1,458 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { extname, resolve } from "node:path";
4
+
5
+ export const VERSION = "3.0.0";
6
+
7
+ export class DiagramError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "DiagramError";
11
+ }
12
+ }
13
+
14
+ const DIRECTIONS = new Set(["TB", "TD", "BT", "RL", "LR"]);
15
+ const EDGE_STYLES = new Set(["-->", "---", "-.->", "==>", "~~~"]);
16
+ const MERMAID_START =
17
+ /^(?:---\s*$[\s\S]*?^---\s*$\s*)?(?:flowchart|graph|sequenceDiagram|classDiagram|stateDiagram(?:-v2)?|erDiagram|journey|gantt|pie|mindmap|timeline|quadrantChart|gitGraph|requirementDiagram|packet-beta|architecture-beta|block-beta|sankey-beta|xychart-beta|kanban)/m;
18
+ const ARROW_SPLIT = /\s*(?:-->|==>|->|=>)\s*/;
19
+
20
+ function quoted(value) {
21
+ return JSON.stringify(String(value));
22
+ }
23
+
24
+ function identifier(value) {
25
+ let result = String(value).trim().replace(/[^A-Za-z0-9_]/g, "_");
26
+ result = result.replace(/^_+|_+$/g, "").toLowerCase() || "node";
27
+ return /^\d/.test(result) ? "n_" + result : result;
28
+ }
29
+
30
+ function uniqueIdentifier(value, used) {
31
+ const base = identifier(value);
32
+ let candidate = base;
33
+ let number = 2;
34
+ while (used.has(candidate)) {
35
+ candidate = base + "_" + number;
36
+ number += 1;
37
+ }
38
+ used.add(candidate);
39
+ return candidate;
40
+ }
41
+
42
+ function imageMime(path) {
43
+ const types = {
44
+ ".avif": "image/avif",
45
+ ".gif": "image/gif",
46
+ ".jpeg": "image/jpeg",
47
+ ".jpg": "image/jpeg",
48
+ ".png": "image/png",
49
+ ".svg": "image/svg+xml",
50
+ ".webp": "image/webp"
51
+ };
52
+ return types[extname(path).toLowerCase()] || "application/octet-stream";
53
+ }
54
+
55
+ function imageSource(source, embed) {
56
+ if (!embed || /^(?:https?:\/\/|data:)/.test(source)) {
57
+ return source;
58
+ }
59
+ const expanded = source.startsWith("~/")
60
+ ? resolve(homedir(), source.slice(2))
61
+ : resolve(source);
62
+ if (!existsSync(expanded)) {
63
+ return source;
64
+ }
65
+ const encoded = readFileSync(expanded).toString("base64");
66
+ return "data:" + imageMime(expanded) + ";base64," + encoded;
67
+ }
68
+
69
+ export class Node {
70
+ constructor({
71
+ id,
72
+ label,
73
+ shape = "rectangle",
74
+ image = null,
75
+ height = 80,
76
+ position = "t",
77
+ constraint = true,
78
+ className = null,
79
+ href = null,
80
+ tooltip = null
81
+ }) {
82
+ this.id = id;
83
+ this.label = label;
84
+ this.shape = shape;
85
+ this.image = image;
86
+ this.height = height;
87
+ this.position = position;
88
+ this.constraint = constraint;
89
+ this.className = className;
90
+ this.href = href;
91
+ this.tooltip = tooltip;
92
+ }
93
+
94
+ render({ embedImages = true } = {}) {
95
+ const nodeId = identifier(this.id);
96
+ const label = quoted(this.label);
97
+ if (this.image) {
98
+ if (!Number.isFinite(this.height) || this.height <= 0) {
99
+ throw new DiagramError("image height must be greater than zero");
100
+ }
101
+ if (!["t", "b"].includes(this.position)) {
102
+ throw new DiagramError("image position must be 't' or 'b'");
103
+ }
104
+ const source = quoted(imageSource(this.image, embedImages));
105
+ const constraint = this.constraint ? "on" : "off";
106
+ return (
107
+ nodeId +
108
+ "@{ img: " +
109
+ source +
110
+ ", label: " +
111
+ label +
112
+ ', pos: "' +
113
+ this.position +
114
+ '", h: ' +
115
+ this.height +
116
+ ', constraint: "' +
117
+ constraint +
118
+ '" }'
119
+ );
120
+ }
121
+ const shapes = {
122
+ rectangle: ["[", "]"],
123
+ round: ["(", ")"],
124
+ stadium: ["([", "])"],
125
+ diamond: ["{", "}"],
126
+ circle: ["((", "))"],
127
+ database: ["[(", ")]"]
128
+ };
129
+ if (!(this.shape in shapes)) {
130
+ throw new DiagramError("unknown node shape: " + quoted(this.shape));
131
+ }
132
+ const [start, end] = shapes[this.shape];
133
+ return nodeId + start + label + end;
134
+ }
135
+ }
136
+
137
+ export class Edge {
138
+ constructor({ source, target, label = null, style = "-->" }) {
139
+ this.source = source;
140
+ this.target = target;
141
+ this.label = label;
142
+ this.style = style;
143
+ }
144
+
145
+ render() {
146
+ if (!EDGE_STYLES.has(this.style)) {
147
+ throw new DiagramError("unknown edge style: " + quoted(this.style));
148
+ }
149
+ const label = this.label ? "|" + this.label + "|" : "";
150
+ return (
151
+ identifier(this.source) +
152
+ " " +
153
+ this.style +
154
+ label +
155
+ " " +
156
+ identifier(this.target)
157
+ );
158
+ }
159
+ }
160
+
161
+ export class Diagram {
162
+ constructor({
163
+ direction = "LR",
164
+ title = null,
165
+ nodes = [],
166
+ edges = [],
167
+ classDefs = {}
168
+ } = {}) {
169
+ this.direction = direction;
170
+ this.title = title;
171
+ this.nodes = nodes;
172
+ this.edges = edges;
173
+ this.classDefs = classDefs;
174
+ }
175
+
176
+ render({ embedImages = true } = {}) {
177
+ const direction = String(this.direction).toUpperCase();
178
+ if (!DIRECTIONS.has(direction)) {
179
+ throw new DiagramError("unknown direction: " + quoted(direction));
180
+ }
181
+ const lines = [];
182
+ if (this.title) {
183
+ lines.push("---", "title: " + quoted(this.title), "---");
184
+ }
185
+ lines.push("flowchart " + direction);
186
+ const known = new Set();
187
+ for (const node of this.nodes) {
188
+ const nodeId = identifier(node.id);
189
+ if (known.has(nodeId)) {
190
+ throw new DiagramError("duplicate node id: " + quoted(node.id));
191
+ }
192
+ known.add(nodeId);
193
+ lines.push(" " + node.render({ embedImages }));
194
+ }
195
+ for (const edge of this.edges) {
196
+ if (!known.has(identifier(edge.source))) {
197
+ throw new DiagramError(
198
+ "edge references missing node: " + quoted(edge.source)
199
+ );
200
+ }
201
+ if (!known.has(identifier(edge.target))) {
202
+ throw new DiagramError(
203
+ "edge references missing node: " + quoted(edge.target)
204
+ );
205
+ }
206
+ lines.push(" " + edge.render());
207
+ }
208
+ for (const node of this.nodes) {
209
+ const nodeId = identifier(node.id);
210
+ if (node.className) {
211
+ lines.push(" class " + nodeId + " " + identifier(node.className));
212
+ }
213
+ if (node.href) {
214
+ const tooltip = node.tooltip ? " " + quoted(node.tooltip) : "";
215
+ lines.push(" click " + nodeId + " " + quoted(node.href) + tooltip);
216
+ }
217
+ }
218
+ for (const [name, styles] of Object.entries(this.classDefs)) {
219
+ lines.push(" classDef " + identifier(name) + " " + styles);
220
+ }
221
+ return lines.join("\n");
222
+ }
223
+ }
224
+
225
+ export function wrapMermaid(source) {
226
+ source = String(source).trim();
227
+ if (/^```mermaid(?:\s|$)/i.test(source)) {
228
+ return source;
229
+ }
230
+ if (!source) {
231
+ throw new DiagramError("diagram input is empty");
232
+ }
233
+ return "```mermaid\n" + source + "\n```";
234
+ }
235
+
236
+ function endpoint(value) {
237
+ value = String(value).trim();
238
+ if (!value) {
239
+ throw new DiagramError("empty node in arrow chain");
240
+ }
241
+ const separator = value.indexOf(":");
242
+ if (separator !== -1) {
243
+ const possibleId = value.slice(0, separator).trim();
244
+ const label = value.slice(separator + 1).trim();
245
+ if (possibleId && label) {
246
+ return [possibleId, label];
247
+ }
248
+ }
249
+ return [null, value];
250
+ }
251
+
252
+ export function fromText(
253
+ text,
254
+ {
255
+ direction = "LR",
256
+ title = null,
257
+ images = {},
258
+ imageHeight = 80
259
+ } = {}
260
+ ) {
261
+ const lines = String(text)
262
+ .split(/\r?\n/)
263
+ .map((line) => line.trim())
264
+ .filter(Boolean);
265
+ if (lines.length === 0) {
266
+ throw new DiagramError("diagram input is empty");
267
+ }
268
+ const diagram = new Diagram({ direction, title });
269
+ const used = new Set();
270
+ const byLabel = new Map();
271
+ const byExplicitId = new Map();
272
+
273
+ function getNode(raw) {
274
+ const [explicitId, label] = endpoint(raw);
275
+ const explicitKey = explicitId ? identifier(explicitId) : null;
276
+ const labelKey = label.toLowerCase();
277
+ if (explicitKey && byExplicitId.has(explicitKey)) {
278
+ return byExplicitId.get(explicitKey);
279
+ }
280
+ if (!explicitId && byLabel.has(labelKey)) {
281
+ return byLabel.get(labelKey);
282
+ }
283
+ const nodeId = uniqueIdentifier(explicitId || label, used);
284
+ diagram.nodes.push(new Node({ id: nodeId, label }));
285
+ byLabel.set(labelKey, nodeId);
286
+ if (explicitKey) {
287
+ byExplicitId.set(explicitKey, nodeId);
288
+ }
289
+ return nodeId;
290
+ }
291
+
292
+ for (const line of lines) {
293
+ const ids = line.split(ARROW_SPLIT).map(getNode);
294
+ for (let index = 0; index < ids.length - 1; index += 1) {
295
+ diagram.edges.push(
296
+ new Edge({ source: ids[index], target: ids[index + 1] })
297
+ );
298
+ }
299
+ }
300
+
301
+ for (const [key, source] of Object.entries(images || {})) {
302
+ const normalized = identifier(key);
303
+ let node = diagram.nodes.find(
304
+ (item) =>
305
+ identifier(item.id) === normalized ||
306
+ String(item.label).toLowerCase() === key.toLowerCase()
307
+ );
308
+ if (!node) {
309
+ node = new Node({ id: uniqueIdentifier(key, used), label: key });
310
+ diagram.nodes.push(node);
311
+ }
312
+ node.image = String(source);
313
+ node.height = Number(imageHeight);
314
+ }
315
+ return diagram;
316
+ }
317
+
318
+ function isMapping(value) {
319
+ return value !== null && typeof value === "object" && !Array.isArray(value);
320
+ }
321
+
322
+ export function fromObject(value) {
323
+ if (!isMapping(value)) {
324
+ throw new DiagramError("JSON input must be an object");
325
+ }
326
+ const raw = value.mermaid ?? value.code;
327
+ if (raw !== undefined && raw !== null) {
328
+ return String(raw).trim();
329
+ }
330
+ if (!["flowchart", "graph"].includes(String(value.type ?? "flowchart"))) {
331
+ throw new DiagramError("structured JSON supports type 'flowchart' only");
332
+ }
333
+ const rawNodes = value.nodes ?? [];
334
+ const rawEdges = value.edges ?? [];
335
+ const rawDefs = value.classDefs ?? {};
336
+ if (!Array.isArray(rawNodes) || !rawNodes.every(isMapping)) {
337
+ throw new DiagramError("'nodes' must be an array of objects");
338
+ }
339
+ if (!Array.isArray(rawEdges) || !rawEdges.every(isMapping)) {
340
+ throw new DiagramError("'edges' must be an array of objects");
341
+ }
342
+ if (!isMapping(rawDefs)) {
343
+ throw new DiagramError("'classDefs' must be an object");
344
+ }
345
+
346
+ const nodes = rawNodes.map((item) => {
347
+ if (!Object.hasOwn(item, "id")) {
348
+ throw new DiagramError("every node needs an 'id'");
349
+ }
350
+ const id = String(item.id);
351
+ return new Node({
352
+ id,
353
+ label: String(item.label ?? id),
354
+ shape: String(item.shape ?? "rectangle"),
355
+ image: item.image == null ? null : String(item.image),
356
+ height: Number(item.height ?? 80),
357
+ position: String(item.position ?? "t"),
358
+ constraint: Boolean(item.constraint ?? true),
359
+ className: item.class == null ? null : String(item.class),
360
+ href: item.href == null ? null : String(item.href),
361
+ tooltip: item.tooltip == null ? null : String(item.tooltip)
362
+ });
363
+ });
364
+
365
+ const edges = rawEdges.map((item) => {
366
+ if (!Object.hasOwn(item, "from") || !Object.hasOwn(item, "to")) {
367
+ throw new DiagramError("every edge needs 'from' and 'to'");
368
+ }
369
+ return new Edge({
370
+ source: String(item.from),
371
+ target: String(item.to),
372
+ label: item.label == null ? null : String(item.label),
373
+ style: String(item.style ?? "-->")
374
+ });
375
+ });
376
+
377
+ return new Diagram({
378
+ direction: String(value.direction ?? "LR"),
379
+ title: value.title == null ? null : String(value.title),
380
+ nodes,
381
+ edges,
382
+ classDefs: Object.fromEntries(
383
+ Object.entries(rawDefs).map(([key, item]) => [String(key), String(item)])
384
+ )
385
+ });
386
+ }
387
+
388
+ export const fromDict = fromObject;
389
+
390
+ export function generate(
391
+ value,
392
+ {
393
+ fence = true,
394
+ direction = "LR",
395
+ title = null,
396
+ images = {},
397
+ imageHeight = 80,
398
+ embedImages = true,
399
+ forceJson = false
400
+ } = {}
401
+ ) {
402
+ let source;
403
+ if (isMapping(value)) {
404
+ const parsed = fromObject(value);
405
+ source =
406
+ typeof parsed === "string"
407
+ ? parsed
408
+ : parsed.render({ embedImages });
409
+ } else {
410
+ const raw = String(value).trim();
411
+ if (!raw) {
412
+ throw new DiagramError("diagram input is empty");
413
+ }
414
+ let parsedJson = null;
415
+ if (forceJson || raw.startsWith("{")) {
416
+ try {
417
+ parsedJson = JSON.parse(raw);
418
+ } catch (error) {
419
+ if (forceJson) {
420
+ throw new DiagramError("invalid JSON: " + error.message);
421
+ }
422
+ }
423
+ if (parsedJson !== null && !isMapping(parsedJson)) {
424
+ throw new DiagramError("JSON input must be an object");
425
+ }
426
+ }
427
+ if (isMapping(parsedJson)) {
428
+ const parsed = fromObject(parsedJson);
429
+ source =
430
+ typeof parsed === "string"
431
+ ? parsed
432
+ : parsed.render({ embedImages });
433
+ } else if (/^```mermaid(?:\s|$)/i.test(raw)) {
434
+ if (fence) {
435
+ return raw;
436
+ }
437
+ return raw
438
+ .replace(/^```mermaid\s*\n?/i, "")
439
+ .replace(/\n?```$/, "")
440
+ .trim();
441
+ } else if (MERMAID_START.test(raw)) {
442
+ if (Object.keys(images || {}).length > 0) {
443
+ throw new DiagramError(
444
+ "--image works with plain-text flowcharts, not raw Mermaid"
445
+ );
446
+ }
447
+ source = raw;
448
+ } else {
449
+ source = fromText(raw, {
450
+ direction,
451
+ title,
452
+ images,
453
+ imageHeight
454
+ }).render({ embedImages });
455
+ }
456
+ }
457
+ return fence ? wrapMermaid(source) : source;
458
+ }