website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


src/_helpers/highlightCode.js
import Prism from "prismjs";
import loadPrismLanguage from "prismjs/components/index.js";

// Avoid "Language does not exist" console noise for extensions we don't
// have a mapping for — those just fall back to plain, unhighlighted text.
loadPrismLanguage.silent = true;

// File-extension -> Prism language name, for the cases where they don't
// already match (or where Prism has no component at all, mapped to null
// so we skip straight to plain text instead of a failed load attempt).
const EXT_TO_LANG = {
  h: "c",
  html: "markup",
  jsonc: "json",
  kt: "kotlin",
  lock: "toml",
  md: "markdown",
  mdx: "markdown",
  mjs: "javascript",
  plist: "markup",
  rs: "rust",
  scm: "scheme",
  sh: "bash",
  ts: "typescript",
  mi: null,
  nu: null,
  plum: null,
  txt: null,
  typ: null,
};

// Fenced-code-block language identifiers (the "rust" in ```rust) mostly
// already match Prism's own component ids, unlike file extensions —
// except for a handful of common short forms people actually type in
// READMEs, which need the same kind of aliasing EXT_TO_LANG does above.
const FENCE_LANG_ALIASES = {
  js: "javascript",
  sh: "bash",
  rs: "rust",
  ts: "typescript",
  yml: "yaml",
  html: "markup",
  htm: "markup",
};

const grammarCache = new Map();

/** Load (and cache) a Prism grammar by its canonical component id. */
const loadGrammar = (lang) => {
  if (grammarCache.has(lang)) return grammarCache.get(lang);
  if (!Prism.languages[lang]) loadPrismLanguage(lang);
  const grammar = Prism.languages[lang] ?? null;
  const result = grammar ? { lang, grammar } : null;
  grammarCache.set(lang, result);
  return result;
};

const getLanguage = (ext) => {
  const lang = Object.prototype.hasOwnProperty.call(EXT_TO_LANG, ext) ? EXT_TO_LANG[ext] : ext;
  return lang ? loadGrammar(lang) : null;
};

/**
 * Highlight source text for a given file extension using Prism, at build
 * time — no client-side highlighter ships to the browser. Falls back to
 * (still-escaped) plain text when there's no Prism grammar for the
 * extension.
 *
 * @returns {{ html: string, lang: string }}
 */
export const highlightCode = (text, ext) => {
  const found = getLanguage(ext);
  if (!found) {
    return { html: escapeHtml(text), lang: ext || "text" };
  }
  return { html: Prism.highlight(text, found.grammar, found.lang), lang: found.lang };
};

/**
 * Highlight a fenced markdown code block by its declared language (the
 * "rust" in ```rust), for markdown-it's `highlight` option — used for
 * repo READMEs, which render through a separate MarkdownIt instance
 * rather than highlightCode()'s extension-keyed lookup. markdown-it
 * wraps whatever this returns in <pre><code class="language-{lang}">,
 * matching the shared pre/code styling and .token.* colors in
 * shared.css. Falls back to plain escaped text, same as highlightCode.
 *
 * @returns {string} inner HTML for markdown-it's fence renderer
 */
export const highlightFenceCode = (text, lang) => {
  const normalized = lang ? (FENCE_LANG_ALIASES[lang.toLowerCase()] ?? lang.toLowerCase()) : null;
  const found = normalized ? loadGrammar(normalized) : null;
  return found ? Prism.highlight(text, found.grammar, found.lang) : escapeHtml(text);
};

const escapeHtml = (str) =>
  String(str).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);