From ed83b2c59002df76130ce19b5faacfcdbc4de2e9 Mon Sep 17 00:00:00 2001 From: eezd Date: Thu, 22 Oct 2026 13:08:12 +0800 Subject: [PATCH] Replace Shiki code highlighting with Prism.js --- internal/api/themes/black.css | 4 +- internal/api/themes/white.css | 4 +- web/package-lock.json | 571 +------------------------------ web/package.json | 3 +- web/src/editor/codeblocks.ts | 28 +- web/src/highlight/client.test.ts | 4 +- web/src/highlight/protocol.ts | 4 +- web/src/highlight/theme.test.ts | 22 -- web/src/highlight/worker.test.ts | 97 ++++-- web/src/highlight/worker.ts | 95 +++-- web/src/share/main.ts | 28 +- web/src/themes/base.css | 12 +- 12 files changed, 194 insertions(+), 678 deletions(-) delete mode 100644 web/src/highlight/theme.test.ts diff --git a/internal/api/themes/black.css b/internal/api/themes/black.css index 3a37395..8273ec1 100644 --- a/internal/api/themes/black.css +++ b/internal/api/themes/black.css @@ -16,8 +16,8 @@ --rm-shadow: 0 12px 36px rgba(0, 0, 0, 0.6); --rm-inline-code-bg: #202127; --rm-inline-code-text: #f19a58; - --rm-code-bg: #24292e; - --rm-code-text: #e1e4e8; + --rm-code-bg: #2d2d2d; + --rm-code-text: #ccc; --rm-highlight-bg: rgba(232, 196, 119, 0.25); } diff --git a/internal/api/themes/white.css b/internal/api/themes/white.css index f06dd1f..2b22bee 100644 --- a/internal/api/themes/white.css +++ b/internal/api/themes/white.css @@ -16,7 +16,7 @@ --rm-shadow: 0 12px 36px rgba(24, 31, 44, 0.12); --rm-inline-code-bg: #f8f8f8; --rm-inline-code-text: #e96900; - --rm-code-bg: #24292e; - --rm-code-text: #e1e4e8; + --rm-code-bg: #2d2d2d; + --rm-code-text: #ccc; --rm-highlight-bg: #fff0a8; } diff --git a/web/package-lock.json b/web/package-lock.json index 3778a5b..e6f7afa 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -17,11 +17,12 @@ "gemoji": "^8.1.0", "katex": "^0.18.11", "mermaid": "^12.2.0", - "shiki": "^4.5.0", + "prismjs": "^1.30.0", "turndown": "^7.2.4", "vue": "^3.5.44" }, "devDependencies": { + "@types/prismjs": "^1.26.6", "@types/turndown": "^5.0.6", "@vitejs/plugin-vue": "^6.0.10", "commonmark-spec": "^0.31.2", @@ -696,106 +697,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@shikijs/core": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", - "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", - "license": "MIT", - "dependencies": { - "@shikijs/primitive": "4.5.0", - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5", - "hast-util-to-html": "^9.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/engine-javascript": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", - "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.6" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/engine-oniguruma": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", - "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/langs": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", - "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/primitive": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", - "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/themes": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", - "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", - "license": "MIT", - "dependencies": { - "@shikijs/types": "4.5.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/types": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", - "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", - "license": "MIT", - "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@shikijs/vscode-textmate": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", - "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT" - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1080,24 +981,6 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, "node_modules/@types/node": { "version": "26.6.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", @@ -1108,6 +991,13 @@ "undici-types": "~8.9.0" } }, + "node_modules/@types/prismjs": { + "version": "1.26.6", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.6.tgz", + "integrity": "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -1122,12 +1012,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, "node_modules/@types/whatwg-mimetype": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", @@ -1145,12 +1029,6 @@ "@types/node": "*" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", - "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", - "license": "ISC" - }, "node_modules/@upsetjs/venn.js": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", @@ -1410,16 +1288,6 @@ "node": ">=4.0" } }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/chai": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", @@ -1430,26 +1298,6 @@ "node": ">=18" } }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/chevrotain": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", @@ -1466,16 +1314,6 @@ "node": ">=22.0.0" } }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/commander": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", @@ -2036,15 +1874,6 @@ "robust-predicates": "^3.0.2" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2055,19 +1884,6 @@ "node": ">=8" } }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/dompurify": { "version": "3.4.17", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", @@ -2202,52 +2018,6 @@ "node": ">=20.0.0" } }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -2608,27 +2378,6 @@ "node": ">= 20" } }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/mermaid": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.2.0.tgz", @@ -2663,95 +2412,6 @@ "node": ">=22.12.0" } }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", - "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, "node_modules/muggle-string": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", @@ -2791,23 +2451,6 @@ "node": ">=12.20.0" } }, - "node_modules/oniguruma-parser": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", - "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", - "license": "MIT" - }, - "node_modules/oniguruma-to-es": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", - "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", - "license": "MIT", - "dependencies": { - "oniguruma-parser": "^0.12.2", - "regex": "^6.1.0", - "regex-recursion": "^6.0.2" - } - }, "node_modules/package-manager-detector": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", @@ -2890,40 +2533,15 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/property-information": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", - "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=6" } }, - "node_modules/regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", - "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-recursion": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", - "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", - "license": "MIT", - "dependencies": { - "regex-utilities": "^2.3.0" - } - }, - "node_modules/regex-utilities": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", - "license": "MIT" - }, "node_modules/robust-predicates": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", @@ -2988,25 +2606,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/shiki": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", - "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", - "license": "MIT", - "dependencies": { - "@shikijs/core": "4.5.0", - "@shikijs/engine-javascript": "4.5.0", - "@shikijs/engine-oniguruma": "4.5.0", - "@shikijs/langs": "4.5.0", - "@shikijs/themes": "4.5.0", - "@shikijs/types": "4.5.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.5" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/source-map-js": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", @@ -3016,16 +2615,6 @@ "node": ">=0.10.0" } }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/std-env": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", @@ -3033,20 +2622,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/style-mod": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", @@ -3095,16 +2670,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, "node_modules/ts-dedent": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", @@ -3148,74 +2713,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/uuid": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz", @@ -3229,34 +2726,6 @@ "uuid": "dist-node/bin/uuid" } }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/vite": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", @@ -3513,16 +2982,6 @@ "optional": true } } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } } } } diff --git a/web/package.json b/web/package.json index 82fcce9..2ee6c45 100644 --- a/web/package.json +++ b/web/package.json @@ -20,11 +20,12 @@ "gemoji": "^8.1.0", "katex": "^0.18.11", "mermaid": "^12.2.0", - "shiki": "^4.5.0", + "prismjs": "^1.30.0", "turndown": "^7.2.4", "vue": "^3.5.44" }, "devDependencies": { + "@types/prismjs": "^1.26.6", "@types/turndown": "^5.0.6", "@vitejs/plugin-vue": "^6.0.10", "commonmark-spec": "^0.31.2", diff --git a/web/src/editor/codeblocks.ts b/web/src/editor/codeblocks.ts index a39265a..f683bbe 100644 --- a/web/src/editor/codeblocks.ts +++ b/web/src/editor/codeblocks.ts @@ -36,16 +36,11 @@ const codeTokenField = StateField.define({ const markCache = new Map() -function tokenMark(color: string, fontStyle: number): Decoration { - const key = color + '|' + fontStyle - let deco = markCache.get(key) +function tokenMark(classes: string): Decoration { + let deco = markCache.get(classes) if (!deco) { - let style = color ? `color:${color}` : '' - if (fontStyle & 1) style += ';font-style:italic' - if (fontStyle & 2) style += ';font-weight:bold' - if (fontStyle & 4) style += ';text-decoration:underline' - deco = Decoration.mark({ attributes: { style } }) - markCache.set(key, deco) + deco = Decoration.mark({ class: classes }) + markCache.set(classes, deco) } return deco } @@ -53,10 +48,19 @@ function tokenMark(color: string, fontStyle: number): Decoration { function tokensToDecorations(block: CodeBlock, run: TokenRun | null): Range[] { const out: Range[] = [] if (!run) return out - for (let i = 0; i < run.length; i += 4) { - const from = block.from + (run[i] as number) - out.push(tokenMark(run[i + 2] as string, run[i + 3] as number).range(from, from + (run[i + 1] as number))) + let at = block.from + function visit(tokens: TokenRun) { + for (const token of tokens) { + if (typeof token === 'string') at += token.length + else { + const from = at + const index = out.length + visit(token.content) + if (at > from) out.splice(index, 0, tokenMark(token.classes).range(from, at)) + } + } } + visit(run) return out } diff --git a/web/src/highlight/client.test.ts b/web/src/highlight/client.test.ts index 00031bf..2945a91 100644 --- a/web/src/highlight/client.test.ts +++ b/web/src/highlight/client.test.ts @@ -34,6 +34,6 @@ it('启动和运行失败均结束请求,失败不会伪装成缓存结果且 await crashed const recovered = requestTokens('python', 'print(1)') const replacement = control.workers[1]! - replacement.dispatchEvent(new MessageEvent('message', { data: { id: replacement.request!.id, tokens: [0, 5, '#fff', 0] } })) - await expect(recovered).resolves.toEqual([0, 5, '#fff', 0]) + replacement.dispatchEvent(new MessageEvent('message', { data: { id: replacement.request!.id, tokens: [{ classes: 'token function', content: ['print'] }] } })) + await expect(recovered).resolves.toEqual([{ classes: 'token function', content: ['print'] }]) }) diff --git a/web/src/highlight/protocol.ts b/web/src/highlight/protocol.ts index 1a1f72e..71aec42 100644 --- a/web/src/highlight/protocol.ts +++ b/web/src/highlight/protocol.ts @@ -6,8 +6,8 @@ export interface HighlightRequest { code: string } -// 扁平数组:[offset, length, color, fontStyle, offset, length, ...],避免大量小对象跨线程拷贝 -export type TokenRun = (number | string)[] +// 保留 Prism 的嵌套 token 与原文,编辑器和分享页共用官方 token 类样式。 +export type TokenRun = (string | { classes: string; content: TokenRun })[] // 语言不受支持时 tokens 为 null;实际失败必须与纯文本结果区分。 export type HighlightResponse = { id: number; tokens: TokenRun | null } | { id: number; error: Error } diff --git a/web/src/highlight/theme.test.ts b/web/src/highlight/theme.test.ts deleted file mode 100644 index 07accdc..0000000 --- a/web/src/highlight/theme.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { afterEach, expect, it, vi } from 'vitest' -import type { HighlightResponse } from './protocol' - -afterEach(() => vi.unstubAllGlobals()) - -it('编辑器与分享页共用的 Worker 返回 GitHub Dark 原始配色', async () => { - let reply: HighlightResponse | undefined - const worker = new class extends EventTarget { - postMessage(response: HighlightResponse) { reply = response } - }() - vi.stubGlobal('self', worker) - await import('./worker') - const code = 'const value = "hello" // comment' - worker.dispatchEvent(new MessageEvent('message', { data: { id: 1, lang: 'javascript', code } })) - await vi.waitFor(() => expect(reply).toBeDefined()) - if (!reply || !('tokens' in reply) || !reply.tokens) throw new Error('高亮 Worker 未返回颜色') - const colors = new Map() - for (let i = 0; i < reply.tokens.length; i += 4) colors.set(reply.tokens[i], reply.tokens[i + 2]) - expect(colors.get(0)).toBe('#F97583') - expect(colors.get(code.indexOf('"hello"'))).toBe('#9ECBFF') - expect(colors.get(code.indexOf('//'))).toBe('#6A737D') -}) diff --git a/web/src/highlight/worker.test.ts b/web/src/highlight/worker.test.ts index c2e4db7..4a05b7d 100644 --- a/web/src/highlight/worker.test.ts +++ b/web/src/highlight/worker.test.ts @@ -1,29 +1,78 @@ -import { afterEach, expect, it, vi } from 'vitest' -import { bundledLanguages } from 'shiki/langs' -import type { HighlightResponse } from './protocol' +import { afterAll, beforeAll, expect, it, vi } from 'vitest' +import type { HighlightResponse, TokenRun } from './protocol' -afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals() }) +const replies = new Map() +const worker = new class extends EventTarget { + postMessage(response: HighlightResponse) { replies.set(response.id, response) } +}() +let nextId = 1 -it('未知语言正常完成,语言加载失败返回错误并允许下一次明确请求恢复', async () => { - const replies = new Map() - const worker = new class extends EventTarget { - postMessage(response: HighlightResponse) { replies.set(response.id, response) } - }() +beforeAll(async () => { vi.stubGlobal('self', worker) + vi.stubGlobal('Prism', {}) await import('./worker') - for (const [id, lang] of ['unknown-language', 'constructor', '__proto__'].entries()) { - worker.dispatchEvent(new MessageEvent('message', { data: { id, lang, code: 'text' } })) - await vi.waitFor(() => expect(replies.get(id)).toEqual({ id, tokens: null })) - } - const cause = new Error('语言资源加载失败') - const load = vi.spyOn(bundledLanguages, 'python').mockRejectedValueOnce(cause) - worker.dispatchEvent(new MessageEvent('message', { data: { id: 10, lang: 'python', code: 'print(1)' } })) - await vi.waitFor(() => expect(replies.has(10)).toBe(true)) - const failure = replies.get(10)! - expect('error' in failure && failure.error.cause).toBe(cause) - load.mockRestore() - worker.dispatchEvent(new MessageEvent('message', { data: { id: 11, lang: 'python', code: 'print(1)' } })) - await vi.waitFor(() => expect(replies.has(11)).toBe(true)) - const success = replies.get(11)! - expect('tokens' in success && success.tokens?.length).toBeGreaterThan(0) +}) +afterAll(() => { vi.restoreAllMocks(); vi.unstubAllGlobals() }) + +async function highlight(lang: string, code: string) { + const id = nextId++ + worker.dispatchEvent(new MessageEvent('message', { data: { id, lang, code } })) + await vi.waitFor(() => expect(replies.has(id)).toBe(true)) + const reply = replies.get(id)! + if ('error' in reply) throw reply.error + return reply.tokens +} +function text(tokens: TokenRun): string { + return tokens.map((token) => typeof token === 'string' ? token : text(token.content)).join('') +} +function hasToken(tokens: TokenRun, classes: string, content: string): boolean { + return tokens.some((token) => typeof token !== 'string' && ((token.classes === classes && text(token.content) === content) || hasToken(token.content, classes, content))) +} + +it('首次高亮 HTML 就包含内嵌脚本和样式,不依赖其他代码块先加载语法', async () => { + const source = '' + const tokens = await highlight('html', source) + expect(tokens).not.toBeNull() + expect(text(tokens!)).toBe(source) + expect(hasToken(tokens!, 'token keyword', 'const')).toBe(true) + expect(hasToken(tokens!, 'token property', 'color')).toBe(true) +}) + +it('Prism token 保留原文、语言别名与嵌套语法,不再返回旧颜色', async () => { + const source = 'const value = `中文😀 ${name}` // comment\n' + const tokens = await highlight('js', source) + expect(tokens).not.toBeNull() + expect(text(tokens!)).toBe(source) + expect(hasToken(tokens!, 'token keyword', 'const')).toBe(true) + expect(hasToken(tokens!, 'token template-string', '`中文😀 ${name}`')).toBe(true) + expect(hasToken(tokens!, 'token interpolation', '${name}')).toBe(true) + expect(hasToken(tokens!, 'token comment', '// comment')).toBe(true) + expect(await highlight('javascript', source)).toEqual(tokens) +}) + +it('按需加载语言依赖,并发请求和常见别名得到实际高亮', async () => { + const samples = [ + ['python', 'print("hello")', 'token string', '"hello"'], + ['tsx', 'const App = () =>
你好
', 'token attr-value', '="hello"'], + ['sh', 'echo "$HOME"', 'token environment constant', '$HOME'], + ['yaml', 'name: hello', 'token key atrule', 'name'], + ['json', '{"value": true}', 'token boolean', 'true'], + ['go', 'package main\nfunc main() {}', 'token keyword', 'package'], + ['php', '
中文😀
', 'token keyword', 'echo'], + ] + await Promise.all(samples.map(async ([lang, source, classes, content]) => { + const tokens = await highlight(lang, source) + expect(tokens).not.toBeNull() + expect(text(tokens!)).toBe(source) + expect(hasToken(tokens!, classes, content), lang).toBe(true) + })) +}) + +it('未知语言保持纯文本,真实分词失败返回原始原因且不阻断下一次请求', async () => { + for (const lang of ['unknown-language', 'constructor', '__proto__']) expect(await highlight(lang, 'text')).toBeNull() + const { default: prism } = await import('prismjs') + const cause = new Error('分词失败') + vi.spyOn(prism, 'tokenize').mockImplementationOnce(() => { throw cause }) + await expect(highlight('python', 'print(1)')).rejects.toMatchObject({ cause }) + expect(await highlight('python', 'print(1)')).not.toBeNull() }) diff --git a/web/src/highlight/worker.ts b/web/src/highlight/worker.ts index fbadad2..03fc13d 100644 --- a/web/src/highlight/worker.ts +++ b/web/src/highlight/worker.ts @@ -1,43 +1,70 @@ -import { createHighlighterCore, type HighlighterCore } from 'shiki/core' -import { createJavaScriptRegexEngine } from 'shiki/engine/javascript' -import { bundledLanguages } from 'shiki/langs' -import githubDark from 'shiki/themes/github-dark.mjs' +import type { Grammar, Token } from 'prismjs' +import components from 'prismjs/components.json' +import getLoader, { type ComponentCategory } from 'prismjs/dependencies' import type { HighlightRequest, HighlightResponse, TokenRun } from './protocol' -const THEME = 'github-dark' - -const highlighterReady: Promise = createHighlighterCore({ - themes: [githubDark], - langs: [], - engine: createJavaScriptRegexEngine({ forgiving: true }), -}) - -// 同一语言的并发请求共用一次加载 -const langLoads = new Map>() - -function ensureLanguage(hl: HighlighterCore, lang: string): Promise { - let load = langLoads.get(lang) - if (!load) { - const importer = Object.hasOwn(bundledLanguages, lang) ? bundledLanguages[lang as keyof typeof bundledLanguages] : undefined - load = importer - ? importer().then((mod) => hl.loadLanguage(mod.default)).then(() => true).catch((error) => { langLoads.delete(lang); throw error }) - : Promise.resolve(false) - langLoads.set(lang, load) +// 复用应用自己的 Worker 协议,禁止 Prism 注册另一套消息处理器。 +Object.assign(globalThis, { Prism: { manual: true, disableWorkerMessageHandler: true } }) +const prismReady = import('prismjs').then((mod) => mod.default) +const imports = import.meta.glob(['../../node_modules/prismjs/components/prism-*.js', '!../../node_modules/prismjs/components/*.min.js', '!../../node_modules/prismjs/components/prism-core.js']) +const { meta, ...entries } = components.languages +const languages: ComponentCategory = entries +const aliases = new Map() +for (const [id, entry] of Object.entries(languages)) { + aliases.set(id, id) + if (typeof entry !== 'string') { + for (const alias of typeof entry.alias === 'string' ? [entry.alias] : entry.alias ?? []) aliases.set(alias, id) } - return load +} + +// Prism 默认包预载这四种基础语法,确保 HTML 的样式和脚本嵌套高亮不依赖请求顺序。 +const loaded = new Set(['markup', 'css', 'clike', 'javascript']) +let loadTail = Promise.resolve() + +function loadLanguage(id: string): Promise { + const ready = loadTail.then(async () => { + const wanted = new Set() + function include(lang: string) { + if (wanted.has(lang) || loaded.has(lang)) return + wanted.add(lang) + const entry = languages[lang] + if (typeof entry !== 'string') { + // 首次加载时也包含可选嵌套语法,避免后续依赖 ESM 无法重复执行的组件。 + for (const refs of [entry.require, entry.optional, entry.modify]) { + for (const dep of typeof refs === 'string' ? [refs] : refs ?? []) include(dep) + } + } + } + include(id) + for (const lang of getLoader({ languages }, [...wanted]).getIds()) { + if (loaded.has(lang)) continue + await imports[`../../node_modules/prismjs/components/prism-${lang}.js`]() + loaded.add(lang) + } + }) + // 语法组件修改共享注册表,按顺序加载;单次失败不能阻塞后续独立请求。 + loadTail = ready.catch(() => {}) + return ready +} + +function serialize(content: string | Token | (string | Token)[]): TokenRun { + return (Array.isArray(content) ? content : [content]).map((token) => { + if (typeof token === 'string') return token + const aliases = typeof token.alias === 'string' ? [token.alias] : token.alias ?? [] + return { classes: ['token', token.type, ...aliases].join(' '), content: serialize(token.content) } + }) } async function highlight(req: HighlightRequest): Promise { - const hl = await highlighterReady - if (!(await ensureLanguage(hl, req.lang))) return null - const run: TokenRun = [] - for (const line of hl.codeToTokensBase(req.code, { lang: req.lang, theme: THEME })) { - for (const t of line) { - if (!t.content.trim()) continue - run.push(t.offset, t.content.length, t.color ?? '', t.fontStyle ?? 0) - } - } - return run + const id = aliases.get(req.lang) + if (!id) return null + const prism = await prismReady + await loadLanguage(id) + const env: { code: string; grammar: Grammar; language: string; tokens?: (string | Token)[] } = { code: req.code, grammar: prism.languages[id], language: id } + prism.hooks.run('before-tokenize', env) + env.tokens = prism.tokenize(env.code, env.grammar) + prism.hooks.run('after-tokenize', env) + return serialize(env.tokens) } self.addEventListener('message', async (e: MessageEvent) => { diff --git a/web/src/share/main.ts b/web/src/share/main.ts index 3826605..d430e9b 100644 --- a/web/src/share/main.ts +++ b/web/src/share/main.ts @@ -1,5 +1,5 @@ // 只读分享页(/s/{token}):取回笔记内容,用与编辑器同一套解析器渲染为 HTML。不加载编辑器 -import { requestTokens } from '../highlight/client' +import { requestTokens, type TokenRun } from '../highlight/client' import { renderMarkdown } from '../markdown/html' import { hydrate, renderMermaid } from '../markdown/render' import { applyAppearance, sanitizeAppearance } from '../themes/appearance' @@ -278,22 +278,18 @@ async function decorateCode(code: HTMLElement) { const run = await requestTokens(lang, text) if (!run) return const out = document.createDocumentFragment() - let at = 0 - for (let i = 0; i < run.length; i += 4) { - const from = run[i] as number - const to = from + (run[i + 1] as number) - if (from > at) out.append(text.slice(at, from)) - const span = document.createElement('span') - const fontStyle = run[i + 3] as number - if (run[i + 2]) span.style.color = run[i + 2] as string - if (fontStyle & 1) span.style.fontStyle = 'italic' - if (fontStyle & 2) span.style.fontWeight = 'bold' - if (fontStyle & 4) span.style.textDecoration = 'underline' - span.textContent = text.slice(from, to) - out.append(span) - at = to + function appendTokens(parent: DocumentFragment | HTMLElement, tokens: TokenRun) { + for (const token of tokens) { + if (typeof token === 'string') parent.append(token) + else { + const span = document.createElement('span') + span.className = token.classes + appendTokens(span, token.content) + parent.append(span) + } + } } - out.append(text.slice(at)) + appendTokens(out, run) code.replaceChildren(out) } diff --git a/web/src/themes/base.css b/web/src/themes/base.css index 8ca707b..a314259 100644 --- a/web/src/themes/base.css +++ b/web/src/themes/base.css @@ -1,6 +1,8 @@ +@import 'prismjs/themes/prism-tomorrow.css'; + /* * 基础主题。--rm-* 变量即主题 API,第三方主题只需覆盖这些变量。 - * 编辑排版适配用户提供的 Obsidian CSS 片段,代码块颜色使用 Shiki GitHub Dark。 + * 编辑排版适配用户提供的 Obsidian CSS 片段,代码块颜色使用 Prism Tomorrow Night。 */ :root { color-scheme: light; @@ -31,8 +33,8 @@ --rm-inline-code-bg: #f8f8f8; --rm-inline-code-text: #e96900; - --rm-code-bg: #24292e; - --rm-code-text: #e1e4e8; + --rm-code-bg: #2d2d2d; + --rm-code-text: #ccc; --rm-code-font-size: 14px; --rm-highlight-bg: rgba(255, 213, 0, 0.35); } @@ -47,7 +49,7 @@ --rm-accent-strong: #42b883; --rm-inline-code-bg: #2f2f2f; --rm-inline-code-text: #f19a58; - --rm-code-bg: #24292e; + --rm-code-bg: #2d2d2d; --rm-highlight-bg: rgba(255, 213, 0, 0.28); --rm-sidebar-bg: #191b1d; @@ -69,7 +71,7 @@ --rm-accent-strong: #42b883; --rm-inline-code-bg: #2f2f2f; --rm-inline-code-text: #f19a58; - --rm-code-bg: #24292e; + --rm-code-bg: #2d2d2d; --rm-highlight-bg: rgba(255, 213, 0, 0.28); --rm-sidebar-bg: #191b1d;