diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2fd9f03..4a88277 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -39,33 +39,30 @@ "@codemirror/view": "^6.38.8", "@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", + "@lezer/lr": "^1.4.4", "@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", + "@types/katex": "^0.16.7", "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", "java-parser": "^3.0.1", + "katex": "^0.16.25", "linguist-languages": "^9.1.0", - "markdown-it": "^14.1.0", + "marked": "^17.0.1", "mermaid": "^11.12.1", - "npm": "^11.6.3", "php-parser": "^3.2.5", "pinia": "^3.0.4", "pinia-plugin-persistedstate": "^4.7.1", - "prettier": "^3.6.2", + "prettier": "^3.7.2", "sass": "^1.94.2", - "vue": "^3.5.24", - "vue-i18n": "^11.2.1", + "vue": "^3.5.25", + "vue-i18n": "^11.2.2", "vue-pick-colors": "^1.8.0", "vue-router": "^4.6.3" }, @@ -74,21 +71,21 @@ "@lezer/generator": "^1.8.0", "@types/node": "^24.10.1", "@vitejs/plugin-vue": "^6.0.2", - "@wailsio/runtime": "*", + "@wailsio/runtime": "latest", "cross-env": "^10.1.0", "eslint": "^9.39.1", - "eslint-plugin-vue": "^10.6.0", + "eslint-plugin-vue": "^10.6.2", "globals": "^16.5.0", - "happy-dom": "^20.0.10", + "happy-dom": "^20.0.11", "typescript": "^5.9.3", - "typescript-eslint": "^8.47.0", + "typescript-eslint": "^8.48.0", "unplugin-vue-components": "^30.0.0", "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", "vitepress": "^2.0.0-alpha.12", - "vitest": "^4.0.13", + "vitest": "^4.0.14", "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.1.4" + "vue-tsc": "^3.1.5" } }, "node_modules/@antfu/install-pkg": { @@ -145,7 +142,6 @@ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.28.5" }, @@ -219,7 +215,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz", "integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", @@ -268,7 +263,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz", "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", @@ -295,7 +289,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/lang-html/-/lang-html-6.4.11.tgz", "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", @@ -323,7 +316,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz", "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", @@ -536,7 +528,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -613,7 +604,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -623,7 +613,6 @@ "resolved": "https://registry.npmmirror.com/@codemirror/view/-/view-6.38.8.tgz", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1351,13 +1340,13 @@ } }, "node_modules/@intlify/core-base": { - "version": "11.2.1", - "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.1.tgz", - "integrity": "sha512-2V1A4yaN9ElAnQ6ih3HHEc+jZ+sHV6BlQHjCsnIVlOotL5NCUgJElIxgUFiJs6zV4puoAq3hHuQIfWNp+J+8yQ==", + "version": "11.2.2", + "resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.2.2.tgz", + "integrity": "sha512-0mCTBOLKIqFUP3BzwuFW23hYEl9g/wby6uY//AC5hTgQfTsM2srCYF2/hYGp+a5DZ/HIFIgKkLJMzXTt30r0JQ==", "license": "MIT", "dependencies": { - "@intlify/message-compiler": "11.2.1", - "@intlify/shared": "11.2.1" + "@intlify/message-compiler": "11.2.2", + "@intlify/shared": "11.2.2" }, "engines": { "node": ">= 16" @@ -1367,12 +1356,12 @@ } }, "node_modules/@intlify/message-compiler": { - "version": "11.2.1", - "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.1.tgz", - "integrity": "sha512-J2454D3Agg3Kvgaj14gxTleJU8/H06Sisz7C2BwiHF0/i5Soyfb5ySpwn8GCL6yscDbOGj6xM+lUe6gO6BFQyg==", + "version": "11.2.2", + "resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.2.2.tgz", + "integrity": "sha512-XS2p8Ff5JxWsKhgfld4/MRQzZRQ85drMMPhb7Co6Be4ZOgqJX1DzcZt0IFgGTycgqL8rkYNwgnD443Q+TapOoA==", "license": "MIT", "dependencies": { - "@intlify/shared": "11.2.1", + "@intlify/shared": "11.2.2", "source-map-js": "^1.0.2" }, "engines": { @@ -1383,9 +1372,9 @@ } }, "node_modules/@intlify/shared": { - "version": "11.2.1", - "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.1.tgz", - "integrity": "sha512-O67LZM4dbfr70WCsZLW+g+pIXdgQ66laLVd/FicW7iYgP/RuH0X1FDGSh+Hr9Gou/8TeldUE6KmTGdLwX2ufIA==", + "version": "11.2.2", + "resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.2.2.tgz", + "integrity": "sha512-OtCmyFpSXxNu/oET/aN6HtPCbZ01btXVd0f3w00YsHOb13Kverk1jzA2k47pAekM55qbUw421fvPF1yxZ+gicw==", "license": "MIT", "engines": { "node": ">= 16" @@ -1447,8 +1436,7 @@ "version": "1.3.0", "resolved": "https://registry.npmmirror.com/@lezer/common/-/common-1.3.0.tgz", "integrity": "sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@lezer/cpp": { "version": "1.1.3", @@ -1502,7 +1490,6 @@ "resolved": "https://registry.npmmirror.com/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -1534,7 +1521,6 @@ "resolved": "https://registry.npmmirror.com/@lezer/javascript/-/javascript-1.5.1.tgz", "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", @@ -1563,11 +1549,10 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.3", - "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.3.tgz", - "integrity": "sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==", + "version": "1.4.4", + "resolved": "https://registry.npmmirror.com/@lezer/lr/-/lr-1.4.4.tgz", + "integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -1654,89 +1639,6 @@ "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", @@ -1746,42 +1648,50 @@ "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", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, + "node_modules/@nuxt/kit": { + "version": "3.17.4", + "resolved": "https://registry.npmmirror.com/@nuxt/kit/-/kit-3.17.4.tgz", + "integrity": "sha512-l+hY8sy2XFfg3PigZj+PTu6+KIJzmbACTRimn1ew/gtCz+F38f6KTF4sMRTN5CUxiB8TRENgEonASmkAWfpO9Q==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "c12": "^3.0.4", + "consola": "^3.4.2", + "defu": "^6.1.4", + "destr": "^2.0.5", + "errx": "^0.1.0", + "exsolve": "^1.0.5", + "ignore": "^7.0.4", + "jiti": "^2.4.2", + "klona": "^2.0.6", + "knitwork": "^1.2.0", + "mlly": "^1.7.4", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "pkg-types": "^2.1.0", + "scule": "^1.3.0", + "semver": "^7.7.2", + "std-env": "^3.9.0", + "tinyglobby": "^0.2.13", + "ufo": "^1.6.1", + "unctx": "^2.4.1", + "unimport": "^5.0.1", + "untyped": "^2.0.0" }, "engines": { - "node": ">= 8" + "node": ">=18.12.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, + "node_modules/@nuxt/kit/node_modules/ignore": { + "version": "7.0.4", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.4.tgz", + "integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==", "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" + "node": ">= 4" } }, "node_modules/@parcel/watcher": { @@ -2923,16 +2833,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "license": "MIT" + }, "node_modules/@types/linkify-it": { "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", @@ -2953,6 +2871,7 @@ "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": { @@ -2961,7 +2880,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2995,17 +2913,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz", - "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", + "integrity": "sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/type-utils": "8.47.0", - "@typescript-eslint/utils": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/type-utils": "8.48.0", + "@typescript-eslint/utils": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -3019,7 +2937,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.47.0", + "@typescript-eslint/parser": "^8.48.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -3035,17 +2953,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.47.0.tgz", - "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.48.0.tgz", + "integrity": "sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4" }, "engines": { @@ -3061,14 +2978,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.47.0.tgz", - "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.48.0.tgz", + "integrity": "sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.47.0", - "@typescript-eslint/types": "^8.47.0", + "@typescript-eslint/tsconfig-utils": "^8.48.0", + "@typescript-eslint/types": "^8.48.0", "debug": "^4.3.4" }, "engines": { @@ -3083,14 +3000,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz", - "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.48.0.tgz", + "integrity": "sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0" + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3101,9 +3018,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz", - "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.0.tgz", + "integrity": "sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==", "dev": true, "license": "MIT", "engines": { @@ -3118,15 +3035,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz", - "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.48.0.tgz", + "integrity": "sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -3143,9 +3060,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.47.0.tgz", - "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.48.0.tgz", + "integrity": "sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==", "dev": true, "license": "MIT", "engines": { @@ -3157,21 +3074,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz", - "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.0.tgz", + "integrity": "sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.47.0", - "@typescript-eslint/tsconfig-utils": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/visitor-keys": "8.47.0", + "@typescript-eslint/project-service": "8.48.0", + "@typescript-eslint/tsconfig-utils": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/visitor-keys": "8.48.0", "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", + "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "engines": { @@ -3212,16 +3128,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.47.0.tgz", - "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.48.0.tgz", + "integrity": "sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.47.0", - "@typescript-eslint/types": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0" + "@typescript-eslint/scope-manager": "8.48.0", + "@typescript-eslint/types": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3236,13 +3152,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz", - "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.0.tgz", + "integrity": "sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.47.0", + "@typescript-eslint/types": "8.48.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -3278,16 +3194,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.13.tgz", - "integrity": "sha512-zYtcnNIBm6yS7Gpr7nFTmq8ncowlMdOJkWLqYvhr/zweY6tFbDkDi8BPPOeHxEtK1rSI69H7Fd4+1sqvEGli6w==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/expect/-/expect-4.0.14.tgz", + "integrity": "sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.13", - "@vitest/utils": "4.0.13", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" }, @@ -3296,13 +3212,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.13.tgz", - "integrity": "sha512-eNCwzrI5djoauklwP1fuslHBjrbR8rqIVbvNlAnkq1OTa6XT+lX68mrtPirNM9TnR69XUPt4puBCx2Wexseylg==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/mocker/-/mocker-4.0.14.tgz", + "integrity": "sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.13", + "@vitest/spy": "4.0.14", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3333,9 +3249,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.13.tgz", - "integrity": "sha512-ooqfze8URWbI2ozOeLDMh8YZxWDpGXoeY3VOgcDnsUxN0jPyPWSUvjPQWqDGCBks+opWlN1E4oP1UYl3C/2EQA==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-4.0.14.tgz", + "integrity": "sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3346,13 +3262,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.13.tgz", - "integrity": "sha512-9IKlAru58wcVaWy7hz6qWPb2QzJTKt+IOVKjAx5vb5rzEFPTL6H4/R9BMvjZ2ppkxKgTrFONEJFtzvnyEpiT+A==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/runner/-/runner-4.0.14.tgz", + "integrity": "sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.13", + "@vitest/utils": "4.0.14", "pathe": "^2.0.3" }, "funding": { @@ -3360,13 +3276,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.13.tgz", - "integrity": "sha512-hb7Usvyika1huG6G6l191qu1urNPsq1iFc2hmdzQY3F5/rTgqQnwwplyf8zoYHkpt7H6rw5UfIw6i/3qf9oSxQ==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-4.0.14.tgz", + "integrity": "sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.13", + "@vitest/pretty-format": "4.0.14", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3375,9 +3291,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.13.tgz", - "integrity": "sha512-hSu+m4se0lDV5yVIcNWqjuncrmBgwaXa2utFLIrBkQCQkt+pSwyZTPFQAZiiF/63j8jYa8uAeUZ3RSfcdWaYWw==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/spy/-/spy-4.0.14.tgz", + "integrity": "sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==", "dev": true, "license": "MIT", "funding": { @@ -3385,13 +3301,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.13.tgz", - "integrity": "sha512-ydozWyQ4LZuu8rLp47xFUWis5VOKMdHjXCWhs1LuJsTNKww+pTHQNK4e0assIB9K80TxFyskENL6vCu3j34EYA==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/@vitest/utils/-/utils-4.0.14.tgz", + "integrity": "sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.13", + "@vitest/pretty-format": "4.0.14", "tinyrainbow": "^3.0.3" }, "funding": { @@ -3428,39 +3344,39 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.24.tgz", - "integrity": "sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.25.tgz", + "integrity": "sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", - "@vue/shared": "3.5.24", + "@vue/shared": "3.5.25", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.24.tgz", - "integrity": "sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.25.tgz", + "integrity": "sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.24", - "@vue/shared": "3.5.24" + "@vue/compiler-core": "3.5.25", + "@vue/shared": "3.5.25" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.24.tgz", - "integrity": "sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.25.tgz", + "integrity": "sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==", "license": "MIT", "dependencies": { "@babel/parser": "^7.28.5", - "@vue/compiler-core": "3.5.24", - "@vue/compiler-dom": "3.5.24", - "@vue/compiler-ssr": "3.5.24", - "@vue/shared": "3.5.24", + "@vue/compiler-core": "3.5.25", + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -3468,13 +3384,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.24.tgz", - "integrity": "sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.25.tgz", + "integrity": "sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.24", - "@vue/shared": "3.5.24" + "@vue/compiler-dom": "3.5.25", + "@vue/shared": "3.5.25" } }, "node_modules/@vue/devtools-api": { @@ -3511,9 +3427,9 @@ } }, "node_modules/@vue/language-core": { - "version": "3.1.4", - "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.4.tgz", - "integrity": "sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==", + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.1.5.tgz", + "integrity": "sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==", "dev": true, "license": "MIT", "dependencies": { @@ -3548,53 +3464,53 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.24.tgz", - "integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.25.tgz", + "integrity": "sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.24" + "@vue/shared": "3.5.25" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.24.tgz", - "integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.25.tgz", + "integrity": "sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.24", - "@vue/shared": "3.5.24" + "@vue/reactivity": "3.5.25", + "@vue/shared": "3.5.25" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz", - "integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.25.tgz", + "integrity": "sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.24", - "@vue/runtime-core": "3.5.24", - "@vue/shared": "3.5.24", + "@vue/reactivity": "3.5.25", + "@vue/runtime-core": "3.5.25", + "@vue/shared": "3.5.25", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.24.tgz", - "integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.25.tgz", + "integrity": "sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.24", - "@vue/shared": "3.5.24" + "@vue/compiler-ssr": "3.5.25", + "@vue/shared": "3.5.25" }, "peerDependencies": { - "vue": "3.5.24" + "vue": "3.5.25" } }, "node_modules/@vue/shared": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.24.tgz", - "integrity": "sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.25.tgz", + "integrity": "sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==", "license": "MIT" }, "node_modules/@vueuse/core": { @@ -3735,7 +3651,6 @@ "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3771,9 +3686,9 @@ } }, "node_modules/alien-signals": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.0.tgz", - "integrity": "sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==", + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.1.tgz", + "integrity": "sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==", "dev": true, "license": "MIT" }, @@ -3797,6 +3712,7 @@ "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": { @@ -3924,8 +3840,8 @@ "version": "3.0.3", "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -4129,6 +4045,7 @@ "integrity": "sha512-t5FaZTYbbCtvxuZq9xxIruYydrAGsJ+8UdP0pZzMiK2xl/gNiSOy0OxhLzHUEEb0m1QXYqfzfvyIFEmz/g9lqg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", @@ -4277,7 +4194,6 @@ "resolved": "https://registry.npmmirror.com/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -4334,6 +4250,7 @@ "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "consola": "^3.2.3" } @@ -4445,6 +4362,7 @@ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^14.18.0 || >=16.10.0" } @@ -4627,9 +4545,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/cytoscape": { @@ -4637,7 +4555,6 @@ "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -5047,7 +4964,6 @@ "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -5249,7 +5165,8 @@ "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/detect-libc": { "version": "1.0.3", @@ -5325,6 +5242,7 @@ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -5387,7 +5305,8 @@ "resolved": "https://registry.npmmirror.com/errx/-/errx-0.1.0.tgz", "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/es-define-property": { "version": "1.0.1", @@ -5489,7 +5408,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5545,9 +5463,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.6.0", - "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.6.0.tgz", - "integrity": "sha512-TsoFluWxOpsJlE/l2jJygLQLWBPJ3Qdkesv7tBIunICbTcG0dS1/NBw/Ol4tJw5kHWlAVds4lUmC29/vlPUcEQ==", + "version": "10.6.2", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-10.6.2.tgz", + "integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==", "dev": true, "license": "MIT", "dependencies": { @@ -5720,36 +5638,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5764,16 +5652,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5791,8 +5669,8 @@ "version": "7.1.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -5844,7 +5722,6 @@ "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tabbable": "^6.3.0" } @@ -5935,6 +5812,7 @@ "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", @@ -6006,9 +5884,9 @@ "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==", + "version": "20.0.11", + "resolved": "https://registry.npmmirror.com/happy-dom/-/happy-dom-20.0.11.tgz", + "integrity": "sha512-QsCdAUHAmiDeKeaNojb1OHOPF7NjcWPBR7obdu3NwH2a/oyQaLg5d0aaCy/9My6CdPChYF07dvz5chaXBGaD4g==", "dev": true, "license": "MIT", "dependencies": { @@ -6165,15 +6043,6 @@ "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", @@ -6423,8 +6292,8 @@ "version": "7.0.0", "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.12.0" } @@ -6517,6 +6386,7 @@ "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", "license": "MIT", "optional": true, + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -6526,7 +6396,8 @@ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/js-yaml": { "version": "4.1.1", @@ -6599,6 +6470,7 @@ "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 8" } @@ -6608,7 +6480,8 @@ "resolved": "https://registry.npmmirror.com/knitwork/-/knitwork-1.2.0.tgz", "integrity": "sha512-xYSH7AvuQ6nXkq42x0v5S8/Iry+cfulBz/DJQzhIyESdLD7425jXsPy4vn5cCXU+HhRN2kVw51Vd1K6/By4BQg==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/kolorist": { "version": "1.8.0", @@ -6673,15 +6546,6 @@ "integrity": "sha512-rsfEA3KUoRBB1rNttxAqq23/vzHtsN9EGegzVV3FWom1sWNyUZDhqHjb9fP/UJSFKisteVrSpI2NGf+AdXhYMQ==", "license": "MIT" }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/local-pkg": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", @@ -6750,28 +6614,10 @@ "dev": true, "license": "MIT" }, - "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", - "peer": true, - "dependencies": { - "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/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "version": "17.0.1", + "resolved": "https://registry.npmmirror.com/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -6803,9 +6649,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "version": "13.2.0", + "resolved": "https://registry.npmmirror.com/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", "dev": true, "license": "MIT", "dependencies": { @@ -6824,22 +6670,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/mermaid": { "version": "11.12.1", "resolved": "https://registry.npmmirror.com/mermaid/-/mermaid-11.12.1.tgz", @@ -6868,6 +6698,18 @@ "uuid": "^11.1.0" } }, + "node_modules/mermaid/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" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -6966,8 +6808,8 @@ "version": "4.0.8", "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -7116,7 +6958,8 @@ "resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.6.tgz", "integrity": "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/node-stdlib-browser": { "version": "1.3.1", @@ -7164,2383 +7007,6 @@ "dev": true, "license": "MIT" }, - "node_modules/npm": { - "version": "11.6.3", - "resolved": "https://registry.npmmirror.com/npm/-/npm-11.6.3.tgz", - "integrity": "sha512-QIWnYxYuDjrNGaTp0jrTqgl45QHM+UfdcjPBKmia4LsBkHY8TvEjZpkAVrNO7EOJA//tOP3k+9cioXwqdAfukg==", - "bundleDependencies": [ - "@isaacs/string-locale-compare", - "@npmcli/arborist", - "@npmcli/config", - "@npmcli/fs", - "@npmcli/map-workspaces", - "@npmcli/metavuln-calculator", - "@npmcli/package-json", - "@npmcli/promise-spawn", - "@npmcli/redact", - "@npmcli/run-script", - "@sigstore/tuf", - "abbrev", - "archy", - "cacache", - "chalk", - "ci-info", - "cli-columns", - "fastest-levenshtein", - "fs-minipass", - "glob", - "graceful-fs", - "hosted-git-info", - "ini", - "init-package-json", - "is-cidr", - "json-parse-even-better-errors", - "libnpmaccess", - "libnpmdiff", - "libnpmexec", - "libnpmfund", - "libnpmorg", - "libnpmpack", - "libnpmpublish", - "libnpmsearch", - "libnpmteam", - "libnpmversion", - "make-fetch-happen", - "minimatch", - "minipass", - "minipass-pipeline", - "ms", - "node-gyp", - "nopt", - "npm-audit-report", - "npm-install-checks", - "npm-package-arg", - "npm-pick-manifest", - "npm-profile", - "npm-registry-fetch", - "npm-user-validate", - "p-map", - "pacote", - "parse-conflict-json", - "proc-log", - "qrcode-terminal", - "read", - "semver", - "spdx-expression-parse", - "ssri", - "supports-color", - "tar", - "text-table", - "tiny-relative-date", - "treeverse", - "validate-npm-package-name", - "which" - ], - "license": "Artistic-2.0", - "workspaces": [ - "docs", - "smoke-tests", - "mock-globals", - "mock-registry", - "workspaces/*" - ], - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^9.1.7", - "@npmcli/config": "^10.4.3", - "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^5.0.1", - "@npmcli/metavuln-calculator": "^9.0.3", - "@npmcli/package-json": "^7.0.2", - "@npmcli/promise-spawn": "^9.0.1", - "@npmcli/redact": "^4.0.0", - "@npmcli/run-script": "^10.0.3", - "@sigstore/tuf": "^4.0.0", - "abbrev": "^4.0.0", - "archy": "~1.0.0", - "cacache": "^20.0.2", - "chalk": "^5.6.2", - "ci-info": "^4.3.1", - "cli-columns": "^4.0.0", - "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.3", - "glob": "^11.0.3", - "graceful-fs": "^4.2.11", - "hosted-git-info": "^9.0.2", - "ini": "^6.0.0", - "init-package-json": "^8.2.3", - "is-cidr": "^6.0.1", - "json-parse-even-better-errors": "^5.0.0", - "libnpmaccess": "^10.0.3", - "libnpmdiff": "^8.0.10", - "libnpmexec": "^10.1.9", - "libnpmfund": "^7.0.10", - "libnpmorg": "^8.0.1", - "libnpmpack": "^9.0.10", - "libnpmpublish": "^11.1.3", - "libnpmsearch": "^9.0.1", - "libnpmteam": "^8.0.2", - "libnpmversion": "^8.0.3", - "make-fetch-happen": "^15.0.3", - "minimatch": "^10.1.1", - "minipass": "^7.1.1", - "minipass-pipeline": "^1.2.4", - "ms": "^2.1.2", - "node-gyp": "^12.1.0", - "nopt": "^9.0.0", - "npm-audit-report": "^6.0.0", - "npm-install-checks": "^8.0.0", - "npm-package-arg": "^13.0.2", - "npm-pick-manifest": "^11.0.3", - "npm-profile": "^12.0.1", - "npm-registry-fetch": "^19.1.1", - "npm-user-validate": "^3.0.0", - "p-map": "^7.0.3", - "pacote": "^21.0.4", - "parse-conflict-json": "^5.0.1", - "proc-log": "^6.0.0", - "qrcode-terminal": "^0.12.0", - "read": "^4.1.0", - "semver": "^7.7.3", - "spdx-expression-parse": "^4.0.0", - "ssri": "^13.0.0", - "supports-color": "^10.2.2", - "tar": "^7.5.2", - "text-table": "~0.2.0", - "tiny-relative-date": "^2.0.2", - "treeverse": "^3.0.0", - "validate-npm-package-name": "^7.0.0", - "which": "^6.0.0" - }, - "bin": { - "npm": "bin/npm-cli.js", - "npx": "bin/npx-cli.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/npm/node_modules/@isaacs/string-locale-compare": { - "version": "1.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/@npmcli/agent": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^11.2.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "9.1.7", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/map-workspaces": "^5.0.0", - "@npmcli/metavuln-calculator": "^9.0.2", - "@npmcli/name-from-folder": "^4.0.0", - "@npmcli/node-gyp": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/query": "^4.0.0", - "@npmcli/redact": "^4.0.0", - "@npmcli/run-script": "^10.0.0", - "bin-links": "^6.0.0", - "cacache": "^20.0.1", - "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^9.0.0", - "json-stringify-nice": "^1.1.4", - "lru-cache": "^11.2.1", - "minimatch": "^10.0.3", - "nopt": "^8.0.0", - "npm-install-checks": "^8.0.0", - "npm-package-arg": "^13.0.0", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "pacote": "^21.0.2", - "parse-conflict-json": "^5.0.1", - "proc-log": "^6.0.0", - "proggy": "^3.0.0", - "promise-all-reject-late": "^1.0.0", - "promise-call-limit": "^3.0.1", - "semver": "^7.3.7", - "ssri": "^13.0.0", - "treeverse": "^3.0.0", - "walk-up-path": "^4.0.0" - }, - "bin": { - "arborist": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/arborist/node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.4.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/map-workspaces": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "ci-info": "^4.0.0", - "ini": "^6.0.0", - "nopt": "^8.1.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config/node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/config/node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/fs": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/git": { - "version": "7.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/promise-spawn": "^9.0.0", - "ini": "^6.0.0", - "lru-cache": "^11.2.1", - "npm-pick-manifest": "^11.0.1", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^5.0.0", - "npm-normalize-package-bin": "^5.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "5.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/name-from-folder": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "glob": "^11.0.3", - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "9.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cacache": "^20.0.0", - "json-parse-even-better-errors": "^5.0.0", - "pacote": "^21.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "7.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "glob": "^11.0.3", - "hosted-git-info": "^9.0.0", - "json-parse-even-better-errors": "^5.0.0", - "proc-log": "^6.0.0", - "semver": "^7.5.3", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "9.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@npmcli/redact": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "10.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/node-gyp": "^5.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "node-gyp": "^12.1.0", - "proc-log": "^6.0.0", - "which": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/bundle": { - "version": "4.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/core": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.5.0", - "inBundle": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign": { - "version": "4.0.1", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0", - "make-fetch-happen": "^15.0.2", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "4.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.5.0", - "tuf-js": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "3.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/canonical-json": "2.0.0", - "minimatch": "^9.0.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models/node_modules/minimatch": { - "version": "9.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/abbrev": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/agent-base": { - "version": "7.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/ansi-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/ansi-styles": { - "version": "6.2.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/aproba": { - "version": "2.1.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/archy": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/balanced-match": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/bin-links": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cmd-shim": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "proc-log": "^6.0.0", - "read-cmd-shim": "^6.0.0", - "write-file-atomic": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/binary-extensions": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/brace-expansion": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/npm/node_modules/cacache": { - "version": "20.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^11.0.3", - "lru-cache": "^11.1.0", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^13.0.0", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/chalk": { - "version": "5.6.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/npm/node_modules/chownr": { - "version": "3.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/ci-info": { - "version": "4.3.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/cidr-regex": { - "version": "5.0.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "ip-regex": "5.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/npm/node_modules/cli-columns": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/npm/node_modules/cmd-shim": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/color-convert": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/npm/node_modules/color-name": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/common-ancestor-path": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cross-spawn": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cross-spawn/node_modules/isexe": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/cross-spawn/node_modules/which": { - "version": "2.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/cssesc": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/debug": { - "version": "4.4.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/diff": { - "version": "8.0.2", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/npm/node_modules/eastasianwidth": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/emoji-regex": { - "version": "8.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/encoding": { - "version": "0.1.13", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/npm/node_modules/env-paths": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/err-code": { - "version": "2.0.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/exponential-backoff": { - "version": "3.1.3", - "inBundle": true, - "license": "Apache-2.0" - }, - "node_modules/npm/node_modules/fastest-levenshtein": { - "version": "1.0.16", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/npm/node_modules/foreground-child": { - "version": "3.3.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/glob": { - "version": "11.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/graceful-fs": { - "version": "4.2.11", - "inBundle": true, - "license": "ISC" - }, - "node_modules/npm/node_modules/hosted-git-info": { - "version": "9.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^11.1.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/http-cache-semantics": { - "version": "4.2.0", - "inBundle": true, - "license": "BSD-2-Clause" - }, - "node_modules/npm/node_modules/http-proxy-agent": { - "version": "7.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/https-proxy-agent": { - "version": "7.0.6", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/iconv-lite": { - "version": "0.6.3", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm/node_modules/ignore-walk": { - "version": "8.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minimatch": "^10.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/imurmurhash": { - "version": "0.1.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/npm/node_modules/ini": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/init-package-json": { - "version": "8.2.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^7.0.0", - "npm-package-arg": "^13.0.0", - "promzard": "^2.0.0", - "read": "^4.0.0", - "semver": "^7.7.2", - "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/ip-address": { - "version": "10.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/npm/node_modules/ip-regex": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/is-cidr": { - "version": "6.0.1", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "cidr-regex": "5.0.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/npm/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/jackspeak": { - "version": "4.1.1", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/json-stringify-nice": { - "version": "1.1.4", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/jsonparse": { - "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff": { - "version": "6.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/just-diff-apply": { - "version": "5.5.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/libnpmaccess": { - "version": "10.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-package-arg": "^13.0.0", - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff": { - "version": "8.0.10", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.7", - "@npmcli/installed-package-contents": "^3.0.0", - "binary-extensions": "^3.0.0", - "diff": "^8.0.2", - "minimatch": "^10.0.3", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2", - "tar": "^7.5.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff/node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", - "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" - }, - "bin": { - "installed-package-contents": "bin/index.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff/node_modules/npm-bundled": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", - "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmdiff/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", - "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/libnpmexec": { - "version": "10.1.9", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.7", - "@npmcli/package-json": "^7.0.0", - "@npmcli/run-script": "^10.0.0", - "ci-info": "^4.0.0", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "read": "^4.0.0", - "semver": "^7.3.7", - "signal-exit": "^4.1.0", - "walk-up-path": "^4.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmfund": { - "version": "7.0.10", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.7" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmorg": { - "version": "8.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpack": { - "version": "9.0.10", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/arborist": "^9.1.7", - "@npmcli/run-script": "^10.0.0", - "npm-package-arg": "^13.0.0", - "pacote": "^21.0.2" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmpublish": { - "version": "11.1.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/package-json": "^7.0.0", - "ci-info": "^4.0.0", - "npm-package-arg": "^13.0.0", - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.7", - "sigstore": "^4.0.0", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmsearch": { - "version": "9.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmteam": { - "version": "8.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "aproba": "^2.0.0", - "npm-registry-fetch": "^19.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/libnpmversion": { - "version": "8.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/run-script": "^10.0.0", - "json-parse-even-better-errors": "^5.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/lru-cache": { - "version": "11.2.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/make-fetch-happen": { - "version": "15.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^4.0.0", - "cacache": "^20.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "ssri": "^13.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/minimatch": { - "version": "10.1.1", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/minipass": { - "version": "7.1.2", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-collect": { - "version": "2.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/npm/node_modules/minipass-fetch": { - "version": "5.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/minipass-flush": { - "version": "1.0.5", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline": { - "version": "1.2.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized": { - "version": "1.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/minizlib": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/ms": { - "version": "2.1.3", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/mute-stream": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/negotiator": { - "version": "1.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/npm/node_modules/node-gyp": { - "version": "12.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^15.0.0", - "nopt": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "tar": "^7.5.2", - "tinyglobby": "^0.2.12", - "which": "^6.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/nopt": { - "version": "9.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^4.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-audit-report": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-bundled": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-normalize-package-bin": "^5.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-install-checks": { - "version": "8.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-package-arg": { - "version": "13.0.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "hosted-git-info": "^9.0.0", - "proc-log": "^6.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^7.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-packlist": { - "version": "10.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "ignore-walk": "^8.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "11.0.3", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-install-checks": "^8.0.0", - "npm-normalize-package-bin": "^5.0.0", - "npm-package-arg": "^13.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-profile": { - "version": "12.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "19.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/redact": "^4.0.0", - "jsonparse": "^1.3.1", - "make-fetch-happen": "^15.0.0", - "minipass": "^7.0.2", - "minipass-fetch": "^5.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^13.0.0", - "proc-log": "^6.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/npm-user-validate": { - "version": "3.0.0", - "inBundle": true, - "license": "BSD-2-Clause", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/p-map": { - "version": "7.0.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/package-json-from-dist": { - "version": "1.0.1", - "inBundle": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/npm/node_modules/pacote": { - "version": "21.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/git": "^7.0.0", - "@npmcli/installed-package-contents": "^4.0.0", - "@npmcli/package-json": "^7.0.0", - "@npmcli/promise-spawn": "^9.0.0", - "@npmcli/run-script": "^10.0.0", - "cacache": "^20.0.0", - "fs-minipass": "^3.0.0", - "minipass": "^7.0.2", - "npm-package-arg": "^13.0.0", - "npm-packlist": "^10.0.1", - "npm-pick-manifest": "^11.0.1", - "npm-registry-fetch": "^19.0.0", - "proc-log": "^6.0.0", - "promise-retry": "^2.0.1", - "sigstore": "^4.0.0", - "ssri": "^13.0.0", - "tar": "^7.4.3" - }, - "bin": { - "pacote": "bin/index.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/parse-conflict-json": { - "version": "5.0.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "json-parse-even-better-errors": "^5.0.0", - "just-diff": "^6.0.0", - "just-diff-apply": "^5.2.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/path-key": { - "version": "3.1.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/path-scurry": { - "version": "2.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm/node_modules/proc-log": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/proggy": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/promise-all-reject-late": { - "version": "1.0.1", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-call-limit": { - "version": "3.0.2", - "inBundle": true, - "license": "ISC", - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/promise-retry": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/promzard": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "read": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/qrcode-terminal": { - "version": "0.12.0", - "inBundle": true, - "bin": { - "qrcode-terminal": "bin/qrcode-terminal.js" - } - }, - "node_modules/npm/node_modules/read": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "mute-stream": "^2.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/read-cmd-shim": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/retry": { - "version": "0.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm/node_modules/safer-buffer": { - "version": "2.1.2", - "inBundle": true, - "license": "MIT", - "optional": true - }, - "node_modules/npm/node_modules/semver": { - "version": "7.7.3", - "inBundle": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/npm/node_modules/shebang-command": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/shebang-regex": { - "version": "3.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/signal-exit": { - "version": "4.1.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/sigstore": { - "version": "4.0.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^4.0.0", - "@sigstore/core": "^3.0.0", - "@sigstore/protobuf-specs": "^0.5.0", - "@sigstore/sign": "^4.0.0", - "@sigstore/tuf": "^4.0.0", - "@sigstore/verify": "^3.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/smart-buffer": { - "version": "4.2.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks": { - "version": "2.8.7", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/npm/node_modules/socks-proxy-agent": { - "version": "8.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/npm/node_modules/spdx-correct": { - "version": "3.2.0", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.5.0", - "inBundle": true, - "license": "CC-BY-3.0" - }, - "node_modules/npm/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.22", - "inBundle": true, - "license": "CC0-1.0" - }, - "node_modules/npm/node_modules/ssri": { - "version": "13.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/string-width": { - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi": { - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/npm/node_modules/supports-color": { - "version": "10.2.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/npm/node_modules/tar": { - "version": "7.5.2", - "inBundle": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/text-table": { - "version": "0.2.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tiny-relative-date": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/tinyglobby": { - "version": "0.2.15", - "inBundle": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "inBundle": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/npm/node_modules/treeverse": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@tufjs/models": "4.0.0", - "debug": "^4.4.1", - "make-fetch-happen": "^15.0.0" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/unique-filename": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "5.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/util-deprecate": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/validate-npm-package-license": { - "version": "3.0.4", - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { - "version": "3.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/walk-up-path": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/npm/node_modules/which": { - "version": "6.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/wrap-ansi": { - "version": "8.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "9.2.2", - "inBundle": true, - "license": "MIT" - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { - "version": "5.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/npm/node_modules/write-file-atomic": { - "version": "7.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^20.17.0 || >=22.9.0" - } - }, - "node_modules/npm/node_modules/yallist": { - "version": "4.0.0", - "inBundle": true, - "license": "ISC" - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", @@ -9560,6 +7026,7 @@ "integrity": "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", @@ -9635,12 +7102,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/oniguruma-parser": { "version": "0.12.1", @@ -9882,8 +7361,8 @@ "version": "2.3.1", "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, "license": "MIT", + "optional": true, "engines": { "node": ">=8.6" }, @@ -9896,7 +7375,6 @@ "resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz", "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", "license": "MIT", - "peer": true, "dependencies": { "@vue/devtools-api": "^7.7.7" }, @@ -10041,11 +7519,10 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.7.2", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.7.2.tgz", + "integrity": "sha512-n3HV2J6QhItCXndGa3oMWvWFAgN1ibnS7R9mt6iokScBOC0Ul9/iZORmU2IWUMcyAQaMPjTlY3uT34TqocUxMA==", "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10116,15 +7593,6 @@ "node": ">=6" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", @@ -10166,27 +7634,6 @@ "node": ">=0.4.x" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmmirror.com/randombytes/-/randombytes-2.1.0.tgz", @@ -10214,6 +7661,7 @@ "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" @@ -10311,17 +7759,6 @@ "node": ">=4" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", @@ -10351,7 +7788,6 @@ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -10398,30 +7834,6 @@ "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", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz", @@ -10478,7 +7890,6 @@ "resolved": "https://registry.npmmirror.com/sass/-/sass-1.94.2.tgz", "integrity": "sha512-N+7WK20/wOr7CzA2snJcUSSNTCzeCGUTFY3OgeQP3mZ1aj9NMQ0mSTXwlrnd89j33zzQJGqIN52GIOmYrfq46A==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -10499,7 +7910,8 @@ "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/semver": { "version": "7.7.2", @@ -10818,6 +8230,7 @@ "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "js-tokens": "^9.0.1" }, @@ -10950,7 +8363,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10987,8 +8399,8 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -11070,7 +8482,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11080,16 +8491,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.47.0", - "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.47.0.tgz", - "integrity": "sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==", + "version": "8.48.0", + "resolved": "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.48.0.tgz", + "integrity": "sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.47.0", - "@typescript-eslint/parser": "8.47.0", - "@typescript-eslint/typescript-estree": "8.47.0", - "@typescript-eslint/utils": "8.47.0" + "@typescript-eslint/eslint-plugin": "8.48.0", + "@typescript-eslint/parser": "8.48.0", + "@typescript-eslint/typescript-estree": "8.48.0", + "@typescript-eslint/utils": "8.48.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -11103,12 +8514,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/ufo": { "version": "1.6.1", "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", @@ -11121,6 +8526,7 @@ "integrity": "sha512-AbaYw0Nm4mK4qjhns67C+kgxR2YWiwlDBPzxrN8h8C6VtAdCgditAY5Dezu3IJy4XVqAnbrXt9oQJvsn3fyozg==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "acorn": "^8.14.0", "estree-walker": "^3.0.3", @@ -11134,6 +8540,7 @@ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -11151,6 +8558,7 @@ "integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "acorn": "^8.14.1", "escape-string-regexp": "^5.0.0", @@ -11177,6 +8585,7 @@ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -11190,6 +8599,7 @@ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@types/estree": "^1.0.0" } @@ -11200,6 +8610,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -11302,6 +8713,7 @@ "integrity": "sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "pathe": "^2.0.2", "picomatch": "^4.0.2" @@ -11319,6 +8731,7 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=12" }, @@ -11411,6 +8824,7 @@ "integrity": "sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "citty": "^0.1.6", "defu": "^6.1.4", @@ -11523,7 +8937,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -11634,7 +9047,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -11732,23 +9144,23 @@ "license": "MIT" }, "node_modules/vitest": { - "version": "4.0.13", - "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.13.tgz", - "integrity": "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ==", + "version": "4.0.14", + "resolved": "https://registry.npmmirror.com/vitest/-/vitest-4.0.14.tgz", + "integrity": "sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.13", - "@vitest/mocker": "4.0.13", - "@vitest/pretty-format": "4.0.13", - "@vitest/runner": "4.0.13", - "@vitest/snapshot": "4.0.13", - "@vitest/spy": "4.0.13", - "@vitest/utils": "4.0.13", - "debug": "^4.4.3", + "@vitest/expect": "4.0.14", + "@vitest/mocker": "4.0.14", + "@vitest/pretty-format": "4.0.14", + "@vitest/runner": "4.0.14", + "@vitest/snapshot": "4.0.14", + "@vitest/spy": "4.0.14", + "@vitest/utils": "4.0.14", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", @@ -11771,12 +9183,11 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", - "@types/debug": "^4.1.12", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.13", - "@vitest/browser-preview": "4.0.13", - "@vitest/browser-webdriverio": "4.0.13", - "@vitest/ui": "4.0.13", + "@vitest/browser-playwright": "4.0.14", + "@vitest/browser-preview": "4.0.14", + "@vitest/browser-webdriverio": "4.0.14", + "@vitest/ui": "4.0.14", "happy-dom": "*", "jsdom": "*" }, @@ -11787,9 +9198,6 @@ "@opentelemetry/api": { "optional": true }, - "@types/debug": { - "optional": true - }, "@types/node": { "optional": true }, @@ -11884,17 +9292,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.24", - "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.24.tgz", - "integrity": "sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==", + "version": "3.5.25", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.25.tgz", + "integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==", "license": "MIT", - "peer": true, "dependencies": { - "@vue/compiler-dom": "3.5.24", - "@vue/compiler-sfc": "3.5.24", - "@vue/runtime-dom": "3.5.24", - "@vue/server-renderer": "3.5.24", - "@vue/shared": "3.5.24" + "@vue/compiler-dom": "3.5.25", + "@vue/compiler-sfc": "3.5.25", + "@vue/runtime-dom": "3.5.25", + "@vue/server-renderer": "3.5.25", + "@vue/shared": "3.5.25" }, "peerDependencies": { "typescript": "*" @@ -11911,7 +9318,6 @@ "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", @@ -11931,13 +9337,13 @@ } }, "node_modules/vue-i18n": { - "version": "11.2.1", - "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.1.tgz", - "integrity": "sha512-cc3Wx4eJZac9WMS8mxhfYiCipm9PBQ2Dz15piWYm7DwNcCehaKRgpolEdiqrjjT27T3Wijz3xJ7NeIc8ofIWAA==", + "version": "11.2.2", + "resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.2.2.tgz", + "integrity": "sha512-ULIKZyRluUPRCZmihVgUvpq8hJTtOqnbGZuv4Lz+byEKZq4mU0g92og414l6f/4ju+L5mORsiUuEPYrAuX2NJg==", "license": "MIT", "dependencies": { - "@intlify/core-base": "11.2.1", - "@intlify/shared": "11.2.1", + "@intlify/core-base": "11.2.2", + "@intlify/shared": "11.2.2", "@vue/devtools-api": "^6.5.0" }, "engines": { @@ -11991,14 +9397,14 @@ "license": "MIT" }, "node_modules/vue-tsc": { - "version": "3.1.4", - "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.4.tgz", - "integrity": "sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==", + "version": "3.1.5", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.1.5.tgz", + "integrity": "sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.23", - "@vue/language-core": "3.1.4" + "@vue/language-core": "3.1.5" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/frontend/package.json b/frontend/package.json index a2e8ea0..36658ce 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -53,33 +53,30 @@ "@codemirror/view": "^6.38.8", "@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", + "@lezer/lr": "^1.4.4", "@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", + "@types/katex": "^0.16.7", "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", "java-parser": "^3.0.1", + "katex": "^0.16.25", "linguist-languages": "^9.1.0", - "markdown-it": "^14.1.0", + "marked": "^17.0.1", "mermaid": "^11.12.1", - "npm": "^11.6.3", "php-parser": "^3.2.5", "pinia": "^3.0.4", "pinia-plugin-persistedstate": "^4.7.1", - "prettier": "^3.6.2", + "prettier": "^3.7.2", "sass": "^1.94.2", - "vue": "^3.5.24", - "vue-i18n": "^11.2.1", + "vue": "^3.5.25", + "vue-i18n": "^11.2.2", "vue-pick-colors": "^1.8.0", "vue-router": "^4.6.3" }, @@ -91,18 +88,18 @@ "@wailsio/runtime": "latest", "cross-env": "^10.1.0", "eslint": "^9.39.1", - "eslint-plugin-vue": "^10.6.0", + "eslint-plugin-vue": "^10.6.2", "globals": "^16.5.0", - "happy-dom": "^20.0.10", + "happy-dom": "^20.0.11", "typescript": "^5.9.3", - "typescript-eslint": "^8.47.0", + "typescript-eslint": "^8.48.0", "unplugin-vue-components": "^30.0.0", "vite": "npm:rolldown-vite@latest", "vite-plugin-node-polyfills": "^0.24.0", "vitepress": "^2.0.0-alpha.12", - "vitest": "^4.0.13", + "vitest": "^4.0.14", "vue-eslint-parser": "^10.2.0", - "vue-tsc": "^3.1.4" + "vue-tsc": "^3.1.5" }, "overrides": { "vite": "npm:rolldown-vite@latest" diff --git a/frontend/src/assets/styles/variables.css b/frontend/src/assets/styles/variables.css index eddcea4..83a03d5 100644 --- a/frontend/src/assets/styles/variables.css +++ b/frontend/src/assets/styles/variables.css @@ -49,6 +49,21 @@ --voidraft-loading-glow: 0 0 10px rgba(50, 255, 50, 0.5), 0 0 5px rgba(100, 255, 100, 0.5); --voidraft-loading-done-color: #66ff66; --voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(10, 16, 10, 0.5) 50%); + + /* Markdown 代码块样式 - 暗色主题 */ + --cm-codeblock-bg: rgba(46, 51, 69, 0.8); + --cm-codeblock-radius: 0.4rem; + + + /* Markdown 内联代码样式 */ + --cm-inline-code-bg: oklch(28% 0.02 255); + + /* Markdown 上标/下标样式 */ + --cm-superscript-color: inherit; + --cm-subscript-color: inherit; + + /* Markdown 高亮样式 */ + --cm-highlight-background: rgba(250, 204, 21, 0.35); } /* 亮色主题 */ @@ -96,6 +111,20 @@ --voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2); --voidraft-loading-done-color: #008800; --voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%); + + /* Markdown 代码块样式 - 亮色主题 */ + --cm-codeblock-bg: oklch(92.9% 0.013 255.508); + --cm-codeblock-radius: 0.4rem; + + /* Markdown 内联代码样式 */ + --cm-inline-code-bg: oklch(92.9% 0.013 255.508); + + /* Markdown 上标/下标样式 */ + --cm-superscript-color: inherit; + --cm-subscript-color: inherit; + + /* Markdown 高亮样式 */ + --cm-highlight-background: rgba(253, 224, 71, 0.45); } /* 跟随系统的浅色偏好 */ @@ -144,5 +173,19 @@ --voidraft-loading-glow: 0 0 10px rgba(0, 160, 0, 0.3), 0 0 5px rgba(0, 120, 0, 0.2); --voidraft-loading-done-color: #008800; --voidraft-loading-overlay: linear-gradient(transparent 0%, rgba(220, 240, 220, 0.5) 50%); + + /* Markdown 代码块样式 - 亮色主题 */ + --cm-codeblock-bg: oklch(92.9% 0.013 255.508); + --cm-codeblock-radius: 0.4rem; + + /* Markdown 内联代码样式 */ + --cm-inline-code-bg: oklch(92.9% 0.013 255.508); + + /* Markdown 上标/下标样式 */ + --cm-superscript-color: inherit; + --cm-subscript-color: inherit; + + /* Markdown 高亮样式 */ + --cm-highlight-background: rgba(253, 224, 71, 0.45); } } diff --git a/frontend/src/common/constant/config.ts b/frontend/src/common/constant/config.ts index 3f19326..f34e7a7 100644 --- a/frontend/src/common/constant/config.ts +++ b/frontend/src/common/constant/config.ts @@ -1,43 +1,19 @@ import { AppConfig, - AppearanceConfig, - EditingConfig, - GeneralConfig, + AuthMethod, LanguageType, SystemThemeType, TabType, - UpdatesConfig, - UpdateSourceType, - GitBackupConfig, - AuthMethod + UpdateSourceType } from '@/../bindings/voidraft/internal/models/models'; import {FONT_OPTIONS} from './fonts'; -// 配置键映射和限制的类型定义 -export type GeneralConfigKeyMap = { - readonly [K in keyof GeneralConfig]: string; -}; - -export type EditingConfigKeyMap = { - readonly [K in keyof EditingConfig]: string; -}; - -export type AppearanceConfigKeyMap = { - readonly [K in keyof AppearanceConfig]: string; -}; - -export type UpdatesConfigKeyMap = { - readonly [K in keyof UpdatesConfig]: string; -}; - -export type BackupConfigKeyMap = { - readonly [K in keyof GitBackupConfig]: string; -}; - export type NumberConfigKey = 'fontSize' | 'tabSize' | 'lineHeight'; +export type ConfigSection = 'general' | 'editing' | 'appearance' | 'updates' | 'backup'; -// 配置键映射 -export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = { +// 统一配置键映射(平级展开) +export const CONFIG_KEY_MAP = { + // general alwaysOnTop: 'general.alwaysOnTop', dataPath: 'general.dataPath', enableSystemTray: 'general.enableSystemTray', @@ -47,9 +23,7 @@ export const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = { enableWindowSnap: 'general.enableWindowSnap', enableLoadingAnimation: 'general.enableLoadingAnimation', enableTabs: 'general.enableTabs', -} as const; - -export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = { + // editing fontSize: 'editing.fontSize', fontFamily: 'editing.fontFamily', fontWeight: 'editing.fontWeight', @@ -57,16 +31,12 @@ export const EDITING_CONFIG_KEY_MAP: EditingConfigKeyMap = { enableTabIndent: 'editing.enableTabIndent', tabSize: 'editing.tabSize', tabType: 'editing.tabType', - autoSaveDelay: 'editing.autoSaveDelay' -} as const; - -export const APPEARANCE_CONFIG_KEY_MAP: AppearanceConfigKeyMap = { + autoSaveDelay: 'editing.autoSaveDelay', + // appearance language: 'appearance.language', systemTheme: 'appearance.systemTheme', - currentTheme: 'appearance.currentTheme' -} as const; - -export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = { + currentTheme: 'appearance.currentTheme', + // updates version: 'updates.version', autoUpdate: 'updates.autoUpdate', primarySource: 'updates.primarySource', @@ -74,10 +44,8 @@ export const UPDATES_CONFIG_KEY_MAP: UpdatesConfigKeyMap = { backupBeforeUpdate: 'updates.backupBeforeUpdate', updateTimeout: 'updates.updateTimeout', github: 'updates.github', - gitea: 'updates.gitea' -} as const; - -export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = { + gitea: 'updates.gitea', + // backup enabled: 'backup.enabled', repo_url: 'backup.repo_url', auth_method: 'backup.auth_method', @@ -90,6 +58,8 @@ export const BACKUP_CONFIG_KEY_MAP: BackupConfigKeyMap = { auto_backup: 'backup.auto_backup', } as const; +export type ConfigKey = keyof typeof CONFIG_KEY_MAP; + // 配置限制 export const CONFIG_LIMITS = { fontSize: {min: 12, max: 28, default: 13}, diff --git a/frontend/src/common/constant/emojies.ts b/frontend/src/common/constant/emojies.ts new file mode 100644 index 0000000..5939148 --- /dev/null +++ b/frontend/src/common/constant/emojies.ts @@ -0,0 +1,1945 @@ +export interface EmojiDefs { + [key: string]: string; +} + +export const emojies: EmojiDefs = { + "100": "💯", + "1234": "🔢", + "grinning": "😀", + "smiley": "😃", + "smile": "😄", + "grin": "😁", + "laughing": "😆", + "satisfied": "😆", + "sweat_smile": "😅", + "rofl": "🤣", + "joy": "😂", + "slightly_smiling_face": "🙂", + "upside_down_face": "🙃", + "melting_face": "🫠", + "wink": "😉", + "blush": "😊", + "innocent": "😇", + "smiling_face_with_three_hearts": "🥰", + "heart_eyes": "😍", + "star_struck": "🤩", + "kissing_heart": "😘", + "kissing": "😗", + "relaxed": "☺️", + "kissing_closed_eyes": "😚", + "kissing_smiling_eyes": "😙", + "smiling_face_with_tear": "🥲", + "yum": "😋", + "stuck_out_tongue": "😛", + "stuck_out_tongue_winking_eye": "😜", + "zany_face": "🤪", + "stuck_out_tongue_closed_eyes": "😝", + "money_mouth_face": "🤑", + "hugs": "🤗", + "hand_over_mouth": "🤭", + "face_with_open_eyes_and_hand_over_mouth": "🫢", + "face_with_peeking_eye": "🫣", + "shushing_face": "🤫", + "thinking": "🤔", + "saluting_face": "🫡", + "zipper_mouth_face": "🤐", + "raised_eyebrow": "🤨", + "neutral_face": "😐", + "expressionless": "😑", + "no_mouth": "😶", + "dotted_line_face": "🫥", + "face_in_clouds": "😶‍🌫️", + "smirk": "😏", + "unamused": "😒", + "roll_eyes": "🙄", + "grimacing": "😬", + "face_exhaling": "😮‍💨", + "lying_face": "🤥", + "shaking_face": "🫨", + "relieved": "😌", + "pensive": "😔", + "sleepy": "😪", + "drooling_face": "🤤", + "sleeping": "😴", + "mask": "😷", + "face_with_thermometer": "🤒", + "face_with_head_bandage": "🤕", + "nauseated_face": "🤢", + "vomiting_face": "🤮", + "sneezing_face": "🤧", + "hot_face": "🥵", + "cold_face": "🥶", + "woozy_face": "🥴", + "dizzy_face": "😵", + "face_with_spiral_eyes": "😵‍💫", + "exploding_head": "🤯", + "cowboy_hat_face": "🤠", + "partying_face": "🥳", + "disguised_face": "🥸", + "sunglasses": "😎", + "nerd_face": "🤓", + "monocle_face": "🧐", + "confused": "😕", + "face_with_diagonal_mouth": "🫤", + "worried": "😟", + "slightly_frowning_face": "🙁", + "frowning_face": "☹️", + "open_mouth": "😮", + "hushed": "😯", + "astonished": "😲", + "flushed": "😳", + "pleading_face": "🥺", + "face_holding_back_tears": "🥹", + "frowning": "😦", + "anguished": "😧", + "fearful": "😨", + "cold_sweat": "😰", + "disappointed_relieved": "😥", + "cry": "😢", + "sob": "😭", + "scream": "😱", + "confounded": "😖", + "persevere": "😣", + "disappointed": "😞", + "sweat": "😓", + "weary": "😩", + "tired_face": "😫", + "yawning_face": "🥱", + "triumph": "😤", + "rage": "😡", + "pout": "😡", + "angry": "😠", + "cursing_face": "🤬", + "smiling_imp": "😈", + "imp": "👿", + "skull": "💀", + "skull_and_crossbones": "☠️", + "hankey": "💩", + "poop": "💩", + "shit": "💩", + "clown_face": "🤡", + "japanese_ogre": "👹", + "japanese_goblin": "👺", + "ghost": "👻", + "alien": "👽", + "space_invader": "👾", + "robot": "🤖", + "smiley_cat": "😺", + "smile_cat": "😸", + "joy_cat": "😹", + "heart_eyes_cat": "😻", + "smirk_cat": "😼", + "kissing_cat": "😽", + "scream_cat": "🙀", + "crying_cat_face": "😿", + "pouting_cat": "😾", + "see_no_evil": "🙈", + "hear_no_evil": "🙉", + "speak_no_evil": "🙊", + "love_letter": "💌", + "cupid": "💘", + "gift_heart": "💝", + "sparkling_heart": "💖", + "heartpulse": "💗", + "heartbeat": "💓", + "revolving_hearts": "💞", + "two_hearts": "💕", + "heart_decoration": "💟", + "heavy_heart_exclamation": "❣️", + "broken_heart": "💔", + "heart_on_fire": "❤️‍🔥", + "mending_heart": "❤️‍🩹", + "heart": "❤️", + "pink_heart": "🩷", + "orange_heart": "🧡", + "yellow_heart": "💛", + "green_heart": "💚", + "blue_heart": "💙", + "light_blue_heart": "🩵", + "purple_heart": "💜", + "brown_heart": "🤎", + "black_heart": "🖤", + "grey_heart": "🩶", + "white_heart": "🤍", + "kiss": "💋", + "anger": "💢", + "boom": "💥", + "collision": "💥", + "dizzy": "💫", + "sweat_drops": "💦", + "dash": "💨", + "hole": "🕳️", + "speech_balloon": "💬", + "eye_speech_bubble": "👁️‍🗨️", + "left_speech_bubble": "🗨️", + "right_anger_bubble": "🗯️", + "thought_balloon": "💭", + "zzz": "💤", + "wave": "👋", + "raised_back_of_hand": "🤚", + "raised_hand_with_fingers_splayed": "🖐️", + "hand": "✋", + "raised_hand": "✋", + "vulcan_salute": "🖖", + "rightwards_hand": "🫱", + "leftwards_hand": "🫲", + "palm_down_hand": "🫳", + "palm_up_hand": "🫴", + "leftwards_pushing_hand": "🫷", + "rightwards_pushing_hand": "🫸", + "ok_hand": "👌", + "pinched_fingers": "🤌", + "pinching_hand": "🤏", + "v": "✌️", + "crossed_fingers": "🤞", + "hand_with_index_finger_and_thumb_crossed": "🫰", + "love_you_gesture": "🤟", + "metal": "🤘", + "call_me_hand": "🤙", + "point_left": "👈", + "point_right": "👉", + "point_up_2": "👆", + "middle_finger": "🖕", + "fu": "🖕", + "point_down": "👇", + "point_up": "☝️", + "index_pointing_at_the_viewer": "🫵", + "+1": "👍", + "thumbsup": "👍", + "-1": "👎", + "thumbsdown": "👎", + "fist_raised": "✊", + "fist": "✊", + "fist_oncoming": "👊", + "facepunch": "👊", + "punch": "👊", + "fist_left": "🤛", + "fist_right": "🤜", + "clap": "👏", + "raised_hands": "🙌", + "heart_hands": "🫶", + "open_hands": "👐", + "palms_up_together": "🤲", + "handshake": "🤝", + "pray": "🙏", + "writing_hand": "✍️", + "nail_care": "💅", + "selfie": "🤳", + "muscle": "💪", + "mechanical_arm": "🦾", + "mechanical_leg": "🦿", + "leg": "🦵", + "foot": "🦶", + "ear": "👂", + "ear_with_hearing_aid": "🦻", + "nose": "👃", + "brain": "🧠", + "anatomical_heart": "🫀", + "lungs": "🫁", + "tooth": "🦷", + "bone": "🦴", + "eyes": "👀", + "eye": "👁️", + "tongue": "👅", + "lips": "👄", + "biting_lip": "🫦", + "baby": "👶", + "child": "🧒", + "boy": "👦", + "girl": "👧", + "adult": "🧑", + "blond_haired_person": "👱", + "man": "👨", + "bearded_person": "🧔", + "man_beard": "🧔‍♂️", + "woman_beard": "🧔‍♀️", + "red_haired_man": "👨‍🦰", + "curly_haired_man": "👨‍🦱", + "white_haired_man": "👨‍🦳", + "bald_man": "👨‍🦲", + "woman": "👩", + "red_haired_woman": "👩‍🦰", + "person_red_hair": "🧑‍🦰", + "curly_haired_woman": "👩‍🦱", + "person_curly_hair": "🧑‍🦱", + "white_haired_woman": "👩‍🦳", + "person_white_hair": "🧑‍🦳", + "bald_woman": "👩‍🦲", + "person_bald": "🧑‍🦲", + "blond_haired_woman": "👱‍♀️", + "blonde_woman": "👱‍♀️", + "blond_haired_man": "👱‍♂️", + "older_adult": "🧓", + "older_man": "👴", + "older_woman": "👵", + "frowning_person": "🙍", + "frowning_man": "🙍‍♂️", + "frowning_woman": "🙍‍♀️", + "pouting_face": "🙎", + "pouting_man": "🙎‍♂️", + "pouting_woman": "🙎‍♀️", + "no_good": "🙅", + "no_good_man": "🙅‍♂️", + "ng_man": "🙅‍♂️", + "no_good_woman": "🙅‍♀️", + "ng_woman": "🙅‍♀️", + "ok_person": "🙆", + "ok_man": "🙆‍♂️", + "ok_woman": "🙆‍♀️", + "tipping_hand_person": "💁", + "information_desk_person": "💁", + "tipping_hand_man": "💁‍♂️", + "sassy_man": "💁‍♂️", + "tipping_hand_woman": "💁‍♀️", + "sassy_woman": "💁‍♀️", + "raising_hand": "🙋", + "raising_hand_man": "🙋‍♂️", + "raising_hand_woman": "🙋‍♀️", + "deaf_person": "🧏", + "deaf_man": "🧏‍♂️", + "deaf_woman": "🧏‍♀️", + "bow": "🙇", + "bowing_man": "🙇‍♂️", + "bowing_woman": "🙇‍♀️", + "facepalm": "🤦", + "man_facepalming": "🤦‍♂️", + "woman_facepalming": "🤦‍♀️", + "shrug": "🤷", + "man_shrugging": "🤷‍♂️", + "woman_shrugging": "🤷‍♀️", + "health_worker": "🧑‍⚕️", + "man_health_worker": "👨‍⚕️", + "woman_health_worker": "👩‍⚕️", + "student": "🧑‍🎓", + "man_student": "👨‍🎓", + "woman_student": "👩‍🎓", + "teacher": "🧑‍🏫", + "man_teacher": "👨‍🏫", + "woman_teacher": "👩‍🏫", + "judge": "🧑‍⚖️", + "man_judge": "👨‍⚖️", + "woman_judge": "👩‍⚖️", + "farmer": "🧑‍🌾", + "man_farmer": "👨‍🌾", + "woman_farmer": "👩‍🌾", + "cook": "🧑‍🍳", + "man_cook": "👨‍🍳", + "woman_cook": "👩‍🍳", + "mechanic": "🧑‍🔧", + "man_mechanic": "👨‍🔧", + "woman_mechanic": "👩‍🔧", + "factory_worker": "🧑‍🏭", + "man_factory_worker": "👨‍🏭", + "woman_factory_worker": "👩‍🏭", + "office_worker": "🧑‍💼", + "man_office_worker": "👨‍💼", + "woman_office_worker": "👩‍💼", + "scientist": "🧑‍🔬", + "man_scientist": "👨‍🔬", + "woman_scientist": "👩‍🔬", + "technologist": "🧑‍💻", + "man_technologist": "👨‍💻", + "woman_technologist": "👩‍💻", + "singer": "🧑‍🎤", + "man_singer": "👨‍🎤", + "woman_singer": "👩‍🎤", + "artist": "🧑‍🎨", + "man_artist": "👨‍🎨", + "woman_artist": "👩‍🎨", + "pilot": "🧑‍✈️", + "man_pilot": "👨‍✈️", + "woman_pilot": "👩‍✈️", + "astronaut": "🧑‍🚀", + "man_astronaut": "👨‍🚀", + "woman_astronaut": "👩‍🚀", + "firefighter": "🧑‍🚒", + "man_firefighter": "👨‍🚒", + "woman_firefighter": "👩‍🚒", + "police_officer": "👮", + "cop": "👮", + "policeman": "👮‍♂️", + "policewoman": "👮‍♀️", + "detective": "🕵️", + "male_detective": "🕵️‍♂️", + "female_detective": "🕵️‍♀️", + "guard": "💂", + "guardsman": "💂‍♂️", + "guardswoman": "💂‍♀️", + "ninja": "🥷", + "construction_worker": "👷", + "construction_worker_man": "👷‍♂️", + "construction_worker_woman": "👷‍♀️", + "person_with_crown": "🫅", + "prince": "🤴", + "princess": "👸", + "person_with_turban": "👳", + "man_with_turban": "👳‍♂️", + "woman_with_turban": "👳‍♀️", + "man_with_gua_pi_mao": "👲", + "woman_with_headscarf": "🧕", + "person_in_tuxedo": "🤵", + "man_in_tuxedo": "🤵‍♂️", + "woman_in_tuxedo": "🤵‍♀️", + "person_with_veil": "👰", + "man_with_veil": "👰‍♂️", + "woman_with_veil": "👰‍♀️", + "bride_with_veil": "👰‍♀️", + "pregnant_woman": "🤰", + "pregnant_man": "🫃", + "pregnant_person": "🫄", + "breast_feeding": "🤱", + "woman_feeding_baby": "👩‍🍼", + "man_feeding_baby": "👨‍🍼", + "person_feeding_baby": "🧑‍🍼", + "angel": "👼", + "santa": "🎅", + "mrs_claus": "🤶", + "mx_claus": "🧑‍🎄", + "superhero": "🦸", + "superhero_man": "🦸‍♂️", + "superhero_woman": "🦸‍♀️", + "supervillain": "🦹", + "supervillain_man": "🦹‍♂️", + "supervillain_woman": "🦹‍♀️", + "mage": "🧙", + "mage_man": "🧙‍♂️", + "mage_woman": "🧙‍♀️", + "fairy": "🧚", + "fairy_man": "🧚‍♂️", + "fairy_woman": "🧚‍♀️", + "vampire": "🧛", + "vampire_man": "🧛‍♂️", + "vampire_woman": "🧛‍♀️", + "merperson": "🧜", + "merman": "🧜‍♂️", + "mermaid": "🧜‍♀️", + "elf": "🧝", + "elf_man": "🧝‍♂️", + "elf_woman": "🧝‍♀️", + "genie": "🧞", + "genie_man": "🧞‍♂️", + "genie_woman": "🧞‍♀️", + "zombie": "🧟", + "zombie_man": "🧟‍♂️", + "zombie_woman": "🧟‍♀️", + "troll": "🧌", + "massage": "💆", + "massage_man": "💆‍♂️", + "massage_woman": "💆‍♀️", + "haircut": "💇", + "haircut_man": "💇‍♂️", + "haircut_woman": "💇‍♀️", + "walking": "🚶", + "walking_man": "🚶‍♂️", + "walking_woman": "🚶‍♀️", + "standing_person": "🧍", + "standing_man": "🧍‍♂️", + "standing_woman": "🧍‍♀️", + "kneeling_person": "🧎", + "kneeling_man": "🧎‍♂️", + "kneeling_woman": "🧎‍♀️", + "person_with_probing_cane": "🧑‍🦯", + "man_with_probing_cane": "👨‍🦯", + "woman_with_probing_cane": "👩‍🦯", + "person_in_motorized_wheelchair": "🧑‍🦼", + "man_in_motorized_wheelchair": "👨‍🦼", + "woman_in_motorized_wheelchair": "👩‍🦼", + "person_in_manual_wheelchair": "🧑‍🦽", + "man_in_manual_wheelchair": "👨‍🦽", + "woman_in_manual_wheelchair": "👩‍🦽", + "runner": "🏃", + "running": "🏃", + "running_man": "🏃‍♂️", + "running_woman": "🏃‍♀️", + "woman_dancing": "💃", + "dancer": "💃", + "man_dancing": "🕺", + "business_suit_levitating": "🕴️", + "dancers": "👯", + "dancing_men": "👯‍♂️", + "dancing_women": "👯‍♀️", + "sauna_person": "🧖", + "sauna_man": "🧖‍♂️", + "sauna_woman": "🧖‍♀️", + "climbing": "🧗", + "climbing_man": "🧗‍♂️", + "climbing_woman": "🧗‍♀️", + "person_fencing": "🤺", + "horse_racing": "🏇", + "skier": "⛷️", + "snowboarder": "🏂", + "golfing": "🏌️", + "golfing_man": "🏌️‍♂️", + "golfing_woman": "🏌️‍♀️", + "surfer": "🏄", + "surfing_man": "🏄‍♂️", + "surfing_woman": "🏄‍♀️", + "rowboat": "🚣", + "rowing_man": "🚣‍♂️", + "rowing_woman": "🚣‍♀️", + "swimmer": "🏊", + "swimming_man": "🏊‍♂️", + "swimming_woman": "🏊‍♀️", + "bouncing_ball_person": "⛹️", + "bouncing_ball_man": "⛹️‍♂️", + "basketball_man": "⛹️‍♂️", + "bouncing_ball_woman": "⛹️‍♀️", + "basketball_woman": "⛹️‍♀️", + "weight_lifting": "🏋️", + "weight_lifting_man": "🏋️‍♂️", + "weight_lifting_woman": "🏋️‍♀️", + "bicyclist": "🚴", + "biking_man": "🚴‍♂️", + "biking_woman": "🚴‍♀️", + "mountain_bicyclist": "🚵", + "mountain_biking_man": "🚵‍♂️", + "mountain_biking_woman": "🚵‍♀️", + "cartwheeling": "🤸", + "man_cartwheeling": "🤸‍♂️", + "woman_cartwheeling": "🤸‍♀️", + "wrestling": "🤼", + "men_wrestling": "🤼‍♂️", + "women_wrestling": "🤼‍♀️", + "water_polo": "🤽", + "man_playing_water_polo": "🤽‍♂️", + "woman_playing_water_polo": "🤽‍♀️", + "handball_person": "🤾", + "man_playing_handball": "🤾‍♂️", + "woman_playing_handball": "🤾‍♀️", + "juggling_person": "🤹", + "man_juggling": "🤹‍♂️", + "woman_juggling": "🤹‍♀️", + "lotus_position": "🧘", + "lotus_position_man": "🧘‍♂️", + "lotus_position_woman": "🧘‍♀️", + "bath": "🛀", + "sleeping_bed": "🛌", + "people_holding_hands": "🧑‍🤝‍🧑", + "two_women_holding_hands": "👭", + "couple": "👫", + "two_men_holding_hands": "👬", + "couplekiss": "💏", + "couplekiss_man_woman": "👩‍❤️‍💋‍👨", + "couplekiss_man_man": "👨‍❤️‍💋‍👨", + "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", + "couple_with_heart": "💑", + "couple_with_heart_woman_man": "👩‍❤️‍👨", + "couple_with_heart_man_man": "👨‍❤️‍👨", + "couple_with_heart_woman_woman": "👩‍❤️‍👩", + "family": "👪", + "family_man_woman_boy": "👨‍👩‍👦", + "family_man_woman_girl": "👨‍👩‍👧", + "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", + "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", + "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", + "family_man_man_boy": "👨‍👨‍👦", + "family_man_man_girl": "👨‍👨‍👧", + "family_man_man_girl_boy": "👨‍👨‍👧‍👦", + "family_man_man_boy_boy": "👨‍👨‍👦‍👦", + "family_man_man_girl_girl": "👨‍👨‍👧‍👧", + "family_woman_woman_boy": "👩‍👩‍👦", + "family_woman_woman_girl": "👩‍👩‍👧", + "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", + "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", + "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", + "family_man_boy": "👨‍👦", + "family_man_boy_boy": "👨‍👦‍👦", + "family_man_girl": "👨‍👧", + "family_man_girl_boy": "👨‍👧‍👦", + "family_man_girl_girl": "👨‍👧‍👧", + "family_woman_boy": "👩‍👦", + "family_woman_boy_boy": "👩‍👦‍👦", + "family_woman_girl": "👩‍👧", + "family_woman_girl_boy": "👩‍👧‍👦", + "family_woman_girl_girl": "👩‍👧‍👧", + "speaking_head": "🗣️", + "bust_in_silhouette": "👤", + "busts_in_silhouette": "👥", + "people_hugging": "🫂", + "footprints": "👣", + "monkey_face": "🐵", + "monkey": "🐒", + "gorilla": "🦍", + "orangutan": "🦧", + "dog": "🐶", + "dog2": "🐕", + "guide_dog": "🦮", + "service_dog": "🐕‍🦺", + "poodle": "🐩", + "wolf": "🐺", + "fox_face": "🦊", + "raccoon": "🦝", + "cat": "🐱", + "cat2": "🐈", + "black_cat": "🐈‍⬛", + "lion": "🦁", + "tiger": "🐯", + "tiger2": "🐅", + "leopard": "🐆", + "horse": "🐴", + "moose": "🫎", + "donkey": "🫏", + "racehorse": "🐎", + "unicorn": "🦄", + "zebra": "🦓", + "deer": "🦌", + "bison": "🦬", + "cow": "🐮", + "ox": "🐂", + "water_buffalo": "🐃", + "cow2": "🐄", + "pig": "🐷", + "pig2": "🐖", + "boar": "🐗", + "pig_nose": "🐽", + "ram": "🐏", + "sheep": "🐑", + "goat": "🐐", + "dromedary_camel": "🐪", + "camel": "🐫", + "llama": "🦙", + "giraffe": "🦒", + "elephant": "🐘", + "mammoth": "🦣", + "rhinoceros": "🦏", + "hippopotamus": "🦛", + "mouse": "🐭", + "mouse2": "🐁", + "rat": "🐀", + "hamster": "🐹", + "rabbit": "🐰", + "rabbit2": "🐇", + "chipmunk": "🐿️", + "beaver": "🦫", + "hedgehog": "🦔", + "bat": "🦇", + "bear": "🐻", + "polar_bear": "🐻‍❄️", + "koala": "🐨", + "panda_face": "🐼", + "sloth": "🦥", + "otter": "🦦", + "skunk": "🦨", + "kangaroo": "🦘", + "badger": "🦡", + "feet": "🐾", + "paw_prints": "🐾", + "turkey": "🦃", + "chicken": "🐔", + "rooster": "🐓", + "hatching_chick": "🐣", + "baby_chick": "🐤", + "hatched_chick": "🐥", + "bird": "🐦", + "penguin": "🐧", + "dove": "🕊️", + "eagle": "🦅", + "duck": "🦆", + "swan": "🦢", + "owl": "🦉", + "dodo": "🦤", + "feather": "🪶", + "flamingo": "🦩", + "peacock": "🦚", + "parrot": "🦜", + "wing": "🪽", + "black_bird": "🐦‍⬛", + "goose": "🪿", + "frog": "🐸", + "crocodile": "🐊", + "turtle": "🐢", + "lizard": "🦎", + "snake": "🐍", + "dragon_face": "🐲", + "dragon": "🐉", + "sauropod": "🦕", + "t-rex": "🦖", + "whale": "🐳", + "whale2": "🐋", + "dolphin": "🐬", + "flipper": "🐬", + "seal": "🦭", + "fish": "🐟", + "tropical_fish": "🐠", + "blowfish": "🐡", + "shark": "🦈", + "octopus": "🐙", + "shell": "🐚", + "coral": "🪸", + "jellyfish": "🪼", + "snail": "🐌", + "butterfly": "🦋", + "bug": "🐛", + "ant": "🐜", + "bee": "🐝", + "honeybee": "🐝", + "beetle": "🪲", + "lady_beetle": "🐞", + "cricket": "🦗", + "cockroach": "🪳", + "spider": "🕷️", + "spider_web": "🕸️", + "scorpion": "🦂", + "mosquito": "🦟", + "fly": "🪰", + "worm": "🪱", + "microbe": "🦠", + "bouquet": "💐", + "cherry_blossom": "🌸", + "white_flower": "💮", + "lotus": "🪷", + "rosette": "🏵️", + "rose": "🌹", + "wilted_flower": "🥀", + "hibiscus": "🌺", + "sunflower": "🌻", + "blossom": "🌼", + "tulip": "🌷", + "hyacinth": "🪻", + "seedling": "🌱", + "potted_plant": "🪴", + "evergreen_tree": "🌲", + "deciduous_tree": "🌳", + "palm_tree": "🌴", + "cactus": "🌵", + "ear_of_rice": "🌾", + "herb": "🌿", + "shamrock": "☘️", + "four_leaf_clover": "🍀", + "maple_leaf": "🍁", + "fallen_leaf": "🍂", + "leaves": "🍃", + "empty_nest": "🪹", + "nest_with_eggs": "🪺", + "mushroom": "🍄", + "grapes": "🍇", + "melon": "🍈", + "watermelon": "🍉", + "tangerine": "🍊", + "orange": "🍊", + "mandarin": "🍊", + "lemon": "🍋", + "banana": "🍌", + "pineapple": "🍍", + "mango": "🥭", + "apple": "🍎", + "green_apple": "🍏", + "pear": "🍐", + "peach": "🍑", + "cherries": "🍒", + "strawberry": "🍓", + "blueberries": "🫐", + "kiwi_fruit": "🥝", + "tomato": "🍅", + "olive": "🫒", + "coconut": "🥥", + "avocado": "🥑", + "eggplant": "🍆", + "potato": "🥔", + "carrot": "🥕", + "corn": "🌽", + "hot_pepper": "🌶️", + "bell_pepper": "🫑", + "cucumber": "🥒", + "leafy_green": "🥬", + "broccoli": "🥦", + "garlic": "🧄", + "onion": "🧅", + "peanuts": "🥜", + "beans": "🫘", + "chestnut": "🌰", + "ginger_root": "🫚", + "pea_pod": "🫛", + "bread": "🍞", + "croissant": "🥐", + "baguette_bread": "🥖", + "flatbread": "🫓", + "pretzel": "🥨", + "bagel": "🥯", + "pancakes": "🥞", + "waffle": "🧇", + "cheese": "🧀", + "meat_on_bone": "🍖", + "poultry_leg": "🍗", + "cut_of_meat": "🥩", + "bacon": "🥓", + "hamburger": "🍔", + "fries": "🍟", + "pizza": "🍕", + "hotdog": "🌭", + "sandwich": "🥪", + "taco": "🌮", + "burrito": "🌯", + "tamale": "🫔", + "stuffed_flatbread": "🥙", + "falafel": "🧆", + "egg": "🥚", + "fried_egg": "🍳", + "shallow_pan_of_food": "🥘", + "stew": "🍲", + "fondue": "🫕", + "bowl_with_spoon": "🥣", + "green_salad": "🥗", + "popcorn": "🍿", + "butter": "🧈", + "salt": "🧂", + "canned_food": "🥫", + "bento": "🍱", + "rice_cracker": "🍘", + "rice_ball": "🍙", + "rice": "🍚", + "curry": "🍛", + "ramen": "🍜", + "spaghetti": "🍝", + "sweet_potato": "🍠", + "oden": "🍢", + "sushi": "🍣", + "fried_shrimp": "🍤", + "fish_cake": "🍥", + "moon_cake": "🥮", + "dango": "🍡", + "dumpling": "🥟", + "fortune_cookie": "🥠", + "takeout_box": "🥡", + "crab": "🦀", + "lobster": "🦞", + "shrimp": "🦐", + "squid": "🦑", + "oyster": "🦪", + "icecream": "🍦", + "shaved_ice": "🍧", + "ice_cream": "🍨", + "doughnut": "🍩", + "cookie": "🍪", + "birthday": "🎂", + "cake": "🍰", + "cupcake": "🧁", + "pie": "🥧", + "chocolate_bar": "🍫", + "candy": "🍬", + "lollipop": "🍭", + "custard": "🍮", + "honey_pot": "🍯", + "baby_bottle": "🍼", + "milk_glass": "🥛", + "coffee": "☕", + "teapot": "🫖", + "tea": "🍵", + "sake": "🍶", + "champagne": "🍾", + "wine_glass": "🍷", + "cocktail": "🍸", + "tropical_drink": "🍹", + "beer": "🍺", + "beers": "🍻", + "clinking_glasses": "🥂", + "tumbler_glass": "🥃", + "pouring_liquid": "🫗", + "cup_with_straw": "🥤", + "bubble_tea": "🧋", + "beverage_box": "🧃", + "mate": "🧉", + "ice_cube": "🧊", + "chopsticks": "🥢", + "plate_with_cutlery": "🍽️", + "fork_and_knife": "🍴", + "spoon": "🥄", + "hocho": "🔪", + "knife": "🔪", + "jar": "🫙", + "amphora": "🏺", + "earth_africa": "🌍", + "earth_americas": "🌎", + "earth_asia": "🌏", + "globe_with_meridians": "🌐", + "world_map": "🗺️", + "japan": "🗾", + "compass": "🧭", + "mountain_snow": "🏔️", + "mountain": "⛰️", + "volcano": "🌋", + "mount_fuji": "🗻", + "camping": "🏕️", + "beach_umbrella": "🏖️", + "desert": "🏜️", + "desert_island": "🏝️", + "national_park": "🏞️", + "stadium": "🏟️", + "classical_building": "🏛️", + "building_construction": "🏗️", + "bricks": "🧱", + "rock": "🪨", + "wood": "🪵", + "hut": "🛖", + "houses": "🏘️", + "derelict_house": "🏚️", + "house": "🏠", + "house_with_garden": "🏡", + "office": "🏢", + "post_office": "🏣", + "european_post_office": "🏤", + "hospital": "🏥", + "bank": "🏦", + "hotel": "🏨", + "love_hotel": "🏩", + "convenience_store": "🏪", + "school": "🏫", + "department_store": "🏬", + "factory": "🏭", + "japanese_castle": "🏯", + "european_castle": "🏰", + "wedding": "💒", + "tokyo_tower": "🗼", + "statue_of_liberty": "🗽", + "church": "⛪", + "mosque": "🕌", + "hindu_temple": "🛕", + "synagogue": "🕍", + "shinto_shrine": "⛩️", + "kaaba": "🕋", + "fountain": "⛲", + "tent": "⛺", + "foggy": "🌁", + "night_with_stars": "🌃", + "cityscape": "🏙️", + "sunrise_over_mountains": "🌄", + "sunrise": "🌅", + "city_sunset": "🌆", + "city_sunrise": "🌇", + "bridge_at_night": "🌉", + "hotsprings": "♨️", + "carousel_horse": "🎠", + "playground_slide": "🛝", + "ferris_wheel": "🎡", + "roller_coaster": "🎢", + "barber": "💈", + "circus_tent": "🎪", + "steam_locomotive": "🚂", + "railway_car": "🚃", + "bullettrain_side": "🚄", + "bullettrain_front": "🚅", + "train2": "🚆", + "metro": "🚇", + "light_rail": "🚈", + "station": "🚉", + "tram": "🚊", + "monorail": "🚝", + "mountain_railway": "🚞", + "train": "🚋", + "bus": "🚌", + "oncoming_bus": "🚍", + "trolleybus": "🚎", + "minibus": "🚐", + "ambulance": "🚑", + "fire_engine": "🚒", + "police_car": "🚓", + "oncoming_police_car": "🚔", + "taxi": "🚕", + "oncoming_taxi": "🚖", + "car": "🚗", + "red_car": "🚗", + "oncoming_automobile": "🚘", + "blue_car": "🚙", + "pickup_truck": "🛻", + "truck": "🚚", + "articulated_lorry": "🚛", + "tractor": "🚜", + "racing_car": "🏎️", + "motorcycle": "🏍️", + "motor_scooter": "🛵", + "manual_wheelchair": "🦽", + "motorized_wheelchair": "🦼", + "auto_rickshaw": "🛺", + "bike": "🚲", + "kick_scooter": "🛴", + "skateboard": "🛹", + "roller_skate": "🛼", + "busstop": "🚏", + "motorway": "🛣️", + "railway_track": "🛤️", + "oil_drum": "🛢️", + "fuelpump": "⛽", + "wheel": "🛞", + "rotating_light": "🚨", + "traffic_light": "🚥", + "vertical_traffic_light": "🚦", + "stop_sign": "🛑", + "construction": "🚧", + "anchor": "⚓", + "ring_buoy": "🛟", + "boat": "⛵", + "sailboat": "⛵", + "canoe": "🛶", + "speedboat": "🚤", + "passenger_ship": "🛳️", + "ferry": "⛴️", + "motor_boat": "🛥️", + "ship": "🚢", + "airplane": "✈️", + "small_airplane": "🛩️", + "flight_departure": "🛫", + "flight_arrival": "🛬", + "parachute": "🪂", + "seat": "💺", + "helicopter": "🚁", + "suspension_railway": "🚟", + "mountain_cableway": "🚠", + "aerial_tramway": "🚡", + "artificial_satellite": "🛰️", + "rocket": "🚀", + "flying_saucer": "🛸", + "bellhop_bell": "🛎️", + "luggage": "🧳", + "hourglass": "⌛", + "hourglass_flowing_sand": "⏳", + "watch": "⌚", + "alarm_clock": "⏰", + "stopwatch": "⏱️", + "timer_clock": "⏲️", + "mantelpiece_clock": "🕰️", + "clock12": "🕛", + "clock1230": "🕧", + "clock1": "🕐", + "clock130": "🕜", + "clock2": "🕑", + "clock230": "🕝", + "clock3": "🕒", + "clock330": "🕞", + "clock4": "🕓", + "clock430": "🕟", + "clock5": "🕔", + "clock530": "🕠", + "clock6": "🕕", + "clock630": "🕡", + "clock7": "🕖", + "clock730": "🕢", + "clock8": "🕗", + "clock830": "🕣", + "clock9": "🕘", + "clock930": "🕤", + "clock10": "🕙", + "clock1030": "🕥", + "clock11": "🕚", + "clock1130": "🕦", + "new_moon": "🌑", + "waxing_crescent_moon": "🌒", + "first_quarter_moon": "🌓", + "moon": "🌔", + "waxing_gibbous_moon": "🌔", + "full_moon": "🌕", + "waning_gibbous_moon": "🌖", + "last_quarter_moon": "🌗", + "waning_crescent_moon": "🌘", + "crescent_moon": "🌙", + "new_moon_with_face": "🌚", + "first_quarter_moon_with_face": "🌛", + "last_quarter_moon_with_face": "🌜", + "thermometer": "🌡️", + "sunny": "☀️", + "full_moon_with_face": "🌝", + "sun_with_face": "🌞", + "ringed_planet": "🪐", + "star": "⭐", + "star2": "🌟", + "stars": "🌠", + "milky_way": "🌌", + "cloud": "☁️", + "partly_sunny": "⛅", + "cloud_with_lightning_and_rain": "⛈️", + "sun_behind_small_cloud": "🌤️", + "sun_behind_large_cloud": "🌥️", + "sun_behind_rain_cloud": "🌦️", + "cloud_with_rain": "🌧️", + "cloud_with_snow": "🌨️", + "cloud_with_lightning": "🌩️", + "tornado": "🌪️", + "fog": "🌫️", + "wind_face": "🌬️", + "cyclone": "🌀", + "rainbow": "🌈", + "closed_umbrella": "🌂", + "open_umbrella": "☂️", + "umbrella": "☔", + "parasol_on_ground": "⛱️", + "zap": "⚡", + "snowflake": "❄️", + "snowman_with_snow": "☃️", + "snowman": "⛄", + "comet": "☄️", + "fire": "🔥", + "droplet": "💧", + "ocean": "🌊", + "jack_o_lantern": "🎃", + "christmas_tree": "🎄", + "fireworks": "🎆", + "sparkler": "🎇", + "firecracker": "🧨", + "sparkles": "✨", + "balloon": "🎈", + "tada": "🎉", + "confetti_ball": "🎊", + "tanabata_tree": "🎋", + "bamboo": "🎍", + "dolls": "🎎", + "flags": "🎏", + "wind_chime": "🎐", + "rice_scene": "🎑", + "red_envelope": "🧧", + "ribbon": "🎀", + "gift": "🎁", + "reminder_ribbon": "🎗️", + "tickets": "🎟️", + "ticket": "🎫", + "medal_military": "🎖️", + "trophy": "🏆", + "medal_sports": "🏅", + "1st_place_medal": "🥇", + "2nd_place_medal": "🥈", + "3rd_place_medal": "🥉", + "soccer": "⚽", + "baseball": "⚾", + "softball": "🥎", + "basketball": "🏀", + "volleyball": "🏐", + "football": "🏈", + "rugby_football": "🏉", + "tennis": "🎾", + "flying_disc": "🥏", + "bowling": "🎳", + "cricket_game": "🏏", + "field_hockey": "🏑", + "ice_hockey": "🏒", + "lacrosse": "🥍", + "ping_pong": "🏓", + "badminton": "🏸", + "boxing_glove": "🥊", + "martial_arts_uniform": "🥋", + "goal_net": "🥅", + "golf": "⛳", + "ice_skate": "⛸️", + "fishing_pole_and_fish": "🎣", + "diving_mask": "🤿", + "running_shirt_with_sash": "🎽", + "ski": "🎿", + "sled": "🛷", + "curling_stone": "🥌", + "dart": "🎯", + "yo_yo": "🪀", + "kite": "🪁", + "gun": "🔫", + "8ball": "🎱", + "crystal_ball": "🔮", + "magic_wand": "🪄", + "video_game": "🎮", + "joystick": "🕹️", + "slot_machine": "🎰", + "game_die": "🎲", + "jigsaw": "🧩", + "teddy_bear": "🧸", + "pinata": "🪅", + "mirror_ball": "🪩", + "nesting_dolls": "🪆", + "spades": "♠️", + "hearts": "♥️", + "diamonds": "♦️", + "clubs": "♣️", + "chess_pawn": "♟️", + "black_joker": "🃏", + "mahjong": "🀄", + "flower_playing_cards": "🎴", + "performing_arts": "🎭", + "framed_picture": "🖼️", + "art": "🎨", + "thread": "🧵", + "sewing_needle": "🪡", + "yarn": "🧶", + "knot": "🪢", + "eyeglasses": "👓", + "dark_sunglasses": "🕶️", + "goggles": "🥽", + "lab_coat": "🥼", + "safety_vest": "🦺", + "necktie": "👔", + "shirt": "👕", + "tshirt": "👕", + "jeans": "👖", + "scarf": "🧣", + "gloves": "🧤", + "coat": "🧥", + "socks": "🧦", + "dress": "👗", + "kimono": "👘", + "sari": "🥻", + "one_piece_swimsuit": "🩱", + "swim_brief": "🩲", + "shorts": "🩳", + "bikini": "👙", + "womans_clothes": "👚", + "folding_hand_fan": "🪭", + "purse": "👛", + "handbag": "👜", + "pouch": "👝", + "shopping": "🛍️", + "school_satchel": "🎒", + "thong_sandal": "🩴", + "mans_shoe": "👞", + "shoe": "👞", + "athletic_shoe": "👟", + "hiking_boot": "🥾", + "flat_shoe": "🥿", + "high_heel": "👠", + "sandal": "👡", + "ballet_shoes": "🩰", + "boot": "👢", + "hair_pick": "🪮", + "crown": "👑", + "womans_hat": "👒", + "tophat": "🎩", + "mortar_board": "🎓", + "billed_cap": "🧢", + "military_helmet": "🪖", + "rescue_worker_helmet": "⛑️", + "prayer_beads": "📿", + "lipstick": "💄", + "ring": "💍", + "gem": "💎", + "mute": "🔇", + "speaker": "🔈", + "sound": "🔉", + "loud_sound": "🔊", + "loudspeaker": "📢", + "mega": "📣", + "postal_horn": "📯", + "bell": "🔔", + "no_bell": "🔕", + "musical_score": "🎼", + "musical_note": "🎵", + "notes": "🎶", + "studio_microphone": "🎙️", + "level_slider": "🎚️", + "control_knobs": "🎛️", + "microphone": "🎤", + "headphones": "🎧", + "radio": "📻", + "saxophone": "🎷", + "accordion": "🪗", + "guitar": "🎸", + "musical_keyboard": "🎹", + "trumpet": "🎺", + "violin": "🎻", + "banjo": "🪕", + "drum": "🥁", + "long_drum": "🪘", + "maracas": "🪇", + "flute": "🪈", + "iphone": "📱", + "calling": "📲", + "phone": "☎️", + "telephone": "☎️", + "telephone_receiver": "📞", + "pager": "📟", + "fax": "📠", + "battery": "🔋", + "low_battery": "🪫", + "electric_plug": "🔌", + "computer": "💻", + "desktop_computer": "🖥️", + "printer": "🖨️", + "keyboard": "⌨️", + "computer_mouse": "🖱️", + "trackball": "🖲️", + "minidisc": "💽", + "floppy_disk": "💾", + "cd": "💿", + "dvd": "📀", + "abacus": "🧮", + "movie_camera": "🎥", + "film_strip": "🎞️", + "film_projector": "📽️", + "clapper": "🎬", + "tv": "📺", + "camera": "📷", + "camera_flash": "📸", + "video_camera": "📹", + "vhs": "📼", + "mag": "🔍", + "mag_right": "🔎", + "candle": "🕯️", + "bulb": "💡", + "flashlight": "🔦", + "izakaya_lantern": "🏮", + "lantern": "🏮", + "diya_lamp": "🪔", + "notebook_with_decorative_cover": "📔", + "closed_book": "📕", + "book": "📖", + "open_book": "📖", + "green_book": "📗", + "blue_book": "📘", + "orange_book": "📙", + "books": "📚", + "notebook": "📓", + "ledger": "📒", + "page_with_curl": "📃", + "scroll": "📜", + "page_facing_up": "📄", + "newspaper": "📰", + "newspaper_roll": "🗞️", + "bookmark_tabs": "📑", + "bookmark": "🔖", + "label": "🏷️", + "moneybag": "💰", + "coin": "🪙", + "yen": "💴", + "dollar": "💵", + "euro": "💶", + "pound": "💷", + "money_with_wings": "💸", + "credit_card": "💳", + "receipt": "🧾", + "chart": "💹", + "envelope": "✉️", + "email": "📧", + "e-mail": "📧", + "incoming_envelope": "📨", + "envelope_with_arrow": "📩", + "outbox_tray": "📤", + "inbox_tray": "📥", + "package": "📦", + "mailbox": "📫", + "mailbox_closed": "📪", + "mailbox_with_mail": "📬", + "mailbox_with_no_mail": "📭", + "postbox": "📮", + "ballot_box": "🗳️", + "pencil2": "✏️", + "black_nib": "✒️", + "fountain_pen": "🖋️", + "pen": "🖊️", + "paintbrush": "🖌️", + "crayon": "🖍️", + "memo": "📝", + "pencil": "📝", + "briefcase": "💼", + "file_folder": "📁", + "open_file_folder": "📂", + "card_index_dividers": "🗂️", + "date": "📅", + "calendar": "📆", + "spiral_notepad": "🗒️", + "spiral_calendar": "🗓️", + "card_index": "📇", + "chart_with_upwards_trend": "📈", + "chart_with_downwards_trend": "📉", + "bar_chart": "📊", + "clipboard": "📋", + "pushpin": "📌", + "round_pushpin": "📍", + "paperclip": "📎", + "paperclips": "🖇️", + "straight_ruler": "📏", + "triangular_ruler": "📐", + "scissors": "✂️", + "card_file_box": "🗃️", + "file_cabinet": "🗄️", + "wastebasket": "🗑️", + "lock": "🔒", + "unlock": "🔓", + "lock_with_ink_pen": "🔏", + "closed_lock_with_key": "🔐", + "key": "🔑", + "old_key": "🗝️", + "hammer": "🔨", + "axe": "🪓", + "pick": "⛏️", + "hammer_and_pick": "⚒️", + "hammer_and_wrench": "🛠️", + "dagger": "🗡️", + "crossed_swords": "⚔️", + "bomb": "💣", + "boomerang": "🪃", + "bow_and_arrow": "🏹", + "shield": "🛡️", + "carpentry_saw": "🪚", + "wrench": "🔧", + "screwdriver": "🪛", + "nut_and_bolt": "🔩", + "gear": "⚙️", + "clamp": "🗜️", + "balance_scale": "⚖️", + "probing_cane": "🦯", + "link": "🔗", + "chains": "⛓️", + "hook": "🪝", + "toolbox": "🧰", + "magnet": "🧲", + "ladder": "🪜", + "alembic": "⚗️", + "test_tube": "🧪", + "petri_dish": "🧫", + "dna": "🧬", + "microscope": "🔬", + "telescope": "🔭", + "satellite": "📡", + "syringe": "💉", + "drop_of_blood": "🩸", + "pill": "💊", + "adhesive_bandage": "🩹", + "crutch": "🩼", + "stethoscope": "🩺", + "x_ray": "🩻", + "door": "🚪", + "elevator": "🛗", + "mirror": "🪞", + "window": "🪟", + "bed": "🛏️", + "couch_and_lamp": "🛋️", + "chair": "🪑", + "toilet": "🚽", + "plunger": "🪠", + "shower": "🚿", + "bathtub": "🛁", + "mouse_trap": "🪤", + "razor": "🪒", + "lotion_bottle": "🧴", + "safety_pin": "🧷", + "broom": "🧹", + "basket": "🧺", + "roll_of_paper": "🧻", + "bucket": "🪣", + "soap": "🧼", + "bubbles": "🫧", + "toothbrush": "🪥", + "sponge": "🧽", + "fire_extinguisher": "🧯", + "shopping_cart": "🛒", + "smoking": "🚬", + "coffin": "⚰️", + "headstone": "🪦", + "funeral_urn": "⚱️", + "nazar_amulet": "🧿", + "hamsa": "🪬", + "moyai": "🗿", + "placard": "🪧", + "identification_card": "🪪", + "atm": "🏧", + "put_litter_in_its_place": "🚮", + "potable_water": "🚰", + "wheelchair": "♿", + "mens": "🚹", + "womens": "🚺", + "restroom": "🚻", + "baby_symbol": "🚼", + "wc": "🚾", + "passport_control": "🛂", + "customs": "🛃", + "baggage_claim": "🛄", + "left_luggage": "🛅", + "warning": "⚠️", + "children_crossing": "🚸", + "no_entry": "⛔", + "no_entry_sign": "🚫", + "no_bicycles": "🚳", + "no_smoking": "🚭", + "do_not_litter": "🚯", + "non-potable_water": "🚱", + "no_pedestrians": "🚷", + "no_mobile_phones": "📵", + "underage": "🔞", + "radioactive": "☢️", + "biohazard": "☣️", + "arrow_up": "⬆️", + "arrow_upper_right": "↗️", + "arrow_right": "➡️", + "arrow_lower_right": "↘️", + "arrow_down": "⬇️", + "arrow_lower_left": "↙️", + "arrow_left": "⬅️", + "arrow_upper_left": "↖️", + "arrow_up_down": "↕️", + "left_right_arrow": "↔️", + "leftwards_arrow_with_hook": "↩️", + "arrow_right_hook": "↪️", + "arrow_heading_up": "⤴️", + "arrow_heading_down": "⤵️", + "arrows_clockwise": "🔃", + "arrows_counterclockwise": "🔄", + "back": "🔙", + "end": "🔚", + "on": "🔛", + "soon": "🔜", + "top": "🔝", + "place_of_worship": "🛐", + "atom_symbol": "⚛️", + "om": "🕉️", + "star_of_david": "✡️", + "wheel_of_dharma": "☸️", + "yin_yang": "☯️", + "latin_cross": "✝️", + "orthodox_cross": "☦️", + "star_and_crescent": "☪️", + "peace_symbol": "☮️", + "menorah": "🕎", + "six_pointed_star": "🔯", + "khanda": "🪯", + "aries": "♈", + "taurus": "♉", + "gemini": "♊", + "cancer": "♋", + "leo": "♌", + "virgo": "♍", + "libra": "♎", + "scorpius": "♏", + "sagittarius": "♐", + "capricorn": "♑", + "aquarius": "♒", + "pisces": "♓", + "ophiuchus": "⛎", + "twisted_rightwards_arrows": "🔀", + "repeat": "🔁", + "repeat_one": "🔂", + "arrow_forward": "▶️", + "fast_forward": "⏩", + "next_track_button": "⏭️", + "play_or_pause_button": "⏯️", + "arrow_backward": "◀️", + "rewind": "⏪", + "previous_track_button": "⏮️", + "arrow_up_small": "🔼", + "arrow_double_up": "⏫", + "arrow_down_small": "🔽", + "arrow_double_down": "⏬", + "pause_button": "⏸️", + "stop_button": "⏹️", + "record_button": "⏺️", + "eject_button": "⏏️", + "cinema": "🎦", + "low_brightness": "🔅", + "high_brightness": "🔆", + "signal_strength": "📶", + "wireless": "🛜", + "vibration_mode": "📳", + "mobile_phone_off": "📴", + "female_sign": "♀️", + "male_sign": "♂️", + "transgender_symbol": "⚧️", + "heavy_multiplication_x": "✖️", + "heavy_plus_sign": "➕", + "heavy_minus_sign": "➖", + "heavy_division_sign": "➗", + "heavy_equals_sign": "🟰", + "infinity": "♾️", + "bangbang": "‼️", + "interrobang": "⁉️", + "question": "❓", + "grey_question": "❔", + "grey_exclamation": "❕", + "exclamation": "❗", + "heavy_exclamation_mark": "❗", + "wavy_dash": "〰️", + "currency_exchange": "💱", + "heavy_dollar_sign": "💲", + "medical_symbol": "⚕️", + "recycle": "♻️", + "fleur_de_lis": "⚜️", + "trident": "🔱", + "name_badge": "📛", + "beginner": "🔰", + "o": "⭕", + "white_check_mark": "✅", + "ballot_box_with_check": "☑️", + "heavy_check_mark": "✔️", + "x": "❌", + "negative_squared_cross_mark": "❎", + "curly_loop": "➰", + "loop": "➿", + "part_alternation_mark": "〽️", + "eight_spoked_asterisk": "✳️", + "eight_pointed_black_star": "✴️", + "sparkle": "❇️", + "copyright": "©️", + "registered": "®️", + "tm": "™️", + "hash": "#️⃣", + "asterisk": "*️⃣", + "zero": "0️⃣", + "one": "1️⃣", + "two": "2️⃣", + "three": "3️⃣", + "four": "4️⃣", + "five": "5️⃣", + "six": "6️⃣", + "seven": "7️⃣", + "eight": "8️⃣", + "nine": "9️⃣", + "keycap_ten": "🔟", + "capital_abcd": "🔠", + "abcd": "🔡", + "symbols": "🔣", + "abc": "🔤", + "a": "🅰️", + "ab": "🆎", + "b": "🅱️", + "cl": "🆑", + "cool": "🆒", + "free": "🆓", + "information_source": "ℹ️", + "id": "🆔", + "m": "Ⓜ️", + "new": "🆕", + "ng": "🆖", + "o2": "🅾️", + "ok": "🆗", + "parking": "🅿️", + "sos": "🆘", + "up": "🆙", + "vs": "🆚", + "koko": "🈁", + "sa": "🈂️", + "ideograph_advantage": "🉐", + "accept": "🉑", + "congratulations": "㊗️", + "secret": "㊙️", + "u6e80": "🈵", + "red_circle": "🔴", + "orange_circle": "🟠", + "yellow_circle": "🟡", + "green_circle": "🟢", + "large_blue_circle": "🔵", + "purple_circle": "🟣", + "brown_circle": "🟤", + "black_circle": "⚫", + "white_circle": "⚪", + "red_square": "🟥", + "orange_square": "🟧", + "yellow_square": "🟨", + "green_square": "🟩", + "blue_square": "🟦", + "purple_square": "🟪", + "brown_square": "🟫", + "black_large_square": "⬛", + "white_large_square": "⬜", + "black_medium_square": "◼️", + "white_medium_square": "◻️", + "black_medium_small_square": "◾", + "white_medium_small_square": "◽", + "black_small_square": "▪️", + "white_small_square": "▫️", + "large_orange_diamond": "🔶", + "large_blue_diamond": "🔷", + "small_orange_diamond": "🔸", + "small_blue_diamond": "🔹", + "small_red_triangle": "🔺", + "small_red_triangle_down": "🔻", + "diamond_shape_with_a_dot_inside": "💠", + "radio_button": "🔘", + "white_square_button": "🔳", + "black_square_button": "🔲", + "checkered_flag": "🏁", + "triangular_flag_on_post": "🚩", + "crossed_flags": "🎌", + "black_flag": "🏴", + "white_flag": "🏳️", + "rainbow_flag": "🏳️‍🌈", + "transgender_flag": "🏳️‍⚧️", + "pirate_flag": "🏴‍☠️", + "ascension_island": "🇦🇨", + "andorra": "🇦🇩", + "united_arab_emirates": "🇦🇪", + "afghanistan": "🇦🇫", + "antigua_barbuda": "🇦🇬", + "anguilla": "🇦🇮", + "albania": "🇦🇱", + "armenia": "🇦🇲", + "angola": "🇦🇴", + "antarctica": "🇦🇶", + "argentina": "🇦🇷", + "american_samoa": "🇦🇸", + "austria": "🇦🇹", + "australia": "🇦🇺", + "aruba": "🇦🇼", + "aland_islands": "🇦🇽", + "azerbaijan": "🇦🇿", + "bosnia_herzegovina": "🇧🇦", + "barbados": "🇧🇧", + "bangladesh": "🇧🇩", + "belgium": "🇧🇪", + "burkina_faso": "🇧🇫", + "bulgaria": "🇧🇬", + "bahrain": "🇧🇭", + "burundi": "🇧🇮", + "benin": "🇧🇯", + "st_barthelemy": "🇧🇱", + "bermuda": "🇧🇲", + "brunei": "🇧🇳", + "bolivia": "🇧🇴", + "caribbean_netherlands": "🇧🇶", + "brazil": "🇧🇷", + "bahamas": "🇧🇸", + "bhutan": "🇧🇹", + "bouvet_island": "🇧🇻", + "botswana": "🇧🇼", + "belarus": "🇧🇾", + "belize": "🇧🇿", + "canada": "🇨🇦", + "cocos_islands": "🇨🇨", + "congo_kinshasa": "🇨🇩", + "central_african_republic": "🇨🇫", + "congo_brazzaville": "🇨🇬", + "switzerland": "🇨🇭", + "cote_divoire": "🇨🇮", + "cook_islands": "🇨🇰", + "chile": "🇨🇱", + "cameroon": "🇨🇲", + "cn": "🇨🇳", + "colombia": "🇨🇴", + "clipperton_island": "🇨🇵", + "costa_rica": "🇨🇷", + "cuba": "🇨🇺", + "cape_verde": "🇨🇻", + "curacao": "🇨🇼", + "christmas_island": "🇨🇽", + "cyprus": "🇨🇾", + "czech_republic": "🇨🇿", + "de": "🇩🇪", + "diego_garcia": "🇩🇬", + "djibouti": "🇩🇯", + "denmark": "🇩🇰", + "dominica": "🇩🇲", + "dominican_republic": "🇩🇴", + "algeria": "🇩🇿", + "ceuta_melilla": "🇪🇦", + "ecuador": "🇪🇨", + "estonia": "🇪🇪", + "egypt": "🇪🇬", + "western_sahara": "🇪🇭", + "eritrea": "🇪🇷", + "es": "🇪🇸", + "ethiopia": "🇪🇹", + "eu": "🇪🇺", + "european_union": "🇪🇺", + "finland": "🇫🇮", + "fiji": "🇫🇯", + "falkland_islands": "🇫🇰", + "micronesia": "🇫🇲", + "faroe_islands": "🇫🇴", + "fr": "🇫🇷", + "gabon": "🇬🇦", + "gb": "🇬🇧", + "uk": "🇬🇧", + "grenada": "🇬🇩", + "georgia": "🇬🇪", + "french_guiana": "🇬🇫", + "guernsey": "🇬🇬", + "ghana": "🇬🇭", + "gibraltar": "🇬🇮", + "greenland": "🇬🇱", + "gambia": "🇬🇲", + "guinea": "🇬🇳", + "guadeloupe": "🇬🇵", + "equatorial_guinea": "🇬🇶", + "greece": "🇬🇷", + "south_georgia_south_sandwich_islands": "🇬🇸", + "guatemala": "🇬🇹", + "guam": "🇬🇺", + "guinea_bissau": "🇬🇼", + "guyana": "🇬🇾", + "hong_kong": "🇭🇰", + "heard_mcdonald_islands": "🇭🇲", + "honduras": "🇭🇳", + "croatia": "🇭🇷", + "haiti": "🇭🇹", + "hungary": "🇭🇺", + "canary_islands": "🇮🇨", + "indonesia": "🇮🇩", + "ireland": "🇮🇪", + "israel": "🇮🇱", + "isle_of_man": "🇮🇲", + "india": "🇮🇳", + "british_indian_ocean_territory": "🇮🇴", + "iraq": "🇮🇶", + "iran": "🇮🇷", + "iceland": "🇮🇸", + "it": "🇮🇹", + "jersey": "🇯🇪", + "jamaica": "🇯🇲", + "jordan": "🇯🇴", + "jp": "🇯🇵", + "kenya": "🇰🇪", + "kyrgyzstan": "🇰🇬", + "cambodia": "🇰🇭", + "kiribati": "🇰🇮", + "comoros": "🇰🇲", + "st_kitts_nevis": "🇰🇳", + "north_korea": "🇰🇵", + "kr": "🇰🇷", + "kuwait": "🇰🇼", + "cayman_islands": "🇰🇾", + "kazakhstan": "🇰🇿", + "laos": "🇱🇦", + "lebanon": "🇱🇧", + "st_lucia": "🇱🇨", + "liechtenstein": "🇱🇮", + "sri_lanka": "🇱🇰", + "liberia": "🇱🇷", + "lesotho": "🇱🇸", + "lithuania": "🇱🇹", + "luxembourg": "🇱🇺", + "latvia": "🇱🇻", + "libya": "🇱🇾", + "morocco": "🇲🇦", + "monaco": "🇲🇨", + "moldova": "🇲🇩", + "montenegro": "🇲🇪", + "st_martin": "🇲🇫", + "madagascar": "🇲🇬", + "marshall_islands": "🇲🇭", + "macedonia": "🇲🇰", + "mali": "🇲🇱", + "myanmar": "🇲🇲", + "mongolia": "🇲🇳", + "macau": "🇲🇴", + "northern_mariana_islands": "🇲🇵", + "martinique": "🇲🇶", + "mauritania": "🇲🇷", + "montserrat": "🇲🇸", + "malta": "🇲🇹", + "mauritius": "🇲🇺", + "maldives": "🇲🇻", + "malawi": "🇲🇼", + "mexico": "🇲🇽", + "malaysia": "🇲🇾", + "mozambique": "🇲🇿", + "namibia": "🇳🇦", + "new_caledonia": "🇳🇨", + "niger": "🇳🇪", + "norfolk_island": "🇳🇫", + "nigeria": "🇳🇬", + "nicaragua": "🇳🇮", + "netherlands": "🇳🇱", + "norway": "🇳🇴", + "nepal": "🇳🇵", + "nauru": "🇳🇷", + "niue": "🇳🇺", + "new_zealand": "🇳🇿", + "oman": "🇴🇲", + "panama": "🇵🇦", + "peru": "🇵🇪", + "french_polynesia": "🇵🇫", + "papua_new_guinea": "🇵🇬", + "philippines": "🇵🇭", + "pakistan": "🇵🇰", + "poland": "🇵🇱", + "st_pierre_miquelon": "🇵🇲", + "pitcairn_islands": "🇵🇳", + "puerto_rico": "🇵🇷", + "palestinian_territories": "🇵🇸", + "portugal": "🇵🇹", + "palau": "🇵🇼", + "paraguay": "🇵🇾", + "qatar": "🇶🇦", + "reunion": "🇷🇪", + "romania": "🇷🇴", + "serbia": "🇷🇸", + "ru": "🇷🇺", + "rwanda": "🇷🇼", + "saudi_arabia": "🇸🇦", + "solomon_islands": "🇸🇧", + "seychelles": "🇸🇨", + "sudan": "🇸🇩", + "sweden": "🇸🇪", + "singapore": "🇸🇬", + "st_helena": "🇸🇭", + "slovenia": "🇸🇮", + "svalbard_jan_mayen": "🇸🇯", + "slovakia": "🇸🇰", + "sierra_leone": "🇸🇱", + "san_marino": "🇸🇲", + "senegal": "🇸🇳", + "somalia": "🇸🇴", + "suriname": "🇸🇷", + "south_sudan": "🇸🇸", + "sao_tome_principe": "🇸🇹", + "el_salvador": "🇸🇻", + "sint_maarten": "🇸🇽", + "syria": "🇸🇾", + "swaziland": "🇸🇿", + "tristan_da_cunha": "🇹🇦", + "turks_caicos_islands": "🇹🇨", + "chad": "🇹🇩", + "french_southern_territories": "🇹🇫", + "togo": "🇹🇬", + "thailand": "🇹🇭", + "tajikistan": "🇹🇯", + "tokelau": "🇹🇰", + "timor_leste": "🇹🇱", + "turkmenistan": "🇹🇲", + "tunisia": "🇹🇳", + "tonga": "🇹🇴", + "tr": "🇹🇷", + "trinidad_tobago": "🇹🇹", + "tuvalu": "🇹🇻", + "taiwan": "🇹🇼", + "tanzania": "🇹🇿", + "ukraine": "🇺🇦", + "uganda": "🇺🇬", + "us_outlying_islands": "🇺🇲", + "united_nations": "🇺🇳", + "us": "🇺🇸", + "uruguay": "🇺🇾", + "uzbekistan": "🇺🇿", + "vatican_city": "🇻🇦", + "st_vincent_grenadines": "🇻🇨", + "venezuela": "🇻🇪", + "british_virgin_islands": "🇻🇬", + "us_virgin_islands": "🇻🇮", + "vietnam": "🇻🇳", + "vanuatu": "🇻🇺", + "wallis_futuna": "🇼🇫", + "samoa": "🇼🇸", + "kosovo": "🇽🇰", + "yemen": "🇾🇪", + "mayotte": "🇾🇹", + "south_africa": "🇿🇦", + "zambia": "🇿🇲", + "zimbabwe": "🇿🇼", + "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", + "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", + "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿" +}; + + +export interface EmojiShortcuts { + [key: string]: string | string[]; +} + +const shortcuts: EmojiShortcuts = { + angry: ['>:(', '>:-('], + blush: [':")', ':-")'], + broken_heart: [' 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 deleted file mode 100644 index 925679d..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-deflist/index.ts +++ /dev/null @@ -1,209 +0,0 @@ -// 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-emojis/index.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/index.ts deleted file mode 100644 index ca69b59..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { default as bare } from './lib/bare'; -export { default as light } from './lib/light'; -export { default as full } from './lib/full'; - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/bare.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/bare.ts deleted file mode 100644 index 2d7669f..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/bare.ts +++ /dev/null @@ -1,26 +0,0 @@ -import MarkdownIt from 'markdown-it'; -import emoji_html from './render'; -import emoji_replace from './replace'; -import normalize_opts, { EmojiOptions } from './normalize_opts'; - -/** - * Bare emoji 插件(不包含预定义的 emoji 数据) - */ -export default function emoji_plugin(md: MarkdownIt, options?: Partial): void { - const defaults: EmojiOptions = { - defs: {}, - shortcuts: {}, - enabled: [] - }; - - const opts = normalize_opts(md.utils.assign({}, defaults, options || {}) as EmojiOptions); - - md.renderer.rules.emoji = emoji_html; - - md.core.ruler.after( - 'linkify', - 'emoji', - emoji_replace(md, opts.defs, opts.shortcuts, opts.scanRE, opts.replaceRE) - ); -} - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/full.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/full.ts deleted file mode 100644 index 6e6a097..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/full.ts +++ /dev/null @@ -1,1910 +0,0 @@ -// Generated, don't edit -import { EmojiDefs } from '../normalize_opts'; - -const emojies: EmojiDefs = { - "100": "💯", - "1234": "🔢", - "grinning": "😀", - "smiley": "😃", - "smile": "😄", - "grin": "😁", - "laughing": "😆", - "satisfied": "😆", - "sweat_smile": "😅", - "rofl": "🤣", - "joy": "😂", - "slightly_smiling_face": "🙂", - "upside_down_face": "🙃", - "melting_face": "🫠", - "wink": "😉", - "blush": "😊", - "innocent": "😇", - "smiling_face_with_three_hearts": "🥰", - "heart_eyes": "😍", - "star_struck": "🤩", - "kissing_heart": "😘", - "kissing": "😗", - "relaxed": "☺️", - "kissing_closed_eyes": "😚", - "kissing_smiling_eyes": "😙", - "smiling_face_with_tear": "🥲", - "yum": "😋", - "stuck_out_tongue": "😛", - "stuck_out_tongue_winking_eye": "😜", - "zany_face": "🤪", - "stuck_out_tongue_closed_eyes": "😝", - "money_mouth_face": "🤑", - "hugs": "🤗", - "hand_over_mouth": "🤭", - "face_with_open_eyes_and_hand_over_mouth": "🫢", - "face_with_peeking_eye": "🫣", - "shushing_face": "🤫", - "thinking": "🤔", - "saluting_face": "🫡", - "zipper_mouth_face": "🤐", - "raised_eyebrow": "🤨", - "neutral_face": "😐", - "expressionless": "😑", - "no_mouth": "😶", - "dotted_line_face": "🫥", - "face_in_clouds": "😶‍🌫️", - "smirk": "😏", - "unamused": "😒", - "roll_eyes": "🙄", - "grimacing": "😬", - "face_exhaling": "😮‍💨", - "lying_face": "🤥", - "shaking_face": "🫨", - "relieved": "😌", - "pensive": "😔", - "sleepy": "😪", - "drooling_face": "🤤", - "sleeping": "😴", - "mask": "😷", - "face_with_thermometer": "🤒", - "face_with_head_bandage": "🤕", - "nauseated_face": "🤢", - "vomiting_face": "🤮", - "sneezing_face": "🤧", - "hot_face": "🥵", - "cold_face": "🥶", - "woozy_face": "🥴", - "dizzy_face": "😵", - "face_with_spiral_eyes": "😵‍💫", - "exploding_head": "🤯", - "cowboy_hat_face": "🤠", - "partying_face": "🥳", - "disguised_face": "🥸", - "sunglasses": "😎", - "nerd_face": "🤓", - "monocle_face": "🧐", - "confused": "😕", - "face_with_diagonal_mouth": "🫤", - "worried": "😟", - "slightly_frowning_face": "🙁", - "frowning_face": "☹️", - "open_mouth": "😮", - "hushed": "😯", - "astonished": "😲", - "flushed": "😳", - "pleading_face": "🥺", - "face_holding_back_tears": "🥹", - "frowning": "😦", - "anguished": "😧", - "fearful": "😨", - "cold_sweat": "😰", - "disappointed_relieved": "😥", - "cry": "😢", - "sob": "😭", - "scream": "😱", - "confounded": "😖", - "persevere": "😣", - "disappointed": "😞", - "sweat": "😓", - "weary": "😩", - "tired_face": "😫", - "yawning_face": "🥱", - "triumph": "😤", - "rage": "😡", - "pout": "😡", - "angry": "😠", - "cursing_face": "🤬", - "smiling_imp": "😈", - "imp": "👿", - "skull": "💀", - "skull_and_crossbones": "☠️", - "hankey": "💩", - "poop": "💩", - "shit": "💩", - "clown_face": "🤡", - "japanese_ogre": "👹", - "japanese_goblin": "👺", - "ghost": "👻", - "alien": "👽", - "space_invader": "👾", - "robot": "🤖", - "smiley_cat": "😺", - "smile_cat": "😸", - "joy_cat": "😹", - "heart_eyes_cat": "😻", - "smirk_cat": "😼", - "kissing_cat": "😽", - "scream_cat": "🙀", - "crying_cat_face": "😿", - "pouting_cat": "😾", - "see_no_evil": "🙈", - "hear_no_evil": "🙉", - "speak_no_evil": "🙊", - "love_letter": "💌", - "cupid": "💘", - "gift_heart": "💝", - "sparkling_heart": "💖", - "heartpulse": "💗", - "heartbeat": "💓", - "revolving_hearts": "💞", - "two_hearts": "💕", - "heart_decoration": "💟", - "heavy_heart_exclamation": "❣️", - "broken_heart": "💔", - "heart_on_fire": "❤️‍🔥", - "mending_heart": "❤️‍🩹", - "heart": "❤️", - "pink_heart": "🩷", - "orange_heart": "🧡", - "yellow_heart": "💛", - "green_heart": "💚", - "blue_heart": "💙", - "light_blue_heart": "🩵", - "purple_heart": "💜", - "brown_heart": "🤎", - "black_heart": "🖤", - "grey_heart": "🩶", - "white_heart": "🤍", - "kiss": "💋", - "anger": "💢", - "boom": "💥", - "collision": "💥", - "dizzy": "💫", - "sweat_drops": "💦", - "dash": "💨", - "hole": "🕳️", - "speech_balloon": "💬", - "eye_speech_bubble": "👁️‍🗨️", - "left_speech_bubble": "🗨️", - "right_anger_bubble": "🗯️", - "thought_balloon": "💭", - "zzz": "💤", - "wave": "👋", - "raised_back_of_hand": "🤚", - "raised_hand_with_fingers_splayed": "🖐️", - "hand": "✋", - "raised_hand": "✋", - "vulcan_salute": "🖖", - "rightwards_hand": "🫱", - "leftwards_hand": "🫲", - "palm_down_hand": "🫳", - "palm_up_hand": "🫴", - "leftwards_pushing_hand": "🫷", - "rightwards_pushing_hand": "🫸", - "ok_hand": "👌", - "pinched_fingers": "🤌", - "pinching_hand": "🤏", - "v": "✌️", - "crossed_fingers": "🤞", - "hand_with_index_finger_and_thumb_crossed": "🫰", - "love_you_gesture": "🤟", - "metal": "🤘", - "call_me_hand": "🤙", - "point_left": "👈", - "point_right": "👉", - "point_up_2": "👆", - "middle_finger": "🖕", - "fu": "🖕", - "point_down": "👇", - "point_up": "☝️", - "index_pointing_at_the_viewer": "🫵", - "+1": "👍", - "thumbsup": "👍", - "-1": "👎", - "thumbsdown": "👎", - "fist_raised": "✊", - "fist": "✊", - "fist_oncoming": "👊", - "facepunch": "👊", - "punch": "👊", - "fist_left": "🤛", - "fist_right": "🤜", - "clap": "👏", - "raised_hands": "🙌", - "heart_hands": "🫶", - "open_hands": "👐", - "palms_up_together": "🤲", - "handshake": "🤝", - "pray": "🙏", - "writing_hand": "✍️", - "nail_care": "💅", - "selfie": "🤳", - "muscle": "💪", - "mechanical_arm": "🦾", - "mechanical_leg": "🦿", - "leg": "🦵", - "foot": "🦶", - "ear": "👂", - "ear_with_hearing_aid": "🦻", - "nose": "👃", - "brain": "🧠", - "anatomical_heart": "🫀", - "lungs": "🫁", - "tooth": "🦷", - "bone": "🦴", - "eyes": "👀", - "eye": "👁️", - "tongue": "👅", - "lips": "👄", - "biting_lip": "🫦", - "baby": "👶", - "child": "🧒", - "boy": "👦", - "girl": "👧", - "adult": "🧑", - "blond_haired_person": "👱", - "man": "👨", - "bearded_person": "🧔", - "man_beard": "🧔‍♂️", - "woman_beard": "🧔‍♀️", - "red_haired_man": "👨‍🦰", - "curly_haired_man": "👨‍🦱", - "white_haired_man": "👨‍🦳", - "bald_man": "👨‍🦲", - "woman": "👩", - "red_haired_woman": "👩‍🦰", - "person_red_hair": "🧑‍🦰", - "curly_haired_woman": "👩‍🦱", - "person_curly_hair": "🧑‍🦱", - "white_haired_woman": "👩‍🦳", - "person_white_hair": "🧑‍🦳", - "bald_woman": "👩‍🦲", - "person_bald": "🧑‍🦲", - "blond_haired_woman": "👱‍♀️", - "blonde_woman": "👱‍♀️", - "blond_haired_man": "👱‍♂️", - "older_adult": "🧓", - "older_man": "👴", - "older_woman": "👵", - "frowning_person": "🙍", - "frowning_man": "🙍‍♂️", - "frowning_woman": "🙍‍♀️", - "pouting_face": "🙎", - "pouting_man": "🙎‍♂️", - "pouting_woman": "🙎‍♀️", - "no_good": "🙅", - "no_good_man": "🙅‍♂️", - "ng_man": "🙅‍♂️", - "no_good_woman": "🙅‍♀️", - "ng_woman": "🙅‍♀️", - "ok_person": "🙆", - "ok_man": "🙆‍♂️", - "ok_woman": "🙆‍♀️", - "tipping_hand_person": "💁", - "information_desk_person": "💁", - "tipping_hand_man": "💁‍♂️", - "sassy_man": "💁‍♂️", - "tipping_hand_woman": "💁‍♀️", - "sassy_woman": "💁‍♀️", - "raising_hand": "🙋", - "raising_hand_man": "🙋‍♂️", - "raising_hand_woman": "🙋‍♀️", - "deaf_person": "🧏", - "deaf_man": "🧏‍♂️", - "deaf_woman": "🧏‍♀️", - "bow": "🙇", - "bowing_man": "🙇‍♂️", - "bowing_woman": "🙇‍♀️", - "facepalm": "🤦", - "man_facepalming": "🤦‍♂️", - "woman_facepalming": "🤦‍♀️", - "shrug": "🤷", - "man_shrugging": "🤷‍♂️", - "woman_shrugging": "🤷‍♀️", - "health_worker": "🧑‍⚕️", - "man_health_worker": "👨‍⚕️", - "woman_health_worker": "👩‍⚕️", - "student": "🧑‍🎓", - "man_student": "👨‍🎓", - "woman_student": "👩‍🎓", - "teacher": "🧑‍🏫", - "man_teacher": "👨‍🏫", - "woman_teacher": "👩‍🏫", - "judge": "🧑‍⚖️", - "man_judge": "👨‍⚖️", - "woman_judge": "👩‍⚖️", - "farmer": "🧑‍🌾", - "man_farmer": "👨‍🌾", - "woman_farmer": "👩‍🌾", - "cook": "🧑‍🍳", - "man_cook": "👨‍🍳", - "woman_cook": "👩‍🍳", - "mechanic": "🧑‍🔧", - "man_mechanic": "👨‍🔧", - "woman_mechanic": "👩‍🔧", - "factory_worker": "🧑‍🏭", - "man_factory_worker": "👨‍🏭", - "woman_factory_worker": "👩‍🏭", - "office_worker": "🧑‍💼", - "man_office_worker": "👨‍💼", - "woman_office_worker": "👩‍💼", - "scientist": "🧑‍🔬", - "man_scientist": "👨‍🔬", - "woman_scientist": "👩‍🔬", - "technologist": "🧑‍💻", - "man_technologist": "👨‍💻", - "woman_technologist": "👩‍💻", - "singer": "🧑‍🎤", - "man_singer": "👨‍🎤", - "woman_singer": "👩‍🎤", - "artist": "🧑‍🎨", - "man_artist": "👨‍🎨", - "woman_artist": "👩‍🎨", - "pilot": "🧑‍✈️", - "man_pilot": "👨‍✈️", - "woman_pilot": "👩‍✈️", - "astronaut": "🧑‍🚀", - "man_astronaut": "👨‍🚀", - "woman_astronaut": "👩‍🚀", - "firefighter": "🧑‍🚒", - "man_firefighter": "👨‍🚒", - "woman_firefighter": "👩‍🚒", - "police_officer": "👮", - "cop": "👮", - "policeman": "👮‍♂️", - "policewoman": "👮‍♀️", - "detective": "🕵️", - "male_detective": "🕵️‍♂️", - "female_detective": "🕵️‍♀️", - "guard": "💂", - "guardsman": "💂‍♂️", - "guardswoman": "💂‍♀️", - "ninja": "🥷", - "construction_worker": "👷", - "construction_worker_man": "👷‍♂️", - "construction_worker_woman": "👷‍♀️", - "person_with_crown": "🫅", - "prince": "🤴", - "princess": "👸", - "person_with_turban": "👳", - "man_with_turban": "👳‍♂️", - "woman_with_turban": "👳‍♀️", - "man_with_gua_pi_mao": "👲", - "woman_with_headscarf": "🧕", - "person_in_tuxedo": "🤵", - "man_in_tuxedo": "🤵‍♂️", - "woman_in_tuxedo": "🤵‍♀️", - "person_with_veil": "👰", - "man_with_veil": "👰‍♂️", - "woman_with_veil": "👰‍♀️", - "bride_with_veil": "👰‍♀️", - "pregnant_woman": "🤰", - "pregnant_man": "🫃", - "pregnant_person": "🫄", - "breast_feeding": "🤱", - "woman_feeding_baby": "👩‍🍼", - "man_feeding_baby": "👨‍🍼", - "person_feeding_baby": "🧑‍🍼", - "angel": "👼", - "santa": "🎅", - "mrs_claus": "🤶", - "mx_claus": "🧑‍🎄", - "superhero": "🦸", - "superhero_man": "🦸‍♂️", - "superhero_woman": "🦸‍♀️", - "supervillain": "🦹", - "supervillain_man": "🦹‍♂️", - "supervillain_woman": "🦹‍♀️", - "mage": "🧙", - "mage_man": "🧙‍♂️", - "mage_woman": "🧙‍♀️", - "fairy": "🧚", - "fairy_man": "🧚‍♂️", - "fairy_woman": "🧚‍♀️", - "vampire": "🧛", - "vampire_man": "🧛‍♂️", - "vampire_woman": "🧛‍♀️", - "merperson": "🧜", - "merman": "🧜‍♂️", - "mermaid": "🧜‍♀️", - "elf": "🧝", - "elf_man": "🧝‍♂️", - "elf_woman": "🧝‍♀️", - "genie": "🧞", - "genie_man": "🧞‍♂️", - "genie_woman": "🧞‍♀️", - "zombie": "🧟", - "zombie_man": "🧟‍♂️", - "zombie_woman": "🧟‍♀️", - "troll": "🧌", - "massage": "💆", - "massage_man": "💆‍♂️", - "massage_woman": "💆‍♀️", - "haircut": "💇", - "haircut_man": "💇‍♂️", - "haircut_woman": "💇‍♀️", - "walking": "🚶", - "walking_man": "🚶‍♂️", - "walking_woman": "🚶‍♀️", - "standing_person": "🧍", - "standing_man": "🧍‍♂️", - "standing_woman": "🧍‍♀️", - "kneeling_person": "🧎", - "kneeling_man": "🧎‍♂️", - "kneeling_woman": "🧎‍♀️", - "person_with_probing_cane": "🧑‍🦯", - "man_with_probing_cane": "👨‍🦯", - "woman_with_probing_cane": "👩‍🦯", - "person_in_motorized_wheelchair": "🧑‍🦼", - "man_in_motorized_wheelchair": "👨‍🦼", - "woman_in_motorized_wheelchair": "👩‍🦼", - "person_in_manual_wheelchair": "🧑‍🦽", - "man_in_manual_wheelchair": "👨‍🦽", - "woman_in_manual_wheelchair": "👩‍🦽", - "runner": "🏃", - "running": "🏃", - "running_man": "🏃‍♂️", - "running_woman": "🏃‍♀️", - "woman_dancing": "💃", - "dancer": "💃", - "man_dancing": "🕺", - "business_suit_levitating": "🕴️", - "dancers": "👯", - "dancing_men": "👯‍♂️", - "dancing_women": "👯‍♀️", - "sauna_person": "🧖", - "sauna_man": "🧖‍♂️", - "sauna_woman": "🧖‍♀️", - "climbing": "🧗", - "climbing_man": "🧗‍♂️", - "climbing_woman": "🧗‍♀️", - "person_fencing": "🤺", - "horse_racing": "🏇", - "skier": "⛷️", - "snowboarder": "🏂", - "golfing": "🏌️", - "golfing_man": "🏌️‍♂️", - "golfing_woman": "🏌️‍♀️", - "surfer": "🏄", - "surfing_man": "🏄‍♂️", - "surfing_woman": "🏄‍♀️", - "rowboat": "🚣", - "rowing_man": "🚣‍♂️", - "rowing_woman": "🚣‍♀️", - "swimmer": "🏊", - "swimming_man": "🏊‍♂️", - "swimming_woman": "🏊‍♀️", - "bouncing_ball_person": "⛹️", - "bouncing_ball_man": "⛹️‍♂️", - "basketball_man": "⛹️‍♂️", - "bouncing_ball_woman": "⛹️‍♀️", - "basketball_woman": "⛹️‍♀️", - "weight_lifting": "🏋️", - "weight_lifting_man": "🏋️‍♂️", - "weight_lifting_woman": "🏋️‍♀️", - "bicyclist": "🚴", - "biking_man": "🚴‍♂️", - "biking_woman": "🚴‍♀️", - "mountain_bicyclist": "🚵", - "mountain_biking_man": "🚵‍♂️", - "mountain_biking_woman": "🚵‍♀️", - "cartwheeling": "🤸", - "man_cartwheeling": "🤸‍♂️", - "woman_cartwheeling": "🤸‍♀️", - "wrestling": "🤼", - "men_wrestling": "🤼‍♂️", - "women_wrestling": "🤼‍♀️", - "water_polo": "🤽", - "man_playing_water_polo": "🤽‍♂️", - "woman_playing_water_polo": "🤽‍♀️", - "handball_person": "🤾", - "man_playing_handball": "🤾‍♂️", - "woman_playing_handball": "🤾‍♀️", - "juggling_person": "🤹", - "man_juggling": "🤹‍♂️", - "woman_juggling": "🤹‍♀️", - "lotus_position": "🧘", - "lotus_position_man": "🧘‍♂️", - "lotus_position_woman": "🧘‍♀️", - "bath": "🛀", - "sleeping_bed": "🛌", - "people_holding_hands": "🧑‍🤝‍🧑", - "two_women_holding_hands": "👭", - "couple": "👫", - "two_men_holding_hands": "👬", - "couplekiss": "💏", - "couplekiss_man_woman": "👩‍❤️‍💋‍👨", - "couplekiss_man_man": "👨‍❤️‍💋‍👨", - "couplekiss_woman_woman": "👩‍❤️‍💋‍👩", - "couple_with_heart": "💑", - "couple_with_heart_woman_man": "👩‍❤️‍👨", - "couple_with_heart_man_man": "👨‍❤️‍👨", - "couple_with_heart_woman_woman": "👩‍❤️‍👩", - "family": "👪", - "family_man_woman_boy": "👨‍👩‍👦", - "family_man_woman_girl": "👨‍👩‍👧", - "family_man_woman_girl_boy": "👨‍👩‍👧‍👦", - "family_man_woman_boy_boy": "👨‍👩‍👦‍👦", - "family_man_woman_girl_girl": "👨‍👩‍👧‍👧", - "family_man_man_boy": "👨‍👨‍👦", - "family_man_man_girl": "👨‍👨‍👧", - "family_man_man_girl_boy": "👨‍👨‍👧‍👦", - "family_man_man_boy_boy": "👨‍👨‍👦‍👦", - "family_man_man_girl_girl": "👨‍👨‍👧‍👧", - "family_woman_woman_boy": "👩‍👩‍👦", - "family_woman_woman_girl": "👩‍👩‍👧", - "family_woman_woman_girl_boy": "👩‍👩‍👧‍👦", - "family_woman_woman_boy_boy": "👩‍👩‍👦‍👦", - "family_woman_woman_girl_girl": "👩‍👩‍👧‍👧", - "family_man_boy": "👨‍👦", - "family_man_boy_boy": "👨‍👦‍👦", - "family_man_girl": "👨‍👧", - "family_man_girl_boy": "👨‍👧‍👦", - "family_man_girl_girl": "👨‍👧‍👧", - "family_woman_boy": "👩‍👦", - "family_woman_boy_boy": "👩‍👦‍👦", - "family_woman_girl": "👩‍👧", - "family_woman_girl_boy": "👩‍👧‍👦", - "family_woman_girl_girl": "👩‍👧‍👧", - "speaking_head": "🗣️", - "bust_in_silhouette": "👤", - "busts_in_silhouette": "👥", - "people_hugging": "🫂", - "footprints": "👣", - "monkey_face": "🐵", - "monkey": "🐒", - "gorilla": "🦍", - "orangutan": "🦧", - "dog": "🐶", - "dog2": "🐕", - "guide_dog": "🦮", - "service_dog": "🐕‍🦺", - "poodle": "🐩", - "wolf": "🐺", - "fox_face": "🦊", - "raccoon": "🦝", - "cat": "🐱", - "cat2": "🐈", - "black_cat": "🐈‍⬛", - "lion": "🦁", - "tiger": "🐯", - "tiger2": "🐅", - "leopard": "🐆", - "horse": "🐴", - "moose": "🫎", - "donkey": "🫏", - "racehorse": "🐎", - "unicorn": "🦄", - "zebra": "🦓", - "deer": "🦌", - "bison": "🦬", - "cow": "🐮", - "ox": "🐂", - "water_buffalo": "🐃", - "cow2": "🐄", - "pig": "🐷", - "pig2": "🐖", - "boar": "🐗", - "pig_nose": "🐽", - "ram": "🐏", - "sheep": "🐑", - "goat": "🐐", - "dromedary_camel": "🐪", - "camel": "🐫", - "llama": "🦙", - "giraffe": "🦒", - "elephant": "🐘", - "mammoth": "🦣", - "rhinoceros": "🦏", - "hippopotamus": "🦛", - "mouse": "🐭", - "mouse2": "🐁", - "rat": "🐀", - "hamster": "🐹", - "rabbit": "🐰", - "rabbit2": "🐇", - "chipmunk": "🐿️", - "beaver": "🦫", - "hedgehog": "🦔", - "bat": "🦇", - "bear": "🐻", - "polar_bear": "🐻‍❄️", - "koala": "🐨", - "panda_face": "🐼", - "sloth": "🦥", - "otter": "🦦", - "skunk": "🦨", - "kangaroo": "🦘", - "badger": "🦡", - "feet": "🐾", - "paw_prints": "🐾", - "turkey": "🦃", - "chicken": "🐔", - "rooster": "🐓", - "hatching_chick": "🐣", - "baby_chick": "🐤", - "hatched_chick": "🐥", - "bird": "🐦", - "penguin": "🐧", - "dove": "🕊️", - "eagle": "🦅", - "duck": "🦆", - "swan": "🦢", - "owl": "🦉", - "dodo": "🦤", - "feather": "🪶", - "flamingo": "🦩", - "peacock": "🦚", - "parrot": "🦜", - "wing": "🪽", - "black_bird": "🐦‍⬛", - "goose": "🪿", - "frog": "🐸", - "crocodile": "🐊", - "turtle": "🐢", - "lizard": "🦎", - "snake": "🐍", - "dragon_face": "🐲", - "dragon": "🐉", - "sauropod": "🦕", - "t-rex": "🦖", - "whale": "🐳", - "whale2": "🐋", - "dolphin": "🐬", - "flipper": "🐬", - "seal": "🦭", - "fish": "🐟", - "tropical_fish": "🐠", - "blowfish": "🐡", - "shark": "🦈", - "octopus": "🐙", - "shell": "🐚", - "coral": "🪸", - "jellyfish": "🪼", - "snail": "🐌", - "butterfly": "🦋", - "bug": "🐛", - "ant": "🐜", - "bee": "🐝", - "honeybee": "🐝", - "beetle": "🪲", - "lady_beetle": "🐞", - "cricket": "🦗", - "cockroach": "🪳", - "spider": "🕷️", - "spider_web": "🕸️", - "scorpion": "🦂", - "mosquito": "🦟", - "fly": "🪰", - "worm": "🪱", - "microbe": "🦠", - "bouquet": "💐", - "cherry_blossom": "🌸", - "white_flower": "💮", - "lotus": "🪷", - "rosette": "🏵️", - "rose": "🌹", - "wilted_flower": "🥀", - "hibiscus": "🌺", - "sunflower": "🌻", - "blossom": "🌼", - "tulip": "🌷", - "hyacinth": "🪻", - "seedling": "🌱", - "potted_plant": "🪴", - "evergreen_tree": "🌲", - "deciduous_tree": "🌳", - "palm_tree": "🌴", - "cactus": "🌵", - "ear_of_rice": "🌾", - "herb": "🌿", - "shamrock": "☘️", - "four_leaf_clover": "🍀", - "maple_leaf": "🍁", - "fallen_leaf": "🍂", - "leaves": "🍃", - "empty_nest": "🪹", - "nest_with_eggs": "🪺", - "mushroom": "🍄", - "grapes": "🍇", - "melon": "🍈", - "watermelon": "🍉", - "tangerine": "🍊", - "orange": "🍊", - "mandarin": "🍊", - "lemon": "🍋", - "banana": "🍌", - "pineapple": "🍍", - "mango": "🥭", - "apple": "🍎", - "green_apple": "🍏", - "pear": "🍐", - "peach": "🍑", - "cherries": "🍒", - "strawberry": "🍓", - "blueberries": "🫐", - "kiwi_fruit": "🥝", - "tomato": "🍅", - "olive": "🫒", - "coconut": "🥥", - "avocado": "🥑", - "eggplant": "🍆", - "potato": "🥔", - "carrot": "🥕", - "corn": "🌽", - "hot_pepper": "🌶️", - "bell_pepper": "🫑", - "cucumber": "🥒", - "leafy_green": "🥬", - "broccoli": "🥦", - "garlic": "🧄", - "onion": "🧅", - "peanuts": "🥜", - "beans": "🫘", - "chestnut": "🌰", - "ginger_root": "🫚", - "pea_pod": "🫛", - "bread": "🍞", - "croissant": "🥐", - "baguette_bread": "🥖", - "flatbread": "🫓", - "pretzel": "🥨", - "bagel": "🥯", - "pancakes": "🥞", - "waffle": "🧇", - "cheese": "🧀", - "meat_on_bone": "🍖", - "poultry_leg": "🍗", - "cut_of_meat": "🥩", - "bacon": "🥓", - "hamburger": "🍔", - "fries": "🍟", - "pizza": "🍕", - "hotdog": "🌭", - "sandwich": "🥪", - "taco": "🌮", - "burrito": "🌯", - "tamale": "🫔", - "stuffed_flatbread": "🥙", - "falafel": "🧆", - "egg": "🥚", - "fried_egg": "🍳", - "shallow_pan_of_food": "🥘", - "stew": "🍲", - "fondue": "🫕", - "bowl_with_spoon": "🥣", - "green_salad": "🥗", - "popcorn": "🍿", - "butter": "🧈", - "salt": "🧂", - "canned_food": "🥫", - "bento": "🍱", - "rice_cracker": "🍘", - "rice_ball": "🍙", - "rice": "🍚", - "curry": "🍛", - "ramen": "🍜", - "spaghetti": "🍝", - "sweet_potato": "🍠", - "oden": "🍢", - "sushi": "🍣", - "fried_shrimp": "🍤", - "fish_cake": "🍥", - "moon_cake": "🥮", - "dango": "🍡", - "dumpling": "🥟", - "fortune_cookie": "🥠", - "takeout_box": "🥡", - "crab": "🦀", - "lobster": "🦞", - "shrimp": "🦐", - "squid": "🦑", - "oyster": "🦪", - "icecream": "🍦", - "shaved_ice": "🍧", - "ice_cream": "🍨", - "doughnut": "🍩", - "cookie": "🍪", - "birthday": "🎂", - "cake": "🍰", - "cupcake": "🧁", - "pie": "🥧", - "chocolate_bar": "🍫", - "candy": "🍬", - "lollipop": "🍭", - "custard": "🍮", - "honey_pot": "🍯", - "baby_bottle": "🍼", - "milk_glass": "🥛", - "coffee": "☕", - "teapot": "🫖", - "tea": "🍵", - "sake": "🍶", - "champagne": "🍾", - "wine_glass": "🍷", - "cocktail": "🍸", - "tropical_drink": "🍹", - "beer": "🍺", - "beers": "🍻", - "clinking_glasses": "🥂", - "tumbler_glass": "🥃", - "pouring_liquid": "🫗", - "cup_with_straw": "🥤", - "bubble_tea": "🧋", - "beverage_box": "🧃", - "mate": "🧉", - "ice_cube": "🧊", - "chopsticks": "🥢", - "plate_with_cutlery": "🍽️", - "fork_and_knife": "🍴", - "spoon": "🥄", - "hocho": "🔪", - "knife": "🔪", - "jar": "🫙", - "amphora": "🏺", - "earth_africa": "🌍", - "earth_americas": "🌎", - "earth_asia": "🌏", - "globe_with_meridians": "🌐", - "world_map": "🗺️", - "japan": "🗾", - "compass": "🧭", - "mountain_snow": "🏔️", - "mountain": "⛰️", - "volcano": "🌋", - "mount_fuji": "🗻", - "camping": "🏕️", - "beach_umbrella": "🏖️", - "desert": "🏜️", - "desert_island": "🏝️", - "national_park": "🏞️", - "stadium": "🏟️", - "classical_building": "🏛️", - "building_construction": "🏗️", - "bricks": "🧱", - "rock": "🪨", - "wood": "🪵", - "hut": "🛖", - "houses": "🏘️", - "derelict_house": "🏚️", - "house": "🏠", - "house_with_garden": "🏡", - "office": "🏢", - "post_office": "🏣", - "european_post_office": "🏤", - "hospital": "🏥", - "bank": "🏦", - "hotel": "🏨", - "love_hotel": "🏩", - "convenience_store": "🏪", - "school": "🏫", - "department_store": "🏬", - "factory": "🏭", - "japanese_castle": "🏯", - "european_castle": "🏰", - "wedding": "💒", - "tokyo_tower": "🗼", - "statue_of_liberty": "🗽", - "church": "⛪", - "mosque": "🕌", - "hindu_temple": "🛕", - "synagogue": "🕍", - "shinto_shrine": "⛩️", - "kaaba": "🕋", - "fountain": "⛲", - "tent": "⛺", - "foggy": "🌁", - "night_with_stars": "🌃", - "cityscape": "🏙️", - "sunrise_over_mountains": "🌄", - "sunrise": "🌅", - "city_sunset": "🌆", - "city_sunrise": "🌇", - "bridge_at_night": "🌉", - "hotsprings": "♨️", - "carousel_horse": "🎠", - "playground_slide": "🛝", - "ferris_wheel": "🎡", - "roller_coaster": "🎢", - "barber": "💈", - "circus_tent": "🎪", - "steam_locomotive": "🚂", - "railway_car": "🚃", - "bullettrain_side": "🚄", - "bullettrain_front": "🚅", - "train2": "🚆", - "metro": "🚇", - "light_rail": "🚈", - "station": "🚉", - "tram": "🚊", - "monorail": "🚝", - "mountain_railway": "🚞", - "train": "🚋", - "bus": "🚌", - "oncoming_bus": "🚍", - "trolleybus": "🚎", - "minibus": "🚐", - "ambulance": "🚑", - "fire_engine": "🚒", - "police_car": "🚓", - "oncoming_police_car": "🚔", - "taxi": "🚕", - "oncoming_taxi": "🚖", - "car": "🚗", - "red_car": "🚗", - "oncoming_automobile": "🚘", - "blue_car": "🚙", - "pickup_truck": "🛻", - "truck": "🚚", - "articulated_lorry": "🚛", - "tractor": "🚜", - "racing_car": "🏎️", - "motorcycle": "🏍️", - "motor_scooter": "🛵", - "manual_wheelchair": "🦽", - "motorized_wheelchair": "🦼", - "auto_rickshaw": "🛺", - "bike": "🚲", - "kick_scooter": "🛴", - "skateboard": "🛹", - "roller_skate": "🛼", - "busstop": "🚏", - "motorway": "🛣️", - "railway_track": "🛤️", - "oil_drum": "🛢️", - "fuelpump": "⛽", - "wheel": "🛞", - "rotating_light": "🚨", - "traffic_light": "🚥", - "vertical_traffic_light": "🚦", - "stop_sign": "🛑", - "construction": "🚧", - "anchor": "⚓", - "ring_buoy": "🛟", - "boat": "⛵", - "sailboat": "⛵", - "canoe": "🛶", - "speedboat": "🚤", - "passenger_ship": "🛳️", - "ferry": "⛴️", - "motor_boat": "🛥️", - "ship": "🚢", - "airplane": "✈️", - "small_airplane": "🛩️", - "flight_departure": "🛫", - "flight_arrival": "🛬", - "parachute": "🪂", - "seat": "💺", - "helicopter": "🚁", - "suspension_railway": "🚟", - "mountain_cableway": "🚠", - "aerial_tramway": "🚡", - "artificial_satellite": "🛰️", - "rocket": "🚀", - "flying_saucer": "🛸", - "bellhop_bell": "🛎️", - "luggage": "🧳", - "hourglass": "⌛", - "hourglass_flowing_sand": "⏳", - "watch": "⌚", - "alarm_clock": "⏰", - "stopwatch": "⏱️", - "timer_clock": "⏲️", - "mantelpiece_clock": "🕰️", - "clock12": "🕛", - "clock1230": "🕧", - "clock1": "🕐", - "clock130": "🕜", - "clock2": "🕑", - "clock230": "🕝", - "clock3": "🕒", - "clock330": "🕞", - "clock4": "🕓", - "clock430": "🕟", - "clock5": "🕔", - "clock530": "🕠", - "clock6": "🕕", - "clock630": "🕡", - "clock7": "🕖", - "clock730": "🕢", - "clock8": "🕗", - "clock830": "🕣", - "clock9": "🕘", - "clock930": "🕤", - "clock10": "🕙", - "clock1030": "🕥", - "clock11": "🕚", - "clock1130": "🕦", - "new_moon": "🌑", - "waxing_crescent_moon": "🌒", - "first_quarter_moon": "🌓", - "moon": "🌔", - "waxing_gibbous_moon": "🌔", - "full_moon": "🌕", - "waning_gibbous_moon": "🌖", - "last_quarter_moon": "🌗", - "waning_crescent_moon": "🌘", - "crescent_moon": "🌙", - "new_moon_with_face": "🌚", - "first_quarter_moon_with_face": "🌛", - "last_quarter_moon_with_face": "🌜", - "thermometer": "🌡️", - "sunny": "☀️", - "full_moon_with_face": "🌝", - "sun_with_face": "🌞", - "ringed_planet": "🪐", - "star": "⭐", - "star2": "🌟", - "stars": "🌠", - "milky_way": "🌌", - "cloud": "☁️", - "partly_sunny": "⛅", - "cloud_with_lightning_and_rain": "⛈️", - "sun_behind_small_cloud": "🌤️", - "sun_behind_large_cloud": "🌥️", - "sun_behind_rain_cloud": "🌦️", - "cloud_with_rain": "🌧️", - "cloud_with_snow": "🌨️", - "cloud_with_lightning": "🌩️", - "tornado": "🌪️", - "fog": "🌫️", - "wind_face": "🌬️", - "cyclone": "🌀", - "rainbow": "🌈", - "closed_umbrella": "🌂", - "open_umbrella": "☂️", - "umbrella": "☔", - "parasol_on_ground": "⛱️", - "zap": "⚡", - "snowflake": "❄️", - "snowman_with_snow": "☃️", - "snowman": "⛄", - "comet": "☄️", - "fire": "🔥", - "droplet": "💧", - "ocean": "🌊", - "jack_o_lantern": "🎃", - "christmas_tree": "🎄", - "fireworks": "🎆", - "sparkler": "🎇", - "firecracker": "🧨", - "sparkles": "✨", - "balloon": "🎈", - "tada": "🎉", - "confetti_ball": "🎊", - "tanabata_tree": "🎋", - "bamboo": "🎍", - "dolls": "🎎", - "flags": "🎏", - "wind_chime": "🎐", - "rice_scene": "🎑", - "red_envelope": "🧧", - "ribbon": "🎀", - "gift": "🎁", - "reminder_ribbon": "🎗️", - "tickets": "🎟️", - "ticket": "🎫", - "medal_military": "🎖️", - "trophy": "🏆", - "medal_sports": "🏅", - "1st_place_medal": "🥇", - "2nd_place_medal": "🥈", - "3rd_place_medal": "🥉", - "soccer": "⚽", - "baseball": "⚾", - "softball": "🥎", - "basketball": "🏀", - "volleyball": "🏐", - "football": "🏈", - "rugby_football": "🏉", - "tennis": "🎾", - "flying_disc": "🥏", - "bowling": "🎳", - "cricket_game": "🏏", - "field_hockey": "🏑", - "ice_hockey": "🏒", - "lacrosse": "🥍", - "ping_pong": "🏓", - "badminton": "🏸", - "boxing_glove": "🥊", - "martial_arts_uniform": "🥋", - "goal_net": "🥅", - "golf": "⛳", - "ice_skate": "⛸️", - "fishing_pole_and_fish": "🎣", - "diving_mask": "🤿", - "running_shirt_with_sash": "🎽", - "ski": "🎿", - "sled": "🛷", - "curling_stone": "🥌", - "dart": "🎯", - "yo_yo": "🪀", - "kite": "🪁", - "gun": "🔫", - "8ball": "🎱", - "crystal_ball": "🔮", - "magic_wand": "🪄", - "video_game": "🎮", - "joystick": "🕹️", - "slot_machine": "🎰", - "game_die": "🎲", - "jigsaw": "🧩", - "teddy_bear": "🧸", - "pinata": "🪅", - "mirror_ball": "🪩", - "nesting_dolls": "🪆", - "spades": "♠️", - "hearts": "♥️", - "diamonds": "♦️", - "clubs": "♣️", - "chess_pawn": "♟️", - "black_joker": "🃏", - "mahjong": "🀄", - "flower_playing_cards": "🎴", - "performing_arts": "🎭", - "framed_picture": "🖼️", - "art": "🎨", - "thread": "🧵", - "sewing_needle": "🪡", - "yarn": "🧶", - "knot": "🪢", - "eyeglasses": "👓", - "dark_sunglasses": "🕶️", - "goggles": "🥽", - "lab_coat": "🥼", - "safety_vest": "🦺", - "necktie": "👔", - "shirt": "👕", - "tshirt": "👕", - "jeans": "👖", - "scarf": "🧣", - "gloves": "🧤", - "coat": "🧥", - "socks": "🧦", - "dress": "👗", - "kimono": "👘", - "sari": "🥻", - "one_piece_swimsuit": "🩱", - "swim_brief": "🩲", - "shorts": "🩳", - "bikini": "👙", - "womans_clothes": "👚", - "folding_hand_fan": "🪭", - "purse": "👛", - "handbag": "👜", - "pouch": "👝", - "shopping": "🛍️", - "school_satchel": "🎒", - "thong_sandal": "🩴", - "mans_shoe": "👞", - "shoe": "👞", - "athletic_shoe": "👟", - "hiking_boot": "🥾", - "flat_shoe": "🥿", - "high_heel": "👠", - "sandal": "👡", - "ballet_shoes": "🩰", - "boot": "👢", - "hair_pick": "🪮", - "crown": "👑", - "womans_hat": "👒", - "tophat": "🎩", - "mortar_board": "🎓", - "billed_cap": "🧢", - "military_helmet": "🪖", - "rescue_worker_helmet": "⛑️", - "prayer_beads": "📿", - "lipstick": "💄", - "ring": "💍", - "gem": "💎", - "mute": "🔇", - "speaker": "🔈", - "sound": "🔉", - "loud_sound": "🔊", - "loudspeaker": "📢", - "mega": "📣", - "postal_horn": "📯", - "bell": "🔔", - "no_bell": "🔕", - "musical_score": "🎼", - "musical_note": "🎵", - "notes": "🎶", - "studio_microphone": "🎙️", - "level_slider": "🎚️", - "control_knobs": "🎛️", - "microphone": "🎤", - "headphones": "🎧", - "radio": "📻", - "saxophone": "🎷", - "accordion": "🪗", - "guitar": "🎸", - "musical_keyboard": "🎹", - "trumpet": "🎺", - "violin": "🎻", - "banjo": "🪕", - "drum": "🥁", - "long_drum": "🪘", - "maracas": "🪇", - "flute": "🪈", - "iphone": "📱", - "calling": "📲", - "phone": "☎️", - "telephone": "☎️", - "telephone_receiver": "📞", - "pager": "📟", - "fax": "📠", - "battery": "🔋", - "low_battery": "🪫", - "electric_plug": "🔌", - "computer": "💻", - "desktop_computer": "🖥️", - "printer": "🖨️", - "keyboard": "⌨️", - "computer_mouse": "🖱️", - "trackball": "🖲️", - "minidisc": "💽", - "floppy_disk": "💾", - "cd": "💿", - "dvd": "📀", - "abacus": "🧮", - "movie_camera": "🎥", - "film_strip": "🎞️", - "film_projector": "📽️", - "clapper": "🎬", - "tv": "📺", - "camera": "📷", - "camera_flash": "📸", - "video_camera": "📹", - "vhs": "📼", - "mag": "🔍", - "mag_right": "🔎", - "candle": "🕯️", - "bulb": "💡", - "flashlight": "🔦", - "izakaya_lantern": "🏮", - "lantern": "🏮", - "diya_lamp": "🪔", - "notebook_with_decorative_cover": "📔", - "closed_book": "📕", - "book": "📖", - "open_book": "📖", - "green_book": "📗", - "blue_book": "📘", - "orange_book": "📙", - "books": "📚", - "notebook": "📓", - "ledger": "📒", - "page_with_curl": "📃", - "scroll": "📜", - "page_facing_up": "📄", - "newspaper": "📰", - "newspaper_roll": "🗞️", - "bookmark_tabs": "📑", - "bookmark": "🔖", - "label": "🏷️", - "moneybag": "💰", - "coin": "🪙", - "yen": "💴", - "dollar": "💵", - "euro": "💶", - "pound": "💷", - "money_with_wings": "💸", - "credit_card": "💳", - "receipt": "🧾", - "chart": "💹", - "envelope": "✉️", - "email": "📧", - "e-mail": "📧", - "incoming_envelope": "📨", - "envelope_with_arrow": "📩", - "outbox_tray": "📤", - "inbox_tray": "📥", - "package": "📦", - "mailbox": "📫", - "mailbox_closed": "📪", - "mailbox_with_mail": "📬", - "mailbox_with_no_mail": "📭", - "postbox": "📮", - "ballot_box": "🗳️", - "pencil2": "✏️", - "black_nib": "✒️", - "fountain_pen": "🖋️", - "pen": "🖊️", - "paintbrush": "🖌️", - "crayon": "🖍️", - "memo": "📝", - "pencil": "📝", - "briefcase": "💼", - "file_folder": "📁", - "open_file_folder": "📂", - "card_index_dividers": "🗂️", - "date": "📅", - "calendar": "📆", - "spiral_notepad": "🗒️", - "spiral_calendar": "🗓️", - "card_index": "📇", - "chart_with_upwards_trend": "📈", - "chart_with_downwards_trend": "📉", - "bar_chart": "📊", - "clipboard": "📋", - "pushpin": "📌", - "round_pushpin": "📍", - "paperclip": "📎", - "paperclips": "🖇️", - "straight_ruler": "📏", - "triangular_ruler": "📐", - "scissors": "✂️", - "card_file_box": "🗃️", - "file_cabinet": "🗄️", - "wastebasket": "🗑️", - "lock": "🔒", - "unlock": "🔓", - "lock_with_ink_pen": "🔏", - "closed_lock_with_key": "🔐", - "key": "🔑", - "old_key": "🗝️", - "hammer": "🔨", - "axe": "🪓", - "pick": "⛏️", - "hammer_and_pick": "⚒️", - "hammer_and_wrench": "🛠️", - "dagger": "🗡️", - "crossed_swords": "⚔️", - "bomb": "💣", - "boomerang": "🪃", - "bow_and_arrow": "🏹", - "shield": "🛡️", - "carpentry_saw": "🪚", - "wrench": "🔧", - "screwdriver": "🪛", - "nut_and_bolt": "🔩", - "gear": "⚙️", - "clamp": "🗜️", - "balance_scale": "⚖️", - "probing_cane": "🦯", - "link": "🔗", - "chains": "⛓️", - "hook": "🪝", - "toolbox": "🧰", - "magnet": "🧲", - "ladder": "🪜", - "alembic": "⚗️", - "test_tube": "🧪", - "petri_dish": "🧫", - "dna": "🧬", - "microscope": "🔬", - "telescope": "🔭", - "satellite": "📡", - "syringe": "💉", - "drop_of_blood": "🩸", - "pill": "💊", - "adhesive_bandage": "🩹", - "crutch": "🩼", - "stethoscope": "🩺", - "x_ray": "🩻", - "door": "🚪", - "elevator": "🛗", - "mirror": "🪞", - "window": "🪟", - "bed": "🛏️", - "couch_and_lamp": "🛋️", - "chair": "🪑", - "toilet": "🚽", - "plunger": "🪠", - "shower": "🚿", - "bathtub": "🛁", - "mouse_trap": "🪤", - "razor": "🪒", - "lotion_bottle": "🧴", - "safety_pin": "🧷", - "broom": "🧹", - "basket": "🧺", - "roll_of_paper": "🧻", - "bucket": "🪣", - "soap": "🧼", - "bubbles": "🫧", - "toothbrush": "🪥", - "sponge": "🧽", - "fire_extinguisher": "🧯", - "shopping_cart": "🛒", - "smoking": "🚬", - "coffin": "⚰️", - "headstone": "🪦", - "funeral_urn": "⚱️", - "nazar_amulet": "🧿", - "hamsa": "🪬", - "moyai": "🗿", - "placard": "🪧", - "identification_card": "🪪", - "atm": "🏧", - "put_litter_in_its_place": "🚮", - "potable_water": "🚰", - "wheelchair": "♿", - "mens": "🚹", - "womens": "🚺", - "restroom": "🚻", - "baby_symbol": "🚼", - "wc": "🚾", - "passport_control": "🛂", - "customs": "🛃", - "baggage_claim": "🛄", - "left_luggage": "🛅", - "warning": "⚠️", - "children_crossing": "🚸", - "no_entry": "⛔", - "no_entry_sign": "🚫", - "no_bicycles": "🚳", - "no_smoking": "🚭", - "do_not_litter": "🚯", - "non-potable_water": "🚱", - "no_pedestrians": "🚷", - "no_mobile_phones": "📵", - "underage": "🔞", - "radioactive": "☢️", - "biohazard": "☣️", - "arrow_up": "⬆️", - "arrow_upper_right": "↗️", - "arrow_right": "➡️", - "arrow_lower_right": "↘️", - "arrow_down": "⬇️", - "arrow_lower_left": "↙️", - "arrow_left": "⬅️", - "arrow_upper_left": "↖️", - "arrow_up_down": "↕️", - "left_right_arrow": "↔️", - "leftwards_arrow_with_hook": "↩️", - "arrow_right_hook": "↪️", - "arrow_heading_up": "⤴️", - "arrow_heading_down": "⤵️", - "arrows_clockwise": "🔃", - "arrows_counterclockwise": "🔄", - "back": "🔙", - "end": "🔚", - "on": "🔛", - "soon": "🔜", - "top": "🔝", - "place_of_worship": "🛐", - "atom_symbol": "⚛️", - "om": "🕉️", - "star_of_david": "✡️", - "wheel_of_dharma": "☸️", - "yin_yang": "☯️", - "latin_cross": "✝️", - "orthodox_cross": "☦️", - "star_and_crescent": "☪️", - "peace_symbol": "☮️", - "menorah": "🕎", - "six_pointed_star": "🔯", - "khanda": "🪯", - "aries": "♈", - "taurus": "♉", - "gemini": "♊", - "cancer": "♋", - "leo": "♌", - "virgo": "♍", - "libra": "♎", - "scorpius": "♏", - "sagittarius": "♐", - "capricorn": "♑", - "aquarius": "♒", - "pisces": "♓", - "ophiuchus": "⛎", - "twisted_rightwards_arrows": "🔀", - "repeat": "🔁", - "repeat_one": "🔂", - "arrow_forward": "▶️", - "fast_forward": "⏩", - "next_track_button": "⏭️", - "play_or_pause_button": "⏯️", - "arrow_backward": "◀️", - "rewind": "⏪", - "previous_track_button": "⏮️", - "arrow_up_small": "🔼", - "arrow_double_up": "⏫", - "arrow_down_small": "🔽", - "arrow_double_down": "⏬", - "pause_button": "⏸️", - "stop_button": "⏹️", - "record_button": "⏺️", - "eject_button": "⏏️", - "cinema": "🎦", - "low_brightness": "🔅", - "high_brightness": "🔆", - "signal_strength": "📶", - "wireless": "🛜", - "vibration_mode": "📳", - "mobile_phone_off": "📴", - "female_sign": "♀️", - "male_sign": "♂️", - "transgender_symbol": "⚧️", - "heavy_multiplication_x": "✖️", - "heavy_plus_sign": "➕", - "heavy_minus_sign": "➖", - "heavy_division_sign": "➗", - "heavy_equals_sign": "🟰", - "infinity": "♾️", - "bangbang": "‼️", - "interrobang": "⁉️", - "question": "❓", - "grey_question": "❔", - "grey_exclamation": "❕", - "exclamation": "❗", - "heavy_exclamation_mark": "❗", - "wavy_dash": "〰️", - "currency_exchange": "💱", - "heavy_dollar_sign": "💲", - "medical_symbol": "⚕️", - "recycle": "♻️", - "fleur_de_lis": "⚜️", - "trident": "🔱", - "name_badge": "📛", - "beginner": "🔰", - "o": "⭕", - "white_check_mark": "✅", - "ballot_box_with_check": "☑️", - "heavy_check_mark": "✔️", - "x": "❌", - "negative_squared_cross_mark": "❎", - "curly_loop": "➰", - "loop": "➿", - "part_alternation_mark": "〽️", - "eight_spoked_asterisk": "✳️", - "eight_pointed_black_star": "✴️", - "sparkle": "❇️", - "copyright": "©️", - "registered": "®️", - "tm": "™️", - "hash": "#️⃣", - "asterisk": "*️⃣", - "zero": "0️⃣", - "one": "1️⃣", - "two": "2️⃣", - "three": "3️⃣", - "four": "4️⃣", - "five": "5️⃣", - "six": "6️⃣", - "seven": "7️⃣", - "eight": "8️⃣", - "nine": "9️⃣", - "keycap_ten": "🔟", - "capital_abcd": "🔠", - "abcd": "🔡", - "symbols": "🔣", - "abc": "🔤", - "a": "🅰️", - "ab": "🆎", - "b": "🅱️", - "cl": "🆑", - "cool": "🆒", - "free": "🆓", - "information_source": "ℹ️", - "id": "🆔", - "m": "Ⓜ️", - "new": "🆕", - "ng": "🆖", - "o2": "🅾️", - "ok": "🆗", - "parking": "🅿️", - "sos": "🆘", - "up": "🆙", - "vs": "🆚", - "koko": "🈁", - "sa": "🈂️", - "ideograph_advantage": "🉐", - "accept": "🉑", - "congratulations": "㊗️", - "secret": "㊙️", - "u6e80": "🈵", - "red_circle": "🔴", - "orange_circle": "🟠", - "yellow_circle": "🟡", - "green_circle": "🟢", - "large_blue_circle": "🔵", - "purple_circle": "🟣", - "brown_circle": "🟤", - "black_circle": "⚫", - "white_circle": "⚪", - "red_square": "🟥", - "orange_square": "🟧", - "yellow_square": "🟨", - "green_square": "🟩", - "blue_square": "🟦", - "purple_square": "🟪", - "brown_square": "🟫", - "black_large_square": "⬛", - "white_large_square": "⬜", - "black_medium_square": "◼️", - "white_medium_square": "◻️", - "black_medium_small_square": "◾", - "white_medium_small_square": "◽", - "black_small_square": "▪️", - "white_small_square": "▫️", - "large_orange_diamond": "🔶", - "large_blue_diamond": "🔷", - "small_orange_diamond": "🔸", - "small_blue_diamond": "🔹", - "small_red_triangle": "🔺", - "small_red_triangle_down": "🔻", - "diamond_shape_with_a_dot_inside": "💠", - "radio_button": "🔘", - "white_square_button": "🔳", - "black_square_button": "🔲", - "checkered_flag": "🏁", - "triangular_flag_on_post": "🚩", - "crossed_flags": "🎌", - "black_flag": "🏴", - "white_flag": "🏳️", - "rainbow_flag": "🏳️‍🌈", - "transgender_flag": "🏳️‍⚧️", - "pirate_flag": "🏴‍☠️", - "ascension_island": "🇦🇨", - "andorra": "🇦🇩", - "united_arab_emirates": "🇦🇪", - "afghanistan": "🇦🇫", - "antigua_barbuda": "🇦🇬", - "anguilla": "🇦🇮", - "albania": "🇦🇱", - "armenia": "🇦🇲", - "angola": "🇦🇴", - "antarctica": "🇦🇶", - "argentina": "🇦🇷", - "american_samoa": "🇦🇸", - "austria": "🇦🇹", - "australia": "🇦🇺", - "aruba": "🇦🇼", - "aland_islands": "🇦🇽", - "azerbaijan": "🇦🇿", - "bosnia_herzegovina": "🇧🇦", - "barbados": "🇧🇧", - "bangladesh": "🇧🇩", - "belgium": "🇧🇪", - "burkina_faso": "🇧🇫", - "bulgaria": "🇧🇬", - "bahrain": "🇧🇭", - "burundi": "🇧🇮", - "benin": "🇧🇯", - "st_barthelemy": "🇧🇱", - "bermuda": "🇧🇲", - "brunei": "🇧🇳", - "bolivia": "🇧🇴", - "caribbean_netherlands": "🇧🇶", - "brazil": "🇧🇷", - "bahamas": "🇧🇸", - "bhutan": "🇧🇹", - "bouvet_island": "🇧🇻", - "botswana": "🇧🇼", - "belarus": "🇧🇾", - "belize": "🇧🇿", - "canada": "🇨🇦", - "cocos_islands": "🇨🇨", - "congo_kinshasa": "🇨🇩", - "central_african_republic": "🇨🇫", - "congo_brazzaville": "🇨🇬", - "switzerland": "🇨🇭", - "cote_divoire": "🇨🇮", - "cook_islands": "🇨🇰", - "chile": "🇨🇱", - "cameroon": "🇨🇲", - "cn": "🇨🇳", - "colombia": "🇨🇴", - "clipperton_island": "🇨🇵", - "costa_rica": "🇨🇷", - "cuba": "🇨🇺", - "cape_verde": "🇨🇻", - "curacao": "🇨🇼", - "christmas_island": "🇨🇽", - "cyprus": "🇨🇾", - "czech_republic": "🇨🇿", - "de": "🇩🇪", - "diego_garcia": "🇩🇬", - "djibouti": "🇩🇯", - "denmark": "🇩🇰", - "dominica": "🇩🇲", - "dominican_republic": "🇩🇴", - "algeria": "🇩🇿", - "ceuta_melilla": "🇪🇦", - "ecuador": "🇪🇨", - "estonia": "🇪🇪", - "egypt": "🇪🇬", - "western_sahara": "🇪🇭", - "eritrea": "🇪🇷", - "es": "🇪🇸", - "ethiopia": "🇪🇹", - "eu": "🇪🇺", - "european_union": "🇪🇺", - "finland": "🇫🇮", - "fiji": "🇫🇯", - "falkland_islands": "🇫🇰", - "micronesia": "🇫🇲", - "faroe_islands": "🇫🇴", - "fr": "🇫🇷", - "gabon": "🇬🇦", - "gb": "🇬🇧", - "uk": "🇬🇧", - "grenada": "🇬🇩", - "georgia": "🇬🇪", - "french_guiana": "🇬🇫", - "guernsey": "🇬🇬", - "ghana": "🇬🇭", - "gibraltar": "🇬🇮", - "greenland": "🇬🇱", - "gambia": "🇬🇲", - "guinea": "🇬🇳", - "guadeloupe": "🇬🇵", - "equatorial_guinea": "🇬🇶", - "greece": "🇬🇷", - "south_georgia_south_sandwich_islands": "🇬🇸", - "guatemala": "🇬🇹", - "guam": "🇬🇺", - "guinea_bissau": "🇬🇼", - "guyana": "🇬🇾", - "hong_kong": "🇭🇰", - "heard_mcdonald_islands": "🇭🇲", - "honduras": "🇭🇳", - "croatia": "🇭🇷", - "haiti": "🇭🇹", - "hungary": "🇭🇺", - "canary_islands": "🇮🇨", - "indonesia": "🇮🇩", - "ireland": "🇮🇪", - "israel": "🇮🇱", - "isle_of_man": "🇮🇲", - "india": "🇮🇳", - "british_indian_ocean_territory": "🇮🇴", - "iraq": "🇮🇶", - "iran": "🇮🇷", - "iceland": "🇮🇸", - "it": "🇮🇹", - "jersey": "🇯🇪", - "jamaica": "🇯🇲", - "jordan": "🇯🇴", - "jp": "🇯🇵", - "kenya": "🇰🇪", - "kyrgyzstan": "🇰🇬", - "cambodia": "🇰🇭", - "kiribati": "🇰🇮", - "comoros": "🇰🇲", - "st_kitts_nevis": "🇰🇳", - "north_korea": "🇰🇵", - "kr": "🇰🇷", - "kuwait": "🇰🇼", - "cayman_islands": "🇰🇾", - "kazakhstan": "🇰🇿", - "laos": "🇱🇦", - "lebanon": "🇱🇧", - "st_lucia": "🇱🇨", - "liechtenstein": "🇱🇮", - "sri_lanka": "🇱🇰", - "liberia": "🇱🇷", - "lesotho": "🇱🇸", - "lithuania": "🇱🇹", - "luxembourg": "🇱🇺", - "latvia": "🇱🇻", - "libya": "🇱🇾", - "morocco": "🇲🇦", - "monaco": "🇲🇨", - "moldova": "🇲🇩", - "montenegro": "🇲🇪", - "st_martin": "🇲🇫", - "madagascar": "🇲🇬", - "marshall_islands": "🇲🇭", - "macedonia": "🇲🇰", - "mali": "🇲🇱", - "myanmar": "🇲🇲", - "mongolia": "🇲🇳", - "macau": "🇲🇴", - "northern_mariana_islands": "🇲🇵", - "martinique": "🇲🇶", - "mauritania": "🇲🇷", - "montserrat": "🇲🇸", - "malta": "🇲🇹", - "mauritius": "🇲🇺", - "maldives": "🇲🇻", - "malawi": "🇲🇼", - "mexico": "🇲🇽", - "malaysia": "🇲🇾", - "mozambique": "🇲🇿", - "namibia": "🇳🇦", - "new_caledonia": "🇳🇨", - "niger": "🇳🇪", - "norfolk_island": "🇳🇫", - "nigeria": "🇳🇬", - "nicaragua": "🇳🇮", - "netherlands": "🇳🇱", - "norway": "🇳🇴", - "nepal": "🇳🇵", - "nauru": "🇳🇷", - "niue": "🇳🇺", - "new_zealand": "🇳🇿", - "oman": "🇴🇲", - "panama": "🇵🇦", - "peru": "🇵🇪", - "french_polynesia": "🇵🇫", - "papua_new_guinea": "🇵🇬", - "philippines": "🇵🇭", - "pakistan": "🇵🇰", - "poland": "🇵🇱", - "st_pierre_miquelon": "🇵🇲", - "pitcairn_islands": "🇵🇳", - "puerto_rico": "🇵🇷", - "palestinian_territories": "🇵🇸", - "portugal": "🇵🇹", - "palau": "🇵🇼", - "paraguay": "🇵🇾", - "qatar": "🇶🇦", - "reunion": "🇷🇪", - "romania": "🇷🇴", - "serbia": "🇷🇸", - "ru": "🇷🇺", - "rwanda": "🇷🇼", - "saudi_arabia": "🇸🇦", - "solomon_islands": "🇸🇧", - "seychelles": "🇸🇨", - "sudan": "🇸🇩", - "sweden": "🇸🇪", - "singapore": "🇸🇬", - "st_helena": "🇸🇭", - "slovenia": "🇸🇮", - "svalbard_jan_mayen": "🇸🇯", - "slovakia": "🇸🇰", - "sierra_leone": "🇸🇱", - "san_marino": "🇸🇲", - "senegal": "🇸🇳", - "somalia": "🇸🇴", - "suriname": "🇸🇷", - "south_sudan": "🇸🇸", - "sao_tome_principe": "🇸🇹", - "el_salvador": "🇸🇻", - "sint_maarten": "🇸🇽", - "syria": "🇸🇾", - "swaziland": "🇸🇿", - "tristan_da_cunha": "🇹🇦", - "turks_caicos_islands": "🇹🇨", - "chad": "🇹🇩", - "french_southern_territories": "🇹🇫", - "togo": "🇹🇬", - "thailand": "🇹🇭", - "tajikistan": "🇹🇯", - "tokelau": "🇹🇰", - "timor_leste": "🇹🇱", - "turkmenistan": "🇹🇲", - "tunisia": "🇹🇳", - "tonga": "🇹🇴", - "tr": "🇹🇷", - "trinidad_tobago": "🇹🇹", - "tuvalu": "🇹🇻", - "taiwan": "🇹🇼", - "tanzania": "🇹🇿", - "ukraine": "🇺🇦", - "uganda": "🇺🇬", - "us_outlying_islands": "🇺🇲", - "united_nations": "🇺🇳", - "us": "🇺🇸", - "uruguay": "🇺🇾", - "uzbekistan": "🇺🇿", - "vatican_city": "🇻🇦", - "st_vincent_grenadines": "🇻🇨", - "venezuela": "🇻🇪", - "british_virgin_islands": "🇻🇬", - "us_virgin_islands": "🇻🇮", - "vietnam": "🇻🇳", - "vanuatu": "🇻🇺", - "wallis_futuna": "🇼🇫", - "samoa": "🇼🇸", - "kosovo": "🇽🇰", - "yemen": "🇾🇪", - "mayotte": "🇾🇹", - "south_africa": "🇿🇦", - "zambia": "🇿🇲", - "zimbabwe": "🇿🇼", - "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", - "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", - "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿" -}; - -export default emojies; diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/light.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/light.ts deleted file mode 100644 index acaed2f..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/light.ts +++ /dev/null @@ -1,158 +0,0 @@ -// Generated, don't edit -import { EmojiDefs } from '../normalize_opts'; - -const emojies: EmojiDefs = { - "grinning": "😀", - "smiley": "😃", - "smile": "😄", - "grin": "😁", - "laughing": "😆", - "satisfied": "😆", - "sweat_smile": "😅", - "joy": "😂", - "wink": "😉", - "blush": "😊", - "innocent": "😇", - "heart_eyes": "😍", - "kissing_heart": "😘", - "kissing": "😗", - "kissing_closed_eyes": "😚", - "kissing_smiling_eyes": "😙", - "yum": "😋", - "stuck_out_tongue": "😛", - "stuck_out_tongue_winking_eye": "😜", - "stuck_out_tongue_closed_eyes": "😝", - "neutral_face": "😐", - "expressionless": "😑", - "no_mouth": "😶", - "smirk": "😏", - "unamused": "😒", - "relieved": "😌", - "pensive": "😔", - "sleepy": "😪", - "sleeping": "😴", - "mask": "😷", - "dizzy_face": "😵", - "sunglasses": "😎", - "confused": "😕", - "worried": "😟", - "open_mouth": "😮", - "hushed": "😯", - "astonished": "😲", - "flushed": "😳", - "frowning": "😦", - "anguished": "😧", - "fearful": "😨", - "cold_sweat": "😰", - "disappointed_relieved": "😥", - "cry": "😢", - "sob": "😭", - "scream": "😱", - "confounded": "😖", - "persevere": "😣", - "disappointed": "😞", - "sweat": "😓", - "weary": "😩", - "tired_face": "😫", - "rage": "😡", - "pout": "😡", - "angry": "😠", - "smiling_imp": "😈", - "smiley_cat": "😺", - "smile_cat": "😸", - "joy_cat": "😹", - "heart_eyes_cat": "😻", - "smirk_cat": "😼", - "kissing_cat": "😽", - "scream_cat": "🙀", - "crying_cat_face": "😿", - "pouting_cat": "😾", - "heart": "❤️", - "hand": "✋", - "raised_hand": "✋", - "v": "✌️", - "point_up": "☝️", - "fist_raised": "✊", - "fist": "✊", - "monkey_face": "🐵", - "cat": "🐱", - "cow": "🐮", - "mouse": "🐭", - "coffee": "☕", - "hotsprings": "♨️", - "anchor": "⚓", - "airplane": "✈️", - "hourglass": "⌛", - "watch": "⌚", - "sunny": "☀️", - "star": "⭐", - "cloud": "☁️", - "umbrella": "☔", - "zap": "⚡", - "snowflake": "❄️", - "sparkles": "✨", - "black_joker": "🃏", - "mahjong": "🀄", - "phone": "☎️", - "telephone": "☎️", - "envelope": "✉️", - "pencil2": "✏️", - "black_nib": "✒️", - "scissors": "✂️", - "wheelchair": "♿", - "warning": "⚠️", - "aries": "♈", - "taurus": "♉", - "gemini": "♊", - "cancer": "♋", - "leo": "♌", - "virgo": "♍", - "libra": "♎", - "scorpius": "♏", - "sagittarius": "♐", - "capricorn": "♑", - "aquarius": "♒", - "pisces": "♓", - "heavy_multiplication_x": "✖️", - "heavy_plus_sign": "➕", - "heavy_minus_sign": "➖", - "heavy_division_sign": "➗", - "bangbang": "‼️", - "interrobang": "⁉️", - "question": "❓", - "grey_question": "❔", - "grey_exclamation": "❕", - "exclamation": "❗", - "heavy_exclamation_mark": "❗", - "wavy_dash": "〰️", - "recycle": "♻️", - "white_check_mark": "✅", - "ballot_box_with_check": "☑️", - "heavy_check_mark": "✔️", - "x": "❌", - "negative_squared_cross_mark": "❎", - "curly_loop": "➰", - "loop": "➿", - "part_alternation_mark": "〽️", - "eight_spoked_asterisk": "✳️", - "eight_pointed_black_star": "✴️", - "sparkle": "❇️", - "copyright": "©️", - "registered": "®️", - "tm": "™️", - "information_source": "ℹ️", - "m": "Ⓜ️", - "black_circle": "⚫", - "white_circle": "⚪", - "black_large_square": "⬛", - "white_large_square": "⬜", - "black_medium_square": "◼️", - "white_medium_square": "◻️", - "black_medium_small_square": "◾", - "white_medium_small_square": "◽", - "black_small_square": "▪️", - "white_small_square": "▫️" -}; - -export default emojies; - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/shortcuts.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/shortcuts.ts deleted file mode 100644 index 418c6c0..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/data/shortcuts.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Emoticons -> Emoji mapping. -// -// (!) Some patterns skipped, to avoid collisions -// without increase matcher complicity. Than can change in future. -// -// Places to look for more emoticons info: -// -// - http://en.wikipedia.org/wiki/List_of_emoticons#Western -// - https://github.com/wooorm/emoticon/blob/master/Support.md -// - http://factoryjoe.com/projects/emoticons/ -// - -import { EmojiShortcuts } from '../normalize_opts'; - -const shortcuts: EmojiShortcuts = { - angry: ['>:(', '>:-('], - blush: [':")', ':-")'], - broken_heart: ['): void { - const defaults: EmojiOptions = { - defs: emojies_defs, - shortcuts: emojies_shortcuts, - enabled: [] - }; - - const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions; - - bare_emoji_plugin(md, opts); -} - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/light.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/light.ts deleted file mode 100644 index 74f8321..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/light.ts +++ /dev/null @@ -1,21 +0,0 @@ -import MarkdownIt from 'markdown-it'; -import emojies_defs from './data/light'; -import emojies_shortcuts from './data/shortcuts'; -import bare_emoji_plugin from './bare'; -import { EmojiOptions } from './normalize_opts'; - -/** - * Light emoji 插件(包含常用的 emoji 数据) - */ -export default function emoji_plugin(md: MarkdownIt, options?: Partial): void { - const defaults: EmojiOptions = { - defs: emojies_defs, - shortcuts: emojies_shortcuts, - enabled: [] - }; - - const opts = md.utils.assign({}, defaults, options || {}) as EmojiOptions; - - bare_emoji_plugin(md, opts); -} - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/normalize_opts.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/normalize_opts.ts deleted file mode 100644 index 53eb426..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/normalize_opts.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Emoji 定义类型 - */ -export interface EmojiDefs { - [key: string]: string; -} - -/** - * Emoji 快捷方式类型 - */ -export interface EmojiShortcuts { - [key: string]: string | string[]; -} - -/** - * 输入选项接口 - */ -export interface EmojiOptions { - defs: EmojiDefs; - shortcuts: EmojiShortcuts; - enabled: string[]; -} - -/** - * 标准化后的选项接口 - */ -export interface NormalizedEmojiOptions { - defs: EmojiDefs; - shortcuts: { [key: string]: string }; - scanRE: RegExp; - replaceRE: RegExp; -} - -/** - * 转义正则表达式特殊字符 - */ -function quoteRE(str: string): string { - return str.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&'); -} - -/** - * 将输入选项转换为更可用的格式并编译搜索正则表达式 - */ -export default function normalize_opts(options: EmojiOptions): NormalizedEmojiOptions { - let emojies = options.defs; - - // Filter emojies by whitelist, if needed - if (options.enabled.length) { - emojies = Object.keys(emojies).reduce((acc: EmojiDefs, key: string) => { - if (options.enabled.indexOf(key) >= 0) acc[key] = emojies[key]; - return acc; - }, {}); - } - - // Flatten shortcuts to simple object: { alias: emoji_name } - const shortcuts = Object.keys(options.shortcuts).reduce((acc: { [key: string]: string }, key: string) => { - // Skip aliases for filtered emojies, to reduce regexp - if (!emojies[key]) return acc; - - if (Array.isArray(options.shortcuts[key])) { - (options.shortcuts[key] as string[]).forEach((alias: string) => { acc[alias] = key; }); - return acc; - } - - acc[options.shortcuts[key] as string] = key; - return acc; - }, {}); - - const keys = Object.keys(emojies); - let names: string; - - // If no definitions are given, return empty regex to avoid replacements with 'undefined'. - if (keys.length === 0) { - names = '^$'; - } else { - // Compile regexp - names = keys - .map((name: string) => { return `:${name}:`; }) - .concat(Object.keys(shortcuts)) - .sort() - .reverse() - .map((name: string) => { return quoteRE(name); }) - .join('|'); - } - const scanRE = RegExp(names); - const replaceRE = RegExp(names, 'g'); - - return { - defs: emojies, - shortcuts, - scanRE, - replaceRE - }; -} - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/render.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/render.ts deleted file mode 100644 index db40f94..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/render.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Token } from 'markdown-it'; - -/** - * Emoji 渲染函数 - */ -export default function emoji_html(tokens: Token[], idx: number): string { - return tokens[idx].content; -} - diff --git a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/replace.ts b/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/replace.ts deleted file mode 100644 index 556fb54..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-emojis/lib/replace.ts +++ /dev/null @@ -1,97 +0,0 @@ -import MarkdownIt, { StateCore, Token } from 'markdown-it'; -import { EmojiDefs } from './normalize_opts'; - -/** - * Emoji 和快捷方式替换逻辑 - * - * 注意:理论上,在内联链中解析 :smile: 并只留下快捷方式会更快。 - * 但是,谁在乎呢... - */ -export default function create_rule( - md: MarkdownIt, - emojies: EmojiDefs, - shortcuts: { [key: string]: string }, - scanRE: RegExp, - replaceRE: RegExp -) { - const arrayReplaceAt = md.utils.arrayReplaceAt; - const ucm = md.utils.lib.ucmicro; - const has = md.utils.has; - const ZPCc = new RegExp([ucm.Z.source, ucm.P.source, ucm.Cc.source].join('|')); - - function splitTextToken(text: string, level: number, TokenConstructor: any): Token[] { - let last_pos = 0; - const nodes: Token[] = []; - - text.replace(replaceRE, function (match: string, offset: number, src: string): string { - let emoji_name: string; - // Validate emoji name - if (has(shortcuts, match)) { - // replace shortcut with full name - emoji_name = shortcuts[match]; - - // Don't allow letters before any shortcut (as in no ":/" in http://) - if (offset > 0 && !ZPCc.test(src[offset - 1])) return ''; - - // Don't allow letters after any shortcut - if (offset + match.length < src.length && !ZPCc.test(src[offset + match.length])) { - return ''; - } - } else { - emoji_name = match.slice(1, -1); - } - - // Add new tokens to pending list - if (offset > last_pos) { - const token = new TokenConstructor('text', '', 0); - token.content = text.slice(last_pos, offset); - nodes.push(token); - } - - const token = new TokenConstructor('emoji', '', 0); - token.markup = emoji_name; - token.content = emojies[emoji_name]; - nodes.push(token); - - last_pos = offset + match.length; - return ''; - }); - - if (last_pos < text.length) { - const token = new TokenConstructor('text', '', 0); - token.content = text.slice(last_pos); - nodes.push(token); - } - - return nodes; - } - - return function emoji_replace(state: StateCore): void { - let token: Token; - const blockTokens = state.tokens; - let autolinkLevel = 0; - - 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. - // Use reversed logic in links start/end match - for (let i = tokens.length - 1; i >= 0; i--) { - token = tokens[i]; - - if (token.type === 'link_open' || token.type === 'link_close') { - if (token.info === 'auto') { autolinkLevel -= token.nesting; } - } - - if (token.type === 'text' && autolinkLevel === 0 && scanRE.test(token.content)) { - // replace current node - blockTokens[j].children = tokens = arrayReplaceAt( - tokens, i, splitTextToken(token.content, token.level, state.Token) - ); - } - } - } - }; -} - 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 deleted file mode 100644 index 1f80bba..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-footnote/index.ts +++ /dev/null @@ -1,390 +0,0 @@ -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 deleted file mode 100644 index 9521bde..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-ins/index.ts +++ /dev/null @@ -1,160 +0,0 @@ -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 deleted file mode 100644 index ed18705..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-mark/index.ts +++ /dev/null @@ -1,160 +0,0 @@ -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 deleted file mode 100644 index 734383d..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 deleted file mode 100644 index f3c4486..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-mermaid/utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index f2e00cf..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-sub/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -// 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 deleted file mode 100644 index eb5c8cd..0000000 --- a/frontend/src/common/markdown-it/plugins/markdown-it-sup/index.ts +++ /dev/null @@ -1,66 +0,0 @@ -// 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/components/toolbar/Toolbar.vue b/frontend/src/components/toolbar/Toolbar.vue index bc41b08..4821b5d 100644 --- a/frontend/src/components/toolbar/Toolbar.vue +++ b/frontend/src/components/toolbar/Toolbar.vue @@ -13,8 +13,6 @@ 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 {markdownPreviewManager} from "@/views/editor/extensions/markdownPreview/manager"; const editorStore = useEditorStore(); const configStore = useConfigStore(); @@ -25,7 +23,6 @@ const {t} = useI18n(); const router = useRouter(); const canFormatCurrentBlock = ref(false); -const canPreviewMarkdown = ref(false); const isLoaded = shallowRef(false); const { documentStats } = toRefs(editorStore); @@ -36,10 +33,6 @@ const isCurrentWindowOnTop = computed(() => { return config.value.general.alwaysOnTop || systemStore.isWindowOnTop; }); -// 当前文档的预览是否打开 -const isCurrentBlockPreviewing = computed(() => { - return markdownPreviewManager.isVisible(); -}); // 切换窗口置顶状态 const toggleAlwaysOnTop = async () => { @@ -68,22 +61,12 @@ const formatCurrentBlock = () => { formatBlockContent(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; } @@ -94,7 +77,6 @@ const updateButtonStates = () => { // 提前返回,减少不必要的计算 if (!activeBlock) { canFormatCurrentBlock.value = false; - canPreviewMarkdown.value = false; return; } @@ -102,11 +84,9 @@ const updateButtonStates = () => { const language = getLanguage(languageName as any); canFormatCurrentBlock.value = Boolean(language?.prettier); - canPreviewMarkdown.value = languageName.toLowerCase() === 'md'; } catch (error) { console.warn('Error checking block capabilities:', error); canFormatCurrentBlock.value = false; - canPreviewMarkdown.value = false; } }; @@ -160,7 +140,6 @@ watch( cleanupListeners = setupEditorListeners(newView); } else { canFormatCurrentBlock.value = false; - canPreviewMarkdown.value = false; } }); }, @@ -254,21 +233,6 @@ const statsData = computed(() => ({ - -
    - - - - -
    -
    { // Font options (no longer localized) const fontOptions = computed(() => FONT_OPTIONS); - // 计算属性 - 使用工厂函数简化 + // 计算属性 const createLimitComputed = (key: NumberConfigKey) => computed(() => CONFIG_LIMITS[key]); const limits = Object.fromEntries( (['fontSize', 'tabSize', 'lineHeight'] as const).map(key => [key, createLimitComputed(key)]) ) as Record>; - // 通用配置更新方法 - const updateGeneralConfig = async (key: K, value: GeneralConfig[K]): Promise => { - // 确保配置已加载 + // 统一配置更新方法 + const updateConfig = async (key: K, value: any): Promise => { if (!state.configLoaded && !state.isLoading) { await initConfig(); } - const backendKey = GENERAL_CONFIG_KEY_MAP[key]; + const backendKey = CONFIG_KEY_MAP[key]; if (!backendKey) { - throw new Error(`No backend key mapping found for general.${key.toString()}`); + throw new Error(`No backend key mapping found for ${String(key)}`); } + // 从 backendKey 提取 section(例如 'general.alwaysOnTop' -> 'general') + const section = backendKey.split('.')[0] as ConfigSection; + await ConfigService.Set(backendKey, value); - state.config.general[key] = value; + (state.config[section] as any)[key] = value; }; - const updateEditingConfig = async (key: K, value: EditingConfig[K]): Promise => { - // 确保配置已加载 - if (!state.configLoaded && !state.isLoading) { - await initConfig(); - } - - const backendKey = EDITING_CONFIG_KEY_MAP[key]; - if (!backendKey) { - throw new Error(`No backend key mapping found for editing.${key.toString()}`); - } - - await ConfigService.Set(backendKey, value); - state.config.editing[key] = value; + // 只更新本地状态,不保存到后端 + const updateConfigLocal = (key: K, value: any): void => { + const backendKey = CONFIG_KEY_MAP[key]; + const section = backendKey.split('.')[0] as ConfigSection; + (state.config[section] as any)[key] = value; }; - const updateAppearanceConfig = async (key: K, value: AppearanceConfig[K]): Promise => { - // 确保配置已加载 - if (!state.configLoaded && !state.isLoading) { - await initConfig(); - } - - const backendKey = APPEARANCE_CONFIG_KEY_MAP[key]; - if (!backendKey) { - throw new Error(`No backend key mapping found for appearance.${key.toString()}`); - } - - await ConfigService.Set(backendKey, value); - state.config.appearance[key] = value; - }; - - const updateUpdatesConfig = async (key: K, value: UpdatesConfig[K]): Promise => { - // 确保配置已加载 - if (!state.configLoaded && !state.isLoading) { - await initConfig(); - } - - const backendKey = UPDATES_CONFIG_KEY_MAP[key]; - if (!backendKey) { - throw new Error(`No backend key mapping found for updates.${key.toString()}`); - } - - await ConfigService.Set(backendKey, value); - state.config.updates[key] = value; - }; - - const updateBackupConfig = async (key: K, value: GitBackupConfig[K]): Promise => { - // 确保配置已加载 - if (!state.configLoaded && !state.isLoading) { - await initConfig(); - } - - const backendKey = BACKUP_CONFIG_KEY_MAP[key]; - if (!backendKey) { - throw new Error(`No backend key mapping found for backup.${key.toString()}`); - } - - await ConfigService.Set(backendKey, value); - state.config.backup[key] = value; + // 保存指定配置到后端 + const saveConfig = async (key: K): Promise => { + const backendKey = CONFIG_KEY_MAP[key]; + const section = backendKey.split('.')[0] as ConfigSection; + await ConfigService.Set(backendKey, (state.config[section] as any)[key]); }; // 加载配置 @@ -155,22 +105,24 @@ export const useConfigStore = defineStore('config', () => { const clamp = (value: number) => ConfigUtils.clamp(value, limit.min, limit.max); return { - increase: async () => await updateEditingConfig(key, clamp(state.config.editing[key] + 1)), - decrease: async () => await updateEditingConfig(key, clamp(state.config.editing[key] - 1)), - set: async (value: number) => await updateEditingConfig(key, clamp(value)), - reset: async () => await updateEditingConfig(key, limit.default) + increase: async () => await updateConfig(key, clamp(state.config.editing[key] + 1)), + decrease: async () => await updateConfig(key, clamp(state.config.editing[key] - 1)), + set: async (value: number) => await updateConfig(key, clamp(value)), + reset: async () => await updateConfig(key, limit.default), + increaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] + 1)), + decreaseLocal: () => updateConfigLocal(key, clamp(state.config.editing[key] - 1)) }; }; const createEditingToggler = (key: T) => - async () => await updateEditingConfig(key, !state.config.editing[key] as EditingConfig[T]); + async () => await updateConfig(key as ConfigKey, !state.config.editing[key] as EditingConfig[T]); // 枚举值切换器 const createEnumToggler = (key: 'tabType', values: readonly T[]) => async () => { const currentIndex = values.indexOf(state.config.editing[key] as T); const nextIndex = (currentIndex + 1) % values.length; - return await updateEditingConfig(key, values[nextIndex]); + return await updateConfig(key, values[nextIndex]); }; // 重置配置 @@ -192,21 +144,19 @@ export const useConfigStore = defineStore('config', () => { // 语言设置方法 const setLanguage = async (language: LanguageType): Promise => { - await updateAppearanceConfig('language', language); - - // 同步更新前端语言 + await updateConfig('language', language); const frontendLocale = ConfigUtils.backendLanguageToFrontend(language); locale.value = frontendLocale as any; }; // 系统主题设置方法 const setSystemTheme = async (systemTheme: SystemThemeType): Promise => { - await updateAppearanceConfig('systemTheme', systemTheme); + await updateConfig('systemTheme', systemTheme); }; // 当前主题设置方法 const setCurrentTheme = async (themeName: string): Promise => { - await updateAppearanceConfig('currentTheme', themeName); + await updateConfig('currentTheme', themeName); }; @@ -238,21 +188,12 @@ export const useConfigStore = defineStore('config', () => { const togglers = { tabIndent: createEditingToggler('enableTabIndent'), alwaysOnTop: async () => { - await updateGeneralConfig('alwaysOnTop', !state.config.general.alwaysOnTop); - // 立即应用窗口置顶状态 + await updateConfig('alwaysOnTop', !state.config.general.alwaysOnTop); await runtime.Window.SetAlwaysOnTop(state.config.general.alwaysOnTop); }, tabType: createEnumToggler('tabType', CONFIG_LIMITS.tabType.values) }; - // 字符串配置设置器 - const setters = { - fontFamily: async (value: string) => await updateEditingConfig('fontFamily', value), - fontWeight: async (value: string) => await updateEditingConfig('fontWeight', value), - dataPath: async (value: string) => await updateGeneralConfig('dataPath', value), - autoSaveDelay: async (value: number) => await updateEditingConfig('autoSaveDelay', value) - }; - return { // 状态 config: computed(() => state.config), @@ -281,10 +222,14 @@ export const useConfigStore = defineStore('config', () => { decreaseFontSize: adjusters.fontSize.decrease, resetFontSize: adjusters.fontSize.reset, setFontSize: adjusters.fontSize.set, + // 字体大小操作 + increaseFontSizeLocal: adjusters.fontSize.increaseLocal, + decreaseFontSizeLocal: adjusters.fontSize.decreaseLocal, + saveFontSize: () => saveConfig('fontSize'), // Tab操作 toggleTabIndent: togglers.tabIndent, - setEnableTabIndent: (value: boolean) => updateEditingConfig('enableTabIndent', value), + setEnableTabIndent: (value: boolean) => updateConfig('enableTabIndent', value), ...adjusters.tabSize, increaseTabSize: adjusters.tabSize.increase, decreaseTabSize: adjusters.tabSize.decrease, @@ -296,59 +241,53 @@ export const useConfigStore = defineStore('config', () => { // 窗口操作 toggleAlwaysOnTop: togglers.alwaysOnTop, - setAlwaysOnTop: (value: boolean) => updateGeneralConfig('alwaysOnTop', value), + setAlwaysOnTop: (value: boolean) => updateConfig('alwaysOnTop', value), // 字体操作 - setFontFamily: setters.fontFamily, - setFontWeight: setters.fontWeight, + setFontFamily: (value: string) => updateConfig('fontFamily', value), + setFontWeight: (value: string) => updateConfig('fontWeight', value), // 路径操作 - setDataPath: setters.dataPath, + setDataPath: (value: string) => updateConfig('dataPath', value), // 保存配置相关方法 - setAutoSaveDelay: setters.autoSaveDelay, + setAutoSaveDelay: (value: number) => updateConfig('autoSaveDelay', value), // 热键配置相关方法 - setEnableGlobalHotkey: (value: boolean) => updateGeneralConfig('enableGlobalHotkey', value), - setGlobalHotkey: (hotkey: any) => updateGeneralConfig('globalHotkey', hotkey), + setEnableGlobalHotkey: (value: boolean) => updateConfig('enableGlobalHotkey', value), + setGlobalHotkey: (hotkey: any) => updateConfig('globalHotkey', hotkey), // 系统托盘配置相关方法 - setEnableSystemTray: (value: boolean) => updateGeneralConfig('enableSystemTray', value), + setEnableSystemTray: (value: boolean) => updateConfig('enableSystemTray', value), // 开机启动配置相关方法 setStartAtLogin: async (value: boolean) => { - // 先更新配置文件 - await updateGeneralConfig('startAtLogin', value); - // 再调用系统设置API + await updateConfig('startAtLogin', value); await StartupService.SetEnabled(value); }, // 窗口吸附配置相关方法 - setEnableWindowSnap: async (value: boolean) => await updateGeneralConfig('enableWindowSnap', value), + setEnableWindowSnap: (value: boolean) => updateConfig('enableWindowSnap', value), // 加载动画配置相关方法 - setEnableLoadingAnimation: async (value: boolean) => await updateGeneralConfig('enableLoadingAnimation', value), + setEnableLoadingAnimation: (value: boolean) => updateConfig('enableLoadingAnimation', value), // 标签页配置相关方法 - setEnableTabs: async (value: boolean) => await updateGeneralConfig('enableTabs', value), + setEnableTabs: (value: boolean) => updateConfig('enableTabs', value), // 更新配置相关方法 - setAutoUpdate: async (value: boolean) => await updateUpdatesConfig('autoUpdate', value), + setAutoUpdate: (value: boolean) => updateConfig('autoUpdate', value), // 备份配置相关方法 - setEnableBackup: async (value: boolean) => { - await updateBackupConfig('enabled', value); - }, - setAutoBackup: async (value: boolean) => { - await updateBackupConfig('auto_backup', value); - }, - setRepoUrl: async (value: string) => await updateBackupConfig('repo_url', value), - setAuthMethod: async (value: AuthMethod) => await updateBackupConfig('auth_method', value), - setUsername: async (value: string) => await updateBackupConfig('username', value), - setPassword: async (value: string) => await updateBackupConfig('password', value), - setToken: async (value: string) => await updateBackupConfig('token', value), - setSshKeyPath: async (value: string) => await updateBackupConfig('ssh_key_path', value), - setSshKeyPassphrase: async (value: string) => await updateBackupConfig('ssh_key_passphrase', value), - setBackupInterval: async (value: number) => await updateBackupConfig('backup_interval', value), + setEnableBackup: (value: boolean) => updateConfig('enabled', value), + setAutoBackup: (value: boolean) => updateConfig('auto_backup', value), + setRepoUrl: (value: string) => updateConfig('repo_url', value), + setAuthMethod: (value: AuthMethod) => updateConfig('auth_method', value), + setUsername: (value: string) => updateConfig('username', value), + setPassword: (value: string) => updateConfig('password', value), + setToken: (value: string) => updateConfig('token', value), + setSshKeyPath: (value: string) => updateConfig('ssh_key_path', value), + setSshKeyPassphrase: (value: string) => updateConfig('ssh_key_passphrase', value), + setBackupInterval: (value: number) => updateConfig('backup_interval', value), }; }); \ No newline at end of file diff --git a/frontend/src/stores/editorStore.ts b/frontend/src/stores/editorStore.ts index 52c60a5..afb1791 100644 --- a/frontend/src/stores/editorStore.ts +++ b/frontend/src/stores/editorStore.ts @@ -29,8 +29,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'; +import markdownExtensions from "@/views/editor/extensions/markdown"; export interface DocumentStats { lines: number; @@ -242,10 +242,12 @@ export const useEditorStore = defineStore('editor', () => { fontWeight: configStore.config.editing.fontWeight }); - const wheelZoomExtension = createWheelZoomExtension( - () => configStore.increaseFontSize(), - () => configStore.decreaseFontSize() - ); + const wheelZoomExtension = createWheelZoomExtension({ + increaseFontSize: () => configStore.increaseFontSizeLocal(), + decreaseFontSize: () => configStore.decreaseFontSizeLocal(), + onSave: () => configStore.saveFontSize(), + saveDelay: 500 + }); // 统计扩展 const statsExtension = createStatsUpdateExtension(updateDocumentStats); @@ -261,8 +263,6 @@ export const useEditorStore = defineStore('editor', () => { const httpExtension = createHttpClientExtension(); - // Markdown预览扩展 - const previewExtension = markdownPreviewExtension(); // 再次检查操作有效性 if (!operationManager.isOperationValid(operationId, documentId)) { @@ -298,7 +298,7 @@ export const useEditorStore = defineStore('editor', () => { codeBlockExtension, ...dynamicExtensions, ...httpExtension, - previewExtension + markdownExtensions ]; // 创建编辑器状态 diff --git a/frontend/src/stores/themeStore.ts b/frontend/src/stores/themeStore.ts index 55787ef..a6bae00 100644 --- a/frontend/src/stores/themeStore.ts +++ b/frontend/src/stores/themeStore.ts @@ -138,7 +138,6 @@ export const useThemeStore = defineStore('theme', () => { const refreshEditorTheme = () => { applyThemeToDOM(currentTheme.value); - const editorStore = useEditorStore(); editorStore?.applyThemeSettings(); }; diff --git a/frontend/src/views/editor/Editor.vue b/frontend/src/views/editor/Editor.vue index adcdeaf..028f614 100644 --- a/frontend/src/views/editor/Editor.vue +++ b/frontend/src/views/editor/Editor.vue @@ -1,18 +1,17 @@ @@ -47,21 +45,17 @@ onBeforeUnmount(() => {
    - + - -
    - -
    - - -
    + +
    + - + - + - +
    @@ -74,15 +68,6 @@ onBeforeUnmount(() => { flex-direction: column; position: relative; - .editor-wrapper { - width: 100%; - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; - } - .editor { width: 100%; height: 100%; diff --git a/frontend/src/views/editor/basic/wheelZoomExtension.ts b/frontend/src/views/editor/basic/wheelZoomExtension.ts index b87a88a..59110b3 100644 --- a/frontend/src/views/editor/basic/wheelZoomExtension.ts +++ b/frontend/src/views/editor/basic/wheelZoomExtension.ts @@ -1,25 +1,40 @@ import {EditorView} from '@codemirror/view'; import type {Extension} from '@codemirror/state'; +import {createDebounce} from '@/common/utils/debounce'; -type FontAdjuster = () => Promise | void; +type FontAdjuster = () => void; +type SaveCallback = () => Promise | void; -const runAdjuster = (adjuster: FontAdjuster) => { - try { - const result = adjuster(); - if (result && typeof (result as Promise).then === 'function') { - (result as Promise).catch((error) => { - console.error('Failed to adjust font size:', error); - }); - } - } catch (error) { - console.error('Failed to adjust font size:', error); - } -}; +export interface WheelZoomOptions { + /** 增加字体大小的回调(立即执行) */ + increaseFontSize: FontAdjuster; + /** 减少字体大小的回调(立即执行) */ + decreaseFontSize: FontAdjuster; + /** 保存回调(防抖执行),在滚动结束后调用 */ + onSave?: SaveCallback; + /** 保存防抖延迟(毫秒),默认 300ms */ + saveDelay?: number; +} + +export const createWheelZoomExtension = (options: WheelZoomOptions): Extension => { + const {increaseFontSize, decreaseFontSize, onSave, saveDelay = 300} = options; + + // 如果有 onSave 回调,创建防抖版本 + const {debouncedFn: debouncedSave} = onSave + ? createDebounce(() => { + try { + const result = onSave(); + if (result && typeof (result as Promise).then === 'function') { + (result as Promise).catch((error) => { + console.error('Failed to save font size:', error); + }); + } + } catch (error) { + console.error('Failed to save font size:', error); + } + }, {delay: saveDelay}) + : {debouncedFn: null}; -export const createWheelZoomExtension = ( - increaseFontSize: FontAdjuster, - decreaseFontSize: FontAdjuster -): Extension => { return EditorView.domEventHandlers({ wheel(event) { if (!event.ctrlKey) { @@ -28,10 +43,16 @@ export const createWheelZoomExtension = ( event.preventDefault(); + // 立即更新字体大小 if (event.deltaY < 0) { - runAdjuster(increaseFontSize); + increaseFontSize(); } else if (event.deltaY > 0) { - runAdjuster(decreaseFontSize); + decreaseFontSize(); + } + + // 防抖保存 + if (debouncedSave) { + debouncedSave(); } return true; diff --git a/frontend/src/views/editor/extensions/codeblock/decorations.ts b/frontend/src/views/editor/extensions/codeblock/decorations.ts index 1ecf639..3b78782 100644 --- a/frontend/src/views/editor/extensions/codeblock/decorations.ts +++ b/frontend/src/views/editor/extensions/codeblock/decorations.ts @@ -115,6 +115,10 @@ const atomicNoteBlock = ViewPlugin.fromClass( /** * 块背景层 - 修复高度计算问题 + * + * 使用 lineBlockAt 获取行坐标,而不是 coordsAtPos 获取字符坐标。 + * 这样即使某些字符被隐藏(如 heading 的 # 标记 fontSize: 0), + * 行的坐标也不会受影响,边界线位置正确。 */ const blockLayer = layer({ above: false, @@ -135,14 +139,17 @@ const blockLayer = layer({ return; } - // view.coordsAtPos 如果编辑器不可见则返回 null - const fromCoordsTop = view.coordsAtPos(Math.max(block.content.from, view.visibleRanges[0].from))?.top; - let toCoordsBottom = view.coordsAtPos(Math.min(block.content.to, view.visibleRanges[view.visibleRanges.length - 1].to))?.bottom; + const fromPos = Math.max(block.content.from, view.visibleRanges[0].from); + const toPos = Math.min(block.content.to, view.visibleRanges[view.visibleRanges.length - 1].to); - if (fromCoordsTop === undefined || toCoordsBottom === undefined) { - idx++; - return; - } + // 使用 lineBlockAt 获取行的坐标,不受字符样式(如 fontSize: 0)影响 + const fromLineBlock = view.lineBlockAt(fromPos); + const toLineBlock = view.lineBlockAt(toPos); + + // lineBlockAt 返回的 top 是相对于内容区域的偏移 + // 转换为视口坐标进行后续计算 + const fromCoordsTop = fromLineBlock.top + view.documentTop; + let toCoordsBottom = toLineBlock.bottom + view.documentTop; // 对最后一个块进行特殊处理,让它直接延伸到底部 if (idx === blocks.length - 1) { @@ -151,7 +158,7 @@ const blockLayer = layer({ // 让最后一个块直接延伸到编辑器底部 if (contentBottom < editorHeight) { - const extraHeight = editorHeight - contentBottom-10; + const extraHeight = editorHeight - contentBottom - 10; toCoordsBottom += extraHeight; } } diff --git a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts index f63fc01..59f2044 100644 --- a/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts +++ b/frontend/src/views/editor/extensions/codeblock/lang-parser/languages.ts @@ -5,9 +5,14 @@ import {jsonLanguage} from "@codemirror/lang-json"; import {pythonLanguage} from "@codemirror/lang-python"; import {javascriptLanguage, typescriptLanguage} from "@codemirror/lang-javascript"; -import {htmlLanguage} from "@codemirror/lang-html"; +import {html, htmlLanguage} from "@codemirror/lang-html"; import {StandardSQL} from "@codemirror/lang-sql"; -import {markdownLanguage} from "@codemirror/lang-markdown"; +import {markdown, markdownLanguage} from "@codemirror/lang-markdown"; +import {Subscript, Superscript, Table} from "@lezer/markdown"; +import {Highlight} from "@/views/editor/extensions/markdown/syntax/highlight"; +import {Insert} from "@/views/editor/extensions/markdown/syntax/insert"; +import {Math} from "@/views/editor/extensions/markdown/syntax/math"; +import {Footnote} from "@/views/editor/extensions/markdown/syntax/footnote"; import {javaLanguage} from "@codemirror/lang-java"; import {phpLanguage} from "@codemirror/lang-php"; import {cssLanguage} from "@codemirror/lang-css"; @@ -22,9 +27,9 @@ import {wastLanguage} from "@codemirror/lang-wast"; import {sassLanguage} from "@codemirror/lang-sass"; import {lessLanguage} from "@codemirror/lang-less"; import {angularLanguage} from "@codemirror/lang-angular"; -import { svelteLanguage } from "@replit/codemirror-lang-svelte"; -import { httpLanguage } from "@/views/editor/extensions/httpclient/language/http-language"; -import { mermaidLanguage } from '@/views/editor/language/mermaid'; +import {svelteLanguage} from "@replit/codemirror-lang-svelte"; +import {httpLanguage} from "@/views/editor/extensions/httpclient/language/http-language"; +import {mermaidLanguage} from '@/views/editor/language/mermaid'; import {StreamLanguage} from "@codemirror/language"; import {ruby} from "@codemirror/legacy-modes/mode/ruby"; import {shell} from "@codemirror/legacy-modes/mode/shell"; @@ -64,6 +69,7 @@ import dartPrettierPlugin from "@/common/prettier/plugins/dart"; import luaPrettierPlugin from "@/common/prettier/plugins/lua"; import webPrettierPlugin from "@/common/prettier/plugins/web"; import * as prettierPluginEstree from "prettier/plugins/estree"; +import {languages} from "@codemirror/language-data"; /** * 语言信息类 @@ -110,7 +116,19 @@ export const LANGUAGES: LanguageInfo[] = [ parser: "sql", plugins: [sqlPrettierPlugin] }), - new LanguageInfo("md", "Markdown", markdownLanguage.parser, ["md"], { + new LanguageInfo("md", "Markdown", markdown({ + base: markdownLanguage, + extensions: [Subscript, Superscript, Highlight, Insert, Math, Footnote, Table], + completeHTMLTags: true, + pasteURLAsLink: true, + htmlTagLanguage: html({ + matchClosingTags: true, + autoCloseTags: true + }), + addKeymap: true, + codeLanguages: languages, + + }).language.parser, ["md"], { parser: "markdown", plugins: [markdownPrettierPlugin] }), diff --git a/frontend/src/views/editor/extensions/markdown/index.ts b/frontend/src/views/editor/extensions/markdown/index.ts new file mode 100644 index 0000000..ccac3aa --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/index.ts @@ -0,0 +1,41 @@ +import { Extension } from '@codemirror/state'; +import { blockquote } from './plugins/blockquote'; +import { codeblock } from './plugins/code-block'; +import { headings } from './plugins/heading'; +import { hideMarks } from './plugins/hide-mark'; +import { image } from './plugins/image'; +import { links } from './plugins/link'; +import { lists } from './plugins/list'; +import { headingSlugField } from './state/heading-slug'; +import { emoji } from './plugins/emoji'; +import { horizontalRule } from './plugins/horizontal-rule'; +import { inlineCode } from './plugins/inline-code'; +import { subscriptSuperscript } from './plugins/subscript-superscript'; +import { highlight } from './plugins/highlight'; +import { insert } from './plugins/insert'; +import { math } from './plugins/math'; +import { footnote } from './plugins/footnote'; + +/** + * markdown extensions + */ +export const markdownExtensions: Extension = [ + headingSlugField, + blockquote(), + codeblock(), + headings(), + hideMarks(), + lists(), + links(), + image(), + emoji(), + horizontalRule(), + inlineCode(), + subscriptSuperscript(), + highlight(), + insert(), + math(), + footnote(), +]; + +export default markdownExtensions; diff --git a/frontend/src/views/editor/extensions/markdown/plugins/blockquote.ts b/frontend/src/views/editor/extensions/markdown/plugins/blockquote.ts new file mode 100644 index 0000000..6455840 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/blockquote.ts @@ -0,0 +1,102 @@ +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate +} from '@codemirror/view'; +import { Range } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { isCursorInRange, invisibleDecoration } from '../util'; + +/** + * Blockquote plugin. + * + * Features: + * - Decorates blockquote with left border + * - Hides quote marks (>) when cursor is outside + * - Supports nested blockquotes + */ +export function blockquote() { + return [blockQuotePlugin, baseTheme]; +} + +/** + * Build blockquote decorations. + */ +function buildBlockQuoteDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + const processedLines = new Set(); + + syntaxTree(view.state).iterate({ + enter(node) { + if (node.type.name !== 'Blockquote') return; + + const cursorInBlockquote = isCursorInRange(view.state, [node.from, node.to]); + + // Only add decorations when cursor is outside the blockquote + // This allows selection highlighting to be visible when editing + if (!cursorInBlockquote) { + // Add line decoration for each line in the blockquote + const startLine = view.state.doc.lineAt(node.from).number; + const endLine = view.state.doc.lineAt(node.to).number; + + for (let i = startLine; i <= endLine; i++) { + if (!processedLines.has(i)) { + processedLines.add(i); + const line = view.state.doc.line(i); + decorations.push( + Decoration.line({ class: 'cm-blockquote' }).range(line.from) + ); + } + } + + // Hide quote marks when cursor is outside + const cursor = node.node.cursor(); + cursor.iterate((child) => { + if (child.type.name === 'QuoteMark') { + decorations.push( + invisibleDecoration.range(child.from, child.to) + ); + } + }); + } + + // Don't recurse into nested blockquotes (handled by outer iteration) + return false; + } + }); + + return Decoration.set(decorations, true); +} + +/** + * Blockquote plugin class. + */ +class BlockQuotePlugin { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = buildBlockQuoteDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged || update.selectionSet) { + this.decorations = buildBlockQuoteDecorations(update.view); + } + } +} + +const blockQuotePlugin = ViewPlugin.fromClass(BlockQuotePlugin, { + decorations: (v) => v.decorations +}); + +/** + * Base theme for blockquotes. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-blockquote': { + borderLeft: '4px solid var(--cm-blockquote-border, #ccc)', + color: 'var(--cm-blockquote-color, #666)' + } +}); diff --git a/frontend/src/views/editor/extensions/markdown/plugins/code-block.ts b/frontend/src/views/editor/extensions/markdown/plugins/code-block.ts new file mode 100644 index 0000000..3a23f39 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/code-block.ts @@ -0,0 +1,297 @@ +import { Extension, Range } from '@codemirror/state'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate, + WidgetType +} from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; +import { isCursorInRange } from '../util'; + +/** Code block node types in syntax tree */ +const CODE_BLOCK_TYPES = ['FencedCode', 'CodeBlock'] as const; + +/** Copy button icon SVGs (size controlled by CSS) */ +const ICON_COPY = ``; +const ICON_CHECK = ``; + +/** Cache for code block metadata */ +interface CodeBlockData { + from: number; + to: number; + language: string | null; + content: string; +} + +/** + * Code block extension with language label and copy button. + * + * Features: + * - Adds background styling to code blocks + * - Shows language label + copy button when language is specified + * - Hides markers when cursor is outside block + * - Optimized with viewport-only rendering + */ +export const codeblock = (): Extension => [codeBlockPlugin, baseTheme]; + +/** + * Widget for displaying language label and copy button. + * Handles click events directly on the button element. + */ +class CodeBlockInfoWidget extends WidgetType { + constructor( + readonly data: CodeBlockData, + readonly view: EditorView + ) { + super(); + } + + eq(other: CodeBlockInfoWidget): boolean { + return other.data.from === this.data.from && + other.data.language === this.data.language; + } + + toDOM(): HTMLElement { + const container = document.createElement('span'); + container.className = 'cm-code-block-info'; + + // Only show language label if specified + if (this.data.language) { + const lang = document.createElement('span'); + lang.className = 'cm-code-block-lang'; + lang.textContent = this.data.language; + container.append(lang); + } + + const btn = document.createElement('button'); + btn.className = 'cm-code-block-copy-btn'; + btn.title = 'Copy'; + btn.innerHTML = ICON_COPY; + + // Direct click handler - more reliable than eventHandlers + btn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.handleCopy(btn); + }); + + // Prevent mousedown from affecting editor + btn.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + + container.append(btn); + return container; + } + + private handleCopy(btn: HTMLButtonElement): void { + const content = getCodeContent(this.view, this.data.from, this.data.to); + if (!content) return; + + navigator.clipboard.writeText(content).then(() => { + btn.innerHTML = ICON_CHECK; + setTimeout(() => { + btn.innerHTML = ICON_COPY; + }, 1500); + }); + } + + // Ignore events to prevent editor focus changes + ignoreEvent(): boolean { + return true; + } +} + +/** + * Extract language from code block node. + */ +function getLanguage(view: EditorView, node: any, offset: number): string | null { + let lang: string | null = null; + node.toTree().iterate({ + enter: ({ type, from, to }) => { + if (type.name === 'CodeInfo') { + lang = view.state.doc.sliceString(offset + from, offset + to).trim(); + } + } + }); + return lang; +} + +/** + * Extract code content (without fence markers). + */ +function getCodeContent(view: EditorView, from: number, to: number): string { + const lines = view.state.doc.sliceString(from, to).split('\n'); + return lines.length >= 2 ? lines.slice(1, -1).join('\n') : ''; +} + +/** + * Build decorations for visible code blocks. + */ +function buildDecorations(view: EditorView): { decorations: DecorationSet; blocks: Map } { + const decorations: Range[] = []; + const blocks = new Map(); + const seen = new Set(); + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + if (!CODE_BLOCK_TYPES.includes(type.name as any)) return; + + const key = `${nodeFrom}:${nodeTo}`; + if (seen.has(key)) return; + seen.add(key); + + const inBlock = isCursorInRange(view.state, [nodeFrom, nodeTo]); + if (inBlock) return; + + const language = getLanguage(view, node, nodeFrom); + const startLine = view.state.doc.lineAt(nodeFrom); + const endLine = view.state.doc.lineAt(nodeTo); + + for (let num = startLine.number; num <= endLine.number; num++) { + const line = view.state.doc.line(num); + const pos: string[] = ['cm-codeblock']; + if (num === startLine.number) pos.push('cm-codeblock-begin'); + if (num === endLine.number) pos.push('cm-codeblock-end'); + + decorations.push( + Decoration.line({ class: pos.join(' ') }).range(line.from) + ); + } + + // Info widget with copy button (always show, language label only if specified) + const content = getCodeContent(view, nodeFrom, nodeTo); + const data: CodeBlockData = { from: nodeFrom, to: nodeTo, language, content }; + blocks.set(nodeFrom, data); + + decorations.push( + Decoration.widget({ + widget: new CodeBlockInfoWidget(data, view), + side: 1 + }).range(startLine.to) + ); + + // Hide markers + node.toTree().iterate({ + enter: ({ type: t, from: f, to: t2 }) => { + if (t.name === 'CodeInfo' || t.name === 'CodeMark') { + decorations.push(Decoration.replace({}).range(nodeFrom + f, nodeFrom + t2)); + } + } + }); + } + }); + } + + return { decorations: Decoration.set(decorations, true), blocks }; +} + +/** + * Code block plugin with optimized updates. + */ +class CodeBlockPluginClass { + decorations: DecorationSet; + blocks: Map; + private lastHead = -1; + + constructor(view: EditorView) { + const result = buildDecorations(view); + this.decorations = result.decorations; + this.blocks = result.blocks; + this.lastHead = view.state.selection.main.head; + } + + update(update: ViewUpdate): void { + const { docChanged, viewportChanged, selectionSet } = update; + + // Skip rebuild if cursor stayed on same line + if (selectionSet && !docChanged && !viewportChanged) { + const newHead = update.state.selection.main.head; + const oldLine = update.startState.doc.lineAt(this.lastHead).number; + const newLine = update.state.doc.lineAt(newHead).number; + + if (oldLine === newLine) { + this.lastHead = newHead; + return; + } + } + + if (docChanged || viewportChanged || selectionSet) { + const result = buildDecorations(update.view); + this.decorations = result.decorations; + this.blocks = result.blocks; + this.lastHead = update.state.selection.main.head; + } + } +} + +const codeBlockPlugin = ViewPlugin.fromClass(CodeBlockPluginClass, { + decorations: (v) => v.decorations +}); + +/** + * Base theme for code blocks. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-codeblock': { + backgroundColor: 'var(--cm-codeblock-bg)' + }, + '.cm-codeblock-begin': { + borderTopLeftRadius: 'var(--cm-codeblock-radius)', + borderTopRightRadius: 'var(--cm-codeblock-radius)', + position: 'relative', + boxShadow: 'inset 0 1px 0 var(--text-primary)' + }, + '.cm-codeblock-end': { + borderBottomLeftRadius: 'var(--cm-codeblock-radius)', + borderBottomRightRadius: 'var(--cm-codeblock-radius)', + boxShadow: 'inset 0 -1px 0 var(--text-primary)' + }, + '.cm-code-block-info': { + position: 'absolute', + right: '8px', + top: '50%', + transform: 'translateY(-50%)', + display: 'inline-flex', + alignItems: 'center', + gap: '0.5em', + zIndex: '5', + opacity: '0.5', + transition: 'opacity 0.15s' + }, + '.cm-code-block-info:hover': { + opacity: '1' + }, + '.cm-code-block-lang': { + color: 'var(--cm-codeblock-lang, var(--cm-foreground))', + textTransform: 'lowercase', + userSelect: 'none' + }, + '.cm-code-block-copy-btn': { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + padding: '0.15em', + border: 'none', + borderRadius: '2px', + background: 'transparent', + color: 'var(--cm-codeblock-lang, var(--cm-foreground))', + cursor: 'pointer', + opacity: '0.7', + transition: 'opacity 0.15s, background 0.15s' + }, + '.cm-code-block-copy-btn:hover': { + opacity: '1', + background: 'rgba(128, 128, 128, 0.2)' + }, + '.cm-code-block-copy-btn svg': { + width: '1em', + height: '1em' + } +}); diff --git a/frontend/src/views/editor/extensions/markdown/plugins/emoji.ts b/frontend/src/views/editor/extensions/markdown/plugins/emoji.ts new file mode 100644 index 0000000..4eb7fe4 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/emoji.ts @@ -0,0 +1,181 @@ +import { Extension, RangeSetBuilder } from '@codemirror/state'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate, + WidgetType +} from '@codemirror/view'; +import { isCursorInRange } from '../util'; +import { emojies } from '@/common/constant/emojies'; + +/** + * Emoji plugin that converts :emoji_name: to actual emoji characters. + * + * Features: + * - Detects emoji patterns like :smile:, :heart:, etc. + * - Replaces them with actual emoji characters + * - Shows the original text when cursor is nearby + * - Uses RangeSetBuilder for optimal performance + * - Supports 1900+ emojis from the comprehensive emoji dictionary + */ +export const emoji = (): Extension => [emojiPlugin, baseTheme]; + +/** + * Emoji regex pattern for matching :emoji_name: syntax. + */ +const EMOJI_REGEX = /:([a-z0-9_+\-]+):/gi; + +/** + * Emoji widget with optimized rendering. + */ +class EmojiWidget extends WidgetType { + constructor( + readonly emoji: string, + readonly name: string + ) { + super(); + } + + eq(other: EmojiWidget): boolean { + return other.emoji === this.emoji; + } + + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-emoji'; + span.textContent = this.emoji; + span.title = `:${this.name}:`; + return span; + } +} + +/** + * Match result for emoji patterns. + */ +interface EmojiMatch { + from: number; + to: number; + name: string; + emoji: string; +} + +/** + * Find all emoji matches in a text range. + */ +function findEmojiMatches(text: string, offset: number): EmojiMatch[] { + const matches: EmojiMatch[] = []; + let match: RegExpExecArray | null; + + // Reset regex state + EMOJI_REGEX.lastIndex = 0; + + while ((match = EMOJI_REGEX.exec(text)) !== null) { + const name = match[1].toLowerCase(); + const emoji = emojies[name]; + + if (emoji) { + matches.push({ + from: offset + match.index, + to: offset + match.index + match[0].length, + name, + emoji + }); + } + } + + return matches; +} + +/** + * Build emoji decorations using RangeSetBuilder. + */ +function buildEmojiDecorations(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + const doc = view.state.doc; + + for (const { from, to } of view.visibleRanges) { + const text = doc.sliceString(from, to); + const matches = findEmojiMatches(text, from); + + for (const match of matches) { + // Skip if cursor is in this range + if (isCursorInRange(view.state, [match.from, match.to])) { + continue; + } + + builder.add( + match.from, + match.to, + Decoration.replace({ + widget: new EmojiWidget(match.emoji, match.name) + }) + ); + } + } + + return builder.finish(); +} + +/** + * Emoji plugin with optimized update detection. + */ +class EmojiPlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildEmojiDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + // Always rebuild on doc or viewport change + if (update.docChanged || update.viewportChanged) { + this.decorations = buildEmojiDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + // For selection changes, check if we moved significantly + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + + // Only rebuild if cursor moved to a different position + if (newHead !== this.lastSelectionHead) { + this.decorations = buildEmojiDecorations(update.view); + this.lastSelectionHead = newHead; + } + } + } +} + +const emojiPlugin = ViewPlugin.fromClass(EmojiPlugin, { + decorations: (v) => v.decorations +}); + +/** + * Base theme for emoji. + * Inherits font size and line height from parent element. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-emoji': { + verticalAlign: 'middle', + cursor: 'default' + } +}); + +/** + * Get all available emoji names. + */ +export function getEmojiNames(): string[] { + return Object.keys(emojies); +} + +/** + * Get emoji by name. + */ +export function getEmoji(name: string): string | undefined { + return emojies[name.toLowerCase()]; +} diff --git a/frontend/src/views/editor/extensions/markdown/plugins/footnote.ts b/frontend/src/views/editor/extensions/markdown/plugins/footnote.ts new file mode 100644 index 0000000..2d24b31 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/footnote.ts @@ -0,0 +1,782 @@ +/** + * Footnote plugin for CodeMirror. + * + * Features: + * - Renders footnote references as superscript numbers/labels + * - Renders inline footnotes as superscript numbers with embedded content + * - Shows footnote content on hover (tooltip) + * - Click to jump between reference and definition + * - Hides syntax marks when cursor is outside + * + * Syntax (MultiMarkdown/PHP Markdown Extra): + * - Reference: [^id] → renders as superscript + * - Definition: [^id]: content + * - Inline footnote: ^[content] → renders as superscript with embedded content + */ + +import { Extension, Range, StateField, EditorState } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate, + WidgetType, + hoverTooltip, + Tooltip, +} from '@codemirror/view'; +import { isCursorInRange, invisibleDecoration } from '../util'; + +// ============================================================================ +// Types +// ============================================================================ + +/** + * Information about a footnote definition. + */ +interface FootnoteDefinition { + /** The footnote identifier (e.g., "1", "note") */ + id: string; + /** The content of the footnote */ + content: string; + /** Start position in document */ + from: number; + /** End position in document */ + to: number; +} + +/** + * Information about a footnote reference. + */ +interface FootnoteReference { + /** The footnote identifier */ + id: string; + /** Start position in document */ + from: number; + /** End position in document */ + to: number; + /** Numeric index (1-based, for display) */ + index: number; +} + +/** + * Information about an inline footnote. + */ +interface InlineFootnoteInfo { + /** The content of the inline footnote */ + content: string; + /** Start position in document */ + from: number; + /** End position in document */ + to: number; + /** Numeric index (1-based, for display) */ + index: number; +} + +/** + * Collected footnote data from the document. + * Uses Maps for O(1) lookup by position and id. + */ +interface FootnoteData { + definitions: Map; + references: FootnoteReference[]; + inlineFootnotes: InlineFootnoteInfo[]; + // Index maps for O(1) lookup + referencesByPos: Map; + inlineByPos: Map; + firstRefById: Map; +} + +// ============================================================================ +// Footnote Collection +// ============================================================================ + +/** + * Collect all footnote definitions, references, and inline footnotes from the document. + * Builds index maps for O(1) lookup during decoration and tooltip handling. + */ +function collectFootnotes(state: EditorState): FootnoteData { + const definitions = new Map(); + const references: FootnoteReference[] = []; + const inlineFootnotes: InlineFootnoteInfo[] = []; + // Index maps for fast lookup + const referencesByPos = new Map(); + const inlineByPos = new Map(); + const firstRefById = new Map(); + const seenIds = new Map(); + let inlineIndex = 0; + + syntaxTree(state).iterate({ + enter: ({ type, from, to, node }) => { + if (type.name === 'FootnoteDefinition') { + const labelNode = node.getChild('FootnoteDefinitionLabel'); + const contentNode = node.getChild('FootnoteDefinitionContent'); + + if (labelNode) { + const id = state.sliceDoc(labelNode.from, labelNode.to); + const content = contentNode + ? state.sliceDoc(contentNode.from, contentNode.to).trim() + : ''; + + definitions.set(id, { id, content, from, to }); + } + } else if (type.name === 'FootnoteReference') { + const labelNode = node.getChild('FootnoteReferenceLabel'); + + if (labelNode) { + const id = state.sliceDoc(labelNode.from, labelNode.to); + + if (!seenIds.has(id)) { + seenIds.set(id, seenIds.size + 1); + } + + const ref: FootnoteReference = { + id, + from, + to, + index: seenIds.get(id)!, + }; + + references.push(ref); + referencesByPos.set(from, ref); + + // Track first reference for each id + if (!firstRefById.has(id)) { + firstRefById.set(id, ref); + } + } + } else if (type.name === 'InlineFootnote') { + const contentNode = node.getChild('InlineFootnoteContent'); + + if (contentNode) { + const content = state.sliceDoc(contentNode.from, contentNode.to).trim(); + inlineIndex++; + + const info: InlineFootnoteInfo = { + content, + from, + to, + index: inlineIndex, + }; + + inlineFootnotes.push(info); + inlineByPos.set(from, info); + } + } + }, + }); + + return { + definitions, + references, + inlineFootnotes, + referencesByPos, + inlineByPos, + firstRefById, + }; +} + +// ============================================================================ +// State Field +// ============================================================================ + +/** + * State field to track footnote data across the document. + * This allows efficient lookup for tooltips and navigation. + */ +export const footnoteDataField = StateField.define({ + create(state) { + return collectFootnotes(state); + }, + update(value, tr) { + if (tr.docChanged) { + return collectFootnotes(tr.state); + } + return value; + }, +}); + +// ============================================================================ +// Widget +// ============================================================================ + +/** + * Widget to display footnote reference as superscript. + */ +class FootnoteRefWidget extends WidgetType { + constructor( + readonly id: string, + readonly index: number, + readonly hasDefinition: boolean + ) { + super(); + } + + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-footnote-ref'; + span.textContent = `[${this.index}]`; + span.dataset.footnoteId = this.id; + + if (!this.hasDefinition) { + span.classList.add('cm-footnote-ref-undefined'); + } + + return span; + } + + eq(other: FootnoteRefWidget): boolean { + return this.id === other.id && this.index === other.index; + } + + ignoreEvent(): boolean { + return false; + } +} + +/** + * Widget to display inline footnote as superscript. + */ +class InlineFootnoteWidget extends WidgetType { + constructor( + readonly content: string, + readonly index: number + ) { + super(); + } + + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-inline-footnote-ref'; + span.textContent = `[${this.index}]`; + span.dataset.footnoteContent = this.content; + span.dataset.footnoteIndex = String(this.index); + + return span; + } + + eq(other: InlineFootnoteWidget): boolean { + return this.content === other.content && this.index === other.index; + } + + ignoreEvent(): boolean { + return false; + } +} + +/** + * Widget to display footnote definition label. + */ +class FootnoteDefLabelWidget extends WidgetType { + constructor(readonly id: string) { + super(); + } + + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-footnote-def-label'; + span.textContent = `[${this.id}]`; + span.dataset.footnoteId = this.id; + return span; + } + + eq(other: FootnoteDefLabelWidget): boolean { + return this.id === other.id; + } + + ignoreEvent(): boolean { + return false; + } +} + +// ============================================================================ +// Decorations +// ============================================================================ + +/** + * Build decorations for footnote references and inline footnotes. + */ +function buildDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + const data = view.state.field(footnoteDataField); + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + // Handle footnote references + if (type.name === 'FootnoteReference') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + const labelNode = node.getChild('FootnoteReferenceLabel'); + const marks = node.getChildren('FootnoteReferenceMark'); + + if (!labelNode || marks.length < 2) return; + + const id = view.state.sliceDoc(labelNode.from, labelNode.to); + const ref = data.referencesByPos.get(nodeFrom); + + if (!cursorInRange && ref && ref.id === id) { + // Hide the entire syntax and show widget + decorations.push(invisibleDecoration.range(nodeFrom, nodeTo)); + + // Add widget at the end + const widget = new FootnoteRefWidget( + id, + ref.index, + data.definitions.has(id) + ); + decorations.push( + Decoration.widget({ + widget, + side: 1, + }).range(nodeTo) + ); + } + } + + // Handle footnote definitions + if (type.name === 'FootnoteDefinition') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + const marks = node.getChildren('FootnoteDefinitionMark'); + const labelNode = node.getChild('FootnoteDefinitionLabel'); + + if (!cursorInRange && marks.length >= 2 && labelNode) { + const id = view.state.sliceDoc(labelNode.from, labelNode.to); + + // Hide the entire [^id]: part + decorations.push(invisibleDecoration.range(marks[0].from, marks[1].to)); + + // Add widget to show [id] + const widget = new FootnoteDefLabelWidget(id); + decorations.push( + Decoration.widget({ + widget, + side: 1, + }).range(marks[1].to) + ); + } + } + + // Handle inline footnotes + if (type.name === 'InlineFootnote') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + const contentNode = node.getChild('InlineFootnoteContent'); + const marks = node.getChildren('InlineFootnoteMark'); + + if (!contentNode || marks.length < 2) return; + + const inlineNote = data.inlineByPos.get(nodeFrom); + + if (!cursorInRange && inlineNote) { + // Hide the entire syntax and show widget + decorations.push(invisibleDecoration.range(nodeFrom, nodeTo)); + + // Add widget at the end + const widget = new InlineFootnoteWidget( + inlineNote.content, + inlineNote.index + ); + decorations.push( + Decoration.widget({ + widget, + side: 1, + }).range(nodeTo) + ); + } + } + }, + }); + } + + return Decoration.set(decorations, true); +} + +// ============================================================================ +// Plugin Class +// ============================================================================ + +/** + * Footnote view plugin with optimized update detection. + */ +class FootnotePlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + if (newHead !== this.lastSelectionHead) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = newHead; + } + } + } +} + +const footnotePlugin = ViewPlugin.fromClass(FootnotePlugin, { + decorations: (v) => v.decorations, +}); + +// ============================================================================ +// Hover Tooltip +// ============================================================================ + +/** + * Hover tooltip that shows footnote content. + */ +const footnoteHoverTooltip = hoverTooltip( + (view, pos): Tooltip | null => { + const data = view.state.field(footnoteDataField); + + // Check if hovering over a footnote reference widget + const target = document.elementFromPoint( + view.coordsAtPos(pos)?.left ?? 0, + view.coordsAtPos(pos)?.top ?? 0 + ) as HTMLElement | null; + + if (target?.classList.contains('cm-footnote-ref')) { + const id = target.dataset.footnoteId; + if (id) { + const def = data.definitions.get(id); + if (def) { + return { + pos, + above: true, + arrow: true, + create: () => createTooltipDom(id, def.content), + }; + } + } + } + + // Check if hovering over an inline footnote widget + if (target?.classList.contains('cm-inline-footnote-ref')) { + const content = target.dataset.footnoteContent; + const index = target.dataset.footnoteIndex; + if (content && index) { + return { + pos, + above: true, + arrow: true, + create: () => createInlineTooltipDom(parseInt(index), content), + }; + } + } + + // Check if position is within a footnote reference node + let foundId: string | null = null; + let foundPos: number = pos; + let foundInlineContent: string | null = null; + let foundInlineIndex: number | null = null; + + syntaxTree(view.state).iterate({ + from: pos, + to: pos, + enter: ({ type, from, to, node }) => { + if (type.name === 'FootnoteReference') { + const labelNode = node.getChild('FootnoteReferenceLabel'); + if (labelNode && pos >= from && pos <= to) { + foundId = view.state.sliceDoc(labelNode.from, labelNode.to); + foundPos = to; + } + } else if (type.name === 'InlineFootnote') { + const contentNode = node.getChild('InlineFootnoteContent'); + if (contentNode && pos >= from && pos <= to) { + foundInlineContent = view.state.sliceDoc(contentNode.from, contentNode.to); + const inlineNote = data.inlineByPos.get(from); + if (inlineNote) { + foundInlineIndex = inlineNote.index; + } + foundPos = to; + } + } + }, + }); + + if (foundId) { + const def = data.definitions.get(foundId); + if (def) { + const tooltipId = foundId; + const tooltipPos = foundPos; + return { + pos: tooltipPos, + above: true, + arrow: true, + create: () => createTooltipDom(tooltipId, def.content), + }; + } + } + + if (foundInlineContent && foundInlineIndex !== null) { + const tooltipContent = foundInlineContent; + const tooltipIndex = foundInlineIndex; + const tooltipPos = foundPos; + return { + pos: tooltipPos, + above: true, + arrow: true, + create: () => createInlineTooltipDom(tooltipIndex, tooltipContent), + }; + } + + return null; + }, + { hoverTime: 300 } +); + +/** + * Create tooltip DOM element for regular footnote. + */ +function createTooltipDom(id: string, content: string): { dom: HTMLElement } { + const dom = document.createElement('div'); + dom.className = 'cm-footnote-tooltip'; + + const header = document.createElement('div'); + header.className = 'cm-footnote-tooltip-header'; + header.textContent = `[^${id}]`; + + const body = document.createElement('div'); + body.className = 'cm-footnote-tooltip-body'; + body.textContent = content || '(Empty footnote)'; + + dom.appendChild(header); + dom.appendChild(body); + + return { dom }; +} + +/** + * Create tooltip DOM element for inline footnote. + */ +function createInlineTooltipDom(index: number, content: string): { dom: HTMLElement } { + const dom = document.createElement('div'); + dom.className = 'cm-footnote-tooltip'; + + const header = document.createElement('div'); + header.className = 'cm-footnote-tooltip-header'; + header.textContent = `Inline Footnote [${index}]`; + + const body = document.createElement('div'); + body.className = 'cm-footnote-tooltip-body'; + body.textContent = content || '(Empty footnote)'; + + dom.appendChild(header); + dom.appendChild(body); + + return { dom }; +} + +// ============================================================================ +// Click Handler +// ============================================================================ + +/** + * Click handler for footnote navigation. + * Uses mousedown to intercept before editor moves cursor. + * - Click on reference → jump to definition + * - Click on definition label → jump to first reference + */ +const footnoteClickHandler = EditorView.domEventHandlers({ + mousedown(event, view) { + const target = event.target as HTMLElement; + + // Handle click on footnote reference widget + if (target.classList.contains('cm-footnote-ref')) { + const id = target.dataset.footnoteId; + if (id) { + const data = view.state.field(footnoteDataField); + const def = data.definitions.get(id); + if (def) { + // Prevent default to stop cursor from moving to widget position + event.preventDefault(); + // Use setTimeout to dispatch after mousedown completes + setTimeout(() => { + view.dispatch({ + selection: { anchor: def.from }, + scrollIntoView: true, + }); + view.focus(); + }, 0); + return true; + } + } + } + + // Handle click on definition label + if (target.classList.contains('cm-footnote-def-label')) { + const pos = view.posAtDOM(target); + if (pos !== null) { + const data = view.state.field(footnoteDataField); + + // Find which definition this belongs to + for (const [id, def] of data.definitions) { + if (pos >= def.from && pos <= def.to) { + // O(1) lookup for first reference + const firstRef = data.firstRefById.get(id); + if (firstRef) { + event.preventDefault(); + setTimeout(() => { + view.dispatch({ + selection: { anchor: firstRef.from }, + scrollIntoView: true, + }); + view.focus(); + }, 0); + return true; + } + break; + } + } + } + } + + return false; + }, +}); + +// ============================================================================ +// Theme +// ============================================================================ + +/** + * Base theme for footnotes. + */ +const baseTheme = EditorView.baseTheme({ + // Footnote reference (superscript) + '.cm-footnote-ref': { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '1em', + height: '1.2em', + padding: '0 0.25em', + marginLeft: '1px', + fontSize: '0.75em', + fontWeight: '500', + lineHeight: '1', + verticalAlign: 'super', + color: 'var(--cm-footnote-color, #1a73e8)', + backgroundColor: 'var(--cm-footnote-bg, rgba(26, 115, 232, 0.1))', + borderRadius: '3px', + cursor: 'pointer', + transition: 'all 0.15s ease', + textDecoration: 'none', + }, + '.cm-footnote-ref:hover': { + color: 'var(--cm-footnote-hover-color, #1557b0)', + backgroundColor: 'var(--cm-footnote-hover-bg, rgba(26, 115, 232, 0.2))', + }, + '.cm-footnote-ref-undefined': { + color: 'var(--cm-footnote-undefined-color, #d93025)', + backgroundColor: 'var(--cm-footnote-undefined-bg, rgba(217, 48, 37, 0.1))', + }, + + // Inline footnote reference (superscript) - uses distinct color + '.cm-inline-footnote-ref': { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + minWidth: '1em', + height: '1.2em', + padding: '0 0.25em', + marginLeft: '1px', + fontSize: '0.75em', + fontWeight: '500', + lineHeight: '1', + verticalAlign: 'super', + color: 'var(--cm-inline-footnote-color, #e67e22)', + backgroundColor: 'var(--cm-inline-footnote-bg, rgba(230, 126, 34, 0.1))', + borderRadius: '3px', + cursor: 'pointer', + transition: 'all 0.15s ease', + textDecoration: 'none', + }, + '.cm-inline-footnote-ref:hover': { + color: 'var(--cm-inline-footnote-hover-color, #d35400)', + backgroundColor: 'var(--cm-inline-footnote-hover-bg, rgba(230, 126, 34, 0.2))', + }, + + // Footnote definition label + '.cm-footnote-def-label': { + color: 'var(--cm-footnote-def-color, #1a73e8)', + fontWeight: '600', + cursor: 'pointer', + }, + '.cm-footnote-def-label:hover': { + textDecoration: 'underline', + }, + + // Tooltip + '.cm-footnote-tooltip': { + maxWidth: '400px', + padding: '0', + backgroundColor: 'var(--bg-secondary)', + border: '1px solid var(--border-color)', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + overflow: 'hidden', + }, + '.cm-footnote-tooltip-header': { + padding: '6px 12px', + fontSize: '0.8em', + fontWeight: '600', + fontFamily: 'monospace', + color: 'var(--cm-footnote-color, #1a73e8)', + backgroundColor: 'var(--bg-tertiary, rgba(0, 0, 0, 0.05))', + borderBottom: '1px solid var(--border-color)', + }, + '.cm-footnote-tooltip-body': { + padding: '10px 12px', + fontSize: '0.9em', + lineHeight: '1.5', + color: 'var(--text-primary)', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + }, + + // Tooltip animation + '.cm-tooltip:has(.cm-footnote-tooltip)': { + animation: 'cm-footnote-fade-in 0.15s ease-out', + }, + '@keyframes cm-footnote-fade-in': { + from: { opacity: '0', transform: 'translateY(4px)' }, + to: { opacity: '1', transform: 'translateY(0)' }, + }, +}); + +// ============================================================================ +// Export +// ============================================================================ + +/** + * Footnote extension. + * + * Features: + * - Parses footnote references [^id] and definitions [^id]: content + * - Parses inline footnotes ^[content] + * - Renders references and inline footnotes as superscript numbers + * - Shows definition/content on hover + * - Click to navigate between reference and definition + */ +export const footnote = (): Extension => [ + footnoteDataField, + footnotePlugin, + footnoteHoverTooltip, + footnoteClickHandler, + baseTheme, +]; + +export default footnote; + diff --git a/frontend/src/views/editor/extensions/markdown/plugins/heading.ts b/frontend/src/views/editor/extensions/markdown/plugins/heading.ts new file mode 100644 index 0000000..a8f89f0 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/heading.ts @@ -0,0 +1,96 @@ +import { syntaxTree } from '@codemirror/language'; +import { EditorState, StateField, Range } from '@codemirror/state'; +import { Decoration, DecorationSet, EditorView } from '@codemirror/view'; + +/** + * Hidden mark decoration - uses visibility: hidden to hide content + */ +const hiddenMarkDecoration = Decoration.mark({ + class: 'cm-heading-mark-hidden' +}); + +/** + * Check if selection overlaps with a range. + */ +function isSelectionInRange(state: EditorState, from: number, to: number): boolean { + return state.selection.ranges.some( + (range) => from <= range.to && to >= range.from + ); + } + + /** + * Build heading decorations. + * Hides # marks when cursor is not on the heading line. + */ +function buildHeadingDecorations(state: EditorState): DecorationSet { + const decorations: Range[] = []; + + syntaxTree(state).iterate({ + enter(node) { + // Skip if cursor is in this node's range + if (isSelectionInRange(state, node.from, node.to)) return; + + // Handle ATX headings (# Heading) + if (node.type.name.startsWith('ATXHeading')) { + const header = node.node.firstChild; + if (header && header.type.name === 'HeaderMark') { + const from = header.from; + // Include the space after # + const to = Math.min(header.to + 1, node.to); + decorations.push(hiddenMarkDecoration.range(from, to)); + } + } + // Handle Setext headings (underline style) + else if (node.type.name.startsWith('SetextHeading')) { + // Hide the underline marks (=== or ---) + const cursor = node.node.cursor(); + cursor.iterate((child) => { + if (child.type.name === 'HeaderMark') { + decorations.push( + hiddenMarkDecoration.range(child.from, child.to) + ); + } + }); + } + } + }); + + return Decoration.set(decorations, true); +} + +/** + * Heading StateField - manages # mark visibility. + */ +const headingField = StateField.define({ + create(state) { + return buildHeadingDecorations(state); + }, + + update(deco, tr) { + if (tr.docChanged || tr.selection) { + return buildHeadingDecorations(tr.state); + } + return deco.map(tr.changes); + }, + + provide: (f) => EditorView.decorations.from(f) +}); + +/** + * Theme for hidden heading marks. + * + * Uses fontSize: 0 to hide the # mark without leaving whitespace. + * This works correctly now because blockLayer uses lineBlockAt() + * which calculates coordinates based on the entire line, not + * individual characters, so fontSize: 0 doesn't affect boundaries. + */ +const headingTheme = EditorView.baseTheme({ + '.cm-heading-mark-hidden': { + fontSize: '0' + } +}); + +/** + * Headings plugin. + */ +export const headings = () => [headingField, headingTheme]; diff --git a/frontend/src/views/editor/extensions/markdown/plugins/hide-mark.ts b/frontend/src/views/editor/extensions/markdown/plugins/hide-mark.ts new file mode 100644 index 0000000..dcd6939 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/hide-mark.ts @@ -0,0 +1,140 @@ +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate +} from '@codemirror/view'; +import { RangeSetBuilder } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { checkRangeOverlap, isCursorInRange } from '../util'; + +/** + * Node types that contain markers as child elements. + */ +const TYPES_WITH_MARKS = new Set([ + 'Emphasis', + 'StrongEmphasis', + 'InlineCode', + 'Strikethrough' +]); + +/** + * Node types that are markers themselves. + */ +const MARK_TYPES = new Set([ + 'EmphasisMark', + 'CodeMark', + 'StrikethroughMark' +]); + +// Export for external use +export const typesWithMarks = Array.from(TYPES_WITH_MARKS); +export const markTypes = Array.from(MARK_TYPES); + +/** + * Build mark hiding decorations using RangeSetBuilder for optimal performance. + */ +function buildHideMarkDecorations(view: EditorView): DecorationSet { + const builder = new RangeSetBuilder(); + const replaceDecoration = Decoration.replace({}); + + // Track processed ranges to avoid duplicate processing of nested marks + let currentParentRange: [number, number] | null = null; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + if (!TYPES_WITH_MARKS.has(type.name)) return; + + // Skip if this is a nested element within a parent we're already processing + if (currentParentRange && checkRangeOverlap([nodeFrom, nodeTo], currentParentRange)) { + return; + } + + // Update current parent range + currentParentRange = [nodeFrom, nodeTo]; + + // Skip if cursor is in this range + if (isCursorInRange(view.state, [nodeFrom, nodeTo])) return; + + // Iterate through child marks + const innerTree = node.toTree(); + innerTree.iterate({ + enter({ type: markType, from: markFrom, to: markTo }) { + if (!MARK_TYPES.has(markType.name)) return; + + // Add decoration to hide the mark + builder.add( + nodeFrom + markFrom, + nodeFrom + markTo, + replaceDecoration + ); + } + }); + } + }); + } + + return builder.finish(); +} + +/** + * Hide marks plugin with optimized update detection. + * + * This plugin: + * - Hides emphasis marks (*, **, ~~ etc.) when cursor is outside + * - Uses RangeSetBuilder for efficient decoration construction + * - Optimizes selection change detection + */ +class HideMarkPlugin { + decorations: DecorationSet; + private lastSelectionRanges: string = ''; + + constructor(view: EditorView) { + this.decorations = buildHideMarkDecorations(view); + this.lastSelectionRanges = this.serializeSelection(view); + } + + update(update: ViewUpdate) { + // Always rebuild on doc or viewport change + if (update.docChanged || update.viewportChanged) { + this.decorations = buildHideMarkDecorations(update.view); + this.lastSelectionRanges = this.serializeSelection(update.view); + return; + } + + // For selection changes, check if selection actually changed positions + if (update.selectionSet) { + const newRanges = this.serializeSelection(update.view); + if (newRanges !== this.lastSelectionRanges) { + this.decorations = buildHideMarkDecorations(update.view); + this.lastSelectionRanges = newRanges; + } + } + } + + /** + * Serialize selection ranges for comparison. + */ + private serializeSelection(view: EditorView): string { + return view.state.selection.ranges + .map(r => `${r.from}:${r.to}`) + .join(','); + } +} + +/** + * Hide marks plugin. + * + * This plugin: + * - Hides marks when they are not in the editor selection + * - Supports emphasis, strong, inline code, and strikethrough + */ +export const hideMarks = () => [ + ViewPlugin.fromClass(HideMarkPlugin, { + decorations: (v) => v.decorations + }) +]; diff --git a/frontend/src/views/editor/extensions/markdown/plugins/highlight.ts b/frontend/src/views/editor/extensions/markdown/plugins/highlight.ts new file mode 100644 index 0000000..ca6b070 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/highlight.ts @@ -0,0 +1,115 @@ +import { Extension, Range } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate +} from '@codemirror/view'; +import { isCursorInRange, invisibleDecoration } from '../util'; + +/** + * Highlight plugin using syntax tree. + * + * Uses the custom Highlight extension to detect: + * - Highlight: ==text== → renders as highlighted text + * + * Examples: + * - This is ==important== text → This is important text + * - Please ==review this section== carefully + */ +export const highlight = (): Extension => [ + highlightPlugin, + baseTheme +]; + +/** + * Build decorations for highlight using syntax tree. + */ +function buildDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + // Handle Highlight nodes + if (type.name === 'Highlight') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + + // Get the mark nodes (the == characters) + const marks = node.getChildren('HighlightMark'); + + if (!cursorInRange && marks.length >= 2) { + // Hide the opening and closing == marks + decorations.push(invisibleDecoration.range(marks[0].from, marks[0].to)); + decorations.push(invisibleDecoration.range(marks[marks.length - 1].from, marks[marks.length - 1].to)); + + // Apply highlight style to the content between marks + const contentStart = marks[0].to; + const contentEnd = marks[marks.length - 1].from; + if (contentStart < contentEnd) { + decorations.push( + Decoration.mark({ + class: 'cm-highlight' + }).range(contentStart, contentEnd) + ); + } + } + } + } + }); + } + + return Decoration.set(decorations, true); +} + +/** + * Plugin class with optimized update detection. + */ +class HighlightPlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + if (newHead !== this.lastSelectionHead) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = newHead; + } + } + } +} + +const highlightPlugin = ViewPlugin.fromClass( + HighlightPlugin, + { + decorations: (v) => v.decorations + } +); + +/** + * Base theme for highlight. + * Uses mark decoration with a subtle background color. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-highlight': { + backgroundColor: 'var(--cm-highlight-background, rgba(255, 235, 59, 0.4))', + borderRadius: '2px', + } +}); + diff --git a/frontend/src/views/editor/extensions/markdown/plugins/horizontal-rule.ts b/frontend/src/views/editor/extensions/markdown/plugins/horizontal-rule.ts new file mode 100644 index 0000000..6ace7c2 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/horizontal-rule.ts @@ -0,0 +1,110 @@ +import { Extension, StateField, EditorState, Range } from '@codemirror/state'; +import { + DecorationSet, + Decoration, + EditorView, + WidgetType +} from '@codemirror/view'; +import { isCursorInRange } from '../util'; +import { syntaxTree } from '@codemirror/language'; + +/** + * Horizontal rule plugin that renders beautiful horizontal lines. + * + * This plugin: + * - Replaces markdown horizontal rules (---, ***, ___) with styled
    elements + * - Shows the original text when cursor is on the line + * - Uses inline widget to avoid affecting block system boundaries + */ +export const horizontalRule = (): Extension => [ + horizontalRuleField, + baseTheme +]; + +/** + * Widget to display a horizontal rule (inline version). + */ +class HorizontalRuleWidget extends WidgetType { + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-horizontal-rule-widget'; + + const hr = document.createElement('hr'); + hr.className = 'cm-horizontal-rule'; + span.appendChild(hr); + + return span; + } + + eq(_other: HorizontalRuleWidget) { + return true; + } + + ignoreEvent(): boolean { + return false; + } +} + +/** + * Build horizontal rule decorations. + * Uses Decoration.replace WITHOUT block: true to avoid affecting block system. + */ +function buildHorizontalRuleDecorations(state: EditorState): DecorationSet { + const decorations: Range[] = []; + + syntaxTree(state).iterate({ + enter: ({ type, from, to }) => { + if (type.name !== 'HorizontalRule') return; + + // Skip if cursor is on this line + if (isCursorInRange(state, [from, to])) return; + + // Replace the entire horizontal rule with a styled widget + // NOTE: NOT using block: true to avoid affecting codeblock boundaries + decorations.push( + Decoration.replace({ + widget: new HorizontalRuleWidget() + }).range(from, to) + ); + } + }); + + return Decoration.set(decorations, true); +} + +/** + * StateField for horizontal rule decorations. + */ +const horizontalRuleField = StateField.define({ + create(state) { + return buildHorizontalRuleDecorations(state); + }, + update(value, tx) { + if (tx.docChanged || tx.selection) { + return buildHorizontalRuleDecorations(tx.state); + } + return value.map(tx.changes); + }, + provide(field) { + return EditorView.decorations.from(field); + } +}); + +/** + * Base theme for horizontal rules. + * Uses inline-block display to render properly without block: true. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-horizontal-rule-widget': { + display: 'inline-block', + width: '100%', + verticalAlign: 'middle' + }, + '.cm-horizontal-rule': { + width: '100%', + height: '0', + border: 'none', + borderTop: '2px solid var(--cm-hr-color, rgba(128, 128, 128, 0.3))', + margin: '0.5em 0' + } +}); diff --git a/frontend/src/views/editor/extensions/markdown/plugins/html.ts b/frontend/src/views/editor/extensions/markdown/plugins/html.ts new file mode 100644 index 0000000..d270678 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/html.ts @@ -0,0 +1,208 @@ +import { syntaxTree } from '@codemirror/language'; +import { EditorState, Range } from '@codemirror/state'; +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, + WidgetType +} from '@codemirror/view'; +import DOMPurify from 'dompurify'; +import { isCursorInRange } from '../util'; + +interface EmbedBlockData { + from: number; + to: number; + content: string; +} + +/** + * Extract all HTML blocks from the document (both HTMLBlock and HTMLTag). + * Returns all blocks regardless of cursor position. + */ +function extractAllHTMLBlocks(state: EditorState): EmbedBlockData[] { + const blocks = new Array(); + syntaxTree(state).iterate({ + enter({ from, to, name }) { + // Support both block-level HTML (HTMLBlock) and inline HTML tags (HTMLTag) + if (name !== 'HTMLBlock' && name !== 'HTMLTag') return; + const html = state.sliceDoc(from, to); + const content = DOMPurify.sanitize(html); + + // Skip empty content after sanitization + if (!content.trim()) return; + + blocks.push({ from, to, content }); + } + }); + return blocks; +} + +/** + * Build decorations for HTML blocks. + * Only shows preview for blocks where cursor is not inside. + */ +function buildDecorations(state: EditorState, blocks: EmbedBlockData[]): DecorationSet { + const decorations: Range[] = []; + + for (const block of blocks) { + // Skip if cursor is in range + if (isCursorInRange(state, [block.from, block.to])) continue; + + // Hide the original HTML source code + decorations.push(Decoration.replace({}).range(block.from, block.to)); + + // Add the preview widget at the end + decorations.push( + Decoration.widget({ + widget: new HTMLBlockWidget(block), + side: 1 + }).range(block.to) + ); + } + + return Decoration.set(decorations, true); +} + +/** + * Check if selection affects any HTML block (cursor moved in/out of a block). + */ +function selectionAffectsBlocks( + state: EditorState, + prevState: EditorState, + blocks: EmbedBlockData[] +): boolean { + for (const block of blocks) { + const wasInRange = isCursorInRange(prevState, [block.from, block.to]); + const isInRange = isCursorInRange(state, [block.from, block.to]); + if (wasInRange !== isInRange) return true; + } + return false; +} + +/** + * ViewPlugin for HTML block preview. + * Uses smart caching to avoid unnecessary updates during text selection. + */ +class HTMLBlockPlugin { + decorations: DecorationSet; + blocks: EmbedBlockData[]; + + constructor(view: EditorView) { + this.blocks = extractAllHTMLBlocks(view.state); + this.decorations = buildDecorations(view.state, this.blocks); + } + + update(update: ViewUpdate) { + // If document changed, re-extract all blocks + if (update.docChanged) { + this.blocks = extractAllHTMLBlocks(update.state); + this.decorations = buildDecorations(update.state, this.blocks); + return; + } + + // If selection changed, only rebuild if cursor moved in/out of a block + if (update.selectionSet) { + if (selectionAffectsBlocks(update.state, update.startState, this.blocks)) { + this.decorations = buildDecorations(update.state, this.blocks); + } + } + } +} + +const htmlBlockPlugin = ViewPlugin.fromClass(HTMLBlockPlugin, { + decorations: (v) => v.decorations +}); + +class HTMLBlockWidget extends WidgetType { + constructor(public data: EmbedBlockData) { + super(); + } + + toDOM(view: EditorView): HTMLElement { + const wrapper = document.createElement('span'); + wrapper.className = 'cm-html-block-widget'; + + // Content container + const content = document.createElement('span'); + content.className = 'cm-html-block-content'; + // This is sanitized! + content.innerHTML = this.data.content; + + // Edit button + const editBtn = document.createElement('button'); + editBtn.className = 'cm-html-block-edit-btn'; + editBtn.innerHTML = ` + + + `; + editBtn.title = 'Edit HTML'; + + editBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + view.dispatch({ + selection: { anchor: this.data.from } + }); + view.focus(); + }); + + wrapper.appendChild(content); + wrapper.appendChild(editBtn); + + return wrapper; + } + + eq(widget: HTMLBlockWidget): boolean { + return JSON.stringify(widget.data) === JSON.stringify(this.data); + } +} + +/** + * Base theme for HTML blocks. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-html-block-widget': { + display: 'inline-block', + position: 'relative', + maxWidth: '100%', + overflow: 'auto', + verticalAlign: 'middle' + }, + '.cm-html-block-content': { + display: 'inline-block' + }, + // Ensure images are properly sized + '.cm-html-block-content img': { + maxWidth: '100%', + height: 'auto', + display: 'block' + }, + '.cm-html-block-edit-btn': { + position: 'absolute', + top: '4px', + right: '4px', + padding: '4px', + border: 'none', + borderRadius: '4px', + background: 'rgba(128, 128, 128, 0.2)', + color: 'inherit', + cursor: 'pointer', + opacity: '0', + transition: 'opacity 0.2s, background 0.2s', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + zIndex: '10' + }, + '.cm-html-block-widget:hover .cm-html-block-edit-btn': { + opacity: '1' + }, + '.cm-html-block-edit-btn:hover': { + background: 'rgba(128, 128, 128, 0.4)' + } +}); + +// Export the extension with theme +export const htmlBlockExtension = [htmlBlockPlugin, baseTheme]; diff --git a/frontend/src/views/editor/extensions/markdown/plugins/image.ts b/frontend/src/views/editor/extensions/markdown/plugins/image.ts new file mode 100644 index 0000000..7be6a1e --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/image.ts @@ -0,0 +1,220 @@ +import { syntaxTree } from '@codemirror/language'; +import { Extension, Range } from '@codemirror/state'; +import { + DecorationSet, + Decoration, + WidgetType, + EditorView, + ViewPlugin, + ViewUpdate, + hoverTooltip, + Tooltip +} from '@codemirror/view'; + +interface ImageInfo { + src: string; + from: number; + to: number; + alt: string; +} + +const IMAGE_EXT_RE = /\.(png|jpe?g|gif|webp|svg|bmp|ico|avif|apng|tiff?)(\?.*)?$/i; +const IMAGE_ALT_RE = /(?:!\[)(.*?)(?:\])/; +const ICON = ``; + +function isImageUrl(url: string): boolean { + return IMAGE_EXT_RE.test(url) || url.startsWith('data:image/'); +} + +function extractImages(view: EditorView): ImageInfo[] { + const result: ImageInfo[] = []; + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ name, node, from: f, to: t }) => { + if (name !== 'Image') return; + const urlNode = node.getChild('URL'); + if (!urlNode) return; + const src = view.state.sliceDoc(urlNode.from, urlNode.to); + if (!isImageUrl(src)) return; + const text = view.state.sliceDoc(f, t); + const alt = text.match(IMAGE_ALT_RE)?.[1] ?? ''; + result.push({ src, from: f, to: t, alt }); + } + }); + } + return result; +} + +class IndicatorWidget extends WidgetType { + constructor(readonly info: ImageInfo) { + super(); + } + + toDOM(): HTMLElement { + const el = document.createElement('span'); + el.className = 'cm-image-indicator'; + el.innerHTML = ICON; + return el; + } + + eq(other: IndicatorWidget): boolean { + return this.info.from === other.info.from && this.info.src === other.info.src; + } +} + +class ImagePlugin { + decorations: DecorationSet; + images: ImageInfo[] = []; + + constructor(view: EditorView) { + this.images = extractImages(view); + this.decorations = this.build(); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.images = extractImages(update.view); + this.decorations = this.build(); + } + } + + private build(): DecorationSet { + const deco: Range[] = []; + for (const img of this.images) { + deco.push(Decoration.widget({ widget: new IndicatorWidget(img), side: 1 }).range(img.to)); + } + return Decoration.set(deco, true); + } + + getImageAt(pos: number): ImageInfo | null { + for (const img of this.images) { + if (pos >= img.to && pos <= img.to + 1) { + return img; + } + } + return null; + } +} + +const imagePlugin = ViewPlugin.fromClass(ImagePlugin, { + decorations: (v) => v.decorations +}); + +const imageHoverTooltip = hoverTooltip( + (view, pos): Tooltip | null => { + const plugin = view.plugin(imagePlugin); + if (!plugin) return null; + + const img = plugin.getImageAt(pos); + if (!img) return null; + + return { + pos: img.to, + above: true, + arrow: true, + create: () => { + const dom = document.createElement('div'); + dom.className = 'cm-image-tooltip cm-image-loading'; + + const spinner = document.createElement('span'); + spinner.className = 'cm-image-spinner'; + + const imgEl = document.createElement('img'); + imgEl.src = img.src; + imgEl.alt = img.alt; + + imgEl.onload = () => { + dom.classList.remove('cm-image-loading'); + }; + imgEl.onerror = () => { + spinner.remove(); + imgEl.remove(); + dom.textContent = 'Failed to load image'; + dom.classList.remove('cm-image-loading'); + dom.classList.add('cm-image-tooltip-error'); + }; + + dom.append(spinner, imgEl); + return { dom }; + } + }; + }, + { hoverTime: 300 } +); + +const theme = EditorView.baseTheme({ + '.cm-image-indicator': { + display: 'inline-flex', + alignItems: 'center', + marginLeft: '4px', + verticalAlign: 'middle', + cursor: 'pointer', + opacity: '0.5', + color: 'var(--cm-link-color, #1a73e8)', + transition: 'opacity 0.15s', + '& svg': { width: '14px', height: '14px' } + }, + '.cm-image-indicator:hover': { opacity: '1' }, + '.cm-image-tooltip': { + position: 'relative', + background: ` + linear-gradient(45deg, #e0e0e0 25%, transparent 25%), + linear-gradient(-45deg, #e0e0e0 25%, transparent 25%), + linear-gradient(45deg, transparent 75%, #e0e0e0 75%), + linear-gradient(-45deg, transparent 75%, #e0e0e0 75%) + `, + backgroundColor: '#fff', + backgroundSize: '12px 12px', + backgroundPosition: '0 0, 0 6px, 6px -6px, -6px 0px', + border: '1px solid var(--border-color)', + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)', + '& img': { + display: 'block', + maxWidth: '60vw', + maxHeight: '50vh', + opacity: '1', + transition: 'opacity 0.15s ease-out' + } + }, + '.cm-image-loading': { + minWidth: '48px', + minHeight: '48px', + '& img': { opacity: '0' } + }, + '.cm-image-spinner': { + position: 'absolute', + top: '50%', + left: '50%', + width: '16px', + height: '16px', + marginTop: '-8px', + marginLeft: '-8px', + border: '2px solid #ccc', + borderTopColor: '#666', + borderRadius: '50%', + animation: 'cm-spin 0.5s linear infinite' + }, + '.cm-image-tooltip:not(.cm-image-loading) .cm-image-spinner': { + display: 'none' + }, + '@keyframes cm-spin': { + to: { transform: 'rotate(360deg)' } + }, + '.cm-image-tooltip-error': { + padding: '16px 24px', + fontSize: '12px', + color: 'var(--text-muted)' + }, + '.cm-tooltip-arrow:before': { + borderTopColor: 'var(--border-color) !important', + borderBottomColor: 'var(--border-color) !important' + }, + '.cm-tooltip-arrow:after': { + borderTopColor: '#fff !important', + borderBottomColor: '#fff !important' + } +}); + +export const image = (): Extension => [imagePlugin, imageHoverTooltip, theme]; diff --git a/frontend/src/views/editor/extensions/markdown/plugins/inline-code.ts b/frontend/src/views/editor/extensions/markdown/plugins/inline-code.ts new file mode 100644 index 0000000..f766e5e --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/inline-code.ts @@ -0,0 +1,111 @@ +import { Extension, Range } from '@codemirror/state'; +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate +} from '@codemirror/view'; +import { syntaxTree } from '@codemirror/language'; +import { isCursorInRange } from '../util'; + +/** + * Inline code styling plugin. + * + * This plugin adds visual styling to inline code (`code`): + * - Background color + * - Border radius + * - Padding effect via marks + */ +export const inlineCode = (): Extension => [inlineCodePlugin, baseTheme]; + +/** + * Build inline code decorations. + */ +function buildInlineCodeDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo }) => { + if (type.name !== 'InlineCode') return; + + const cursorInCode = isCursorInRange(view.state, [nodeFrom, nodeTo]); + + // Skip background decoration when cursor is in the code + // This allows selection highlighting to be visible when editing + if (cursorInCode) return; + + // Get the actual code content (excluding backticks) + const text = view.state.doc.sliceString(nodeFrom, nodeTo); + + // Find backtick positions + let codeStart = nodeFrom; + let codeEnd = nodeTo; + + // Skip opening backticks + let i = 0; + while (i < text.length && text[i] === '`') { + codeStart++; + i++; + } + + // Skip closing backticks + let j = text.length - 1; + while (j >= 0 && text[j] === '`') { + codeEnd--; + j--; + } + + // Only add decoration if there's actual content + if (codeStart < codeEnd) { + // Add mark decoration for the code content + decorations.push( + Decoration.mark({ + class: 'cm-inline-code' + }).range(codeStart, codeEnd) + ); + } + } + }); + } + + return Decoration.set(decorations, true); +} + +/** + * Inline code plugin class. + */ +class InlineCodePlugin { + decorations: DecorationSet; + + constructor(view: EditorView) { + this.decorations = buildInlineCodeDecorations(view); + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged || update.selectionSet) { + this.decorations = buildInlineCodeDecorations(update.view); + } + } +} + +const inlineCodePlugin = ViewPlugin.fromClass(InlineCodePlugin, { + decorations: (v) => v.decorations +}); + +/** + * Base theme for inline code. + * Uses CSS variables from variables.css for consistent theming. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-inline-code': { + backgroundColor: 'var(--cm-inline-code-bg)', + borderRadius: '0.25rem', + padding: '0.1rem 0.3rem', + fontFamily: 'var(--voidraft-font-mono)' + } +}); + diff --git a/frontend/src/views/editor/extensions/markdown/plugins/insert.ts b/frontend/src/views/editor/extensions/markdown/plugins/insert.ts new file mode 100644 index 0000000..c5e42d3 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/insert.ts @@ -0,0 +1,114 @@ +import { Extension, Range } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate +} from '@codemirror/view'; +import { isCursorInRange, invisibleDecoration } from '../util'; + +/** + * Insert plugin using syntax tree. + * + * Uses the custom Insert extension to detect: + * - Insert: ++text++ → renders as inserted text (underline) + * + * Examples: + * - This is ++inserted++ text → This is inserted text + * - Please ++review this section++ carefully + */ +export const insert = (): Extension => [ + insertPlugin, + baseTheme +]; + +/** + * Build decorations for insert using syntax tree. + */ +function buildDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + // Handle Insert nodes + if (type.name === 'Insert') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + + // Get the mark nodes (the ++ characters) + const marks = node.getChildren('InsertMark'); + + if (!cursorInRange && marks.length >= 2) { + // Hide the opening and closing ++ marks + decorations.push(invisibleDecoration.range(marks[0].from, marks[0].to)); + decorations.push(invisibleDecoration.range(marks[marks.length - 1].from, marks[marks.length - 1].to)); + + // Apply insert style to the content between marks + const contentStart = marks[0].to; + const contentEnd = marks[marks.length - 1].from; + if (contentStart < contentEnd) { + decorations.push( + Decoration.mark({ + class: 'cm-insert' + }).range(contentStart, contentEnd) + ); + } + } + } + } + }); + } + + return Decoration.set(decorations, true); +} + +/** + * Plugin class with optimized update detection. + */ +class InsertPlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + if (newHead !== this.lastSelectionHead) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = newHead; + } + } + } +} + +const insertPlugin = ViewPlugin.fromClass( + InsertPlugin, + { + decorations: (v) => v.decorations + } +); + +/** + * Base theme for insert. + * Uses underline decoration for inserted text. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-insert': { + textDecoration: 'underline', + } +}); + diff --git a/frontend/src/views/editor/extensions/markdown/plugins/link.ts b/frontend/src/views/editor/extensions/markdown/plugins/link.ts new file mode 100644 index 0000000..9acc0c5 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/link.ts @@ -0,0 +1,142 @@ +import { syntaxTree } from '@codemirror/language'; +import { Range } from '@codemirror/state'; +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate +} from '@codemirror/view'; +import { checkRangeOverlap, isCursorInRange, invisibleDecoration } from '../util'; + +/** + * Pattern for auto-link markers (< and >). + */ +const AUTO_LINK_MARK_RE = /^<|>$/g; + +/** + * Parent node types that should not process. + * - Image: handled by image plugin + * - LinkReference: reference link definitions like [label]: url should be fully visible + */ +const BLACKLISTED_PARENTS = new Set(['Image', 'LinkReference']); + +/** + * Links plugin. + * + * Features: + * - Hides link markup when cursor is outside + * - Link icons and click events are handled by hyperlink extension + */ +export const links = () => [goToLinkPlugin]; + +/** + * Build link decorations. + * Only hides markdown syntax marks, no icons added. + * Uses array + Decoration.set() for automatic sorting. + */ +function buildLinkDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + const selectionRanges = view.state.selection.ranges; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + if (type.name !== 'URL') return; + + const parent = node.parent; + if (!parent || BLACKLISTED_PARENTS.has(parent.name)) return; + + const marks = parent.getChildren('LinkMark'); + const linkTitle = parent.getChild('LinkTitle'); + + // Find the ']' mark position to distinguish between link text and link target + // Link structure: [display text](url) + // We should only hide the URL in the () part, not in the [] part + const closeBracketMark = marks.find((mark) => { + const text = view.state.sliceDoc(mark.from, mark.to); + return text === ']'; + }); + + // If URL is before ']', it's part of the display text, don't hide it + if (closeBracketMark && nodeFrom < closeBracketMark.from) { + return; + } + + // Check if cursor overlaps with the link + const cursorOverlaps = selectionRanges.some((range) => + checkRangeOverlap([range.from, range.to], [parent.from, parent.to]) + ); + + // Hide link marks and URL when cursor is outside + if (!cursorOverlaps && marks.length > 0) { + for (const mark of marks) { + decorations.push(invisibleDecoration.range(mark.from, mark.to)); + } + decorations.push(invisibleDecoration.range(nodeFrom, nodeTo)); + + if (linkTitle) { + decorations.push(invisibleDecoration.range(linkTitle.from, linkTitle.to)); + } + } + + // Get link content + const linkContent = view.state.sliceDoc(nodeFrom, nodeTo); + + // Handle auto-links with < > markers + if (AUTO_LINK_MARK_RE.test(linkContent)) { + if (!isCursorInRange(view.state, [node.from, node.to])) { + decorations.push(invisibleDecoration.range(nodeFrom, nodeFrom + 1)); + decorations.push(invisibleDecoration.range(nodeTo - 1, nodeTo)); + } + } + } + }); + } + + // Use Decoration.set with sort=true to handle unsorted ranges + return Decoration.set(decorations, true); +} + +/** + * Link plugin with optimized update detection. + */ +class LinkPlugin { + decorations: DecorationSet; + private lastSelectionRanges: string = ''; + + constructor(view: EditorView) { + this.decorations = buildLinkDecorations(view); + this.lastSelectionRanges = this.serializeSelection(view); + } + + update(update: ViewUpdate) { + // Always rebuild on doc or viewport change + if (update.docChanged || update.viewportChanged) { + this.decorations = buildLinkDecorations(update.view); + this.lastSelectionRanges = this.serializeSelection(update.view); + return; + } + + // For selection changes, check if selection actually changed + if (update.selectionSet) { + const newRanges = this.serializeSelection(update.view); + if (newRanges !== this.lastSelectionRanges) { + this.decorations = buildLinkDecorations(update.view); + this.lastSelectionRanges = newRanges; + } + } + } + + private serializeSelection(view: EditorView): string { + return view.state.selection.ranges + .map((r) => `${r.from}:${r.to}`) + .join(','); + } +} + +export const goToLinkPlugin = ViewPlugin.fromClass(LinkPlugin, { + decorations: (v) => v.decorations +}); diff --git a/frontend/src/views/editor/extensions/markdown/plugins/list.ts b/frontend/src/views/editor/extensions/markdown/plugins/list.ts new file mode 100644 index 0000000..0008dc1 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/list.ts @@ -0,0 +1,273 @@ +import { + Decoration, + DecorationSet, + EditorView, + ViewPlugin, + ViewUpdate, + WidgetType +} from '@codemirror/view'; +import { Range, StateField, Transaction } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { isCursorInRange } from '../util'; + +/** + * Pattern for bullet list markers. + */ +const BULLET_LIST_MARKER_RE = /^[-+*]$/; + +/** + * Lists plugin. + * + * Features: + * - Custom bullet mark rendering (- → •) + * - Interactive task list checkboxes + */ +export const lists = () => [listBulletPlugin, taskListField, baseTheme]; + +// ============================================================================ +// List Bullet Plugin +// ============================================================================ + +/** + * Widget to render list bullet mark. + */ +class ListBulletWidget extends WidgetType { + constructor(readonly bullet: string) { + super(); + } + + eq(other: ListBulletWidget): boolean { + return other.bullet === this.bullet; + } + + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-list-bullet'; + span.textContent = '•'; + return span; + } +} + +/** + * Build list bullet decorations. + */ +function buildListBulletDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + if (type.name !== 'ListMark') return; + + // Skip if this is part of a task list (has Task sibling) + const parent = node.parent; + if (parent) { + const task = parent.getChild('Task'); + if (task) return; + } + + // Skip if cursor is in this range + if (isCursorInRange(view.state, [nodeFrom, nodeTo])) return; + + const listMark = view.state.sliceDoc(nodeFrom, nodeTo); + if (BULLET_LIST_MARKER_RE.test(listMark)) { + decorations.push( + Decoration.replace({ + widget: new ListBulletWidget(listMark) + }).range(nodeFrom, nodeTo) + ); + } + } + }); + } + + return Decoration.set(decorations, true); +} + +/** + * List bullet plugin. + */ +class ListBulletPlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildListBulletDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildListBulletDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + const oldLine = update.startState.doc.lineAt(this.lastSelectionHead); + const newLine = update.state.doc.lineAt(newHead); + + if (oldLine.number !== newLine.number) { + this.decorations = buildListBulletDecorations(update.view); + } + this.lastSelectionHead = newHead; + } + } +} + +const listBulletPlugin = ViewPlugin.fromClass(ListBulletPlugin, { + decorations: (v) => v.decorations +}); + +// ============================================================================ +// Task List Plugin (using StateField to avoid flickering) +// ============================================================================ + +/** + * Widget to render checkbox for a task list item. + */ +class TaskCheckboxWidget extends WidgetType { + constructor( + readonly checked: boolean, + readonly pos: number // Position of the checkbox character in document + ) { + super(); + } + + eq(other: TaskCheckboxWidget): boolean { + return other.checked === this.checked && other.pos === this.pos; + } + + toDOM(view: EditorView): HTMLElement { + const wrap = document.createElement('span'); + wrap.setAttribute('aria-hidden', 'true'); + wrap.className = 'cm-task-checkbox'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = this.checked; + checkbox.tabIndex = -1; + + // Handle click directly in the widget + checkbox.addEventListener('mousedown', (e) => { + e.preventDefault(); + e.stopPropagation(); + + const newValue = !this.checked; + view.dispatch({ + changes: { + from: this.pos, + to: this.pos + 1, + insert: newValue ? 'x' : ' ' + } + }); + }); + + wrap.appendChild(checkbox); + return wrap; + } + + ignoreEvent(): boolean { + return false; + } +} + +/** + * Build task list decorations from state. + */ +function buildTaskListDecorations(state: import('@codemirror/state').EditorState): DecorationSet { + const decorations: Range[] = []; + + syntaxTree(state).iterate({ + enter: ({ type, from: taskFrom, to: taskTo, node }) => { + if (type.name !== 'Task') return; + + const listItem = node.parent; + if (!listItem || listItem.type.name !== 'ListItem') return; + + const listMark = listItem.getChild('ListMark'); + const taskMarker = node.getChild('TaskMarker'); + + if (!listMark || !taskMarker) return; + + const replaceFrom = listMark.from; + const replaceTo = taskMarker.to; + + // Check if cursor is in this range + if (isCursorInRange(state, [replaceFrom, replaceTo])) return; + + // Check if task is checked - position of x or space is taskMarker.from + 1 + const markerText = state.sliceDoc(taskMarker.from, taskMarker.to); + const isChecked = markerText.length >= 2 && 'xX'.includes(markerText[1]); + const checkboxPos = taskMarker.from + 1; // Position of the x or space + + if (isChecked) { + decorations.push( + Decoration.mark({ class: 'cm-task-checked' }).range(taskFrom, taskTo) + ); + } + + // Replace "- [x]" or "- [ ]" with checkbox widget + decorations.push( + Decoration.replace({ + widget: new TaskCheckboxWidget(isChecked, checkboxPos) + }).range(replaceFrom, replaceTo) + ); + } + }); + + return Decoration.set(decorations, true); +} + +/** + * Task list StateField - uses incremental updates to avoid flickering. + */ +const taskListField = StateField.define({ + create(state) { + return buildTaskListDecorations(state); + }, + + update(value, tr: Transaction) { + // Only rebuild when document or selection changes + if (tr.docChanged || tr.selection) { + return buildTaskListDecorations(tr.state); + } + return value; + }, + + provide(field) { + return EditorView.decorations.from(field); + } +}); + +// ============================================================================ +// Theme +// ============================================================================ + +/** + * Base theme for lists. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-list-bullet': { + color: 'var(--cm-list-bullet-color, inherit)' + }, + '.cm-task-checked': { + textDecoration: 'line-through', + opacity: '0.6' + }, + '.cm-task-checkbox': { + display: 'inline-block', + verticalAlign: 'baseline' + }, + '.cm-task-checkbox input': { + cursor: 'pointer', + margin: '0', + width: '1em', + height: '1em', + position: 'relative', + top: '0.1em' + } +}); diff --git a/frontend/src/views/editor/extensions/markdown/plugins/math.ts b/frontend/src/views/editor/extensions/markdown/plugins/math.ts new file mode 100644 index 0000000..f8f258d --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/math.ts @@ -0,0 +1,358 @@ +/** + * Math plugin for CodeMirror using KaTeX. + * + * Features: + * - Renders inline math $...$ as inline formula + * - Renders block math $$...$$ as block formula + * - Block math: lines remain, content hidden, formula overlays on top + * - Shows source when cursor is inside + */ + +import { Extension, Range } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate, + WidgetType +} from '@codemirror/view'; +import katex from 'katex'; +import 'katex/dist/katex.min.css'; +import { isCursorInRange, invisibleDecoration } from '../util'; + +// ============================================================================ +// Inline Math Widget +// ============================================================================ + +/** + * Widget to display inline math formula. + */ +class InlineMathWidget extends WidgetType { + private html: string; + private error: string | null = null; + + constructor(readonly latex: string) { + super(); + try { + this.html = katex.renderToString(latex, { + throwOnError: true, + displayMode: false, + output: 'html' + }); + } catch (e) { + this.error = e instanceof Error ? e.message : 'Render error'; + this.html = ''; + } + } + + toDOM(): HTMLElement { + const span = document.createElement('span'); + span.className = 'cm-inline-math'; + + if (this.error) { + span.textContent = this.latex; + span.title = this.error; + } else { + span.innerHTML = this.html; + } + + return span; + } + + eq(other: InlineMathWidget): boolean { + return this.latex === other.latex; + } + + ignoreEvent(): boolean { + return false; + } +} + +// ============================================================================ +// Block Math Widget +// ============================================================================ + +/** + * Widget to display block math formula. + * Uses absolute positioning to overlay on source lines. + */ +class BlockMathWidget extends WidgetType { + private html: string; + private error: string | null = null; + + constructor( + readonly latex: string, + readonly lineCount: number = 1, + readonly lineHeight: number = 22 + ) { + super(); + try { + this.html = katex.renderToString(latex, { + throwOnError: false, + displayMode: true, + output: 'html' + }); + } catch (e) { + this.error = e instanceof Error ? e.message : 'Render error'; + this.html = ''; + } + } + + toDOM(): HTMLElement { + const container = document.createElement('div'); + container.className = 'cm-block-math-container'; + // Set height to cover all source lines + const height = this.lineCount * this.lineHeight; + container.style.height = `${height}px`; + + const inner = document.createElement('div'); + inner.className = 'cm-block-math'; + + if (this.error) { + inner.textContent = this.latex; + inner.title = this.error; + } else { + inner.innerHTML = this.html; + } + + container.appendChild(inner); + return container; + } + + eq(other: BlockMathWidget): boolean { + return this.latex === other.latex && this.lineCount === other.lineCount; + } + + ignoreEvent(): boolean { + return false; + } +} + +// ============================================================================ +// Decorations +// ============================================================================ + +/** + * Build decorations for math formulas. + */ +function buildDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + // Handle inline math + if (type.name === 'InlineMath') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + const marks = node.getChildren('InlineMathMark'); + + if (!cursorInRange && marks.length >= 2) { + // Get latex content (without $ marks) + const latex = view.state.sliceDoc(marks[0].to, marks[marks.length - 1].from); + + // Hide the entire syntax + decorations.push(invisibleDecoration.range(nodeFrom, nodeTo)); + + // Add widget at the end + decorations.push( + Decoration.widget({ + widget: new InlineMathWidget(latex), + side: 1 + }).range(nodeTo) + ); + } + } + + // Handle block math ($$...$$) + if (type.name === 'BlockMath') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + const marks = node.getChildren('BlockMathMark'); + + if (!cursorInRange && marks.length >= 2) { + // Get latex content (without $$ marks) + const latex = view.state.sliceDoc(marks[0].to, marks[marks.length - 1].from).trim(); + + // Calculate line info + const startLine = view.state.doc.lineAt(nodeFrom); + const endLine = view.state.doc.lineAt(nodeTo); + const lineCount = endLine.number - startLine.number + 1; + const lineHeight = view.defaultLineHeight; + + // Check if block math spans multiple lines + const hasLineBreak = lineCount > 1; + + if (hasLineBreak) { + // For multi-line: use line decorations to hide content + for (let lineNum = startLine.number; lineNum <= endLine.number; lineNum++) { + const line = view.state.doc.line(lineNum); + decorations.push( + Decoration.line({ + class: 'cm-block-math-line' + }).range(line.from) + ); + } + + // Add widget on the first line (positioned absolutely) + decorations.push( + Decoration.widget({ + widget: new BlockMathWidget(latex, lineCount, lineHeight), + side: -1 + }).range(startLine.from) + ); + } else { + // Single line: make content transparent, overlay widget + decorations.push( + Decoration.mark({ + class: 'cm-block-math-content-hidden' + }).range(nodeFrom, nodeTo) + ); + + // Add widget at the start (positioned absolutely) + decorations.push( + Decoration.widget({ + widget: new BlockMathWidget(latex, 1, lineHeight), + side: -1 + }).range(nodeFrom) + ); + } + } + } + } + }); + } + + return Decoration.set(decorations, true); +} + +// ============================================================================ +// Plugin +// ============================================================================ + +/** + * Math plugin with optimized update detection. + */ +class MathPlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + if (newHead !== this.lastSelectionHead) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = newHead; + } + } + } +} + +const mathPlugin = ViewPlugin.fromClass( + MathPlugin, + { + decorations: (v) => v.decorations + } +); + +// ============================================================================ +// Theme +// ============================================================================ + +/** + * Base theme for math. + */ +const baseTheme = EditorView.baseTheme({ + // Inline math + '.cm-inline-math': { + display: 'inline', + verticalAlign: 'baseline', + }, + '.cm-inline-math .katex': { + fontSize: 'inherit', + }, + + // Block math container - absolute positioned to overlay on source + '.cm-block-math-container': { + position: 'absolute', + left: '0', + right: '0', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + pointerEvents: 'none', + zIndex: '1', + }, + + // Block math inner + '.cm-block-math': { + display: 'inline-block', + textAlign: 'center', + pointerEvents: 'auto', + }, + '.cm-block-math .katex-display': { + margin: '0', + }, + '.cm-block-math .katex': { + fontSize: '1.1em', + }, + + // Hidden line content for block math (text transparent but line preserved) + // Use high specificity to override rainbow brackets and other plugins + '.cm-line.cm-block-math-line': { + color: 'transparent !important', + caretColor: 'transparent', + }, + '.cm-line.cm-block-math-line span': { + color: 'transparent !important', + }, + // Override rainbow brackets in hidden math lines + '.cm-line.cm-block-math-line [class*="cm-rainbow-bracket"]': { + color: 'transparent !important', + }, + + // Hidden content for single-line block math + '.cm-block-math-content-hidden': { + color: 'transparent !important', + }, + '.cm-block-math-content-hidden span': { + color: 'transparent !important', + }, + '.cm-block-math-content-hidden [class*="cm-rainbow-bracket"]': { + color: 'transparent !important', + }, +}); + +// ============================================================================ +// Export +// ============================================================================ + +/** + * Math extension. + * + * Features: + * - Parses inline math $...$ and block math $$...$$ + * - Renders formulas using KaTeX + * - Block math preserves line structure, overlays rendered formula + * - Shows source when cursor is inside + */ +export const math = (): Extension => [ + mathPlugin, + baseTheme +]; + +export default math; + diff --git a/frontend/src/views/editor/extensions/markdown/plugins/subscript-superscript.ts b/frontend/src/views/editor/extensions/markdown/plugins/subscript-superscript.ts new file mode 100644 index 0000000..83f9d32 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/plugins/subscript-superscript.ts @@ -0,0 +1,152 @@ +import { Extension, Range } from '@codemirror/state'; +import { syntaxTree } from '@codemirror/language'; +import { + ViewPlugin, + DecorationSet, + Decoration, + EditorView, + ViewUpdate +} from '@codemirror/view'; +import { isCursorInRange, invisibleDecoration } from '../util'; + +/** + * Subscript and Superscript plugin using syntax tree. + * + * Uses lezer-markdown's Subscript and Superscript extensions to detect: + * - Superscript: ^text^ → renders as superscript + * - Subscript: ~text~ → renders as subscript + * + * Note: Inline footnotes ^[content] are handled by the Footnote extension + * which parses InlineFootnote before Superscript in the syntax tree. + * + * Examples: + * - 19^th^ → 19ᵗʰ (superscript) + * - H~2~O → H₂O (subscript) + */ +export const subscriptSuperscript = (): Extension => [ + subscriptSuperscriptPlugin, + baseTheme +]; + +/** + * Build decorations for subscript and superscript using syntax tree. + */ +function buildDecorations(view: EditorView): DecorationSet { + const decorations: Range[] = []; + + for (const { from, to } of view.visibleRanges) { + syntaxTree(view.state).iterate({ + from, + to, + enter: ({ type, from: nodeFrom, to: nodeTo, node }) => { + // Handle Superscript nodes + // Note: InlineFootnote ^[content] is parsed before Superscript, + // so we don't need to check for bracket patterns here + if (type.name === 'Superscript') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + + // Get the mark nodes (the ^ characters) + const marks = node.getChildren('SuperscriptMark'); + + if (!cursorInRange && marks.length >= 2) { + // Hide the opening and closing ^ marks + decorations.push(invisibleDecoration.range(marks[0].from, marks[0].to)); + decorations.push(invisibleDecoration.range(marks[marks.length - 1].from, marks[marks.length - 1].to)); + + // Apply superscript style to the content between marks + const contentStart = marks[0].to; + const contentEnd = marks[marks.length - 1].from; + if (contentStart < contentEnd) { + decorations.push( + Decoration.mark({ + class: 'cm-superscript' + }).range(contentStart, contentEnd) + ); + } + } + } + + // Handle Subscript nodes + if (type.name === 'Subscript') { + const cursorInRange = isCursorInRange(view.state, [nodeFrom, nodeTo]); + + // Get the mark nodes (the ~ characters) + const marks = node.getChildren('SubscriptMark'); + + if (!cursorInRange && marks.length >= 2) { + // Hide the opening and closing ~ marks + decorations.push(invisibleDecoration.range(marks[0].from, marks[0].to)); + decorations.push(invisibleDecoration.range(marks[marks.length - 1].from, marks[marks.length - 1].to)); + + // Apply subscript style to the content between marks + const contentStart = marks[0].to; + const contentEnd = marks[marks.length - 1].from; + if (contentStart < contentEnd) { + decorations.push( + Decoration.mark({ + class: 'cm-subscript' + }).range(contentStart, contentEnd) + ); + } + } + } + } + }); + } + + return Decoration.set(decorations, true); +} + +/** + * Plugin class with optimized update detection. + */ +class SubscriptSuperscriptPlugin { + decorations: DecorationSet; + private lastSelectionHead: number = -1; + + constructor(view: EditorView) { + this.decorations = buildDecorations(view); + this.lastSelectionHead = view.state.selection.main.head; + } + + update(update: ViewUpdate) { + if (update.docChanged || update.viewportChanged) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = update.state.selection.main.head; + return; + } + + if (update.selectionSet) { + const newHead = update.state.selection.main.head; + if (newHead !== this.lastSelectionHead) { + this.decorations = buildDecorations(update.view); + this.lastSelectionHead = newHead; + } + } + } +} + +const subscriptSuperscriptPlugin = ViewPlugin.fromClass( + SubscriptSuperscriptPlugin, + { + decorations: (v) => v.decorations + } +); + +/** + * Base theme for subscript and superscript. + * Uses mark decoration instead of widget to avoid layout issues. + * fontSize uses smaller value as subscript/superscript are naturally smaller. + */ +const baseTheme = EditorView.baseTheme({ + '.cm-superscript': { + verticalAlign: 'super', + fontSize: '0.75em', + color: 'var(--cm-superscript-color, inherit)' + }, + '.cm-subscript': { + verticalAlign: 'sub', + fontSize: '0.75em', + color: 'var(--cm-subscript-color, inherit)' + } +}); diff --git a/frontend/src/views/editor/extensions/markdown/state/heading-slug.ts b/frontend/src/views/editor/extensions/markdown/state/heading-slug.ts new file mode 100644 index 0000000..9f93d58 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/state/heading-slug.ts @@ -0,0 +1,56 @@ +import { syntaxTree } from '@codemirror/language'; +import { EditorState, StateField } from '@codemirror/state'; +import { Slugger } from '../util'; +import {SyntaxNode} from "@lezer/common"; + +/** + * A heading slug is a string that is used to identify/reference + * a heading in the document. Heading slugs are URI-compatible and can be used + * in permalinks as heading IDs. + */ +export interface HeadingSlug { + slug: string; + pos: number; +} + +/** + * A plugin that stores the calculated slugs of the document headings in the + * editor state. These can be useful when resolving links to headings inside + * the document. + */ +export const headingSlugField = StateField.define({ + create: (state) => { + const slugs = extractSlugs(state); + return slugs; + }, + update: (value, tx) => { + if (tx.docChanged) return extractSlugs(tx.state); + return value; + }, + compare: (a, b) => + a.length === b.length && + a.every((slug, i) => slug.slug === b[i].slug && slug.pos === b[i].pos) +}); + +/** + * + * @param state - The current editor state. + * @returns An array of heading slugs. + */ +function extractSlugs(state: EditorState): HeadingSlug[] { + const slugs: HeadingSlug[] = []; + const slugger = new Slugger(); + syntaxTree(state).iterate({ + enter: ({ name, from, to, node }) => { + // Capture ATXHeading and SetextHeading + if (!name.includes('Heading')) return; + const mark: SyntaxNode | null = node.getChild('HeaderMark'); + + const headerText = state.sliceDoc(from, to).split(''); + headerText.splice(mark!.from - from, mark!.to - mark!.from); + const slug = slugger.slug(headerText.join('').trim()); + slugs.push({ slug, pos: from }); + } + }); + return slugs; +} diff --git a/frontend/src/views/editor/extensions/markdown/syntax/footnote.ts b/frontend/src/views/editor/extensions/markdown/syntax/footnote.ts new file mode 100644 index 0000000..d3b7536 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/syntax/footnote.ts @@ -0,0 +1,259 @@ +/** + * Footnote extension for Lezer Markdown parser. + * + * Parses footnote syntax compatible with MultiMarkdown/PHP Markdown Extra. + * + * Syntax: + * - Footnote reference: [^id] or [^1] + * - Footnote definition: [^id]: content (at line start) + * - Inline footnote: ^[content] (content is inline, no separate definition needed) + * + * Examples: + * - This is text[^1] with a footnote. + * - [^1]: This is the footnote content. + * - This is text^[inline footnote content] with inline footnote. + */ + +import { MarkdownConfig, Line, BlockContext, InlineContext } from '@lezer/markdown'; +import { CharCode, isFootnoteIdChar } from '../util'; + +/** + * Parse inline footnote ^[content]. + * + * @param cx - Inline context + * @param pos - Start position (at ^) + * @returns Position after element, or -1 if no match + */ +function parseInlineFootnote(cx: InlineContext, pos: number): number { + const end = cx.end; + + // Minimum: ^[ + content + ] = at least 4 chars + if (end < pos + 3) return -1; + + // Track bracket depth for nested brackets + let bracketDepth = 1; + let hasContent = false; + const contentStart = pos + 2; + + for (let i = contentStart; i < end; i++) { + const char = cx.char(i); + + // Don't allow newlines + if (char === CharCode.Newline) return -1; + + // Track bracket depth + if (char === CharCode.OpenBracket) { + bracketDepth++; + } else if (char === CharCode.CloseBracket) { + bracketDepth--; + if (bracketDepth === 0) { + // Found closing bracket - must have content + if (!hasContent) return -1; + + // Create element with marks and content + return cx.addElement(cx.elt('InlineFootnote', pos, i + 1, [ + cx.elt('InlineFootnoteMark', pos, contentStart), + cx.elt('InlineFootnoteContent', contentStart, i), + cx.elt('InlineFootnoteMark', i, i + 1) + ])); + } + } else { + hasContent = true; + } + } + + return -1; +} + +/** + * Parse footnote reference [^id]. + * + * @param cx - Inline context + * @param pos - Start position (at [) + * @returns Position after element, or -1 if no match + */ +function parseFootnoteReference(cx: InlineContext, pos: number): number { + const end = cx.end; + + // Minimum: [^ + id + ] = at least 4 chars + if (end < pos + 3) return -1; + + let hasValidId = false; + const labelStart = pos + 2; + + for (let i = labelStart; i < end; i++) { + const char = cx.char(i); + + // Found closing bracket + if (char === CharCode.CloseBracket) { + if (!hasValidId) return -1; + + // Create element with marks and label + return cx.addElement(cx.elt('FootnoteReference', pos, i + 1, [ + cx.elt('FootnoteReferenceMark', pos, labelStart), + cx.elt('FootnoteReferenceLabel', labelStart, i), + cx.elt('FootnoteReferenceMark', i, i + 1) + ])); + } + + // Don't allow newlines + if (char === CharCode.Newline) return -1; + + // Validate id character using O(1) lookup table + if (isFootnoteIdChar(char)) { + hasValidId = true; + } else { + return -1; + } + } + + return -1; +} + +/** + * Parse footnote definition [^id]: content. + * + * @param cx - Block context + * @param line - Current line + * @returns True if parsed successfully + */ +function parseFootnoteDefinition(cx: BlockContext, line: Line): boolean { + const text = line.text; + const len = text.length; + + // Minimum: [^id]: = at least 5 chars + if (len < 5) return false; + + // Find ]: pattern - use O(1) lookup for ID chars + let labelEnd = 2; + while (labelEnd < len) { + const char = text.charCodeAt(labelEnd); + + if (char === CharCode.CloseBracket) { + // Check for : after ] + if (labelEnd + 1 < len && text.charCodeAt(labelEnd + 1) === CharCode.Colon) { + break; + } + return false; + } + + // Use O(1) lookup table + if (!isFootnoteIdChar(char)) return false; + + labelEnd++; + } + + // Validate ]: was found + if (labelEnd >= len || + text.charCodeAt(labelEnd) !== CharCode.CloseBracket || + text.charCodeAt(labelEnd + 1) !== CharCode.Colon) { + return false; + } + + // Calculate positions (all at once to avoid repeated arithmetic) + const start = cx.lineStart; + const openMarkEnd = start + 2; + const labelEndPos = start + labelEnd; + const closeMarkEnd = start + labelEnd + 2; + + // Skip optional space after : + let contentOffset = labelEnd + 2; + if (contentOffset < len) { + const spaceChar = text.charCodeAt(contentOffset); + if (spaceChar === CharCode.Space || spaceChar === CharCode.Tab) { + contentOffset++; + } + } + + // Build children array + const children = [ + cx.elt('FootnoteDefinitionMark', start, openMarkEnd), + cx.elt('FootnoteDefinitionLabel', openMarkEnd, labelEndPos), + cx.elt('FootnoteDefinitionMark', labelEndPos, closeMarkEnd) + ]; + + // Add content if present + if (contentOffset < len) { + children.push(cx.elt('FootnoteDefinitionContent', start + contentOffset, start + len)); + } + + // Create and add block element + cx.addElement(cx.elt('FootnoteDefinition', start, start + len, children)); + cx.nextLine(); + return true; +} + +/** + * Footnote extension for Lezer Markdown. + * + * Defines nodes: + * - FootnoteReference: Inline reference [^id] + * - FootnoteReferenceMark: The [^ and ] delimiters + * - FootnoteReferenceLabel: The id part + * - FootnoteDefinition: Block definition [^id]: content + * - FootnoteDefinitionMark: The [^, ]: delimiters + * - FootnoteDefinitionLabel: The id part in definition + * - FootnoteDefinitionContent: The content part + * - InlineFootnote: Inline footnote ^[content] + * - InlineFootnoteMark: The ^[ and ] delimiters + * - InlineFootnoteContent: The content part + */ +export const Footnote: MarkdownConfig = { + defineNodes: [ + // Inline reference nodes + { name: 'FootnoteReference' }, + { name: 'FootnoteReferenceMark' }, + { name: 'FootnoteReferenceLabel' }, + // Block definition nodes + { name: 'FootnoteDefinition', block: true }, + { name: 'FootnoteDefinitionMark' }, + { name: 'FootnoteDefinitionLabel' }, + { name: 'FootnoteDefinitionContent' }, + // Inline footnote nodes + { name: 'InlineFootnote' }, + { name: 'InlineFootnoteMark' }, + { name: 'InlineFootnoteContent' }, + ], + + parseInline: [ + { + name: 'InlineFootnote', + parse(cx, next, pos) { + // Fast path: must start with ^[ + if (next !== CharCode.Caret || cx.char(pos + 1) !== CharCode.OpenBracket) { + return -1; + } + return parseInlineFootnote(cx, pos); + }, + before: 'Superscript', + }, + { + name: 'FootnoteReference', + parse(cx, next, pos) { + // Fast path: must start with [^ + if (next !== CharCode.OpenBracket || cx.char(pos + 1) !== CharCode.Caret) { + return -1; + } + return parseFootnoteReference(cx, pos); + }, + before: 'Link', + }, + ], + + parseBlock: [ + { + name: 'FootnoteDefinition', + parse(cx: BlockContext, line: Line): boolean { + // Fast path: must start with [^ + if (line.text.charCodeAt(0) !== CharCode.OpenBracket || + line.text.charCodeAt(1) !== CharCode.Caret) { + return false; + } + return parseFootnoteDefinition(cx, line); + }, + before: 'LinkReference', + }, + ], +}; + +export default Footnote; diff --git a/frontend/src/views/editor/extensions/markdown/syntax/highlight.ts b/frontend/src/views/editor/extensions/markdown/syntax/highlight.ts new file mode 100644 index 0000000..d20bc15 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/syntax/highlight.ts @@ -0,0 +1,38 @@ +/** + * Highlight extension for Lezer Markdown parser. + * + * Parses ==highlight== syntax similar to Obsidian/Mark style. + * + * Syntax: ==text== → renders as highlighted text + * + * Example: + * - This is ==important== text → This is important text + */ + +import { MarkdownConfig } from '@lezer/markdown'; +import { CharCode, createPairedDelimiterParser } from '../util'; + +/** + * Highlight extension for Lezer Markdown. + * Defines: + * - Highlight: The container node for highlighted content + * - HighlightMark: The == delimiter marks + */ +export const Highlight: MarkdownConfig = { + defineNodes: [ + { name: 'Highlight' }, + { name: 'HighlightMark' } + ], + parseInline: [ + createPairedDelimiterParser({ + name: 'Highlight', + nodeName: 'Highlight', + markName: 'HighlightMark', + delimChar: CharCode.Equal, + isDouble: true, + after: 'Emphasis' + }) + ] +}; + +export default Highlight; diff --git a/frontend/src/views/editor/extensions/markdown/syntax/insert.ts b/frontend/src/views/editor/extensions/markdown/syntax/insert.ts new file mode 100644 index 0000000..db0268a --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/syntax/insert.ts @@ -0,0 +1,41 @@ +/** + * Insert extension for Lezer Markdown parser. + * + * Parses ++insert++ syntax for inserted/underlined text. + * + * Syntax: ++text++ → renders as inserted text (underline) + * + * Example: + * - This is ++inserted++ text → This is inserted text + */ + +import { MarkdownConfig } from '@lezer/markdown'; +import { CharCode, createPairedDelimiterParser } from '../util'; + +/** + * Insert extension for Lezer Markdown. + * + * Uses optimized factory function for O(n) single-pass parsing. + * + * Defines: + * - Insert: The container node for inserted content + * - InsertMark: The ++ delimiter marks + */ +export const Insert: MarkdownConfig = { + defineNodes: [ + { name: 'Insert' }, + { name: 'InsertMark' } + ], + parseInline: [ + createPairedDelimiterParser({ + name: 'Insert', + nodeName: 'Insert', + markName: 'InsertMark', + delimChar: CharCode.Plus, + isDouble: true, + after: 'Emphasis' + }) + ] +}; + +export default Insert; diff --git a/frontend/src/views/editor/extensions/markdown/syntax/math.ts b/frontend/src/views/editor/extensions/markdown/syntax/math.ts new file mode 100644 index 0000000..b6f2548 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/syntax/math.ts @@ -0,0 +1,146 @@ +/** + * Math extension for Lezer Markdown parser. + * + * Parses LaTeX math syntax: + * - Inline math: $E=mc^2$ → renders as inline formula + * - Block math: $$...$$ → renders as block formula (can be multi-line) + */ + +import { MarkdownConfig, InlineContext } from '@lezer/markdown'; +import { CharCode } from '../util'; + +/** + * Parse block math ($$...$$). + * Allows multi-line content and handles escaped $. + * + * @param cx - Inline context + * @param pos - Start position (at first $) + * @returns Position after element, or -1 if no match + */ +function parseBlockMath(cx: InlineContext, pos: number): number { + const end = cx.end; + + // Don't match $$$ or more + if (cx.char(pos + 2) === CharCode.Dollar) return -1; + + // Minimum: $$ + content + $$ = at least 5 chars + const minEnd = pos + 4; + if (end < minEnd) return -1; + + // Search for closing $$ + const searchEnd = end - 1; + for (let i = pos + 2; i < searchEnd; i++) { + const char = cx.char(i); + + // Skip escaped $ (backslash followed by any char) + if (char === CharCode.Backslash) { + i++; // Skip next char + continue; + } + + // Found potential closing $$ + if (char === CharCode.Dollar) { + const nextChar = cx.char(i + 1); + if (nextChar !== CharCode.Dollar) continue; + + // Don't match $$$ + if (i + 2 < end && cx.char(i + 2) === CharCode.Dollar) continue; + + // Ensure content exists + if (i === pos + 2) return -1; + + // Create element with marks + return cx.addElement(cx.elt('BlockMath', pos, i + 2, [ + cx.elt('BlockMathMark', pos, pos + 2), + cx.elt('BlockMathMark', i, i + 2) + ])); + } + } + + return -1; +} + +/** + * Parse inline math ($...$). + * Single line only, handles escaped $. + * + * @param cx - Inline context + * @param pos - Start position (at $) + * @returns Position after element, or -1 if no match + */ +function parseInlineMath(cx: InlineContext, pos: number): number { + const end = cx.end; + + // Don't match if preceded by backslash (escaped) + if (pos > 0 && cx.char(pos - 1) === CharCode.Backslash) return -1; + + // Minimum: $ + content + $ = at least 3 chars + if (end < pos + 2) return -1; + + // Search for closing $ + for (let i = pos + 1; i < end; i++) { + const char = cx.char(i); + + // Newline not allowed in inline math + if (char === CharCode.Newline) return -1; + + // Skip escaped $ + if (char === CharCode.Backslash && i + 1 < end && cx.char(i + 1) === CharCode.Dollar) { + i++; // Skip next char + continue; + } + + // Found potential closing $ + if (char === CharCode.Dollar) { + // Don't match $$ + if (i + 1 < end && cx.char(i + 1) === CharCode.Dollar) continue; + + // Ensure content exists + if (i === pos + 1) return -1; + + // Create element with marks + return cx.addElement(cx.elt('InlineMath', pos, i + 1, [ + cx.elt('InlineMathMark', pos, pos + 1), + cx.elt('InlineMathMark', i, i + 1) + ])); + } + } + + return -1; +} + +/** + * Math extension for Lezer Markdown. + * + * Defines: + * - InlineMath: Inline math formula $...$ + * - InlineMathMark: The $ delimiter marks for inline + * - BlockMath: Block math formula $$...$$ + * - BlockMathMark: The $$ delimiter marks for block + */ +export const Math: MarkdownConfig = { + defineNodes: [ + { name: 'InlineMath' }, + { name: 'InlineMathMark' }, + { name: 'BlockMath' }, + { name: 'BlockMathMark' } + ], + parseInline: [ + { + name: 'Math', + parse(cx, next, pos) { + // Fast path: must start with $ + if (next !== CharCode.Dollar) return -1; + + // Check for $$ (block math) vs $ (inline math) + const isBlock = cx.char(pos + 1) === CharCode.Dollar; + + return isBlock ? parseBlockMath(cx, pos) : parseInlineMath(cx, pos); + }, + // Parse after emphasis to avoid conflicts + after: 'Emphasis' + } + ] +}; + +export default Math; diff --git a/frontend/src/views/editor/extensions/markdown/util.ts b/frontend/src/views/editor/extensions/markdown/util.ts new file mode 100644 index 0000000..2fb2a86 --- /dev/null +++ b/frontend/src/views/editor/extensions/markdown/util.ts @@ -0,0 +1,202 @@ +import { Decoration } from '@codemirror/view'; +import { EditorState } from '@codemirror/state'; +import type { InlineContext, InlineParser } from '@lezer/markdown'; + +/** + * ASCII character codes for common delimiters. + */ +export const enum CharCode { + Space = 32, + Tab = 9, + Newline = 10, + Backslash = 92, + Dollar = 36, // $ + Plus = 43, // + + Equal = 61, // = + OpenBracket = 91, // [ + CloseBracket = 93, // ] + Caret = 94, // ^ + Colon = 58, // : + Hyphen = 45, // - + Underscore = 95, // _ +} + +/** + * Pre-computed lookup table for footnote ID characters. + * Valid characters: 0-9, A-Z, a-z, _, - + * Uses Uint8Array for memory efficiency and O(1) lookup. + */ +const FOOTNOTE_ID_CHARS = new Uint8Array(128); +// Initialize lookup table (0-9: 48-57, A-Z: 65-90, a-z: 97-122, _: 95, -: 45) +for (let i = 48; i <= 57; i++) FOOTNOTE_ID_CHARS[i] = 1; // 0-9 +for (let i = 65; i <= 90; i++) FOOTNOTE_ID_CHARS[i] = 1; // A-Z +for (let i = 97; i <= 122; i++) FOOTNOTE_ID_CHARS[i] = 1; // a-z +FOOTNOTE_ID_CHARS[95] = 1; // _ +FOOTNOTE_ID_CHARS[45] = 1; // - + +/** + * O(1) check if a character is valid for footnote ID. + * @param code - ASCII character code + * @returns True if valid footnote ID character + */ +export function isFootnoteIdChar(code: number): boolean { + return code < 128 && FOOTNOTE_ID_CHARS[code] === 1; +} + +/** + * Configuration for paired delimiter parser factory. + */ +export interface PairedDelimiterConfig { + /** Parser name */ + name: string; + /** Node name for the container element */ + nodeName: string; + /** Node name for the delimiter marks */ + markName: string; + /** First delimiter character code */ + delimChar: number; + /** Whether delimiter is doubled (e.g., == vs =) */ + isDouble: true; + /** Whether to allow newlines in content */ + allowNewlines?: boolean; + /** Parse order - after which parser */ + after?: string; + /** Parse order - before which parser */ + before?: string; +} + +/** + * Factory function to create a paired delimiter inline parser. + * Optimized with: + * - Fast path early return + * - Minimal function calls in loop + * - Pre-computed delimiter length + * + * @param config - Parser configuration + * @returns InlineParser for MarkdownConfig + */ +export function createPairedDelimiterParser(config: PairedDelimiterConfig): InlineParser { + const { name, nodeName, markName, delimChar, allowNewlines = false, after, before } = config; + const delimLen = 2; // Always double delimiter for these parsers + + return { + name, + parse(cx: InlineContext, next: number, pos: number): number { + // Fast path: check first character + if (next !== delimChar) return -1; + + // Check second delimiter character + if (cx.char(pos + 1) !== delimChar) return -1; + + // Don't match triple delimiter (e.g., ===, +++) + if (cx.char(pos + 2) === delimChar) return -1; + + // Calculate search bounds + const searchEnd = cx.end - 1; + const contentStart = pos + delimLen; + + // Look for closing delimiter + for (let i = contentStart; i < searchEnd; i++) { + const char = cx.char(i); + + // Check for newline (unless allowed) + if (!allowNewlines && char === CharCode.Newline) return -1; + + // Found potential closing delimiter + if (char === delimChar && cx.char(i + 1) === delimChar) { + // Don't match triple delimiter + if (i + 2 < cx.end && cx.char(i + 2) === delimChar) continue; + + // Create element with marks + return cx.addElement(cx.elt(nodeName, pos, i + delimLen, [ + cx.elt(markName, pos, contentStart), + cx.elt(markName, i, i + delimLen) + ])); + } + } + + return -1; + }, + ...(after && { after }), + ...(before && { before }) + }; +} + + +/** + * Tuple representation of a range [from, to]. + */ +export type RangeTuple = [number, number]; + +/** + * Check if two ranges overlap (touch or intersect). + * Based on the visual diagram on https://stackoverflow.com/a/25369187 + * + * @param range1 - First range + * @param range2 - Second range + * @returns True if the ranges overlap + */ +export function checkRangeOverlap( + range1: RangeTuple, + range2: RangeTuple +): boolean { + return range1[0] <= range2[1] && range2[0] <= range1[1]; +} + +/** + * Check if any of the editor cursors is in the given range. + * + * @param state - Editor state + * @param range - Range to check + * @returns True if the cursor is in the range + */ +export function isCursorInRange( + state: EditorState, + range: RangeTuple +): boolean { + return state.selection.ranges.some((selection) => + checkRangeOverlap(range, [selection.from, selection.to]) + ); +} + +/** + * Decoration to simply hide anything (replace with nothing). + */ +export const invisibleDecoration = Decoration.replace({}); + + +/** + * Class for generating unique slugs from heading contents. + */ +export class Slugger { + /** Occurrences for each slug. */ + private occurrences: Map = new Map(); + + /** + * Generate a slug from the given content. + * + * @param text - Content to generate the slug from + * @returns The generated slug + */ + public slug(text: string): string { + let slug = text + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^\w-]+/g, ''); + + const count = this.occurrences.get(slug) || 0; + if (count > 0) { + slug += '-' + count; + } + this.occurrences.set(slug, count + 1); + + return slug; + } + + /** + * Reset the slugger state. + */ + public reset(): void { + this.occurrences.clear(); + } +} diff --git a/frontend/src/views/editor/extensions/markdownPreview/PreviewPanel.vue b/frontend/src/views/editor/extensions/markdownPreview/PreviewPanel.vue deleted file mode 100644 index 8e7723b..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/PreviewPanel.vue +++ /dev/null @@ -1,520 +0,0 @@ - - - - - - diff --git a/frontend/src/views/editor/extensions/markdownPreview/github-markdown.css b/frontend/src/views/editor/extensions/markdownPreview/github-markdown.css deleted file mode 100644 index d95cc85..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/github-markdown.css +++ /dev/null @@ -1,1229 +0,0 @@ -.markdown-body { - --base-size-4: 0.25rem; - --base-size-8: 0.5rem; - --base-size-16: 1rem; - --base-size-24: 1.5rem; - --base-size-40: 2.5rem; - --base-text-weight-normal: 400; - --base-text-weight-medium: 500; - --base-text-weight-semibold: 600; - --fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; - --fgColor-accent: Highlight; -} - -/* 暗色主题变量 */ -.markdown-body[data-theme="dark"] { - /* dark */ - color-scheme: dark; - --focus-outlineColor: #1f6feb; - --fgColor-default: #f0f6fc; - --fgColor-muted: #9198a1; - --fgColor-accent: #4493f8; - --fgColor-success: #3fb950; - --fgColor-attention: #d29922; - --fgColor-danger: #f85149; - --fgColor-done: #ab7df8; - --bgColor-default: #0d1117; - --bgColor-muted: #151b23; - --bgColor-neutral-muted: #656c7633; - --bgColor-attention-muted: #bb800926; - --borderColor-default: #3d444d; - --borderColor-muted: #3d444db3; - --borderColor-neutral-muted: #3d444db3; - --borderColor-accent-emphasis: #1f6feb; - --borderColor-success-emphasis: #238636; - --borderColor-attention-emphasis: #9e6a03; - --borderColor-danger-emphasis: #da3633; - --borderColor-done-emphasis: #8957e5; - --color-prettylights-syntax-comment: #9198a1; - --color-prettylights-syntax-constant: #79c0ff; - --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; - --color-prettylights-syntax-entity: #d2a8ff; - --color-prettylights-syntax-storage-modifier-import: #f0f6fc; - --color-prettylights-syntax-entity-tag: #7ee787; - --color-prettylights-syntax-keyword: #ff7b72; - --color-prettylights-syntax-string: #a5d6ff; - --color-prettylights-syntax-variable: #ffa657; - --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; - --color-prettylights-syntax-brackethighlighter-angle: #9198a1; - --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; - --color-prettylights-syntax-invalid-illegal-bg: #8e1519; - --color-prettylights-syntax-carriage-return-text: #f0f6fc; - --color-prettylights-syntax-carriage-return-bg: #b62324; - --color-prettylights-syntax-string-regexp: #7ee787; - --color-prettylights-syntax-markup-list: #f2cc60; - --color-prettylights-syntax-markup-heading: #1f6feb; - --color-prettylights-syntax-markup-italic: #f0f6fc; - --color-prettylights-syntax-markup-bold: #f0f6fc; - --color-prettylights-syntax-markup-deleted-text: #ffdcd7; - --color-prettylights-syntax-markup-deleted-bg: #67060c; - --color-prettylights-syntax-markup-inserted-text: #aff5b4; - --color-prettylights-syntax-markup-inserted-bg: #033a16; - --color-prettylights-syntax-markup-changed-text: #ffdfb6; - --color-prettylights-syntax-markup-changed-bg: #5a1e02; - --color-prettylights-syntax-markup-ignored-text: #f0f6fc; - --color-prettylights-syntax-markup-ignored-bg: #1158c7; - --color-prettylights-syntax-meta-diff-range: #d2a8ff; - --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d; - } - -/* 亮色主题变量 */ -.markdown-body[data-theme="light"] { - /* light */ - color-scheme: light; - --focus-outlineColor: #0969da; - --fgColor-default: #1f2328; - --fgColor-muted: #59636e; - --fgColor-accent: #0969da; - --fgColor-success: #1a7f37; - --fgColor-attention: #9a6700; - --fgColor-danger: #d1242f; - --fgColor-done: #8250df; - --bgColor-default: #ffffff; - --bgColor-muted: #f6f8fa; - --bgColor-neutral-muted: #818b981f; - --bgColor-attention-muted: #fff8c5; - --borderColor-default: #d1d9e0; - --borderColor-muted: #d1d9e0b3; - --borderColor-neutral-muted: #d1d9e0b3; - --borderColor-accent-emphasis: #0969da; - --borderColor-success-emphasis: #1a7f37; - --borderColor-attention-emphasis: #9a6700; - --borderColor-danger-emphasis: #cf222e; - --borderColor-done-emphasis: #8250df; - --color-prettylights-syntax-comment: #59636e; - --color-prettylights-syntax-constant: #0550ae; - --color-prettylights-syntax-constant-other-reference-link: #0a3069; - --color-prettylights-syntax-entity: #6639ba; - --color-prettylights-syntax-storage-modifier-import: #1f2328; - --color-prettylights-syntax-entity-tag: #0550ae; - --color-prettylights-syntax-keyword: #cf222e; - --color-prettylights-syntax-string: #0a3069; - --color-prettylights-syntax-variable: #953800; - --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; - --color-prettylights-syntax-brackethighlighter-angle: #59636e; - --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; - --color-prettylights-syntax-invalid-illegal-bg: #82071e; - --color-prettylights-syntax-carriage-return-text: #f6f8fa; - --color-prettylights-syntax-carriage-return-bg: #cf222e; - --color-prettylights-syntax-string-regexp: #116329; - --color-prettylights-syntax-markup-list: #3b2300; - --color-prettylights-syntax-markup-heading: #0550ae; - --color-prettylights-syntax-markup-italic: #1f2328; - --color-prettylights-syntax-markup-bold: #1f2328; - --color-prettylights-syntax-markup-deleted-text: #82071e; - --color-prettylights-syntax-markup-deleted-bg: #ffebe9; - --color-prettylights-syntax-markup-inserted-text: #116329; - --color-prettylights-syntax-markup-inserted-bg: #dafbe1; - --color-prettylights-syntax-markup-changed-text: #953800; - --color-prettylights-syntax-markup-changed-bg: #ffd8b5; - --color-prettylights-syntax-markup-ignored-text: #d1d9e0; - --color-prettylights-syntax-markup-ignored-bg: #0550ae; - --color-prettylights-syntax-meta-diff-range: #8250df; - --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98; - } - - -.markdown-body { - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - margin: 0; - color: var(--fgColor-default); - background-color: var(--bgColor-default); - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; - font-size: 16px; - line-height: 1.5; - word-wrap: break-word; -} - -.markdown-body .octicon { - display: inline-block; - fill: currentColor; - vertical-align: text-bottom; -} - -.markdown-body h1:hover .anchor .octicon-link:before, -.markdown-body h2:hover .anchor .octicon-link:before, -.markdown-body h3:hover .anchor .octicon-link:before, -.markdown-body h4:hover .anchor .octicon-link:before, -.markdown-body h5:hover .anchor .octicon-link:before, -.markdown-body h6:hover .anchor .octicon-link:before { - width: 16px; - height: 16px; - content: ' '; - display: inline-block; - background-color: currentColor; - -webkit-mask-image: url("data:image/svg+xml,"); - mask-image: url("data:image/svg+xml,"); -} - -.markdown-body details, -.markdown-body figcaption, -.markdown-body figure { - display: block; -} - -.markdown-body summary { - display: list-item; -} - -.markdown-body [hidden] { - display: none !important; -} - -.markdown-body a { - background-color: transparent; - color: var(--fgColor-accent); - text-decoration: none; -} - -.markdown-body abbr[title] { - border-bottom: none; - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -.markdown-body b, -.markdown-body strong { - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body dfn { - font-style: italic; -} - -.markdown-body h1 { - margin: .67em 0; - font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; - font-size: 2em; - border-bottom: 1px solid var(--borderColor-muted); -} - -.markdown-body mark { - background-color: var(--bgColor-attention-muted); - color: var(--fgColor-default); -} - -.markdown-body small { - font-size: 90%; -} - -.markdown-body sub, -.markdown-body sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -.markdown-body sub { - bottom: -0.25em; -} - -.markdown-body sup { - top: -0.5em; -} - -.markdown-body img { - border-style: none; - max-width: 100%; - box-sizing: content-box; -} - -.markdown-body code, -.markdown-body kbd, -.markdown-body pre, -.markdown-body samp { - font-family: monospace; - font-size: 1em; -} - -.markdown-body figure { - margin: 1em var(--base-size-40); -} - -.markdown-body hr { - box-sizing: content-box; - overflow: hidden; - background: transparent; - border-bottom: 1px solid var(--borderColor-muted); - height: .25em; - padding: 0; - margin: var(--base-size-24) 0; - background-color: var(--borderColor-default); - border: 0; -} - -.markdown-body input { - font: inherit; - margin: 0; - overflow: visible; - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - -.markdown-body [type=button], -.markdown-body [type=reset], -.markdown-body [type=submit] { - -webkit-appearance: button; - appearance: button; -} - -.markdown-body [type=checkbox], -.markdown-body [type=radio] { - box-sizing: border-box; - padding: 0; -} - -.markdown-body [type=number]::-webkit-inner-spin-button, -.markdown-body [type=number]::-webkit-outer-spin-button { - height: auto; -} - -.markdown-body [type=search]::-webkit-search-cancel-button, -.markdown-body [type=search]::-webkit-search-decoration { - -webkit-appearance: none; - appearance: none; -} - -.markdown-body ::-webkit-input-placeholder { - color: inherit; - opacity: .54; -} - -.markdown-body ::-webkit-file-upload-button { - -webkit-appearance: button; - appearance: button; - font: inherit; -} - -.markdown-body a:hover { - text-decoration: underline; -} - -.markdown-body ::placeholder { - color: var(--fgColor-muted); - opacity: 1; -} - -.markdown-body hr::before { - display: table; - content: ""; -} - -.markdown-body hr::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body table { - border-spacing: 0; - border-collapse: collapse; - display: block; - width: max-content; - max-width: 100%; - overflow: auto; - font-variant: tabular-nums; -} - -.markdown-body td, -.markdown-body th { - padding: 0; -} - -.markdown-body details summary { - cursor: pointer; -} - -.markdown-body a:focus, -.markdown-body [role=button]:focus, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=checkbox]:focus { - outline: 2px solid var(--focus-outlineColor); - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:focus:not(:focus-visible), -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body input[type=radio]:focus:not(:focus-visible), -.markdown-body input[type=checkbox]:focus:not(:focus-visible) { - outline: solid 1px transparent; -} - -.markdown-body a:focus-visible, -.markdown-body [role=button]:focus-visible, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus-visible { - outline: 2px solid var(--focus-outlineColor); - outline-offset: -2px; - box-shadow: none; -} - -.markdown-body a:not([class]):focus, -.markdown-body a:not([class]):focus-visible, -.markdown-body input[type=radio]:focus, -.markdown-body input[type=radio]:focus-visible, -.markdown-body input[type=checkbox]:focus, -.markdown-body input[type=checkbox]:focus-visible { - outline-offset: 0; -} - -.markdown-body kbd { - display: inline-block; - padding: var(--base-size-4); - font: 11px var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); - line-height: 10px; - color: var(--fgColor-default); - vertical-align: middle; - background-color: var(--bgColor-muted); - border: solid 1px var(--borderColor-neutral-muted); - border-bottom-color: var(--borderColor-neutral-muted); - border-radius: 6px; - box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted); -} - -.markdown-body h1, -.markdown-body h2, -.markdown-body h3, -.markdown-body h4, -.markdown-body h5, -.markdown-body h6 { - margin-top: var(--base-size-24); - margin-bottom: var(--base-size-16); - font-weight: var(--base-text-weight-semibold, 600); - line-height: 1.25; -} - -.markdown-body h2 { - font-weight: var(--base-text-weight-semibold, 600); - padding-bottom: .3em; - font-size: 1.5em; - border-bottom: 1px solid var(--borderColor-muted); -} - -.markdown-body h3 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: 1.25em; -} - -.markdown-body h4 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: 1em; -} - -.markdown-body h5 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: .875em; -} - -.markdown-body h6 { - font-weight: var(--base-text-weight-semibold, 600); - font-size: .85em; - color: var(--fgColor-muted); -} - -.markdown-body p { - margin-top: 0; - margin-bottom: 10px; -} - -.markdown-body blockquote { - margin: 0; - padding: 0 1em; - color: var(--fgColor-muted); - border-left: .25em solid var(--borderColor-default); -} - -.markdown-body ul, -.markdown-body ol { - margin-top: 0; - margin-bottom: 0; - padding-left: 2em; -} - -.markdown-body ol ol, -.markdown-body ul ol { - list-style-type: lower-roman; -} - -.markdown-body ul ul ol, -.markdown-body ul ol ol, -.markdown-body ol ul ol, -.markdown-body ol ol ol { - list-style-type: lower-alpha; -} - -.markdown-body dd { - margin-left: 0; -} - -.markdown-body tt, -.markdown-body code, -.markdown-body samp { - font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); - font-size: 12px; -} - -.markdown-body pre { - margin-top: 0; - margin-bottom: 0; - font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); - font-size: 12px; - word-wrap: normal; -} - -.markdown-body .octicon { - display: inline-block; - overflow: visible !important; - vertical-align: text-bottom; - fill: currentColor; -} - -.markdown-body input::-webkit-outer-spin-button, -.markdown-body input::-webkit-inner-spin-button { - margin: 0; - appearance: none; -} - -.markdown-body .mr-2 { - margin-right: var(--base-size-8, 8px) !important; -} - -.markdown-body::before { - display: table; - content: ""; -} - -.markdown-body::after { - display: table; - clear: both; - content: ""; -} - -.markdown-body>*:first-child { - margin-top: 0 !important; -} - -.markdown-body>*:last-child { - margin-bottom: 0 !important; -} - -.markdown-body a:not([href]) { - color: inherit; - text-decoration: none; -} - -.markdown-body .absent { - color: var(--fgColor-danger); -} - -.markdown-body .anchor { - float: left; - padding-right: var(--base-size-4); - margin-left: -20px; - line-height: 1; -} - -.markdown-body .anchor:focus { - outline: none; -} - -.markdown-body p, -.markdown-body blockquote, -.markdown-body ul, -.markdown-body ol, -.markdown-body dl, -.markdown-body table, -.markdown-body pre, -.markdown-body details { - margin-top: 0; - margin-bottom: var(--base-size-16); -} - -.markdown-body blockquote>:first-child { - margin-top: 0; -} - -.markdown-body blockquote>:last-child { - margin-bottom: 0; -} - -.markdown-body h1 .octicon-link, -.markdown-body h2 .octicon-link, -.markdown-body h3 .octicon-link, -.markdown-body h4 .octicon-link, -.markdown-body h5 .octicon-link, -.markdown-body h6 .octicon-link { - color: var(--fgColor-default); - vertical-align: middle; - visibility: hidden; -} - -.markdown-body h1:hover .anchor, -.markdown-body h2:hover .anchor, -.markdown-body h3:hover .anchor, -.markdown-body h4:hover .anchor, -.markdown-body h5:hover .anchor, -.markdown-body h6:hover .anchor { - text-decoration: none; -} - -.markdown-body h1:hover .anchor .octicon-link, -.markdown-body h2:hover .anchor .octicon-link, -.markdown-body h3:hover .anchor .octicon-link, -.markdown-body h4:hover .anchor .octicon-link, -.markdown-body h5:hover .anchor .octicon-link, -.markdown-body h6:hover .anchor .octicon-link { - visibility: visible; -} - -.markdown-body h1 tt, -.markdown-body h1 code, -.markdown-body h2 tt, -.markdown-body h2 code, -.markdown-body h3 tt, -.markdown-body h3 code, -.markdown-body h4 tt, -.markdown-body h4 code, -.markdown-body h5 tt, -.markdown-body h5 code, -.markdown-body h6 tt, -.markdown-body h6 code { - padding: 0 .2em; - font-size: inherit; -} - -.markdown-body summary h1, -.markdown-body summary h2, -.markdown-body summary h3, -.markdown-body summary h4, -.markdown-body summary h5, -.markdown-body summary h6 { - display: inline-block; -} - -.markdown-body summary h1 .anchor, -.markdown-body summary h2 .anchor, -.markdown-body summary h3 .anchor, -.markdown-body summary h4 .anchor, -.markdown-body summary h5 .anchor, -.markdown-body summary h6 .anchor { - margin-left: -40px; -} - -.markdown-body summary h1, -.markdown-body summary h2 { - padding-bottom: 0; - border-bottom: 0; -} - -.markdown-body ul.no-list, -.markdown-body ol.no-list { - padding: 0; - list-style-type: none; -} - -.markdown-body ol[type="a s"] { - list-style-type: lower-alpha; -} - -.markdown-body ol[type="A s"] { - list-style-type: upper-alpha; -} - -.markdown-body ol[type="i s"] { - list-style-type: lower-roman; -} - -.markdown-body ol[type="I s"] { - list-style-type: upper-roman; -} - -.markdown-body ol[type="1"] { - list-style-type: decimal; -} - -.markdown-body div>ol:not([type]) { - list-style-type: decimal; -} - -.markdown-body ul ul, -.markdown-body ul ol, -.markdown-body ol ol, -.markdown-body ol ul { - margin-top: 0; - margin-bottom: 0; -} - -.markdown-body li>p { - margin-top: var(--base-size-16); -} - -.markdown-body li+li { - margin-top: .25em; -} - -.markdown-body dl { - padding: 0; -} - -.markdown-body dl dt { - padding: 0; - margin-top: var(--base-size-16); - font-size: 1em; - font-style: italic; - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body dl dd { - padding: 0 var(--base-size-16); - margin-bottom: var(--base-size-16); -} - -.markdown-body table th { - font-weight: var(--base-text-weight-semibold, 600); -} - -.markdown-body table th, -.markdown-body table td { - padding: 6px 13px; - border: 1px solid var(--borderColor-default); -} - -.markdown-body table td>:last-child { - margin-bottom: 0; -} - -.markdown-body table tr { - background-color: var(--bgColor-default); - border-top: 1px solid var(--borderColor-muted); -} - -.markdown-body table tr:nth-child(2n) { - background-color: var(--bgColor-muted); -} - -.markdown-body table img { - background-color: transparent; -} - -.markdown-body img[align=right] { - padding-left: 20px; -} - -.markdown-body img[align=left] { - padding-right: 20px; -} - -.markdown-body .emoji { - max-width: none; - vertical-align: text-top; - background-color: transparent; -} - -.markdown-body span.frame { - display: block; - overflow: hidden; -} - -.markdown-body span.frame>span { - display: block; - float: left; - width: auto; - padding: 7px; - margin: 13px 0 0; - overflow: hidden; - border: 1px solid var(--borderColor-default); -} - -.markdown-body span.frame span img { - display: block; - float: left; -} - -.markdown-body span.frame span span { - display: block; - padding: 5px 0 0; - clear: both; - color: var(--fgColor-default); -} - -.markdown-body span.align-center { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-center>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: center; -} - -.markdown-body span.align-center span img { - margin: 0 auto; - text-align: center; -} - -.markdown-body span.align-right { - display: block; - overflow: hidden; - clear: both; -} - -.markdown-body span.align-right>span { - display: block; - margin: 13px 0 0; - overflow: hidden; - text-align: right; -} - -.markdown-body span.align-right span img { - margin: 0; - text-align: right; -} - -.markdown-body span.float-left { - display: block; - float: left; - margin-right: 13px; - overflow: hidden; -} - -.markdown-body span.float-left span { - margin: 13px 0 0; -} - -.markdown-body span.float-right { - display: block; - float: right; - margin-left: 13px; - overflow: hidden; -} - -.markdown-body span.float-right>span { - display: block; - margin: 13px auto 0; - overflow: hidden; - text-align: right; -} - -.markdown-body code, -.markdown-body tt { - padding: .2em .4em; - margin: 0; - font-size: 85%; - white-space: break-spaces; - background-color: var(--bgColor-neutral-muted); - border-radius: 6px; -} - -.markdown-body code br, -.markdown-body tt br { - display: none; -} - -.markdown-body del code { - text-decoration: inherit; -} - -.markdown-body samp { - font-size: 85%; -} - -.markdown-body pre code { - font-size: 100%; -} - -.markdown-body pre>code { - padding: 0; - margin: 0; - word-break: normal; - white-space: pre; - background: transparent; - border: 0; -} - -.markdown-body .highlight { - margin-bottom: var(--base-size-16); -} - -.markdown-body .highlight pre { - margin-bottom: 0; - word-break: normal; -} - -.markdown-body .highlight pre, -.markdown-body pre { - padding: var(--base-size-16); - overflow: auto; - font-size: 85%; - line-height: 1.45; - color: var(--fgColor-default); - background-color: var(--bgColor-muted); - border-radius: 6px; -} - -.markdown-body pre code, -.markdown-body pre tt { - display: inline; - max-width: fit-content; - padding: 0; - margin: 0; - overflow: visible; - line-height: inherit; - word-wrap: normal; - background-color: transparent; - border: 0; -} - -.markdown-body .csv-data td, -.markdown-body .csv-data th { - padding: 5px; - overflow: hidden; - font-size: 12px; - line-height: 1; - text-align: left; - white-space: nowrap; -} - -.markdown-body .csv-data .blob-num { - padding: 10px var(--base-size-8) 9px; - text-align: right; - background: var(--bgColor-default); - border: 0; -} - -.markdown-body .csv-data tr { - border-top: 0; -} - -.markdown-body .csv-data th { - font-weight: var(--base-text-weight-semibold, 600); - background: var(--bgColor-muted); - border-top: 0; -} - -.markdown-body [data-footnote-ref]::before { - content: "["; -} - -.markdown-body [data-footnote-ref]::after { - content: "]"; -} - -.markdown-body .footnotes { - font-size: 12px; - color: var(--fgColor-muted); - border-top: 1px solid var(--borderColor-default); -} - -.markdown-body .footnotes ol { - padding-left: var(--base-size-16); -} - -.markdown-body .footnotes ol ul { - display: inline-block; - padding-left: var(--base-size-16); - margin-top: var(--base-size-16); -} - -.markdown-body .footnotes li { - position: relative; -} - -.markdown-body .footnotes li:target::before { - position: absolute; - top: calc(var(--base-size-8)*-1); - right: calc(var(--base-size-8)*-1); - bottom: calc(var(--base-size-8)*-1); - left: calc(var(--base-size-24)*-1); - pointer-events: none; - content: ""; - border: 2px solid var(--borderColor-accent-emphasis); - border-radius: 6px; -} - -.markdown-body .footnotes li:target { - color: var(--fgColor-default); -} - -.markdown-body .footnotes .data-footnote-backref g-emoji { - font-family: monospace; -} - -.markdown-body body:has(:modal) { - padding-right: var(--dialog-scrollgutter) !important; -} - -.markdown-body .pl-c { - color: var(--color-prettylights-syntax-comment); -} - -.markdown-body .pl-c1, -.markdown-body .pl-s .pl-v { - color: var(--color-prettylights-syntax-constant); -} - -.markdown-body .pl-e, -.markdown-body .pl-en { - color: var(--color-prettylights-syntax-entity); -} - -.markdown-body .pl-smi, -.markdown-body .pl-s .pl-s1 { - color: var(--color-prettylights-syntax-storage-modifier-import); -} - -.markdown-body .pl-ent { - color: var(--color-prettylights-syntax-entity-tag); -} - -.markdown-body .pl-k { - color: var(--color-prettylights-syntax-keyword); -} - -.markdown-body .pl-s, -.markdown-body .pl-pds, -.markdown-body .pl-s .pl-pse .pl-s1, -.markdown-body .pl-sr, -.markdown-body .pl-sr .pl-cce, -.markdown-body .pl-sr .pl-sre, -.markdown-body .pl-sr .pl-sra { - color: var(--color-prettylights-syntax-string); -} - -.markdown-body .pl-v, -.markdown-body .pl-smw { - color: var(--color-prettylights-syntax-variable); -} - -.markdown-body .pl-bu { - color: var(--color-prettylights-syntax-brackethighlighter-unmatched); -} - -.markdown-body .pl-ii { - color: var(--color-prettylights-syntax-invalid-illegal-text); - background-color: var(--color-prettylights-syntax-invalid-illegal-bg); -} - -.markdown-body .pl-c2 { - color: var(--color-prettylights-syntax-carriage-return-text); - background-color: var(--color-prettylights-syntax-carriage-return-bg); -} - -.markdown-body .pl-sr .pl-cce { - font-weight: bold; - color: var(--color-prettylights-syntax-string-regexp); -} - -.markdown-body .pl-ml { - color: var(--color-prettylights-syntax-markup-list); -} - -.markdown-body .pl-mh, -.markdown-body .pl-mh .pl-en, -.markdown-body .pl-ms { - font-weight: bold; - color: var(--color-prettylights-syntax-markup-heading); -} - -.markdown-body .pl-mi { - font-style: italic; - color: var(--color-prettylights-syntax-markup-italic); -} - -.markdown-body .pl-mb { - font-weight: bold; - color: var(--color-prettylights-syntax-markup-bold); -} - -.markdown-body .pl-md { - color: var(--color-prettylights-syntax-markup-deleted-text); - background-color: var(--color-prettylights-syntax-markup-deleted-bg); -} - -.markdown-body .pl-mi1 { - color: var(--color-prettylights-syntax-markup-inserted-text); - background-color: var(--color-prettylights-syntax-markup-inserted-bg); -} - -.markdown-body .pl-mc { - color: var(--color-prettylights-syntax-markup-changed-text); - background-color: var(--color-prettylights-syntax-markup-changed-bg); -} - -.markdown-body .pl-mi2 { - color: var(--color-prettylights-syntax-markup-ignored-text); - background-color: var(--color-prettylights-syntax-markup-ignored-bg); -} - -.markdown-body .pl-mdr { - font-weight: bold; - color: var(--color-prettylights-syntax-meta-diff-range); -} - -.markdown-body .pl-ba { - color: var(--color-prettylights-syntax-brackethighlighter-angle); -} - -.markdown-body .pl-sg { - color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); -} - -.markdown-body .pl-corl { - text-decoration: underline; - color: var(--color-prettylights-syntax-constant-other-reference-link); -} - -.markdown-body [role=button]:focus:not(:focus-visible), -.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible), -.markdown-body button:focus:not(:focus-visible), -.markdown-body summary:focus:not(:focus-visible), -.markdown-body a:focus:not(:focus-visible) { - outline: none; - box-shadow: none; -} - -.markdown-body [tabindex="0"]:focus:not(:focus-visible), -.markdown-body details-dialog:focus:not(:focus-visible) { - outline: none; -} - -.markdown-body g-emoji { - display: inline-block; - min-width: 1ch; - font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; - font-size: 1em; - font-style: normal !important; - font-weight: var(--base-text-weight-normal, 400); - line-height: 1; - vertical-align: -0.075em; -} - -.markdown-body g-emoji img { - width: 1em; - height: 1em; -} - -.markdown-body .task-list-item { - list-style-type: none; -} - -.markdown-body .task-list-item label { - font-weight: var(--base-text-weight-normal, 400); -} - -.markdown-body .task-list-item.enabled label { - cursor: pointer; -} - -.markdown-body .task-list-item+.task-list-item { - margin-top: var(--base-size-4); -} - -.markdown-body .task-list-item .handle { - display: none; -} - -.markdown-body .task-list-item-checkbox { - margin: 0 .2em .25em -1.4em; - vertical-align: middle; -} - -.markdown-body ul:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body ol:dir(rtl) .task-list-item-checkbox { - margin: 0 -1.6em .25em .2em; -} - -.markdown-body .contains-task-list:hover .task-list-item-convert-container, -.markdown-body .contains-task-list:focus-within .task-list-item-convert-container { - display: block; - width: auto; - height: 24px; - overflow: visible; - clip: auto; -} - -.markdown-body ::-webkit-calendar-picker-indicator { - filter: invert(50%); -} - -.markdown-body .markdown-alert { - padding: var(--base-size-8) var(--base-size-16); - margin-bottom: var(--base-size-16); - color: inherit; - border-left: .25em solid var(--borderColor-default); -} - -.markdown-body .markdown-alert>:first-child { - margin-top: 0; -} - -.markdown-body .markdown-alert>:last-child { - margin-bottom: 0; -} - -.markdown-body .markdown-alert .markdown-alert-title { - display: flex; - font-weight: var(--base-text-weight-medium, 500); - align-items: center; - line-height: 1; -} - -.markdown-body .markdown-alert.markdown-alert-note { - border-left-color: var(--borderColor-accent-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { - color: var(--fgColor-accent); -} - -.markdown-body .markdown-alert.markdown-alert-important { - border-left-color: var(--borderColor-done-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { - color: var(--fgColor-done); -} - -.markdown-body .markdown-alert.markdown-alert-warning { - border-left-color: var(--borderColor-attention-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { - color: var(--fgColor-attention); -} - -.markdown-body .markdown-alert.markdown-alert-tip { - border-left-color: var(--borderColor-success-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { - color: var(--fgColor-success); -} - -.markdown-body .markdown-alert.markdown-alert-caution { - border-left-color: var(--borderColor-danger-emphasis); -} - -.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { - color: var(--fgColor-danger); -} - -.markdown-body>*:first-child>.heading-element:first-child { - margin-top: 0 !important; -} - -.markdown-body .highlight pre:has(+.zeroclipboard-container) { - min-height: 52px; -} - diff --git a/frontend/src/views/editor/extensions/markdownPreview/index.ts b/frontend/src/views/editor/extensions/markdownPreview/index.ts deleted file mode 100644 index 224a2c4..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/index.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { EditorView, ViewUpdate, ViewPlugin } from "@codemirror/view"; -import { Extension } from "@codemirror/state"; -import { useDocumentStore } from "@/stores/documentStore"; -import { getActiveNoteBlock } from "../codeblock/state"; -import { markdownPreviewManager } from "./manager"; - -/** - * 切换预览面板的命令 - */ -export function toggleMarkdownPreview(view: EditorView): boolean { - const documentStore = useDocumentStore(); - 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 (markdownPreviewManager.isVisible()) { - markdownPreviewManager.hide(); - } else { - markdownPreviewManager.show( - view, - currentDocumentId, - activeBlock.content.from, - activeBlock.content.to - ); - } - - return true; -} - -/** - * 预览同步插件 - */ -const previewSyncPlugin = ViewPlugin.fromClass( - class { - constructor(private view: EditorView) {} - - update(update: ViewUpdate) { - // 只在预览可见时处理 - if (!markdownPreviewManager.isVisible()) { - return; - } - - const documentStore = useDocumentStore(); - const currentDocumentId = documentStore.currentDocumentId; - const previewDocId = markdownPreviewManager.getCurrentDocumentId(); - - // 如果切换了文档,关闭预览 - if (currentDocumentId !== previewDocId) { - markdownPreviewManager.hide(); - return; - } - - // 文档内容改变时,更新预览 - if (update.docChanged) { - const activeBlock = getActiveNoteBlock(update.state as any); - - // 如果不再是 Markdown 块,关闭预览 - if (!activeBlock || activeBlock.language.name.toLowerCase() !== 'md') { - markdownPreviewManager.hide(); - return; - } - - const range = markdownPreviewManager.getCurrentBlockRange(); - - // 如果切换到其他块,关闭预览 - if (range && activeBlock.content.from !== range.from) { - markdownPreviewManager.hide(); - return; - } - - // 更新预览内容 - const newContent = update.state.doc.sliceString( - activeBlock.content.from, - activeBlock.content.to - ); - markdownPreviewManager.updateContent( - newContent, - activeBlock.content.from, - activeBlock.content.to - ); - } - } - - destroy() { - markdownPreviewManager.destroy(); - } - } -); - -/** - * 导出 Markdown 预览扩展 - */ -export function markdownPreviewExtension(): Extension { - return [previewSyncPlugin]; -} diff --git a/frontend/src/views/editor/extensions/markdownPreview/manager.ts b/frontend/src/views/editor/extensions/markdownPreview/manager.ts deleted file mode 100644 index b562d73..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/manager.ts +++ /dev/null @@ -1,161 +0,0 @@ -import type { EditorView } from '@codemirror/view'; -import { shallowRef, type ShallowRef } from 'vue'; - -/** - * 预览面板位置配置 - */ -interface PreviewPosition { - height: number; -} - -/** - * 预览状态 - */ -interface PreviewState { - visible: boolean; - position: PreviewPosition; - content: string; - blockFrom: number; - blockTo: number; - documentId: number | null; - view: EditorView | null; -} - -/** - * Markdown 预览管理器类 - */ -class MarkdownPreviewManager { - private state: ShallowRef = shallowRef({ - visible: false, - position: { height: 300 }, - content: '', - blockFrom: 0, - blockTo: 0, - documentId: null, - view: null - }); - - /** - * 获取状态(供 Vue 组件使用) - */ - useState() { - return this.state; - } - - /** - * 显示预览面板 - */ - show(view: EditorView, documentId: number, blockFrom: number, blockTo: number): void { - const content = view.state.doc.sliceString(blockFrom, blockTo); - - // 计算初始高度(编辑器容器高度的 50%) - const containerHeight = view.dom.parentElement?.clientHeight || view.dom.clientHeight; - const defaultHeight = Math.floor(containerHeight * 0.5); - - this.state.value = { - visible: true, - position: { height: Math.max(10, defaultHeight) }, - content, - blockFrom, - blockTo, - documentId, - view - }; - } - - /** - * 更新预览内容(文档编辑时调用) - */ - updateContent(content: string, blockFrom: number, blockTo: number): void { - if (!this.state.value.visible) return; - - this.state.value = { - ...this.state.value, - content, - blockFrom, - blockTo - }; - } - - /** - * 更新面板高度 - */ - updateHeight(height: number): void { - if (!this.state.value.visible) return; - - this.state.value = { - ...this.state.value, - position: { height: Math.max(10, height) } - }; - } - - /** - * 隐藏预览面板 - */ - hide(): void { - if (!this.state.value.visible) return; - - const view = this.state.value.view; - - this.state.value = { - visible: false, - position: { height: 300 }, - content: '', - blockFrom: 0, - blockTo: 0, - documentId: null, - view: null - }; - - // 关闭后聚焦编辑器 - if (view) { - view.focus(); - } - } - - /** - * 检查预览是否可见 - */ - isVisible(): boolean { - return this.state.value.visible; - } - - /** - * 获取当前预览的文档 ID - */ - getCurrentDocumentId(): number | null { - return this.state.value.documentId; - } - - /** - * 获取当前预览的块范围 - */ - getCurrentBlockRange(): { from: number; to: number } | null { - if (!this.state.value.visible) return null; - return { - from: this.state.value.blockFrom, - to: this.state.value.blockTo - }; - } - - /** - * 清理资源 - */ - destroy(): void { - this.state.value = { - visible: false, - position: { height: 300 }, - content: '', - blockFrom: 0, - blockTo: 0, - documentId: null, - view: null - }; - } -} - -/** - * 导出单例 - */ -export const markdownPreviewManager = new MarkdownPreviewManager(); - diff --git a/frontend/src/views/editor/extensions/markdownPreview/renderer.ts b/frontend/src/views/editor/extensions/markdownPreview/renderer.ts deleted file mode 100644 index bf8aca6..0000000 --- a/frontend/src/views/editor/extensions/markdownPreview/renderer.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * 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-emojis/' -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/spellcheck/spellcheck.ts b/frontend/src/views/editor/extensions/spellcheck/spellcheck.ts new file mode 100644 index 0000000..ee08676 --- /dev/null +++ b/frontend/src/views/editor/extensions/spellcheck/spellcheck.ts @@ -0,0 +1,8 @@ +import type { Extension } from '@codemirror/state' +import { EditorView } from '@codemirror/view' + +export const spellcheck = (): Extension => { + return EditorView.contentAttributes.of({ + spellcheck: 'true', + }) +} diff --git a/frontend/src/views/settings/pages/UpdatesPage.vue b/frontend/src/views/settings/pages/UpdatesPage.vue index a603ae5..62a30ed 100644 --- a/frontend/src/views/settings/pages/UpdatesPage.vue +++ b/frontend/src/views/settings/pages/UpdatesPage.vue @@ -6,19 +6,16 @@ import { useUpdateStore } from '@/stores/updateStore'; import SettingSection from '../components/SettingSection.vue'; import SettingItem from '../components/SettingItem.vue'; import ToggleSwitch from '../components/ToggleSwitch.vue'; -import markdownit from 'markdown-it' +import { marked } from 'marked'; const { t } = useI18n(); const configStore = useConfigStore(); const updateStore = useUpdateStore(); -// 初始化Remarkable实例并配置 -const md = markdownit({ - html: true, // 允许HTML - linkify: false, // 不解析链接 - typographer: true, // 开启智能引号 - xhtmlOut: true, // 使用xhtml语法输出 - breaks: true, // 允许换行 +// 配置marked +marked.setOptions({ + breaks: true, // 允许换行 + gfm: true, // GitHub风格Markdown }); // 计算属性 @@ -29,10 +26,10 @@ const autoCheckUpdates = computed({ } }); -// 使用Remarkable解析Markdown +// 使用marked解析Markdown const parseMarkdown = (markdown: string) => { if (!markdown) return ''; - return md.render(markdown); + return marked.parse(markdown) as string; }; // 处理更新按钮点击