website
git clone https://git.pyrossh.dev/website
木 Personal website of pyrossh. Built with astrojs, shiki, vite.
src/_helpers/highlightCode.js
| 3c79a68 | 1 | import Prism from "prismjs"; |
| 3c79a68 | 2 | import loadPrismLanguage from "prismjs/components/index.js"; |
| 3c79a68 | 3 | |
| 3c79a68 | 4 | // Avoid "Language does not exist" console noise for extensions we don't |
| 3c79a68 | 5 | // have a mapping for — those just fall back to plain, unhighlighted text. |
| 3c79a68 | 6 | loadPrismLanguage.silent = true; |
| 3c79a68 | 7 | |
| 3c79a68 | 8 | // File-extension -> Prism language name, for the cases where they don't |
| 3c79a68 | 9 | // already match (or where Prism has no component at all, mapped to null |
| 3c79a68 | 10 | // so we skip straight to plain text instead of a failed load attempt). |
| 3c79a68 | 11 | const EXT_TO_LANG = { |
| 3c79a68 | 12 | h: "c", |
| 3c79a68 | 13 | html: "markup", |
| 3c79a68 | 14 | jsonc: "json", |
| 3c79a68 | 15 | kt: "kotlin", |
| 3c79a68 | 16 | lock: "toml", |
| 3c79a68 | 17 | md: "markdown", |
| 3c79a68 | 18 | mdx: "markdown", |
| 3c79a68 | 19 | mjs: "javascript", |
| 3c79a68 | 20 | plist: "markup", |
| 3c79a68 | 21 | rs: "rust", |
| 3c79a68 | 22 | scm: "scheme", |
| 3c79a68 | 23 | sh: "bash", |
| 3c79a68 | 24 | ts: "typescript", |
| 3c79a68 | 25 | mi: null, |
| 3c79a68 | 26 | nu: null, |
| 3c79a68 | 27 | plum: null, |
| 3c79a68 | 28 | txt: null, |
| 3c79a68 | 29 | typ: null, |
| 3c79a68 | 30 | }; |
| 3c79a68 | 31 | |
| 44991dd | 32 | // Fenced-code-block language identifiers (the "rust" in ```rust) mostly |
| 44991dd | 33 | // already match Prism's own component ids, unlike file extensions — |
| 44991dd | 34 | // except for a handful of common short forms people actually type in |
| 44991dd | 35 | // READMEs, which need the same kind of aliasing EXT_TO_LANG does above. |
| 44991dd | 36 | const FENCE_LANG_ALIASES = { |
| 44991dd | 37 | js: "javascript", |
| 44991dd | 38 | sh: "bash", |
| 44991dd | 39 | rs: "rust", |
| 44991dd | 40 | ts: "typescript", |
| 44991dd | 41 | yml: "yaml", |
| 44991dd | 42 | html: "markup", |
| 44991dd | 43 | htm: "markup", |
| 44991dd | 44 | }; |
| 3c79a68 | 45 | |
| 44991dd | 46 | const grammarCache = new Map(); |
| 3c79a68 | 47 | |
| 44991dd | 48 | /** Load (and cache) a Prism grammar by its canonical component id. */ |
| 44991dd | 49 | const loadGrammar = (lang) => { |
| 44991dd | 50 | if (grammarCache.has(lang)) return grammarCache.get(lang); |
| 44991dd | 51 | if (!Prism.languages[lang]) loadPrismLanguage(lang); |
| 44991dd | 52 | const grammar = Prism.languages[lang] ?? null; |
| 3c79a68 | 53 | const result = grammar ? { lang, grammar } : null; |
| 44991dd | 54 | grammarCache.set(lang, result); |
| 3c79a68 | 55 | return result; |
| 3c79a68 | 56 | }; |
| 3c79a68 | 57 | |
| 44991dd | 58 | const getLanguage = (ext) => { |
| 44991dd | 59 | const lang = Object.prototype.hasOwnProperty.call(EXT_TO_LANG, ext) ? EXT_TO_LANG[ext] : ext; |
| 44991dd | 60 | return lang ? loadGrammar(lang) : null; |
| 44991dd | 61 | }; |
| 44991dd | 62 | |
| 3c79a68 | 63 | /** |
| 3c79a68 | 64 | * Highlight source text for a given file extension using Prism, at build |
| 3c79a68 | 65 | * time — no client-side highlighter ships to the browser. Falls back to |
| 3c79a68 | 66 | * (still-escaped) plain text when there's no Prism grammar for the |
| 3c79a68 | 67 | * extension. |
| 3c79a68 | 68 | * |
| 3c79a68 | 69 | * @returns {{ html: string, lang: string }} |
| 3c79a68 | 70 | */ |
| 3c79a68 | 71 | export const highlightCode = (text, ext) => { |
| 3c79a68 | 72 | const found = getLanguage(ext); |
| 3c79a68 | 73 | if (!found) { |
| 3c79a68 | 74 | return { html: escapeHtml(text), lang: ext || "text" }; |
| 3c79a68 | 75 | } |
| 3c79a68 | 76 | return { html: Prism.highlight(text, found.grammar, found.lang), lang: found.lang }; |
| 3c79a68 | 77 | }; |
| 3c79a68 | 78 | |
| 44991dd | 79 | /** |
| 44991dd | 80 | * Highlight a fenced markdown code block by its declared language (the |
| 44991dd | 81 | * "rust" in ```rust), for markdown-it's `highlight` option — used for |
| 44991dd | 82 | * repo READMEs, which render through a separate MarkdownIt instance |
| 44991dd | 83 | * rather than highlightCode()'s extension-keyed lookup. markdown-it |
| 44991dd | 84 | * wraps whatever this returns in <pre><code class="language-{lang}">, |
| 44991dd | 85 | * matching the shared pre/code styling and .token.* colors in |
| 44991dd | 86 | * shared.css. Falls back to plain escaped text, same as highlightCode. |
| 44991dd | 87 | * |
| 44991dd | 88 | * @returns {string} inner HTML for markdown-it's fence renderer |
| 44991dd | 89 | */ |
| 44991dd | 90 | export const highlightFenceCode = (text, lang) => { |
| 44991dd | 91 | const normalized = lang ? (FENCE_LANG_ALIASES[lang.toLowerCase()] ?? lang.toLowerCase()) : null; |
| 44991dd | 92 | const found = normalized ? loadGrammar(normalized) : null; |
| 44991dd | 93 | return found ? Prism.highlight(text, found.grammar, found.lang) : escapeHtml(text); |
| 44991dd | 94 | }; |
| 44991dd | 95 | |
| 3c79a68 | 96 | const escapeHtml = (str) => |
| 3c79a68 | 97 | String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]); |