Replace Shiki code highlighting with Prism.js

This commit is contained in:
eezd committed 2026-10-22 13:08:12 +08:00
1 parent b2d5a1f319
commit ed83b2c590
12 files changed
+194 -678

No files matched your search

+2 -2
View File
@@ -16,8 +16,8 @@
--rm-shadow: 0 12px 36px rgba(0, 0, 0, 0.6); --rm-shadow: 0 12px 36px rgba(0, 0, 0, 0.6);
--rm-inline-code-bg: #202127; --rm-inline-code-bg: #202127;
--rm-inline-code-text: #f19a58; --rm-inline-code-text: #f19a58;
--rm-code-bg: #24292e; --rm-code-bg: #2d2d2d;
--rm-code-text: #e1e4e8; --rm-code-text: #ccc;
--rm-highlight-bg: rgba(232, 196, 119, 0.25); --rm-highlight-bg: rgba(232, 196, 119, 0.25);
} }
+2 -2
View File
@@ -16,7 +16,7 @@
--rm-shadow: 0 12px 36px rgba(24, 31, 44, 0.12); --rm-shadow: 0 12px 36px rgba(24, 31, 44, 0.12);
--rm-inline-code-bg: #f8f8f8; --rm-inline-code-bg: #f8f8f8;
--rm-inline-code-text: #e96900; --rm-inline-code-text: #e96900;
--rm-code-bg: #24292e; --rm-code-bg: #2d2d2d;
--rm-code-text: #e1e4e8; --rm-code-text: #ccc;
--rm-highlight-bg: #fff0a8; --rm-highlight-bg: #fff0a8;
} }
+15 -556
View File
@@ -17,11 +17,12 @@
"gemoji": "^8.1.0", "gemoji": "^8.1.0",
"katex": "^0.18.11", "katex": "^0.18.11",
"mermaid": "^12.2.0", "mermaid": "^12.2.0",
"shiki": "^4.5.0", "prismjs": "^1.30.0",
"turndown": "^7.2.4", "turndown": "^7.2.4",
"vue": "^3.5.44" "vue": "^3.5.44"
}, },
"devDependencies": { "devDependencies": {
"@types/prismjs": "^1.26.6",
"@types/turndown": "^5.0.6", "@types/turndown": "^5.0.6",
"@vitejs/plugin-vue": "^6.0.10", "@vitejs/plugin-vue": "^6.0.10",
"commonmark-spec": "^0.31.2", "commonmark-spec": "^0.31.2",
@@ -696,106 +697,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/chai": {
"version": "5.2.3", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -1080,24 +981,6 @@
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "26.6.5", "version": "26.6.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz",
@@ -1108,6 +991,13 @@
"undici-types": "~8.9.0" "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": { "node_modules/@types/trusted-types": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -1122,12 +1012,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/whatwg-mimetype": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
@@ -1145,12 +1029,6 @@
"@types/node": "*" "@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": { "node_modules/@upsetjs/venn.js": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
@@ -1410,16 +1288,6 @@
"node": ">=4.0" "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": { "node_modules/chai": {
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
@@ -1430,26 +1298,6 @@
"node": ">=18" "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": { "node_modules/chevrotain": {
"version": "13.2.0", "version": "13.2.0",
"resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz",
@@ -1466,16 +1314,6 @@
"node": ">=22.0.0" "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": { "node_modules/commander": {
"version": "15.0.0", "version": "15.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
@@ -2036,15 +1874,6 @@
"robust-predicates": "^3.0.2" "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": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -2055,19 +1884,6 @@
"node": ">=8" "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": { "node_modules/dompurify": {
"version": "3.4.17", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz",
@@ -2202,52 +2018,6 @@
"node": ">=20.0.0" "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": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -2608,27 +2378,6 @@
"node": ">= 20" "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": { "node_modules/mermaid": {
"version": "12.2.0", "version": "12.2.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.2.0.tgz", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.2.0.tgz",
@@ -2663,95 +2412,6 @@
"node": ">=22.12.0" "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": { "node_modules/muggle-string": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
@@ -2791,23 +2451,6 @@
"node": ">=12.20.0" "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": { "node_modules/package-manager-detector": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz",
@@ -2890,40 +2533,15 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/property-information": { "node_modules/prismjs": {
"version": "7.2.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
"integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
"license": "MIT", "license": "MIT",
"funding": { "engines": {
"type": "github", "node": ">=6"
"url": "https://github.com/sponsors/wooorm"
} }
}, },
"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": { "node_modules/robust-predicates": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
@@ -2988,25 +2606,6 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "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": { "node_modules/source-map-js": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
@@ -3016,16 +2615,6 @@
"node": ">=0.10.0" "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": { "node_modules/std-env": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
@@ -3033,20 +2622,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/style-mod": {
"version": "4.1.4", "version": "4.1.4",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz",
@@ -3095,16 +2670,6 @@
"url": "https://github.com/sponsors/SuperchupuDev" "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": { "node_modules/ts-dedent": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz",
@@ -3148,74 +2713,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/uuid": {
"version": "14.0.3", "version": "14.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.3.tgz",
@@ -3229,34 +2726,6 @@
"uuid": "dist-node/bin/uuid" "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": { "node_modules/vite": {
"version": "8.3.4", "version": "8.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz",
@@ -3513,16 +2982,6 @@
"optional": true "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"
}
} }
} }
} }
+2 -1
View File
@@ -20,11 +20,12 @@
"gemoji": "^8.1.0", "gemoji": "^8.1.0",
"katex": "^0.18.11", "katex": "^0.18.11",
"mermaid": "^12.2.0", "mermaid": "^12.2.0",
"shiki": "^4.5.0", "prismjs": "^1.30.0",
"turndown": "^7.2.4", "turndown": "^7.2.4",
"vue": "^3.5.44" "vue": "^3.5.44"
}, },
"devDependencies": { "devDependencies": {
"@types/prismjs": "^1.26.6",
"@types/turndown": "^5.0.6", "@types/turndown": "^5.0.6",
"@vitejs/plugin-vue": "^6.0.10", "@vitejs/plugin-vue": "^6.0.10",
"commonmark-spec": "^0.31.2", "commonmark-spec": "^0.31.2",
+16 -12
View File
@@ -36,16 +36,11 @@ const codeTokenField = StateField.define<DecorationSet>({
const markCache = new Map<string, Decoration>() const markCache = new Map<string, Decoration>()
function tokenMark(color: string, fontStyle: number): Decoration { function tokenMark(classes: string): Decoration {
const key = color + '|' + fontStyle let deco = markCache.get(classes)
let deco = markCache.get(key)
if (!deco) { if (!deco) {
let style = color ? `color:${color}` : '' deco = Decoration.mark({ class: classes })
if (fontStyle & 1) style += ';font-style:italic' markCache.set(classes, deco)
if (fontStyle & 2) style += ';font-weight:bold'
if (fontStyle & 4) style += ';text-decoration:underline'
deco = Decoration.mark({ attributes: { style } })
markCache.set(key, deco)
} }
return deco return deco
} }
@@ -53,10 +48,19 @@ function tokenMark(color: string, fontStyle: number): Decoration {
function tokensToDecorations(block: CodeBlock, run: TokenRun | null): Range<Decoration>[] { function tokensToDecorations(block: CodeBlock, run: TokenRun | null): Range<Decoration>[] {
const out: Range<Decoration>[] = [] const out: Range<Decoration>[] = []
if (!run) return out if (!run) return out
for (let i = 0; i < run.length; i += 4) { let at = block.from
const from = block.from + (run[i] as number) function visit(tokens: TokenRun) {
out.push(tokenMark(run[i + 2] as string, run[i + 3] as number).range(from, from + (run[i + 1] as number))) 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 return out
} }
+2 -2
View File
@@ -34,6 +34,6 @@ it('启动和运行失败均结束请求,失败不会伪装成缓存结果且
await crashed await crashed
const recovered = requestTokens('python', 'print(1)') const recovered = requestTokens('python', 'print(1)')
const replacement = control.workers[1]! const replacement = control.workers[1]!
replacement.dispatchEvent(new MessageEvent('message', { data: { id: replacement.request!.id, tokens: [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([0, 5, '#fff', 0]) await expect(recovered).resolves.toEqual([{ classes: 'token function', content: ['print'] }])
}) })
+2 -2
View File
@@ -6,8 +6,8 @@ export interface HighlightRequest {
code: string code: string
} }
// 扁平数组:[offset, length, color, fontStyle, offset, length, ...],避免大量小对象跨线程拷贝 // 保留 Prism 的嵌套 token 与原文,编辑器和分享页共用官方 token 类样式。
export type TokenRun = (number | string)[] export type TokenRun = (string | { classes: string; content: TokenRun })[]
// 语言不受支持时 tokens 为 null;实际失败必须与纯文本结果区分。 // 语言不受支持时 tokens 为 null;实际失败必须与纯文本结果区分。
export type HighlightResponse = { id: number; tokens: TokenRun | null } | { id: number; error: Error } export type HighlightResponse = { id: number; tokens: TokenRun | null } | { id: number; error: Error }
-22
View File
@@ -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<string | number, string | number>()
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')
})
+73 -24
View File
@@ -1,29 +1,78 @@
import { afterEach, expect, it, vi } from 'vitest' import { afterAll, beforeAll, expect, it, vi } from 'vitest'
import { bundledLanguages } from 'shiki/langs' import type { HighlightResponse, TokenRun } from './protocol'
import type { HighlightResponse } from './protocol'
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals() }) const replies = new Map<number, HighlightResponse>()
const worker = new class extends EventTarget {
postMessage(response: HighlightResponse) { replies.set(response.id, response) }
}()
let nextId = 1
it('未知语言正常完成,语言加载失败返回错误并允许下一次明确请求恢复', async () => { beforeAll(async () => {
const replies = new Map<number, HighlightResponse>()
const worker = new class extends EventTarget {
postMessage(response: HighlightResponse) { replies.set(response.id, response) }
}()
vi.stubGlobal('self', worker) vi.stubGlobal('self', worker)
vi.stubGlobal('Prism', {})
await import('./worker') await import('./worker')
for (const [id, lang] of ['unknown-language', 'constructor', '__proto__'].entries()) { })
worker.dispatchEvent(new MessageEvent('message', { data: { id, lang, code: 'text' } })) afterAll(() => { vi.restoreAllMocks(); vi.unstubAllGlobals() })
await vi.waitFor(() => expect(replies.get(id)).toEqual({ id, tokens: null }))
} async function highlight(lang: string, code: string) {
const cause = new Error('语言资源加载失败') const id = nextId++
const load = vi.spyOn(bundledLanguages, 'python').mockRejectedValueOnce(cause) worker.dispatchEvent(new MessageEvent('message', { data: { id, lang, code } }))
worker.dispatchEvent(new MessageEvent('message', { data: { id: 10, lang: 'python', code: 'print(1)' } })) await vi.waitFor(() => expect(replies.has(id)).toBe(true))
await vi.waitFor(() => expect(replies.has(10)).toBe(true)) const reply = replies.get(id)!
const failure = replies.get(10)! if ('error' in reply) throw reply.error
expect('error' in failure && failure.error.cause).toBe(cause) return reply.tokens
load.mockRestore() }
worker.dispatchEvent(new MessageEvent('message', { data: { id: 11, lang: 'python', code: 'print(1)' } })) function text(tokens: TokenRun): string {
await vi.waitFor(() => expect(replies.has(11)).toBe(true)) return tokens.map((token) => typeof token === 'string' ? token : text(token.content)).join('')
const success = replies.get(11)! }
expect('tokens' in success && success.tokens?.length).toBeGreaterThan(0) 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 = '<script>const value = 1</script><style>a { color: red }</style>'
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 = () => <div title="hello">你好</div>', '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', '<?php echo $value; ?><div>中文😀</div>', '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()
}) })
+61 -34
View File
@@ -1,43 +1,70 @@
import { createHighlighterCore, type HighlighterCore } from 'shiki/core' import type { Grammar, Token } from 'prismjs'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript' import components from 'prismjs/components.json'
import { bundledLanguages } from 'shiki/langs' import getLoader, { type ComponentCategory } from 'prismjs/dependencies'
import githubDark from 'shiki/themes/github-dark.mjs'
import type { HighlightRequest, HighlightResponse, TokenRun } from './protocol' import type { HighlightRequest, HighlightResponse, TokenRun } from './protocol'
const THEME = 'github-dark' // 复用应用自己的 Worker 协议,禁止 Prism 注册另一套消息处理器。
Object.assign(globalThis, { Prism: { manual: true, disableWorkerMessageHandler: true } })
const highlighterReady: Promise<HighlighterCore> = createHighlighterCore({ const prismReady = import('prismjs').then((mod) => mod.default)
themes: [githubDark], const imports = import.meta.glob(['../../node_modules/prismjs/components/prism-*.js', '!../../node_modules/prismjs/components/*.min.js', '!../../node_modules/prismjs/components/prism-core.js'])
langs: [], const { meta, ...entries } = components.languages
engine: createJavaScriptRegexEngine({ forgiving: true }), const languages: ComponentCategory = entries
}) const aliases = new Map<string, string>()
for (const [id, entry] of Object.entries(languages)) {
// 同一语言的并发请求共用一次加载 aliases.set(id, id)
const langLoads = new Map<string, Promise<boolean>>() if (typeof entry !== 'string') {
for (const alias of typeof entry.alias === 'string' ? [entry.alias] : entry.alias ?? []) aliases.set(alias, id)
function ensureLanguage(hl: HighlighterCore, lang: string): Promise<boolean> {
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)
} }
return load }
// Prism 默认包预载这四种基础语法,确保 HTML 的样式和脚本嵌套高亮不依赖请求顺序。
const loaded = new Set(['markup', 'css', 'clike', 'javascript'])
let loadTail = Promise.resolve()
function loadLanguage(id: string): Promise<void> {
const ready = loadTail.then(async () => {
const wanted = new Set<string>()
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<TokenRun | null> { async function highlight(req: HighlightRequest): Promise<TokenRun | null> {
const hl = await highlighterReady const id = aliases.get(req.lang)
if (!(await ensureLanguage(hl, req.lang))) return null if (!id) return null
const run: TokenRun = [] const prism = await prismReady
for (const line of hl.codeToTokensBase(req.code, { lang: req.lang, theme: THEME })) { await loadLanguage(id)
for (const t of line) { const env: { code: string; grammar: Grammar; language: string; tokens?: (string | Token)[] } = { code: req.code, grammar: prism.languages[id], language: id }
if (!t.content.trim()) continue prism.hooks.run('before-tokenize', env)
run.push(t.offset, t.content.length, t.color ?? '', t.fontStyle ?? 0) env.tokens = prism.tokenize(env.code, env.grammar)
} prism.hooks.run('after-tokenize', env)
} return serialize(env.tokens)
return run
} }
self.addEventListener('message', async (e: MessageEvent<HighlightRequest>) => { self.addEventListener('message', async (e: MessageEvent<HighlightRequest>) => {
+12 -16
View File
@@ -1,5 +1,5 @@
// 只读分享页(/s/{token}):取回笔记内容,用与编辑器同一套解析器渲染为 HTML。不加载编辑器 // 只读分享页(/s/{token}):取回笔记内容,用与编辑器同一套解析器渲染为 HTML。不加载编辑器
import { requestTokens } from '../highlight/client' import { requestTokens, type TokenRun } from '../highlight/client'
import { renderMarkdown } from '../markdown/html' import { renderMarkdown } from '../markdown/html'
import { hydrate, renderMermaid } from '../markdown/render' import { hydrate, renderMermaid } from '../markdown/render'
import { applyAppearance, sanitizeAppearance } from '../themes/appearance' import { applyAppearance, sanitizeAppearance } from '../themes/appearance'
@@ -278,22 +278,18 @@ async function decorateCode(code: HTMLElement) {
const run = await requestTokens(lang, text) const run = await requestTokens(lang, text)
if (!run) return if (!run) return
const out = document.createDocumentFragment() const out = document.createDocumentFragment()
let at = 0 function appendTokens(parent: DocumentFragment | HTMLElement, tokens: TokenRun) {
for (let i = 0; i < run.length; i += 4) { for (const token of tokens) {
const from = run[i] as number if (typeof token === 'string') parent.append(token)
const to = from + (run[i + 1] as number) else {
if (from > at) out.append(text.slice(at, from)) const span = document.createElement('span')
const span = document.createElement('span') span.className = token.classes
const fontStyle = run[i + 3] as number appendTokens(span, token.content)
if (run[i + 2]) span.style.color = run[i + 2] as string parent.append(span)
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
} }
out.append(text.slice(at)) appendTokens(out, run)
code.replaceChildren(out) code.replaceChildren(out)
} }
+7 -5
View File
@@ -1,6 +1,8 @@
@import 'prismjs/themes/prism-tomorrow.css';
/* /*
* 基础主题。--rm-* 变量即主题 API,第三方主题只需覆盖这些变量。 * 基础主题。--rm-* 变量即主题 API,第三方主题只需覆盖这些变量。
* 编辑排版适配用户提供的 Obsidian CSS 片段,代码块颜色使用 Shiki GitHub Dark。 * 编辑排版适配用户提供的 Obsidian CSS 片段,代码块颜色使用 Prism Tomorrow Night。
*/ */
:root { :root {
color-scheme: light; color-scheme: light;
@@ -31,8 +33,8 @@
--rm-inline-code-bg: #f8f8f8; --rm-inline-code-bg: #f8f8f8;
--rm-inline-code-text: #e96900; --rm-inline-code-text: #e96900;
--rm-code-bg: #24292e; --rm-code-bg: #2d2d2d;
--rm-code-text: #e1e4e8; --rm-code-text: #ccc;
--rm-code-font-size: 14px; --rm-code-font-size: 14px;
--rm-highlight-bg: rgba(255, 213, 0, 0.35); --rm-highlight-bg: rgba(255, 213, 0, 0.35);
} }
@@ -47,7 +49,7 @@
--rm-accent-strong: #42b883; --rm-accent-strong: #42b883;
--rm-inline-code-bg: #2f2f2f; --rm-inline-code-bg: #2f2f2f;
--rm-inline-code-text: #f19a58; --rm-inline-code-text: #f19a58;
--rm-code-bg: #24292e; --rm-code-bg: #2d2d2d;
--rm-highlight-bg: rgba(255, 213, 0, 0.28); --rm-highlight-bg: rgba(255, 213, 0, 0.28);
--rm-sidebar-bg: #191b1d; --rm-sidebar-bg: #191b1d;
@@ -69,7 +71,7 @@
--rm-accent-strong: #42b883; --rm-accent-strong: #42b883;
--rm-inline-code-bg: #2f2f2f; --rm-inline-code-bg: #2f2f2f;
--rm-inline-code-text: #f19a58; --rm-inline-code-text: #f19a58;
--rm-code-bg: #24292e; --rm-code-bg: #2d2d2d;
--rm-highlight-bg: rgba(255, 213, 0, 0.28); --rm-highlight-bg: rgba(255, 213, 0, 0.28);
--rm-sidebar-bg: #191b1d; --rm-sidebar-bg: #191b1d;