├── .npmignore ├── bun.lockb ├── jsconfig.json ├── src ├── index.js ├── createNodeTree.js ├── validation.js └── convertToMermaid.js ├── package.json ├── LICENSE ├── ExampleUsage.js ├── .gitignore ├── tests ├── buildJsonCanvasHierarchy.test.js └── generateMermaidFlowchart.test.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tests/ 3 | .gitignore 4 | bun.lockb 5 | ExampleUsage.js 6 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexwiench/JSON-Canvas-To-Mermaid/HEAD/bun.lockb -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "composite": true, 11 | "strict": true, 12 | "downlevelIteration": true, 13 | "skipLibCheck": true, 14 | "jsx": "react-jsx", 15 | "allowSyntheticDefaultImports": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "allowJs": true, 18 | "types": [ 19 | "bun-types" // add Bun global 20 | ] 21 | // "include": ["src/**/*"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as validation from './validation.js'; 2 | import { createNodeTree } from './createNodeTree.js'; 3 | import { convertToMermaid } from './convertToMermaid.js'; 4 | 5 | /** 6 | * JSON Canvas utility library for working with JSON Canvas data and generating Mermaid flowcharts. 7 | * @namespace 8 | */ 9 | export const jsonCanvas = { 10 | /** 11 | * Validation functions for JSON Canvas data and related parameters. 12 | * @namespace 13 | */ 14 | validate: { 15 | data: validation.validateJsonCanvasData, 16 | colors: validation.validateCustomColors, 17 | direction: validation.validateGraphDirection, 18 | }, 19 | 20 | createNodeTree, 21 | convertToMermaid, 22 | }; 23 | 24 | export default convertToMermaid; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-canvas-to-mermaid", 3 | "version": "2.0.1", 4 | "description": "A module to generate Mermaid Graph syntax from JSON Canvas data", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "bun test" 8 | }, 9 | "keywords": [ 10 | "mermaid", 11 | "flowchart", 12 | "json canvas", 13 | "obsidian", 14 | "mermaidjs" 15 | ], 16 | "author": "Alex Wiench", 17 | "license": "MIT", 18 | "repository": "https://github.com/alexwiench/JSON-Canvas-To-Mermaid", 19 | "bugs": "https://github.com/alexwiench/JSON-Canvas-To-Mermaid/issues", 20 | "homepage": "https://alexwiench.github.io/json-canvas-to-mermaid-demo/", 21 | "module": "src/index.js", 22 | "type": "module", 23 | "devDependencies": { 24 | "bun-types": "latest" 25 | }, 26 | "peerDependencies": { 27 | "typescript": "^5.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [Alex Wiench] 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. 22 | -------------------------------------------------------------------------------- /ExampleUsage.js: -------------------------------------------------------------------------------- 1 | import convertToMermaid from './src/convertToMermaid.js'; 2 | import { createNodeTree } from './src/createNodeTree.js'; 3 | 4 | let data = { 5 | nodes: [ 6 | { 7 | id: '6f002d2b0257ffa4', 8 | x: -1601, 9 | y: -826, 10 | width: 631, 11 | height: 100, 12 | color: '#248a42', 13 | type: 'group', 14 | label: 'Group One', 15 | }, 16 | { 17 | id: '5696e6f4d7feef3b', 18 | x: -1581, 19 | y: -806, 20 | width: 250, 21 | height: 60, 22 | color: '4', 23 | type: 'text', 24 | text: 'Node One', 25 | }, 26 | { 27 | id: 'eb886f14ff2b15a1', 28 | x: -1240, 29 | y: -806, 30 | width: 250, 31 | height: 60, 32 | color: '6', 33 | type: 'text', 34 | text: 'Node Two', 35 | }, 36 | ], 37 | edges: [ 38 | { 39 | id: 'ed452a5525485f24', 40 | fromNode: '5696e6f4d7feef3b', 41 | fromSide: 'right', 42 | toNode: 'eb886f14ff2b15a1', 43 | toSide: 'left', 44 | color: '2', 45 | }, 46 | ], 47 | }; 48 | 49 | // Output json data with `children` property added 50 | console.log(createNodeTree(data)); 51 | 52 | // OPTIONAL - Overwrite any or all of the 6 default colors 53 | const customColors = { 54 | 2: '#ff0000', 55 | 4: '#00ff00', 56 | }; 57 | 58 | // OPTIONAL - Change Mermaid graph direction 59 | const graphDirection = 'LR'; 60 | 61 | // Output mermaid flowchart 62 | console.log(convertToMermaid(data, customColors, graphDirection)); 63 | -------------------------------------------------------------------------------- /src/createNodeTree.js: -------------------------------------------------------------------------------- 1 | import { validateJsonCanvasData } from './validation.js'; 2 | 3 | // ========== CREATE NODE HIERARCHY ========== 4 | /** 5 | * Builds a hierarchical structure from JSON Canvas data by assigning children to group nodes. 6 | * 7 | * @param {Object} data - The JSON Canvas data object. 8 | * @param {Array} data.nodes - An array of nodes from the JSON Canvas data. 9 | * @param {Array} data.edges - An array of edges from the JSON Canvas data. 10 | * @returns {Object} The hierarchical structure object. 11 | * @returns {Array} output.nodes - An array of nodes with the `children` property added. 12 | * @returns {Array} output.edges - An array of edges (remains unchanged from the input). 13 | * 14 | * @description 15 | * This function creates a parent-child hierarchy for nodes based on their spatial relationships: 16 | * - Group nodes can contain other nodes (including other groups). 17 | * - A node is considered a child of a group if its center point is within the group's bounds. 18 | * - Each node can have only one parent group. 19 | * - Non-group nodes have their 'children' property set to null. 20 | * - The function preserves the original edge data. 21 | */ 22 | 23 | export function createNodeTree(data) { 24 | validateJsonCanvasData(data); 25 | 26 | function isPointInsideGroup(point, group) { 27 | const { x, y, width, height } = group; 28 | return point.x >= x && point.x <= x + width && point.y >= y && point.y <= y + height; 29 | } 30 | 31 | function findMidpoint(node) { 32 | return { 33 | x: node.x + node.width / 2, 34 | y: node.y + node.height / 2, 35 | }; 36 | } 37 | 38 | function sortGroupsByArea(nodes) { 39 | return nodes.slice().sort((a, b) => { 40 | if (a.type !== 'group' || b.type !== 'group') return 0; 41 | const areaA = a.width * a.height; 42 | const areaB = b.width * b.height; 43 | return areaA - areaB; 44 | }); 45 | } 46 | 47 | const sortedNodes = sortGroupsByArea(data.nodes); 48 | 49 | const output = { 50 | nodes: [], 51 | edges: [...data.edges], 52 | }; 53 | 54 | sortedNodes.forEach((node) => { 55 | output.nodes.push({ 56 | ...node, 57 | children: node.type === 'group' ? [] : null, 58 | }); 59 | }); 60 | 61 | const nodeMap = output.nodes.reduce((acc, node) => { 62 | acc[node.id] = node; 63 | return acc; 64 | }, {}); 65 | 66 | output.nodes.forEach((node, index) => { 67 | if (node.type === 'group') { 68 | const midpoint = findMidpoint(node); 69 | for (let i = index + 1; i < sortedNodes.length; i++) { 70 | const potentialParent = sortedNodes[i]; 71 | if (potentialParent.type !== 'group') continue; 72 | if (isPointInsideGroup(midpoint, potentialParent)) { 73 | nodeMap[potentialParent.id].children.push(node.id); 74 | break; 75 | } 76 | } 77 | } else { 78 | const nodeCenter = findMidpoint(node); 79 | for (let i = 0; i < sortedNodes.length; i++) { 80 | const potentialParent = sortedNodes[i]; 81 | if (potentialParent.type !== 'group') continue; 82 | if (isPointInsideGroup(nodeCenter, potentialParent)) { 83 | nodeMap[potentialParent.id].children.push(node.id); 84 | break; 85 | } 86 | } 87 | } 88 | }); 89 | 90 | return output; 91 | } 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | # A place to run random tests 178 | tests/test.js 179 | -------------------------------------------------------------------------------- /tests/buildJsonCanvasHierarchy.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from 'bun:test'; 2 | import { jsonCanvas } from '../src/index.js'; 3 | 4 | describe('buildJsonCanvasHierarchy', () => { 5 | describe('Group Hierarchy', () => { 6 | test('should create correct hierarchy for nested group structure', () => { 7 | const input = { 8 | nodes: [ 9 | { 10 | id: 'Group 1', 11 | x: -300, 12 | y: -380, 13 | width: 620, 14 | height: 320, 15 | type: 'group', 16 | label: 'Group 1', 17 | }, 18 | { 19 | id: 'Group 2', 20 | x: -260, 21 | y: -240, 22 | width: 540, 23 | height: 140, 24 | type: 'group', 25 | label: 'Group 2', 26 | }, 27 | { id: 'Node 1', x: -260, y: -340, width: 250, height: 60, type: 'text', text: 'Node 1' }, 28 | { id: 'Node 2', x: -220, y: -200, width: 250, height: 60, type: 'text', text: 'Node 2' }, 29 | { id: 'Node 3', x: -300, y: -20, width: 250, height: 60, type: 'text', text: 'Node 3' }, 30 | ], 31 | edges: [], 32 | }; 33 | 34 | const result = jsonCanvas.createNodeTree(input); 35 | 36 | expect(result.nodes).toHaveLength(5); 37 | expect(result.nodes.find((n) => n.id === 'Group 1').children).toEqual(['Group 2', 'Node 1']); 38 | expect(result.nodes.find((n) => n.id === 'Group 2').children).toEqual(['Node 2']); 39 | expect(result.nodes.find((n) => n.id === 'Node 1').children).toBeNull(); 40 | expect(result.nodes.find((n) => n.id === 'Node 2').children).toBeNull(); 41 | expect(result.nodes.find((n) => n.id === 'Node 3').children).toBeNull(); 42 | }); 43 | 44 | test('should handle overlapping groups correctly', () => { 45 | const input = { 46 | nodes: [ 47 | { id: 'Group 1', x: 0, y: 0, width: 200, height: 200, type: 'group', label: 'Group 1' }, 48 | { 49 | id: 'Group 2', 50 | x: 100, 51 | y: 100, 52 | width: 200, 53 | height: 200, 54 | type: 'group', 55 | label: 'Group 2', 56 | }, 57 | { id: 'Node 1', x: 150, y: 150, width: 50, height: 50, type: 'text', text: 'Node 1' }, 58 | ], 59 | edges: [], 60 | }; 61 | 62 | const result = jsonCanvas.createNodeTree(input); 63 | 64 | expect(result.nodes.find((n) => n.id === 'Group 1').children).toEqual(['Node 1']); 65 | expect(result.nodes.find((n) => n.id === 'Group 2').children).toEqual(['Group 1']); 66 | }); 67 | }); 68 | 69 | describe('Node Placement', () => { 70 | test('should correctly place nodes in groups', () => { 71 | const input = { 72 | nodes: [ 73 | { id: 'Group', x: 0, y: 0, width: 300, height: 300, type: 'group', label: 'Group' }, 74 | { id: 'Inside', x: 50, y: 50, width: 50, height: 50, type: 'text', text: 'Inside' }, 75 | { id: 'Outside', x: 350, y: 350, width: 50, height: 50, type: 'text', text: 'Outside' }, 76 | ], 77 | edges: [], 78 | }; 79 | 80 | const result = jsonCanvas.createNodeTree(input); 81 | 82 | expect(result.nodes.find((n) => n.id === 'Group').children).toEqual(['Inside']); 83 | expect(result.nodes.find((n) => n.id === 'Outside').children).toBeNull(); 84 | }); 85 | }); 86 | 87 | describe('Edge Preservation', () => { 88 | test('should preserve all edge data', () => { 89 | const input = { 90 | nodes: [ 91 | { id: 'Group', x: -340, y: -320, width: 340, height: 140, type: 'group', label: 'Group' }, 92 | { id: 'Node1', x: -300, y: -280, width: 250, height: 60, type: 'text', text: 'Node 1' }, 93 | { id: 'Node2', x: 160, y: -280, width: 250, height: 60, type: 'text', text: 'Node 2' }, 94 | ], 95 | edges: [ 96 | { 97 | id: 'Edge1', 98 | fromNode: 'Group', 99 | fromSide: 'right', 100 | toNode: 'Node2', 101 | toSide: 'top', 102 | fromEnd: 'arrow', 103 | }, 104 | { id: 'Edge2', fromNode: 'Node1', fromSide: 'right', toNode: 'Node2', toSide: 'bottom' }, 105 | ], 106 | }; 107 | 108 | const result = jsonCanvas.createNodeTree(input); 109 | 110 | expect(result.edges).toEqual(input.edges); 111 | }); 112 | }); 113 | 114 | describe('Special Cases', () => { 115 | test('should handle empty input correctly', () => { 116 | const input = { nodes: [], edges: [] }; 117 | const result = jsonCanvas.createNodeTree(input); 118 | expect(result.nodes).toEqual([]); 119 | expect(result.edges).toEqual([]); 120 | }); 121 | 122 | test('should handle input with only non-group nodes', () => { 123 | const input = { 124 | nodes: [ 125 | { id: 'Node1', x: 0, y: 0, width: 100, height: 50, type: 'text', text: 'Node 1' }, 126 | { id: 'Node2', x: 200, y: 0, width: 100, height: 50, type: 'text', text: 'Node 2' }, 127 | ], 128 | edges: [], 129 | }; 130 | 131 | const result = jsonCanvas.createNodeTree(input); 132 | 133 | expect(result.nodes).toHaveLength(2); 134 | expect(result.nodes[0].children).toBeNull(); 135 | expect(result.nodes[1].children).toBeNull(); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates the structure and content of JSON Canvas data. 3 | * @param {Object} data - The JSON Canvas data to validate. 4 | * @throws {Error} If the data or structure is invalid. 5 | */ 6 | export function validateJsonCanvasData(data) { 7 | if (typeof data !== 'object' || data === null) { 8 | throw new Error('Invalid data: must be a non-null object'); 9 | } 10 | 11 | if (!Array.isArray(data.nodes)) { 12 | throw new Error('Invalid data: nodes must be an array'); 13 | } 14 | 15 | if (!Array.isArray(data.edges)) { 16 | throw new Error('Invalid data: edges must be an array'); 17 | } 18 | 19 | // Validate nodes 20 | const nodeIds = new Set(); 21 | data.nodes.forEach((node, index) => { 22 | if (typeof node !== 'object' || node === null) { 23 | throw new Error(`Invalid node at index ${index}: must be a non-null object`); 24 | } 25 | 26 | if (typeof node.id !== 'string' || node.id.trim() === '') { 27 | throw new Error(`Invalid node at index ${index}: id must be a non-empty string`); 28 | } 29 | 30 | if (nodeIds.has(node.id)) { 31 | throw new Error(`Duplicate node id: ${node.id}`); 32 | } 33 | nodeIds.add(node.id); 34 | 35 | if (!['text', 'file', 'link', 'group'].includes(node.type)) { 36 | throw new Error(`Invalid node type at index ${index}: ${node.type}`); 37 | } 38 | 39 | if ( 40 | typeof node.x !== 'number' || 41 | typeof node.y !== 'number' || 42 | typeof node.width !== 'number' || 43 | typeof node.height !== 'number' 44 | ) { 45 | throw new Error(`Invalid node dimensions at index ${index}`); 46 | } 47 | 48 | if (node.color && typeof node.color !== 'string') { 49 | throw new Error(`Invalid node color at index ${index}: must be a string`); 50 | } 51 | 52 | // Type-specific validations 53 | switch (node.type) { 54 | case 'text': 55 | if (typeof node.text !== 'string') { 56 | throw new Error(`Invalid text node at index ${index}: text must be a string`); 57 | } 58 | break; 59 | case 'file': 60 | if (typeof node.file !== 'string' || node.file.trim() === '') { 61 | throw new Error(`Invalid file node at index ${index}: file must be a non-empty string`); 62 | } 63 | if (node.subpath && typeof node.subpath !== 'string') { 64 | throw new Error(`Invalid file node at index ${index}: subpath must be a string`); 65 | } 66 | break; 67 | case 'link': 68 | if (typeof node.url !== 'string' || node.url.trim() === '') { 69 | throw new Error(`Invalid link node at index ${index}: url must be a non-empty string`); 70 | } 71 | break; 72 | case 'group': 73 | if (node.label && typeof node.label !== 'string') { 74 | throw new Error(`Invalid group node at index ${index}: label must be a string`); 75 | } 76 | break; 77 | } 78 | }); 79 | 80 | // Validate edges 81 | data.edges.forEach((edge, index) => { 82 | if (typeof edge !== 'object' || edge === null) { 83 | throw new Error(`Invalid edge at index ${index}: must be a non-null object`); 84 | } 85 | 86 | if (typeof edge.id !== 'string' || edge.id.trim() === '') { 87 | throw new Error(`Invalid edge at index ${index}: id must be a non-empty string`); 88 | } 89 | 90 | if (!nodeIds.has(edge.fromNode) || !nodeIds.has(edge.toNode)) { 91 | throw new Error(`Invalid edge at index ${index}: fromNode or toNode does not exist`); 92 | } 93 | 94 | if (edge.fromSide && !['top', 'right', 'bottom', 'left'].includes(edge.fromSide)) { 95 | throw new Error(`Invalid edge fromSide at index ${index}: ${edge.fromSide}`); 96 | } 97 | 98 | if (edge.toSide && !['top', 'right', 'bottom', 'left'].includes(edge.toSide)) { 99 | throw new Error(`Invalid edge toSide at index ${index}: ${edge.toSide}`); 100 | } 101 | 102 | if (edge.fromEnd && !['none', 'arrow'].includes(edge.fromEnd)) { 103 | throw new Error(`Invalid edge fromEnd at index ${index}: ${edge.fromEnd}`); 104 | } 105 | 106 | if (edge.toEnd && !['none', 'arrow'].includes(edge.toEnd)) { 107 | throw new Error(`Invalid edge toEnd at index ${index}: ${edge.toEnd}`); 108 | } 109 | 110 | if (edge.color && typeof edge.color !== 'string') { 111 | throw new Error(`Invalid edge color at index ${index}: must be a string`); 112 | } 113 | 114 | if (edge.label && typeof edge.label !== 'string') { 115 | throw new Error(`Invalid edge label at index ${index}: must be a string`); 116 | } 117 | }); 118 | } 119 | 120 | /** 121 | * Validates the custom colors object. 122 | * @param {Object} customColors - The custom colors object to validate. 123 | * @throws {Error} If the custom colors are invalid. 124 | */ 125 | export function validateCustomColors(customColors) { 126 | if (typeof customColors !== 'object' || customColors === null) { 127 | throw new Error('Invalid customColors: must be a non-null object'); 128 | } 129 | 130 | if (Object.keys(customColors).length > 6) { 131 | throw new Error('Invalid customColors: maximum of 6 colors allowed'); 132 | } 133 | 134 | for (const [key, value] of Object.entries(customColors)) { 135 | if (!/^[1-6]$/.test(key)) { 136 | throw new Error(`Invalid color key: ${key}. Must be a number from 1 to 6.`); 137 | } 138 | 139 | if (typeof value !== 'string' || !/^#[0-9A-Fa-f]{6}$/.test(value)) { 140 | throw new Error( 141 | `Invalid color value for key ${key}: ${value}. Must be a valid hex color code.` 142 | ); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * Validates the custom direction parameter. 149 | * @param {string} graphDirection - The graph direction to validate. 150 | * @throws {Error} If the graph direction is invalid. 151 | */ 152 | export function validateGraphDirection(graphDirection) { 153 | const validDirections = ['TB', 'LR', 'BT', 'RL']; 154 | if (!validDirections.includes(graphDirection)) { 155 | throw new Error( 156 | `Invalid graph direction ${graphDirection}. Only "TB", "LR", "BT", and "RL" are allowed.` 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /tests/generateMermaidFlowchart.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe } from 'bun:test'; 2 | import { jsonCanvas } from '../src/index.js'; 3 | 4 | describe('generateMermaidFlowchart', () => { 5 | describe('Basic Functionality', () => { 6 | test('should generate correct Mermaid syntax for simple 2-node graph', () => { 7 | const input = { 8 | nodes: [ 9 | { 10 | id: '6b9bdbf30d75d3e5', 11 | x: -348, 12 | y: -229, 13 | width: 250, 14 | height: 60, 15 | type: 'text', 16 | text: 'Node 1', 17 | }, 18 | { 19 | id: 'b955705e854ced5f', 20 | x: -20, 21 | y: -229, 22 | width: 250, 23 | height: 60, 24 | type: 'text', 25 | text: 'Node 2', 26 | }, 27 | ], 28 | edges: [ 29 | { 30 | id: 'cfcd19ac442c28b9', 31 | fromNode: '6b9bdbf30d75d3e5', 32 | fromSide: 'right', 33 | toNode: 'b955705e854ced5f', 34 | toSide: 'left', 35 | }, 36 | ], 37 | }; 38 | 39 | const result = jsonCanvas.convertToMermaid(input); 40 | 41 | expect(result).toContain('graph TB'); 42 | expect(result).toContain('6b9bdbf30d75d3e5["Node 1"]'); 43 | expect(result).toContain('b955705e854ced5f["Node 2"]'); 44 | expect(result).toContain('6b9bdbf30d75d3e5 --> b955705e854ced5f'); 45 | }); 46 | 47 | test('should change graph direction when specified', () => { 48 | const input = { 49 | nodes: [ 50 | { id: 'node1', x: 0, y: 0, width: 100, height: 50, type: 'text', text: 'Node 1' }, 51 | { id: 'node2', x: 100, y: 100, width: 100, height: 50, type: 'text', text: 'Node 2' }, 52 | ], 53 | edges: [{ id: 'edge', fromNode: 'node1', toNode: 'node2' }], 54 | }; 55 | 56 | const result = jsonCanvas.convertToMermaid(input, {}, 'LR'); 57 | 58 | expect(result).toContain('graph LR'); 59 | }); 60 | }); 61 | 62 | describe('Node Types', () => { 63 | test('should handle different node types: text, file, link', () => { 64 | const input = { 65 | nodes: [ 66 | { id: 'text1', x: 0, y: 0, width: 100, height: 50, type: 'text', text: 'Text Node' }, 67 | { id: 'file1', x: 100, y: 0, width: 100, height: 50, type: 'file', file: 'example.txt' }, 68 | { 69 | id: 'link1', 70 | x: 200, 71 | y: 0, 72 | width: 100, 73 | height: 50, 74 | type: 'link', 75 | url: 'https://example.com', 76 | }, 77 | ], 78 | edges: [], 79 | }; 80 | 81 | const result = jsonCanvas.convertToMermaid(input); 82 | 83 | expect(result).toContain('text1["Text Node"]'); 84 | expect(result).toContain('file1["example.txt"]'); 85 | expect(result).toContain('link1["https://example.com"]'); 86 | }); 87 | 88 | test('should handle group nodes with nested nodes', () => { 89 | const input = { 90 | nodes: [ 91 | { id: 'group1', type: 'group', label: 'Group 1', x: 0, y: 0, width: 300, height: 200 }, 92 | { id: 'text1', type: 'text', text: 'Nested Text', x: 50, y: 50, width: 100, height: 50 }, 93 | ], 94 | edges: [], 95 | }; 96 | 97 | const result = jsonCanvas.convertToMermaid(input); 98 | 99 | expect(result).toContain('subgraph group1["Group 1"]'); 100 | expect(result).toContain('text1["Nested Text"]'); 101 | expect(result).toContain('end'); 102 | }); 103 | 104 | test('should handle subpaths in file nodes', () => { 105 | const input = { 106 | nodes: [ 107 | { 108 | id: 'file1', 109 | type: 'file', 110 | file: 'example.txt', 111 | subpath: '/section1', 112 | x: 0, 113 | y: 0, 114 | width: 100, 115 | height: 50, 116 | }, 117 | ], 118 | edges: [], 119 | }; 120 | 121 | const result = jsonCanvas.convertToMermaid(input); 122 | 123 | expect(result).toContain('file1["example.txt/section1"]'); 124 | }); 125 | }); 126 | 127 | describe('Edge Handling', () => { 128 | test('should handle edges with different end types', () => { 129 | const input = { 130 | nodes: [ 131 | { id: 'node1', type: 'text', text: 'Node 1', x: 0, y: 0, width: 100, height: 50 }, 132 | { id: 'node2', type: 'text', text: 'Node 2', x: 200, y: 0, width: 100, height: 50 }, 133 | ], 134 | edges: [ 135 | { id: 'edge1', fromNode: 'node1', toNode: 'node2', fromEnd: 'none', toEnd: 'arrow' }, 136 | { id: 'edge2', fromNode: 'node2', toNode: 'node1', fromEnd: 'arrow', toEnd: 'none' }, 137 | ], 138 | }; 139 | 140 | const result = jsonCanvas.convertToMermaid(input); 141 | 142 | expect(result).toContain('node1 --> node2'); 143 | expect(result).toContain('node2 <-- node1'); 144 | }); 145 | 146 | test('should handle edge labels', () => { 147 | const input = { 148 | nodes: [ 149 | { id: 'node1', type: 'text', text: 'Node 1', x: 0, y: 0, width: 100, height: 50 }, 150 | { id: 'node2', type: 'text', text: 'Node 2', x: 200, y: 0, width: 100, height: 50 }, 151 | ], 152 | edges: [{ id: 'edge1', fromNode: 'node1', toNode: 'node2', label: 'Connection' }], 153 | }; 154 | 155 | const result = jsonCanvas.convertToMermaid(input); 156 | 157 | expect(result).toContain('node1 --> |Connection| node2'); 158 | }); 159 | }); 160 | 161 | describe('Styling', () => { 162 | test('should apply custom colors to nodes when provided', () => { 163 | const input = { 164 | nodes: [ 165 | { 166 | id: 'node1', 167 | x: 0, 168 | y: 0, 169 | width: 100, 170 | height: 50, 171 | type: 'text', 172 | text: 'Colored Node', 173 | color: '1', 174 | }, 175 | ], 176 | edges: [], 177 | }; 178 | const customColors = { 1: '#ff0000' }; 179 | 180 | const result = jsonCanvas.convertToMermaid(input, customColors); 181 | 182 | expect(result).toContain('style node1 fill:#ff0000'); 183 | }); 184 | 185 | test('should apply custom colors to edges when provided', () => { 186 | const input = { 187 | nodes: [{ id: 'node1', type: 'text', text: 'Node 1', x: 0, y: 0, width: 100, height: 50 }], 188 | edges: [{ id: 'edge1', fromNode: 'node1', toNode: 'node1', color: '2' }], 189 | }; 190 | 191 | const result = jsonCanvas.convertToMermaid(input, { 2: '#00ff00' }); 192 | 193 | expect(result).toContain('linkStyle 0 stroke:#00ff00'); 194 | }); 195 | }); 196 | }); 197 | -------------------------------------------------------------------------------- /src/convertToMermaid.js: -------------------------------------------------------------------------------- 1 | import { createNodeTree } from './createNodeTree.js'; 2 | import { validateCustomColors, validateGraphDirection } from './validation.js'; 3 | 4 | // ========== CREATE MERMAID SYNTAX ========== 5 | /** 6 | * Generates a Mermaid Flowchart syntax based on the provided JSON Canvas data. 7 | * 8 | * @param {Object} data - The JSON Canvas data object containing nodes and edges. 9 | * @param {Object} [customColors={}] - Optional custom color mapping for nodes and edges. 10 | * Keys are color identifiers, values are hex color codes. Maximum of 6 colors. 11 | * Example: { 1: '#ff0000', 2: '#00ff00', 3: '#0000ff' } 12 | * @param {string} [graphDirection='TB'] - Optional direction of the graph. 13 | * Valid options are: 'TB' (top to bottom), 'LR' (left to right), 14 | * 'BT' (bottom to top), 'RL' (right to left). 15 | * @returns {string} The generated Mermaid Flowchart syntax. 16 | * @throws {Error} If an invalid graph direction is provided. 17 | * 18 | * @description 19 | * This function converts JSON Canvas data into Mermaid Flowchart syntax: 20 | * - Supports various node types: text, file, link, and group. 21 | * - Handles nested group structures. 22 | * - Applies custom colors to nodes and edges if provided. 23 | * - Generates appropriate syntax for different edge types and labels. 24 | * - The output can be used directly with Mermaid to render a flowchart. 25 | */ 26 | 27 | export function convertToMermaid(data, customColors = {}, graphDirection = 'TB') { 28 | // Validate parameters 29 | // The data parameter is validated in the createNodeTree function so we don't need to validate it here. 30 | validateCustomColors(customColors); 31 | validateGraphDirection(graphDirection); 32 | 33 | // ========== COLOR GENERATION ========== 34 | // Adds custom colors to the default color map if provided. 35 | function createColorMap(customColors) { 36 | const defaultColorMap = { 37 | 1: '#fb464c', // red 38 | 2: '#e9973f', // orange 39 | 3: '#e0de71', // yellow 40 | 4: '#44cf6e', // green 41 | 5: '#53dfdd', // cyan 42 | 6: '#a882ff', // purple 43 | }; 44 | 45 | const colorMap = { ...defaultColorMap, ...customColors }; 46 | return colorMap; 47 | } 48 | 49 | const colorMap = createColorMap(customColors); 50 | 51 | // Helper function to get the color based on the custom color map 52 | function getColor(color) { 53 | return colorMap[color] || color; 54 | } 55 | 56 | //Bu8ild new data with children arrays 57 | const hierarchicalData = createNodeTree(data); 58 | 59 | // ========== GENERATE MERMAID CODE ========== 60 | // Uses the hierarchical data to generate the Mermaid Flowchart syntax 61 | function generateMermaidFlowchart(data) { 62 | const { nodes, edges } = data; 63 | 64 | // This will store styles for nodes and edges/lines for use later 65 | let graphStyles = ''; 66 | 67 | // Helper function to generate Mermaid Flowchart syntax for a node 68 | function generateNodeSyntax(node) { 69 | const { id, type, label, text, file, subpath, url, color } = node; 70 | 71 | // Add styling for node 72 | generateNodeStyle(node); 73 | 74 | if (type === 'group') { 75 | // Handle empty group label 76 | let newGroupLabel; 77 | if (label === '') { 78 | newGroupLabel = ' '; 79 | } else { 80 | newGroupLabel = label; 81 | } 82 | return `subgraph ${id}["${newGroupLabel}"]\n${generateSubgraphSyntax(node)}\nend\n`; 83 | } else if (type === 'text') { 84 | //Handle empty node label 85 | let newText; 86 | if (text === '') { 87 | newText = ' '; 88 | } else { 89 | newText = text; 90 | } 91 | return `${id}["${newText}"]\n`; 92 | } else if (type === 'file') { 93 | const fileLabel = subpath ? `${file}${subpath}` : file; 94 | return `${id}["${fileLabel}"]\n`; 95 | } else if (type === 'link') { 96 | return `${id}["${url}"]\n`; 97 | } 98 | return ''; 99 | } 100 | 101 | // Helper function to generate Mermaid Flowchart syntax for a subgraph 102 | function generateSubgraphSyntax(node) { 103 | const { children } = node; 104 | let syntax = ''; 105 | if (children && children.length > 0) { 106 | for (const childId of children) { 107 | const childNode = nodes.find((n) => n.id === childId); 108 | if (childNode) { 109 | syntax += generateNodeSyntax(childNode); 110 | } 111 | } 112 | } 113 | return syntax; 114 | } 115 | // Helper function to generate Mermaid Flowchart syntax for an edge 116 | function generateEdgeSyntax(edge) { 117 | const { fromNode, toNode, fromEnd = 'none', toEnd = 'arrow', label } = edge; 118 | generateEdgeStyle(edge); 119 | 120 | const arrowStyleMap = { 121 | 'none-arrow': '-->', 122 | 'arrow-none': '<--', 123 | 'arrow-arrow': '<-->', 124 | 'none-none': '---', 125 | }; 126 | const arrowStyle = arrowStyleMap[`${fromEnd}-${toEnd}`] || '---'; 127 | 128 | // check if lable exists 129 | const edgeLabel = label ? ` |${label}|` : ''; 130 | 131 | return `${fromNode} ${arrowStyle}${edgeLabel} ${toNode}\n`; 132 | } 133 | 134 | // Helper function to push brightness of hex colors around 135 | function adjustBrightness(hex, percent) { 136 | // Remove the '#' character if present 137 | hex = hex.replace('#', ''); 138 | 139 | // Convert the hex color to RGB 140 | let r = parseInt(hex.substring(0, 2), 16); 141 | let g = parseInt(hex.substring(2, 4), 16); 142 | let b = parseInt(hex.substring(4, 6), 16); 143 | 144 | // Adjust the brightness by the specified percentage 145 | const amount = Math.round(2.55 * percent); 146 | r = Math.max(0, Math.min(255, r + amount)); 147 | g = Math.max(0, Math.min(255, g + amount)); 148 | b = Math.max(0, Math.min(255, b + amount)); 149 | 150 | // Convert the RGB values back to hex 151 | const rr = r.toString(16).padStart(2, '0'); 152 | const gg = g.toString(16).padStart(2, '0'); 153 | const bb = b.toString(16).padStart(2, '0'); 154 | 155 | return `#${rr}${gg}${bb}`; 156 | } 157 | 158 | // Helper function to generate Mermaid Styling for a node 159 | function generateNodeStyle(node) { 160 | const { id, color, type } = node; 161 | 162 | // Check to see if color exists 163 | if (!color) { 164 | return; 165 | } 166 | 167 | const nodeColor = getColor(color); 168 | const nodeColorALT = adjustBrightness(nodeColor, -20); 169 | 170 | let nodeStyle = `style ${id} fill:${nodeColor}, stroke:${nodeColorALT}\n`; 171 | 172 | graphStyles += nodeStyle; 173 | } 174 | 175 | // Helper function to generate Mermaid Styling for an edge 176 | let edgeCounter = 0; 177 | function generateEdgeStyle(edge) { 178 | const { color } = edge; 179 | 180 | // Check to see if color exists 181 | if (!color) { 182 | edgeCounter++; 183 | return; 184 | } 185 | const edgeColor = getColor(color); 186 | let edgeStyle = `linkStyle ${edgeCounter} stroke:${edgeColor}\n`; 187 | 188 | edgeCounter++; 189 | graphStyles += edgeStyle; 190 | } 191 | 192 | // Start writing graph syntax 193 | let flowchartSyntax = `graph ${graphDirection}\n`; 194 | 195 | // Generate Mermaid Flowchart syntax for each node 196 | for (const node of nodes) { 197 | flowchartSyntax += generateNodeSyntax(node); 198 | } 199 | 200 | // Generate Mermaid Flowchart syntax for each edge/line 201 | for (const edge of edges) { 202 | flowchartSyntax += generateEdgeSyntax(edge); 203 | } 204 | 205 | // Add generated styles at the end 206 | flowchartSyntax += graphStyles; 207 | 208 | return flowchartSyntax; 209 | } 210 | 211 | const mermaidFlowchart = generateMermaidFlowchart(hierarchicalData); 212 | 213 | return mermaidFlowchart; 214 | } 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Canvas to Mermaid 2 | 3 | [![npm version](https://badge.fury.io/js/json-canvas-to-mermaid.svg)](https://badge.fury.io/js/json-canvas-to-mermaid) 4 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 5 | 6 | Transform your JSON Canvas data into Mermaid flowcharts with ease. This lightweight and powerful library bridges the gap between JSON Canvas and Mermaid, enabling you to visualize your canvas data in a whole new way. 7 | 8 | > [!TIP] 9 | > Give it a try! Convert `.canvas` files to Mermaid syntax right in your browser. https://alexwiench.github.io/json-canvas-to-mermaid-demo/ 10 | 11 | ## Table of Contents 12 | 13 | - [Table of Contents](#table-of-contents) 14 | - [Features](#features) 15 | - [Installation](#installation) 16 | - [How It Works](#how-it-works) 17 | - [Usage](#usage) 18 | - [API Reference](#api-reference) 19 | - [`convertToMermaid(data, customColors, graphDirection)`](#converttomermaiddata-customcolors-graphdirection) 20 | - [Additional Utilities](#additional-utilities) 21 | - [Examples](#examples) 22 | - [Basic Usage](#basic-usage) 23 | - [Advanced Usage](#advanced-usage) 24 | - [Using Validation and CreateNodeTree Utilities](#using-validation-and-createnodetree-utilities) 25 | - [Validation](#validation) 26 | - [CreateNodeTree](#createnodetree) 27 | - [What are JSON Canvas and Mermaid?](#what-are-json-canvas-and-mermaid) 28 | - [License](#license) 29 | 30 | ## Features 31 | 32 | - Convert JSON Canvas data to Mermaid flowchart syntax 33 | - Support for all node types and features in the JSON Canvas spec 34 | - Handle nested group structures 35 | - Apply custom colors to nodes and edges 36 | - Generate appropriate syntax for different edge types and labels 37 | - Zero dependencies 38 | 39 | ## Installation 40 | 41 | ```bash 42 | npm install json-canvas-to-mermaid 43 | ``` 44 | 45 | ## How It Works 46 | 47 | JSON Canvas to Mermaid works in three main steps: 48 | 49 | 1. **Validation**: It checks the input JSON Canvas data for correctness and completeness. 50 | 2. **Hierarchy Creation**: It builds a tree-like structure from the flat JSON Canvas data, identifying parent-child relationships between nodes. 51 | 3. **Mermaid Syntax Generation**: It converts the hierarchical structure into Mermaid flowchart syntax, handling various node types, edges, and styling. 52 | 53 | ## Usage 54 | 55 | Here's a quick example of how to use the library: 56 | 57 | ```javascript 58 | import convertToMermaid from 'json-canvas-to-mermaid'; 59 | 60 | const jsonCanvasData = { 61 | nodes: [ 62 | { id: 'node1', type: 'text', text: 'Hello', x: 0, y: 0, width: 100, height: 50 }, 63 | { id: 'node2', type: 'text', text: 'World', x: 200, y: 0, width: 100, height: 50 }, 64 | ], 65 | edges: [{ id: 'edge1', fromNode: 'node1', toNode: 'node2' }], 66 | }; 67 | 68 | const mermaidSyntax = convertToMermaid(jsonCanvasData); 69 | console.log(mermaidSyntax); 70 | ``` 71 | 72 | ## API Reference 73 | 74 | ### `convertToMermaid(data, customColors, graphDirection)` 75 | 76 | Converts JSON Canvas data to Mermaid flowchart syntax. 77 | 78 | - `data`: The JSON Canvas data object containing nodes and edges. 79 | - `customColors` (optional): An object mapping color identifiers to hex color codes. Maximum of 6 colors. 80 | - `graphDirection` (optional): The direction of the graph. Valid options are 'TB', 'LR', 'BT', 'RL'. Default is 'TB'. 81 | 82 | Returns a string containing the Mermaid flowchart syntax. 83 | 84 | ### Additional Utilities 85 | 86 | The library also exports some additional utilities that were developed as part of this project. While not necessary for the primary use case, they're available if you need them for other projects: 87 | 88 | - `createNodeTree(data)`: Creates a hierarchical structure from flat JSON Canvas data. 89 | - `validateJsonCanvasData(data)`: Validates the structure and content of JSON Canvas data. 90 | - `validateCustomColors(customColors)`: Validates the custom colors object. 91 | - `validateGraphDirection(graphDirection)`: Validates the graph direction parameter. 92 | 93 | These functions are not required for normal use of the library but are exposed for developers who might find them useful in other contexts. 94 | 95 | ## Examples 96 | 97 | ### Basic Usage 98 | 99 | ```javascript 100 | import convertToMermaid from 'json-canvas-to-mermaid'; 101 | 102 | const data = { 103 | nodes: [ 104 | { id: 'node1', type: 'text', text: 'Node 1', x: 0, y: 0, width: 100, height: 50 }, 105 | { id: 'node2', type: 'text', text: 'Node 2', x: 200, y: 0, width: 100, height: 50 }, 106 | ], 107 | edges: [{ id: 'edge1', fromNode: 'node1', toNode: 'node2' }], 108 | }; 109 | 110 | const mermaidSyntax = convertToMermaid(data); 111 | console.log(mermaidSyntax); 112 | ``` 113 | 114 | ### Advanced Usage 115 | 116 | ```javascript 117 | const customColors = { 118 | 1: '#ff0000', 119 | 2: '#00ff00', 120 | 3: '#0000ff', 121 | }; 122 | 123 | const customDirection = 'LR'; 124 | 125 | const data = { 126 | nodes: [ 127 | { id: 'node1', type: 'text', text: 'Red Node', x: 0, y: 0, width: 100, height: 50, color: '1' }, 128 | { 129 | id: 'node2', 130 | type: 'text', 131 | text: 'Green Node', 132 | x: 200, 133 | y: 0, 134 | width: 100, 135 | height: 50, 136 | color: '2', 137 | }, 138 | ], 139 | edges: [{ id: 'edge1', fromNode: 'node1', toNode: 'node2', color: '3' }], 140 | }; 141 | 142 | const mermaidSyntax = convertToMermaid(data, customColors, customDirection); 143 | ``` 144 | 145 | ### Using Validation and CreateNodeTree Utilities 146 | 147 | While not necessary for the primary use case, the validation and createNodeTree utilities can be useful in certain scenarios. Here's how you can use them: 148 | 149 | #### Validation 150 | 151 | You can use the validation functions to check your JSON Canvas data or other parameters: 152 | 153 | ```javascript 154 | import { jsonCanvas } from 'json-canvas-to-mermaid'; 155 | 156 | const data = { 157 | nodes: [ 158 | { id: 'node1', type: 'text', text: 'Hello', x: 0, y: 0, width: 100, height: 50 }, 159 | { id: 'node2', type: 'text', text: 'World', x: 200, y: 0, width: 100, height: 50 }, 160 | ], 161 | edges: [{ id: 'edge1', fromNode: 'node1', toNode: 'node2' }], 162 | }; 163 | 164 | try { 165 | jsonCanvas.validate.data(data); 166 | console.log('JSON Canvas data is valid'); 167 | } catch (error) { 168 | console.error('Invalid JSON Canvas data:', error.message); 169 | } 170 | 171 | // Validate custom colors 172 | const customColors = { 1: '#ff0000', 2: '#00ff00' }; 173 | try { 174 | jsonCanvas.validate.colors(customColors); 175 | console.log('Custom colors are valid'); 176 | } catch (error) { 177 | console.error('Invalid custom colors:', error.message); 178 | } 179 | 180 | // Validate graph direction 181 | const direction = 'LR'; 182 | try { 183 | jsonCanvas.validate.direction(direction); 184 | console.log('Graph direction is valid'); 185 | } catch (error) { 186 | console.error('Invalid graph direction:', error.message); 187 | } 188 | ``` 189 | 190 | #### CreateNodeTree 191 | 192 | The createNodeTree function can be used to generate a hierarchical structure from flat JSON Canvas data: 193 | 194 | ```javascript 195 | import { jsonCanvas } from 'json-canvas-to-mermaid'; 196 | 197 | const data = { 198 | nodes: [ 199 | { id: 'group1', type: 'group', x: 0, y: 0, width: 300, height: 200 }, 200 | { id: 'node1', type: 'text', text: 'Inside Group', x: 50, y: 50, width: 100, height: 50 }, 201 | { id: 'node2', type: 'text', text: 'Outside Group', x: 400, y: 50, width: 100, height: 50 }, 202 | ], 203 | edges: [], 204 | }; 205 | 206 | const hierarchicalData = jsonCanvas.createNodeTree(data); 207 | console.log(JSON.stringify(hierarchicalData, null, 2)); 208 | 209 | // Output: 210 | // { 211 | // "nodes": [ 212 | // { 213 | // "id": "group1", 214 | // "type": "group", 215 | // "x": 0, 216 | // "y": 0, 217 | // "width": 300, 218 | // "height": 200, 219 | // "children": ["node1"] 220 | // }, 221 | // { 222 | // "id": "node1", 223 | // "type": "text", 224 | // "text": "Inside Group", 225 | // "x": 50, 226 | // "y": 50, 227 | // "width": 100, 228 | // "height": 50, 229 | // "children": null 230 | // }, 231 | // { 232 | // "id": "node2", 233 | // "type": "text", 234 | // "text": "Outside Group", 235 | // "x": 400, 236 | // "y": 50, 237 | // "width": 100, 238 | // "height": 50, 239 | // "children": null 240 | // } 241 | // ], 242 | // "edges": [] 243 | // } 244 | ``` 245 | 246 | This hierarchical structure can be useful if you need to process the JSON Canvas data in a tree-like format for other purposes in your application. 247 | 248 | ## What are JSON Canvas and Mermaid? 249 | 250 | Learn more about those projects here: 251 | 252 | - JSON Canvas: https://jsoncanvas.org/ 253 | - Mermaid: https://mermaid.js.org/ 254 | 255 | ## License 256 | 257 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 258 | --------------------------------------------------------------------------------