feat: render interactive Mermaid diagrams
This commit is contained in:
Generated
+646
-1
@@ -15,12 +15,14 @@
|
||||
"@wecom/aibot-node-sdk": "^1.0.6",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"dingtalk-stream": "^2.1.6-beta.1",
|
||||
"dompurify": "^3.4.13",
|
||||
"fflate": "^0.8.3",
|
||||
"html-to-text": "^10.0.0",
|
||||
"i18next": "^25.10.10",
|
||||
"json5": "^2.2.3",
|
||||
"katex": "^0.16.47",
|
||||
"lucide-react": "^1.27.0",
|
||||
"mermaid": "^11.16.1",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
"pdfjs-dist": "^6.2.108",
|
||||
"ppu-paddle-ocr": "^6.4.0",
|
||||
@@ -76,6 +78,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@antfu/install-pkg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
|
||||
"integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"package-manager-detector": "^1.3.0",
|
||||
"tinyexec": "^1.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/algorithm": {
|
||||
"version": "0.1.26",
|
||||
"resolved": "https://registry.npmjs.org/@antv/algorithm/-/algorithm-0.1.26.tgz",
|
||||
@@ -671,6 +686,12 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@braintree/sanitize-url": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
|
||||
"integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
@@ -684,6 +705,12 @@
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@chevrotain/types": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz",
|
||||
"integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@continuedev/cli": {
|
||||
"version": "1.5.47",
|
||||
"resolved": "https://registry.npmjs.org/@continuedev/cli/-/cli-1.5.47.tgz",
|
||||
@@ -1929,6 +1956,23 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@iconify/types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
|
||||
"integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@iconify/utils": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz",
|
||||
"integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antfu/install-pkg": "^1.1.0",
|
||||
"@iconify/types": "^2.0.0",
|
||||
"import-meta-resolve": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.33.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
|
||||
@@ -2285,6 +2329,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mermaid-js/parser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz",
|
||||
"integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@chevrotain/types": "~11.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
|
||||
@@ -3337,24 +3390,111 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/d3-axis": "*",
|
||||
"@types/d3-brush": "*",
|
||||
"@types/d3-chord": "*",
|
||||
"@types/d3-color": "*",
|
||||
"@types/d3-contour": "*",
|
||||
"@types/d3-delaunay": "*",
|
||||
"@types/d3-dispatch": "*",
|
||||
"@types/d3-drag": "*",
|
||||
"@types/d3-dsv": "*",
|
||||
"@types/d3-ease": "*",
|
||||
"@types/d3-fetch": "*",
|
||||
"@types/d3-force": "*",
|
||||
"@types/d3-format": "*",
|
||||
"@types/d3-geo": "*",
|
||||
"@types/d3-hierarchy": "*",
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-path": "*",
|
||||
"@types/d3-polygon": "*",
|
||||
"@types/d3-quadtree": "*",
|
||||
"@types/d3-random": "*",
|
||||
"@types/d3-scale": "*",
|
||||
"@types/d3-scale-chromatic": "*",
|
||||
"@types/d3-selection": "*",
|
||||
"@types/d3-shape": "*",
|
||||
"@types/d3-time": "*",
|
||||
"@types/d3-time-format": "*",
|
||||
"@types/d3-timer": "*",
|
||||
"@types/d3-transition": "*",
|
||||
"@types/d3-zoom": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-axis": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-brush": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-chord": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-contour": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "*",
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-drag": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
@@ -3418,6 +3558,12 @@
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-polygon": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
@@ -3445,6 +3591,12 @@
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-selection": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
@@ -3460,12 +3612,37 @@
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-time-format": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-transition": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-zoom": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "*",
|
||||
"@types/d3-selection": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/debug": {
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
|
||||
@@ -3629,6 +3806,13 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
@@ -3884,6 +4068,16 @@
|
||||
"integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@upsetjs/venn.js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
|
||||
"integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-transition": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
|
||||
@@ -5120,6 +5314,15 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cose-base": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
|
||||
"integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"layout-base": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-dirname": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
|
||||
@@ -5179,6 +5382,95 @@
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cytoscape": {
|
||||
"version": "3.34.1",
|
||||
"resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.1.tgz",
|
||||
"integrity": "sha512-Lr0RvH9H75y9ar8h9Toy6u4lxRSCcxUq+hHcQ26sVWo6BnaQp1gwEZOYqwuYTZhyW7npyKnNLP8oJ2p1/3OZ7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cytoscape-cose-bilkent": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
|
||||
"integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cose-base": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cytoscape": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cytoscape-fcose": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
|
||||
"integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cose-base": "^2.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"cytoscape": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cytoscape-fcose/node_modules/cose-base": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
|
||||
"integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"layout-base": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cytoscape-fcose/node_modules/layout-base": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
|
||||
"integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3": {
|
||||
"version": "7.9.0",
|
||||
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
||||
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "3",
|
||||
"d3-axis": "3",
|
||||
"d3-brush": "3",
|
||||
"d3-chord": "3",
|
||||
"d3-color": "3",
|
||||
"d3-contour": "4",
|
||||
"d3-delaunay": "6",
|
||||
"d3-dispatch": "3",
|
||||
"d3-drag": "3",
|
||||
"d3-dsv": "3",
|
||||
"d3-ease": "3",
|
||||
"d3-fetch": "3",
|
||||
"d3-force": "3",
|
||||
"d3-format": "3",
|
||||
"d3-geo": "3",
|
||||
"d3-hierarchy": "3",
|
||||
"d3-interpolate": "3",
|
||||
"d3-path": "3",
|
||||
"d3-polygon": "3",
|
||||
"d3-quadtree": "3",
|
||||
"d3-random": "3",
|
||||
"d3-scale": "4",
|
||||
"d3-scale-chromatic": "3",
|
||||
"d3-selection": "3",
|
||||
"d3-shape": "3",
|
||||
"d3-time": "3",
|
||||
"d3-time-format": "4",
|
||||
"d3-timer": "3",
|
||||
"d3-transition": "3",
|
||||
"d3-zoom": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
@@ -5191,12 +5483,49 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-axis": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
||||
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-brush": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
||||
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "3",
|
||||
"d3-transition": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-chord": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
||||
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
@@ -5206,6 +5535,30 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-contour": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
||||
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-delaunay": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delaunator": "5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
@@ -5215,6 +5568,19 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dsv": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||
@@ -5399,6 +5765,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-polygon": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
||||
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
@@ -5423,6 +5798,46 @@
|
||||
"integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-sankey": {
|
||||
"version": "0.12.3",
|
||||
"resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
|
||||
"integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-array": "1 - 2",
|
||||
"d3-shape": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/d3-array": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"internmap": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/d3-path": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
|
||||
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/d3-shape": {
|
||||
"version": "1.3.7",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
|
||||
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"d3-path": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-sankey/node_modules/internmap": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
|
||||
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
@@ -5452,6 +5867,15 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
@@ -5497,6 +5921,41 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dagre": {
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
|
||||
@@ -5507,6 +5966,16 @@
|
||||
"lodash": "^4.17.15"
|
||||
}
|
||||
},
|
||||
"node_modules/dagre-d3-es": {
|
||||
"version": "7.0.14",
|
||||
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
|
||||
"integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"lodash-es": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
@@ -5536,6 +6005,12 @@
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.21",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz",
|
||||
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -5675,6 +6150,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delaunator": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
|
||||
"integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"robust-predicates": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -5856,6 +6340,15 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
@@ -6245,6 +6738,17 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz",
|
||||
"integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks",
|
||||
"tests/types"
|
||||
]
|
||||
},
|
||||
"node_modules/es6-error": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||
@@ -7254,6 +7758,12 @@
|
||||
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hachure-fill": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
|
||||
"integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@@ -7789,6 +8299,16 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/import-meta-resolve": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz",
|
||||
"integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/imurmurhash": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
||||
@@ -8234,6 +8754,17 @@
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/khroma": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
|
||||
"integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
|
||||
},
|
||||
"node_modules/layout-base": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
|
||||
"integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lazy-val": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||
@@ -8381,6 +8912,18 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "16.4.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
|
||||
"integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/matcher": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
|
||||
@@ -8737,6 +9280,35 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.16.1",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz",
|
||||
"integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.2",
|
||||
"@iconify/utils": "^3.0.2",
|
||||
"@mermaid-js/parser": "^1.2.0",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@upsetjs/venn.js": "^2.0.0",
|
||||
"cytoscape": "^3.33.3",
|
||||
"cytoscape-cose-bilkent": "^4.1.0",
|
||||
"cytoscape-fcose": "^2.2.0",
|
||||
"d3": "^7.9.0",
|
||||
"d3-sankey": "^0.12.3",
|
||||
"dagre-d3-es": "7.0.14",
|
||||
"dayjs": "^1.11.20",
|
||||
"dompurify": "^3.3.3",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"katex": "^0.16.45",
|
||||
"khroma": "^2.1.0",
|
||||
"marked": "^16.3.0",
|
||||
"roughjs": "^4.6.6",
|
||||
"stylis": "^4.3.6",
|
||||
"ts-dedent": "^2.2.0",
|
||||
"uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
|
||||
@@ -10050,6 +10622,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/package-manager-detector": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz",
|
||||
"integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parchment": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
|
||||
@@ -10116,6 +10694,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-data-parser": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
|
||||
"integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -10323,6 +10907,22 @@
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/points-on-curve": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
|
||||
"integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/points-on-path": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
|
||||
"integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-data-parser": "0.1.0",
|
||||
"points-on-curve": "0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.25",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
|
||||
@@ -11194,6 +11794,12 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/robust-predicates": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
|
||||
"integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.62.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.3.tgz",
|
||||
@@ -11239,6 +11845,18 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/roughjs": {
|
||||
"version": "4.6.6",
|
||||
"resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
|
||||
"integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hachure-fill": "^0.5.2",
|
||||
"path-data-parser": "^0.1.0",
|
||||
"points-on-curve": "^0.2.0",
|
||||
"points-on-path": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
@@ -11753,6 +12371,12 @@
|
||||
"inline-style-parser": "0.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/stylis": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz",
|
||||
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sumchecker": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz",
|
||||
@@ -11885,7 +12509,6 @@
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
|
||||
"integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -12036,6 +12659,15 @@
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-dedent": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
|
||||
"integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.10"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -12427,6 +13059,19 @@
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
|
||||
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist-node/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -120,6 +120,18 @@
|
||||
{
|
||||
"from": "node_modules/katex/LICENSE",
|
||||
"to": "licenses/katex-MIT.txt"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/mermaid/LICENSE",
|
||||
"to": "licenses/mermaid-MIT.txt"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/dompurify/LICENSE",
|
||||
"to": "licenses/dompurify-Apache-2.0.txt"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/dompurify/LICENSE-MPL",
|
||||
"to": "licenses/dompurify-MPL-2.0.txt"
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
@@ -165,12 +177,14 @@
|
||||
"@wecom/aibot-node-sdk": "^1.0.6",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"dingtalk-stream": "^2.1.6-beta.1",
|
||||
"dompurify": "^3.4.13",
|
||||
"fflate": "^0.8.3",
|
||||
"html-to-text": "^10.0.0",
|
||||
"i18next": "^25.10.10",
|
||||
"json5": "^2.2.3",
|
||||
"katex": "^0.16.47",
|
||||
"lucide-react": "^1.27.0",
|
||||
"mermaid": "^11.16.1",
|
||||
"onnxruntime-web": "^1.23.2",
|
||||
"pdfjs-dist": "^6.2.108",
|
||||
"ppu-paddle-ocr": "^6.4.0",
|
||||
|
||||
@@ -4314,6 +4314,82 @@ describe('App', () => {
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('excludes ignored heartbeat suggestions from the navigation badge', async () => {
|
||||
const heartbeatId = '00000000-0000-4000-8000-000000000701'
|
||||
const cancelledTaskId = '00000000-0000-4000-8000-000000000801'
|
||||
const pendingTaskId = '00000000-0000-4000-8000-000000000802'
|
||||
vi.mocked(api.heartbeats.list).mockResolvedValue([
|
||||
{
|
||||
id: heartbeatId,
|
||||
projectId,
|
||||
name: '每日回顾',
|
||||
timezone: 'Asia/Shanghai',
|
||||
recurrence: { type: 'daily', localTime: '09:00' },
|
||||
enabled: true,
|
||||
lookbackHours: 24,
|
||||
retentionDays: 30,
|
||||
nextRunAt: '2026-08-05T01:00:00.000Z',
|
||||
createdAt: '2026-08-01T00:00:00.000Z',
|
||||
updatedAt: '2026-08-01T00:00:00.000Z'
|
||||
}
|
||||
])
|
||||
vi.mocked(api.heartbeats.history).mockResolvedValue({
|
||||
runs: [],
|
||||
entries: [
|
||||
{
|
||||
id: '00000000-0000-4000-8000-000000000901',
|
||||
configId: heartbeatId,
|
||||
runId: '00000000-0000-4000-8000-000000000902',
|
||||
scheduledFor: '2026-08-04T01:00:00.000Z',
|
||||
summary: '建议处理两个后续行动。',
|
||||
highlights: [],
|
||||
proposedMemoryIds: [],
|
||||
followUpTaskIds: [cancelledTaskId, pendingTaskId],
|
||||
createdAt: '2026-08-04T01:00:00.000Z'
|
||||
}
|
||||
]
|
||||
})
|
||||
vi.mocked(api.tasks.list).mockResolvedValue([
|
||||
{
|
||||
id: cancelledTaskId,
|
||||
projectId,
|
||||
title: '已忽略建议',
|
||||
instructions: '无需继续处理。',
|
||||
origin: 'assistant',
|
||||
status: 'cancelled',
|
||||
createdAt: '2026-08-04T01:00:00.000Z'
|
||||
},
|
||||
{
|
||||
id: pendingTaskId,
|
||||
projectId,
|
||||
title: '待处理建议',
|
||||
instructions: '继续处理此建议。',
|
||||
origin: 'assistant',
|
||||
status: 'paused',
|
||||
createdAt: '2026-08-04T01:00:00.000Z'
|
||||
}
|
||||
])
|
||||
|
||||
try {
|
||||
render(<App />)
|
||||
|
||||
expect(
|
||||
await screen.findByLabelText('1 条待处理建议')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByLabelText('2 条待处理建议')
|
||||
).not.toBeInTheDocument()
|
||||
} finally {
|
||||
cleanup()
|
||||
vi.mocked(api.heartbeats.list).mockResolvedValue([])
|
||||
vi.mocked(api.heartbeats.history).mockResolvedValue({
|
||||
runs: [],
|
||||
entries: []
|
||||
})
|
||||
vi.mocked(api.tasks.list).mockResolvedValue([])
|
||||
}
|
||||
})
|
||||
|
||||
it('shows retryable page-local knowledge errors without an empty-state flash', async () => {
|
||||
vi.mocked(api.knowledge.getSnapshot).mockRejectedValueOnce(
|
||||
new Error('知识数据库暂时不可用')
|
||||
|
||||
@@ -2120,7 +2120,9 @@ function App(): React.JSX.Element {
|
||||
).length +
|
||||
assistantTasks.filter(
|
||||
(task) =>
|
||||
taskIds.has(task.id) && task.status !== 'completed'
|
||||
taskIds.has(task.id) &&
|
||||
task.status !== 'completed' &&
|
||||
task.status !== 'cancelled'
|
||||
).length
|
||||
)
|
||||
}, [assistantMemories, assistantTasks, heartbeatEntries])
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
KnowledgeGraphNode,
|
||||
KnowledgeGraphRelation
|
||||
} from '../../shared/contracts'
|
||||
import { useDocumentTheme } from './use-document-theme'
|
||||
|
||||
type ChartKnowledgeGraphNode = Omit<
|
||||
KnowledgeGraphNode,
|
||||
@@ -347,7 +348,7 @@ export function KnowledgeGraphChart({
|
||||
() => graphRevision(nodes, relations),
|
||||
[nodes, relations]
|
||||
)
|
||||
const [themeRevision, setThemeRevision] = useState(0)
|
||||
const documentTheme = useDocumentTheme()
|
||||
const [renderError, setRenderError] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -407,20 +408,6 @@ export function KnowledgeGraphChart({
|
||||
})
|
||||
}, [dataRevision, fitViewRequest, renderErrorFallback])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof MutationObserver !== 'function') {
|
||||
return
|
||||
}
|
||||
const observer = new MutationObserver(() => {
|
||||
setThemeRevision((revision) => revision + 1)
|
||||
})
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
})
|
||||
return () => observer.disconnect()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const container = containerRef.current
|
||||
if (!container) {
|
||||
@@ -583,7 +570,7 @@ export function KnowledgeGraphChart({
|
||||
locale,
|
||||
relationFallback,
|
||||
renderErrorFallback,
|
||||
themeRevision
|
||||
documentTheme
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
act,
|
||||
cleanup,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor
|
||||
} from '@testing-library/react'
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest'
|
||||
import { changeUiLocale } from './i18n'
|
||||
import { MarkdownRenderer } from './MarkdownRenderer'
|
||||
|
||||
const mermaidMock = vi.hoisted(() => ({
|
||||
initialize: vi.fn(),
|
||||
render: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('mermaid', () => ({
|
||||
default: mermaidMock
|
||||
}))
|
||||
|
||||
describe('MarkdownRenderer', () => {
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => {
|
||||
mermaidMock.render.mockResolvedValue({
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50">
|
||||
<rect id="safe-node" width="100" height="50" />
|
||||
</svg>`
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
delete document.documentElement.dataset.theme
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders CommonMark and GitHub Flavored Markdown', () => {
|
||||
render(
|
||||
@@ -165,6 +200,282 @@ $\\href{javascript:alert(1)}{unsafe}$`}</MarkdownRenderer>
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders Mermaid fences with strict bounded configuration', async () => {
|
||||
const { container } = render(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
flowchart LR
|
||||
Start --> Finish
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText('正在绘制 Mermaid 图表…')
|
||||
).toHaveAttribute('role', 'status')
|
||||
const diagram = await screen.findByRole('region', {
|
||||
name: 'Mermaid 图表,可横向滚动'
|
||||
})
|
||||
|
||||
expect(diagram).toHaveAttribute('tabindex', '0')
|
||||
expect(container.querySelector('#safe-node')).toBeInTheDocument()
|
||||
expect(mermaidMock.render).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^goodbuddy-mermaid-\d+-0$/u),
|
||||
'flowchart LR\n Start --> Finish'
|
||||
)
|
||||
expect(mermaidMock.initialize).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
htmlLabels: false,
|
||||
maxEdges: 300,
|
||||
maxTextSize: 20_000,
|
||||
securityLevel: 'strict',
|
||||
startOnLoad: false,
|
||||
suppressErrorRendering: true,
|
||||
theme: 'default'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('shows source and opens an accessible zoomable Mermaid viewer', async () => {
|
||||
render(
|
||||
<div className="app-shell">
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
flowchart LR
|
||||
Start --> Finish
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
</div>
|
||||
)
|
||||
|
||||
await screen.findByRole('region', {
|
||||
name: 'Mermaid 图表,可横向滚动'
|
||||
})
|
||||
const sourceButton = screen.getByRole('button', {
|
||||
name: '查看源码'
|
||||
})
|
||||
expect(sourceButton).toHaveAttribute('aria-expanded', 'false')
|
||||
fireEvent.click(sourceButton)
|
||||
expect(
|
||||
screen.getByRole('button', { name: '隐藏源码' })
|
||||
).toHaveAttribute('aria-expanded', 'true')
|
||||
expect(screen.getByText(/Start --> Finish/u)).toBeInTheDocument()
|
||||
|
||||
const viewerButton = screen.getByRole('button', {
|
||||
name: '打开大图'
|
||||
})
|
||||
viewerButton.focus()
|
||||
fireEvent.click(viewerButton)
|
||||
const dialog = screen.getByRole('dialog', {
|
||||
name: 'Mermaid 大图'
|
||||
})
|
||||
expect(dialog).toHaveAttribute('aria-modal', 'true')
|
||||
expect(
|
||||
screen.getByRole('button', { name: '关闭 Mermaid 大图' })
|
||||
).toHaveFocus()
|
||||
expect(
|
||||
document.querySelector<HTMLElement>('.app-shell')?.inert
|
||||
).toBe(true)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '放大图表' }))
|
||||
expect(screen.getByLabelText('当前缩放比例')).toHaveTextContent(
|
||||
'125%'
|
||||
)
|
||||
const canvas = screen.getByRole('region', {
|
||||
name: '可缩放、可拖动的 Mermaid 图表'
|
||||
})
|
||||
expect(canvas).toHaveAttribute('tabindex', '0')
|
||||
canvas.focus()
|
||||
expect(canvas).toHaveFocus()
|
||||
fireEvent.keyDown(dialog, { key: 'Tab' })
|
||||
expect(
|
||||
screen.getByRole('button', { name: '缩小图表' })
|
||||
).toHaveFocus()
|
||||
fireEvent.wheel(canvas, { deltaY: -100 })
|
||||
fireEvent.wheel(canvas, { deltaY: -100 })
|
||||
fireEvent.wheel(canvas, { deltaY: -100 })
|
||||
await waitFor(() =>
|
||||
expect(screen.getByLabelText('当前缩放比例')).toHaveTextContent(
|
||||
'200%'
|
||||
)
|
||||
)
|
||||
fireEvent.click(screen.getByRole('button', { name: '重置缩放' }))
|
||||
expect(screen.getByLabelText('当前缩放比例')).toHaveTextContent(
|
||||
'100%'
|
||||
)
|
||||
|
||||
fireEvent.keyDown(dialog, { key: 'Escape' })
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
await waitFor(() => expect(viewerButton).toHaveFocus())
|
||||
expect(
|
||||
document.querySelector<HTMLElement>('.app-shell')?.inert
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('closes Mermaid controls when streamed source changes', async () => {
|
||||
const { rerender } = render(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
flowchart LR
|
||||
A --> B
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
await screen.findByRole('region', {
|
||||
name: 'Mermaid 图表,可横向滚动'
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '查看源码' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开大图' }))
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
flowchart LR
|
||||
A --> B --> C
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
||||
await screen.findByRole('region', {
|
||||
name: 'Mermaid 图表,可横向滚动'
|
||||
})
|
||||
expect(
|
||||
screen.getByRole('button', { name: '查看源码' })
|
||||
).toHaveAttribute('aria-expanded', 'false')
|
||||
expect(screen.queryByText(/A --> B --> C/u)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('pans the large Mermaid diagram with pointer dragging', async () => {
|
||||
render(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
flowchart LR
|
||||
A --> B
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
await screen.findByRole('region', {
|
||||
name: 'Mermaid 图表,可横向滚动'
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开大图' }))
|
||||
const canvas = screen.getByRole('region', {
|
||||
name: '可缩放、可拖动的 Mermaid 图表'
|
||||
})
|
||||
const setPointerCapture = vi.fn()
|
||||
const releasePointerCapture = vi.fn()
|
||||
Object.defineProperties(canvas, {
|
||||
setPointerCapture: { value: setPointerCapture },
|
||||
hasPointerCapture: { value: () => true },
|
||||
releasePointerCapture: { value: releasePointerCapture }
|
||||
})
|
||||
canvas.scrollLeft = 30
|
||||
canvas.scrollTop = 40
|
||||
|
||||
fireEvent.pointerDown(canvas, {
|
||||
button: 0,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
pointerId: 9
|
||||
})
|
||||
fireEvent.pointerMove(canvas, {
|
||||
clientX: 70,
|
||||
clientY: 60,
|
||||
pointerId: 9
|
||||
})
|
||||
|
||||
expect(setPointerCapture).toHaveBeenCalledWith(9)
|
||||
expect(canvas).toHaveClass('mermaid-viewer__canvas--dragging')
|
||||
expect(canvas.scrollLeft).toBe(60)
|
||||
expect(canvas.scrollTop).toBe(80)
|
||||
|
||||
fireEvent.pointerUp(canvas, { pointerId: 9 })
|
||||
expect(releasePointerCapture).toHaveBeenCalledWith(9)
|
||||
expect(canvas).not.toHaveClass(
|
||||
'mermaid-viewer__canvas--dragging'
|
||||
)
|
||||
})
|
||||
|
||||
it('sanitizes Mermaid SVG without enabling diagram interactions', async () => {
|
||||
mermaidMock.render.mockResolvedValueOnce({
|
||||
svg: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 50">
|
||||
<style>@\\69mport url("https://unsafe.test/style.css");</style>
|
||||
<script>window.bad = true</script>
|
||||
<a href="javascript:alert(1)">
|
||||
<rect id="unsafe-node" onclick="alert(1)"
|
||||
style="fill: url(javascript:alert(1))" width="100" height="50" />
|
||||
</a>
|
||||
<rect id="safe-node" fill="url(#gradient)" width="20" height="20" />
|
||||
</svg>`
|
||||
})
|
||||
|
||||
const { container } = render(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
flowchart LR
|
||||
A --> B
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
|
||||
await screen.findByRole('region', {
|
||||
name: 'Mermaid 图表,可横向滚动'
|
||||
})
|
||||
expect(container.querySelector('script')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('a')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('style')).not.toBeInTheDocument()
|
||||
expect(container.querySelector('#unsafe-node')).not.toHaveAttribute(
|
||||
'onclick'
|
||||
)
|
||||
expect(container.querySelector('#unsafe-node')).not.toHaveAttribute(
|
||||
'style'
|
||||
)
|
||||
expect(container.querySelector('#safe-node')).toHaveAttribute(
|
||||
'fill',
|
||||
'url(#gradient)'
|
||||
)
|
||||
})
|
||||
|
||||
it('rerenders Mermaid diagrams when the application theme changes', async () => {
|
||||
render(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
sequenceDiagram
|
||||
Alice->>Bob: Hello
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mermaidMock.render).toHaveBeenCalledTimes(1)
|
||||
)
|
||||
act(() => {
|
||||
document.documentElement.dataset.theme = 'dark'
|
||||
})
|
||||
await waitFor(() =>
|
||||
expect(mermaidMock.render).toHaveBeenCalledTimes(2)
|
||||
)
|
||||
expect(mermaidMock.initialize).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
darkMode: true,
|
||||
theme: 'dark'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('shows Mermaid source when rendering fails or input is oversized', async () => {
|
||||
mermaidMock.render.mockRejectedValueOnce(
|
||||
new Error('Invalid Mermaid syntax')
|
||||
)
|
||||
const { rerender } = render(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
not a diagram
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
|
||||
expect(
|
||||
await screen.findByRole('alert')
|
||||
).toHaveTextContent('无法绘制 Mermaid 图表')
|
||||
expect(screen.getByText('not a diagram')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<MarkdownRenderer>{`\`\`\`mermaid
|
||||
${'A'.repeat(20_001)}
|
||||
\`\`\``}</MarkdownRenderer>
|
||||
)
|
||||
expect(
|
||||
await screen.findByRole('alert')
|
||||
).toHaveTextContent('无法绘制 Mermaid 图表')
|
||||
expect(mermaidMock.render).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('updates table accessibility copy when the locale changes', async () => {
|
||||
render(
|
||||
<MarkdownRenderer>{`| Name |
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { memo, useMemo } from 'react'
|
||||
import {
|
||||
Children,
|
||||
isValidElement,
|
||||
lazy,
|
||||
memo,
|
||||
Suspense,
|
||||
useMemo
|
||||
} from 'react'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
@@ -6,6 +13,12 @@ import remarkMath from 'remark-math'
|
||||
import type { Components } from 'react-markdown'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const MermaidDiagram = lazy(() =>
|
||||
import('./MermaidDiagram').then((module) => ({
|
||||
default: module.MermaidDiagram
|
||||
}))
|
||||
)
|
||||
|
||||
const linkComponent: Components['a'] = ({
|
||||
children,
|
||||
node,
|
||||
@@ -19,9 +32,46 @@ const linkComponent: Components['a'] = ({
|
||||
)
|
||||
}
|
||||
|
||||
function markdownComponents(tableAriaLabel: string): Components {
|
||||
function markdownComponents(
|
||||
tableAriaLabel: string,
|
||||
mermaidLoadingLabel: string
|
||||
): Components {
|
||||
return {
|
||||
a: linkComponent,
|
||||
pre: ({ children, node, ...properties }) => {
|
||||
void node
|
||||
const child = Children.count(children) === 1
|
||||
? Children.only(children)
|
||||
: undefined
|
||||
if (
|
||||
isValidElement<{
|
||||
children?: React.ReactNode
|
||||
className?: string
|
||||
}>(child) &&
|
||||
/(?:^|\s)language-mermaid(?:\s|$)/iu.test(
|
||||
child.props.className ?? ''
|
||||
)
|
||||
) {
|
||||
const source = String(child.props.children ?? '').replace(
|
||||
/\n$/u,
|
||||
''
|
||||
)
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<figure aria-busy="true" className="mermaid-diagram">
|
||||
<figcaption role="status">
|
||||
{mermaidLoadingLabel}
|
||||
</figcaption>
|
||||
</figure>
|
||||
}
|
||||
>
|
||||
<MermaidDiagram source={source} />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
return <pre {...properties}>{children}</pre>
|
||||
},
|
||||
table: ({ children, node, ...properties }) => {
|
||||
void node
|
||||
return (
|
||||
@@ -149,7 +199,11 @@ export const MarkdownRenderer = memo(function MarkdownRenderer({
|
||||
}: MarkdownRendererProps): React.JSX.Element {
|
||||
const { t } = useTranslation('app')
|
||||
const components = useMemo(
|
||||
() => markdownComponents(t('markdown.scrollableTable')),
|
||||
() =>
|
||||
markdownComponents(
|
||||
t('markdown.scrollableTable'),
|
||||
t('markdown.mermaidLoading')
|
||||
),
|
||||
[t]
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,617 @@
|
||||
import DOMPurify from 'dompurify'
|
||||
import {
|
||||
Code2,
|
||||
Maximize2,
|
||||
RotateCcw,
|
||||
X,
|
||||
ZoomIn,
|
||||
ZoomOut
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
useEffect,
|
||||
useId,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Mermaid, MermaidConfig } from 'mermaid'
|
||||
import { activateModalFocus, trapTabFocus } from './dialog-focus'
|
||||
import { useDocumentTheme } from './use-document-theme'
|
||||
import './markdown-mermaid.css'
|
||||
|
||||
const MAX_MERMAID_TEXT_SIZE = 20_000
|
||||
const MAX_MERMAID_EDGES = 300
|
||||
const MAX_MERMAID_DIMENSION = 4_096
|
||||
const MIN_VIEWER_ZOOM = 0.5
|
||||
const MAX_VIEWER_ZOOM = 3
|
||||
const VIEWER_ZOOM_STEP = 0.25
|
||||
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
|
||||
const unsafeCssPattern =
|
||||
/(?:@import|\bexpression\s*\(|(?:https?|ftp|file|data|javascript|vbscript):|\/\/|-moz-binding|\bbehavior\s*:)/iu
|
||||
const cssUrlPattern = /url\s*\(\s*(['"]?)(.*?)\1\s*\)/giu
|
||||
|
||||
const forbiddenTags = [
|
||||
'a',
|
||||
'animate',
|
||||
'animatemotion',
|
||||
'animatetransform',
|
||||
'discard',
|
||||
'foreignobject',
|
||||
'iframe',
|
||||
'image',
|
||||
'mpath',
|
||||
'set',
|
||||
'script'
|
||||
]
|
||||
const forbiddenAttributes = ['href', 'xlink:href']
|
||||
const sharedSanitizerConfig = {
|
||||
ALLOW_DATA_ATTR: false,
|
||||
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||
FORBID_ATTR: forbiddenAttributes,
|
||||
FORBID_TAGS: forbiddenTags
|
||||
} as const
|
||||
const secureConfigKeys = [
|
||||
'secure',
|
||||
'securityLevel',
|
||||
'startOnLoad',
|
||||
'maxTextSize',
|
||||
'suppressErrorRendering',
|
||||
'maxEdges',
|
||||
'htmlLabels',
|
||||
'dompurifyConfig',
|
||||
'theme',
|
||||
'themeCSS',
|
||||
'themeVariables',
|
||||
'fontFamily',
|
||||
'altFontFamily',
|
||||
'flowchart'
|
||||
]
|
||||
|
||||
let mermaidModulePromise: Promise<Mermaid> | undefined
|
||||
let mermaidRenderQueue = Promise.resolve()
|
||||
let mermaidDiagramSequence = 0
|
||||
|
||||
type MermaidRenderState =
|
||||
| {
|
||||
source: string
|
||||
status: 'ready'
|
||||
svg: string
|
||||
theme: 'light' | 'dark'
|
||||
}
|
||||
| {
|
||||
source: string
|
||||
status: 'error'
|
||||
theme: 'light' | 'dark'
|
||||
}
|
||||
|
||||
function loadMermaid(): Promise<Mermaid> {
|
||||
mermaidModulePromise ??= import('mermaid').then(
|
||||
(module) => module.default
|
||||
)
|
||||
return mermaidModulePromise
|
||||
}
|
||||
|
||||
function containsUnsafeCss(value: string): boolean {
|
||||
const normalizedValue = value
|
||||
.replace(/\/\*[\s\S]*?\*\//gu, '')
|
||||
.replace(
|
||||
/\\([0-9a-f]{1,6}\s?|.)/giu,
|
||||
(_match, escaped: string) => {
|
||||
const hex = escaped.trim()
|
||||
return /^[0-9a-f]{1,6}$/iu.test(hex)
|
||||
? String.fromCodePoint(Number.parseInt(hex, 16))
|
||||
: escaped
|
||||
}
|
||||
)
|
||||
if (unsafeCssPattern.test(normalizedValue)) {
|
||||
return true
|
||||
}
|
||||
cssUrlPattern.lastIndex = 0
|
||||
for (
|
||||
let match = cssUrlPattern.exec(normalizedValue);
|
||||
match;
|
||||
match = cssUrlPattern.exec(normalizedValue)
|
||||
) {
|
||||
if (!match[2]?.trim().startsWith('#')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function sanitizeMermaidSvg(svg: string): string {
|
||||
const sanitized = DOMPurify.sanitize(svg, {
|
||||
...sharedSanitizerConfig,
|
||||
USE_PROFILES: {
|
||||
svg: true,
|
||||
svgFilters: true
|
||||
}
|
||||
})
|
||||
const documentNode = new DOMParser().parseFromString(
|
||||
sanitized,
|
||||
'image/svg+xml'
|
||||
)
|
||||
const root = documentNode.documentElement
|
||||
if (
|
||||
root.localName !== 'svg' ||
|
||||
root.namespaceURI !== SVG_NAMESPACE ||
|
||||
documentNode.querySelector('parsererror')
|
||||
) {
|
||||
throw new Error('Mermaid returned invalid SVG')
|
||||
}
|
||||
|
||||
for (const element of [root, ...root.querySelectorAll('*')]) {
|
||||
if (
|
||||
element.localName === 'style' &&
|
||||
containsUnsafeCss(element.textContent ?? '')
|
||||
) {
|
||||
element.remove()
|
||||
continue
|
||||
}
|
||||
for (const attribute of [...element.attributes]) {
|
||||
const attributeName = attribute.name.toLowerCase()
|
||||
if (
|
||||
attributeName.startsWith('on') ||
|
||||
forbiddenAttributes.includes(attributeName) ||
|
||||
containsUnsafeCss(attribute.value)
|
||||
) {
|
||||
element.removeAttribute(attribute.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const viewBox = root
|
||||
.getAttribute('viewBox')
|
||||
?.trim()
|
||||
.split(/[\s,]+/u)
|
||||
.map(Number)
|
||||
if (
|
||||
viewBox?.length === 4 &&
|
||||
viewBox.every(Number.isFinite) &&
|
||||
viewBox[2]! > 0 &&
|
||||
viewBox[3]! > 0
|
||||
) {
|
||||
const scale = Math.min(
|
||||
1,
|
||||
MAX_MERMAID_DIMENSION / viewBox[2]!,
|
||||
MAX_MERMAID_DIMENSION / viewBox[3]!
|
||||
)
|
||||
root.setAttribute('width', String(Math.ceil(viewBox[2]! * scale)))
|
||||
root.setAttribute('height', String(Math.ceil(viewBox[3]! * scale)))
|
||||
root.style.removeProperty('max-width')
|
||||
}
|
||||
|
||||
return new XMLSerializer().serializeToString(root)
|
||||
}
|
||||
|
||||
function mermaidConfig(darkTheme: boolean): MermaidConfig {
|
||||
return {
|
||||
darkMode: darkTheme,
|
||||
deterministicIds: true,
|
||||
dompurifyConfig: sharedSanitizerConfig,
|
||||
fontFamily:
|
||||
'"Inter Variable", "Noto Sans SC Variable", "Segoe UI Variable", sans-serif',
|
||||
htmlLabels: false,
|
||||
logLevel: 'fatal',
|
||||
maxEdges: MAX_MERMAID_EDGES,
|
||||
maxTextSize: MAX_MERMAID_TEXT_SIZE,
|
||||
secure: secureConfigKeys,
|
||||
securityLevel: 'strict',
|
||||
startOnLoad: false,
|
||||
suppressErrorRendering: true,
|
||||
theme: darkTheme ? 'dark' : 'default'
|
||||
}
|
||||
}
|
||||
|
||||
function renderMermaid(
|
||||
source: string,
|
||||
id: string,
|
||||
darkTheme: boolean,
|
||||
shouldRender: () => boolean
|
||||
): Promise<string> {
|
||||
if (source.length > MAX_MERMAID_TEXT_SIZE) {
|
||||
return Promise.reject(new Error('Mermaid source is too large'))
|
||||
}
|
||||
|
||||
const render = mermaidRenderQueue.then(async () => {
|
||||
if (!shouldRender()) {
|
||||
throw new Error('Mermaid render was superseded')
|
||||
}
|
||||
const mermaid = await loadMermaid()
|
||||
if (!shouldRender()) {
|
||||
throw new Error('Mermaid render was superseded')
|
||||
}
|
||||
mermaid.initialize(mermaidConfig(darkTheme))
|
||||
const result = await mermaid.render(id, source)
|
||||
return sanitizeMermaidSvg(result.svg)
|
||||
})
|
||||
mermaidRenderQueue = render.then(
|
||||
() => undefined,
|
||||
() => undefined
|
||||
)
|
||||
return render
|
||||
}
|
||||
|
||||
type MermaidDiagramProps = {
|
||||
source: string
|
||||
}
|
||||
|
||||
type MermaidViewerProps = {
|
||||
onClose: () => void
|
||||
svg: string
|
||||
}
|
||||
|
||||
function clampViewerZoom(zoom: number): number {
|
||||
return Math.min(MAX_VIEWER_ZOOM, Math.max(MIN_VIEWER_ZOOM, zoom))
|
||||
}
|
||||
|
||||
function MermaidViewer({
|
||||
onClose,
|
||||
svg
|
||||
}: MermaidViewerProps): React.JSX.Element {
|
||||
const { i18n, t } = useTranslation('app')
|
||||
const titleId = useId()
|
||||
const hintId = useId()
|
||||
const dialogRef = useRef<HTMLElement>(null)
|
||||
const closeRef = useRef<HTMLButtonElement>(null)
|
||||
const dragRef = useRef<{
|
||||
pointerId: number
|
||||
startX: number
|
||||
startY: number
|
||||
scrollLeft: number
|
||||
scrollTop: number
|
||||
} | undefined>(undefined)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const [zoom, setZoom] = useState(1)
|
||||
const pendingWheelZoomRef = useRef(0)
|
||||
const zoomFrameRef = useRef<number | undefined>(undefined)
|
||||
const percentFormatter = useMemo(
|
||||
() =>
|
||||
new Intl.NumberFormat(i18n.resolvedLanguage, {
|
||||
maximumFractionDigits: 0,
|
||||
style: 'percent'
|
||||
}),
|
||||
[i18n.resolvedLanguage]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const deactivateModalFocus = activateModalFocus(
|
||||
() => closeRef.current
|
||||
)
|
||||
return () => {
|
||||
if (zoomFrameRef.current !== undefined) {
|
||||
cancelAnimationFrame(zoomFrameRef.current)
|
||||
}
|
||||
deactivateModalFocus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const adjustZoom = (delta: number): void => {
|
||||
setZoom((current) => clampViewerZoom(current + delta))
|
||||
}
|
||||
|
||||
const queueWheelZoom = (delta: number): void => {
|
||||
pendingWheelZoomRef.current += delta
|
||||
if (zoomFrameRef.current !== undefined) {
|
||||
return
|
||||
}
|
||||
zoomFrameRef.current = requestAnimationFrame(() => {
|
||||
const pendingZoom = pendingWheelZoomRef.current
|
||||
pendingWheelZoomRef.current = 0
|
||||
zoomFrameRef.current = undefined
|
||||
adjustZoom(pendingZoom)
|
||||
})
|
||||
}
|
||||
|
||||
const finishDragging = (
|
||||
event: React.PointerEvent<HTMLDivElement>
|
||||
): void => {
|
||||
if (dragRef.current?.pointerId !== event.pointerId) {
|
||||
return
|
||||
}
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId)
|
||||
}
|
||||
dragRef.current = undefined
|
||||
setDragging(false)
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="mermaid-viewer-backdrop"
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section
|
||||
aria-describedby={hintId}
|
||||
aria-labelledby={titleId}
|
||||
aria-modal="true"
|
||||
className="mermaid-viewer"
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault()
|
||||
onClose()
|
||||
return
|
||||
}
|
||||
trapTabFocus(event, dialogRef.current)
|
||||
}}
|
||||
ref={dialogRef}
|
||||
role="dialog"
|
||||
>
|
||||
<header className="mermaid-viewer__header">
|
||||
<div>
|
||||
<strong id={titleId}>{t('markdown.mermaidViewerTitle')}</strong>
|
||||
<small id={hintId}>
|
||||
{t('markdown.mermaidViewerHint')}
|
||||
</small>
|
||||
</div>
|
||||
<div className="mermaid-viewer__controls">
|
||||
<button
|
||||
aria-label={t('markdown.mermaidZoomOut')}
|
||||
className="icon-button"
|
||||
disabled={zoom <= MIN_VIEWER_ZOOM}
|
||||
onClick={() => adjustZoom(-VIEWER_ZOOM_STEP)}
|
||||
title={t('markdown.mermaidZoomOut')}
|
||||
type="button"
|
||||
>
|
||||
<ZoomOut aria-hidden="true" size={17} />
|
||||
</button>
|
||||
<output
|
||||
aria-label={t('markdown.mermaidZoomLevel')}
|
||||
className="mermaid-viewer__zoom"
|
||||
>
|
||||
{percentFormatter.format(zoom)}
|
||||
</output>
|
||||
<button
|
||||
aria-label={t('markdown.mermaidZoomIn')}
|
||||
className="icon-button"
|
||||
disabled={zoom >= MAX_VIEWER_ZOOM}
|
||||
onClick={() => adjustZoom(VIEWER_ZOOM_STEP)}
|
||||
title={t('markdown.mermaidZoomIn')}
|
||||
type="button"
|
||||
>
|
||||
<ZoomIn aria-hidden="true" size={17} />
|
||||
</button>
|
||||
<button
|
||||
aria-label={t('markdown.mermaidResetZoom')}
|
||||
className="icon-button"
|
||||
disabled={zoom === 1}
|
||||
onClick={() => setZoom(1)}
|
||||
title={t('markdown.mermaidResetZoom')}
|
||||
type="button"
|
||||
>
|
||||
<RotateCcw aria-hidden="true" size={16} />
|
||||
</button>
|
||||
<button
|
||||
aria-label={t('markdown.mermaidCloseViewer')}
|
||||
className="icon-button"
|
||||
onClick={onClose}
|
||||
ref={closeRef}
|
||||
title={t('markdown.mermaidCloseViewer')}
|
||||
type="button"
|
||||
>
|
||||
<X aria-hidden="true" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div
|
||||
aria-label={t('markdown.mermaidViewerCanvas')}
|
||||
className={`mermaid-viewer__canvas${
|
||||
dragging ? ' mermaid-viewer__canvas--dragging' : ''
|
||||
}`}
|
||||
onPointerCancel={finishDragging}
|
||||
onPointerDown={(event) => {
|
||||
if (event.button !== 0) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
dragRef.current = {
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
scrollLeft: event.currentTarget.scrollLeft,
|
||||
scrollTop: event.currentTarget.scrollTop
|
||||
}
|
||||
event.currentTarget.setPointerCapture(event.pointerId)
|
||||
setDragging(true)
|
||||
}}
|
||||
onPointerMove={(event) => {
|
||||
const drag = dragRef.current
|
||||
if (drag?.pointerId !== event.pointerId) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
event.currentTarget.scrollLeft =
|
||||
drag.scrollLeft - (event.clientX - drag.startX)
|
||||
event.currentTarget.scrollTop =
|
||||
drag.scrollTop - (event.clientY - drag.startY)
|
||||
}}
|
||||
onPointerUp={finishDragging}
|
||||
onWheel={(event) => {
|
||||
if (event.deltaY === 0) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
queueWheelZoom(
|
||||
event.deltaY < 0
|
||||
? VIEWER_ZOOM_STEP
|
||||
: -VIEWER_ZOOM_STEP
|
||||
)
|
||||
}}
|
||||
role="region"
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="mermaid-viewer__diagram"
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
style={{ zoom }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
export function MermaidDiagram({
|
||||
source
|
||||
}: MermaidDiagramProps): React.JSX.Element {
|
||||
const { t } = useTranslation('app')
|
||||
const idRef = useRef(
|
||||
`goodbuddy-mermaid-${++mermaidDiagramSequence}`
|
||||
)
|
||||
const renderRevisionRef = useRef(0)
|
||||
const sourceId = useId()
|
||||
const documentTheme = useDocumentTheme()
|
||||
const [renderState, setRenderState] =
|
||||
useState<MermaidRenderState>()
|
||||
const [visibleSource, setVisibleSource] = useState<string>()
|
||||
const [viewerSource, setViewerSource] = useState<string>()
|
||||
const sourceVisible = visibleSource === source
|
||||
const viewerOpen = viewerSource === source
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
const renderRevision = renderRevisionRef.current++
|
||||
const timeout = window.setTimeout(() => {
|
||||
void renderMermaid(
|
||||
source.trim(),
|
||||
`${idRef.current}-${renderRevision}`,
|
||||
documentTheme === 'dark',
|
||||
() => active
|
||||
).then(
|
||||
(svg) => {
|
||||
if (active) {
|
||||
setRenderState({
|
||||
source,
|
||||
status: 'ready',
|
||||
svg,
|
||||
theme: documentTheme
|
||||
})
|
||||
}
|
||||
},
|
||||
() => {
|
||||
if (active) {
|
||||
setRenderState({
|
||||
source,
|
||||
status: 'error',
|
||||
theme: documentTheme
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}, 120)
|
||||
|
||||
return () => {
|
||||
active = false
|
||||
window.clearTimeout(timeout)
|
||||
}
|
||||
}, [documentTheme, source])
|
||||
|
||||
const currentRenderState =
|
||||
renderState?.source === source &&
|
||||
renderState.theme === documentTheme
|
||||
? renderState
|
||||
: undefined
|
||||
const retainedSvg =
|
||||
renderState?.status === 'ready' &&
|
||||
renderState.source === source
|
||||
? renderState.svg
|
||||
: undefined
|
||||
const svg =
|
||||
currentRenderState?.status === 'ready'
|
||||
? currentRenderState.svg
|
||||
: retainedSvg
|
||||
|
||||
if (currentRenderState?.status === 'error') {
|
||||
return (
|
||||
<figure className="mermaid-diagram mermaid-diagram--error">
|
||||
<figcaption role="alert">
|
||||
{t('markdown.mermaidError')}
|
||||
</figcaption>
|
||||
<pre>
|
||||
<code className="language-mermaid">{source}</code>
|
||||
</pre>
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<figure
|
||||
aria-busy={!currentRenderState}
|
||||
className="mermaid-diagram"
|
||||
>
|
||||
{svg ? (
|
||||
<>
|
||||
<div
|
||||
aria-label={t('markdown.mermaidActions')}
|
||||
className="mermaid-diagram__actions"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
aria-controls={sourceId}
|
||||
aria-expanded={sourceVisible}
|
||||
aria-label={t(
|
||||
sourceVisible
|
||||
? 'markdown.mermaidHideSource'
|
||||
: 'markdown.mermaidViewSource'
|
||||
)}
|
||||
className={`icon-button${
|
||||
sourceVisible ? ' icon-button--active' : ''
|
||||
}`}
|
||||
onClick={() =>
|
||||
setVisibleSource(sourceVisible ? undefined : source)
|
||||
}
|
||||
title={t(
|
||||
sourceVisible
|
||||
? 'markdown.mermaidHideSource'
|
||||
: 'markdown.mermaidViewSource'
|
||||
)}
|
||||
type="button"
|
||||
>
|
||||
<Code2 aria-hidden="true" size={17} />
|
||||
</button>
|
||||
<button
|
||||
aria-label={t('markdown.mermaidOpenViewer')}
|
||||
className="icon-button"
|
||||
onClick={() => setViewerSource(source)}
|
||||
title={t('markdown.mermaidOpenViewer')}
|
||||
type="button"
|
||||
>
|
||||
<Maximize2 aria-hidden="true" size={17} />
|
||||
</button>
|
||||
</div>
|
||||
{!viewerOpen && (
|
||||
<div
|
||||
aria-label={t('markdown.mermaidDiagram')}
|
||||
className="mermaid-diagram__viewport"
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
role="region"
|
||||
tabIndex={0}
|
||||
/>
|
||||
)}
|
||||
{sourceVisible && (
|
||||
<pre className="mermaid-diagram__source" id={sourceId}>
|
||||
<code className="language-mermaid">{source}</code>
|
||||
</pre>
|
||||
)}
|
||||
{viewerOpen && (
|
||||
<MermaidViewer
|
||||
onClose={() => setViewerSource(undefined)}
|
||||
svg={svg}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<figcaption className="mermaid-diagram__loading" role="status">
|
||||
{t('markdown.mermaidLoading')}
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ type TabKeyEvent = {
|
||||
}
|
||||
|
||||
const focusableSelector =
|
||||
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled])'
|
||||
'button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
|
||||
export function trapTabFocus(
|
||||
event: TabKeyEvent,
|
||||
|
||||
@@ -473,6 +473,23 @@ export const app = {
|
||||
evidenceExcerpt: '{{source}}: {{excerpt}}'
|
||||
},
|
||||
markdown: {
|
||||
scrollableTable: 'Table, horizontally scrollable'
|
||||
scrollableTable: 'Table, horizontally scrollable',
|
||||
mermaidDiagram: 'Mermaid diagram, horizontally scrollable',
|
||||
mermaidLoading: 'Rendering Mermaid diagram…',
|
||||
mermaidError:
|
||||
'The Mermaid diagram could not be rendered. Its source is shown below.',
|
||||
mermaidActions: 'Mermaid diagram actions',
|
||||
mermaidViewSource: 'View source',
|
||||
mermaidHideSource: 'Hide source',
|
||||
mermaidOpenViewer: 'Open large diagram',
|
||||
mermaidViewerTitle: 'Large Mermaid diagram',
|
||||
mermaidViewerHint:
|
||||
'Use the mouse wheel or buttons to zoom, and drag the canvas to pan.',
|
||||
mermaidViewerCanvas: 'Zoomable, pannable Mermaid diagram',
|
||||
mermaidZoomOut: 'Zoom out diagram',
|
||||
mermaidZoomIn: 'Zoom in diagram',
|
||||
mermaidResetZoom: 'Reset zoom',
|
||||
mermaidZoomLevel: 'Current zoom level',
|
||||
mermaidCloseViewer: 'Close large Mermaid diagram'
|
||||
}
|
||||
} satisfies TranslationShape<typeof chineseApp>
|
||||
|
||||
@@ -439,6 +439,21 @@ export const app = {
|
||||
evidenceExcerpt: '{{source}}:{{excerpt}}'
|
||||
},
|
||||
markdown: {
|
||||
scrollableTable: '表格,可横向滚动'
|
||||
scrollableTable: '表格,可横向滚动',
|
||||
mermaidDiagram: 'Mermaid 图表,可横向滚动',
|
||||
mermaidLoading: '正在绘制 Mermaid 图表…',
|
||||
mermaidError: '无法绘制 Mermaid 图表,已保留原始图表代码。',
|
||||
mermaidActions: 'Mermaid 图表操作',
|
||||
mermaidViewSource: '查看源码',
|
||||
mermaidHideSource: '隐藏源码',
|
||||
mermaidOpenViewer: '打开大图',
|
||||
mermaidViewerTitle: 'Mermaid 大图',
|
||||
mermaidViewerHint: '使用滚轮或按钮缩放,拖动画布浏览。',
|
||||
mermaidViewerCanvas: '可缩放、可拖动的 Mermaid 图表',
|
||||
mermaidZoomOut: '缩小图表',
|
||||
mermaidZoomIn: '放大图表',
|
||||
mermaidResetZoom: '重置缩放',
|
||||
mermaidZoomLevel: '当前缩放比例',
|
||||
mermaidCloseViewer: '关闭 Mermaid 大图'
|
||||
}
|
||||
} as const
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
.mermaid-diagram {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
margin: var(--space-3) 0;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
|
||||
.mermaid-diagram__actions {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: var(--space-2);
|
||||
right: var(--space-2);
|
||||
display: flex;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: var(--shadow-card);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mermaid-diagram__actions .icon-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.mermaid-diagram__viewport {
|
||||
min-width: 0;
|
||||
padding: var(--space-4);
|
||||
overflow-x: auto;
|
||||
overscroll-behavior-inline: contain;
|
||||
}
|
||||
|
||||
.mermaid-diagram__viewport:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.mermaid-diagram__viewport > svg {
|
||||
display: block;
|
||||
min-width: min(100%, 320px);
|
||||
max-width: none !important;
|
||||
height: auto;
|
||||
min-height: 80px;
|
||||
margin-inline: auto;
|
||||
font-family: var(--font-family-ui);
|
||||
}
|
||||
|
||||
.markdown-content .mermaid-diagram__source {
|
||||
max-height: 320px;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
border-radius: 0 0 var(--radius-control) var(--radius-control);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mermaid-diagram__loading,
|
||||
.mermaid-diagram--error > figcaption {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.mermaid-diagram--error {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-subtle);
|
||||
}
|
||||
|
||||
.mermaid-diagram--error > figcaption {
|
||||
color: var(--danger-strong);
|
||||
}
|
||||
|
||||
.markdown-content .mermaid-diagram--error > pre {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--danger-border);
|
||||
border-radius: 0 0 var(--radius-control) var(--radius-control);
|
||||
}
|
||||
|
||||
.mermaid-viewer-backdrop {
|
||||
position: fixed;
|
||||
z-index: 120;
|
||||
display: grid;
|
||||
padding: var(--space-4);
|
||||
background: var(--overlay-backdrop);
|
||||
inset: 38px 0 0;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.mermaid-viewer {
|
||||
display: grid;
|
||||
width: min(1280px, 100%);
|
||||
height: min(860px, calc(100vh - 70px));
|
||||
min-height: 0;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--surface-raised);
|
||||
box-shadow: var(--shadow-dialog);
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.mermaid-viewer__header {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.mermaid-viewer__header > div:first-child {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mermaid-viewer__header strong {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-section-title);
|
||||
}
|
||||
|
||||
.mermaid-viewer__header small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.mermaid-viewer__controls {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.mermaid-viewer__zoom {
|
||||
min-width: 48px;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-caption);
|
||||
font-variant-numeric: tabular-nums;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mermaid-viewer__canvas {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
padding: var(--space-6);
|
||||
overflow: auto;
|
||||
background: var(--surface-subtle);
|
||||
cursor: grab;
|
||||
overscroll-behavior: contain;
|
||||
place-items: center;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.mermaid-viewer__canvas--dragging {
|
||||
cursor: grabbing;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.mermaid-viewer__canvas:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.mermaid-viewer__diagram {
|
||||
display: grid;
|
||||
min-width: max-content;
|
||||
min-height: max-content;
|
||||
transform-origin: center;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.mermaid-viewer__diagram > svg {
|
||||
display: block;
|
||||
max-width: none !important;
|
||||
height: auto;
|
||||
font-family: var(--font-family-ui);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.mermaid-viewer-backdrop {
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.mermaid-viewer {
|
||||
height: calc(100vh - 54px);
|
||||
}
|
||||
|
||||
.mermaid-viewer__header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mermaid-viewer__controls {
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { ResolvedAppearanceTheme } from './theme'
|
||||
|
||||
const listeners = new Set<() => void>()
|
||||
let observer: MutationObserver | undefined
|
||||
|
||||
function resolvedDocumentTheme(): ResolvedAppearanceTheme {
|
||||
return document.documentElement.dataset.theme === 'dark'
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
|
||||
function notifyThemeListeners(): void {
|
||||
for (const listener of listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToDocumentTheme(listener: () => void): () => void {
|
||||
listeners.add(listener)
|
||||
if (
|
||||
listeners.size === 1 &&
|
||||
typeof MutationObserver === 'function'
|
||||
) {
|
||||
observer = new MutationObserver(notifyThemeListeners)
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme']
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
if (listeners.size === 0) {
|
||||
observer?.disconnect()
|
||||
observer = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useDocumentTheme(): ResolvedAppearanceTheme {
|
||||
return useSyncExternalStore(
|
||||
subscribeToDocumentTheme,
|
||||
resolvedDocumentTheme,
|
||||
resolvedDocumentTheme
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user