import type { AppThemeAppearance } from "@/lib/app/appTheme"; export type AiCodeHighlighter = (content: string, lang: string, appearance?: AppThemeAppearance) => string; interface AiShikiCodeHighlighterOptions { appearance: () => AppThemeAppearance; } const SHIKI_THEMES = { dark: "github-dark", light: "github-light", } as const; const SHIKI_LANGUAGES = ["bash", "css", "go", "html", "java", "javascript", "json", "markdown", "php", "python", "rust", "shellscript", "sql", "tsx", "typescript", "vue", "xml", "yaml"] as const; const SHIKI_LANG_BY_AI_LABEL: Record = { BASH: "bash", CLICKHOUSE: "sql", CSS: "css", GO: "go", HTML: "html", JAVA: "java", JAVASCRIPT: "javascript", JS: "javascript", JSON: "json", MARKDOWN: "markdown", MYSQL: "sql", PHP: "php", POSTGRESQL: "sql", PYTHON: "python", RUST: "rust", SHELL: "shellscript", SH: "shellscript", SQL: "sql", SQLITE: "sql", TS: "typescript", TSQL: "sql", TSX: "tsx", TYPESCRIPT: "typescript", VUE: "vue", XML: "xml", YAML: "yaml", YML: "yaml", ZSH: "shellscript", }; type ShikiHighlighter = Awaited>; let highlighterPromise: Promise | undefined; export async function createAiShikiCodeHighlighter(options: AiShikiCodeHighlighterOptions): Promise { const highlighter = await getAiShikiHighlighter(); return (content, lang, appearance = options.appearance()) => highlighter.codeToHtml(content, { lang: resolveShikiLanguage(lang), structure: "inline", theme: SHIKI_THEMES[appearance], }); } /** * Like `createAiShikiCodeHighlighter`, but keeps shiki's per-line `.line` * wrappers (structure "classic") instead of flattening them away. Callers * that lay out code as a block — one line per row, e.g. with CSS-counter * line numbers — need this; `structure: "inline"` discards the `.line` * elements entirely, so counters/line-based layout silently no-op. */ export async function createAiShikiBlockCodeHighlighter(options: AiShikiCodeHighlighterOptions): Promise { const highlighter = await getAiShikiHighlighter(); return (content, lang, appearance = options.appearance()) => { const html = highlighter.codeToHtml(content, { lang: resolveShikiLanguage(lang), structure: "classic", theme: SHIKI_THEMES[appearance], }); return extractShikiCodeInnerHtml(html); }; } /** * Shiki's "classic" structure wraps lines in `
...
`; * callers that supply their own `
/` shell only want what's inside.
 * It also joins `` siblings with a literal "\n" text node —
 * redundant once `.line{display:block}` already breaks each line, and doubles
 * the visual line spacing under `white-space:pre` if left in.
 */
function extractShikiCodeInnerHtml(html: string): string {
  const match = /]*>([\s\S]*)<\/code>/.exec(html);
  const inner = match ? match[1] : html;
  return inner.replace(/<\/span>\n/g, "");
}

function getAiShikiHighlighter(): Promise {
  highlighterPromise ??= loadAiShikiHighlighter();
  return highlighterPromise;
}

async function loadAiShikiHighlighter(): Promise {
  const [{ createHighlighterCore }, { createJavaScriptRegexEngine }, githubDark, githubLight, bash, css, go, html, java, javascript, json, markdown, php, python, rust, shellscript, sql, tsx, typescript, vue, xml, yaml] = await Promise.all([
    import("shiki/core"),
    import("shiki/engine/javascript"),
    import("shiki/themes/github-dark.mjs"),
    import("shiki/themes/github-light.mjs"),
    import("shiki/langs/bash.mjs"),
    import("shiki/langs/css.mjs"),
    import("shiki/langs/go.mjs"),
    import("shiki/langs/html.mjs"),
    import("shiki/langs/java.mjs"),
    import("shiki/langs/javascript.mjs"),
    import("shiki/langs/json.mjs"),
    import("shiki/langs/markdown.mjs"),
    import("shiki/langs/php.mjs"),
    import("shiki/langs/python.mjs"),
    import("shiki/langs/rust.mjs"),
    import("shiki/langs/shellscript.mjs"),
    import("shiki/langs/sql.mjs"),
    import("shiki/langs/tsx.mjs"),
    import("shiki/langs/typescript.mjs"),
    import("shiki/langs/vue.mjs"),
    import("shiki/langs/xml.mjs"),
    import("shiki/langs/yaml.mjs"),
  ]);

  return createHighlighterCore({
    engine: createJavaScriptRegexEngine(),
    langs: [bash.default, css.default, go.default, html.default, java.default, javascript.default, json.default, markdown.default, php.default, python.default, rust.default, shellscript.default, sql.default, tsx.default, typescript.default, vue.default, xml.default, yaml.default],
    themes: [githubDark.default, githubLight.default],
  });
}

function resolveShikiLanguage(lang: string): (typeof SHIKI_LANGUAGES)[number] | "text" {
  return SHIKI_LANG_BY_AI_LABEL[lang.toUpperCase()] ?? "text";
}