@ateeducacion/mind-elixir-export-mermaid 0.1.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 Área de Tecnología Educativa (Gobierno de Canarias) and 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 ADDED
@@ -0,0 +1,120 @@
1
+ # Mind Elixir export to Mermaid
2
+
3
+ [![CI](https://github.com/ateeducacion/mind-elixir-export-mermaid/actions/workflows/ci.yml/badge.svg)](https://github.com/ateeducacion/mind-elixir-export-mermaid/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/ateeducacion/mind-elixir-export-mermaid/branch/main/graph/badge.svg)](https://codecov.io/gh/ateeducacion/mind-elixir-export-mermaid)
5
+ [![npm](https://img.shields.io/npm/v/@ateeducacion/mind-elixir-export-mermaid)](https://www.npmjs.com/package/@ateeducacion/mind-elixir-export-mermaid)
6
+ [![Publish](https://github.com/ateeducacion/mind-elixir-export-mermaid/actions/workflows/publish.yml/badge.svg)](https://github.com/ateeducacion/mind-elixir-export-mermaid/actions/workflows/publish.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ Converts [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) mind maps to
10
+ [Mermaid `mindmap`](https://mermaid.js.org/syntax/mindmap.html) source.
11
+
12
+ Mermaid text is tiny, and it can be rendered to a sharp, lightweight SVG by
13
+ [Mermaid](https://mermaid.js.org) or by [Pele](https://github.com/obsidianmd/pele), which
14
+ needs no DOM and also runs on a server. GitHub, GitLab, Obsidian, Notion and many
15
+ documentation tools display it as well. For a 16-node map, the SVG that Pele renders from
16
+ this output is 8 KB (1.7 KB gzip), against 84 KB for a PNG screenshot of the same map.
17
+
18
+ No runtime dependencies, no DOM, and it works in browsers, Node.js, Bun and Deno.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ npm install @ateeducacion/mind-elixir-export-mermaid
24
+ ```
25
+
26
+ ## Use
27
+
28
+ ```ts
29
+ import MindElixir from "mind-elixir";
30
+ import { mindElixirToMermaid } from "@ateeducacion/mind-elixir-export-mermaid";
31
+
32
+ const mind = new MindElixir({ el: "#map" });
33
+ mind.init(MindElixir.new("Science"));
34
+
35
+ const source = mindElixirToMermaid(mind.getData());
36
+ // mindmap
37
+ // n0(("Science"))
38
+ ```
39
+
40
+ It takes the data returned by `mind.getData()`, or a single node and its descendants.
41
+
42
+ Render it with Pele:
43
+
44
+ ```ts
45
+ import { render } from "pele";
46
+
47
+ const { svg } = render(source);
48
+ ```
49
+
50
+ or with Mermaid:
51
+
52
+ ```ts
53
+ import mermaid from "mermaid";
54
+
55
+ const { svg } = await mermaid.render("mindmap", source);
56
+ ```
57
+
58
+ ## Options
59
+
60
+ ```ts
61
+ mindElixirToMermaid(data, {
62
+ rootShape: "circle",
63
+ nodeShape: "rounded",
64
+ shape: (node, depth) => (depth === 1 ? "cloud" : undefined),
65
+ className: (node) => (node.branchColor === "#c0392b" ? "red" : undefined),
66
+ icon: (node) => (node.tags?.includes("book") ? "fa fa-book" : undefined),
67
+ markdown: true,
68
+ omitCollapsed: false,
69
+ indent: 2,
70
+ });
71
+ ```
72
+
73
+ | Option | Default | Description |
74
+ | ------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `rootShape` | `"circle"` | Shape of the root: `square`, `rounded`, `circle`, `bang`, `cloud` or `hexagon`. |
76
+ | `nodeShape` | `"rounded"` | Shape of the other nodes. |
77
+ | `shape(node, depth)` | | Shape of one node; return `undefined` to use the defaults. |
78
+ | `className(node, depth)` | | CSS classes written as `:::name` (letters, digits, `-` and `_`). |
79
+ | `icon(node, depth)` | | Icon classes written as `::icon(...)`, for example `"fa fa-book"`. |
80
+ | `markdown` | `false` | Write [markdown strings](https://mermaid.js.org/syntax/mindmap.html#markdown-strings), keeping bold (`fontWeight: "bold"`), italic (`fontStyle: "italic"`) and line breaks. Otherwise line breaks become spaces. |
81
+ | `omitCollapsed` | `false` | Leave out the children of folded nodes (`expanded: false`). |
82
+ | `indent` | `2` | Spaces per level, from 1 to 8. |
83
+
84
+ `escapeLabel(text, markdown?)` is exported too, for building labels by hand.
85
+
86
+ ## What is exported
87
+
88
+ The hierarchy, the order and the text of every node. Text is written with Mermaid's
89
+ entity codes (`#quot;`, `#lt;`, `#35;`, …), so a topic can contain quotes, brackets,
90
+ `#`, `*`, `_` or HTML-like text and still renders exactly as typed, never as markup.
91
+
92
+ Mermaid's `mindmap` syntax has no equivalent for per-node colours and font styles (other
93
+ than bold and italic in markdown mode), branch sides, folded state, arrows, summaries,
94
+ links, images or tags, so those are not exported. Keep the Mind Elixir data as the source
95
+ of your map and use this output as a view of it. The `className` option can carry some of
96
+ that information as CSS classes if your page styles them.
97
+
98
+ Invalid input throws a `TypeError`, and an invalid option, class name or icon throws a
99
+ `RangeError`, rather than writing source that Mermaid cannot parse.
100
+
101
+ ## Background
102
+
103
+ This package was proposed in
104
+ [mind-elixir-core#386](https://github.com/SSShooter/mind-elixir-core/issues/386). Thanks
105
+ to [@SSShooter](https://github.com/SSShooter) for Mind Elixir and for the suggestion to
106
+ make it a separate package. It is used by [eXeLearning](https://github.com/exelearning/exelearning),
107
+ an open-source authoring tool for educational content, whose published content needs a
108
+ static image of each mind map.
109
+
110
+ ## Develop
111
+
112
+ ```sh
113
+ npm ci
114
+ npm run check # format, typecheck, tests with 100% coverage, build
115
+ npm run test:package # pack, install the tarball in an empty project and import it
116
+ ```
117
+
118
+ ## License
119
+
120
+ [MIT](LICENSE)
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Converts Mind Elixir mind maps to Mermaid `mindmap` source.
3
+ *
4
+ * The output can be rendered to SVG by Mermaid (https://mermaid.js.org) or by Pele
5
+ * (https://github.com/obsidianmd/pele), and is understood by GitHub, GitLab, Obsidian and
6
+ * other tools. It keeps the hierarchy, the order and the text of the map. Mermaid mindmaps
7
+ * have no per-node colours, branch sides, arrows or summaries, so those are not exported.
8
+ */
9
+ /** The fields of a Mind Elixir node this module reads (compatible with `NodeObj`). */
10
+ export interface MindElixirNode {
11
+ topic: string;
12
+ children?: readonly MindElixirNode[] | undefined;
13
+ expanded?: boolean | undefined;
14
+ style?: {
15
+ fontWeight?: string | undefined;
16
+ fontStyle?: string | undefined;
17
+ } | undefined;
18
+ }
19
+ /** The fields of Mind Elixir data this module reads (compatible with `MindElixirData`). */
20
+ export interface MindElixirData {
21
+ nodeData: MindElixirNode;
22
+ }
23
+ /** Mermaid mindmap node shapes. */
24
+ export type MermaidShape = "square" | "rounded" | "circle" | "bang" | "cloud" | "hexagon";
25
+ /**
26
+ * Options. `N` is the node type of your data (Mind Elixir's `NodeObj`, for example), which
27
+ * the callbacks receive.
28
+ */
29
+ export interface MermaidExportOptions<N extends MindElixirNode = MindElixirNode> {
30
+ /** Shape of the root node. Default: `"circle"`. */
31
+ rootShape?: MermaidShape;
32
+ /** Shape of the other nodes. Default: `"rounded"`. */
33
+ nodeShape?: MermaidShape;
34
+ /** Shape of one node, overriding the two options above when it returns a shape. */
35
+ shape?: (node: N, depth: number) => MermaidShape | undefined;
36
+ /** CSS classes for a node, written as `:::name`. Names may contain letters, digits, `-` and `_`. */
37
+ className?: (node: N, depth: number) => string | readonly string[] | undefined;
38
+ /** Icon classes for a node, written as `::icon(...)`, for example `"fa fa-book"`. */
39
+ icon?: (node: N, depth: number) => string | undefined;
40
+ /**
41
+ * Write labels as Mermaid markdown strings, which keep bold (`fontWeight: "bold"`),
42
+ * italic (`fontStyle: "italic"`) and line breaks. Default: `false` (plain labels, line
43
+ * breaks become spaces).
44
+ */
45
+ markdown?: boolean;
46
+ /** Leave out the children of folded nodes (`expanded: false`). Default: `false`. */
47
+ omitCollapsed?: boolean;
48
+ /** Spaces per level. Default: `2`. */
49
+ indent?: number;
50
+ }
51
+ /** Escapes a topic for a quoted Mermaid label. */
52
+ export declare function escapeLabel(text: string, markdown?: boolean): string;
53
+ /**
54
+ * Converts Mind Elixir data (as returned by `mind.getData()`) or a single node and its
55
+ * descendants to Mermaid `mindmap` source.
56
+ *
57
+ * @throws {TypeError} when the input is not a Mind Elixir node or data.
58
+ * @throws {RangeError} when an option or a class name/icon returned by a callback is invalid.
59
+ */
60
+ export declare function mindElixirToMermaid<N extends MindElixirNode>(data: {
61
+ nodeData: N;
62
+ } | N, options?: MermaidExportOptions<N>): string;
63
+ export default mindElixirToMermaid;
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Converts Mind Elixir mind maps to Mermaid `mindmap` source.
3
+ *
4
+ * The output can be rendered to SVG by Mermaid (https://mermaid.js.org) or by Pele
5
+ * (https://github.com/obsidianmd/pele), and is understood by GitHub, GitLab, Obsidian and
6
+ * other tools. It keeps the hierarchy, the order and the text of the map. Mermaid mindmaps
7
+ * have no per-node colours, branch sides, arrows or summaries, so those are not exported.
8
+ */
9
+ const SHAPES = {
10
+ square: ["[", "]"],
11
+ rounded: ["(", ")"],
12
+ circle: ["((", "))"],
13
+ bang: [")", "("],
14
+ cloud: ["))", "(("],
15
+ hexagon: ["{{", "}}"],
16
+ };
17
+ const CLASS_NAME = /^[A-Za-z_][\w-]*$/;
18
+ const ICON = /^[\w -]+$/;
19
+ /**
20
+ * Mermaid entity codes for the characters Mermaid would otherwise read as syntax, markup,
21
+ * entities or formatting (Pele formats `*` and `_` even outside markdown strings). `#` comes
22
+ * first, so the codes added afterwards are not encoded again.
23
+ */
24
+ const ENTITIES = [
25
+ [/#/g, "#35;"],
26
+ [/"/g, "#quot;"],
27
+ [/</g, "#lt;"],
28
+ [/>/g, "#gt;"],
29
+ [/'/g, "#39;"],
30
+ [/`/g, "#96;"],
31
+ [/\*/g, "#42;"],
32
+ [/_/g, "#95;"],
33
+ ];
34
+ /** Escapes a topic for a quoted Mermaid label. */
35
+ export function escapeLabel(text, markdown = false) {
36
+ let label = String(text).replace(/\r\n?/g, "\n").replace(/\t/g, " ");
37
+ for (const [pattern, code] of ENTITIES)
38
+ label = label.replace(pattern, code);
39
+ if (markdown) {
40
+ label = label
41
+ .split("\n")
42
+ .map((line) => line.trim())
43
+ .join("\n")
44
+ .trim();
45
+ }
46
+ else {
47
+ label = label.replace(/\s*\n\s*/g, " ").trim();
48
+ }
49
+ // An empty quoted label is a syntax error
50
+ return label || " ";
51
+ }
52
+ function label(node, markdown) {
53
+ const text = escapeLabel(node.topic, markdown);
54
+ if (!markdown)
55
+ return `"${text}"`;
56
+ let formatted = text;
57
+ if (text.trim()) {
58
+ if (node.style?.fontStyle === "italic")
59
+ formatted = `*${formatted}*`;
60
+ if (node.style?.fontWeight === "bold")
61
+ formatted = `**${formatted}**`;
62
+ }
63
+ return `"\`${formatted}\`"`;
64
+ }
65
+ function classes(value) {
66
+ if (value === undefined)
67
+ return [];
68
+ const names = (typeof value === "string" ? value.split(/\s+/) : [...value]).filter(Boolean);
69
+ for (const name of names) {
70
+ if (!CLASS_NAME.test(name))
71
+ throw new RangeError(`Invalid Mermaid class name: ${JSON.stringify(name)}`);
72
+ }
73
+ return names;
74
+ }
75
+ function isNode(value) {
76
+ return (typeof value === "object" &&
77
+ value !== null &&
78
+ typeof value.topic === "string");
79
+ }
80
+ /**
81
+ * Converts Mind Elixir data (as returned by `mind.getData()`) or a single node and its
82
+ * descendants to Mermaid `mindmap` source.
83
+ *
84
+ * @throws {TypeError} when the input is not a Mind Elixir node or data.
85
+ * @throws {RangeError} when an option or a class name/icon returned by a callback is invalid.
86
+ */
87
+ export function mindElixirToMermaid(data, options = {}) {
88
+ const root = isNode(data)
89
+ ? data
90
+ : data?.nodeData;
91
+ if (!isNode(root))
92
+ throw new TypeError("Expected Mind Elixir data or a node with a string topic");
93
+ const indent = options.indent ?? 2;
94
+ if (!Number.isInteger(indent) || indent < 1 || indent > 8) {
95
+ throw new RangeError("indent must be an integer from 1 to 8");
96
+ }
97
+ const markdown = options.markdown === true;
98
+ const rootShape = options.rootShape ?? "circle";
99
+ const nodeShape = options.nodeShape ?? "rounded";
100
+ for (const shape of [rootShape, nodeShape]) {
101
+ if (!(shape in SHAPES))
102
+ throw new RangeError(`Unknown Mermaid shape: ${JSON.stringify(shape)}`);
103
+ }
104
+ const lines = ["mindmap"];
105
+ let count = 0;
106
+ // Iterative, so a very deep map cannot overflow the call stack
107
+ const stack = [
108
+ { node: root, depth: 0 },
109
+ ];
110
+ while (stack.length > 0) {
111
+ const { node: current, depth } = stack.pop();
112
+ // Every node of the map has the caller's node type
113
+ const node = current;
114
+ if (!isNode(node))
115
+ throw new TypeError("Every node must have a string topic");
116
+ const shape = options.shape?.(node, depth) ?? (depth === 0 ? rootShape : nodeShape);
117
+ const brackets = SHAPES[shape];
118
+ if (!brackets)
119
+ throw new RangeError(`Unknown Mermaid shape: ${JSON.stringify(shape)}`);
120
+ const pad = " ".repeat(indent * (depth + 1));
121
+ lines.push(`${pad}n${count++}${brackets[0]}${label(node, markdown)}${brackets[1]}`);
122
+ const icon = options.icon?.(node, depth);
123
+ if (icon !== undefined) {
124
+ if (!ICON.test(icon))
125
+ throw new RangeError(`Invalid Mermaid icon: ${JSON.stringify(icon)}`);
126
+ lines.push(`${pad}::icon(${icon})`);
127
+ }
128
+ const names = classes(options.className?.(node, depth));
129
+ if (names.length > 0)
130
+ lines.push(`${pad}:::${names.join(" ")}`);
131
+ if (options.omitCollapsed && node.expanded === false)
132
+ continue;
133
+ const children = node.children ?? [];
134
+ for (let i = children.length - 1; i >= 0; i--)
135
+ stack.push({ node: children[i], depth: depth + 1 });
136
+ }
137
+ return lines.join("\n");
138
+ }
139
+ export default mindElixirToMermaid;
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@ateeducacion/mind-elixir-export-mermaid",
3
+ "version": "0.1.0",
4
+ "description": "Export Mind Elixir mind maps to Mermaid mindmap syntax, for lightweight vector output with Mermaid or Pele",
5
+ "license": "MIT",
6
+ "author": "Ernesto Serrano",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ateeducacion/mind-elixir-export-mermaid.git"
10
+ },
11
+ "homepage": "https://github.com/ateeducacion/mind-elixir-export-mermaid#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ateeducacion/mind-elixir-export-mermaid/issues"
14
+ },
15
+ "keywords": [
16
+ "mind-elixir",
17
+ "mermaid",
18
+ "mindmap",
19
+ "mind-map",
20
+ "pele",
21
+ "svg",
22
+ "export"
23
+ ],
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "sideEffects": false,
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.build.json",
47
+ "typecheck": "tsc --noEmit",
48
+ "format": "prettier --write .",
49
+ "format:check": "prettier --check .",
50
+ "test": "vitest run --coverage",
51
+ "test:watch": "vitest",
52
+ "check": "npm run format:check && npm run typecheck && npm test && npm run build",
53
+ "prepack": "npm run build",
54
+ "test:package": "node scripts/test-package.mjs"
55
+ },
56
+ "devDependencies": {
57
+ "@types/node": "26.6.4",
58
+ "@vitest/coverage-v8": "5.0.3",
59
+ "mind-elixir": "5.15.1",
60
+ "pele": "1.6.0",
61
+ "prettier": "3.9.9",
62
+ "typescript": "7.0.2",
63
+ "vitest": "5.0.3"
64
+ }
65
+ }