From 75000fc571c5e73725afd066acf096410a1e5150 Mon Sep 17 00:00:00 2001
From: eezd
Date: Sat, 17 Oct 2026 13:38:53 +0800
Subject: [PATCH] Add Typora extension syntax
- Parser extensions shared by the editor and the HTML renderer: ==highlight==, $inline math$
(Pandoc rules, so amounts like $5 are not math), $$ math blocks, footnote references and
definitions, [TOC], and YAML front matter
- Live preview: highlight, superscript/subscript, emoji names (gemoji), footnote references
with the definition as tooltip, a clickable table of contents, styled front matter
- Math (KaTeX) and Mermaid render in place; while editing a block the source is shown with a
live preview below. KaTeX, Mermaid and the emoji table are loaded only when a note uses them
- KaTeX is pinned to 0.18 so Mermaid shares the same copy
- Move editor focus tracking out of table.ts so block widgets and tables use the same state
---
web/package-lock.json | 1162 ++++++++++++++++++++++++++-
web/package.json | 3 +
web/src/editor/blocks.ts | 58 ++
web/src/editor/focus.ts | 37 +
web/src/editor/index.ts | 18 +-
web/src/editor/livepreview.ts | 86 +-
web/src/editor/table.test.ts | 3 +-
web/src/editor/table.ts | 54 +-
web/src/editor/widgets.ts | 202 ++++-
web/src/markdown/extensions.test.ts | 64 ++
web/src/markdown/extensions.ts | 198 +++++
web/src/markdown/html.test.ts | 15 +
web/src/markdown/html.ts | 30 +-
web/src/markdown/render.ts | 64 ++
web/src/themes/base.css | 44 +
15 files changed, 1991 insertions(+), 47 deletions(-)
create mode 100644 web/src/editor/blocks.ts
create mode 100644 web/src/editor/focus.ts
create mode 100644 web/src/markdown/extensions.test.ts
create mode 100644 web/src/markdown/extensions.ts
create mode 100644 web/src/markdown/render.ts
diff --git a/web/package-lock.json b/web/package-lock.json
index 7dfab37..ae80e87 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -14,6 +14,9 @@
"@lezer/common": "^1.5.3",
"@lezer/highlight": "^1.2.5",
"@lezer/markdown": "^1.8.0",
+ "gemoji": "^8.1.0",
+ "katex": "^0.18.11",
+ "mermaid": "^12.2.0",
"shiki": "^4.5.0",
"vue": "^3.5.44"
},
@@ -27,6 +30,19 @@
"vue-tsc": "^3.3.13"
}
},
+ "node_modules/@antfu/install-pkg": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz",
+ "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==",
+ "license": "MIT",
+ "dependencies": {
+ "package-manager-detector": "^1.8.0",
+ "tinyexec": "^1.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
@@ -73,6 +89,49 @@
"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/@chevrotain/cst-dts-gen": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz",
+ "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/gast": "13.2.0",
+ "@chevrotain/types": "13.2.0"
+ }
+ },
+ "node_modules/@chevrotain/gast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz",
+ "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/types": "13.2.0"
+ }
+ },
+ "node_modules/@chevrotain/regexp-to-ast": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz",
+ "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/types": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz",
+ "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@chevrotain/utils": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz",
+ "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==",
+ "license": "Apache-2.0"
+ },
"node_modules/@codemirror/autocomplete": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
@@ -210,6 +269,23 @@
"w3c-keyname": "^2.2.4"
}
},
+ "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.7",
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz",
+ "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@antfu/install-pkg": "^2.0.1",
+ "@iconify/types": "^2.0.0",
+ "import-meta-resolve": "^4.2.0"
+ }
+ },
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -310,6 +386,18 @@
"integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==",
"license": "MIT"
},
+ "node_modules/@mermaid-js/parser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz",
+ "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@chevrotain/types": "~13.2.0"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
"node_modules/@oxc-project/types": {
"version": "0.154.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.154.0.tgz",
@@ -711,6 +799,259 @@
"assertion-error": "^2.0.1"
}
},
+ "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",
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-fetch": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-dsv": "*"
+ }
+ },
+ "node_modules/@types/d3-force": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-format": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
+ "node_modules/@types/d3-hierarchy": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "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",
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-random": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz",
+ "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-selection": {
+ "version": "3.0.12",
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz",
+ "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "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.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz",
+ "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-interpolate": "*",
+ "@types/d3-selection": "*"
+ }
+ },
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
@@ -725,6 +1066,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "license": "MIT"
+ },
"node_modules/@types/hast": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
@@ -753,6 +1100,13 @@
"undici-types": "~8.9.0"
}
},
+ "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",
@@ -782,6 +1136,16 @@
"integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==",
"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-vue": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.10.tgz",
@@ -1071,6 +1435,22 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/chevrotain": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz",
+ "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@chevrotain/cst-dts-gen": "13.2.0",
+ "@chevrotain/gast": "13.2.0",
+ "@chevrotain/regexp-to-ast": "13.2.0",
+ "@chevrotain/types": "13.2.0",
+ "@chevrotain/utils": "13.2.0"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -1081,6 +1461,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/commander": {
+ "version": "15.0.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
+ "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
"node_modules/commonmark-spec": {
"version": "0.31.2",
"resolved": "https://registry.npmjs.org/commonmark-spec/-/commonmark-spec-0.31.2.tgz",
@@ -1088,6 +1477,15 @@
"dev": true,
"license": "CC-BY-SA-4.0"
},
+ "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/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
@@ -1100,6 +1498,529 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/cytoscape": {
+ "version": "3.34.4",
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.4.tgz",
+ "integrity": "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg==",
+ "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",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "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-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",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "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",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "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",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "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",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "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",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "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-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/dayjs": {
+ "version": "1.11.23",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz",
+ "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==",
+ "license": "MIT"
+ },
+ "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/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -1132,6 +2053,21 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/dompurify": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
+ "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/elkjs": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
+ "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==",
+ "license": "EPL-2.0"
+ },
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1151,6 +2087,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-toolkit": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz",
+ "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks",
+ "tests/types",
+ "tests/browser-compat"
+ ]
+ },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -1204,6 +2152,22 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/gemoji": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/gemoji/-/gemoji-8.1.0.tgz",
+ "integrity": "sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "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/happy-dom": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-21.0.3.tgz",
@@ -1269,6 +2233,65 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/katex": {
+ "version": "0.18.11",
+ "resolved": "https://registry.npmjs.org/katex/-/katex-0.18.11.tgz",
+ "integrity": "sha512-yvyz/2VMKqoho/35Hat5w37wIEdWJ5ElVMidTAlWrxr5uW4aBGsEpveTmKh18/zw08v/7lGswCPon4OhqmqrTA==",
+ "deprecated": "Accidentally published with breaking changes. Use 0.19.0 instead.",
+ "funding": [
+ "https://opencollective.com/katex",
+ "https://github.com/sponsors/katex"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^15.0.0"
+ },
+ "bin": {
+ "katex": "cli.js"
+ }
+ },
+ "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/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
@@ -1542,6 +2565,12 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
"node_modules/magic-string": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
@@ -1552,6 +2581,18 @@
"@jridgewell/sourcemap-codec": "^1.6.0"
}
},
+ "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/mdast-util-to-hast": {
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
@@ -1573,6 +2614,40 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/mermaid": {
+ "version": "12.2.0",
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.2.0.tgz",
+ "integrity": "sha512-w7nr/SFrLO7BaVTs3dQTvYSV6nYSBj0Yy9DjBhsIJtAcHqRzXSq4Pg32ujHZ/AdWsAMNEvvhbm5dt9E2tOiHzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@braintree/sanitize-url": "^7.1.2",
+ "@iconify/utils": "^3.0.2",
+ "@mermaid-js/parser": "^2.0.1",
+ "@types/d3": "^7.4.3",
+ "@upsetjs/venn.js": "^2.0.0",
+ "chevrotain": "~13.2.0",
+ "cytoscape": "^3.34.0",
+ "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.21",
+ "dompurify": "^3.4.12",
+ "elkjs": "^0.9.3",
+ "es-toolkit": "^1.45.1",
+ "katex": "^0.18.0",
+ "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"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
"node_modules/micromark-util-character": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
@@ -1718,6 +2793,12 @@
"regex-recursion": "^6.0.2"
}
},
+ "node_modules/package-manager-detector": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz",
+ "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==",
+ "license": "MIT"
+ },
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -1725,6 +2806,12 @@
"dev": true,
"license": "MIT"
},
+ "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/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -1744,6 +2831,22 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "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.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
@@ -1806,6 +2909,12 @@
"integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
"license": "MIT"
},
+ "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/rolldown": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.14.tgz",
@@ -1840,6 +2949,30 @@
"@rolldown/binding-win32-x64-msvc": "1.2.14"
}
},
+ "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/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/shiki": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz",
@@ -1905,6 +3038,12 @@
"integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==",
"license": "MIT"
},
+ "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/tinybench": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz",
@@ -1919,7 +3058,6 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1952,6 +3090,15 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "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/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
@@ -2041,6 +3188,19 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/uuid": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz",
+ "integrity": "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist-node/bin/uuid"
+ }
+ },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
diff --git a/web/package.json b/web/package.json
index f04dc07..16b1712 100644
--- a/web/package.json
+++ b/web/package.json
@@ -17,6 +17,9 @@
"@lezer/common": "^1.5.3",
"@lezer/highlight": "^1.2.5",
"@lezer/markdown": "^1.8.0",
+ "gemoji": "^8.1.0",
+ "katex": "^0.18.11",
+ "mermaid": "^12.2.0",
"shiki": "^4.5.0",
"vue": "^3.5.44"
},
diff --git a/web/src/editor/blocks.ts b/web/src/editor/blocks.ts
new file mode 100644
index 0000000..e8f6ba0
--- /dev/null
+++ b/web/src/editor/blocks.ts
@@ -0,0 +1,58 @@
+import { syntaxTree } from '@codemirror/language'
+import { StateField, type EditorState, type Range } from '@codemirror/state'
+import { Decoration, EditorView, type DecorationSet } from '@codemirror/view'
+import type { SyntaxNode } from '@lezer/common'
+import { codeContentRange, fenceLanguage } from './codeblocks'
+import { editorFocused, setEditorFocus } from './focus'
+import { MathWidget, MermaidWidget } from './widgets'
+
+// 公式块与 Mermaid 图表:未编辑时整块替换为渲染结果;光标在块内时显示源码,并在下方实时预览。
+// 跨行的块级替换只能由 StateField 提供;与表格一样只处理顶层的块
+
+/** 返回块的源码内容及其相对块起点的偏移(点击渲染结果时光标落在内容开头) */
+function blockContent(state: EditorState, node: SyntaxNode): { text: string; offset: number } | null {
+ if (node.name === 'BlockMath') {
+ const marks = node.getChildren('BlockMathMark')
+ const from = state.doc.lineAt(node.from).to + 1
+ const to = marks.length > 1 ? state.doc.lineAt(marks[marks.length - 1].from).from - 1 : node.to
+ return { text: from <= to ? state.sliceDoc(from, to) : '', offset: Math.min(from, node.to) - node.from }
+ }
+ if (node.name === 'FencedCode') {
+ const info = node.getChild('CodeInfo')
+ if (!info || fenceLanguage(state, info.from, info.to) !== 'mermaid') return null
+ const { from, to } = codeContentRange(state, node)
+ return { text: state.sliceDoc(from, to), offset: from - node.from }
+ }
+ return null
+}
+
+function buildBlocks(state: EditorState): DecorationSet {
+ const out: Range[] = []
+ const sel = state.field(editorFocused) ? state.selection.ranges : []
+ for (let node = syntaxTree(state).topNode.firstChild; node; node = node.nextSibling) {
+ const content = blockContent(state, node)
+ if (!content) continue
+ const editing = sel.some((r) => r.from <= node.to && r.to >= node.from)
+ const widget = (editOffset: number | null) =>
+ node.name === 'BlockMath' ? new MathWidget(content.text, true, editOffset) : new MermaidWidget(content.text, editOffset)
+ if (editing) out.push(Decoration.widget({ widget: widget(null), block: true, side: 1 }).range(node.to))
+ else out.push(Decoration.replace({ widget: widget(content.offset), block: true }).range(node.from, node.to))
+ }
+ return Decoration.set(out)
+}
+
+export const renderedBlocks = StateField.define({
+ create: buildBlocks,
+ update(value, tr) {
+ if (
+ tr.docChanged ||
+ tr.selection ||
+ tr.effects.some((e) => e.is(setEditorFocus)) ||
+ syntaxTree(tr.startState) !== syntaxTree(tr.state)
+ ) {
+ return buildBlocks(tr.state)
+ }
+ return value
+ },
+ provide: (f) => EditorView.decorations.from(f),
+})
diff --git a/web/src/editor/focus.ts b/web/src/editor/focus.ts
new file mode 100644
index 0000000..7ae7787
--- /dev/null
+++ b/web/src/editor/focus.ts
@@ -0,0 +1,37 @@
+import { StateEffect, StateField } from '@codemirror/state'
+import { ViewPlugin, type EditorView, type ViewUpdate } from '@codemirror/view'
+
+// 编辑器焦点状态。块级渲染组件(表格、公式块、Mermaid)由 StateField 提供,拿不到视图的焦点,
+// 由视图插件把焦点变化同步进状态:获得焦点且光标在块内时显示源码,否则渲染
+// (打开笔记时光标默认在文首,不应因此展开开头的块)
+export const setEditorFocus = StateEffect.define()
+
+export const editorFocused = StateField.define({
+ create: () => false,
+ update(value, tr) {
+ for (const e of tr.effects) if (e.is(setEditorFocus)) value = e.value
+ return value
+ },
+})
+
+const focusTracker = ViewPlugin.fromClass(
+ class {
+ constructor(readonly view: EditorView) {
+ this.sync()
+ }
+ update(u: ViewUpdate) {
+ if (u.focusChanged) this.sync()
+ }
+ // 不能在 update 中同步 dispatch,推迟到微任务
+ sync() {
+ queueMicrotask(() => {
+ const focused = this.view.hasFocus
+ if (this.view.state.field(editorFocused, false) !== undefined && this.view.state.field(editorFocused) !== focused) {
+ this.view.dispatch({ effects: setEditorFocus.of(focused) })
+ }
+ })
+ }
+ },
+)
+
+export const editorFocus = [editorFocused, focusTracker]
diff --git a/web/src/editor/index.ts b/web/src/editor/index.ts
index 9722e6a..27ef1d4 100644
--- a/web/src/editor/index.ts
+++ b/web/src/editor/index.ts
@@ -1,14 +1,28 @@
import { defaultKeymap, history, historyKeymap, indentWithTab, redo } from '@codemirror/commands'
import { markdownKeymap, markdownLanguage } from '@codemirror/lang-markdown'
+import { Language } from '@codemirror/language'
import { EditorState, Prec, type Extension } from '@codemirror/state'
import { drawSelection, EditorView, keymap, type ViewUpdate } from '@codemirror/view'
+import type { MarkdownParser } from '@lezer/markdown'
+import { typoraExtensions } from '../markdown/extensions'
+import { renderedBlocks } from './blocks'
import { codeBlockHighlight } from './codeblocks'
+import { editorFocus } from './focus'
import { livePreview, resolveImageUrl } from './livepreview'
import { tableEditing } from './table'
import { markdownTheme } from './theme'
export { pasteImages } from './paste'
+// 在 lang-markdown 的语法集(CommonMark + GFM + 上下标 + emoji)上加 Typora 扩展。
+// 沿用其语言数据,markdownKeymap 中的命令据此识别 Markdown(isActiveAt 按语言数据判断)
+const rawmarkLanguage = new Language(
+ markdownLanguage.data,
+ (markdownLanguage.parser as MarkdownParser).configure(typoraExtensions),
+ [],
+ 'markdown',
+)
+
export interface EditorStateOptions {
doc: string
resolveImage?: (src: string) => string
@@ -25,12 +39,14 @@ export function createEditorState(opts: EditorStateOptions): EditorState {
EditorView.lineWrapping,
// 不用 lang-markdown 的 markdown():它会静态引入 lang-html/css/javascript 用于嵌套 HTML 高亮,
// 首屏包 gzip 从约 108 KB 增至约 180 KB,而 Live Preview 不需要 HTML 高亮
- markdownLanguage,
+ rawmarkLanguage,
Prec.high(keymap.of(markdownKeymap)),
markdownTheme,
livePreview,
codeBlockHighlight,
+ editorFocus,
tableEditing,
+ renderedBlocks,
// CodeMirror 只在 Linux/Mac 上把 Ctrl/Cmd+Shift+Z 绑定为重做,Windows 上会退化成撤销;
// 与 Typora、VS Code 保持一致,所有平台都支持
keymap.of([indentWithTab, { key: 'Mod-Shift-z', run: redo, preventDefault: true }, ...defaultKeymap, ...historyKeymap]),
diff --git a/web/src/editor/livepreview.ts b/web/src/editor/livepreview.ts
index 84d16c8..2004498 100644
--- a/web/src/editor/livepreview.ts
+++ b/web/src/editor/livepreview.ts
@@ -3,7 +3,18 @@ import { Facet, type EditorState, type Range } from '@codemirror/state'
import { Decoration, EditorView, ViewPlugin, type DecorationSet, type ViewUpdate } from '@codemirror/view'
import type { SyntaxNode } from '@lezer/common'
import { codeContentRange, fenceLanguage, fencedCodeAt } from './codeblocks'
-import { BulletWidget, CheckboxWidget, CodeHeaderWidget, HorizontalRuleWidget, ImageWidget } from './widgets'
+import {
+ BulletWidget,
+ CheckboxWidget,
+ CodeHeaderWidget,
+ EmojiWidget,
+ FootnoteRefWidget,
+ HorizontalRuleWidget,
+ ImageWidget,
+ MathWidget,
+ TocWidget,
+ type TocEntry,
+} from './widgets'
// 把笔记中书写的图片地址解析为浏览器可加载的 URL(相对路径需结合笔记所在目录)
export const resolveImageUrl = Facet.define<(src: string) => string, (src: string) => string>({
@@ -13,6 +24,10 @@ export const resolveImageUrl = Facet.define<(src: string) => string, (src: strin
const hidden = Decoration.replace({})
const inlineCode = Decoration.mark({ class: 'cm-rm-inline-code' })
const linkText = Decoration.mark({ class: 'cm-rm-link' })
+const highlightText = Decoration.mark({ class: 'cm-rm-highlight' })
+const superscriptText = Decoration.mark({ class: 'cm-rm-sup' })
+const subscriptText = Decoration.mark({ class: 'cm-rm-sub' })
+const footnoteLabel = Decoration.mark({ class: 'cm-rm-footnote-label' })
const lineDecos = new Map()
function lineDeco(cls: string): Decoration {
@@ -66,6 +81,32 @@ function buildDecorations(view: EditorView): DecorationSet {
return end
}
+ // 脚注定义(标签 → 内容)与目录条目,只在可见区域出现相应语法时才计算
+ let footnoteMap: Map | undefined
+ function footnotes() {
+ if (!footnoteMap) {
+ footnoteMap = new Map()
+ for (let n = syntaxTree(state).topNode.firstChild; n; n = n.nextSibling) {
+ if (n.name !== 'FootnoteDefinition') continue
+ const label = n.getChild('FootnoteLabel')!
+ footnoteMap.set(state.sliceDoc(label.from + 2, label.to - 2), state.sliceDoc(label.to, n.to).trim())
+ }
+ }
+ return footnoteMap
+ }
+
+ function tocEntries(): TocEntry[] {
+ const entries: TocEntry[] = []
+ for (let n = syntaxTree(state).topNode.firstChild; n; n = n.nextSibling) {
+ const m = /^(ATX|Setext)Heading(\d)$/.exec(n.name)
+ if (!m) continue
+ const raw = m[1] === 'ATX' ? state.sliceDoc(n.from, n.to).replace(/^#+\s*/, '').replace(/\s+#+\s*$/, '') : doc.lineAt(n.from).text
+ const text = raw.replace(/\*\*|__|==|~~|`/g, '').trim()
+ if (text) entries.push({ level: Number(m[2]), text, pos: n.from })
+ }
+ return entries
+ }
+
for (const { from, to } of view.visibleRanges) {
syntaxTree(state).iterate({
from,
@@ -90,8 +131,51 @@ function buildDecorations(view: EditorView): DecorationSet {
case 'StrikethroughMark':
case 'SuperscriptMark':
case 'SubscriptMark':
+ case 'HighlightMark':
if (!touches(node.parent!)) hide(node.from, node.to)
return
+ case 'Highlight':
+ out.push(highlightText.range(node.from, node.to))
+ return
+ case 'Superscript':
+ out.push(superscriptText.range(node.from, node.to))
+ return
+ case 'Subscript':
+ out.push(subscriptText.range(node.from, node.to))
+ return
+ case 'InlineMath':
+ if (!touches(node) && lineOf(node.from) === lineOf(node.to)) {
+ const widget = new MathWidget(state.sliceDoc(node.from + 1, node.to - 1), false, 1)
+ out.push(Decoration.replace({ widget }).range(node.from, node.to))
+ }
+ return false
+ case 'Emoji':
+ if (!touches(node)) out.push(Decoration.replace({ widget: new EmojiWidget(state.sliceDoc(node.from + 1, node.to - 1)) }).range(node.from, node.to))
+ return
+ case 'FootnoteReference':
+ if (!touches(node)) {
+ const label = state.sliceDoc(node.from + 2, node.to - 1)
+ out.push(Decoration.replace({ widget: new FootnoteRefWidget(label, footnotes().get(label)) }).range(node.from, node.to))
+ }
+ return
+ case 'FootnoteDefinition':
+ classLines(node.from, node.to, () => 'cm-rm-footnote')
+ return
+ case 'FootnoteLabel':
+ if (!isActive(node.from)) out.push(footnoteLabel.range(node.from, node.to))
+ return
+ case 'TableOfContents':
+ if (!isActive(node.from)) out.push(Decoration.replace({ widget: new TocWidget(tocEntries()) }).range(node.from, node.to))
+ return
+ case 'FrontMatter':
+ classLines(node.from, node.to, (n, first, last) =>
+ n === first ? 'cm-rm-frontmatter cm-rm-frontmatter-begin' : n === last ? 'cm-rm-frontmatter cm-rm-frontmatter-end' : 'cm-rm-frontmatter',
+ )
+ return false
+ case 'BlockMath':
+ // 未编辑时由 blocks.ts 整块替换为渲染结果,这里只负责编辑时的源码样式
+ classLines(node.from, node.to, () => 'cm-rm-math-source')
+ return false
case 'InlineCode':
out.push(inlineCode.range(node.from, node.to))
return
diff --git a/web/src/editor/table.test.ts b/web/src/editor/table.test.ts
index 962e432..885005c 100644
--- a/web/src/editor/table.test.ts
+++ b/web/src/editor/table.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { createEditorState } from './index'
-import { setEditorFocus, tableField } from './table'
+import { setEditorFocus } from './focus'
+import { tableField } from './table'
const doc = '| a | b |\n| --- | --- |\n| 1 | 2 |\n\n正文'
diff --git a/web/src/editor/table.ts b/web/src/editor/table.ts
index 3e40c0b..34522bc 100644
--- a/web/src/editor/table.ts
+++ b/web/src/editor/table.ts
@@ -1,9 +1,11 @@
import { isolateHistory, redo, undo } from '@codemirror/commands'
import { syntaxTree } from '@codemirror/language'
-import { StateEffect, StateField, type EditorState, type Range } from '@codemirror/state'
-import { Decoration, EditorView, ViewPlugin, WidgetType, type DecorationSet, type ViewUpdate } from '@codemirror/view'
+import { StateField, type EditorState, type Range } from '@codemirror/state'
+import { Decoration, EditorView, WidgetType, type DecorationSet } from '@codemirror/view'
import type { SyntaxNode } from '@lezer/common'
import { renderInlineMarkdown } from '../markdown/html'
+import { hydrate } from '../markdown/render'
+import { editorFocused, setEditorFocus } from './focus'
type Align = '' | 'left' | 'center' | 'right'
@@ -94,6 +96,11 @@ function findTable(state: EditorState, pos: number): SyntaxNode | null {
// ---- 渲染组件 ----
+function renderCell(cell: HTMLElement, raw: string) {
+ cell.innerHTML = renderInlineMarkdown(raw)
+ hydrate(cell)
+}
+
interface WidgetState {
model: TableModel
// 最近编辑的单元格,工具栏操作以它为准
@@ -164,7 +171,7 @@ class TableWidget extends WidgetType {
cell.textContent = raw
placeCaretAtEnd(cell)
}
- } else if (old.rows[r].cells[c] !== raw) cell.innerHTML = renderInlineMarkdown(raw)
+ } else if (old.rows[r].cells[c] !== raw) renderCell(cell, raw)
})
return true
}
@@ -184,7 +191,7 @@ class TableWidget extends WidgetType {
cell.dataset.r = String(r)
cell.dataset.c = String(c)
if (this.model.aligns[c]) cell.style.textAlign = this.model.aligns[c]
- cell.innerHTML = renderInlineMarkdown(raw)
+ renderCell(cell, raw)
bindCell(view, wrap, cell)
tr.append(cell)
})
@@ -356,7 +363,7 @@ function bindCell(view: EditorView, wrap: HTMLElement, cell: HTMLElement) {
cell.addEventListener('blur', () => {
cell.removeAttribute('contenteditable')
- cell.innerHTML = renderInlineMarkdown(st().model.rows[r()]?.cells[c()] ?? '')
+ renderCell(cell, st().model.rows[r()]?.cells[c()] ?? '')
})
const moveTo = (nr: number, nc: number) => {
@@ -402,39 +409,8 @@ function bindCell(view: EditorView, wrap: HTMLElement, cell: HTMLElement) {
// ---- 装饰 ----
-// StateField 拿不到视图的焦点状态,由视图插件把焦点变化同步进状态
-const setEditorFocus = StateEffect.define()
-
-const editorFocused = StateField.define({
- create: () => false,
- update(value, tr) {
- for (const e of tr.effects) if (e.is(setEditorFocus)) value = e.value
- return value
- },
-})
-
-const focusTracker = ViewPlugin.fromClass(
- class {
- constructor(readonly view: EditorView) {
- this.sync()
- }
- update(u: ViewUpdate) {
- if (u.focusChanged) this.sync()
- }
- // 不能在 update 中同步 dispatch,推迟到微任务
- sync() {
- queueMicrotask(() => {
- const focused = this.view.hasFocus
- if (this.view.state.field(editorFocused, false) !== undefined && this.view.state.field(editorFocused) !== focused) {
- this.view.dispatch({ effects: setEditorFocus.of(focused) })
- }
- })
- }
- },
-)
-
// 块级替换装饰跨越多行,只能由 StateField 提供,不能来自 ViewPlugin。
-// 与其他语法一致:编辑器获得焦点且光标在表格内时才显示源码(打开笔记时光标默认在文首,不应因此展开开头的表格)
+// 与其他语法一致:编辑器获得焦点且光标在表格内时才显示源码(见 focus.ts)
function buildTables(state: EditorState): DecorationSet {
const out: Range[] = []
const sel = state.field(editorFocused) ? state.selection.ranges : []
@@ -465,6 +441,4 @@ export const tableField = StateField.define({
provide: (f) => EditorView.decorations.from(f),
})
-export { setEditorFocus }
-
-export const tableEditing = [editorFocused, tableField, focusTracker]
+export const tableEditing = tableField
diff --git a/web/src/editor/widgets.ts b/web/src/editor/widgets.ts
index 1dc70a5..ce17ddc 100644
--- a/web/src/editor/widgets.ts
+++ b/web/src/editor/widgets.ts
@@ -1,4 +1,5 @@
-import { WidgetType, type EditorView } from '@codemirror/view'
+import { EditorView, WidgetType } from '@codemirror/view'
+import { emojiFor, renderMath, renderMermaid } from '../markdown/render'
export class BulletWidget extends WidgetType {
eq() {
@@ -156,3 +157,202 @@ export class ImageWidget extends WidgetType {
return 200
}
}
+
+// ---- 扩展语法(公式、Mermaid、emoji、脚注、目录) ----
+
+// 点击渲染结果时把光标移到源码中(offset 为相对组件起点的位置)以展开编辑,做法同图片
+function editOnMousedown(el: HTMLElement, view: EditorView, offset: number) {
+ el.addEventListener('mousedown', (e) => {
+ if (e.button !== 0) return
+ e.preventDefault()
+ view.dispatch({ selection: { anchor: Math.min(view.posAtDOM(el) + offset, view.state.doc.length) } })
+ view.focus()
+ })
+}
+
+function showRenderError(el: HTMLElement, err: unknown) {
+ const pre = document.createElement('pre')
+ pre.className = 'cm-rm-render-error'
+ pre.textContent = err instanceof Error ? err.message : String(err)
+ el.replaceChildren(pre)
+}
+
+/**
+ * 公式。editOffset 为 null 时是编辑中显示在源码下方的预览(不响应点击,内容变化时原地更新),
+ * 否则是替换源码的渲染结果,点击后光标移到源码 editOffset 处
+ */
+export class MathWidget extends WidgetType {
+ constructor(
+ readonly tex: string,
+ readonly display: boolean,
+ readonly editOffset: number | null,
+ ) {
+ super()
+ }
+ eq(other: MathWidget) {
+ return other.tex === this.tex && other.display === this.display && other.editOffset === this.editOffset
+ }
+ toDOM(view: EditorView) {
+ const el = document.createElement(this.display ? 'div' : 'span')
+ el.className = this.display ? 'cm-rm-math-block' : 'cm-rm-math'
+ if (this.editOffset === null) el.classList.add('cm-rm-preview')
+ else editOnMousedown(el, view, this.editOffset)
+ this.render(el, view)
+ return el
+ }
+ // 只复用预览元素:从渲染结果切到编辑时,编辑器会尝试用新组件更新旧元素(旧元素带点击编辑的监听)
+ updateDOM(dom: HTMLElement, view: EditorView) {
+ if (this.editOffset !== null || !dom.classList.contains('cm-rm-preview')) return false
+ this.render(dom, view)
+ return true
+ }
+ private render(el: HTMLElement, view: EditorView) {
+ if (!this.tex.trim()) {
+ el.textContent = '空公式'
+ el.classList.add('cm-rm-placeholder')
+ return
+ }
+ el.classList.remove('cm-rm-placeholder')
+ // 加载 KaTeX 前先显示源码
+ if (!el.firstChild) el.textContent = this.tex
+ void renderMath(el, this.tex, this.display).then(() => view.requestMeasure())
+ }
+ get estimatedHeight() {
+ return this.display ? 60 : -1
+ }
+}
+
+const mermaidSeqs = new WeakMap()
+let mermaidSeq = 0
+
+/** Mermaid 图表,editOffset 含义同 MathWidget */
+export class MermaidWidget extends WidgetType {
+ constructor(
+ readonly code: string,
+ readonly editOffset: number | null,
+ ) {
+ super()
+ }
+ eq(other: MermaidWidget) {
+ return other.code === this.code && other.editOffset === this.editOffset
+ }
+ toDOM(view: EditorView) {
+ const el = document.createElement('div')
+ el.className = 'cm-rm-mermaid'
+ if (this.editOffset === null) el.classList.add('cm-rm-preview')
+ else editOnMousedown(el, view, this.editOffset)
+ el.textContent = '图表渲染中…'
+ this.render(el, view)
+ return el
+ }
+ // 只复用预览元素:从渲染结果切到编辑时,编辑器会尝试用新组件更新旧元素(旧元素带点击编辑的监听)
+ updateDOM(dom: HTMLElement, view: EditorView) {
+ if (this.editOffset !== null || !dom.classList.contains('cm-rm-preview')) return false
+ this.render(dom, view)
+ return true
+ }
+ // 渲染是异步的,连续输入时只采用最后一次的结果,并保留旧图直到新图就绪,避免闪烁
+ private render(el: HTMLElement, view: EditorView) {
+ const seq = ++mermaidSeq
+ mermaidSeqs.set(el, seq)
+ if (!this.code.trim()) {
+ el.textContent = '空图表'
+ return
+ }
+ renderMermaid(this.code).then(
+ (svg) => {
+ if (mermaidSeqs.get(el) !== seq) return
+ el.innerHTML = svg
+ view.requestMeasure()
+ },
+ (err) => {
+ if (mermaidSeqs.get(el) !== seq) return
+ showRenderError(el, err)
+ view.requestMeasure()
+ },
+ )
+ }
+ get estimatedHeight() {
+ return 240
+ }
+}
+
+export class EmojiWidget extends WidgetType {
+ constructor(readonly name: string) {
+ super()
+ }
+ eq(other: EmojiWidget) {
+ return other.name === this.name
+ }
+ toDOM(view: EditorView) {
+ const el = document.createElement('span')
+ el.className = 'cm-rm-emoji'
+ // 名称表加载前或名称未知时显示原文
+ el.textContent = `:${this.name}:`
+ void emojiFor(this.name).then((e) => {
+ if (e) el.textContent = e
+ })
+ editOnMousedown(el, view, 1)
+ return el
+ }
+}
+
+export class FootnoteRefWidget extends WidgetType {
+ constructor(
+ readonly label: string,
+ readonly definition: string | undefined,
+ ) {
+ super()
+ }
+ eq(other: FootnoteRefWidget) {
+ return other.label === this.label && other.definition === this.definition
+ }
+ toDOM(view: EditorView) {
+ const el = document.createElement('sup')
+ el.className = 'cm-rm-footnote-ref'
+ el.textContent = this.label
+ el.title = this.definition ?? '未找到脚注定义'
+ editOnMousedown(el, view, 2)
+ return el
+ }
+}
+
+export interface TocEntry {
+ level: number
+ text: string
+ pos: number
+}
+
+export class TocWidget extends WidgetType {
+ constructor(readonly entries: TocEntry[]) {
+ super()
+ }
+ eq(other: TocWidget) {
+ return JSON.stringify(other.entries) === JSON.stringify(this.entries)
+ }
+ toDOM(view: EditorView) {
+ const el = document.createElement('div')
+ el.className = 'cm-rm-toc'
+ if (!this.entries.length) {
+ el.textContent = '目录(还没有标题)'
+ el.classList.add('cm-rm-placeholder')
+ }
+ const minLevel = Math.min(...this.entries.map((e) => e.level))
+ for (const entry of this.entries) {
+ const item = document.createElement('a')
+ item.className = 'cm-rm-toc-item'
+ item.style.paddingLeft = `${(entry.level - minLevel) * 1.2}em`
+ item.textContent = entry.text
+ item.addEventListener('mousedown', (e) => {
+ e.preventDefault()
+ e.stopPropagation()
+ view.dispatch({ selection: { anchor: entry.pos }, effects: EditorView.scrollIntoView(entry.pos, { y: 'start', yMargin: 24 }) })
+ view.focus()
+ })
+ el.append(item)
+ }
+ // 点击目录空白处展开 [TOC] 源码
+ editOnMousedown(el, view, 0)
+ return el
+ }
+}
diff --git a/web/src/markdown/extensions.test.ts b/web/src/markdown/extensions.test.ts
new file mode 100644
index 0000000..de76734
--- /dev/null
+++ b/web/src/markdown/extensions.test.ts
@@ -0,0 +1,64 @@
+import { parser as base } from '@lezer/markdown'
+import { GFM } from '@lezer/markdown'
+import { describe, expect, it } from 'vitest'
+import { typoraExtensions } from './extensions'
+
+const parser = base.configure([GFM, ...typoraExtensions])
+
+/** 列出指定类型节点覆盖的原文 */
+function nodes(src: string, name: string): string[] {
+ const out: string[] = []
+ parser.parse(src).iterate({
+ enter(n) {
+ if (n.name === name) out.push(src.slice(n.from, n.to))
+ },
+ })
+ return out
+}
+
+describe('Typora 扩展语法', () => {
+ it('==高亮==', () => {
+ expect(nodes('a ==b c== d', 'Highlight')).toEqual(['==b c=='])
+ expect(nodes('a == b ==', 'Highlight')).toEqual([])
+ expect(nodes('x === y', 'Highlight')).toEqual([])
+ })
+
+ it('行内公式:金额与转义不误判', () => {
+ expect(nodes('设 $x^2 + y$ 为', 'InlineMath')).toEqual(['$x^2 + y$'])
+ expect(nodes('价格 $5 和 $10', 'InlineMath')).toEqual([])
+ expect(nodes('$ x$ 与 $x $', 'InlineMath')).toEqual([])
+ expect(nodes('\\$x$ 不是公式', 'InlineMath')).toEqual([])
+ expect(nodes('$a\\$b$', 'InlineMath')).toEqual(['$a\\$b$'])
+ expect(nodes('`$x$`', 'InlineMath')).toEqual([])
+ })
+
+ it('公式块:可打断段落,只在顶层识别,未闭合延续到文末', () => {
+ expect(nodes('段落\n$$\na^2\n$$\n后文', 'BlockMath')).toEqual(['$$\na^2\n$$'])
+ expect(nodes('> $$\n> x\n> $$\n\n外面', 'BlockMath')).toEqual([])
+ expect(nodes('$$\nx\n', 'BlockMath')).toEqual(['$$\nx\n'])
+ expect(nodes('$$x$$', 'BlockMath')).toEqual([])
+ })
+
+ it('脚注引用与定义', () => {
+ expect(nodes('正文[^1]与[^note]。', 'FootnoteReference')).toEqual(['[^1]', '[^note]'])
+ expect(nodes('[^ 1] 不是脚注', 'FootnoteReference')).toEqual([])
+ expect(nodes('[^1]: 第一行\n继续\n\n下一段', 'FootnoteDefinition')).toEqual(['[^1]: 第一行\n继续'])
+ expect(nodes('[^1]: 有 **粗体**', 'StrongEmphasis')).toEqual(['**粗体**'])
+ expect(nodes('[^1]: x', 'LinkReference')).toEqual([])
+ })
+
+ it('[TOC] 独占一行', () => {
+ expect(nodes('[TOC]\n\n# 标题', 'TableOfContents')).toEqual(['[TOC]'])
+ expect(nodes('[toc] ', 'TableOfContents')).toEqual(['[toc] '])
+ expect(nodes('见 [TOC] 处', 'TableOfContents')).toEqual([])
+ })
+
+ it('YAML front matter 只在文档首行', () => {
+ expect(nodes('---\ntitle: a\n---\n# 正文', 'FrontMatter')).toEqual(['---\ntitle: a\n---'])
+ expect(nodes('---\ntitle: a\n...\n正文', 'FrontMatter')).toEqual(['---\ntitle: a\n...'])
+ expect(nodes('正文\n\n---\na: b\n---', 'FrontMatter')).toEqual([])
+ expect(nodes('正文\n\n---\n', 'HorizontalRule')).toEqual(['---'])
+ // 闭合后的 --- 照常解析为分隔线
+ expect(nodes('---\na: 1\n---\n\n---\n', 'HorizontalRule')).toEqual(['---'])
+ })
+})
diff --git a/web/src/markdown/extensions.ts b/web/src/markdown/extensions.ts
new file mode 100644
index 0000000..f663c51
--- /dev/null
+++ b/web/src/markdown/extensions.ts
@@ -0,0 +1,198 @@
+// Typora 扩展语法:==高亮==、$行内公式$、$$公式块$$、脚注、[TOC]、YAML front matter。
+// 上标 ^x^、下标 ~x~、:emoji: 由 @lezer/markdown 自带的扩展解析
+import { tags } from '@lezer/highlight'
+import type { BlockContext, LeafBlockParser, Line, MarkdownConfig } from '@lezer/markdown'
+
+const PUNCTUATION = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~\xA1‐-‧]/
+
+const highlightDelim = { resolve: 'Highlight', mark: 'HighlightMark' }
+
+/** ==高亮==,定界规则与 GFM 删除线相同 */
+const Highlight: MarkdownConfig = {
+ defineNodes: [
+ { name: 'Highlight', style: { 'Highlight/...': tags.special(tags.content) } },
+ { name: 'HighlightMark', style: tags.processingInstruction },
+ ],
+ parseInline: [
+ {
+ name: 'Highlight',
+ parse(cx, next, pos) {
+ if (next !== 61 /* = */ || cx.char(pos + 1) !== 61 || cx.char(pos + 2) === 61) return -1
+ const before = cx.slice(pos - 1, pos)
+ const after = cx.slice(pos + 2, pos + 3)
+ const sBefore = /\s|^$/.test(before)
+ const sAfter = /\s|^$/.test(after)
+ const pBefore = PUNCTUATION.test(before)
+ const pAfter = PUNCTUATION.test(after)
+ return cx.addDelimiter(
+ highlightDelim,
+ pos,
+ pos + 2,
+ !sAfter && (!pAfter || sBefore || pBefore),
+ !sBefore && (!pBefore || sAfter || pAfter),
+ )
+ },
+ after: 'Emphasis',
+ },
+ ],
+}
+
+/**
+ * $行内公式$(Pandoc 规则):开头 $ 后不能是空白,结尾 $ 前不能是空白、后面不能紧跟数字,
+ * 因此"$5 和 $10"这类金额不会被当成公式;\$ 为转义
+ */
+const InlineMath: MarkdownConfig = {
+ defineNodes: [
+ { name: 'InlineMath', style: tags.monospace },
+ { name: 'InlineMathMark', style: tags.processingInstruction },
+ ],
+ parseInline: [
+ {
+ name: 'InlineMath',
+ parse(cx, next, pos) {
+ if (next !== 36 /* $ */ || cx.char(pos + 1) === 36 || /\s/.test(cx.slice(pos + 1, pos + 2))) return -1
+ for (let i = pos + 1; i < cx.end; i++) {
+ const ch = cx.char(i)
+ if (ch === 92 /* \ */) i++
+ else if (ch === 36 && i > pos + 1 && !/\s/.test(cx.slice(i - 1, i)) && !/\d/.test(cx.slice(i + 1, i + 2))) {
+ return cx.addElement(cx.elt('InlineMath', pos, i + 1, [cx.elt('InlineMathMark', pos, pos + 1), cx.elt('InlineMathMark', i, i + 1)]))
+ }
+ }
+ return -1
+ },
+ before: 'Emphasis',
+ },
+ ],
+}
+
+const isMathFence = (line: Line) => /^\$\$\s*$/.test(line.text.slice(line.pos))
+
+/**
+ * 独占一行的 $$ 之间为公式块,未闭合时延续到文档末尾。只在顶层识别:解析器的公开接口无法判断
+ * 下一行是否仍属于引用、列表等容器,与表格一样,容器内的公式块以源码显示
+ */
+const BlockMath: MarkdownConfig = {
+ defineNodes: [
+ { name: 'BlockMath', block: true, style: tags.monospace },
+ { name: 'BlockMathMark', style: tags.processingInstruction },
+ ],
+ parseBlock: [
+ {
+ name: 'BlockMath',
+ parse(cx, line) {
+ if (cx.parentType().name !== 'Document' || !isMathFence(line)) return false
+ const from = cx.lineStart + line.pos
+ const marks = [cx.elt('BlockMathMark', from, from + 2)]
+ while (cx.nextLine()) {
+ if (isMathFence(line)) {
+ marks.push(cx.elt('BlockMathMark', cx.lineStart + line.pos, cx.lineStart + line.pos + 2))
+ cx.nextLine()
+ break
+ }
+ }
+ cx.addElement(cx.elt('BlockMath', from, cx.prevLineEnd(), marks))
+ return true
+ },
+ endLeaf: (cx, line) => cx.parentType().name === 'Document' && isMathFence(line),
+ before: 'FencedCode',
+ },
+ ],
+}
+
+const FOOTNOTE_LABEL = /^\[\^([^\]\s]+)\]/
+
+/** 脚注引用 [^label] 与脚注定义 [^label]: 内容(定义可跨行,直到空行) */
+const Footnote: MarkdownConfig = {
+ defineNodes: [
+ { name: 'FootnoteReference', style: tags.labelName },
+ { name: 'FootnoteDefinition', block: true },
+ { name: 'FootnoteLabel', style: tags.labelName },
+ ],
+ parseInline: [
+ {
+ name: 'FootnoteReference',
+ parse(cx, next, pos) {
+ if (next !== 91 /* [ */ || cx.char(pos + 1) !== 94 /* ^ */) return -1
+ const m = FOOTNOTE_LABEL.exec(cx.slice(pos, cx.end))
+ return m ? cx.addElement(cx.elt('FootnoteReference', pos, pos + m[0].length)) : -1
+ },
+ before: 'Link',
+ },
+ ],
+ parseBlock: [
+ {
+ name: 'FootnoteDefinition',
+ leaf(_cx, leaf) {
+ const m = /^\[\^[^\]\s]+\]:/.exec(leaf.content)
+ if (!m) return null
+ const self: LeafBlockParser = {
+ // "[^1]: 文字"同时也是合法的链接引用定义;本解析器排在前面,续行时去掉其他候选,整块归为脚注定义。
+ // 须原地修改:调用方正在遍历这个数组
+ nextLine(_cx, _line, leaf) {
+ leaf.parsers.splice(0, leaf.parsers.length, self)
+ return false
+ },
+ finish(cx, leaf) {
+ const label = cx.elt('FootnoteLabel', leaf.start, leaf.start + m[0].length)
+ const body = cx.parser.parseInline(leaf.content.slice(m[0].length), leaf.start + m[0].length)
+ cx.addLeafElement(leaf, cx.elt('FootnoteDefinition', leaf.start, leaf.start + leaf.content.length, [label, ...body]))
+ return true
+ },
+ }
+ return self
+ },
+ before: 'LinkReference',
+ },
+ ],
+}
+
+/** 独占一行的 [TOC](不区分大小写)显示为目录 */
+const TableOfContents: MarkdownConfig = {
+ defineNodes: [{ name: 'TableOfContents', block: true, style: tags.meta }],
+ parseBlock: [
+ {
+ name: 'TableOfContents',
+ parse(cx, line) {
+ if (!/^\[toc\]\s*$/i.test(line.text.slice(line.pos))) return false
+ cx.addElement(cx.elt('TableOfContents', cx.lineStart + line.pos, cx.lineStart + line.text.length))
+ cx.nextLine()
+ return true
+ },
+ before: 'LinkReference',
+ },
+ ],
+}
+
+const isFrontMatterEnd = (line: Line) => /^(---|\.\.\.)\s*$/.test(line.text)
+
+/**
+ * 文档首行为 --- 时,到下一个 --- 或 ... 为 YAML front matter。解析器无法向后预读整个文档,
+ * 未闭合时延续到文末——与 Typora(首行输入 --- 即创建 front matter)及 GitHub、Hugo 的识别方式一致
+ */
+const FrontMatter: MarkdownConfig = {
+ defineNodes: [
+ { name: 'FrontMatter', block: true, style: tags.monospace },
+ { name: 'FrontMatterMark', style: tags.processingInstruction },
+ ],
+ parseBlock: [
+ {
+ name: 'FrontMatter',
+ parse(cx: BlockContext, line: Line) {
+ if (cx.lineStart !== 0 || cx.parentType().name !== 'Document' || !/^---\s*$/.test(line.text)) return false
+ const marks = [cx.elt('FrontMatterMark', 0, 3)]
+ while (cx.nextLine()) {
+ if (isFrontMatterEnd(line)) {
+ marks.push(cx.elt('FrontMatterMark', cx.lineStart, cx.lineStart + 3))
+ cx.nextLine()
+ break
+ }
+ }
+ cx.addElement(cx.elt('FrontMatter', 0, cx.prevLineEnd(), marks))
+ return true
+ },
+ before: 'HorizontalRule',
+ },
+ ],
+}
+
+export const typoraExtensions: MarkdownConfig[] = [FrontMatter, Highlight, InlineMath, BlockMath, Footnote, TableOfContents]
diff --git a/web/src/markdown/html.test.ts b/web/src/markdown/html.test.ts
index cd73986..b37100c 100644
--- a/web/src/markdown/html.test.ts
+++ b/web/src/markdown/html.test.ts
@@ -56,6 +56,21 @@ describe('安全模式', () => {
})
})
+describe('Typora 扩展', () => {
+ it('高亮、上下标、脚注引用', () => {
+ expect(renderMarkdown('==a== x^2^ H~2~O[^1]')).toBe('a x2 H2O
\n')
+ })
+ it('公式与 emoji 输出待填充的占位元素,内容被转义', () => {
+ expect(renderMarkdown('$aa<b :smile:
\n')
+ expect(renderMarkdown('$$\n">
\n$$')).toBe(
+ '"><img src=x onerror=alert(1)>
\n',
+ )
+ })
+ it('脚注定义、front matter 与 [TOC]', () => {
+ expect(renderMarkdown('---\ntitle: x\n---\n[TOC]\n\n[^1]: 说明 **重点**')).toBe('\n')
+ })
+})
+
describe('CommonMark spec', () => {
for (const e of examples) {
const test = PARSER_DEVIATIONS.has(e.number) ? it.fails : it
diff --git a/web/src/markdown/html.ts b/web/src/markdown/html.ts
index 7e6fd23..cb543f3 100644
--- a/web/src/markdown/html.ts
+++ b/web/src/markdown/html.ts
@@ -1,8 +1,9 @@
import type { SyntaxNode } from '@lezer/common'
import { Emoji, GFM, parser as commonmarkParser, Subscript, Superscript, type MarkdownParser } from '@lezer/markdown'
+import { typoraExtensions } from './extensions'
-// 与编辑器(lang-markdown 的 markdownLanguage)一致的语法集
-export const markdownParser = commonmarkParser.configure([GFM, Subscript, Superscript, Emoji])
+// 与编辑器一致的语法集(lang-markdown 的 markdownLanguage 加 Typora 扩展)
+export const markdownParser = commonmarkParser.configure([GFM, Subscript, Superscript, Emoji, ...typoraExtensions])
export { commonmarkParser }
@@ -263,6 +264,19 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
return `${innerOf(node, 'SuperscriptMark')}`
case 'Subscript':
return `${innerOf(node, 'SubscriptMark')}`
+ case 'Highlight':
+ return `${innerOf(node, 'HighlightMark')}`
+ // 公式与 emoji 输出占位元素,由 render.ts 的 hydrate 按需加载依赖后填充
+ case 'InlineMath': {
+ const tex = text(node.from + 1, node.to - 1)
+ return `${escapeHtml(tex)}`
+ }
+ case 'Emoji': {
+ const raw = text(node.from, node.to)
+ return `${escapeHtml(raw)}`
+ }
+ case 'FootnoteReference':
+ return ``
case 'InlineCode': {
const marks = node.getChildren('CodeMark')
let code = text(marks[0].to, marks[marks.length - 1].from).replace(/\n/g, ' ')
@@ -416,6 +430,18 @@ export function renderMarkdown(src: string, { parser = markdownParser, unsafe =
return rawHtml(contentLines(node, false, false).map((l) => l.rest).join('\n')) + '\n'
case 'Table':
return table(node)
+ case 'BlockMath': {
+ const marks = node.getChildren('BlockMathMark')
+ const from = Math.min(lineEnd(node.from) + 1, node.to)
+ const to = marks.length > 1 ? Math.max(from, lineStart(marks[marks.length - 1].from) - 1) : node.to
+ const tex = text(from, to)
+ return `${escapeHtml(tex)}
\n`
+ }
+ case 'FootnoteDefinition': {
+ const label = node.getChild('FootnoteLabel')!
+ return `\n`
+ }
+ // front matter 是元数据,不输出;[TOC] 在只读分享页面(M4)中生成
default:
return ''
}
diff --git a/web/src/markdown/render.ts b/web/src/markdown/render.ts
new file mode 100644
index 0000000..18a064c
--- /dev/null
+++ b/web/src/markdown/render.ts
@@ -0,0 +1,64 @@
+// 数学公式、Mermaid 图表、emoji 的渲染。依赖体积较大,首次用到时才加载(各自打成独立的包)
+import type { MermaidConfig } from 'mermaid'
+
+let katexLoad: Promise | undefined
+function loadKatex() {
+ katexLoad ??= Promise.all([import('katex'), import('katex/dist/katex.min.css')]).then(([m]) => m.default)
+ return katexLoad
+}
+
+/** 渲染公式;语法错误以红色显示错误信息而不是抛出 */
+export async function renderMath(el: HTMLElement, tex: string, displayMode: boolean): Promise {
+ const katex = await loadKatex()
+ katex.render(tex, el, { displayMode, throwOnError: false, output: 'htmlAndMathml' })
+}
+
+let mermaidLoad: Promise | undefined
+let mermaidSeq = 0
+function loadMermaid() {
+ mermaidLoad ??= import('mermaid').then(({ default: mermaid }) => {
+ const dark = matchMedia('(prefers-color-scheme: dark)').matches
+ // strict:图表中的 HTML 会被清理,点击事件等交互被禁用
+ const config: MermaidConfig = { startOnLoad: false, securityLevel: 'strict', theme: dark ? 'dark' : 'default' }
+ mermaid.initialize(config)
+ return mermaid
+ })
+ return mermaidLoad
+}
+
+/** 渲染 Mermaid 图表,返回 SVG 标记;语法错误时抛出带说明的 Error */
+export async function renderMermaid(code: string): Promise {
+ const mermaid = await loadMermaid()
+ const id = `rm-mermaid-${++mermaidSeq}`
+ try {
+ return (await mermaid.render(id, code)).svg
+ } catch (err) {
+ // 出错时 mermaid 会把临时节点留在 body 中
+ document.getElementById('d' + id)?.remove()
+ throw err instanceof Error ? err : new Error(String(err))
+ }
+}
+
+let emojiLoad: Promise