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 H2O1

\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('
1 说明 重点
\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 `${escapeHtml(text(node.from + 2, node.to - 1))}` 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 `
${escapeHtml(text(label.from + 2, label.to - 2))} ${inline(node, label.to, node.to).trim()}
\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> | undefined + +/** 按 GitHub/Typora 的名称查找 emoji,如 smile → 😄;未知名称返回 undefined */ +export async function emojiFor(name: string): Promise { + emojiLoad ??= import('gemoji').then(({ gemoji }) => { + const map = new Map() + for (const g of gemoji) for (const n of g.names) map.set(n, g.emoji) + return map + }) + return (await emojiLoad).get(name) +} + +/** 为渲染器输出的占位元素(表格单元格等处)填充公式与 emoji */ +export function hydrate(root: HTMLElement) { + for (const el of root.querySelectorAll('[data-math]')) { + void renderMath(el, el.dataset.math!, el.tagName === 'DIV') + } + for (const el of root.querySelectorAll('[data-emoji]')) { + void emojiFor(el.dataset.emoji!).then((e) => { + if (e) el.textContent = e + }) + } +} diff --git a/web/src/themes/base.css b/web/src/themes/base.css index b0638bb..9fde8e7 100644 --- a/web/src/themes/base.css +++ b/web/src/themes/base.css @@ -24,6 +24,7 @@ --rm-code-bg: #292d3e; --rm-code-text: #e1e4e8; --rm-code-font-size: 14px; + --rm-highlight-bg: rgba(255, 213, 0, 0.35); } @media (prefers-color-scheme: dark) { @@ -37,6 +38,7 @@ --rm-inline-code-bg: #2f2f2f; --rm-inline-code-text: #aac8e4; --rm-code-bg: #242424; + --rm-highlight-bg: rgba(255, 213, 0, 0.28); } } @@ -231,3 +233,45 @@ body { .cm-rm-code-copy { width: 36px; height: 36px; } .cm-line.cm-rm-code { padding: 0 12px; } } + +/* ---- Typora 扩展语法 ---- */ +.cm-rm-highlight, +.cm-rm-table-widget mark { background: var(--rm-highlight-bg); color: inherit; border-radius: 2px; padding: 0 1px; } +.cm-rm-sup { vertical-align: super; font-size: 0.75em; line-height: 0; } +.cm-rm-sub { vertical-align: sub; font-size: 0.75em; line-height: 0; } +.cm-rm-placeholder { color: var(--rm-text-muted); font-style: italic; } + +.cm-rm-math { cursor: pointer; } +.cm-rm-math-block { display: block; padding: 8px 0; text-align: center; overflow-x: auto; overflow-y: hidden; cursor: pointer; } +.cm-rm-math-block .katex-display { margin: 0; } +.cm-line.cm-rm-math-source { font-family: var(--rm-font-mono); font-size: 0.9em; color: var(--rm-text-muted); } +/* 编辑时显示在源码下方的预览 */ +.cm-rm-preview { margin: 4px 0 8px; padding: 8px 12px; border: 1px dashed var(--rm-divider); border-radius: 6px; cursor: default; } + +.cm-rm-mermaid { display: block; padding: 8px 0; text-align: center; overflow-x: auto; cursor: pointer; color: var(--rm-text-muted); } +.cm-rm-mermaid svg { max-width: 100%; height: auto; } +.cm-rm-render-error { + margin: 0; + padding: 8px 12px; + border-radius: 6px; + background: rgba(229, 72, 77, 0.08); + color: #e5484d; + font-family: var(--rm-font-mono); + font-size: 12px; + text-align: left; + white-space: pre-wrap; +} + +.cm-rm-emoji { cursor: text; } + +.cm-rm-footnote-ref { color: var(--rm-accent-strong); font-size: 0.75em; cursor: pointer; padding: 0 1px; } +.cm-line.cm-rm-footnote { font-size: 0.9em; color: var(--rm-text-muted); } +.cm-rm-footnote-label { color: var(--rm-accent-strong); } + +.cm-rm-toc { display: block; padding: 8px 12px; border-left: 3px solid var(--rm-divider); cursor: pointer; } +.cm-rm-toc-item { display: block; color: var(--rm-accent-strong); line-height: 1.8; cursor: pointer; text-decoration: none; } +.cm-rm-toc-item:hover { text-decoration: underline; } + +.cm-line.cm-rm-frontmatter { font-family: var(--rm-font-mono); font-size: 0.85em; color: var(--rm-text-muted); background: var(--rm-inline-code-bg); padding: 0 12px; } +.cm-line.cm-rm-frontmatter-begin { border-radius: 6px 6px 0 0; padding-top: 4px; } +.cm-line.cm-rm-frontmatter-end { border-radius: 0 0 6px 6px; padding-bottom: 4px; }