Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Mermaid diagram blocks in docs + SVG import in diagrams (#610 #611)

Docs — Mermaid blocks:
- New MermaidBlock TipTap extension (src/docs/extensions/mermaid-block.ts)
- NodeView with textarea code editor + rendered SVG preview
- Slash command /diagram and /mermaid insert the block
- Copy SVG button; double-click diagram to open editor
- Mermaid dynamically imported (zero initial bundle impact)
- CSS styles for mermaid-block, toolbar, preview, error, and dark-mode SVG fix

Diagrams — SVG import:
- New src/diagrams/svg-import.ts pure parser (DOMParser + coordinate scaling)
- Handles rect, circle, ellipse, line, polyline, polygon, text, nested <g> groups
- Reads viewBox or width/height for coordinate normalisation
- Simple translate/scale transforms on groups applied to children
- "Import SVG" button + hidden file input wired in toolbar-wiring.ts
- 22 new tests in tests/svg-import.test.ts; all 8379/8379 passing
- Version 0.36.1 → 0.37.0

+2143 -7
+8
CHANGELOG.md
··· 7 7 8 8 ## [Unreleased] 9 9 10 + ## [0.37.0] — 2026-04-14 11 + 12 + ### Added 13 + - Docs: Mermaid diagram blocks — type `/diagram` or `/mermaid` in the editor to insert an inline Mermaid diagram (flowcharts, sequence diagrams, ER, Gantt, class diagrams, etc.). Code editor panel reveals on click; SVG renders with 400ms debounce. Copy SVG button included. Mermaid is dynamically imported to keep initial bundle size unchanged. (#610) 14 + - Diagrams: SVG file import — "Import SVG" button in the diagrams toolbar opens a file picker. Supported elements: `rect`, `circle`, `ellipse`, `line`, `polyline`, `polygon`, `text`, and nested `<g>` groups. Coordinates are scaled from the SVG viewBox to the 960×540 canvas. Unsupported elements (path, image) are silently skipped with a count logged to console. (#611) 15 + 10 16 ## [0.36.1] — 2026-04-14 11 17 12 18 ### Improved ··· 21 27 ## [0.36.0] — 2026-04-13 22 28 23 29 ### Added 30 + - feat: improve PPTX import — images, rich text, rotation, tables, dynamic dimensions (#609) 31 + - feat: improve PPTX import — images, rich text, rotation, tables, dynamic dimensions (#609) 24 32 - feat: PDF and PPTX file import support (#608) 25 33 - PDF import: drop or import a `.pdf` file from the landing page or docs in-editor import menu — text is extracted page-by-page via pdf.js (dynamically loaded) and converted to headings and paragraphs in the TipTap editor (#608) 26 34 - PPTX import: drop or import a `.pptx` file from the landing page — slides are parsed from the ZIP+XML format using JSZip (no new deps), mapped to our canvas element model with title, body, and other text shapes; speaker notes are preserved (#608)
+1215 -4
package-lock.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.35.0", 3 + "version": "0.36.1", 4 4 "lockfileVersion": 3, 5 5 "requires": true, 6 6 "packages": { 7 7 "": { 8 8 "name": "tools", 9 - "version": "0.35.0", 9 + "version": "0.36.1", 10 10 "dependencies": { 11 11 "@tiptap/core": "^2.11.0", 12 12 "@tiptap/extension-code-block-lowlight": "^2.27.2", ··· 42 42 "lowlight": "^3.3.0", 43 43 "mammoth": "^1.12.0", 44 44 "markdown-it": "^14.1.1", 45 + "mermaid": "^11.14.0", 45 46 "pdfjs-dist": "^5.6.205", 46 47 "turndown": "^7.2.2", 47 48 "turndown-plugin-gfm": "^1.0.2", ··· 70 71 "vitest": "^4.1.0" 71 72 } 72 73 }, 74 + "node_modules/@antfu/install-pkg": { 75 + "version": "1.1.0", 76 + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", 77 + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", 78 + "license": "MIT", 79 + "dependencies": { 80 + "package-manager-detector": "^1.3.0", 81 + "tinyexec": "^1.0.1" 82 + }, 83 + "funding": { 84 + "url": "https://github.com/sponsors/antfu" 85 + } 86 + }, 73 87 "node_modules/@asamuzakjp/css-color": { 74 88 "version": "5.0.1", 75 89 "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", ··· 120 134 "node": ">=6.9.0" 121 135 } 122 136 }, 137 + "node_modules/@braintree/sanitize-url": { 138 + "version": "7.1.2", 139 + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", 140 + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", 141 + "license": "MIT" 142 + }, 123 143 "node_modules/@bramus/specificity": { 124 144 "version": "2.4.2", 125 145 "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", ··· 132 152 "bin": { 133 153 "specificity": "bin/cli.js" 134 154 } 155 + }, 156 + "node_modules/@chevrotain/cst-dts-gen": { 157 + "version": "12.0.0", 158 + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", 159 + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", 160 + "license": "Apache-2.0", 161 + "dependencies": { 162 + "@chevrotain/gast": "12.0.0", 163 + "@chevrotain/types": "12.0.0" 164 + } 165 + }, 166 + "node_modules/@chevrotain/gast": { 167 + "version": "12.0.0", 168 + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", 169 + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", 170 + "license": "Apache-2.0", 171 + "dependencies": { 172 + "@chevrotain/types": "12.0.0" 173 + } 174 + }, 175 + "node_modules/@chevrotain/regexp-to-ast": { 176 + "version": "12.0.0", 177 + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", 178 + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", 179 + "license": "Apache-2.0" 180 + }, 181 + "node_modules/@chevrotain/types": { 182 + "version": "12.0.0", 183 + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", 184 + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", 185 + "license": "Apache-2.0" 186 + }, 187 + "node_modules/@chevrotain/utils": { 188 + "version": "12.0.0", 189 + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", 190 + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", 191 + "license": "Apache-2.0" 135 192 }, 136 193 "node_modules/@csstools/color-helpers": { 137 194 "version": "6.0.2", ··· 1363 1420 "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", 1364 1421 "license": "MIT" 1365 1422 }, 1423 + "node_modules/@iconify/types": { 1424 + "version": "2.0.0", 1425 + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", 1426 + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", 1427 + "license": "MIT" 1428 + }, 1429 + "node_modules/@iconify/utils": { 1430 + "version": "3.1.0", 1431 + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", 1432 + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", 1433 + "license": "MIT", 1434 + "dependencies": { 1435 + "@antfu/install-pkg": "^1.1.0", 1436 + "@iconify/types": "^2.0.0", 1437 + "mlly": "^1.8.0" 1438 + } 1439 + }, 1366 1440 "node_modules/@isaacs/cliui": { 1367 1441 "version": "8.0.2", 1368 1442 "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", ··· 1593 1667 "license": "MIT", 1594 1668 "engines": { 1595 1669 "node": ">= 10.0.0" 1670 + } 1671 + }, 1672 + "node_modules/@mermaid-js/parser": { 1673 + "version": "1.1.0", 1674 + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", 1675 + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", 1676 + "license": "MIT", 1677 + "dependencies": { 1678 + "langium": "^4.0.0" 1596 1679 } 1597 1680 }, 1598 1681 "node_modules/@mixmark-io/domino": { ··· 2979 3062 "@types/node": "*" 2980 3063 } 2981 3064 }, 3065 + "node_modules/@types/d3": { 3066 + "version": "7.4.3", 3067 + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", 3068 + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", 3069 + "license": "MIT", 3070 + "dependencies": { 3071 + "@types/d3-array": "*", 3072 + "@types/d3-axis": "*", 3073 + "@types/d3-brush": "*", 3074 + "@types/d3-chord": "*", 3075 + "@types/d3-color": "*", 3076 + "@types/d3-contour": "*", 3077 + "@types/d3-delaunay": "*", 3078 + "@types/d3-dispatch": "*", 3079 + "@types/d3-drag": "*", 3080 + "@types/d3-dsv": "*", 3081 + "@types/d3-ease": "*", 3082 + "@types/d3-fetch": "*", 3083 + "@types/d3-force": "*", 3084 + "@types/d3-format": "*", 3085 + "@types/d3-geo": "*", 3086 + "@types/d3-hierarchy": "*", 3087 + "@types/d3-interpolate": "*", 3088 + "@types/d3-path": "*", 3089 + "@types/d3-polygon": "*", 3090 + "@types/d3-quadtree": "*", 3091 + "@types/d3-random": "*", 3092 + "@types/d3-scale": "*", 3093 + "@types/d3-scale-chromatic": "*", 3094 + "@types/d3-selection": "*", 3095 + "@types/d3-shape": "*", 3096 + "@types/d3-time": "*", 3097 + "@types/d3-time-format": "*", 3098 + "@types/d3-timer": "*", 3099 + "@types/d3-transition": "*", 3100 + "@types/d3-zoom": "*" 3101 + } 3102 + }, 3103 + "node_modules/@types/d3-array": { 3104 + "version": "3.2.2", 3105 + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", 3106 + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", 3107 + "license": "MIT" 3108 + }, 3109 + "node_modules/@types/d3-axis": { 3110 + "version": "3.0.6", 3111 + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", 3112 + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", 3113 + "license": "MIT", 3114 + "dependencies": { 3115 + "@types/d3-selection": "*" 3116 + } 3117 + }, 3118 + "node_modules/@types/d3-brush": { 3119 + "version": "3.0.6", 3120 + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", 3121 + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", 3122 + "license": "MIT", 3123 + "dependencies": { 3124 + "@types/d3-selection": "*" 3125 + } 3126 + }, 3127 + "node_modules/@types/d3-chord": { 3128 + "version": "3.0.6", 3129 + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", 3130 + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", 3131 + "license": "MIT" 3132 + }, 3133 + "node_modules/@types/d3-color": { 3134 + "version": "3.1.3", 3135 + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", 3136 + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", 3137 + "license": "MIT" 3138 + }, 3139 + "node_modules/@types/d3-contour": { 3140 + "version": "3.0.6", 3141 + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", 3142 + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", 3143 + "license": "MIT", 3144 + "dependencies": { 3145 + "@types/d3-array": "*", 3146 + "@types/geojson": "*" 3147 + } 3148 + }, 3149 + "node_modules/@types/d3-delaunay": { 3150 + "version": "6.0.4", 3151 + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", 3152 + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", 3153 + "license": "MIT" 3154 + }, 3155 + "node_modules/@types/d3-dispatch": { 3156 + "version": "3.0.7", 3157 + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", 3158 + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", 3159 + "license": "MIT" 3160 + }, 3161 + "node_modules/@types/d3-drag": { 3162 + "version": "3.0.7", 3163 + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", 3164 + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", 3165 + "license": "MIT", 3166 + "dependencies": { 3167 + "@types/d3-selection": "*" 3168 + } 3169 + }, 3170 + "node_modules/@types/d3-dsv": { 3171 + "version": "3.0.7", 3172 + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", 3173 + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", 3174 + "license": "MIT" 3175 + }, 3176 + "node_modules/@types/d3-ease": { 3177 + "version": "3.0.2", 3178 + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", 3179 + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", 3180 + "license": "MIT" 3181 + }, 3182 + "node_modules/@types/d3-fetch": { 3183 + "version": "3.0.7", 3184 + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", 3185 + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", 3186 + "license": "MIT", 3187 + "dependencies": { 3188 + "@types/d3-dsv": "*" 3189 + } 3190 + }, 3191 + "node_modules/@types/d3-force": { 3192 + "version": "3.0.10", 3193 + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", 3194 + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", 3195 + "license": "MIT" 3196 + }, 3197 + "node_modules/@types/d3-format": { 3198 + "version": "3.0.4", 3199 + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", 3200 + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", 3201 + "license": "MIT" 3202 + }, 3203 + "node_modules/@types/d3-geo": { 3204 + "version": "3.1.0", 3205 + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", 3206 + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", 3207 + "license": "MIT", 3208 + "dependencies": { 3209 + "@types/geojson": "*" 3210 + } 3211 + }, 3212 + "node_modules/@types/d3-hierarchy": { 3213 + "version": "3.1.7", 3214 + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", 3215 + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", 3216 + "license": "MIT" 3217 + }, 3218 + "node_modules/@types/d3-interpolate": { 3219 + "version": "3.0.4", 3220 + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", 3221 + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", 3222 + "license": "MIT", 3223 + "dependencies": { 3224 + "@types/d3-color": "*" 3225 + } 3226 + }, 3227 + "node_modules/@types/d3-path": { 3228 + "version": "3.1.1", 3229 + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", 3230 + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", 3231 + "license": "MIT" 3232 + }, 3233 + "node_modules/@types/d3-polygon": { 3234 + "version": "3.0.2", 3235 + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", 3236 + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", 3237 + "license": "MIT" 3238 + }, 3239 + "node_modules/@types/d3-quadtree": { 3240 + "version": "3.0.6", 3241 + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", 3242 + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", 3243 + "license": "MIT" 3244 + }, 3245 + "node_modules/@types/d3-random": { 3246 + "version": "3.0.3", 3247 + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", 3248 + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", 3249 + "license": "MIT" 3250 + }, 3251 + "node_modules/@types/d3-scale": { 3252 + "version": "4.0.9", 3253 + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", 3254 + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", 3255 + "license": "MIT", 3256 + "dependencies": { 3257 + "@types/d3-time": "*" 3258 + } 3259 + }, 3260 + "node_modules/@types/d3-scale-chromatic": { 3261 + "version": "3.1.0", 3262 + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", 3263 + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", 3264 + "license": "MIT" 3265 + }, 3266 + "node_modules/@types/d3-selection": { 3267 + "version": "3.0.11", 3268 + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", 3269 + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", 3270 + "license": "MIT" 3271 + }, 3272 + "node_modules/@types/d3-shape": { 3273 + "version": "3.1.8", 3274 + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", 3275 + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", 3276 + "license": "MIT", 3277 + "dependencies": { 3278 + "@types/d3-path": "*" 3279 + } 3280 + }, 3281 + "node_modules/@types/d3-time": { 3282 + "version": "3.0.4", 3283 + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", 3284 + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", 3285 + "license": "MIT" 3286 + }, 3287 + "node_modules/@types/d3-time-format": { 3288 + "version": "4.0.3", 3289 + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", 3290 + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", 3291 + "license": "MIT" 3292 + }, 3293 + "node_modules/@types/d3-timer": { 3294 + "version": "3.0.2", 3295 + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", 3296 + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", 3297 + "license": "MIT" 3298 + }, 3299 + "node_modules/@types/d3-transition": { 3300 + "version": "3.0.9", 3301 + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", 3302 + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", 3303 + "license": "MIT", 3304 + "dependencies": { 3305 + "@types/d3-selection": "*" 3306 + } 3307 + }, 3308 + "node_modules/@types/d3-zoom": { 3309 + "version": "3.0.8", 3310 + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", 3311 + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", 3312 + "license": "MIT", 3313 + "dependencies": { 3314 + "@types/d3-interpolate": "*", 3315 + "@types/d3-selection": "*" 3316 + } 3317 + }, 2982 3318 "node_modules/@types/debug": { 2983 3319 "version": "4.1.13", 2984 3320 "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", ··· 3037 3373 "dependencies": { 3038 3374 "@types/node": "*" 3039 3375 } 3376 + }, 3377 + "node_modules/@types/geojson": { 3378 + "version": "7946.0.16", 3379 + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", 3380 + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", 3381 + "license": "MIT" 3040 3382 }, 3041 3383 "node_modules/@types/hast": { 3042 3384 "version": "3.0.4", ··· 3243 3585 "@types/node": "*" 3244 3586 } 3245 3587 }, 3588 + "node_modules/@upsetjs/venn.js": { 3589 + "version": "2.0.0", 3590 + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", 3591 + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", 3592 + "license": "MIT", 3593 + "optionalDependencies": { 3594 + "d3-selection": "^3.0.0", 3595 + "d3-transition": "^3.0.1" 3596 + } 3597 + }, 3246 3598 "node_modules/@vitest/expect": { 3247 3599 "version": "4.1.0", 3248 3600 "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", ··· 3402 3754 "license": "MIT", 3403 3755 "engines": { 3404 3756 "node": ">= 0.6" 3757 + } 3758 + }, 3759 + "node_modules/acorn": { 3760 + "version": "8.16.0", 3761 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", 3762 + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", 3763 + "license": "MIT", 3764 + "bin": { 3765 + "acorn": "bin/acorn" 3766 + }, 3767 + "engines": { 3768 + "node": ">=0.4.0" 3405 3769 } 3406 3770 }, 3407 3771 "node_modules/agent-base": { ··· 4425 4789 "pnpm": ">=8" 4426 4790 } 4427 4791 }, 4792 + "node_modules/chevrotain": { 4793 + "version": "12.0.0", 4794 + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", 4795 + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", 4796 + "license": "Apache-2.0", 4797 + "peer": true, 4798 + "dependencies": { 4799 + "@chevrotain/cst-dts-gen": "12.0.0", 4800 + "@chevrotain/gast": "12.0.0", 4801 + "@chevrotain/regexp-to-ast": "12.0.0", 4802 + "@chevrotain/types": "12.0.0", 4803 + "@chevrotain/utils": "12.0.0" 4804 + }, 4805 + "engines": { 4806 + "node": ">=22.0.0" 4807 + } 4808 + }, 4809 + "node_modules/chevrotain-allstar": { 4810 + "version": "0.4.1", 4811 + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", 4812 + "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", 4813 + "license": "MIT", 4814 + "dependencies": { 4815 + "lodash-es": "^4.17.21" 4816 + }, 4817 + "peerDependencies": { 4818 + "chevrotain": "^12.0.0" 4819 + } 4820 + }, 4428 4821 "node_modules/chownr": { 4429 4822 "version": "1.1.4", 4430 4823 "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", ··· 4675 5068 "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" 4676 5069 } 4677 5070 }, 5071 + "node_modules/confbox": { 5072 + "version": "0.1.8", 5073 + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", 5074 + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", 5075 + "license": "MIT" 5076 + }, 4678 5077 "node_modules/content-disposition": { 4679 5078 "version": "0.5.4", 4680 5079 "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", ··· 4735 5134 "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 4736 5135 "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", 4737 5136 "license": "MIT" 5137 + }, 5138 + "node_modules/cose-base": { 5139 + "version": "1.0.3", 5140 + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", 5141 + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", 5142 + "license": "MIT", 5143 + "dependencies": { 5144 + "layout-base": "^1.0.0" 5145 + } 4738 5146 }, 4739 5147 "node_modules/crc": { 4740 5148 "version": "3.8.0", ··· 4847 5255 "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" 4848 5256 } 4849 5257 }, 5258 + "node_modules/cytoscape": { 5259 + "version": "3.33.2", 5260 + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", 5261 + "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", 5262 + "license": "MIT", 5263 + "peer": true, 5264 + "engines": { 5265 + "node": ">=0.10" 5266 + } 5267 + }, 5268 + "node_modules/cytoscape-cose-bilkent": { 5269 + "version": "4.1.0", 5270 + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", 5271 + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", 5272 + "license": "MIT", 5273 + "dependencies": { 5274 + "cose-base": "^1.0.0" 5275 + }, 5276 + "peerDependencies": { 5277 + "cytoscape": "^3.2.0" 5278 + } 5279 + }, 5280 + "node_modules/cytoscape-fcose": { 5281 + "version": "2.2.0", 5282 + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", 5283 + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", 5284 + "license": "MIT", 5285 + "dependencies": { 5286 + "cose-base": "^2.2.0" 5287 + }, 5288 + "peerDependencies": { 5289 + "cytoscape": "^3.2.0" 5290 + } 5291 + }, 5292 + "node_modules/cytoscape-fcose/node_modules/cose-base": { 5293 + "version": "2.2.0", 5294 + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", 5295 + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", 5296 + "license": "MIT", 5297 + "dependencies": { 5298 + "layout-base": "^2.0.0" 5299 + } 5300 + }, 5301 + "node_modules/cytoscape-fcose/node_modules/layout-base": { 5302 + "version": "2.0.1", 5303 + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", 5304 + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", 5305 + "license": "MIT" 5306 + }, 5307 + "node_modules/d3": { 5308 + "version": "7.9.0", 5309 + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", 5310 + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", 5311 + "license": "ISC", 5312 + "dependencies": { 5313 + "d3-array": "3", 5314 + "d3-axis": "3", 5315 + "d3-brush": "3", 5316 + "d3-chord": "3", 5317 + "d3-color": "3", 5318 + "d3-contour": "4", 5319 + "d3-delaunay": "6", 5320 + "d3-dispatch": "3", 5321 + "d3-drag": "3", 5322 + "d3-dsv": "3", 5323 + "d3-ease": "3", 5324 + "d3-fetch": "3", 5325 + "d3-force": "3", 5326 + "d3-format": "3", 5327 + "d3-geo": "3", 5328 + "d3-hierarchy": "3", 5329 + "d3-interpolate": "3", 5330 + "d3-path": "3", 5331 + "d3-polygon": "3", 5332 + "d3-quadtree": "3", 5333 + "d3-random": "3", 5334 + "d3-scale": "4", 5335 + "d3-scale-chromatic": "3", 5336 + "d3-selection": "3", 5337 + "d3-shape": "3", 5338 + "d3-time": "3", 5339 + "d3-time-format": "4", 5340 + "d3-timer": "3", 5341 + "d3-transition": "3", 5342 + "d3-zoom": "3" 5343 + }, 5344 + "engines": { 5345 + "node": ">=12" 5346 + } 5347 + }, 5348 + "node_modules/d3-array": { 5349 + "version": "3.2.4", 5350 + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", 5351 + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", 5352 + "license": "ISC", 5353 + "dependencies": { 5354 + "internmap": "1 - 2" 5355 + }, 5356 + "engines": { 5357 + "node": ">=12" 5358 + } 5359 + }, 5360 + "node_modules/d3-axis": { 5361 + "version": "3.0.0", 5362 + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", 5363 + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", 5364 + "license": "ISC", 5365 + "engines": { 5366 + "node": ">=12" 5367 + } 5368 + }, 5369 + "node_modules/d3-brush": { 5370 + "version": "3.0.0", 5371 + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", 5372 + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", 5373 + "license": "ISC", 5374 + "dependencies": { 5375 + "d3-dispatch": "1 - 3", 5376 + "d3-drag": "2 - 3", 5377 + "d3-interpolate": "1 - 3", 5378 + "d3-selection": "3", 5379 + "d3-transition": "3" 5380 + }, 5381 + "engines": { 5382 + "node": ">=12" 5383 + } 5384 + }, 5385 + "node_modules/d3-chord": { 5386 + "version": "3.0.1", 5387 + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", 5388 + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", 5389 + "license": "ISC", 5390 + "dependencies": { 5391 + "d3-path": "1 - 3" 5392 + }, 5393 + "engines": { 5394 + "node": ">=12" 5395 + } 5396 + }, 5397 + "node_modules/d3-color": { 5398 + "version": "3.1.0", 5399 + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", 5400 + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", 5401 + "license": "ISC", 5402 + "engines": { 5403 + "node": ">=12" 5404 + } 5405 + }, 5406 + "node_modules/d3-contour": { 5407 + "version": "4.0.2", 5408 + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", 5409 + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", 5410 + "license": "ISC", 5411 + "dependencies": { 5412 + "d3-array": "^3.2.0" 5413 + }, 5414 + "engines": { 5415 + "node": ">=12" 5416 + } 5417 + }, 5418 + "node_modules/d3-delaunay": { 5419 + "version": "6.0.4", 5420 + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", 5421 + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", 5422 + "license": "ISC", 5423 + "dependencies": { 5424 + "delaunator": "5" 5425 + }, 5426 + "engines": { 5427 + "node": ">=12" 5428 + } 5429 + }, 5430 + "node_modules/d3-dispatch": { 5431 + "version": "3.0.1", 5432 + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", 5433 + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", 5434 + "license": "ISC", 5435 + "engines": { 5436 + "node": ">=12" 5437 + } 5438 + }, 5439 + "node_modules/d3-drag": { 5440 + "version": "3.0.0", 5441 + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", 5442 + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", 5443 + "license": "ISC", 5444 + "dependencies": { 5445 + "d3-dispatch": "1 - 3", 5446 + "d3-selection": "3" 5447 + }, 5448 + "engines": { 5449 + "node": ">=12" 5450 + } 5451 + }, 5452 + "node_modules/d3-dsv": { 5453 + "version": "3.0.1", 5454 + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", 5455 + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", 5456 + "license": "ISC", 5457 + "dependencies": { 5458 + "commander": "7", 5459 + "iconv-lite": "0.6", 5460 + "rw": "1" 5461 + }, 5462 + "bin": { 5463 + "csv2json": "bin/dsv2json.js", 5464 + "csv2tsv": "bin/dsv2dsv.js", 5465 + "dsv2dsv": "bin/dsv2dsv.js", 5466 + "dsv2json": "bin/dsv2json.js", 5467 + "json2csv": "bin/json2dsv.js", 5468 + "json2dsv": "bin/json2dsv.js", 5469 + "json2tsv": "bin/json2dsv.js", 5470 + "tsv2csv": "bin/dsv2dsv.js", 5471 + "tsv2json": "bin/dsv2json.js" 5472 + }, 5473 + "engines": { 5474 + "node": ">=12" 5475 + } 5476 + }, 5477 + "node_modules/d3-dsv/node_modules/commander": { 5478 + "version": "7.2.0", 5479 + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", 5480 + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", 5481 + "license": "MIT", 5482 + "engines": { 5483 + "node": ">= 10" 5484 + } 5485 + }, 5486 + "node_modules/d3-dsv/node_modules/iconv-lite": { 5487 + "version": "0.6.3", 5488 + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 5489 + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 5490 + "license": "MIT", 5491 + "dependencies": { 5492 + "safer-buffer": ">= 2.1.2 < 3.0.0" 5493 + }, 5494 + "engines": { 5495 + "node": ">=0.10.0" 5496 + } 5497 + }, 5498 + "node_modules/d3-ease": { 5499 + "version": "3.0.1", 5500 + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", 5501 + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", 5502 + "license": "BSD-3-Clause", 5503 + "engines": { 5504 + "node": ">=12" 5505 + } 5506 + }, 5507 + "node_modules/d3-fetch": { 5508 + "version": "3.0.1", 5509 + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", 5510 + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", 5511 + "license": "ISC", 5512 + "dependencies": { 5513 + "d3-dsv": "1 - 3" 5514 + }, 5515 + "engines": { 5516 + "node": ">=12" 5517 + } 5518 + }, 5519 + "node_modules/d3-force": { 5520 + "version": "3.0.0", 5521 + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", 5522 + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", 5523 + "license": "ISC", 5524 + "dependencies": { 5525 + "d3-dispatch": "1 - 3", 5526 + "d3-quadtree": "1 - 3", 5527 + "d3-timer": "1 - 3" 5528 + }, 5529 + "engines": { 5530 + "node": ">=12" 5531 + } 5532 + }, 5533 + "node_modules/d3-format": { 5534 + "version": "3.1.2", 5535 + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", 5536 + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", 5537 + "license": "ISC", 5538 + "engines": { 5539 + "node": ">=12" 5540 + } 5541 + }, 5542 + "node_modules/d3-geo": { 5543 + "version": "3.1.1", 5544 + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", 5545 + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", 5546 + "license": "ISC", 5547 + "dependencies": { 5548 + "d3-array": "2.5.0 - 3" 5549 + }, 5550 + "engines": { 5551 + "node": ">=12" 5552 + } 5553 + }, 5554 + "node_modules/d3-hierarchy": { 5555 + "version": "3.1.2", 5556 + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", 5557 + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", 5558 + "license": "ISC", 5559 + "engines": { 5560 + "node": ">=12" 5561 + } 5562 + }, 5563 + "node_modules/d3-interpolate": { 5564 + "version": "3.0.1", 5565 + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", 5566 + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", 5567 + "license": "ISC", 5568 + "dependencies": { 5569 + "d3-color": "1 - 3" 5570 + }, 5571 + "engines": { 5572 + "node": ">=12" 5573 + } 5574 + }, 5575 + "node_modules/d3-path": { 5576 + "version": "3.1.0", 5577 + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", 5578 + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", 5579 + "license": "ISC", 5580 + "engines": { 5581 + "node": ">=12" 5582 + } 5583 + }, 5584 + "node_modules/d3-polygon": { 5585 + "version": "3.0.1", 5586 + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", 5587 + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", 5588 + "license": "ISC", 5589 + "engines": { 5590 + "node": ">=12" 5591 + } 5592 + }, 5593 + "node_modules/d3-quadtree": { 5594 + "version": "3.0.1", 5595 + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", 5596 + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", 5597 + "license": "ISC", 5598 + "engines": { 5599 + "node": ">=12" 5600 + } 5601 + }, 5602 + "node_modules/d3-random": { 5603 + "version": "3.0.1", 5604 + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", 5605 + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", 5606 + "license": "ISC", 5607 + "engines": { 5608 + "node": ">=12" 5609 + } 5610 + }, 5611 + "node_modules/d3-sankey": { 5612 + "version": "0.12.3", 5613 + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", 5614 + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", 5615 + "license": "BSD-3-Clause", 5616 + "dependencies": { 5617 + "d3-array": "1 - 2", 5618 + "d3-shape": "^1.2.0" 5619 + } 5620 + }, 5621 + "node_modules/d3-sankey/node_modules/d3-array": { 5622 + "version": "2.12.1", 5623 + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", 5624 + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", 5625 + "license": "BSD-3-Clause", 5626 + "dependencies": { 5627 + "internmap": "^1.0.0" 5628 + } 5629 + }, 5630 + "node_modules/d3-sankey/node_modules/d3-path": { 5631 + "version": "1.0.9", 5632 + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", 5633 + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", 5634 + "license": "BSD-3-Clause" 5635 + }, 5636 + "node_modules/d3-sankey/node_modules/d3-shape": { 5637 + "version": "1.3.7", 5638 + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", 5639 + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", 5640 + "license": "BSD-3-Clause", 5641 + "dependencies": { 5642 + "d3-path": "1" 5643 + } 5644 + }, 5645 + "node_modules/d3-sankey/node_modules/internmap": { 5646 + "version": "1.0.1", 5647 + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", 5648 + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", 5649 + "license": "ISC" 5650 + }, 5651 + "node_modules/d3-scale": { 5652 + "version": "4.0.2", 5653 + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", 5654 + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", 5655 + "license": "ISC", 5656 + "dependencies": { 5657 + "d3-array": "2.10.0 - 3", 5658 + "d3-format": "1 - 3", 5659 + "d3-interpolate": "1.2.0 - 3", 5660 + "d3-time": "2.1.1 - 3", 5661 + "d3-time-format": "2 - 4" 5662 + }, 5663 + "engines": { 5664 + "node": ">=12" 5665 + } 5666 + }, 5667 + "node_modules/d3-scale-chromatic": { 5668 + "version": "3.1.0", 5669 + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", 5670 + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", 5671 + "license": "ISC", 5672 + "dependencies": { 5673 + "d3-color": "1 - 3", 5674 + "d3-interpolate": "1 - 3" 5675 + }, 5676 + "engines": { 5677 + "node": ">=12" 5678 + } 5679 + }, 5680 + "node_modules/d3-selection": { 5681 + "version": "3.0.0", 5682 + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", 5683 + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", 5684 + "license": "ISC", 5685 + "peer": true, 5686 + "engines": { 5687 + "node": ">=12" 5688 + } 5689 + }, 5690 + "node_modules/d3-shape": { 5691 + "version": "3.2.0", 5692 + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", 5693 + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", 5694 + "license": "ISC", 5695 + "dependencies": { 5696 + "d3-path": "^3.1.0" 5697 + }, 5698 + "engines": { 5699 + "node": ">=12" 5700 + } 5701 + }, 5702 + "node_modules/d3-time": { 5703 + "version": "3.1.0", 5704 + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", 5705 + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", 5706 + "license": "ISC", 5707 + "dependencies": { 5708 + "d3-array": "2 - 3" 5709 + }, 5710 + "engines": { 5711 + "node": ">=12" 5712 + } 5713 + }, 5714 + "node_modules/d3-time-format": { 5715 + "version": "4.1.0", 5716 + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", 5717 + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", 5718 + "license": "ISC", 5719 + "dependencies": { 5720 + "d3-time": "1 - 3" 5721 + }, 5722 + "engines": { 5723 + "node": ">=12" 5724 + } 5725 + }, 5726 + "node_modules/d3-timer": { 5727 + "version": "3.0.1", 5728 + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", 5729 + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", 5730 + "license": "ISC", 5731 + "engines": { 5732 + "node": ">=12" 5733 + } 5734 + }, 5735 + "node_modules/d3-transition": { 5736 + "version": "3.0.1", 5737 + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", 5738 + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", 5739 + "license": "ISC", 5740 + "dependencies": { 5741 + "d3-color": "1 - 3", 5742 + "d3-dispatch": "1 - 3", 5743 + "d3-ease": "1 - 3", 5744 + "d3-interpolate": "1 - 3", 5745 + "d3-timer": "1 - 3" 5746 + }, 5747 + "engines": { 5748 + "node": ">=12" 5749 + }, 5750 + "peerDependencies": { 5751 + "d3-selection": "2 - 3" 5752 + } 5753 + }, 5754 + "node_modules/d3-zoom": { 5755 + "version": "3.0.0", 5756 + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", 5757 + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", 5758 + "license": "ISC", 5759 + "dependencies": { 5760 + "d3-dispatch": "1 - 3", 5761 + "d3-drag": "2 - 3", 5762 + "d3-interpolate": "1 - 3", 5763 + "d3-selection": "2 - 3", 5764 + "d3-transition": "2 - 3" 5765 + }, 5766 + "engines": { 5767 + "node": ">=12" 5768 + } 5769 + }, 5770 + "node_modules/dagre-d3-es": { 5771 + "version": "7.0.14", 5772 + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", 5773 + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", 5774 + "license": "MIT", 5775 + "dependencies": { 5776 + "d3": "^7.9.0", 5777 + "lodash-es": "^4.17.21" 5778 + } 5779 + }, 4850 5780 "node_modules/data-urls": { 4851 5781 "version": "7.0.0", 4852 5782 "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", ··· 4966 5896 }, 4967 5897 "funding": { 4968 5898 "url": "https://github.com/sponsors/ljharb" 5899 + } 5900 + }, 5901 + "node_modules/delaunator": { 5902 + "version": "5.1.0", 5903 + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", 5904 + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", 5905 + "license": "ISC", 5906 + "dependencies": { 5907 + "robust-predicates": "^3.0.2" 4969 5908 } 4970 5909 }, 4971 5910 "node_modules/delayed-stream": { ··· 6387 7326 "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 6388 7327 "license": "ISC" 6389 7328 }, 7329 + "node_modules/hachure-fill": { 7330 + "version": "0.5.2", 7331 + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", 7332 + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", 7333 + "license": "MIT" 7334 + }, 6390 7335 "node_modules/has-flag": { 6391 7336 "version": "4.0.0", 6392 7337 "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", ··· 6745 7690 "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 6746 7691 "license": "ISC" 6747 7692 }, 7693 + "node_modules/internmap": { 7694 + "version": "2.0.3", 7695 + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", 7696 + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", 7697 + "license": "ISC", 7698 + "engines": { 7699 + "node": ">=12" 7700 + } 7701 + }, 6748 7702 "node_modules/iobuffer": { 6749 7703 "version": "5.4.0", 6750 7704 "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", ··· 7079 8033 "safe-buffer": "^5.0.1" 7080 8034 } 7081 8035 }, 8036 + "node_modules/katex": { 8037 + "version": "0.16.45", 8038 + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", 8039 + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", 8040 + "funding": [ 8041 + "https://opencollective.com/katex", 8042 + "https://github.com/sponsors/katex" 8043 + ], 8044 + "license": "MIT", 8045 + "dependencies": { 8046 + "commander": "^8.3.0" 8047 + }, 8048 + "bin": { 8049 + "katex": "cli.js" 8050 + } 8051 + }, 8052 + "node_modules/katex/node_modules/commander": { 8053 + "version": "8.3.0", 8054 + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", 8055 + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", 8056 + "license": "MIT", 8057 + "engines": { 8058 + "node": ">= 12" 8059 + } 8060 + }, 7082 8061 "node_modules/keyv": { 7083 8062 "version": "4.5.4", 7084 8063 "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", ··· 7089 8068 "json-buffer": "3.0.1" 7090 8069 } 7091 8070 }, 8071 + "node_modules/khroma": { 8072 + "version": "2.1.0", 8073 + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", 8074 + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" 8075 + }, 8076 + "node_modules/langium": { 8077 + "version": "4.2.2", 8078 + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz", 8079 + "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==", 8080 + "license": "MIT", 8081 + "dependencies": { 8082 + "@chevrotain/regexp-to-ast": "~12.0.0", 8083 + "chevrotain": "~12.0.0", 8084 + "chevrotain-allstar": "~0.4.1", 8085 + "vscode-languageserver": "~9.0.1", 8086 + "vscode-languageserver-textdocument": "~1.0.11", 8087 + "vscode-uri": "~3.1.0" 8088 + }, 8089 + "engines": { 8090 + "node": ">=20.10.0", 8091 + "npm": ">=10.2.3" 8092 + } 8093 + }, 8094 + "node_modules/layout-base": { 8095 + "version": "1.0.2", 8096 + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", 8097 + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", 8098 + "license": "MIT" 8099 + }, 7092 8100 "node_modules/lazy-val": { 7093 8101 "version": "1.0.5", 7094 8102 "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", ··· 7194 8202 "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", 7195 8203 "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", 7196 8204 "dev": true, 8205 + "license": "MIT" 8206 + }, 8207 + "node_modules/lodash-es": { 8208 + "version": "4.18.1", 8209 + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", 8210 + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", 7197 8211 "license": "MIT" 7198 8212 }, 7199 8213 "node_modules/lodash.defaults": { ··· 7432 8446 "markdown-it": "bin/markdown-it.mjs" 7433 8447 } 7434 8448 }, 8449 + "node_modules/marked": { 8450 + "version": "16.4.2", 8451 + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", 8452 + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", 8453 + "license": "MIT", 8454 + "bin": { 8455 + "marked": "bin/marked.js" 8456 + }, 8457 + "engines": { 8458 + "node": ">= 20" 8459 + } 8460 + }, 7435 8461 "node_modules/matcher": { 7436 8462 "version": "3.0.0", 7437 8463 "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", ··· 7486 8512 "url": "https://github.com/sponsors/sindresorhus" 7487 8513 } 7488 8514 }, 8515 + "node_modules/mermaid": { 8516 + "version": "11.14.0", 8517 + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", 8518 + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", 8519 + "license": "MIT", 8520 + "dependencies": { 8521 + "@braintree/sanitize-url": "^7.1.1", 8522 + "@iconify/utils": "^3.0.2", 8523 + "@mermaid-js/parser": "^1.1.0", 8524 + "@types/d3": "^7.4.3", 8525 + "@upsetjs/venn.js": "^2.0.0", 8526 + "cytoscape": "^3.33.1", 8527 + "cytoscape-cose-bilkent": "^4.1.0", 8528 + "cytoscape-fcose": "^2.2.0", 8529 + "d3": "^7.9.0", 8530 + "d3-sankey": "^0.12.3", 8531 + "dagre-d3-es": "7.0.14", 8532 + "dayjs": "^1.11.19", 8533 + "dompurify": "^3.3.1", 8534 + "katex": "^0.16.25", 8535 + "khroma": "^2.1.0", 8536 + "lodash-es": "^4.17.23", 8537 + "marked": "^16.3.0", 8538 + "roughjs": "^4.6.6", 8539 + "stylis": "^4.3.6", 8540 + "ts-dedent": "^2.2.0", 8541 + "uuid": "^11.1.0" 8542 + } 8543 + }, 8544 + "node_modules/mermaid/node_modules/uuid": { 8545 + "version": "11.1.0", 8546 + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 8547 + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 8548 + "funding": [ 8549 + "https://github.com/sponsors/broofa", 8550 + "https://github.com/sponsors/ctavan" 8551 + ], 8552 + "license": "MIT", 8553 + "bin": { 8554 + "uuid": "dist/esm/bin/uuid" 8555 + } 8556 + }, 7489 8557 "node_modules/methods": { 7490 8558 "version": "1.1.2", 7491 8559 "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", ··· 7756 8824 "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 7757 8825 "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 7758 8826 "license": "MIT" 8827 + }, 8828 + "node_modules/mlly": { 8829 + "version": "1.8.2", 8830 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", 8831 + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", 8832 + "license": "MIT", 8833 + "dependencies": { 8834 + "acorn": "^8.16.0", 8835 + "pathe": "^2.0.3", 8836 + "pkg-types": "^1.3.1", 8837 + "ufo": "^1.6.3" 8838 + } 7759 8839 }, 7760 8840 "node_modules/ms": { 7761 8841 "version": "2.0.0", ··· 8059 9139 "dev": true, 8060 9140 "license": "BlueOak-1.0.0" 8061 9141 }, 9142 + "node_modules/package-manager-detector": { 9143 + "version": "1.6.0", 9144 + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", 9145 + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", 9146 + "license": "MIT" 9147 + }, 8062 9148 "node_modules/pako": { 8063 9149 "version": "2.1.0", 8064 9150 "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", ··· 8099 9185 "engines": { 8100 9186 "node": ">= 0.8" 8101 9187 } 9188 + }, 9189 + "node_modules/path-data-parser": { 9190 + "version": "0.1.0", 9191 + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", 9192 + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", 9193 + "license": "MIT" 8102 9194 }, 8103 9195 "node_modules/path-is-absolute": { 8104 9196 "version": "1.0.1", ··· 8153 9245 "version": "2.0.3", 8154 9246 "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", 8155 9247 "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", 8156 - "dev": true, 8157 9248 "license": "MIT" 8158 9249 }, 8159 9250 "node_modules/pdfjs-dist": { ··· 8219 9310 "url": "https://github.com/sponsors/jonschlinkert" 8220 9311 } 8221 9312 }, 9313 + "node_modules/pkg-types": { 9314 + "version": "1.3.1", 9315 + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", 9316 + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", 9317 + "license": "MIT", 9318 + "dependencies": { 9319 + "confbox": "^0.1.8", 9320 + "mlly": "^1.7.4", 9321 + "pathe": "^2.0.1" 9322 + } 9323 + }, 8222 9324 "node_modules/playwright": { 8223 9325 "version": "1.58.2", 8224 9326 "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", ··· 8289 9391 "license": "MIT", 8290 9392 "engines": { 8291 9393 "node": ">=8.0" 9394 + } 9395 + }, 9396 + "node_modules/points-on-curve": { 9397 + "version": "0.2.0", 9398 + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", 9399 + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", 9400 + "license": "MIT" 9401 + }, 9402 + "node_modules/points-on-path": { 9403 + "version": "0.2.1", 9404 + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", 9405 + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", 9406 + "license": "MIT", 9407 + "dependencies": { 9408 + "path-data-parser": "0.1.0", 9409 + "points-on-curve": "0.2.0" 8292 9410 } 8293 9411 }, 8294 9412 "node_modules/postcss": { ··· 8975 10093 "license": "BSD-3-Clause", 8976 10094 "optional": true 8977 10095 }, 10096 + "node_modules/robust-predicates": { 10097 + "version": "3.0.3", 10098 + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", 10099 + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", 10100 + "license": "Unlicense" 10101 + }, 8978 10102 "node_modules/rollup": { 8979 10103 "version": "4.59.0", 8980 10104 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", ··· 9026 10150 "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", 9027 10151 "license": "MIT" 9028 10152 }, 10153 + "node_modules/roughjs": { 10154 + "version": "4.6.6", 10155 + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", 10156 + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", 10157 + "license": "MIT", 10158 + "dependencies": { 10159 + "hachure-fill": "^0.5.2", 10160 + "path-data-parser": "^0.1.0", 10161 + "points-on-curve": "^0.2.0", 10162 + "points-on-path": "^0.2.1" 10163 + } 10164 + }, 10165 + "node_modules/rw": { 10166 + "version": "1.3.3", 10167 + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", 10168 + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", 10169 + "license": "BSD-3-Clause" 10170 + }, 9029 10171 "node_modules/rxjs": { 9030 10172 "version": "7.8.2", 9031 10173 "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", ··· 9620 10762 "node": ">=0.10.0" 9621 10763 } 9622 10764 }, 10765 + "node_modules/stylis": { 10766 + "version": "4.3.6", 10767 + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", 10768 + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", 10769 + "license": "MIT" 10770 + }, 9623 10771 "node_modules/sumchecker": { 9624 10772 "version": "3.0.1", 9625 10773 "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", ··· 9863 11011 "version": "1.0.4", 9864 11012 "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", 9865 11013 "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", 9866 - "dev": true, 9867 11014 "license": "MIT", 9868 11015 "engines": { 9869 11016 "node": ">=18" ··· 9999 11146 "utf8-byte-length": "^1.0.1" 10000 11147 } 10001 11148 }, 11149 + "node_modules/ts-dedent": { 11150 + "version": "2.2.0", 11151 + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", 11152 + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", 11153 + "license": "MIT", 11154 + "engines": { 11155 + "node": ">=6.10" 11156 + } 11157 + }, 10002 11158 "node_modules/tslib": { 10003 11159 "version": "2.8.1", 10004 11160 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", ··· 10584 11740 "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", 10585 11741 "license": "MIT" 10586 11742 }, 11743 + "node_modules/ufo": { 11744 + "version": "1.6.3", 11745 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", 11746 + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", 11747 + "license": "MIT" 11748 + }, 10587 11749 "node_modules/underscore": { 10588 11750 "version": "1.13.8", 10589 11751 "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", ··· 10940 12102 "optional": false 10941 12103 } 10942 12104 } 12105 + }, 12106 + "node_modules/vscode-jsonrpc": { 12107 + "version": "8.2.0", 12108 + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", 12109 + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", 12110 + "license": "MIT", 12111 + "engines": { 12112 + "node": ">=14.0.0" 12113 + } 12114 + }, 12115 + "node_modules/vscode-languageserver": { 12116 + "version": "9.0.1", 12117 + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", 12118 + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", 12119 + "license": "MIT", 12120 + "dependencies": { 12121 + "vscode-languageserver-protocol": "3.17.5" 12122 + }, 12123 + "bin": { 12124 + "installServerIntoExtension": "bin/installServerIntoExtension" 12125 + } 12126 + }, 12127 + "node_modules/vscode-languageserver-protocol": { 12128 + "version": "3.17.5", 12129 + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", 12130 + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", 12131 + "license": "MIT", 12132 + "dependencies": { 12133 + "vscode-jsonrpc": "8.2.0", 12134 + "vscode-languageserver-types": "3.17.5" 12135 + } 12136 + }, 12137 + "node_modules/vscode-languageserver-textdocument": { 12138 + "version": "1.0.12", 12139 + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", 12140 + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", 12141 + "license": "MIT" 12142 + }, 12143 + "node_modules/vscode-languageserver-types": { 12144 + "version": "3.17.5", 12145 + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 12146 + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", 12147 + "license": "MIT" 12148 + }, 12149 + "node_modules/vscode-uri": { 12150 + "version": "3.1.0", 12151 + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", 12152 + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", 12153 + "license": "MIT" 10943 12154 }, 10944 12155 "node_modules/w3c-keyname": { 10945 12156 "version": "2.2.8",
+2 -1
package.json
··· 1 1 { 2 2 "name": "tools", 3 - "version": "0.36.1", 3 + "version": "0.37.0", 4 4 "private": true, 5 5 "type": "module", 6 6 "main": "electron/main.js", ··· 51 51 "lowlight": "^3.3.0", 52 52 "mammoth": "^1.12.0", 53 53 "markdown-it": "^14.1.1", 54 + "mermaid": "^11.14.0", 54 55 "pdfjs-dist": "^5.6.205", 55 56 "turndown": "^7.2.2", 56 57 "turndown-plugin-gfm": "^1.0.2",
+86
src/css/app.css
··· 7961 7961 font-style: italic; 7962 7962 } 7963 7963 7964 + /* ── Mermaid diagram blocks ──────────────────────────────────────────── */ 7965 + 7966 + .mermaid-block { 7967 + border: 1px solid var(--color-border); 7968 + border-radius: var(--radius-md); 7969 + overflow: hidden; 7970 + margin: var(--space-md) 0; 7971 + background: var(--color-surface-raised, var(--color-surface)); 7972 + } 7973 + 7974 + .mermaid-toolbar { 7975 + display: flex; 7976 + align-items: center; 7977 + gap: var(--space-xs); 7978 + padding: var(--space-xs) var(--space-sm); 7979 + background: var(--color-surface); 7980 + border-bottom: 1px solid var(--color-border); 7981 + font-size: 0.78rem; 7982 + } 7983 + 7984 + .mermaid-label { 7985 + font-weight: 600; 7986 + color: var(--color-text-secondary); 7987 + font-size: 0.75rem; 7988 + text-transform: uppercase; 7989 + letter-spacing: 0.04em; 7990 + flex: 1; 7991 + } 7992 + 7993 + .mermaid-btn { 7994 + padding: 2px 8px; 7995 + font-size: 0.78rem; 7996 + border-radius: var(--radius-sm); 7997 + border: 1px solid var(--color-border); 7998 + background: var(--color-surface-raised, var(--color-surface)); 7999 + color: var(--color-text); 8000 + cursor: pointer; 8001 + line-height: 1.4; 8002 + } 8003 + .mermaid-btn:hover { background: var(--color-teal); color: #fff; border-color: var(--color-teal); } 8004 + .mermaid-btn-danger:hover { background: var(--color-danger, #d94a4a); border-color: var(--color-danger, #d94a4a); } 8005 + 8006 + .mermaid-preview { 8007 + padding: var(--space-md); 8008 + display: flex; 8009 + justify-content: center; 8010 + min-height: 60px; 8011 + overflow-x: auto; 8012 + } 8013 + .mermaid-preview svg { max-width: 100%; height: auto; } 8014 + .mermaid-preview-loading { opacity: 0.5; } 8015 + .mermaid-preview-error { padding: var(--space-sm); } 8016 + 8017 + .mermaid-error { 8018 + background: color-mix(in oklch, var(--color-danger, #d94a4a) 10%, transparent); 8019 + border: 1px solid var(--color-danger, #d94a4a); 8020 + border-radius: var(--radius-sm); 8021 + padding: var(--space-sm); 8022 + color: var(--color-danger, #d94a4a); 8023 + font-size: 0.85rem; 8024 + width: 100%; 8025 + } 8026 + .mermaid-error pre { margin: var(--space-xs) 0 0; font-size: 0.78rem; white-space: pre-wrap; overflow-wrap: anywhere; } 8027 + 8028 + .mermaid-editor-wrap { 8029 + border-top: 1px solid var(--color-border); 8030 + } 8031 + .mermaid-code-input { 8032 + width: 100%; 8033 + box-sizing: border-box; 8034 + padding: var(--space-sm); 8035 + font-family: monospace; 8036 + font-size: 0.85rem; 8037 + background: var(--color-surface); 8038 + color: var(--color-text); 8039 + border: none; 8040 + outline: none; 8041 + resize: vertical; 8042 + min-height: 120px; 8043 + line-height: 1.5; 8044 + } 8045 + .mermaid-code-input:focus { background: var(--color-surface-raised, var(--color-surface)); } 8046 + 8047 + /* Dark mode: invert mermaid SVG text colours if using default theme */ 8048 + [data-theme="dark"] .mermaid-preview svg text { fill: var(--color-text) !important; } 8049 + 7964 8050 /* Version badge — z-index must exceed sticky cells (max 5) and sheet UI 7965 8051 but stay below modals/dialogs (1000+). */ 7966 8052 .version-badge {
+4 -2
src/diagrams/index.html
··· 38 38 <a class="app-logo" href="/">Tools</a> 39 39 <input class="doc-title-input" id="diagram-title" type="text" value="Untitled Diagram" spellcheck="false"> 40 40 <span class="topbar-spacer"></span> 41 - <!-- Export buttons --> 42 - <button class="btn-icon btn-sm" id="btn-export-svg" title="Export SVG">SVG</button> 41 + <!-- Import / Export buttons --> 42 + <button class="btn-icon btn-sm" id="btn-import-svg" title="Import SVG file">Import SVG</button> 43 + <input type="file" id="svg-import-input" accept=".svg,image/svg+xml" style="display:none"> 44 + <button class="btn-icon btn-sm" id="btn-export-svg" title="Export SVG">Export SVG</button> 43 45 <button class="btn-icon btn-sm" id="btn-export-png" title="Export PNG">PNG</button> 44 46 <button class="btn-icon" id="btn-ai-chat" title="AI Chat (Cmd+Shift+L)"> 45 47 <svg class="tb-icon" viewBox="0 0 16 16" style="width:16px;height:16px" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h12a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H5l-3 3V4a1 1 0 0 1 1-1z"/><circle cx="5.5" cy="7.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="8" cy="7.5" r="0.5" fill="currentColor" stroke="none"/><circle cx="10.5" cy="7.5" r="0.5" fill="currentColor" stroke="none"/></svg>
+344
src/diagrams/svg-import.ts
··· 1 + /** 2 + * SVG Import — parse an SVG file and convert its shapes to whiteboard elements. 3 + * 4 + * Supported SVG elements: 5 + * rect → rectangle shape 6 + * circle → ellipse shape (equal radii) 7 + * ellipse → ellipse shape 8 + * line → line shape 9 + * polyline / polygon → simplified as line between first and last point 10 + * text / tspan → text shape 11 + * g (group) → flattened (children extracted with group transform applied) 12 + * 13 + * Unsupported (silently skipped): 14 + * <path>, <image>, <use>, <defs>, <symbol>, clip-paths, gradients, masks. 15 + * These are too complex to convert faithfully to our shape model. 16 + * 17 + * Coordinate handling: 18 + * - All coordinates normalised to a 960×540 canvas using the SVG viewBox. 19 + * - If no viewBox, falls back to width/height attributes (or 800×600 default). 20 + * - Simple translate/scale transforms on <g> elements are applied to children. 21 + */ 22 + 23 + import type { Shape } from './whiteboard.js'; 24 + import type { ShapeKind } from './whiteboard-types.js'; 25 + 26 + interface BoundingBox { x: number; y: number; w: number; h: number; } 27 + 28 + // Target canvas size 29 + const TARGET_W = 960; 30 + const TARGET_H = 540; 31 + // Spread imported shapes relative to existing content 32 + const IMPORT_OFFSET = 20; 33 + 34 + let _idCounter = 0; 35 + function nextId(): string { 36 + return `svg-import-${Date.now()}-${++_idCounter}`; 37 + } 38 + 39 + // --------------------------------------------------------------------------- 40 + // Colour helpers 41 + // --------------------------------------------------------------------------- 42 + 43 + function cssColorToHex(color: string): string { 44 + if (!color || color === 'none' || color === 'transparent') return ''; 45 + if (color.startsWith('#')) return color.toLowerCase(); 46 + // Named colours → rough hex (common ones only) 47 + const named: Record<string, string> = { 48 + black: '#000000', white: '#ffffff', red: '#ff0000', green: '#008000', 49 + blue: '#0000ff', yellow: '#ffff00', gray: '#808080', grey: '#808080', 50 + orange: '#ffa500', purple: '#800080', pink: '#ffc0cb', cyan: '#00ffff', 51 + navy: '#000080', teal: '#008080', maroon: '#800000', lime: '#00ff00', 52 + aqua: '#00ffff', silver: '#c0c0c0', fuchsia: '#ff00ff', 53 + }; 54 + const n = named[color.toLowerCase()]; 55 + if (n) return n; 56 + // rgb(r,g,b) 57 + const rgb = color.match(/^rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/i); 58 + if (rgb) { 59 + const hex = (n: number) => n.toString(16).padStart(2, '0'); 60 + return `#${hex(parseInt(rgb[1]!))}${hex(parseInt(rgb[2]!))}${hex(parseInt(rgb[3]!))}`; 61 + } 62 + return '#888888'; // fallback 63 + } 64 + 65 + function parseStrokeWidth(val: string): number { 66 + const n = parseFloat(val); 67 + return isNaN(n) || n <= 0 ? 1 : Math.min(Math.max(n, 0.5), 20); 68 + } 69 + 70 + // --------------------------------------------------------------------------- 71 + // Simple transform parsing (translate / scale only) 72 + // --------------------------------------------------------------------------- 73 + 74 + interface SimpleTransform { tx: number; ty: number; sx: number; sy: number; } 75 + 76 + const IDENTITY: SimpleTransform = { tx: 0, ty: 0, sx: 1, sy: 1 }; 77 + 78 + function parseTransform(val: string): SimpleTransform { 79 + if (!val) return IDENTITY; 80 + let tx = 0, ty = 0, sx = 1, sy = 1; 81 + 82 + const tMatch = val.match(/translate\(\s*([-\d.]+)(?:\s*[, ]\s*([-\d.]+))?\s*\)/); 83 + if (tMatch) { 84 + tx = parseFloat(tMatch[1]!) || 0; 85 + ty = tMatch[2] !== undefined ? parseFloat(tMatch[2]) || 0 : 0; 86 + } 87 + 88 + const sMatch = val.match(/scale\(\s*([-\d.]+)(?:\s*[, ]\s*([-\d.]+))?\s*\)/); 89 + if (sMatch) { 90 + sx = parseFloat(sMatch[1]!) || 1; 91 + sy = sMatch[2] !== undefined ? parseFloat(sMatch[2]) || sx : sx; 92 + } 93 + 94 + return { tx, ty, sx, sy }; 95 + } 96 + 97 + function composeTransform(parent: SimpleTransform, child: SimpleTransform): SimpleTransform { 98 + return { 99 + tx: parent.tx + parent.sx * child.tx, 100 + ty: parent.ty + parent.sy * child.ty, 101 + sx: parent.sx * child.sx, 102 + sy: parent.sy * child.sy, 103 + }; 104 + } 105 + 106 + function applyTransform(tfm: SimpleTransform, x: number, y: number): { x: number; y: number } { 107 + return { x: tfm.tx + tfm.sx * x, y: tfm.ty + tfm.sy * y }; 108 + } 109 + 110 + // --------------------------------------------------------------------------- 111 + // SVG attribute helpers 112 + // --------------------------------------------------------------------------- 113 + 114 + function n(el: Element, name: string): number { 115 + const v = el.getAttribute(name); 116 + return v !== null ? parseFloat(v) || 0 : 0; 117 + } 118 + 119 + function s(el: Element, name: string): string { 120 + return el.getAttribute(name) ?? ''; 121 + } 122 + 123 + /** Read fill/stroke from both presentation attributes and inline style. */ 124 + function readStyle(el: Element): { fill: string; stroke: string; strokeWidth: number } { 125 + const style = el.getAttribute('style') ?? ''; 126 + const parseStyleProp = (prop: string): string => { 127 + const m = style.match(new RegExp(`${prop}\\s*:\\s*([^;]+)`)); 128 + return m ? m[1]!.trim() : ''; 129 + }; 130 + 131 + const fill = parseStyleProp('fill') || s(el, 'fill') || '#ffffff'; 132 + const stroke = parseStyleProp('stroke') || s(el, 'stroke') || '#000000'; 133 + const swStr = parseStyleProp('stroke-width') || s(el, 'stroke-width') || '1'; 134 + return { fill: cssColorToHex(fill), stroke: cssColorToHex(stroke), strokeWidth: parseStrokeWidth(swStr) }; 135 + } 136 + 137 + // --------------------------------------------------------------------------- 138 + // ViewBox → pixel converters 139 + // --------------------------------------------------------------------------- 140 + 141 + interface ViewBox { x: number; y: number; w: number; h: number; } 142 + 143 + function getViewBox(svg: SVGSVGElement): ViewBox { 144 + const vb = svg.getAttribute('viewBox'); 145 + if (vb) { 146 + const parts = vb.trim().split(/[\s,]+/).map(parseFloat); 147 + if (parts.length >= 4) return { x: parts[0]!, y: parts[1]!, w: parts[2]!, h: parts[3]! }; 148 + } 149 + const w = parseFloat(svg.getAttribute('width') ?? '') || 800; 150 + const h = parseFloat(svg.getAttribute('height') ?? '') || 600; 151 + return { x: 0, y: 0, w, h }; 152 + } 153 + 154 + function makeScalers(vb: ViewBox) { 155 + return { 156 + x: (v: number) => Math.round(((v - vb.x) / vb.w) * TARGET_W), 157 + y: (v: number) => Math.round(((v - vb.y) / vb.h) * TARGET_H), 158 + w: (v: number) => Math.max(10, Math.round((v / vb.w) * TARGET_W)), 159 + h: (v: number) => Math.max(10, Math.round((v / vb.h) * TARGET_H)), 160 + }; 161 + } 162 + 163 + // --------------------------------------------------------------------------- 164 + // Element → Shape conversion 165 + // --------------------------------------------------------------------------- 166 + 167 + type Scalers = ReturnType<typeof makeScalers>; 168 + 169 + function makeShape( 170 + kind: ShapeKind, 171 + bbox: BoundingBox, 172 + styleProps: { fill: string; stroke: string; strokeWidth: number }, 173 + label: string, 174 + ): Shape { 175 + return { 176 + id: nextId(), 177 + kind, 178 + x: bbox.x, 179 + y: bbox.y, 180 + width: Math.max(bbox.w, 10), 181 + height: Math.max(bbox.h, 10), 182 + label, 183 + rotation: 0, 184 + opacity: 1, 185 + style: { 186 + fill: styleProps.fill || '#ffffff', 187 + stroke: styleProps.stroke || '#000000', 188 + strokeWidth: String(styleProps.strokeWidth), 189 + }, 190 + }; 191 + } 192 + 193 + function convertRect(el: Element, sc: Scalers, tfm: SimpleTransform, z: number): Shape | null { 194 + const rawX = n(el, 'x'), rawY = n(el, 'y'); 195 + const { x, y } = applyTransform(tfm, rawX, rawY); 196 + const w = n(el, 'width') * tfm.sx, h = n(el, 'height') * tfm.sy; 197 + if (w <= 0 || h <= 0) return null; 198 + return makeShape('rectangle', { x: sc.x(x), y: sc.y(y), w: sc.w(w), h: sc.h(h) }, readStyle(el), ''); 199 + } 200 + 201 + function convertCircle(el: Element, sc: Scalers, tfm: SimpleTransform, z: number): Shape | null { 202 + const rawCx = n(el, 'cx'), rawCy = n(el, 'cy'), r = n(el, 'r') * tfm.sx; 203 + if (r <= 0) return null; 204 + const { x: cx, y: cy } = applyTransform(tfm, rawCx, rawCy); 205 + return makeShape('ellipse', { x: sc.x(cx - r), y: sc.y(cy - r), w: sc.w(r * 2), h: sc.h(r * 2) }, readStyle(el), ''); 206 + } 207 + 208 + function convertEllipse(el: Element, sc: Scalers, tfm: SimpleTransform, z: number): Shape | null { 209 + const rawCx = n(el, 'cx'), rawCy = n(el, 'cy'); 210 + const rx = n(el, 'rx') * tfm.sx, ry = n(el, 'ry') * tfm.sy; 211 + if (rx <= 0 || ry <= 0) return null; 212 + const { x: cx, y: cy } = applyTransform(tfm, rawCx, rawCy); 213 + return makeShape('ellipse', { x: sc.x(cx - rx), y: sc.y(cy - ry), w: sc.w(rx * 2), h: sc.h(ry * 2) }, readStyle(el), ''); 214 + } 215 + 216 + function convertLine(el: Element, sc: Scalers, tfm: SimpleTransform, z: number): Shape | null { 217 + const { x: x1, y: y1 } = applyTransform(tfm, n(el, 'x1'), n(el, 'y1')); 218 + const { x: x2, y: y2 } = applyTransform(tfm, n(el, 'x2'), n(el, 'y2')); 219 + const px1 = sc.x(x1), py1 = sc.y(y1), px2 = sc.x(x2), py2 = sc.y(y2); 220 + const bx = Math.min(px1, px2), by = Math.min(py1, py2); 221 + const bw = Math.abs(px2 - px1), bh = Math.abs(py2 - py1); 222 + const style = readStyle(el); 223 + if (bw < 2 && bh < 2) return null; 224 + return makeShape('line', { x: bx, y: by, w: Math.max(bw, 2), h: Math.max(bh, 2) }, style, ''); 225 + } 226 + 227 + function convertText(el: Element, sc: Scalers, tfm: SimpleTransform, z: number): Shape | null { 228 + const rawX = n(el, 'x'), rawY = n(el, 'y'); 229 + const { x, y } = applyTransform(tfm, rawX, rawY); 230 + const text = el.textContent?.trim() ?? ''; 231 + if (!text) return null; 232 + const fontSize = parseFloat(el.getAttribute('font-size') ?? '') || 16; 233 + const approxW = Math.max(text.length * fontSize * 0.6, 80); 234 + const approxH = fontSize * 1.6; 235 + const fontColor = cssColorToHex(el.getAttribute('fill') ?? '#000000') || '#000000'; 236 + return { 237 + ...makeShape('text', { x: sc.x(x), y: sc.y(y - fontSize), w: sc.w(approxW), h: sc.h(approxH) }, { fill: 'transparent', stroke: 'none', strokeWidth: 0 }, text), 238 + fontSize: sc.h(fontSize), 239 + style: { fill: 'transparent', stroke: 'none', strokeWidth: '0', fontColor }, 240 + }; 241 + } 242 + 243 + // --------------------------------------------------------------------------- 244 + // Group element — recurse with composed transform 245 + // --------------------------------------------------------------------------- 246 + 247 + function extractFromElement(el: Element, sc: Scalers, tfm: SimpleTransform, zBase: number): Shape[] { 248 + const shapes: Shape[] = []; 249 + let z = zBase; 250 + 251 + const tagName = el.tagName.toLowerCase().replace(/^svg:/, ''); 252 + 253 + if (tagName === 'g') { 254 + const childTfm = composeTransform(tfm, parseTransform(el.getAttribute('transform') ?? '')); 255 + for (const child of Array.from(el.children)) { 256 + const childShapes = extractFromElement(child, sc, childTfm, z); 257 + shapes.push(...childShapes); 258 + z += childShapes.length; 259 + } 260 + } else { 261 + const elTfm = composeTransform(tfm, parseTransform(el.getAttribute('transform') ?? '')); 262 + let shape: Shape | null = null; 263 + 264 + if (tagName === 'rect') shape = convertRect(el, sc, elTfm, z); 265 + else if (tagName === 'circle') shape = convertCircle(el, sc, elTfm, z); 266 + else if (tagName === 'ellipse') shape = convertEllipse(el, sc, elTfm, z); 267 + else if (tagName === 'line') shape = convertLine(el, sc, elTfm, z); 268 + else if (tagName === 'text') shape = convertText(el, sc, elTfm, z); 269 + else if (tagName === 'polyline' || tagName === 'polygon') { 270 + // Approximate as bounding box rectangle 271 + const pts = (el.getAttribute('points') ?? '').trim().split(/[\s,]+/).map(parseFloat).filter(n => !isNaN(n)); 272 + if (pts.length >= 4) { 273 + const xs = pts.filter((_, i) => i % 2 === 0).map(v => v * elTfm.sx + elTfm.tx); 274 + const ys = pts.filter((_, i) => i % 2 === 1).map(v => v * elTfm.sy + elTfm.ty); 275 + const minX = Math.min(...xs), minY = Math.min(...ys); 276 + const maxX = Math.max(...xs), maxY = Math.max(...ys); 277 + shape = makeShape('rectangle', { x: sc.x(minX), y: sc.y(minY), w: sc.w(maxX - minX), h: sc.h(maxY - minY) }, readStyle(el), ''); 278 + } 279 + } 280 + // path, image, use, defs, symbol → skipped 281 + 282 + if (shape) shapes.push(shape); 283 + } 284 + 285 + return shapes; 286 + } 287 + 288 + // --------------------------------------------------------------------------- 289 + // Public API 290 + // --------------------------------------------------------------------------- 291 + 292 + export interface SvgImportResult { 293 + shapes: Shape[]; 294 + /** Number of SVG elements that were skipped (unsupported types). */ 295 + skippedCount: number; 296 + /** Total SVG elements encountered (excluding containers). */ 297 + totalCount: number; 298 + } 299 + 300 + /** 301 + * Parse an SVG string and return whiteboard shapes. 302 + * Returns empty shapes array (not an error) for unparseable/empty input. 303 + */ 304 + export function parseSvgToShapes(svgText: string): SvgImportResult { 305 + const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); 306 + const parseError = doc.querySelector('parsererror'); 307 + if (parseError) return { shapes: [], skippedCount: 0, totalCount: 0 }; 308 + 309 + const svgEl = doc.documentElement as unknown as SVGSVGElement; 310 + if (svgEl.tagName.toLowerCase() !== 'svg' && !svgEl.tagName.endsWith('svg')) { 311 + return { shapes: [], skippedCount: 0, totalCount: 0 }; 312 + } 313 + 314 + const vb = getViewBox(svgEl); 315 + const sc = makeScalers(vb); 316 + 317 + // Count total meaningful elements for stats 318 + const COUNTABLE = new Set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'text', 'path', 'image']); 319 + let totalCount = 0; 320 + const countAll = (el: Element) => { 321 + const tag = el.tagName.toLowerCase().replace(/^svg:/, ''); 322 + if (COUNTABLE.has(tag)) totalCount++; 323 + for (const c of Array.from(el.children)) countAll(c); 324 + }; 325 + countAll(svgEl); 326 + 327 + const shapes: Shape[] = []; 328 + const rootTfm = parseTransform(svgEl.getAttribute('transform') ?? ''); 329 + 330 + for (const child of Array.from(svgEl.children)) { 331 + const tag = child.tagName.toLowerCase().replace(/^svg:/, ''); 332 + if (tag === 'defs' || tag === 'style' || tag === 'title' || tag === 'desc') continue; 333 + const extracted = extractFromElement(child, sc, rootTfm, shapes.length); 334 + shapes.push(...extracted); 335 + } 336 + 337 + // Offset imported shapes so they don't land exactly on existing content 338 + for (const shape of shapes) { 339 + shape.x += IMPORT_OFFSET; 340 + shape.y += IMPORT_OFFSET; 341 + } 342 + 343 + return { shapes, skippedCount: totalCount - shapes.length, totalCount }; 344 + }
+32
src/diagrams/toolbar-wiring.ts
··· 18 18 } from './whiteboard.js'; 19 19 import type { WhiteboardState, Shape, Arrow } from './whiteboard.js'; 20 20 import { exportAndDownloadSVG, exportAndDownloadPNG } from './export.js'; 21 + import { parseSvgToShapes } from './svg-import.js'; 21 22 22 23 // --------------------------------------------------------------------------- 23 24 // Deps interface ··· 348 349 deps.setState(ungroupShapes(wb, shape.groupId)); 349 350 deps.syncToYjs(); 350 351 deps.render(); 352 + }); 353 + 354 + // SVG import 355 + const svgImportInput = document.getElementById('svg-import-input') as HTMLInputElement | null; 356 + $('btn-import-svg')?.addEventListener('click', () => svgImportInput?.click()); 357 + svgImportInput?.addEventListener('change', async () => { 358 + const file = svgImportInput.files?.[0]; 359 + if (!file) return; 360 + svgImportInput.value = ''; 361 + try { 362 + const text = await file.text(); 363 + const { shapes, skippedCount } = parseSvgToShapes(text); 364 + if (shapes.length === 0) { 365 + alert('No supported shapes found in SVG. Only rect, circle, ellipse, line, polygon, and text elements are imported.'); 366 + return; 367 + } 368 + deps.pushHistory(); 369 + const state = deps.getState(); 370 + for (const shape of shapes) { 371 + state.shapes.set(shape.id, shape); 372 + } 373 + deps.setState({ ...state }); 374 + deps.syncToYjs(); 375 + deps.render(); 376 + if (skippedCount > 0) { 377 + console.info(`SVG import: ${shapes.length} shapes imported, ${skippedCount} elements skipped (paths, images, etc.)`); 378 + } 379 + } catch (err) { 380 + console.error('SVG import failed:', err); 381 + alert('Failed to import SVG file.'); 382 + } 351 383 }); 352 384 353 385 // Export buttons
+276
src/docs/extensions/mermaid-block.ts
··· 1 + /** 2 + * MermaidBlock TipTap extension — inline diagram blocks for docs. 3 + * 4 + * Renders a Mermaid diagram (flowcharts, sequence diagrams, ER, Gantt, etc.) 5 + * inline within a document. The Mermaid source code is stored as a node 6 + * attribute and is fully E2EE-compatible (plain text in Yjs). 7 + * 8 + * UX: 9 + * - Default state: rendered SVG diagram only 10 + * - Click the edit button (or double-click diagram): shows code editor below 11 + * - Code editor changes re-render with 400ms debounce 12 + * - Error state shows the Mermaid error message 13 + * - Copy SVG and download SVG buttons in the toolbar 14 + * 15 + * Mermaid is dynamically imported on first use to keep the initial bundle small. 16 + */ 17 + 18 + import { Node, mergeAttributes } from '@tiptap/core'; 19 + import type { Editor } from '@tiptap/core'; 20 + 21 + declare module '@tiptap/core' { 22 + interface Commands<ReturnType> { 23 + mermaidDiagram: { 24 + insertMermaidDiagram: (options?: { code?: string }) => ReturnType; 25 + }; 26 + } 27 + } 28 + 29 + const DEFAULT_CODE = `flowchart TD 30 + A[Start] --> B{Decision} 31 + B -->|Yes| C[Do something] 32 + B -->|No| D[Do something else] 33 + C --> E[End] 34 + D --> E`; 35 + 36 + let _diagramCounter = 0; 37 + function nextId(): string { 38 + return `mermaid-render-${++_diagramCounter}`; 39 + } 40 + 41 + /** Dynamically import and initialise Mermaid once. */ 42 + let _mermaidReady = false; 43 + let _mermaidLib: typeof import('mermaid').default | null = null; 44 + 45 + async function getMermaid(): Promise<typeof import('mermaid').default> { 46 + if (_mermaidLib) return _mermaidLib; 47 + const mod = await import('mermaid'); 48 + _mermaidLib = mod.default; 49 + if (!_mermaidReady) { 50 + const dark = document.documentElement.getAttribute('data-theme') === 'dark'; 51 + _mermaidLib.initialize({ 52 + startOnLoad: false, 53 + securityLevel: 'strict', 54 + theme: dark ? 'dark' : 'default', 55 + }); 56 + _mermaidReady = true; 57 + } 58 + return _mermaidLib; 59 + } 60 + 61 + /** Render Mermaid source to SVG string. Returns SVG or throws on syntax error. */ 62 + async function renderToSvg(code: string): Promise<string> { 63 + const mermaid = await getMermaid(); 64 + const id = nextId(); 65 + // mermaid.render creates a temporary element, renders, returns svg string. 66 + const { svg } = await mermaid.render(id, code); 67 + // Clean up any leftover element mermaid may have inserted 68 + document.getElementById(id)?.remove(); 69 + return svg; 70 + } 71 + 72 + function escHtml(s: string): string { 73 + return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); 74 + } 75 + 76 + export const MermaidBlock = Node.create({ 77 + name: 'mermaidDiagram', 78 + group: 'block', 79 + atom: true, 80 + draggable: true, 81 + selectable: true, 82 + 83 + addOptions() { 84 + return {}; 85 + }, 86 + 87 + addAttributes() { 88 + return { 89 + code: { 90 + default: DEFAULT_CODE, 91 + parseHTML: (el: HTMLElement) => el.getAttribute('data-code') ?? DEFAULT_CODE, 92 + renderHTML: (attrs: Record<string, string>) => ({ 'data-code': attrs['code'] ?? DEFAULT_CODE }), 93 + }, 94 + }; 95 + }, 96 + 97 + parseHTML() { 98 + return [{ tag: 'div[data-type="mermaid-diagram"]' }]; 99 + }, 100 + 101 + renderHTML({ HTMLAttributes }: { HTMLAttributes: Record<string, string> }) { 102 + return ['div', mergeAttributes({ 'data-type': 'mermaid-diagram' }, HTMLAttributes)]; 103 + }, 104 + 105 + addCommands() { 106 + return { 107 + insertMermaidDiagram: 108 + (options: { code?: string } = {}) => 109 + ({ commands }: { commands: Editor['commands'] }) => { 110 + return commands.insertContent({ 111 + type: this.name, 112 + attrs: { code: options.code ?? DEFAULT_CODE }, 113 + }); 114 + }, 115 + }; 116 + }, 117 + 118 + addNodeView() { 119 + return ({ node, getPos, editor }: { node: any; getPos: () => number; editor: Editor }) => { 120 + let currentCode: string = node.attrs.code ?? DEFAULT_CODE; 121 + let editOpen = false; 122 + let renderTimer: ReturnType<typeof setTimeout> | null = null; 123 + 124 + // --- DOM structure --- 125 + const dom = document.createElement('div'); 126 + dom.className = 'mermaid-block'; 127 + dom.setAttribute('data-mermaid-block', ''); 128 + 129 + // Toolbar 130 + const toolbar = document.createElement('div'); 131 + toolbar.className = 'mermaid-toolbar'; 132 + toolbar.innerHTML = ` 133 + <span class="mermaid-label">Diagram</span> 134 + <button class="mermaid-btn" data-action="edit" title="Edit diagram code">Edit</button> 135 + <button class="mermaid-btn" data-action="copy-svg" title="Copy SVG to clipboard">Copy SVG</button> 136 + <button class="mermaid-btn mermaid-btn-danger" data-action="delete" title="Delete diagram">&#10006;</button> 137 + `; 138 + 139 + // Preview 140 + const preview = document.createElement('div'); 141 + preview.className = 'mermaid-preview'; 142 + preview.setAttribute('aria-label', 'Mermaid diagram preview'); 143 + 144 + // Code editor (collapsible) 145 + const editorWrap = document.createElement('div'); 146 + editorWrap.className = 'mermaid-editor-wrap'; 147 + editorWrap.hidden = true; 148 + 149 + const textarea = document.createElement('textarea'); 150 + textarea.className = 'mermaid-code-input'; 151 + textarea.spellcheck = false; 152 + textarea.rows = 6; 153 + textarea.value = currentCode; 154 + textarea.setAttribute('aria-label', 'Mermaid diagram source code'); 155 + 156 + editorWrap.appendChild(textarea); 157 + dom.appendChild(toolbar); 158 + dom.appendChild(preview); 159 + dom.appendChild(editorWrap); 160 + 161 + // --- Rendering --- 162 + async function render(code: string): Promise<void> { 163 + preview.classList.add('mermaid-preview-loading'); 164 + try { 165 + const svg = await renderToSvg(code); 166 + preview.innerHTML = svg; 167 + preview.classList.remove('mermaid-preview-loading', 'mermaid-preview-error'); 168 + // Make SVG responsive 169 + const svgEl = preview.querySelector('svg'); 170 + if (svgEl) { 171 + svgEl.style.maxWidth = '100%'; 172 + svgEl.style.height = 'auto'; 173 + } 174 + } catch (err: unknown) { 175 + const msg = err instanceof Error ? err.message : String(err); 176 + preview.innerHTML = `<div class="mermaid-error"><strong>Diagram error</strong><pre>${escHtml(msg)}</pre></div>`; 177 + preview.classList.add('mermaid-preview-error'); 178 + preview.classList.remove('mermaid-preview-loading'); 179 + } 180 + } 181 + 182 + function scheduleRender(code: string): void { 183 + if (renderTimer) clearTimeout(renderTimer); 184 + renderTimer = setTimeout(() => render(code), 400); 185 + } 186 + 187 + function updateNodeCode(code: string): void { 188 + if (typeof getPos !== 'function') return; 189 + const pos = getPos(); 190 + editor.view.dispatch( 191 + editor.view.state.tr.setNodeMarkup(pos, undefined, { ...node.attrs, code }) 192 + ); 193 + } 194 + 195 + function toggleEdit(): void { 196 + editOpen = !editOpen; 197 + editorWrap.hidden = !editOpen; 198 + const btn = toolbar.querySelector('[data-action="edit"]') as HTMLButtonElement | null; 199 + if (btn) btn.textContent = editOpen ? 'Done' : 'Edit'; 200 + if (editOpen) { 201 + textarea.focus(); 202 + textarea.select(); 203 + } 204 + } 205 + 206 + // --- Events --- 207 + toolbar.addEventListener('click', (e) => { 208 + const btn = (e.target as HTMLElement).closest('[data-action]') as HTMLElement | null; 209 + if (!btn) return; 210 + const action = btn.dataset.action; 211 + 212 + if (action === 'edit') { 213 + toggleEdit(); 214 + } else if (action === 'copy-svg') { 215 + const svgEl = preview.querySelector('svg'); 216 + if (svgEl) { 217 + navigator.clipboard.writeText(svgEl.outerHTML).catch(() => {}); 218 + } 219 + } else if (action === 'delete') { 220 + if (typeof getPos === 'function') { 221 + const pos = getPos(); 222 + editor.view.dispatch( 223 + editor.view.state.tr.delete(pos, pos + node.nodeSize) 224 + ); 225 + } 226 + } 227 + }); 228 + 229 + textarea.addEventListener('input', () => { 230 + currentCode = textarea.value; 231 + scheduleRender(currentCode); 232 + }); 233 + 234 + // Commit code to Yjs when textarea loses focus 235 + textarea.addEventListener('blur', () => { 236 + if (currentCode !== (node.attrs.code ?? DEFAULT_CODE)) { 237 + updateNodeCode(currentCode); 238 + } 239 + }); 240 + 241 + // Prevent TipTap from capturing keyboard events inside the textarea 242 + textarea.addEventListener('keydown', (e) => { 243 + e.stopPropagation(); 244 + }); 245 + 246 + // Double-click diagram to open editor 247 + preview.addEventListener('dblclick', () => { 248 + if (!editOpen) toggleEdit(); 249 + }); 250 + 251 + // Initial render 252 + render(currentCode); 253 + 254 + return { 255 + dom, 256 + update(updatedNode: any): boolean { 257 + if (updatedNode.type.name !== 'mermaidDiagram') return false; 258 + const newCode = updatedNode.attrs.code ?? DEFAULT_CODE; 259 + if (newCode !== currentCode) { 260 + currentCode = newCode; 261 + textarea.value = currentCode; 262 + scheduleRender(currentCode); 263 + } 264 + return true; 265 + }, 266 + destroy(): void { 267 + if (renderTimer) clearTimeout(renderTimer); 268 + }, 269 + stopEvent(event: Event): boolean { 270 + // Let textarea handle its own events without TipTap interfering 271 + return event.target === textarea; 272 + }, 273 + }; 274 + }; 275 + }, 276 + });
+3
src/docs/extensions/slash-commands.ts
··· 112 112 horizontalRule: (editor: Editor) => { 113 113 editor.chain().focus().setHorizontalRule().run(); 114 114 }, 115 + mermaid: (editor: Editor) => { 116 + editor.chain().focus().insertMermaidDiagram().run(); 117 + }, 115 118 codeBlock: (editor: Editor) => { 116 119 editor.chain().focus().toggleCodeBlock().run(); 117 120 },
+2
src/docs/main.ts
··· 59 59 import { ZenModeState, ZEN_STORAGE_KEY, ZEN_CLASS } from './zen-mode.js'; 60 60 import { filterCommands, PLACEHOLDER_EMPTY, PLACEHOLDER_BLOCK } from './slash-menu.js'; 61 61 import { createSlashCommands, getCommandExecutor } from './extensions/slash-commands.js'; 62 + import { MermaidBlock } from './extensions/mermaid-block.js'; 62 63 import { createCommandPalette, type PaletteAction } from '../command-palette.js'; 63 64 64 65 // --- Extracted modules (phase 1) --- ··· 217 218 TabSupport, 218 219 MarkdownAutoformat, 219 220 WikiLink, 221 + MermaidBlock, 220 222 createSlashCommands({ 221 223 items: (query) => { 222 224 return filterCommands(query).map(item => ({
+8
src/docs/slash-menu.ts
··· 122 122 123 123 // --- Code --- 124 124 { 125 + id: 'mermaid', 126 + name: 'Diagram', 127 + description: 'Mermaid flowchart, sequence, ER, or Gantt diagram', 128 + category: 'code', 129 + icon: '\u25C7', 130 + shortcut: null, 131 + }, 132 + { 125 133 id: 'codeBlock', 126 134 name: 'Code Block', 127 135 description: 'Fenced code block with syntax highlighting',
+163
tests/svg-import.test.ts
··· 1 + /** 2 + * Tests for SVG import — parseSvgToShapes. 3 + * Uses jsdom for DOMParser. 4 + */ 5 + // @vitest-environment jsdom 6 + 7 + import { describe, it, expect } from 'vitest'; 8 + import { parseSvgToShapes } from '../src/diagrams/svg-import.js'; 9 + 10 + function svg(inner: string, viewBox = '0 0 800 600', extra = ''): string { 11 + return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" ${extra}>${inner}</svg>`; 12 + } 13 + 14 + describe('parseSvgToShapes', () => { 15 + it('returns empty for invalid XML', () => { 16 + const r = parseSvgToShapes('<not xml >>'); 17 + expect(r.shapes).toHaveLength(0); 18 + }); 19 + 20 + it('returns empty for non-SVG XML', () => { 21 + const r = parseSvgToShapes('<html><body></body></html>'); 22 + expect(r.shapes).toHaveLength(0); 23 + }); 24 + 25 + it('returns empty shapes for SVG with no convertible elements', () => { 26 + const r = parseSvgToShapes(svg('<defs/><path d="M0 0 L10 10"/><image href="x.png"/>')); 27 + expect(r.shapes).toHaveLength(0); 28 + }); 29 + 30 + it('converts rect to rectangle shape', () => { 31 + const r = parseSvgToShapes(svg('<rect x="100" y="50" width="200" height="100" fill="#ff0000" stroke="#000000"/>')); 32 + expect(r.shapes).toHaveLength(1); 33 + expect(r.shapes[0]!.kind).toBe('rectangle'); 34 + expect(r.shapes[0]!.style['fill']).toBe('#ff0000'); 35 + }); 36 + 37 + it('converts circle to ellipse shape', () => { 38 + const r = parseSvgToShapes(svg('<circle cx="400" cy="300" r="100" fill="#0000ff"/>')); 39 + expect(r.shapes).toHaveLength(1); 40 + expect(r.shapes[0]!.kind).toBe('ellipse'); 41 + }); 42 + 43 + it('converts ellipse to ellipse shape', () => { 44 + const r = parseSvgToShapes(svg('<ellipse cx="400" cy="300" rx="150" ry="80"/>')); 45 + expect(r.shapes).toHaveLength(1); 46 + expect(r.shapes[0]!.kind).toBe('ellipse'); 47 + }); 48 + 49 + it('converts line to line shape', () => { 50 + const r = parseSvgToShapes(svg('<line x1="0" y1="0" x2="400" y2="300"/>')); 51 + expect(r.shapes).toHaveLength(1); 52 + expect(r.shapes[0]!.kind).toBe('line'); 53 + }); 54 + 55 + it('converts text element with content', () => { 56 + const r = parseSvgToShapes(svg('<text x="100" y="100">Hello World</text>')); 57 + expect(r.shapes).toHaveLength(1); 58 + expect(r.shapes[0]!.kind).toBe('text'); 59 + expect(r.shapes[0]!.label).toBe('Hello World'); 60 + }); 61 + 62 + it('skips text elements with empty content', () => { 63 + const r = parseSvgToShapes(svg('<text x="100" y="100"> </text>')); 64 + expect(r.shapes).toHaveLength(0); 65 + }); 66 + 67 + it('converts polygon by bounding box', () => { 68 + const r = parseSvgToShapes(svg('<polygon points="0,0 100,0 100,100 0,100"/>')); 69 + expect(r.shapes).toHaveLength(1); 70 + expect(r.shapes[0]!.kind).toBe('rectangle'); 71 + }); 72 + 73 + it('handles multiple shapes', () => { 74 + const r = parseSvgToShapes(svg(` 75 + <rect x="0" y="0" width="100" height="100"/> 76 + <circle cx="200" cy="200" r="50"/> 77 + <text x="300" y="300">Label</text> 78 + `)); 79 + expect(r.shapes).toHaveLength(3); 80 + }); 81 + 82 + it('flattens group shapes into individual elements', () => { 83 + const r = parseSvgToShapes(svg(` 84 + <g transform="translate(50,50)"> 85 + <rect x="0" y="0" width="100" height="80"/> 86 + <circle cx="150" cy="50" r="30"/> 87 + </g> 88 + `)); 89 + expect(r.shapes).toHaveLength(2); 90 + }); 91 + 92 + it('applies group translate transform to children', () => { 93 + const r = parseSvgToShapes(svg('<g transform="translate(400,300)"><rect x="0" y="0" width="100" height="100"/></g>')); 94 + expect(r.shapes).toHaveLength(1); 95 + // x should be > 0 due to translate(400,300) applied 96 + expect(r.shapes[0]!.x).toBeGreaterThan(0); 97 + }); 98 + 99 + it('normalises coordinates to canvas bounds', () => { 100 + const r = parseSvgToShapes(svg('<rect x="0" y="0" width="800" height="600"/>')); 101 + expect(r.shapes[0]!.width).toBeLessThanOrEqual(960); 102 + expect(r.shapes[0]!.height).toBeLessThanOrEqual(540); 103 + }); 104 + 105 + it('uses viewBox for coordinate scaling', () => { 106 + // viewBox 0 0 100 100 — rect at 50,50,100,100 → approx half canvas 107 + const r = parseSvgToShapes(svg('<rect x="0" y="0" width="100" height="100"/>', '0 0 100 100')); 108 + expect(r.shapes[0]!.width).toBeCloseTo(960, -2); 109 + expect(r.shapes[0]!.height).toBeCloseTo(540, -2); 110 + }); 111 + 112 + it('falls back to width/height if no viewBox', () => { 113 + const r = parseSvgToShapes(`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300"> 114 + <rect x="0" y="0" width="400" height="300"/> 115 + </svg>`); 116 + expect(r.shapes).toHaveLength(1); 117 + }); 118 + 119 + it('parses named CSS colors', () => { 120 + const r = parseSvgToShapes(svg('<rect x="0" y="0" width="100" height="100" fill="red" stroke="blue"/>')); 121 + expect(r.shapes[0]!.style['fill']).toBe('#ff0000'); 122 + expect(r.shapes[0]!.style['stroke']).toBe('#0000ff'); 123 + }); 124 + 125 + it('parses rgb() color values', () => { 126 + const r = parseSvgToShapes(svg('<rect x="0" y="0" width="100" height="100" fill="rgb(255,128,0)"/>')); 127 + expect(r.shapes[0]!.style['fill']).toBe('#ff8000'); 128 + }); 129 + 130 + it('skips zero-size circles', () => { 131 + const r = parseSvgToShapes(svg('<circle cx="100" cy="100" r="0"/>')); 132 + expect(r.shapes).toHaveLength(0); 133 + }); 134 + 135 + it('skips zero-size rects', () => { 136 + const r = parseSvgToShapes(svg('<rect x="0" y="0" width="0" height="100"/>')); 137 + expect(r.shapes).toHaveLength(0); 138 + }); 139 + 140 + it('offsets imported shapes by IMPORT_OFFSET to avoid overlap', () => { 141 + const r = parseSvgToShapes(svg('<rect x="0" y="0" width="100" height="100"/>')); 142 + expect(r.shapes[0]!.x).toBeGreaterThan(0); 143 + expect(r.shapes[0]!.y).toBeGreaterThan(0); 144 + }); 145 + 146 + it('reports skipped count for unsupported elements', () => { 147 + const r = parseSvgToShapes(svg('<path d="M0 0 L10 10"/><rect x="0" y="0" width="50" height="50"/>')); 148 + expect(r.skippedCount).toBeGreaterThanOrEqual(1); // path skipped 149 + expect(r.shapes).toHaveLength(1); 150 + }); 151 + 152 + it('handles nested groups recursively', () => { 153 + const r = parseSvgToShapes(svg(` 154 + <g> 155 + <g> 156 + <rect x="10" y="10" width="50" height="50"/> 157 + </g> 158 + <circle cx="200" cy="200" r="40"/> 159 + </g> 160 + `)); 161 + expect(r.shapes).toHaveLength(2); 162 + }); 163 + });