diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d4b136d..41fdc45 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -40,21 +40,24 @@ "@cospaia/prettier-plugin-clojure": "^0.0.2", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.3", + "@mdit/plugin-katex": "^0.23.2", + "@mdit/plugin-tasklist": "^0.22.2", "@prettier/plugin-xml": "^3.4.2", "@replit/codemirror-lang-svelte": "^6.0.0", "@toml-tools/lexer": "^1.0.0", "@toml-tools/parser": "^1.0.0", + "@types/markdown-it": "^14.1.2", "codemirror": "^6.0.2", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", "colors-named-hex": "^1.0.2", "groovy-beautify": "^0.0.17", + "highlight.js": "^11.11.1", "hsl-matcher": "^1.2.4", - "i": "^0.3.7", "java-parser": "^3.0.1", - "jsox": "^1.2.123", "linguist-languages": "^9.1.0", - "markdown-exit": "^1.0.0-beta.6", + "markdown-it": "^14.1.0", + "mermaid": "^11.12.1", "npm": "^11.6.2", "php-parser": "^3.2.5", "pinia": "^3.0.4", @@ -76,10 +79,11 @@ "eslint": "^9.39.1", "eslint-plugin-vue": "^10.5.1", "globals": "^16.5.0", + "happy-dom": "^20.0.10", "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", "unplugin-vue-components": "^30.0.0", - "vite": "^7.2.2", + "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", "vitepress": "^2.0.0-alpha.12", "vitest": "^4.0.8", @@ -87,6 +91,37 @@ "vue-tsc": "^3.1.3" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/install-pkg/node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@antfu/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -133,6 +168,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, "node_modules/@chevrotain/cst-dts-gen": { "version": "11.0.3", "resolved": "https://registry.npmmirror.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", @@ -1271,9 +1312,36 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "dev": true, "license": "MIT" }, + "node_modules/@iconify/utils": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@iconify/utils/-/utils-3.0.2.tgz", + "integrity": "sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@antfu/utils": "^9.2.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.1", + "globals": "^15.15.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.1.1", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@intlify/core-base": { "version": "11.1.12", "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.12.tgz", @@ -1574,6 +1642,98 @@ "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "license": "MIT" }, + "node_modules/@mdit/helper": { + "version": "0.22.1", + "resolved": "https://registry.npmmirror.com/@mdit/helper/-/helper-0.22.1.tgz", + "integrity": "sha512-lDpajcdAk84aYCNAM/Mi3djw38DJq7ocLw5VOSMu/u2YKX3/OD37a6Qb59in8Uyp4SiAbQoSHa8px6hgHEpB5g==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mdit/plugin-katex": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/@mdit/plugin-katex/-/plugin-katex-0.23.2.tgz", + "integrity": "sha512-3914dyl/mHrzrdHDm/ZDhOSUhAttZdF80jKfc1RK5gdTcDSpNekn7naTdeP002oey/bDp1tzooEq5UfbRaHXYg==", + "license": "MIT", + "dependencies": { + "@mdit/helper": "0.22.1", + "@mdit/plugin-tex": "0.22.2", + "@types/markdown-it": "^14.1.2", + "katex": "^0.16.25" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mdit/plugin-tasklist": { + "version": "0.22.2", + "resolved": "https://registry.npmmirror.com/@mdit/plugin-tasklist/-/plugin-tasklist-0.22.2.tgz", + "integrity": "sha512-tYxp4tDomTb9NzIphoDXWJxjQZxFuqP4PjU0H9AecUyWuSRP+HICCqe/HVNTTpB0+WDeuVtnxAW9kX08ekxUWw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mdit/plugin-tex": { + "version": "0.22.2", + "resolved": "https://registry.npmmirror.com/@mdit/plugin-tex/-/plugin-tex-0.22.2.tgz", + "integrity": "sha512-iniJQ9BPZc8AGdLPRoyC+nDA0SoDSe+AETma4y2dOk/EbaSZMYgMaZO843mk5JV7eJkfRc6TWcTIE2CqY2/9Rg==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.1.2" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "markdown-it": "^14.1.0" + }, + "peerDependenciesMeta": { + "markdown-it": { + "optional": true + } + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2355,46 +2515,90 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.14.0.tgz", - "integrity": "sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ==", + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/engine-javascript/-/engine-javascript-3.15.0.tgz", + "integrity": "sha512-ZedbOFpopibdLmvTz2sJPJgns8Xvyabe2QbmqMTz07kt1pTzfEvKZc5IqPVO/XFiEbbNyaOpjPBkkr1vlwS+qg==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.14.0", + "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz", - "integrity": "sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug==", + "node_modules/@shikijs/engine-javascript/node_modules/@shikijs/types": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", + "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.14.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.15.0.tgz", + "integrity": "sha512-HnqFsV11skAHvOArMZdLBZZApRSYS4LSztk2K3016Y9VCyZISnlYUYsL2hzlS7tPqKHvNqmI5JSUJZprXloMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.15.0", "@shikijs/vscode-textmate": "^10.0.2" } }, - "node_modules/@shikijs/langs": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.14.0.tgz", - "integrity": "sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg==", + "node_modules/@shikijs/engine-oniguruma/node_modules/@shikijs/types": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", + "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.14.0" + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/langs/-/langs-3.15.0.tgz", + "integrity": "sha512-WpRvEFvkVvO65uKYW4Rzxs+IG0gToyM8SARQMtGGsH4GDMNZrr60qdggXrFOsdfOVssG/QQGEl3FnJ3EZ+8w8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.15.0" + } + }, + "node_modules/@shikijs/langs/node_modules/@shikijs/types": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", + "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/themes": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.14.0.tgz", - "integrity": "sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA==", + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/themes/-/themes-3.15.0.tgz", + "integrity": "sha512-8ow2zWb1IDvCKjYb0KiLNrK4offFdkfNVPXb1OZykpLCzRU6j+efkY+Y7VQjNlNFXonSw+4AOdGYtmqykDbRiQ==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/types": "3.14.0" + "@shikijs/types": "3.15.0" + } + }, + "node_modules/@shikijs/themes/node_modules/@shikijs/types": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", + "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/transformers": { @@ -2463,6 +2667,259 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.0", + "resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.3", + "resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.11", + "resolved": "https://registry.npmmirror.com/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@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.npmmirror.com/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -2477,6 +2934,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-3.0.4.tgz", @@ -2498,14 +2961,12 @@ "version": "5.0.0", "resolved": "https://registry.npmmirror.com/@types/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", "resolved": "https://registry.npmmirror.com/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "dev": true, "license": "MIT", "dependencies": { "@types/linkify-it": "^5", @@ -2526,7 +2987,6 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/@types/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, "license": "MIT" }, "node_modules/@types/node": { @@ -2539,6 +2999,13 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@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.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", @@ -2553,6 +3020,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/whatwg-mimetype": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", + "integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.4", "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", @@ -3292,7 +3766,6 @@ "version": "8.15.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3355,7 +3828,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asn1.js": { @@ -3977,6 +4449,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", @@ -3988,7 +4469,6 @@ "version": "0.2.2", "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "devOptional": true, "license": "MIT" }, "node_modules/consola": { @@ -4037,6 +4517,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/create-ecdh": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/create-ecdh/-/create-ecdh-4.0.4.tgz", @@ -4176,11 +4665,524 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.npmmirror.com/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.13", + "resolved": "https://registry.npmmirror.com/dagre-d3-es/-/dagre-d3-es-7.0.13.tgz", + "integrity": "sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4243,6 +5245,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", @@ -4331,6 +5342,15 @@ "url": "https://bevry.me/fund" } }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "16.5.0", "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.5.0.tgz", @@ -4724,7 +5744,6 @@ "version": "1.0.7", "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", - "devOptional": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -5013,6 +6032,44 @@ "integrity": "sha512-n3GRn7wJMCoPpNOC9bhuHWxnTkb9CwVnQH1RJK4M/F3Edc7l2FOa7wLa8iL2eqt0sQgQLzbxSsvZ7En2fJ8ZUg==", "license": "MIT" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/happy-dom": { + "version": "20.0.10", + "resolved": "https://registry.npmmirror.com/happy-dom/-/happy-dom-20.0.10.tgz", + "integrity": "sha512-6umCCHcjQrhP5oXhrHQQvLB0bwb1UzHAHdsXy+FjtKoYjUhmNZsQL8NivwM1vDvNEChJabVrUYxUnp/ZdYmy2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.0.0", + "@types/whatwg-mimetype": "^3.0.2", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/happy-dom/node_modules/@types/node": { + "version": "20.19.25", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.25.tgz", + "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/happy-dom/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -5141,6 +6198,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -5189,12 +6255,16 @@ "dev": true, "license": "MIT" }, - "node_modules/i": { - "version": "0.3.7", - "resolved": "https://registry.npmmirror.com/i/-/i-0.3.7.tgz", - "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/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.4" + "node": ">=0.10.0" } }, "node_modules/ieee754": { @@ -5268,6 +6338,15 @@ "dev": true, "license": "ISC" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arguments": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz", @@ -5518,13 +6597,20 @@ "dev": true, "license": "MIT" }, - "node_modules/jsox": { - "version": "1.2.123", - "resolved": "https://registry.npmmirror.com/jsox/-/jsox-1.2.123.tgz", - "integrity": "sha512-LYordXJ/0Q4G8pUE1Pvh4fkfGvZY7lRe4WIJKl0wr0rtFDVw9lcdNW95GH0DceJ6E9xh41zJNW0vreEz7xOxCw==", + "node_modules/katex": { + "version": "0.16.25", + "resolved": "https://registry.npmmirror.com/katex/-/katex-0.16.25.tgz", + "integrity": "sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, "bin": { - "jsox": "lib/cli.js" + "katex": "cli.js" } }, "node_modules/keyv": { @@ -5537,6 +6623,11 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmmirror.com/klona/-/klona-2.0.6.tgz", @@ -5556,6 +6647,40 @@ "optional": true, "peer": true }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/langium/node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", @@ -5598,7 +6723,6 @@ "version": "1.1.2", "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", - "devOptional": true, "license": "MIT", "dependencies": { "mlly": "^1.7.4", @@ -5663,29 +6787,33 @@ "dev": true, "license": "MIT" }, - "node_modules/markdown-exit": { - "version": "1.0.0-beta.6", - "resolved": "https://registry.npmmirror.com/markdown-exit/-/markdown-exit-1.0.0-beta.6.tgz", - "integrity": "sha512-ZYw/ztUMNkk8yHYV6ythIeGHx3TxTRhXFQD1xZSPX9mwc5N2x7hHW9c4QojfO9ZKaBkioyPVneX9d/Eey8ilww==", + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "license": "MIT", "dependencies": { - "entities": "^7.0.0", + "argparse": "^2.0.1", + "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/markdown-exit/node_modules/entities": { - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz", - "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "engines": { + "node": ">= 20" } }, "node_modules/math-intrinsics": { @@ -5748,6 +6876,34 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.12.1", + "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.12.1.tgz", + "integrity": "sha512-UlIZrRariB11TY1RtTgUWp65tphtBv4CSq7vyS2ZZ2TgoMjs2nloq+wFqxiwcxlhHUvs7DPGgMjs2aeQxz5h9g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.1", + "@mermaid-js/parser": "^0.6.3", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.13", + "dayjs": "^1.11.18", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.2.1", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -5921,7 +7077,6 @@ "version": "1.8.0", "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "devOptional": true, "license": "MIT", "dependencies": { "acorn": "^8.15.0", @@ -5934,14 +7089,12 @@ "version": "0.1.8", "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "devOptional": true, "license": "MIT" }, "node_modules/mlly/node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.1.8", @@ -5953,7 +7106,6 @@ "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/muggle-string": { @@ -8636,6 +9788,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-manager-detector": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.5.0.tgz", + "integrity": "sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==", + "license": "MIT" + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", @@ -8681,6 +9839,12 @@ "dev": true, "license": "MIT" }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", @@ -8712,7 +9876,6 @@ "version": "2.0.3", "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "devOptional": true, "license": "MIT" }, "node_modules/pbkdf2": { @@ -8861,7 +10024,6 @@ "version": "2.3.0", "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", - "devOptional": true, "license": "MIT", "dependencies": { "confbox": "^0.2.2", @@ -8869,6 +10031,22 @@ "pathe": "^2.0.3" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/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.npmmirror.com/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/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -9035,7 +10213,6 @@ "version": "0.2.11", "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", - "devOptional": true, "funding": [ { "type": "individual", @@ -9231,6 +10408,12 @@ "inherits": "^2.0.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.46.2", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.46.2.tgz", @@ -9271,6 +10454,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmmirror.com/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/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9295,6 +10490,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -9334,6 +10535,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sass": { "version": "1.94.0", "resolved": "https://registry.npmmirror.com/sass/-/sass-1.94.0.tgz", @@ -9445,18 +10652,42 @@ } }, "node_modules/shiki": { - "version": "3.14.0", - "resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.14.0.tgz", - "integrity": "sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g==", + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/shiki/-/shiki-3.15.0.tgz", + "integrity": "sha512-kLdkY6iV3dYbtPwS9KXU7mjfmDm25f5m0IPNFnaXO7TBPcvbUOY72PYXSuSqDzwp+vlH/d7MXpHlKO/x+QoLXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.15.0", + "@shikijs/engine-javascript": "3.15.0", + "@shikijs/engine-oniguruma": "3.15.0", + "@shikijs/langs": "3.15.0", + "@shikijs/themes": "3.15.0", + "@shikijs/types": "3.15.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/shiki/node_modules/@shikijs/core": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/core/-/core-3.15.0.tgz", + "integrity": "sha512-8TOG6yG557q+fMsSVa8nkEDOZNTSxjbbR8l6lF2gyr6Np+jrPlslqDxQkN6rMXCECQ3isNPZAGszAfYoJOPGlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.15.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/shiki/node_modules/@shikijs/types": { + "version": "3.15.0", + "resolved": "https://registry.npmmirror.com/@shikijs/types/-/types-3.15.0.tgz", + "integrity": "sha512-BnP+y/EQnhihgHy4oIAN+6FFtmfTekwOLsQbRw9hOKwqgNy8Bdsjq8B05oAt/ZgvIWWFrshV71ytOrlPfYjIJw==", "dev": true, "license": "MIT", "dependencies": { - "@shikijs/core": "3.14.0", - "@shikijs/engine-javascript": "3.14.0", - "@shikijs/engine-oniguruma": "3.14.0", - "@shikijs/langs": "3.14.0", - "@shikijs/themes": "3.14.0", - "@shikijs/types": "3.14.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } @@ -9669,6 +10900,12 @@ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", "license": "MIT" }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/superjson": { "version": "2.2.2", "resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz", @@ -9851,6 +11088,15 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmmirror.com/tty-browserify/-/tty-browserify-0.0.1.tgz", @@ -9934,7 +11180,6 @@ "version": "1.6.1", "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "devOptional": true, "license": "MIT" }, "node_modules/unctx": { @@ -10305,6 +11550,19 @@ "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", @@ -10645,6 +11903,49 @@ "dev": true, "license": "MIT" }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -10787,6 +12088,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5697ee4..11c5155 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,21 +54,24 @@ "@cospaia/prettier-plugin-clojure": "^0.0.2", "@lezer/highlight": "^1.2.3", "@lezer/lr": "^1.4.3", + "@mdit/plugin-katex": "^0.23.2", + "@mdit/plugin-tasklist": "^0.22.2", "@prettier/plugin-xml": "^3.4.2", "@replit/codemirror-lang-svelte": "^6.0.0", "@toml-tools/lexer": "^1.0.0", "@toml-tools/parser": "^1.0.0", + "@types/markdown-it": "^14.1.2", "codemirror": "^6.0.2", "codemirror-lang-elixir": "^4.0.0", "colors-named": "^1.0.2", "colors-named-hex": "^1.0.2", "groovy-beautify": "^0.0.17", + "highlight.js": "^11.11.1", "hsl-matcher": "^1.2.4", - "i": "^0.3.7", "java-parser": "^3.0.1", - "jsox": "^1.2.123", "linguist-languages": "^9.1.0", - "markdown-exit": "^1.0.0-beta.6", + "markdown-it": "^14.1.0", + "mermaid": "^11.12.1", "npm": "^11.6.2", "php-parser": "^3.2.5", "pinia": "^3.0.4", @@ -90,14 +93,18 @@ "eslint": "^9.39.1", "eslint-plugin-vue": "^10.5.1", "globals": "^16.5.0", + "happy-dom": "^20.0.10", "typescript": "^5.9.3", "typescript-eslint": "^8.46.4", "unplugin-vue-components": "^30.0.0", - "vite": "^7.2.2", + "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", "vitepress": "^2.0.0-alpha.12", "vitest": "^4.0.8", "vue-eslint-parser": "^10.2.0", "vue-tsc": "^3.1.3" + }, + "overrides": { + "vite": "npm:rolldown-vite@latest" } } diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-abbr/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-abbr/index.ts new file mode 100644 index 0000000..534572a --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-abbr/index.ts @@ -0,0 +1,159 @@ +// Enclose abbreviations in tags +// +import MarkdownIt, {StateBlock, StateCore, Token} from 'markdown-it'; + +/** + * 环境接口,包含缩写定义 + */ +interface AbbrEnv { + abbreviations?: { [key: string]: string }; +} + +/** + * markdown-it-abbr 插件 + * 用于支持缩写语法 + */ +export default function abbr_plugin(md: MarkdownIt): void { + const escapeRE = md.utils.escapeRE; + const arrayReplaceAt = md.utils.arrayReplaceAt; + + // ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on; + // you can check character classes here: + // http://www.unicode.org/Public/UNIDATA/UnicodeData.txt + const OTHER_CHARS = ' \r\n$+<=>^`|~'; + + const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source; + const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source; + + function abbr_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { + let labelEnd: number; + let pos = state.bMarks[startLine] + state.tShift[startLine]; + const max = state.eMarks[startLine]; + + if (pos + 2 >= max) { return false; } + + if (state.src.charCodeAt(pos++) !== 0x2A/* * */) { return false; } + if (state.src.charCodeAt(pos++) !== 0x5B/* [ */) { return false; } + + const labelStart = pos; + + for (; pos < max; pos++) { + const ch = state.src.charCodeAt(pos); + if (ch === 0x5B /* [ */) { + return false; + } else if (ch === 0x5D /* ] */) { + labelEnd = pos; + break; + } else if (ch === 0x5C /* \ */) { + pos++; + } + } + + if (labelEnd! < 0 || state.src.charCodeAt(labelEnd! + 1) !== 0x3A/* : */) { + return false; + } + + if (silent) { return true; } + + const label = state.src.slice(labelStart, labelEnd!).replace(/\\(.)/g, '$1'); + const title = state.src.slice(labelEnd! + 2, max).trim(); + if (label.length === 0) { return false; } + if (title.length === 0) { return false; } + + const env = state.env as AbbrEnv; + if (!env.abbreviations) { env.abbreviations = {}; } + // prepend ':' to avoid conflict with Object.prototype members + if (typeof env.abbreviations[':' + label] === 'undefined') { + env.abbreviations[':' + label] = title; + } + + state.line = startLine + 1; + return true; + } + + function abbr_replace(state: StateCore): void { + const blockTokens = state.tokens; + + const env = state.env as AbbrEnv; + if (!env.abbreviations) { return; } + + const regSimple = new RegExp('(?:' + + Object.keys(env.abbreviations).map(function (x: string) { + return x.substr(1); + }).sort(function (a: string, b: string) { + return b.length - a.length; + }).map(escapeRE).join('|') + + ')'); + + const regText = '(^|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE + + '|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' + + '(' + Object.keys(env.abbreviations).map(function (x: string) { + return x.substr(1); + }).sort(function (a: string, b: string) { + return b.length - a.length; + }).map(escapeRE).join('|') + ')' + + '($|' + UNICODE_PUNCT_RE + '|' + UNICODE_SPACE_RE + + '|[' + OTHER_CHARS.split('').map(escapeRE).join('') + '])' + + const reg = new RegExp(regText, 'g'); + + for (let j = 0, l = blockTokens.length; j < l; j++) { + if (blockTokens[j].type !== 'inline') { continue; } + let tokens = blockTokens[j].children!; + + // We scan from the end, to keep position when new tags added. + for (let i = tokens.length - 1; i >= 0; i--) { + const currentToken = tokens[i]; + if (currentToken.type !== 'text') { continue; } + + let pos = 0; + const text = currentToken.content; + reg.lastIndex = 0; + const nodes: Token[] = []; + + // fast regexp run to determine whether there are any abbreviated words + // in the current token + if (!regSimple.test(text)) { continue; } + + let m: RegExpExecArray | null; + + while ((m = reg.exec(text))) { + if (m.index > 0 || m[1].length > 0) { + const token = new state.Token('text', '', 0); + token.content = text.slice(pos, m.index + m[1].length); + nodes.push(token); + } + + const token_o = new state.Token('abbr_open', 'abbr', 1); + token_o.attrs = [['title', env.abbreviations[':' + m[2]]]]; + nodes.push(token_o); + + const token_t = new state.Token('text', '', 0); + token_t.content = m[2]; + nodes.push(token_t); + + const token_c = new state.Token('abbr_close', 'abbr', -1); + nodes.push(token_c); + + reg.lastIndex -= m[3].length; + pos = reg.lastIndex; + } + + if (!nodes.length) { continue; } + + if (pos < text.length) { + const token = new state.Token('text', '', 0); + token.content = text.slice(pos); + nodes.push(token); + } + + // replace current node + blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes); + } + } + } + + md.block.ruler.before('reference', 'abbr_def', abbr_def, { alt: ['paragraph', 'reference'] }); + + md.core.ruler.after('linkify', 'abbr_replace', abbr_replace); +} \ No newline at end of file diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-deflist/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-deflist/index.ts new file mode 100644 index 0000000..925679d --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-deflist/index.ts @@ -0,0 +1,209 @@ +// Process definition lists +// +import MarkdownIt, { StateBlock, Token } from 'markdown-it'; + +/** + * markdown-it-deflist 插件 + * 用于支持定义列表语法 + */ +export default function deflist_plugin(md: MarkdownIt): void { + const isSpace = md.utils.isSpace; + + // Search `[:~][\n ]`, returns next pos after marker on success + // or -1 on fail. + function skipMarker(state: StateBlock, line: number): number { + let start = state.bMarks[line] + state.tShift[line]; + const max = state.eMarks[line]; + + if (start >= max) { return -1; } + + // Check bullet + const marker = state.src.charCodeAt(start++); + if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1; } + + const pos = state.skipSpaces(start); + + // require space after ":" + if (start === pos) { return -1; } + + // no empty definitions, e.g. " : " + if (pos >= max) { return -1; } + + return start; + } + + function markTightParagraphs(state: StateBlock, idx: number): void { + const level = state.level + 2; + + for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) { + if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') { + state.tokens[i + 2].hidden = true; + state.tokens[i].hidden = true; + i += 2; + } + } + } + + function deflist(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { + if (silent) { + // quirk: validation mode validates a dd block only, not a whole deflist + if (state.ddIndent < 0) { return false; } + return skipMarker(state, startLine) >= 0; + } + + let nextLine = startLine + 1; + if (nextLine >= endLine) { return false; } + + if (state.isEmpty(nextLine)) { + nextLine++; + if (nextLine >= endLine) { return false; } + } + + if (state.sCount[nextLine] < state.blkIndent) { return false; } + let contentStart = skipMarker(state, nextLine); + if (contentStart < 0) { return false; } + + // Start list + const listTokIdx = state.tokens.length; + let tight = true; + + const token_dl_o: Token = state.push('dl_open', 'dl', 1); + const listLines: [number, number] = [startLine, 0]; + token_dl_o.map = listLines; + + // + // Iterate list items + // + + let dtLine = startLine; + let ddLine = nextLine; + + // One definition list can contain multiple DTs, + // and one DT can be followed by multiple DDs. + // + // Thus, there is two loops here, and label is + // needed to break out of the second one + // + /* eslint no-labels:0,block-scoped-var:0 */ + OUTER: + for (;;) { + let prevEmptyEnd = false; + + const token_dt_o: Token = state.push('dt_open', 'dt', 1); + token_dt_o.map = [dtLine, dtLine]; + + const token_i: Token = state.push('inline', '', 0); + token_i.map = [dtLine, dtLine]; + token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim(); + token_i.children = []; + + state.push('dt_close', 'dt', -1); + + for (;;) { + const token_dd_o: Token = state.push('dd_open', 'dd', 1); + const itemLines: [number, number] = [nextLine, 0]; + token_dd_o.map = itemLines; + + let pos = contentStart; + const max = state.eMarks[ddLine]; + let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]); + + while (pos < max) { + const ch = state.src.charCodeAt(pos); + + if (isSpace(ch)) { + if (ch === 0x09) { + offset += 4 - offset % 4; + } else { + offset++; + } + } else { + break; + } + + pos++; + } + + contentStart = pos; + + const oldTight = state.tight; + const oldDDIndent = state.ddIndent; + const oldIndent = state.blkIndent; + const oldTShift = state.tShift[ddLine]; + const oldSCount = state.sCount[ddLine]; + const oldParentType = state.parentType; + state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2; + state.tShift[ddLine] = contentStart - state.bMarks[ddLine]; + state.sCount[ddLine] = offset; + state.tight = true; + state.parentType = 'deflist' as any; + + state.md.block.tokenize(state, ddLine, endLine); + + // If any of list item is tight, mark list as tight + if (!state.tight || prevEmptyEnd) { + tight = false; + } + // Item become loose if finish with empty line, + // but we should filter last element, because it means list finish + prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1); + + state.tShift[ddLine] = oldTShift; + state.sCount[ddLine] = oldSCount; + state.tight = oldTight; + state.parentType = oldParentType; + state.blkIndent = oldIndent; + state.ddIndent = oldDDIndent; + + state.push('dd_close', 'dd', -1); + + itemLines[1] = nextLine = state.line; + + if (nextLine >= endLine) { break OUTER; } + + if (state.sCount[nextLine] < state.blkIndent) { break OUTER; } + contentStart = skipMarker(state, nextLine); + if (contentStart < 0) { break; } + + ddLine = nextLine; + + // go to the next loop iteration: + // insert DD tag and repeat checking + } + + if (nextLine >= endLine) { break; } + dtLine = nextLine; + + if (state.isEmpty(dtLine)) { break; } + if (state.sCount[dtLine] < state.blkIndent) { break; } + + ddLine = dtLine + 1; + if (ddLine >= endLine) { break; } + if (state.isEmpty(ddLine)) { ddLine++; } + if (ddLine >= endLine) { break; } + + if (state.sCount[ddLine] < state.blkIndent) { break; } + contentStart = skipMarker(state, ddLine); + if (contentStart < 0) { break; } + + // go to the next loop iteration: + // insert DT and DD tags and repeat checking + } + + // Finilize list + state.push('dl_close', 'dl', -1); + + listLines[1] = nextLine; + + state.line = nextLine; + + // mark paragraphs tight if needed + if (tight) { + markTightParagraphs(state, listTokIdx); + } + + return true; + } + + md.block.ruler.before('paragraph', 'deflist', deflist, { alt: ['paragraph', 'reference', 'blockquote'] }); +} \ No newline at end of file diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-footnote/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-footnote/index.ts new file mode 100644 index 0000000..1f80bba --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-footnote/index.ts @@ -0,0 +1,390 @@ +import MarkdownIt, {Renderer, StateBlock, StateCore, StateInline, Token} from 'markdown-it'; + +/** + * 脚注元数据接口 + */ +interface FootnoteMeta { + id: number; + subId: number; + label: string; +} + +/** + * 脚注列表项接口 + */ +interface FootnoteItem { + label?: string; + content?: string; + tokens?: Token[]; + count: number; +} + +/** + * 环境接口 + */ +interface FootnoteEnv { + footnotes?: { + refs?: { [key: string]: number }; + list?: FootnoteItem[]; + }; + docId?: string; +} + +/// ///////////////////////////////////////////////////////////////////////////// +// Renderer partials + +function render_footnote_anchor_name(tokens: Token[], idx: number, options: any, env: FootnoteEnv): string { + const n = Number(tokens[idx].meta.id + 1).toString(); + let prefix = ''; + + if (typeof env.docId === 'string') prefix = `-${env.docId}-`; + + return prefix + n; +} + +function render_footnote_caption(tokens: Token[], idx: number): string { + let n = Number(tokens[idx].meta.id + 1).toString(); + + if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`; + + return `[${n}]`; +} + +function render_footnote_ref(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string { + const id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf); + const caption = slf.rules.footnote_caption!(tokens, idx, options, env, slf); + let refid = id; + + if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`; + + return `${caption}`; +} + +function render_footnote_block_open(tokens: Token[], idx: number, options: any): string { + return (options.xhtmlOut ? '
\n' : '
\n') + + '
\n' + + '
    \n'; +} + +function render_footnote_block_close(): string { + return '
\n
\n'; +} + +function render_footnote_open(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string { + let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf); + + if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`; + + return `
  • `; +} + +function render_footnote_close(): string { + return '
  • \n'; +} + +function render_footnote_anchor(tokens: Token[], idx: number, options: any, env: FootnoteEnv, slf: Renderer): string { + let id = slf.rules.footnote_anchor_name!(tokens, idx, options, env, slf); + + if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`; + + /* ↩ with escape code to prevent display as Apple Emoji on iOS */ + return ` \u21a9\uFE0E`; +} + +/** + * markdown-it-footnote 插件 + * 用于支持脚注语法 + */ +export default function footnote_plugin(md: MarkdownIt): void { + const parseLinkLabel = md.helpers.parseLinkLabel; + const isSpace = md.utils.isSpace; + + md.renderer.rules.footnote_ref = render_footnote_ref; + md.renderer.rules.footnote_block_open = render_footnote_block_open; + md.renderer.rules.footnote_block_close = render_footnote_block_close; + md.renderer.rules.footnote_open = render_footnote_open; + md.renderer.rules.footnote_close = render_footnote_close; + md.renderer.rules.footnote_anchor = render_footnote_anchor; + + // helpers (only used in other rules, no tokens are attached to those) + md.renderer.rules.footnote_caption = render_footnote_caption; + md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name; + + // Process footnote block definition + function footnote_def(state: StateBlock, startLine: number, endLine: number, silent: boolean): boolean { + const start = state.bMarks[startLine] + state.tShift[startLine]; + const max = state.eMarks[startLine]; + + // line should be at least 5 chars - "[^x]:" + if (start + 4 > max) return false; + + if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false; + if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false; + + let pos: number; + + for (pos = start + 2; pos < max; pos++) { + if (state.src.charCodeAt(pos) === 0x20) return false; + if (state.src.charCodeAt(pos) === 0x5D /* ] */) { + break; + } + } + + if (pos === start + 2) return false; // no empty footnote labels + if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 0x3A /* : */) return false; + if (silent) return true; + pos++; + + const env = state.env as FootnoteEnv; + if (!env.footnotes) env.footnotes = {}; + if (!env.footnotes.refs) env.footnotes.refs = {}; + const label = state.src.slice(start + 2, pos - 2); + env.footnotes.refs[`:${label}`] = -1; + + const token_fref_o = new state.Token('footnote_reference_open', '', 1); + token_fref_o.meta = { label }; + token_fref_o.level = state.level++; + state.tokens.push(token_fref_o); + + const oldBMark = state.bMarks[startLine]; + const oldTShift = state.tShift[startLine]; + const oldSCount = state.sCount[startLine]; + const oldParentType = state.parentType; + + const posAfterColon = pos; + const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]); + let offset = initial; + + while (pos < max) { + const ch = state.src.charCodeAt(pos); + + if (isSpace(ch)) { + if (ch === 0x09) { + offset += 4 - offset % 4; + } else { + offset++; + } + } else { + break; + } + + pos++; + } + + state.tShift[startLine] = pos - posAfterColon; + state.sCount[startLine] = offset - initial; + + state.bMarks[startLine] = posAfterColon; + state.blkIndent += 4; + state.parentType = 'footnote' as any; + + if (state.sCount[startLine] < state.blkIndent) { + state.sCount[startLine] += state.blkIndent; + } + + state.md.block.tokenize(state, startLine, endLine); + + state.parentType = oldParentType; + state.blkIndent -= 4; + state.tShift[startLine] = oldTShift; + state.sCount[startLine] = oldSCount; + state.bMarks[startLine] = oldBMark; + + const token_fref_c = new state.Token('footnote_reference_close', '', -1); + token_fref_c.level = --state.level; + state.tokens.push(token_fref_c); + + return true; + } + + // Process inline footnotes (^[...]) + function footnote_inline(state: StateInline, silent: boolean): boolean { + const max = state.posMax; + const start = state.pos; + + if (start + 2 >= max) return false; + if (state.src.charCodeAt(start) !== 0x5E/* ^ */) return false; + if (state.src.charCodeAt(start + 1) !== 0x5B/* [ */) return false; + + const labelStart = start + 2; + const labelEnd = parseLinkLabel(state, start + 1); + + // parser failed to find ']', so it's not a valid note + if (labelEnd < 0) return false; + + // We found the end of the link, and know for a fact it's a valid link; + // so all that's left to do is to call tokenizer. + // + if (!silent) { + const env = state.env as FootnoteEnv; + if (!env.footnotes) env.footnotes = {}; + if (!env.footnotes.list) env.footnotes.list = []; + const footnoteId = env.footnotes.list.length; + const tokens: Token[] = []; + + state.md.inline.parse( + state.src.slice(labelStart, labelEnd), + state.md, + state.env, + tokens + ); + + const token = state.push('footnote_ref', '', 0); + token.meta = { id: footnoteId }; + + env.footnotes.list[footnoteId] = { + content: state.src.slice(labelStart, labelEnd), + tokens, + count: 0 + }; + } + + state.pos = labelEnd + 1; + state.posMax = max; + return true; + } + + // Process footnote references ([^...]) + function footnote_ref(state: StateInline, silent: boolean): boolean { + const max = state.posMax; + const start = state.pos; + + // should be at least 4 chars - "[^x]" + if (start + 3 > max) return false; + + const env = state.env as FootnoteEnv; + if (!env.footnotes || !env.footnotes.refs) return false; + if (state.src.charCodeAt(start) !== 0x5B/* [ */) return false; + if (state.src.charCodeAt(start + 1) !== 0x5E/* ^ */) return false; + + let pos: number; + + for (pos = start + 2; pos < max; pos++) { + if (state.src.charCodeAt(pos) === 0x20) return false; + if (state.src.charCodeAt(pos) === 0x0A) return false; + if (state.src.charCodeAt(pos) === 0x5D /* ] */) { + break; + } + } + + if (pos === start + 2) return false; // no empty footnote labels + if (pos >= max) return false; + pos++; + + const label = state.src.slice(start + 2, pos - 1); + if (typeof env.footnotes.refs[`:${label}`] === 'undefined') return false; + + if (!silent) { + if (!env.footnotes.list) env.footnotes.list = []; + + let footnoteId: number; + + if (env.footnotes.refs[`:${label}`] < 0) { + footnoteId = env.footnotes.list.length; + env.footnotes.list[footnoteId] = { label, count: 0 }; + env.footnotes.refs[`:${label}`] = footnoteId; + } else { + footnoteId = env.footnotes.refs[`:${label}`]; + } + + const footnoteSubId = env.footnotes.list[footnoteId].count; + env.footnotes.list[footnoteId].count++; + + const token = state.push('footnote_ref', '', 0); + token.meta = { id: footnoteId, subId: footnoteSubId, label }; + } + + state.pos = pos; + state.posMax = max; + return true; + } + + // Glue footnote tokens to end of token stream + function footnote_tail(state: StateCore): void { + let tokens: Token[] | null = null; + let current: Token[]; + let currentLabel: string; + let insideRef = false; + const refTokens: { [key: string]: Token[] } = {}; + + const env = state.env as FootnoteEnv; + if (!env.footnotes) { return; } + + state.tokens = state.tokens.filter(function (tok) { + if (tok.type === 'footnote_reference_open') { + insideRef = true; + current = []; + currentLabel = tok.meta.label; + return false; + } + if (tok.type === 'footnote_reference_close') { + insideRef = false; + // prepend ':' to avoid conflict with Object.prototype members + refTokens[':' + currentLabel] = current; + return false; + } + if (insideRef) { current.push(tok); } + return !insideRef; + }); + + if (!env.footnotes.list) { return; } + const list = env.footnotes.list; + + state.tokens.push(new state.Token('footnote_block_open', '', 1)); + + for (let i = 0, l = list.length; i < l; i++) { + const token_fo = new state.Token('footnote_open', '', 1); + token_fo.meta = { id: i, label: list[i].label }; + state.tokens.push(token_fo); + + if (list[i].tokens) { + tokens = []; + + const token_po = new state.Token('paragraph_open', 'p', 1); + token_po.block = true; + tokens.push(token_po); + + const token_i = new state.Token('inline', '', 0); + token_i.children = list[i].tokens || null; + token_i.content = list[i].content || ''; + tokens.push(token_i); + + const token_pc = new state.Token('paragraph_close', 'p', -1); + token_pc.block = true; + tokens.push(token_pc); + } else if (list[i].label) { + tokens = refTokens[`:${list[i].label}`] || null; + } + + if (tokens) state.tokens = state.tokens.concat(tokens); + + let lastParagraph: Token | null; + + if (state.tokens[state.tokens.length - 1].type === 'paragraph_close') { + lastParagraph = state.tokens.pop()!; + } else { + lastParagraph = null; + } + + const t = list[i].count > 0 ? list[i].count : 1; + for (let j = 0; j < t; j++) { + const token_a = new state.Token('footnote_anchor', '', 0); + token_a.meta = { id: i, subId: j, label: list[i].label }; + state.tokens.push(token_a); + } + + if (lastParagraph) { + state.tokens.push(lastParagraph); + } + + state.tokens.push(new state.Token('footnote_close', '', -1)); + } + + state.tokens.push(new state.Token('footnote_block_close', '', -1)); + } + + md.block.ruler.before('reference', 'footnote_def', footnote_def, { alt: ['paragraph', 'reference'] }); + md.inline.ruler.after('image', 'footnote_inline', footnote_inline); + md.inline.ruler.after('footnote_inline', 'footnote_ref', footnote_ref); + md.core.ruler.after('inline', 'footnote_tail', footnote_tail); +} \ No newline at end of file diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-ins/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-ins/index.ts new file mode 100644 index 0000000..9521bde --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-ins/index.ts @@ -0,0 +1,160 @@ +import MarkdownIt, { StateInline, Token } from 'markdown-it'; + +/** + * 分隔符接口定义 + */ +interface Delimiter { + marker: number; + length: number; + jump: number; + token: number; + end: number; + open: boolean; + close: boolean; +} + +/** + * 扫描结果接口定义 + */ +interface ScanResult { + can_open: boolean; + can_close: boolean; + length: number; +} + +/** + * Token 元数据接口定义 + */ +interface TokenMeta { + delimiters?: Delimiter[]; +} + +/** + * markdown-it-ins 插件 + * 用于支持插入文本语法 ++text++ + */ +export default function ins_plugin(md: MarkdownIt): void { + // Insert each marker as a separate text token, and add it to delimiter list + // + function tokenize(state: StateInline, silent: boolean): boolean { + const start = state.pos; + const marker = state.src.charCodeAt(start); + + if (silent) { return false; } + + if (marker !== 0x2B/* + */) { return false; } + + const scanned = state.scanDelims(state.pos, true) as ScanResult; + let len = scanned.length; + const ch = String.fromCharCode(marker); + + if (len < 2) { return false; } + + if (len % 2) { + const token: Token = state.push('text', '', 0); + token.content = ch; + len--; + } + + for (let i = 0; i < len; i += 2) { + const token: Token = state.push('text', '', 0); + token.content = ch + ch; + + if (!scanned.can_open && !scanned.can_close) { continue; } + + state.delimiters.push({ + marker, + length: 0, // disable "rule of 3" length checks meant for emphasis + jump: i / 2, // 1 delimiter = 2 characters + token: state.tokens.length - 1, + end: -1, + open: scanned.can_open, + close: scanned.can_close + } as Delimiter); + } + + state.pos += scanned.length; + + return true; + } + + // Walk through delimiter list and replace text tokens with tags + // + function postProcess(state: StateInline, delimiters: Delimiter[]): void { + let token: Token; + const loneMarkers: number[] = []; + const max = delimiters.length; + + for (let i = 0; i < max; i++) { + const startDelim = delimiters[i]; + + if (startDelim.marker !== 0x2B/* + */) { + continue; + } + + if (startDelim.end === -1) { + continue; + } + + const endDelim = delimiters[startDelim.end]; + + token = state.tokens[startDelim.token]; + token.type = 'ins_open'; + token.tag = 'ins'; + token.nesting = 1; + token.markup = '++'; + token.content = ''; + + token = state.tokens[endDelim.token]; + token.type = 'ins_close'; + token.tag = 'ins'; + token.nesting = -1; + token.markup = '++'; + token.content = ''; + + if (state.tokens[endDelim.token - 1].type === 'text' && + state.tokens[endDelim.token - 1].content === '+') { + loneMarkers.push(endDelim.token - 1); + } + } + + // If a marker sequence has an odd number of characters, it's splitted + // like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the + // start of the sequence. + // + // So, we have to move all those markers after subsequent s_close tags. + // + while (loneMarkers.length) { + const i = loneMarkers.pop()!; + let j = i + 1; + + while (j < state.tokens.length && state.tokens[j].type === 'ins_close') { + j++; + } + + j--; + + if (i !== j) { + token = state.tokens[j]; + state.tokens[j] = state.tokens[i]; + state.tokens[i] = token; + } + } + } + + md.inline.ruler.before('emphasis', 'ins', tokenize); + md.inline.ruler2.before('emphasis', 'ins', function (state: StateInline): boolean { + const tokens_meta = state.tokens_meta as TokenMeta[]; + const max = (state.tokens_meta || []).length; + + postProcess(state, state.delimiters as Delimiter[]); + + for (let curr = 0; curr < max; curr++) { + if (tokens_meta[curr] && tokens_meta[curr].delimiters) { + postProcess(state, tokens_meta[curr].delimiters!); + } + } + + return true; + }); +} \ No newline at end of file diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-mark/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-mark/index.ts new file mode 100644 index 0000000..ed18705 --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-mark/index.ts @@ -0,0 +1,160 @@ +import MarkdownIt, {StateInline, Token} from 'markdown-it'; + +/** + * 分隔符接口定义 + */ +interface Delimiter { + marker: number; + length: number; + jump: number; + token: number; + end: number; + open: boolean; + close: boolean; +} + +/** + * 扫描结果接口定义 + */ +interface ScanResult { + can_open: boolean; + can_close: boolean; + length: number; +} + +/** + * Token 元数据接口定义 + */ +interface TokenMeta { + delimiters?: Delimiter[]; +} + +/** + * markdown-it-mark 插件 + * 用于支持 ==标记文本== 语法 + */ +export default function markPlugin(md: MarkdownIt): void { + // Insert each marker as a separate text token, and add it to delimiter list + // + function tokenize(state: StateInline, silent: boolean): boolean { + const start = state.pos; + const marker = state.src.charCodeAt(start); + + if (silent) { return false; } + + if (marker !== 0x3D/* = */) { return false; } + + const scanned = state.scanDelims(state.pos, true) as ScanResult; + let len = scanned.length; + const ch = String.fromCharCode(marker); + + if (len < 2) { return false; } + + if (len % 2) { + const token: Token = state.push('text', '', 0); + token.content = ch; + len--; + } + + for (let i = 0; i < len; i += 2) { + const token: Token = state.push('text', '', 0); + token.content = ch + ch; + + if (!scanned.can_open && !scanned.can_close) { continue; } + + state.delimiters.push({ + marker, + length: 0, // disable "rule of 3" length checks meant for emphasis + jump: i / 2, // 1 delimiter = 2 characters + token: state.tokens.length - 1, + end: -1, + open: scanned.can_open, + close: scanned.can_close + } as Delimiter); + } + + state.pos += scanned.length; + + return true; + } + + // Walk through delimiter list and replace text tokens with tags + // + function postProcess(state: StateInline, delimiters: Delimiter[]): void { + const loneMarkers: number[] = []; + const max = delimiters.length; + + for (let i = 0; i < max; i++) { + const startDelim = delimiters[i]; + + if (startDelim.marker !== 0x3D/* = */) { + continue; + } + + if (startDelim.end === -1) { + continue; + } + + const endDelim = delimiters[startDelim.end]; + + const token_o = state.tokens[startDelim.token]; + token_o.type = 'mark_open'; + token_o.tag = 'mark'; + token_o.nesting = 1; + token_o.markup = '=='; + token_o.content = ''; + + const token_c = state.tokens[endDelim.token]; + token_c.type = 'mark_close'; + token_c.tag = 'mark'; + token_c.nesting = -1; + token_c.markup = '=='; + token_c.content = ''; + + if (state.tokens[endDelim.token - 1].type === 'text' && + state.tokens[endDelim.token - 1].content === '=') { + loneMarkers.push(endDelim.token - 1); + } + } + + // If a marker sequence has an odd number of characters, it's splitted + // like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the + // start of the sequence. + // + // So, we have to move all those markers after subsequent s_close tags. + // + while (loneMarkers.length) { + const i = loneMarkers.pop()!; + let j = i + 1; + + while (j < state.tokens.length && state.tokens[j].type === 'mark_close') { + j++; + } + + j--; + + if (i !== j) { + const token = state.tokens[j]; + state.tokens[j] = state.tokens[i]; + state.tokens[i] = token; + } + } + } + + md.inline.ruler.before('emphasis', 'mark', tokenize); + md.inline.ruler2.before('emphasis', 'mark', function (state: StateInline): boolean { + let curr: number; + const tokens_meta = state.tokens_meta as TokenMeta[]; + const max = (state.tokens_meta || []).length; + + postProcess(state, state.delimiters as Delimiter[]); + + for (curr = 0; curr < max; curr++) { + if (tokens_meta[curr] && tokens_meta[curr].delimiters) { + postProcess(state, tokens_meta[curr].delimiters!); + } + } + + return true; + }); +} \ No newline at end of file diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/index.ts new file mode 100644 index 0000000..734383d --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/index.ts @@ -0,0 +1,106 @@ +import mermaid from "mermaid"; +import {genUid, hashCode, sleep} from "./utils"; + +const mermaidCache = new Map(); + +// 缓存计数器,用于清除缓存 +const mermaidCacheCount = new Map(); +let count = 0; + + +let countTmo = setTimeout(() => undefined, 0); +const addCount = () => { + clearTimeout(countTmo); + countTmo = setTimeout(() => { + count++; + clearCache(); + }, 500); +}; + +const clearCache = () => { + for (const key of mermaidCacheCount.keys()) { + const value = mermaidCacheCount.get(key)!; + if (value + 3 < count) { + mermaidCache.delete(key); + mermaidCacheCount.delete(key); + } + } +}; + +/** + * 渲染 mermaid + * @param code mermaid 代码 + * @param targetId 目标 id + * @param count 计数器 + */ +const renderMermaid = async (code: string, targetId: string, count: number) => { + let limit = 100; + while (limit-- > 0) { + const container = document.getElementById(targetId); + if (!container) { + await sleep(100); + continue; + } + try { + const {svg} = await mermaid.render("mermaid-svg-" + genUid(), code, container); + container.innerHTML = svg; + mermaidCache.set(targetId, container); + mermaidCacheCount.set(targetId, count); + } catch (e) { + } + break; + } +}; + +export interface MermaidItOptions { + theme?: "default" | "dark" | "forest" | "neutral" | "base"; +} + +/** + * 更新 mermaid 主题 + */ +export const updateMermaidTheme = (theme: "default" | "dark" | "forest" | "neutral" | "base") => { + mermaid.initialize({ + startOnLoad: false, + theme: theme + }); + // 清空缓存,强制重新渲染 + mermaidCache.clear(); + mermaidCacheCount.clear(); + +}; + +/** + * mermaid 插件 + * @param md markdown-it + * @param options 配置选项 + * @constructor MermaidIt + */ +export const MermaidIt = function (md: any, options?: MermaidItOptions): void { + const theme = options?.theme || "default"; + mermaid.initialize({ + startOnLoad: false, + theme: theme + }); + const defaultRenderer = md.renderer.rules.fence.bind(md.renderer.rules); + md.renderer.rules.fence = (tokens: any, idx: any, options: any, env: any, self: any) => { + addCount(); + const token = tokens[idx]; + const info = token.info.trim(); + if (info === "mermaid") { + const containerId = "mermaid-container-" + hashCode(token.content); + const container = document.createElement("div"); + container.id = containerId; + if (mermaidCache.has(containerId)) { + container.innerHTML = mermaidCache.get(containerId)!.innerHTML; + mermaidCacheCount.set(containerId, count); + } else { + renderMermaid(token.content, containerId, count).then(); + } + return container.outerHTML; + } + // 使用默认的渲染规则 + return defaultRenderer(tokens, idx, options, env, self); + }; +}; + diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/utils.ts b/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/utils.ts new file mode 100644 index 0000000..f3c4486 --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/utils.ts @@ -0,0 +1,49 @@ +import { v4 as uuidv4 } from "uuid"; + +/** + * uuid 生成函数 + * @param split 分隔符 + */ +export const genUid = (split = "") => { + return uuidv4().split("-").join(split); +}; + +/** + * 一个简易的sleep函数 + */ +export const sleep = async (ms: number) => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +}; + +/** + * 计算字符串的hash值 + * 返回一个数字 + * @param str + */ +export const hashCode = (str: string) => { + let hash = 0; + if (str.length === 0) return hash; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash; +}; + +/** + * 一个简易的阻塞函数 + */ +export const awaitFor = async (cb: () => boolean, timeout = 0, errText = "超时暂停阻塞") => { + const start = Date.now(); + while (true) { + if (cb()) return true; + if (timeout && Date.now() - start > timeout) { + console.error("阻塞超时: " + errText); + return false; + } + await sleep(100); + } +}; diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-sub/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-sub/index.ts new file mode 100644 index 0000000..f2e00cf --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-sub/index.ts @@ -0,0 +1,66 @@ +// Process ~subscript~ + +import MarkdownIt, { StateInline, Token } from 'markdown-it'; + +// same as UNESCAPE_MD_RE plus a space +const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g; + +function subscript(state: StateInline, silent: boolean): boolean { + const max = state.posMax; + const start = state.pos; + + if (state.src.charCodeAt(start) !== 0x7E/* ~ */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 2 >= max) { return false; } + + state.pos = start + 1; + let found = false; + + while (state.pos < max) { + if (state.src.charCodeAt(state.pos) === 0x7E/* ~ */) { + found = true; + break; + } + + state.md.inline.skipToken(state); + } + + if (!found || start + 1 === state.pos) { + state.pos = start; + return false; + } + + const content = state.src.slice(start + 1, state.pos); + + // don't allow unescaped spaces/newlines inside + if (content.match(/(^|[^\\])(\\\\)*\s/)) { + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 1; + + // Earlier we checked !silent, but this implementation does not need it + const token_so: Token = state.push('sub_open', 'sub', 1); + token_so.markup = '~'; + + const token_t: Token = state.push('text', '', 0); + token_t.content = content.replace(UNESCAPE_RE, '$1'); + + const token_sc: Token = state.push('sub_close', 'sub', -1); + token_sc.markup = '~'; + + state.pos = state.posMax + 1; + state.posMax = max; + return true; +} + +/** + * markdown-it-sub 插件 + * 用于支持下标语法 ~text~ + */ +export default function sub_plugin(md: MarkdownIt): void { + md.inline.ruler.after('emphasis', 'sub', subscript); +} \ No newline at end of file diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-sup/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-sup/index.ts new file mode 100644 index 0000000..eb5c8cd --- /dev/null +++ b/frontend/src/common/markdown-it/plugins/markdown-it-sup/index.ts @@ -0,0 +1,66 @@ +// Process ^superscript^ + +import MarkdownIt, { StateInline, Token } from 'markdown-it'; + +// same as UNESCAPE_MD_RE plus a space +const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g; + +function superscript(state: StateInline, silent: boolean): boolean { + const max = state.posMax; + const start = state.pos; + + if (state.src.charCodeAt(start) !== 0x5E/* ^ */) { return false; } + if (silent) { return false; } // don't run any pairs in validation mode + if (start + 2 >= max) { return false; } + + state.pos = start + 1; + let found = false; + + while (state.pos < max) { + if (state.src.charCodeAt(state.pos) === 0x5E/* ^ */) { + found = true; + break; + } + + state.md.inline.skipToken(state); + } + + if (!found || start + 1 === state.pos) { + state.pos = start; + return false; + } + + const content = state.src.slice(start + 1, state.pos); + + // don't allow unescaped spaces/newlines inside + if (content.match(/(^|[^\\])(\\\\)*\s/)) { + state.pos = start; + return false; + } + + // found! + state.posMax = state.pos; + state.pos = start + 1; + + // Earlier we checked !silent, but this implementation does not need it + const token_so: Token = state.push('sup_open', 'sup', 1); + token_so.markup = '^'; + + const token_t: Token = state.push('text', '', 0); + token_t.content = content.replace(UNESCAPE_RE, '$1'); + + const token_sc: Token = state.push('sup_close', 'sup', -1); + token_sc.markup = '^'; + + state.pos = state.posMax + 1; + state.posMax = max; + return true; +} + +/** + * markdown-it-sup 插件 + * 用于支持上标语法 ^text^ + */ +export default function sup_plugin(md: MarkdownIt): void { + md.inline.ruler.after('emphasis', 'sup', superscript); +} \ No newline at end of file diff --git a/frontend/src/common/utils/domDiff.test.ts b/frontend/src/common/utils/domDiff.test.ts new file mode 100644 index 0000000..5b11d6f --- /dev/null +++ b/frontend/src/common/utils/domDiff.test.ts @@ -0,0 +1,329 @@ +/** + * DOM Diff 算法单元测试 + */ +import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import { morphNode, morphHTML, morphWithKeys } from './domDiff'; + +describe('DOM Diff Algorithm', () => { + let container: HTMLElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + document.body.removeChild(container); + }); + + describe('morphNode - 基础功能', () => { + test('应该更新文本节点内容', () => { + const fromNode = document.createTextNode('Hello'); + const toNode = document.createTextNode('World'); + container.appendChild(fromNode); + + morphNode(fromNode, toNode); + + expect(fromNode.nodeValue).toBe('World'); + }); + + test('应该保持相同的文本节点不变', () => { + const fromNode = document.createTextNode('Hello'); + const toNode = document.createTextNode('Hello'); + container.appendChild(fromNode); + + const originalNode = fromNode; + morphNode(fromNode, toNode); + + expect(fromNode).toBe(originalNode); + expect(fromNode.nodeValue).toBe('Hello'); + }); + + test('应该替换不同类型的节点', () => { + const fromNode = document.createElement('span'); + fromNode.textContent = 'Hello'; + const toNode = document.createElement('div'); + toNode.textContent = 'World'; + container.appendChild(fromNode); + + morphNode(fromNode, toNode); + + expect(container.firstChild?.nodeName).toBe('DIV'); + expect(container.firstChild?.textContent).toBe('World'); + }); + }); + + describe('morphNode - 属性更新', () => { + test('应该添加新属性', () => { + const fromEl = document.createElement('div'); + const toEl = document.createElement('div'); + toEl.setAttribute('class', 'test'); + toEl.setAttribute('id', 'myid'); + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.getAttribute('class')).toBe('test'); + expect(fromEl.getAttribute('id')).toBe('myid'); + }); + + test('应该更新已存在的属性', () => { + const fromEl = document.createElement('div'); + fromEl.setAttribute('class', 'old'); + const toEl = document.createElement('div'); + toEl.setAttribute('class', 'new'); + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.getAttribute('class')).toBe('new'); + }); + + test('应该删除不存在的属性', () => { + const fromEl = document.createElement('div'); + fromEl.setAttribute('class', 'test'); + fromEl.setAttribute('id', 'myid'); + const toEl = document.createElement('div'); + toEl.setAttribute('class', 'test'); + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.getAttribute('class')).toBe('test'); + expect(fromEl.hasAttribute('id')).toBe(false); + }); + }); + + describe('morphNode - 子节点更新', () => { + test('应该添加新子节点', () => { + const fromEl = document.createElement('ul'); + fromEl.innerHTML = '
  • 1
  • 2
  • '; + const toEl = document.createElement('ul'); + toEl.innerHTML = '
  • 1
  • 2
  • 3
  • '; + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.children.length).toBe(3); + expect(fromEl.children[2].textContent).toBe('3'); + }); + + test('应该删除多余的子节点', () => { + const fromEl = document.createElement('ul'); + fromEl.innerHTML = '
  • 1
  • 2
  • 3
  • '; + const toEl = document.createElement('ul'); + toEl.innerHTML = '
  • 1
  • 2
  • '; + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.children.length).toBe(2); + expect(fromEl.textContent).toBe('12'); + }); + + test('应该更新子节点内容', () => { + const fromEl = document.createElement('div'); + fromEl.innerHTML = '

    Old

    '; + const toEl = document.createElement('div'); + toEl.innerHTML = '

    New

    '; + container.appendChild(fromEl); + + const originalP = fromEl.querySelector('p'); + morphNode(fromEl, toEl); + + // 应该保持同一个 p 元素,只更新内容 + expect(fromEl.querySelector('p')).toBe(originalP); + expect(fromEl.querySelector('p')?.textContent).toBe('New'); + }); + }); + + describe('morphHTML - HTML 字符串更新', () => { + test('应该从 HTML 字符串更新元素', () => { + const element = document.createElement('div'); + element.innerHTML = '

    Old

    '; + container.appendChild(element); + + morphHTML(element, '

    New

    '); + + expect(element.innerHTML).toBe('

    New

    '); + }); + + test('应该处理复杂的 HTML 结构', () => { + const element = document.createElement('div'); + element.innerHTML = '

    Title

    Paragraph

    '; + container.appendChild(element); + + morphHTML(element, '

    New Title

    New Paragraph

    Extra'); + + expect(element.children.length).toBe(3); + expect(element.querySelector('h1')?.textContent).toBe('New Title'); + expect(element.querySelector('p')?.textContent).toBe('New Paragraph'); + expect(element.querySelector('span')?.textContent).toBe('Extra'); + }); + }); + + describe('morphWithKeys - 基于 key 的智能 diff', () => { + test('应该保持相同 key 的节点', () => { + const fromEl = document.createElement('ul'); + fromEl.innerHTML = ` +
  • A
  • +
  • B
  • +
  • C
  • + `; + const toEl = document.createElement('ul'); + toEl.innerHTML = ` +
  • A Updated
  • +
  • B
  • +
  • C
  • + `; + container.appendChild(fromEl); + + const originalA = fromEl.querySelector('[data-key="a"]'); + morphWithKeys(fromEl, toEl); + + expect(fromEl.querySelector('[data-key="a"]')).toBe(originalA); + expect(originalA?.textContent).toBe('A Updated'); + }); + + test('应该重新排序节点', () => { + const fromEl = document.createElement('ul'); + fromEl.innerHTML = ` +
  • A
  • +
  • B
  • +
  • C
  • + `; + const toEl = document.createElement('ul'); + toEl.innerHTML = ` +
  • C
  • +
  • A
  • +
  • B
  • + `; + container.appendChild(fromEl); + + morphWithKeys(fromEl, toEl); + + const keys = Array.from(fromEl.children).map(child => child.getAttribute('data-key')); + expect(keys).toEqual(['c', 'a', 'b']); + }); + + test('应该添加新的 key 节点', () => { + const fromEl = document.createElement('ul'); + fromEl.innerHTML = ` +
  • A
  • +
  • B
  • + `; + const toEl = document.createElement('ul'); + toEl.innerHTML = ` +
  • A
  • +
  • B
  • +
  • C
  • + `; + container.appendChild(fromEl); + + morphWithKeys(fromEl, toEl); + + expect(fromEl.children.length).toBe(3); + expect(fromEl.querySelector('[data-key="c"]')?.textContent).toBe('C'); + }); + + test('应该删除不存在的 key 节点', () => { + const fromEl = document.createElement('ul'); + fromEl.innerHTML = ` +
  • A
  • +
  • B
  • +
  • C
  • + `; + const toEl = document.createElement('ul'); + toEl.innerHTML = ` +
  • A
  • +
  • C
  • + `; + container.appendChild(fromEl); + + morphWithKeys(fromEl, toEl); + + expect(fromEl.children.length).toBe(2); + expect(fromEl.querySelector('[data-key="b"]')).toBeNull(); + }); + }); + + describe('性能测试', () => { + test('应该高效处理大量节点', () => { + const fromEl = document.createElement('ul'); + for (let i = 0; i < 1000; i++) { + const li = document.createElement('li'); + li.textContent = `Item ${i}`; + fromEl.appendChild(li); + } + + const toEl = document.createElement('ul'); + for (let i = 0; i < 1000; i++) { + const li = document.createElement('li'); + li.textContent = `Updated Item ${i}`; + toEl.appendChild(li); + } + + container.appendChild(fromEl); + + const startTime = performance.now(); + morphNode(fromEl, toEl); + const endTime = performance.now(); + + expect(endTime - startTime).toBeLessThan(100); // 应该在 100ms 内完成 + expect(fromEl.children.length).toBe(1000); + expect(fromEl.children[0].textContent).toBe('Updated Item 0'); + }); + }); + + describe('边界情况', () => { + test('应该处理空节点', () => { + const fromEl = document.createElement('div'); + const toEl = document.createElement('div'); + container.appendChild(fromEl); + + expect(() => morphNode(fromEl, toEl)).not.toThrow(); + }); + + test('应该处理只有文本的节点', () => { + const fromEl = document.createElement('div'); + fromEl.textContent = 'Hello'; + const toEl = document.createElement('div'); + toEl.textContent = 'World'; + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.textContent).toBe('World'); + }); + + test('应该处理嵌套的复杂结构', () => { + const fromEl = document.createElement('div'); + fromEl.innerHTML = ` +
    +
    + Text +
    +
    + `; + + const toEl = document.createElement('div'); + toEl.innerHTML = ` +
    +
    + Updated Text + New +
    +
    + `; + + container.appendChild(fromEl); + + morphNode(fromEl, toEl); + + expect(fromEl.querySelector('.outer')?.classList.contains('modified')).toBe(true); + expect(fromEl.querySelector('span')?.textContent).toBe('Updated Text'); + expect(fromEl.querySelector('strong')?.textContent).toBe('New'); + }); + }); +}); + diff --git a/frontend/src/common/utils/domDiff.ts b/frontend/src/common/utils/domDiff.ts new file mode 100644 index 0000000..b898338 --- /dev/null +++ b/frontend/src/common/utils/domDiff.ts @@ -0,0 +1,180 @@ +/** + * 轻量级 DOM Diff 算法实现 + * 基于 morphdom 思路,只更新变化的节点,保持未变化的节点不动 + */ + +/** + * 比较并更新两个 DOM 节点 + * @param fromNode 原节点 + * @param toNode 目标节点 + */ +export function morphNode(fromNode: Node, toNode: Node): void { + // 节点类型不同,直接替换 + if (fromNode.nodeType !== toNode.nodeType || fromNode.nodeName !== toNode.nodeName) { + fromNode.parentNode?.replaceChild(toNode.cloneNode(true), fromNode); + return; + } + + // 文本节点:比较内容 + if (fromNode.nodeType === Node.TEXT_NODE) { + if (fromNode.nodeValue !== toNode.nodeValue) { + fromNode.nodeValue = toNode.nodeValue; + } + return; + } + + // 元素节点:更新属性和子节点 + if (fromNode.nodeType === Node.ELEMENT_NODE) { + const fromEl = fromNode as Element; + const toEl = toNode as Element; + + // 更新属性 + morphAttributes(fromEl, toEl); + + // 更新子节点 + morphChildren(fromEl, toEl); + } +} + +/** + * 更新元素属性 + */ +function morphAttributes(fromEl: Element, toEl: Element): void { + // 移除旧属性 + const fromAttrs = fromEl.attributes; + for (let i = fromAttrs.length - 1; i >= 0; i--) { + const attr = fromAttrs[i]; + if (!toEl.hasAttribute(attr.name)) { + fromEl.removeAttribute(attr.name); + } + } + + // 添加/更新新属性 + const toAttrs = toEl.attributes; + for (let i = 0; i < toAttrs.length; i++) { + const attr = toAttrs[i]; + const fromValue = fromEl.getAttribute(attr.name); + if (fromValue !== attr.value) { + fromEl.setAttribute(attr.name, attr.value); + } + } +} + +/** + * 更新子节点(核心 diff 算法) + */ +function morphChildren(fromEl: Element, toEl: Element): void { + const fromChildren = Array.from(fromEl.childNodes); + const toChildren = Array.from(toEl.childNodes); + + const fromLen = fromChildren.length; + const toLen = toChildren.length; + const minLen = Math.min(fromLen, toLen); + + // 1. 更新公共部分 + for (let i = 0; i < minLen; i++) { + morphNode(fromChildren[i], toChildren[i]); + } + + // 2. 移除多余的旧节点 + if (fromLen > toLen) { + for (let i = fromLen - 1; i >= toLen; i--) { + fromEl.removeChild(fromChildren[i]); + } + } + + // 3. 添加新节点 + if (toLen > fromLen) { + for (let i = fromLen; i < toLen; i++) { + fromEl.appendChild(toChildren[i].cloneNode(true)); + } + } +} + +/** + * 优化版:使用 key 进行更智能的 diff(可选) + * 适用于有 data-key 属性的元素 + */ +export function morphWithKeys(fromEl: Element, toEl: Element): void { + const toChildren = Array.from(toEl.children) as Element[]; + + // 构建 from 的 key 映射 + const fromKeyMap = new Map(); + Array.from(fromEl.children).forEach((child) => { + const key = child.getAttribute('data-key'); + if (key) { + fromKeyMap.set(key, child); + } + }); + + const processedKeys = new Set(); + + // 按照 toChildren 的顺序处理 + toChildren.forEach((toChild, toIndex) => { + const key = toChild.getAttribute('data-key'); + if (!key) return; + + processedKeys.add(key); + const fromChild = fromKeyMap.get(key); + + if (fromChild) { + // 找到对应节点,更新内容 + morphNode(fromChild, toChild); + + // 确保节点在正确的位置 + const currentNode = fromEl.children[toIndex]; + if (currentNode !== fromChild) { + // 将 fromChild 移动到正确位置 + fromEl.insertBefore(fromChild, currentNode); + } + } else { + // 新节点,插入到正确位置 + const currentNode = fromEl.children[toIndex]; + fromEl.insertBefore(toChild.cloneNode(true), currentNode || null); + } + }); + + // 删除不再存在的节点(从后往前删除,避免索引问题) + const childrenToRemove: Element[] = []; + fromKeyMap.forEach((child, key) => { + if (!processedKeys.has(key)) { + childrenToRemove.push(child); + } + }); + childrenToRemove.forEach(child => { + fromEl.removeChild(child); + }); +} + +/** + * 高级 API:直接从 HTML 字符串更新元素 + */ +export function morphHTML(element: Element, htmlString: string): void { + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = htmlString; + + // 更新元素的子节点列表 + morphChildren(element, tempContainer); +} + +/** + * 批量更新(使用 DocumentFragment) + */ +export function batchMorph(element: Element, htmlString: string): void { + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = htmlString; + + const fragment = document.createDocumentFragment(); + Array.from(tempContainer.childNodes).forEach(node => { + fragment.appendChild(node); + }); + + // 清空原内容 + while (element.firstChild) { + element.removeChild(element.firstChild); + } + + // 批量插入 + element.appendChild(fragment); +} + diff --git a/frontend/src/components/toolbar/Toolbar.vue b/frontend/src/components/toolbar/Toolbar.vue index b9fe56e..e6528ca 100644 --- a/frontend/src/components/toolbar/Toolbar.vue +++ b/frontend/src/components/toolbar/Toolbar.vue @@ -13,16 +13,20 @@ import {getActiveNoteBlock} from '@/views/editor/extensions/codeblock/state'; import {getLanguage} from '@/views/editor/extensions/codeblock/lang-parser/languages'; import {formatBlockContent} from '@/views/editor/extensions/codeblock/formatCode'; import {createDebounce} from '@/common/utils/debounce'; +import {toggleMarkdownPreview} from '@/views/editor/extensions/markdownPreview'; +import {usePanelStore} from '@/stores/panelStore'; const editorStore = readonly(useEditorStore()); const configStore = readonly(useConfigStore()); const updateStore = readonly(useUpdateStore()); const windowStore = readonly(useWindowStore()); const systemStore = readonly(useSystemStore()); +const panelStore = readonly(usePanelStore()); const {t} = useI18n(); const router = useRouter(); const canFormatCurrentBlock = ref(false); +const canPreviewMarkdown = ref(false); const isLoaded = shallowRef(false); const { documentStats } = toRefs(editorStore); @@ -33,6 +37,11 @@ const isCurrentWindowOnTop = computed(() => { return config.value.general.alwaysOnTop || systemStore.isWindowOnTop; }); +// 当前文档的预览是否打开 +const isCurrentBlockPreviewing = computed(() => { + return panelStore.markdownPreview.isOpen && !panelStore.markdownPreview.isClosing; +}); + // 切换窗口置顶状态 const toggleAlwaysOnTop = async () => { const currentlyOnTop = isCurrentWindowOnTop.value; @@ -60,11 +69,22 @@ const formatCurrentBlock = () => { formatBlockContent(editorStore.editorView); }; -// 格式化按钮状态更新 - 使用更高效的检查逻辑 -const updateFormatButtonState = () => { - const view = editorStore.editorView; +// 切换 Markdown 预览 +const { debouncedFn: debouncedTogglePreview } = createDebounce(() => { + if (!canPreviewMarkdown.value || !editorStore.editorView) return; + toggleMarkdownPreview(editorStore.editorView as any); +}, { delay: 200 }); + +const togglePreview = () => { + debouncedTogglePreview(); +}; + +// 统一更新按钮状态 +const updateButtonStates = () => { + const view: any = editorStore.editorView; if (!view) { canFormatCurrentBlock.value = false; + canPreviewMarkdown.value = false; return; } @@ -75,20 +95,25 @@ const updateFormatButtonState = () => { // 提前返回,减少不必要的计算 if (!activeBlock) { canFormatCurrentBlock.value = false; + canPreviewMarkdown.value = false; return; } - const language = getLanguage(activeBlock.language.name as any); + const languageName = activeBlock.language.name; + const language = getLanguage(languageName as any); + canFormatCurrentBlock.value = Boolean(language?.prettier); + canPreviewMarkdown.value = languageName.toLowerCase() === 'md'; } catch (error) { - console.warn('Error checking format capability:', error); + console.warn('Error checking block capabilities:', error); canFormatCurrentBlock.value = false; + canPreviewMarkdown.value = false; } }; // 创建带1s防抖的更新函数 -const { debouncedFn: debouncedUpdateFormat, cancel: cancelDebounce } = createDebounce( - updateFormatButtonState, +const { debouncedFn: debouncedUpdateButtonStates, cancel: cancelDebounce } = createDebounce( + updateButtonStates, { delay: 1000 } ); @@ -102,9 +127,9 @@ const setupEditorListeners = (view: any) => { // 使用对象缓存事件处理器,避免重复创建 const eventHandlers = { - click: updateFormatButtonState, - keyup: debouncedUpdateFormat, - focus: updateFormatButtonState + click: updateButtonStates, + keyup: debouncedUpdateButtonStates, + focus: updateButtonStates } as const; const events = Object.entries(eventHandlers).map(([type, handler]) => ({ @@ -131,11 +156,12 @@ watch( if (newView) { // 初始更新状态 - updateFormatButtonState(); + updateButtonStates(); // 设置新监听器 cleanupListeners = setupEditorListeners(newView); } else { canFormatCurrentBlock.value = false; + canPreviewMarkdown.value = false; } }); }, @@ -145,8 +171,8 @@ watch( // 组件生命周期 onMounted(async () => { isLoaded.value = true; - // 首次更新格式化状态 - updateFormatButtonState(); + // 首次更新按钮状态 + updateButtonStates(); await systemStore.setWindowOnTop(isCurrentWindowOnTop.value); }); @@ -229,6 +255,21 @@ const statsData = computed(() => ({ + +
    + + + + +
    +
    ({ } } + .preview-button { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + padding: 2px; + border-radius: 3px; + transition: all 0.2s ease; + + &:hover { + background-color: var(--border-color); + opacity: 0.8; + } + + &.active { + background-color: rgba(100, 149, 237, 0.2); + + svg { + stroke: #6495ed; + } + } + + svg { + width: 14px; + height: 14px; + stroke: var(--text-muted); + transition: stroke 0.2s ease; + } + + &:hover svg { + stroke: var(--text-secondary); + } + } + .settings-btn { background: none; border: none; diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index f5dc5c6..28831ad 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -19,6 +19,8 @@ export default { searchLanguage: 'Search language...', noLanguageFound: 'No language found', formatHint: 'Click Format Block (Ctrl+Shift+F)', + previewMarkdown: 'Preview Markdown', + closePreview: 'Close Preview', // Document selector selectDocument: 'Select Document', searchOrCreateDocument: 'Search or enter new document name...', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 1577c78..3af8323 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -19,6 +19,8 @@ export default { searchLanguage: '搜索语言...', noLanguageFound: '未找到匹配的语言', formatHint: '点击格式化区块(Ctrl+Shift+F)', + previewMarkdown: '预览 Markdown', + closePreview: '关闭预览', // 文档选择器 selectDocument: '选择文档', searchOrCreateDocument: '搜索或输入新文档名...', diff --git a/frontend/src/stores/documentStore.ts b/frontend/src/stores/documentStore.ts index f96bf57..0fd1686 100644 --- a/frontend/src/stores/documentStore.ts +++ b/frontend/src/stores/documentStore.ts @@ -4,6 +4,7 @@ import {DocumentService} from '@/../bindings/voidraft/internal/services'; import {OpenDocumentWindow} from '@/../bindings/voidraft/internal/services/windowservice'; import {Document} from '@/../bindings/voidraft/internal/models/models'; import {useTabStore} from "@/stores/tabStore"; +import type {EditorViewState} from '@/stores/editorStore'; export const useDocumentStore = defineStore('document', () => { const DEFAULT_DOCUMENT_ID = ref(1); // 默认草稿文档ID @@ -14,10 +15,8 @@ export const useDocumentStore = defineStore('document', () => { const currentDocument = ref(null); // === 编辑器状态持久化 === - const documentStates = ref>({}); + // 修复:使用统一的 EditorViewState 类型定义 + const documentStates = ref>({}); // === UI状态 === const showDocumentSelector = ref(false); diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index a3e831d..9366207 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -28,6 +28,8 @@ import {generateContentHash} from "@/common/utils/hashUtils"; import {createTimerManager, type TimerManager} from '@/common/utils/timerUtils'; import {EDITOR_CONFIG} from '@/common/constant/editor'; import {createHttpClientExtension} from "@/views/editor/extensions/httpclient"; +import {markdownPreviewExtension} from "@/views/editor/extensions/markdownPreview"; +import {createDebounce} from '@/common/utils/debounce'; export interface DocumentStats { lines: number; @@ -35,6 +37,11 @@ export interface DocumentStats { selectedCharacters: number; } +// 修复:只保存光标位置,恢复时自动滚动到光标处(更简单可靠) +export interface EditorViewState { + cursorPos: number; +} + interface EditorInstance { view: EditorView; documentId: number; @@ -47,10 +54,8 @@ interface EditorInstance { lastContentHash: string; lastParsed: Date; } | null; - editorState?: { - cursorPos: number; - scrollTop: number; - }; + // 修复:使用统一的类型,可选但不是 undefined | {...} + editorState?: EditorViewState; } export const useEditorStore = defineStore('editor', () => { @@ -72,6 +77,8 @@ export const useEditorStore = defineStore('editor', () => { // 编辑器加载状态 const isLoading = ref(false); + // 修复:使用操作计数器精确管理加载状态 + const loadingOperations = ref(0); // 异步操作管理器 const operationManager = new AsyncManager(); @@ -79,6 +86,13 @@ export const useEditorStore = defineStore('editor', () => { // 自动保存设置 - 从配置动态获取 const getAutoSaveDelay = () => configStore.config.editing.autoSaveDelay; + // 创建防抖的语法树缓存清理函数 + const debouncedClearSyntaxCache = createDebounce((instance) => { + if (instance) { + instance.syntaxTreeCache = null; + } + }, { delay: 500 }); // 500ms 内的多次输入只清理一次 + // === 私有方法 === /** @@ -123,13 +137,13 @@ export const useEditorStore = defineStore('editor', () => { }; /** - * 恢复编辑器的光标和滚动位置 + * 恢复编辑器的光标位置(自动滚动到光标处) */ const restoreEditorState = (instance: EditorInstance, documentId: number): void => { - const savedState = instance.editorState || documentStore.documentStates[documentId]; + const savedState = instance.editorState; if (savedState) { - // 有保存的状态,恢复光标位置和滚动位置 + // 有保存的状态,恢复光标位置 let pos = Math.min(savedState.cursorPos, instance.view.state.doc.length); // 确保位置不在分隔符上 @@ -137,21 +151,23 @@ export const useEditorStore = defineStore('editor', () => { pos = adjustCursorPosition(instance.view, pos); } + // 修复:设置光标位置并居中滚动(更好的用户体验) instance.view.dispatch({ - selection: {anchor: pos, head: pos} + selection: {anchor: pos, head: pos}, + effects: EditorView.scrollIntoView(pos, { + y: "center", // 垂直居中显示 + yMargin: 100 // 上下留一些边距 + }) }); - - // 恢复滚动位置 - instance.view.scrollDOM.scrollTop = savedState.scrollTop; - - // 更新实例状态 - instance.editorState = savedState; } else { // 首次打开或没有记录,光标在文档末尾 const docLength = instance.view.state.doc.length; instance.view.dispatch({ selection: {anchor: docLength, head: docLength}, - scrollIntoView: true + effects: EditorView.scrollIntoView(docLength, { + y: "center", + yMargin: 100 + }) }); } }; @@ -239,6 +255,9 @@ export const useEditorStore = defineStore('editor', () => { const httpExtension = createHttpClientExtension(); + // Markdown预览扩展 + const previewExtension = markdownPreviewExtension(); + // 再次检查操作有效性 if (!operationManager.isOperationValid(operationId, documentId)) { throw new Error('Operation cancelled'); @@ -271,7 +290,8 @@ export const useEditorStore = defineStore('editor', () => { contentChangeExtension, codeBlockExtension, ...dynamicExtensions, - ...httpExtension + ...httpExtension, + previewExtension ]; // 创建编辑器状态 @@ -294,7 +314,9 @@ export const useEditorStore = defineStore('editor', () => { isDirty: false, lastModified: new Date(), autoSaveTimer: createTimerManager(), - syntaxTreeCache: null + syntaxTreeCache: null, + // 修复:创建实例时从 documentStore 读取持久化的编辑器状态 + editorState: documentStore.documentStates[documentId] }; // 使用LRU缓存的onEvict回调处理被驱逐的实例 @@ -332,10 +354,19 @@ export const useEditorStore = defineStore('editor', () => { // 创建新的编辑器实例 const view = await createEditorInstance(content, operationId, documentId); - // 最终检查操作有效性 + // 完善取消操作时的清理逻辑 if (!operationManager.isOperationValid(operationId, documentId)) { - // 如果操作已取消,清理创建的实例 - view.destroy(); + // 如果操作已取消,彻底清理创建的实例 + try { + // 移除 DOM 元素(如果已添加到文档) + if (view.dom && view.dom.parentElement) { + view.dom.remove(); + } + // 销毁编辑器视图 + view.destroy(); + } catch (error) { + console.error('Error cleaning up cancelled editor:', error); + } throw new Error('Operation cancelled'); } @@ -350,34 +381,11 @@ export const useEditorStore = defineStore('editor', () => { if (!instance || !containerElement.value) return; try { - // 保存当前编辑器的状态 - if (currentEditor.value) { - const currentDocId = documentStore.currentDocumentId; - const currentInstance = currentDocId ? editorCache.get(currentDocId) : null; - if (currentInstance) { - // 保存到实例缓存 - currentInstance.editorState = { - cursorPos: currentEditor.value.state.selection.main.head, - scrollTop: currentEditor.value.scrollDOM.scrollTop - }; - // 同时保存到 documentStore 用于持久化 - if (currentDocId) { - documentStore.documentStates[currentDocId] = { - cursorPos: currentEditor.value.state.selection.main.head, - scrollTop: currentEditor.value.scrollDOM.scrollTop - }; - } - } - } - // 移除当前编辑器DOM if (currentEditor.value && currentEditor.value.dom && currentEditor.value.dom.parentElement) { currentEditor.value.dom.remove(); } - // 确保容器为空 - containerElement.value.innerHTML = ''; - // 将目标编辑器DOM添加到容器 containerElement.value.appendChild(instance.view.dom); currentEditor.value = instance.view; @@ -385,16 +393,18 @@ export const useEditorStore = defineStore('editor', () => { // 设置扩展管理器视图 setExtensionManagerView(instance.view, documentId); - // 重新测量和聚焦编辑器 + //使用 nextTick + requestAnimationFrame 确保 DOM 完全渲染 nextTick(() => { - // 恢复编辑器状态(光标位置和滚动位置) - restoreEditorState(instance, documentId); - - // 聚焦编辑器 - instance.view.focus(); - - // 使用缓存的语法树确保方法 - ensureSyntaxTreeCached(instance.view, documentId); + requestAnimationFrame(() => { + // 恢复编辑器状态(光标位置和滚动位置) + restoreEditorState(instance, documentId); + + // 聚焦编辑器 + instance.view.focus(); + + // 使用缓存的语法树确保方法 + ensureSyntaxTreeCached(instance.view, documentId); + }); }); } catch (error) { console.error('Error showing editor:', error); @@ -428,30 +438,20 @@ export const useEditorStore = defineStore('editor', () => { }; // 内容变化处理 - const onContentChange = (documentId: number) => { + const onContentChange = () => { + const documentId = documentStore.currentDocumentId; + if (!documentId) return; const instance = editorCache.get(documentId); if (!instance) return; + // 立即设置脏标记和修改时间(切换文档时需要判断) instance.isDirty = true; instance.lastModified = new Date(); - // 清理语法树缓存,下次访问时重新构建 - instance.syntaxTreeCache = null; + // 优使用防抖清理语法树缓存 + debouncedClearSyntaxCache.debouncedFn(instance); - // 保存当前编辑器状态(光标位置和滚动位置) - if (instance.view) { - instance.editorState = { - cursorPos: instance.view.state.selection.main.head, - scrollTop: instance.view.scrollDOM.scrollTop - }; - // 同时保存到 documentStore 用于持久化 - documentStore.documentStates[documentId] = { - cursorPos: instance.view.state.selection.main.head, - scrollTop: instance.view.scrollDOM.scrollTop - }; - } - - // 设置自动保存定时器 + // 设置自动保存定时器(已经是防抖效果:每次重置定时器) instance.autoSaveTimer.set(() => { saveEditorContent(documentId); }, getAutoSaveDelay()); @@ -471,7 +471,8 @@ export const useEditorStore = defineStore('editor', () => { // 加载编辑器 const loadEditor = async (documentId: number, content: string) => { - // 设置加载状态 + // 修复:使用计数器精确管理加载状态 + loadingOperations.value++; isLoading.value = true; // 开始新的操作 @@ -520,6 +521,9 @@ export const useEditorStore = defineStore('editor', () => { instance.isDirty = false; // 清理语法树缓存,因为内容已更新 instance.syntaxTreeCache = null; + // 修复:内容变了,清空光标位置,避免越界 + instance.editorState = undefined; + delete documentStore.documentStates[documentId]; } } @@ -541,15 +545,20 @@ export const useEditorStore = defineStore('editor', () => { // 完成操作 operationManager.completeOperation(operationId); - // 延迟一段时间后再取消加载状态 + // 修复:使用计数器精确管理加载状态,避免快速切换时状态不准确 + loadingOperations.value--; + // 延迟一段时间后再取消加载状态,但要确保所有操作都完成了 setTimeout(() => { - isLoading.value = false; + if (loadingOperations.value <= 0) { + loadingOperations.value = 0; + isLoading.value = false; + } }, EDITOR_CONFIG.LOADING_DELAY); } }; // 移除编辑器 - const removeEditor = (documentId: number) => { + const removeEditor = async (documentId: number) => { const instance = editorCache.get(documentId); if (instance) { try { @@ -558,6 +567,20 @@ export const useEditorStore = defineStore('editor', () => { operationManager.cancelAllOperations(); } + // 修复:移除前先保存内容(如果有未保存的修改) + if (instance.isDirty) { + await saveEditorContent(documentId); + } + + // 保存光标位置 + if (instance.view && instance.view.state) { + const currentState: EditorViewState = { + cursorPos: instance.view.state.selection.main.head + }; + // 保存到 documentStore 用于持久化 + documentStore.documentStates[documentId] = currentState; + } + // 清除自动保存定时器 instance.autoSaveTimer.clear(); @@ -639,17 +662,14 @@ export const useEditorStore = defineStore('editor', () => { operationManager.cancelAllOperations(); editorCache.clear((_documentId, instance) => { - // 在销毁前保存编辑器状态 + // 修复:清空前只保存光标位置 if (instance.view) { - instance.editorState = { - cursorPos: instance.view.state.selection.main.head, - scrollTop: instance.view.scrollDOM.scrollTop - }; - // 保存到 documentStore 用于持久化 - documentStore.documentStates[instance.documentId] = { - cursorPos: instance.view.state.selection.main.head, - scrollTop: instance.view.scrollDOM.scrollTop + const currentState: EditorViewState = { + cursorPos: instance.view.state.selection.main.head }; + // 同时保存到实例和 documentStore + instance.editorState = currentState; + documentStore.documentStates[instance.documentId] = currentState; } // 清除自动保存定时器 @@ -693,12 +713,24 @@ export const useEditorStore = defineStore('editor', () => { }; // 监听文档切换 - watch(() => documentStore.currentDocument, async (newDoc) => { + watch(() => documentStore.currentDocument, async (newDoc, oldDoc) => { if (newDoc && containerElement.value) { - // 使用 nextTick 确保DOM更新完成后再加载编辑器 - await nextTick(() => { - loadEditor(newDoc.id, newDoc.content); - }); + // 修复:在切换到新文档前,只保存旧文档的光标位置 + if (oldDoc && oldDoc.id !== newDoc.id && currentEditor.value) { + const oldInstance = editorCache.get(oldDoc.id); + if (oldInstance) { + const currentState: EditorViewState = { + cursorPos: currentEditor.value.state.selection.main.head + }; + // 同时保存到实例和 documentStore + oldInstance.editorState = currentState; + documentStore.documentStates[oldDoc.id] = currentState; + } + } + + // 等待 DOM 更新完成,再加载新文档的编辑器 + await nextTick(); + loadEditor(newDoc.id, newDoc.content); } }); diff --git a/frontend/src/stores/panelStore.ts b/frontend/src/stores/panelStore.ts new file mode 100644 index 0000000..7df422e --- /dev/null +++ b/frontend/src/stores/panelStore.ts @@ -0,0 +1,170 @@ +import { defineStore } from 'pinia'; +import { ref, computed } from 'vue'; +import type { EditorView } from '@codemirror/view'; +import { useDocumentStore } from './documentStore'; + +/** + * 单个文档的预览状态 + */ +interface DocumentPreviewState { + isOpen: boolean; + isClosing: boolean; + blockFrom: number; + blockTo: number; +} + +/** + * 面板状态管理 Store + * 管理编辑器中各种面板的显示状态(按文档ID区分) + */ +export const usePanelStore = defineStore('panel', () => { + // 当前编辑器视图引用 + const editorView = ref(null); + + // 每个文档的预览状态 Map + const documentPreviews = ref>(new Map()); + + /** + * 获取当前文档的预览状态 + */ + const markdownPreview = computed(() => { + const documentStore = useDocumentStore(); + const currentDocId = documentStore.currentDocumentId; + + if (currentDocId === null) { + return { + isOpen: false, + isClosing: false, + blockFrom: 0, + blockTo: 0 + }; + } + + return documentPreviews.value.get(currentDocId) || { + isOpen: false, + isClosing: false, + blockFrom: 0, + blockTo: 0 + }; + }); + + /** + * 设置编辑器视图 + */ + const setEditorView = (view: EditorView | null) => { + editorView.value = view; + }; + + /** + * 打开 Markdown 预览面板 + */ + const openMarkdownPreview = (from: number, to: number) => { + const documentStore = useDocumentStore(); + const currentDocId = documentStore.currentDocumentId; + + if (currentDocId === null) return; + + documentPreviews.value.set(currentDocId, { + isOpen: true, + isClosing: false, + blockFrom: from, + blockTo: to + }); + }; + + /** + * 开始关闭 Markdown 预览面板 + */ + const startClosingMarkdownPreview = () => { + const documentStore = useDocumentStore(); + const currentDocId = documentStore.currentDocumentId; + + if (currentDocId === null) return; + + const state = documentPreviews.value.get(currentDocId); + if (state?.isOpen) { + documentPreviews.value.set(currentDocId, { + ...state, + isClosing: true + }); + } + }; + + /** + * 关闭 Markdown 预览面板 + */ + const closeMarkdownPreview = () => { + const documentStore = useDocumentStore(); + const currentDocId = documentStore.currentDocumentId; + + if (currentDocId === null) return; + + documentPreviews.value.set(currentDocId, { + isOpen: false, + isClosing: false, + blockFrom: 0, + blockTo: 0 + }); + }; + + /** + * 更新预览块的范围(用于实时预览) + */ + const updatePreviewRange = (from: number, to: number) => { + const documentStore = useDocumentStore(); + const currentDocId = documentStore.currentDocumentId; + + if (currentDocId === null) return; + + const state = documentPreviews.value.get(currentDocId); + if (state?.isOpen) { + documentPreviews.value.set(currentDocId, { + ...state, + blockFrom: from, + blockTo: to + }); + } + }; + + /** + * 检查指定块是否正在预览 + */ + const isBlockPreviewing = (from: number, to: number): boolean => { + const preview = markdownPreview.value; + return preview.isOpen && + preview.blockFrom === from && + preview.blockTo === to; + }; + + /** + * 重置所有面板状态 + */ + const reset = () => { + documentPreviews.value.clear(); + editorView.value = null; + }; + + /** + * 清理指定文档的预览状态(文档关闭时调用) + */ + const clearDocumentPreview = (documentId: number) => { + documentPreviews.value.delete(documentId); + }; + + return { + // 状态 + editorView, + markdownPreview, + + // 方法 + setEditorView, + openMarkdownPreview, + startClosingMarkdownPreview, + closeMarkdownPreview, + updatePreviewRange, + isBlockPreviewing, + reset, + clearDocumentPreview + }; +}); + diff --git a/frontend/src/views/editor/basic/contentChangeExtension.ts b/frontend/src/views/editor/basic/contentChangeExtension.ts index d86a742..036f372 100644 --- a/frontend/src/views/editor/basic/contentChangeExtension.ts +++ b/frontend/src/views/editor/basic/contentChangeExtension.ts @@ -1,5 +1,4 @@ import { EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view'; -import { useDocumentStore } from '@/stores/documentStore'; import { useEditorStore } from '@/stores/editorStore'; /** @@ -8,7 +7,6 @@ import { useEditorStore } from '@/stores/editorStore'; export function createContentChangePlugin() { return ViewPlugin.fromClass( class ContentChangePlugin { - private documentStore = useDocumentStore(); private editorStore = useEditorStore(); private lastContent = ''; @@ -24,11 +22,8 @@ export function createContentChangePlugin() { this.lastContent = newContent; - // 通知编辑器管理器内容已变化 - const currentDocId = this.documentStore.currentDocumentId; - if (currentDocId) { - this.editorStore.onContentChange(currentDocId); - } + this.editorStore.onContentChange(); + } destroy() { diff --git a/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts b/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts index dfd4e91..14181f3 100644 --- a/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts +++ b/frontend/src/views/editor/extensions/codeblock/cursorProtection.ts @@ -6,6 +6,34 @@ import { EditorView } from '@codemirror/view'; import { EditorSelection } from '@codemirror/state'; import { blockState } from './state'; +import { Block } from './types'; + +/** + * 二分查找:找到包含指定位置的块 + * blocks 数组按位置排序,使用二分查找 O(log n) + */ +function findBlockAtPos(blocks: Block[], pos: number): Block | null { + let left = 0; + let right = blocks.length - 1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const block = blocks[mid]; + + if (pos < block.range.from) { + // 位置在当前块之前 + right = mid - 1; + } else if (pos > block.range.to) { + // 位置在当前块之后 + left = mid + 1; + } else { + // 位置在当前块范围内 + return block; + } + } + + return null; +} /** * 检查位置是否在分隔符区域内 @@ -13,14 +41,13 @@ import { blockState } from './state'; function isInDelimiter(view: EditorView, pos: number): boolean { try { const blocks = view.state.field(blockState, false); - if (!blocks) return false; + if (!blocks || blocks.length === 0) return false; - for (const block of blocks) { - if (pos >= block.delimiter.from && pos < block.delimiter.to) { - return true; - } - } - return false; + const block = findBlockAtPos(blocks, pos); + if (!block) return false; + + // 检查是否在该块的分隔符区域内 + return pos >= block.delimiter.from && pos < block.delimiter.to; } catch { return false; } @@ -34,22 +61,23 @@ function adjustPosition(view: EditorView, pos: number, forward: boolean): number const blocks = view.state.field(blockState, false); if (!blocks || blocks.length === 0) return pos; - for (const block of blocks) { - // 如果位置在分隔符内 - if (pos >= block.delimiter.from && pos < block.delimiter.to) { - // 向前移动:跳到该块内容的开始 - // 向后移动:跳到前一个块的内容末尾 - if (forward) { - return block.content.from; - } else { - // 找到前一个块 - const blockIndex = blocks.indexOf(block); - if (blockIndex > 0) { - const prevBlock = blocks[blockIndex - 1]; - return prevBlock.content.to; - } - return block.delimiter.from; + const block = findBlockAtPos(blocks, pos); + if (!block) return pos; + + // 如果位置在分隔符内 + if (pos >= block.delimiter.from && pos < block.delimiter.to) { + // 向前移动:跳到该块内容的开始 + // 向后移动:跳到前一个块的内容末尾 + if (forward) { + return block.content.from; + } else { + // 找到前一个块的索引 + const blockIndex = blocks.indexOf(block); + if (blockIndex > 0) { + const prevBlock = blocks[blockIndex - 1]; + return prevBlock.content.to; } + return block.delimiter.from; } } diff --git a/frontend/src/views/editor/extensions/markdownPreview/index.ts b/frontend/src/views/editor/extensions/markdownPreview/index.ts new file mode 100644 index 0000000..1b51a50 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/index.ts @@ -0,0 +1,62 @@ +/** + * Markdown 预览扩展主入口 + */ +import { EditorView } from "@codemirror/view"; +import { useThemeStore } from "@/stores/themeStore"; +import { usePanelStore } from "@/stores/panelStore"; +import { useDocumentStore } from "@/stores/documentStore"; +import { getActiveNoteBlock } from "../codeblock/state"; +import { createMarkdownPreviewTheme } from "./styles"; +import { previewPanelState, previewPanelPlugin, togglePreview, closePreviewWithAnimation } from "./state"; + +/** + * 切换预览面板的命令 + */ +export function toggleMarkdownPreview(view: EditorView): boolean { + const panelStore = usePanelStore(); + const documentStore = useDocumentStore(); + const currentState = view.state.field(previewPanelState, false); + const activeBlock = getActiveNoteBlock(view.state as any); + + // 如果当前没有激活的 Markdown 块,不执行操作 + if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') { + return false; + } + + // 获取当前文档ID + const currentDocumentId = documentStore.currentDocumentId; + if (currentDocumentId === null) { + return false; + } + + // 如果预览面板已打开(无论预览的是不是当前块),关闭预览 + if (panelStore.markdownPreview.isOpen && !panelStore.markdownPreview.isClosing) { + // 使用带动画的关闭函数 + closePreviewWithAnimation(view); + } else { + // 否则,打开当前块的预览 + view.dispatch({ + effects: togglePreview.of({ + documentId: currentDocumentId, + blockFrom: activeBlock.content.from, + blockTo: activeBlock.content.to + }) + }); + + // 注意:store 状态由 ViewPlugin 在面板创建成功后更新 + } + + return true; +} + +/** + * 导出 Markdown 预览扩展 + */ +export function markdownPreviewExtension() { + const themeStore = useThemeStore(); + const colors = themeStore.currentColors; + + const theme = colors ? createMarkdownPreviewTheme(colors) : EditorView.baseTheme({}); + + return [previewPanelState, previewPanelPlugin, theme]; +} diff --git a/frontend/src/views/editor/extensions/markdownPreview/markdownRenderer.ts b/frontend/src/views/editor/extensions/markdownPreview/markdownRenderer.ts new file mode 100644 index 0000000..0c424f7 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/markdownRenderer.ts @@ -0,0 +1,117 @@ +/** + * Markdown 渲染器配置和自定义插件 + */ +import MarkdownIt from 'markdown-it'; +import {tasklist} from "@mdit/plugin-tasklist"; +import {katex} from "@mdit/plugin-katex"; +import markPlugin from "@/common/markdown-it/plugins/markdown-it-mark"; +import hljs from 'highlight.js'; +import 'highlight.js/styles/default.css'; +import {full as emoji} from '@/common/markdown-it/plugins/markdown-it-emoji/' +import footnote_plugin from "@/common/markdown-it/plugins/markdown-it-footnote" +import sup_plugin from "@/common/markdown-it/plugins/markdown-it-sup" +import ins_plugin from "@/common/markdown-it/plugins/markdown-it-ins" +import deflist_plugin from "@/common/markdown-it/plugins/markdown-it-deflist" +import abbr_plugin from "@/common/markdown-it/plugins/markdown-it-abbr" +import sub_plugin from "@/common/markdown-it/plugins/markdown-it-sub" +import {MermaidIt} from "@/common/markdown-it/plugins/markdown-it-mermaid" +import {useThemeStore} from '@/stores/themeStore' + +/** + * 自定义链接插件:使用 data-href 替代 href,配合事件委托实现自定义跳转 + */ +export function customLinkPlugin(md: MarkdownIt) { + // 保存默认的 link_open 渲染器 + const defaultRender = md.renderer.rules.link_open || function (tokens, idx, options, env, self) { + return self.renderToken(tokens, idx, options); + }; + + // 重写 link_open 渲染器 + md.renderer.rules.link_open = function (tokens, idx, options, env, self) { + const token = tokens[idx]; + + // 获取 href 属性 + const hrefIndex = token.attrIndex('href'); + if (hrefIndex >= 0) { + const href = token.attrs![hrefIndex][1]; + + // 添加 data-href 属性保存原始链接 + token.attrPush(['data-href', href]); + + // 添加 class 用于样式 + const classIndex = token.attrIndex('class'); + if (classIndex < 0) { + token.attrPush(['class', 'markdown-link']); + } else { + token.attrs![classIndex][1] += ' markdown-link'; + } + + // 移除 href 属性,防止默认跳转 + token.attrs!.splice(hrefIndex, 1); + } + + return defaultRender(tokens, idx, options, env, self); + }; +} + +/** + * 创建 Markdown-It 实例 + */ +export function createMarkdownRenderer(): MarkdownIt { + const themeStore = useThemeStore(); + const mermaidTheme = themeStore.isDarkMode ? "dark" : "default"; + + return new MarkdownIt({ + html: true, + linkify: true, + typographer: true, + breaks: true, + langPrefix: "language-", + highlight: (code, lang) => { + // 对于大代码块(>1000行),跳过高亮以提升性能 + if (code.length > 50000) { + return `
    ${code}
    `; + } + + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(code, {language: lang, ignoreIllegals: true}).value; + } catch (error) { + console.warn(`Failed to highlight code block with language: ${lang}`, error); + return code; + } + } + + // 对于中等大小的代码块(>5000字符),跳过自动检测 + if (code.length > 5000) { + return code; + } + + // 小代码块才使用自动检测 + try { + return hljs.highlightAuto(code).value; + } catch (error) { + console.warn('Failed to auto-highlight code block', error); + return code; + } + } + }) + .use(tasklist, { + disabled: false, + }) + .use(customLinkPlugin) + .use(markPlugin) + .use(emoji) + .use(footnote_plugin) + .use(sup_plugin) + .use(ins_plugin) + .use(deflist_plugin) + .use(abbr_plugin) + .use(sub_plugin) + .use(katex) + .use(MermaidIt, { + theme: mermaidTheme + }); +} + + diff --git a/frontend/src/views/editor/extensions/markdownPreview/panel.ts b/frontend/src/views/editor/extensions/markdownPreview/panel.ts new file mode 100644 index 0000000..e1d173f --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/panel.ts @@ -0,0 +1,373 @@ +/** + * Markdown 预览面板 UI 组件 + */ +import {EditorView, Panel, ViewUpdate} from "@codemirror/view"; +import MarkdownIt from 'markdown-it'; +import * as runtime from "@wailsio/runtime"; +import {previewPanelState} from "./state"; +import {createMarkdownRenderer} from "./markdownRenderer"; +import {updateMermaidTheme} from "@/common/markdown-it/plugins/markdown-it-mermaid"; +import {useThemeStore} from "@/stores/themeStore"; +import {watch} from "vue"; +import {createDebounce} from "@/common/utils/debounce"; +import {morphHTML} from "@/common/utils/domDiff"; + +/** + * Markdown 预览面板类 + */ +export class MarkdownPreviewPanel { + private md: MarkdownIt; + private readonly dom: HTMLDivElement; + private readonly resizeHandle: HTMLDivElement; + private readonly content: HTMLDivElement; + private view: EditorView; + private themeUnwatch?: () => void; + private lastRenderedContent: string = ""; + private debouncedUpdate: ReturnType; + private isDestroyed: boolean = false; // 标记面板是否已销毁 + + constructor(view: EditorView) { + this.view = view; + this.md = createMarkdownRenderer(); + + // 创建防抖更新函数 + this.debouncedUpdate = createDebounce(() => { + this.updateContentInternal(); + }, { delay: 1000 }); + + // 监听主题变化 + const themeStore = useThemeStore(); + this.themeUnwatch = watch(() => themeStore.isDarkMode, (isDark) => { + const newTheme = isDark ? "dark" : "default"; + updateMermaidTheme(newTheme); + this.lastRenderedContent = ""; // 清空缓存,强制重新渲染 + this.updateContentInternal(); + }); + + // 创建 DOM 结构 + this.dom = document.createElement("div"); + this.dom.className = "cm-markdown-preview-panel"; + + this.resizeHandle = document.createElement("div"); + this.resizeHandle.className = "cm-preview-resize-handle"; + + this.content = document.createElement("div"); + this.content.className = "cm-preview-content"; + + this.dom.appendChild(this.resizeHandle); + this.dom.appendChild(this.content); + + // 设置默认高度为编辑器高度的一半 + const defaultHeight = Math.floor(this.view.dom.clientHeight / 2); + this.dom.style.height = `${defaultHeight}px`; + + // 初始化拖动功能 + this.initResize(); + + // 初始化链接点击处理 + this.initLinkHandler(); + + // 初始渲染 + this.updateContentInternal(); + } + + /** + * 初始化链接点击处理(事件委托) + */ + private initLinkHandler(): void { + this.content.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + + // 查找最近的 标签 + let linkElement = target; + while (linkElement && linkElement !== this.content) { + if (linkElement.tagName === 'A') { + const anchor = linkElement as HTMLAnchorElement; + const href = anchor.getAttribute('href'); + + // 处理脚注内部锚点链接 + if (href && href.startsWith('#')) { + e.preventDefault(); + + // 在预览面板内查找目标元素 + const targetId = href.substring(1); + + // 使用 getElementById 而不是 querySelector,因为 ID 可能包含特殊字符(如冒号) + const targetElement = document.getElementById(targetId); + + if (targetElement && this.content.contains(targetElement)) { + // 平滑滚动到目标元素 + targetElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } + return; + } + + // 处理带 data-href 的外部链接 + if (anchor.hasAttribute('data-href')) { + e.preventDefault(); + const url = anchor.getAttribute('data-href'); + if (url && this.isValidUrl(url)) { + runtime.Browser.OpenURL(url); + } + return; + } + + // 处理其他链接 + if (href && !href.startsWith('#')) { + e.preventDefault(); + + // 只有有效的 URL(http/https/mailto/file 等)才用浏览器打开 + if (this.isValidUrl(href)) { + runtime.Browser.OpenURL(href); + } else { + // 相对路径或无效链接,显示提示 + console.warn('Invalid or relative link in preview:', href); + } + return; + } + } + + linkElement = linkElement.parentElement as HTMLElement; + } + }); + } + + /** + * 检查是否是有效的 URL(包含协议) + */ + private isValidUrl(url: string): boolean { + try { + // 检查是否包含协议 + if (url.match(/^[a-zA-Z][a-zA-Z\d+\-.]*:/)) { + const parsedUrl = new URL(url); + // 允许的协议列表 + const allowedProtocols = ['http:', 'https:', 'mailto:', 'file:', 'ftp:']; + return allowedProtocols.includes(parsedUrl.protocol); + } + return false; + } catch { + return false; + } + } + + /** + * 初始化拖动调整高度功能 + */ + private initResize(): void { + let startY = 0; + let startHeight = 0; + + const onMouseMove = (e: MouseEvent) => { + const delta = startY - e.clientY; + const maxHeight = this.getMaxHeight(); + const newHeight = Math.max(100, Math.min(maxHeight, startHeight + delta)); + this.dom.style.height = `${newHeight}px`; + }; + + const onMouseUp = () => { + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + this.resizeHandle.classList.remove("dragging"); + // 恢复 body 样式 + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + this.resizeHandle.addEventListener("mousedown", (e) => { + e.preventDefault(); + startY = e.clientY; + startHeight = this.dom.offsetHeight; + this.resizeHandle.classList.add("dragging"); + // 设置 body 样式,防止拖动时光标闪烁 + document.body.style.cursor = "ns-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }); + } + + /** + * 动态计算最大高度(编辑器高度) + */ + private getMaxHeight(): number { + return this.view.dom.clientHeight; + } + + /** + * 内部更新预览内容(带缓存 + DOM Diff 优化) + */ + private updateContentInternal(): void { + // 如果面板已销毁,直接返回 + if (this.isDestroyed) { + return; + } + + try { + const state = this.view.state; + const currentPreviewState = state.field(previewPanelState, false); + + if (!currentPreviewState) { + return; + } + + const blockContent = state.doc.sliceString( + currentPreviewState.blockFrom, + currentPreviewState.blockTo + ); + + if (!blockContent || blockContent.trim().length === 0) { + return; + } + + // 缓存检查:如果内容没变,不重新渲染 + if (blockContent === this.lastRenderedContent) { + return; + } + + // 对于大内容,使用异步渲染避免阻塞主线程 + if (blockContent.length > 1000) { + this.renderLargeContentAsync(blockContent); + } else { + // 小内容使用 DOM Diff 优化渲染 + this.renderWithDiff(blockContent); + } + + } catch (error) { + console.warn("Error updating preview content:", error); + } + } + + /** + * 使用 DOM Diff 渲染内容(保留未变化的节点) + */ + private renderWithDiff(content: string): void { + // 如果面板已销毁,直接返回 + if (this.isDestroyed) { + return; + } + + try { + const newHtml = this.md.render(content); + + // 如果是首次渲染或内容为空,直接设置 innerHTML + if (!this.lastRenderedContent || this.content.children.length === 0) { + this.content.innerHTML = newHtml; + } else { + // 使用 DOM Diff 增量更新 + morphHTML(this.content, newHtml); + } + + this.lastRenderedContent = content; + } catch (error) { + console.warn("Error rendering with diff:", error); + // 降级到直接设置 innerHTML + if (!this.isDestroyed) { + this.content.innerHTML = this.md.render(content); + this.lastRenderedContent = content; + } + } + } + + /** + * 异步渲染大内容(使用 DOM Diff 优化) + */ + private renderLargeContentAsync(content: string): void { + // 如果面板已销毁,直接返回 + if (this.isDestroyed) { + return; + } + + // 如果是首次渲染,显示加载状态 + if (!this.lastRenderedContent) { + this.content.innerHTML = '
    Rendering...
    '; + } + + // 使用 requestIdleCallback 在浏览器空闲时渲染 + const callback = window.requestIdleCallback || ((cb: IdleRequestCallback) => setTimeout(cb, 1)); + + callback(() => { + // 再次检查是否已销毁(异步回调时可能已经关闭) + if (this.isDestroyed) { + return; + } + + try { + const html = this.md.render(content); + + // 如果是首次渲染或之前内容为空,直接设置 + if (!this.lastRenderedContent || this.content.children.length === 0) { + // 使用 DocumentFragment 减少 DOM 操作 + const fragment = document.createRange().createContextualFragment(html); + this.content.innerHTML = ''; + this.content.appendChild(fragment); + } else { + // 使用 DOM Diff 增量更新(保留滚动位置和未变化的节点) + morphHTML(this.content, html); + } + + this.lastRenderedContent = content; + } catch (error) { + console.warn("Error rendering large content:", error); + if (!this.isDestroyed) { + this.content.innerHTML = '
    Render failed
    '; + } + } + }); + } + + /** + * 响应编辑器更新 + */ + public update(update: ViewUpdate): void { + if (update.docChanged) { + // 文档改变时使用防抖更新 + this.debouncedUpdate.debouncedFn(); + } else if (update.selectionSet) { + // 光标移动时不触发更新 + // 如果需要根据光标位置更新,可以在这里处理 + } + } + + /** + * 清理资源 + */ + public destroy(): void { + // 标记为已销毁,防止异步回调继续执行 + this.isDestroyed = true; + + // 清理防抖 + if (this.debouncedUpdate) { + this.debouncedUpdate.cancel(); + } + + // 清理主题监听 + if (this.themeUnwatch) { + this.themeUnwatch(); + this.themeUnwatch = undefined; + } + + // 清空缓存 + this.lastRenderedContent = ""; + } + + /** + * 获取 CodeMirror Panel 对象 + */ + public getPanel(): Panel { + return { + top: false, + dom: this.dom, + update: (update: ViewUpdate) => this.update(update), + destroy: () => this.destroy() + }; + } +} + +/** + * 创建预览面板 + */ +export function createPreviewPanel(view: EditorView): Panel { + const panel = new MarkdownPreviewPanel(view); + return panel.getPanel(); +} + diff --git a/frontend/src/views/editor/extensions/markdownPreview/state.ts b/frontend/src/views/editor/extensions/markdownPreview/state.ts new file mode 100644 index 0000000..306dd44 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/state.ts @@ -0,0 +1,142 @@ +/** + * Markdown 预览面板的 CodeMirror 状态管理 + */ +import { EditorView, showPanel, ViewUpdate, ViewPlugin } from "@codemirror/view"; +import { StateEffect, StateField } from "@codemirror/state"; +import { getActiveNoteBlock } from "../codeblock/state"; +import { usePanelStore } from "@/stores/panelStore"; +import { createPreviewPanel } from "./panel"; +import type { PreviewState } from "./types"; + +/** + * 定义切换预览面板的 Effect + */ +export const togglePreview = StateEffect.define(); + +/** + * 关闭面板(带动画) + */ +export function closePreviewWithAnimation(view: EditorView): void { + const panelStore = usePanelStore(); + + // 标记开始关闭 + panelStore.startClosingMarkdownPreview(); + + const panelElement = view.dom.querySelector('.cm-panels.cm-panels-bottom') as HTMLElement; + if (panelElement) { + panelElement.style.animation = 'panelSlideDown 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; + // 等待动画完成后再关闭面板 + setTimeout(() => { + view.dispatch({ + effects: togglePreview.of(null) + }); + panelStore.closeMarkdownPreview(); + }, 280); + } else { + view.dispatch({ + effects: togglePreview.of(null) + }); + panelStore.closeMarkdownPreview(); + } +} + +/** + * 定义预览面板的状态字段 + */ +export const previewPanelState = StateField.define({ + create: () => null, + update(value, tr) { + const panelStore = usePanelStore(); + + for (let e of tr.effects) { + if (e.is(togglePreview)) { + value = e.value; + } + } + + // 如果有预览状态,智能管理预览生命周期 + if (value && !value.closing) { + const activeBlock = getActiveNoteBlock(tr.state as any); + + // 关键修复:检查预览状态是否属于当前文档 + // 如果 panelStore 中没有当前文档的预览状态(说明切换了文档), + // 则不执行关闭逻辑,保持其他文档的预览状态 + if (!panelStore.markdownPreview.isOpen) { + // 当前文档没有预览,不处理 + return value; + } + + // 场景1:离开 Markdown 块或无激活块 → 关闭预览 + if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') { + if (!panelStore.markdownPreview.isClosing) { + return { ...value, closing: true }; + } + } + // 场景2:切换到其他块(起始位置变化)→ 关闭预览 + else if (activeBlock.content.from !== value.blockFrom) { + if (!panelStore.markdownPreview.isClosing) { + return { ...value, closing: true }; + } + } + // 场景3:还在同一个块内编辑(只有结束位置变化)→ 更新范围,实时预览 + else if (activeBlock.content.to !== value.blockTo) { + // 更新 panelStore 中的预览范围 + panelStore.updatePreviewRange(value.blockFrom, activeBlock.content.to); + + return { + documentId: value.documentId, + blockFrom: value.blockFrom, + blockTo: activeBlock.content.to, + closing: false + }; + } + } + + return value; + }, + provide: f => showPanel.from(f, state => state ? createPreviewPanel : null) +}); + +/** + * 创建监听插件 + */ +export const previewPanelPlugin = ViewPlugin.fromClass(class { + private lastState: PreviewState | null | undefined = null; + private panelStore = usePanelStore(); + + constructor(private view: EditorView) { + this.lastState = view.state.field(previewPanelState, false); + this.panelStore.setEditorView(view); + } + + update(update: ViewUpdate) { + const currentState = update.state.field(previewPanelState, false); + + // 检测到面板打开(从 null 变为有值,且不是 closing) + if (currentState && !currentState.closing && !this.lastState) { + // 验证面板 DOM 是否真正创建成功 + requestAnimationFrame(() => { + const panelElement = this.view.dom.querySelector('.cm-markdown-preview-panel'); + if (panelElement) { + // 面板创建成功,更新 store 状态 + this.panelStore.openMarkdownPreview(currentState.blockFrom, currentState.blockTo); + } + }); + } + + // 检测到状态变为 closing + if (currentState?.closing && !this.lastState?.closing) { + // 触发关闭动画 + closePreviewWithAnimation(this.view); + } + + this.lastState = currentState; + } + + destroy() { + // 不调用 reset(),因为那会清空所有文档的预览状态 + // 只清理编辑器视图引用 + this.panelStore.setEditorView(null); + } +}); + diff --git a/frontend/src/views/editor/extensions/markdownPreview/styles.ts b/frontend/src/views/editor/extensions/markdownPreview/styles.ts new file mode 100644 index 0000000..f67f926 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/styles.ts @@ -0,0 +1,356 @@ +import { EditorView } from "@codemirror/view"; +import type { ThemeColors } from "@/views/editor/theme/types"; + +/** + * 创建 Markdown 预览面板的主题样式 + */ +export function createMarkdownPreviewTheme(colors: ThemeColors) { + // GitHub 官方颜色变量 + const isDark = colors.dark; + + // GitHub Light 主题颜色 + const lightColors = { + fg: { + default: "#1F2328", + muted: "#656d76", + subtle: "#6e7781" + }, + border: { + default: "#d0d7de", + muted: "#d8dee4" + }, + canvas: { + default: "#ffffff", + subtle: "#f6f8fa" + }, + accent: { + fg: "#0969da", + emphasis: "#0969da" + } + }; + + // GitHub Dark 主题颜色 + const darkColors = { + fg: { + default: "#e6edf3", + muted: "#7d8590", + subtle: "#6e7681" + }, + border: { + default: "#30363d", + muted: "#21262d" + }, + canvas: { + default: "#0d1117", + subtle: "#161b22" + }, + accent: { + fg: "#2f81f7", + emphasis: "#2f81f7" + } + }; + + const ghColors = isDark ? darkColors : lightColors; + + return EditorView.theme({ + // 面板容器 + ".cm-markdown-preview-panel": { + position: "relative", + display: "flex", + flexDirection: "column", + overflow: "hidden" + }, + + // 拖动调整大小的手柄 + ".cm-preview-resize-handle": { + width: "100%", + height: "3px", + backgroundColor: colors.borderColor, + cursor: "ns-resize", + position: "relative", + flexShrink: 0, + transition: "background-color 0.2s ease", + "&:hover": { + backgroundColor: colors.selection + }, + "&.dragging": { + backgroundColor: colors.selection + } + }, + + // 内容区域 + ".cm-preview-content": { + flex: 1, + padding: "45px", + overflow: "auto", + fontSize: "16px", + lineHeight: "1.5", + color: ghColors.fg.default, + wordWrap: "break-word", + fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'", + boxSizing: "border-box", + + // Loading state + "& .markdown-loading, & .markdown-error": { + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: "200px", + fontSize: "14px", + color: ghColors.fg.muted + }, + + "& .markdown-error": { + color: "#f85149" + }, + + // ========== 标题样式 ========== + "& h1, & h2, & h3, & h4, & h5, & h6": { + marginTop: "24px", + marginBottom: "16px", + fontWeight: "600", + lineHeight: "1.25", + color: ghColors.fg.default + }, + "& h1": { + fontSize: "2em", + borderBottom: `1px solid ${ghColors.border.muted}`, + paddingBottom: "0.3em" + }, + "& h2": { + fontSize: "1.5em", + borderBottom: `1px solid ${ghColors.border.muted}`, + paddingBottom: "0.3em" + }, + "& h3": { + fontSize: "1.25em" + }, + "& h4": { + fontSize: "1em" + }, + "& h5": { + fontSize: "0.875em" + }, + "& h6": { + fontSize: "0.85em", + color: ghColors.fg.muted + }, + + // ========== 段落和文本 ========== + "& p": { + marginTop: "0", + marginBottom: "16px" + }, + "& strong": { + fontWeight: "600" + }, + "& em": { + fontStyle: "italic" + }, + "& del": { + textDecoration: "line-through", + opacity: "0.7" + }, + + // ========== 列表 ========== + "& ul, & ol": { + paddingLeft: "2em", + marginTop: "0", + marginBottom: "16px" + }, + "& ul ul, & ul ol, & ol ol, & ol ul": { + marginTop: "0", + marginBottom: "0" + }, + "& li": { + wordWrap: "break-all" + }, + "& li > p": { + marginTop: "16px" + }, + "& li + li": { + marginTop: "0.25em" + }, + + // 任务列表 + "& .task-list-item": { + listStyleType: "none", + position: "relative", + paddingLeft: "1.5em" + }, + "& .task-list-item + .task-list-item": { + marginTop: "3px" + }, + "& .task-list-item input[type='checkbox']": { + font: "inherit", + overflow: "visible", + fontFamily: "inherit", + fontSize: "inherit", + lineHeight: "inherit", + boxSizing: "border-box", + padding: "0", + margin: "0 0.2em 0.25em -1.6em", + verticalAlign: "middle", + cursor: "pointer" + }, + + // ========== 代码块 ========== + "& code, & tt": { + fontFamily: "SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace", + fontSize: "85%", + padding: "0.2em 0.4em", + margin: "0", + backgroundColor: isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(27, 31, 35, 0.05)", + borderRadius: "3px" + }, + + "& pre": { + position: "relative", + backgroundColor: isDark ? "#161b22" : "#f6f8fa", + padding: "40px 16px 16px 16px", + borderRadius: "6px", + overflow: "auto", + margin: "16px 0", + fontSize: "85%", + lineHeight: "1.45", + wordWrap: "normal", + + // macOS 窗口样式 - 使用伪元素创建顶部栏 + "&::before": { + content: '""', + position: "absolute", + top: "0", + left: "0", + right: "0", + height: "28px", + backgroundColor: isDark ? "#1c1c1e" : "#e8e8e8", + borderBottom: `1px solid ${ghColors.border.default}`, + borderRadius: "6px 6px 0 0" + }, + + // macOS 三个控制按钮 + "&::after": { + content: '""', + position: "absolute", + top: "10px", + left: "12px", + width: "12px", + height: "12px", + borderRadius: "50%", + backgroundColor: isDark ? "#ec6a5f" : "#ff5f57", + boxShadow: ` + 18px 0 0 0 ${isDark ? "#f4bf4f" : "#febc2e"}, + 36px 0 0 0 ${isDark ? "#61c554" : "#28c840"} + ` + } + }, + + "& pre code, & pre tt": { + display: "inline", + maxWidth: "auto", + padding: "0", + margin: "0", + overflow: "visible", + lineHeight: "inherit", + wordWrap: "normal", + backgroundColor: "transparent", + border: "0", + fontSize: "100%", + color: ghColors.fg.default, + wordBreak: "normal", + whiteSpace: "pre" + }, + + // ========== 引用块 ========== + "& blockquote": { + margin: "16px 0", + padding: "0 1em", + color: isDark ? "#7d8590" : "#6a737d", + borderLeft: isDark ? "0.25em solid #3b434b" : "0.25em solid #dfe2e5" + }, + "& blockquote > :first-child": { + marginTop: "0" + }, + "& blockquote > :last-child": { + marginBottom: "0" + }, + + // ========== 分割线 ========== + "& hr": { + height: "0.25em", + padding: "0", + margin: "24px 0", + backgroundColor: isDark ? "#21262d" : "#e1e4e8", + border: "0", + overflow: "hidden", + boxSizing: "content-box" + }, + + // ========== 表格 ========== + "& table": { + borderSpacing: "0", + borderCollapse: "collapse", + display: "block", + width: "100%", + overflow: "auto", + marginTop: "0", + marginBottom: "16px" + }, + "& table tr": { + backgroundColor: isDark ? "#0d1117" : "#ffffff", + borderTop: isDark ? "1px solid #21262d" : "1px solid #c6cbd1" + }, + "& table th, & table td": { + padding: "6px 13px", + border: isDark ? "1px solid #30363d" : "1px solid #dfe2e5" + }, + "& table th": { + fontWeight: "600" + }, + + // ========== 链接 ========== + "& a, & .markdown-link": { + color: isDark ? "#58a6ff" : "#0366d6", + textDecoration: "none", + cursor: "pointer", + "&:hover": { + textDecoration: "underline" + } + }, + + // ========== 图片 ========== + "& img": { + maxWidth: "100%", + height: "auto", + borderRadius: "4px", + margin: "16px 0" + }, + + // ========== 其他元素 ========== + "& kbd": { + display: "inline-block", + padding: "3px 5px", + fontSize: "11px", + lineHeight: "10px", + color: ghColors.fg.default, + verticalAlign: "middle", + backgroundColor: ghColors.canvas.subtle, + border: `solid 1px ${isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(175, 184, 193, 0.2)"}`, + borderBottom: `solid 2px ${isDark ? "rgba(110, 118, 129, 0.4)" : "rgba(175, 184, 193, 0.2)"}`, + borderRadius: "6px", + boxShadow: "inset 0 -1px 0 rgba(175, 184, 193, 0.2)" + }, + + // 首个子元素去除上边距 + "& > *:first-child": { + marginTop: "0 !important" + }, + + // 最后一个子元素去除下边距 + "& > *:last-child": { + marginBottom: "0 !important" + } + } + }, { dark: colors.dark }); +} + diff --git a/frontend/src/views/editor/extensions/markdownPreview/types.ts b/frontend/src/views/editor/extensions/markdownPreview/types.ts new file mode 100644 index 0000000..e30deec --- /dev/null +++ b/frontend/src/views/editor/extensions/markdownPreview/types.ts @@ -0,0 +1,12 @@ +/** + * Markdown 预览面板相关类型定义 + */ + +// 预览面板状态 +export interface PreviewState { + documentId: number; // 预览所属的文档ID + blockFrom: number; + blockTo: number; + closing?: boolean; // 标记面板正在关闭 +} + diff --git a/frontend/src/views/editor/theme/base.ts b/frontend/src/views/editor/theme/base.ts index f57410c..d5b6419 100644 --- a/frontend/src/views/editor/theme/base.ts +++ b/frontend/src/views/editor/theme/base.ts @@ -10,280 +10,304 @@ import type {ThemeColors} from './types'; * @returns CodeMirror Extension数组 */ export function createBaseTheme(colors: ThemeColors): Extension { - // 编辑器主题样式 - const theme = EditorView.theme({ - '&': { - color: colors.foreground, - backgroundColor: colors.background, - }, + // 编辑器主题样式 + const theme = EditorView.theme({ + '&': { + color: colors.foreground, + backgroundColor: colors.background, + }, - // 确保编辑器容器背景一致 - '.cm-editor': { - backgroundColor: colors.background, - }, + // 确保编辑器容器背景一致 + '.cm-editor': { + backgroundColor: colors.background, + }, - // 确保滚动区域背景一致 - '.cm-scroller': { - backgroundColor: colors.background, - }, + // 确保滚动区域背景一致 + '.cm-scroller': { + backgroundColor: colors.background, + transition: 'height 0.3s cubic-bezier(0.4, 0, 0.2, 1)', + }, - // 编辑器内容 - '.cm-content': { - caretColor: colors.cursor, - paddingTop: '4px', - }, + // 编辑器内容 + '.cm-content': { + caretColor: colors.cursor, + paddingTop: '4px', + }, - // 光标 - '.cm-cursor, .cm-dropCursor': { - borderLeftColor: colors.cursor, - borderLeftWidth: '2px', - paddingTop: '4px', - marginTop: '-2px', - }, + // 光标 + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: colors.cursor, + borderLeftWidth: '2px', + paddingTop: '4px', + marginTop: '-2px', + }, - // 选择 - '.cm-selectionBackground': { - backgroundColor: colors.selectionBlur, - }, - '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { - backgroundColor: colors.selection, - }, - '.cm-content ::selection': { - backgroundColor: colors.selection, - }, - '.cm-activeLine.code-empty-block-selected': { - backgroundColor: colors.selection, - }, + // 选择 + '.cm-selectionBackground': { + backgroundColor: colors.selectionBlur, + }, + '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { + backgroundColor: colors.selection, + }, + '.cm-content ::selection': { + backgroundColor: colors.selection, + }, + '.cm-activeLine.code-empty-block-selected': { + backgroundColor: colors.selection, + }, - // 当前行高亮 - '.cm-activeLine': { - backgroundColor: colors.activeLine - }, + // 当前行高亮 + '.cm-activeLine': { + backgroundColor: colors.activeLine + }, - // 行号区域 - '.cm-gutters': { - backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)', - color: colors.lineNumber, - border: 'none', - borderRight: colors.dark ? 'none' : `1px solid ${colors.borderLight}`, - padding: '0 2px 0 4px', - userSelect: 'none', - }, - '.cm-activeLineGutter': { - backgroundColor: 'transparent', - color: colors.activeLineNumber, - }, + // 行号区域 + '.cm-gutters': { + backgroundColor: colors.dark ? 'rgba(0,0,0, 0.1)' : 'rgba(0,0,0, 0.04)', + color: colors.lineNumber, + border: 'none', + borderRight: colors.dark ? 'none' : `1px solid ${colors.borderLight}`, + padding: '0 2px 0 4px', + userSelect: 'none', + }, + '.cm-activeLineGutter': { + backgroundColor: 'transparent', + color: colors.activeLineNumber, + }, - // 折叠功能 - '.cm-foldGutter': { - marginLeft: '0px', - }, - '.cm-foldGutter .cm-gutterElement': { - opacity: 0, - transition: 'opacity 400ms', - }, - '.cm-gutters:hover .cm-gutterElement': { - opacity: 1, - }, - '.cm-foldPlaceholder': { - backgroundColor: 'transparent', - border: 'none', - color: colors.comment, - }, + // 折叠功能 + '.cm-foldGutter': { + marginLeft: '0px', + }, + '.cm-foldGutter .cm-gutterElement': { + opacity: 0, + transition: 'opacity 400ms', + }, + '.cm-gutters:hover .cm-gutterElement': { + opacity: 1, + }, + '.cm-foldPlaceholder': { + backgroundColor: 'transparent', + border: 'none', + color: colors.comment, + }, - // 面板 - '.cm-panels': { - backgroundColor: colors.dropdownBackground, - color: colors.foreground - }, - '.cm-panels.cm-panels-top': { - borderBottom: '2px solid black' - }, - '.cm-panels.cm-panels-bottom': { - borderTop: '2px solid black' - }, + // 面板 + '.cm-panels': { + // backgroundColor: colors.dropdownBackground, + // color: colors.foreground + }, + '.cm-panels.cm-panels-top': { + borderBottom: '2px solid black' + }, + '.cm-panels.cm-panels-bottom': { + animation: 'panelSlideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1)' + }, + '@keyframes panelSlideUp': { + from: { + transform: 'translateY(100%)', + opacity: '0' + }, + to: { + transform: 'translateY(0)', + opacity: '1' + } + }, + '@keyframes panelSlideDown': { + from: { + transform: 'translateY(0)', + opacity: '1' + }, + to: { + transform: 'translateY(100%)', + opacity: '0' + } + }, - // 搜索匹配 - '.cm-searchMatch': { - backgroundColor: 'transparent', - outline: `1px solid ${colors.searchMatch}`, - }, - '.cm-searchMatch.cm-searchMatch-selected': { - backgroundColor: colors.searchMatch, - color: colors.background, - }, - '.cm-selectionMatch': { - backgroundColor: colors.dark ? '#50606D' : '#e6f3ff', - }, + // 搜索匹配 + '.cm-searchMatch': { + backgroundColor: 'transparent', + outline: `1px solid ${colors.searchMatch}`, + }, + '.cm-searchMatch.cm-searchMatch-selected': { + backgroundColor: colors.searchMatch, + color: colors.background, + }, + '.cm-selectionMatch': { + backgroundColor: colors.dark ? '#50606D' : '#e6f3ff', + }, - // 括号匹配 - '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { - outline: `0.5px solid ${colors.searchMatch}`, - }, - '&.cm-focused .cm-matchingBracket': { - backgroundColor: colors.matchingBracket, - color: 'inherit', - }, - '&.cm-focused .cm-nonmatchingBracket': { - outline: colors.dark ? '0.5px solid #bc8f8f' : '0.5px solid #d73a49', - }, + // 括号匹配 + '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': { + outline: `0.5px solid ${colors.searchMatch}`, + }, + '&.cm-focused .cm-matchingBracket': { + backgroundColor: colors.matchingBracket, + color: 'inherit', + }, + '&.cm-focused .cm-nonmatchingBracket': { + outline: colors.dark ? '0.5px solid #bc8f8f' : '0.5px solid #d73a49', + }, - // 编辑器焦点 - '&.cm-editor.cm-focused': { - outline: 'none', - }, + // 编辑器焦点 + '&.cm-editor.cm-focused': { + outline: 'none', + }, - // 工具提示 - '.cm-tooltip': { - border: colors.dark ? 'none' : `1px solid ${colors.dropdownBorder}`, - backgroundColor: colors.surface, - color: colors.foreground, - boxShadow: colors.dark ? 'none' : '0 2px 8px rgba(0,0,0,0.1)', - }, - '.cm-tooltip .cm-tooltip-arrow:before': { - borderTopColor: 'transparent', - borderBottomColor: 'transparent', - }, - '.cm-tooltip .cm-tooltip-arrow:after': { - borderTopColor: colors.surface, - borderBottomColor: colors.surface, - }, - '.cm-tooltip-autocomplete': { - '& > ul > li[aria-selected]': { - backgroundColor: colors.activeLine, - color: colors.foreground, - }, - }, + // 工具提示 + '.cm-tooltip': { + border: colors.dark ? 'none' : `1px solid ${colors.dropdownBorder}`, + backgroundColor: colors.surface, + color: colors.foreground, + boxShadow: colors.dark ? 'none' : '0 2px 8px rgba(0,0,0,0.1)', + }, + '.cm-tooltip .cm-tooltip-arrow:before': { + borderTopColor: 'transparent', + borderBottomColor: 'transparent', + }, + '.cm-tooltip .cm-tooltip-arrow:after': { + borderTopColor: colors.surface, + borderBottomColor: colors.surface, + }, + '.cm-tooltip-autocomplete': { + '& > ul > li[aria-selected]': { + backgroundColor: colors.activeLine, + color: colors.foreground, + }, + }, - // 代码块层(自定义) - '.code-blocks-layer': { - width: '100%', - }, - '.code-blocks-layer .block-even, .code-blocks-layer .block-odd': { - width: '100%', - boxSizing: 'content-box', - }, - '.code-blocks-layer .block-even': { - background: colors.background, - borderTop: `1px solid ${colors.borderColor}`, - }, - '.code-blocks-layer .block-even:first-child': { - borderTop: 'none', - }, - '.code-blocks-layer .block-odd': { - background: colors.backgroundSecondary, - borderTop: `1px solid ${colors.borderColor}`, - }, + // 代码块层(自定义) + '.code-blocks-layer': { + width: '100%', + }, + '.code-blocks-layer .block-even, .code-blocks-layer .block-odd': { + width: '100%', + boxSizing: 'content-box', + }, + '.code-blocks-layer .block-even': { + background: colors.background, + borderTop: `1px solid ${colors.borderColor}`, + }, + '.code-blocks-layer .block-even:first-child': { + borderTop: 'none', + }, + '.code-blocks-layer .block-odd': { + background: colors.backgroundSecondary, + borderTop: `1px solid ${colors.borderColor}`, + }, - // 数学计算结果(自定义) - '.code-blocks-math-result': { - paddingLeft: "12px", - position: "relative", - }, - ".code-blocks-math-result .inner": { - background: colors.dark ? '#0e1217' : '#48b57e', - color: colors.dark ? '#a0e7c7' : '#fff', - padding: '0px 4px', - borderRadius: '2px', - boxShadow: colors.dark ? '0 0 3px rgba(0,0,0, 0.3)' : '0 0 3px rgba(0,0,0, 0.1)', - cursor: 'pointer', - whiteSpace: "nowrap", - }, - '.code-blocks-math-result-copied': { - position: "absolute", - top: "0px", - left: "0px", - marginLeft: "calc(100% + 10px)", - width: "60px", - transition: "opacity 500ms", - transitionDelay: "1000ms", - color: colors.dark ? 'rgba(220,240,230, 1.0)' : 'rgba(0,0,0, 0.8)', - }, - '.code-blocks-math-result-copied.fade-out': { - opacity: 0, - }, + // 数学计算结果(自定义) + '.code-blocks-math-result': { + paddingLeft: "12px", + position: "relative", + }, + ".code-blocks-math-result .inner": { + background: colors.dark ? '#0e1217' : '#48b57e', + color: colors.dark ? '#a0e7c7' : '#fff', + padding: '0px 4px', + borderRadius: '2px', + boxShadow: colors.dark ? '0 0 3px rgba(0,0,0, 0.3)' : '0 0 3px rgba(0,0,0, 0.1)', + cursor: 'pointer', + whiteSpace: "nowrap", + }, + '.code-blocks-math-result-copied': { + position: "absolute", + top: "0px", + left: "0px", + marginLeft: "calc(100% + 10px)", + width: "60px", + transition: "opacity 500ms", + transitionDelay: "1000ms", + color: colors.dark ? 'rgba(220,240,230, 1.0)' : 'rgba(0,0,0, 0.8)', + }, + '.code-blocks-math-result-copied.fade-out': { + opacity: 0, + }, - // 代码块开始标记(自定义) - '.code-block-start': { - height: '12px', - position: 'relative', - }, - '.code-block-start.first': { - height: '0px', - }, - }, {dark: colors.dark}); + // 代码块开始标记(自定义) + '.code-block-start': { + height: '12px', + position: 'relative', + }, + '.code-block-start.first': { + height: '0px', + }, + }, {dark: colors.dark}); - // 语法高亮样式 - const highlightStyle = HighlightStyle.define([ - // 关键字 - {tag: tags.keyword, color: colors.keyword}, - - // 操作符 - {tag: [tags.operator, tags.operatorKeyword], color: colors.operator}, - - // 名称、变量 - {tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: colors.variable}, - {tag: [tags.variableName], color: colors.variable}, - {tag: [tags.labelName], color: colors.operator}, - {tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: colors.variable}, - - // 函数 - {tag: [tags.function(tags.variableName)], color: colors.function}, - {tag: [tags.propertyName], color: colors.function}, - - // 类型、类 - {tag: [tags.typeName], color: colors.type}, - {tag: [tags.className], color: colors.class}, - - // 常量 - {tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.constant}, - - // 字符串 - {tag: [tags.processingInstruction, tags.string, tags.inserted], color: colors.string}, - {tag: [tags.special(tags.string)], color: colors.string}, - {tag: [tags.quote], color: colors.comment}, - - // 数字 - {tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], color: colors.number}, - - // 正则表达式 - {tag: [tags.url, tags.escape, tags.regexp, tags.link], color: colors.regexp}, - - // 注释 - {tag: [tags.meta, tags.comment], color: colors.comment, fontStyle: 'italic'}, - - // 分隔符、括号 - {tag: [tags.definition(tags.name), tags.separator], color: colors.variable}, - {tag: [tags.brace], color: colors.variable}, - {tag: [tags.squareBracket], color: colors.dark ? '#bf616a' : colors.keyword}, - {tag: [tags.angleBracket], color: colors.dark ? '#d08770' : colors.operator}, - {tag: [tags.attributeName], color: colors.variable}, - - // 标签 - {tag: [tags.tagName], color: colors.number}, - - // 注解 - {tag: [tags.annotation], color: colors.invalid}, - - // 特殊样式 - {tag: tags.strong, fontWeight: 'bold'}, - {tag: tags.emphasis, fontStyle: 'italic'}, - {tag: tags.strikethrough, textDecoration: 'line-through'}, - {tag: tags.link, color: colors.variable, textDecoration: 'underline'}, - - // 标题 - {tag: tags.heading, fontWeight: 'bold', color: colors.heading}, - {tag: [tags.heading1, tags.heading2], fontSize: '1.4em'}, - {tag: [tags.heading3, tags.heading4], fontSize: '1.2em'}, - {tag: [tags.heading5, tags.heading6], fontSize: '1.1em'}, - - // 无效内容 - {tag: tags.invalid, color: colors.invalid}, - ]); + // 语法高亮样式 + const highlightStyle = HighlightStyle.define([ + // 关键字 + {tag: tags.keyword, color: colors.keyword}, - return [ - theme, - syntaxHighlighting(highlightStyle), - ]; + // 操作符 + {tag: [tags.operator, tags.operatorKeyword], color: colors.operator}, + + // 名称、变量 + {tag: [tags.name, tags.deleted, tags.character, tags.macroName], color: colors.variable}, + {tag: [tags.variableName], color: colors.variable}, + {tag: [tags.labelName], color: colors.operator}, + {tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: colors.variable}, + + // 函数 + {tag: [tags.function(tags.variableName)], color: colors.function}, + {tag: [tags.propertyName], color: colors.function}, + + // 类型、类 + {tag: [tags.typeName], color: colors.type}, + {tag: [tags.className], color: colors.class}, + + // 常量 + {tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: colors.constant}, + + // 字符串 + {tag: [tags.processingInstruction, tags.string, tags.inserted], color: colors.string}, + {tag: [tags.special(tags.string)], color: colors.string}, + {tag: [tags.quote], color: colors.comment}, + + // 数字 + { + tag: [tags.number, tags.changed, tags.annotation, tags.modifier, tags.self, tags.namespace], + color: colors.number + }, + + // 正则表达式 + {tag: [tags.url, tags.escape, tags.regexp, tags.link], color: colors.regexp}, + + // 注释 + {tag: [tags.meta, tags.comment], color: colors.comment, fontStyle: 'italic'}, + + // 分隔符、括号 + {tag: [tags.definition(tags.name), tags.separator], color: colors.variable}, + {tag: [tags.brace], color: colors.variable}, + {tag: [tags.squareBracket], color: colors.dark ? '#bf616a' : colors.keyword}, + {tag: [tags.angleBracket], color: colors.dark ? '#d08770' : colors.operator}, + {tag: [tags.attributeName], color: colors.variable}, + + // 标签 + {tag: [tags.tagName], color: colors.number}, + + // 注解 + {tag: [tags.annotation], color: colors.invalid}, + + // 特殊样式 + {tag: tags.strong, fontWeight: 'bold'}, + {tag: tags.emphasis, fontStyle: 'italic'}, + {tag: tags.strikethrough, textDecoration: 'line-through'}, + {tag: tags.link, color: colors.variable, textDecoration: 'underline'}, + + // 标题 + {tag: tags.heading, fontWeight: 'bold', color: colors.heading}, + {tag: [tags.heading1, tags.heading2], fontSize: '1.4em'}, + {tag: [tags.heading3, tags.heading4], fontSize: '1.2em'}, + {tag: [tags.heading5, tags.heading6], fontSize: '1.1em'}, + + // 无效内容 + {tag: tags.invalid, color: colors.invalid}, + ]); + + return [ + theme, + syntaxHighlighting(highlightStyle), + ]; } diff --git a/frontend/src/views/settings/pages/UpdatesPage.vue b/frontend/src/views/settings/pages/UpdatesPage.vue index 565ea29..e5b0a1b 100644 --- a/frontend/src/views/settings/pages/UpdatesPage.vue +++ b/frontend/src/views/settings/pages/UpdatesPage.vue @@ -6,14 +6,14 @@ import { useUpdateStore } from '@/stores/updateStore'; import SettingSection from '../components/SettingSection.vue'; import SettingItem from '../components/SettingItem.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue'; -import { createMarkdownExit } from 'markdown-exit' +import markdownit from 'markdown-it' const { t } = useI18n(); const configStore = useConfigStore(); const updateStore = useUpdateStore(); // 初始化Remarkable实例并配置 -const md = createMarkdownExit({ +const md = markdownit({ html: true, // 允许HTML linkify: false, // 不解析链接 typographer: true, // 开启智能引号 diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 7834755..33bdf56 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -7,6 +7,10 @@ import {nodePolyfills} from 'vite-plugin-node-polyfills'; export default defineConfig(({mode}: { mode: string }): object => { const env: Record = loadEnv(mode, process.cwd()); return { + test: { + environment: 'happy-dom', + globals: true, + }, publicDir: './public', base: './', resolve: {