@kitn.ai/chat
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
158 lines (138 loc) • 5.64 kB
text/typescript
// On-demand code highlighter built on Shiki's fine-grained core.
//
// Nothing here loads until `highlight()` is first called with highlighting
// enabled — so a component set that never renders a code block ships and runs
// with ZERO Shiki bytes. When a code block does appear, only the core, the
// JavaScript regex engine (no WASM), the one theme, and the one language grammar
// it needs are dynamically imported, each as its own small lazy chunk.
//
// Hosts extend or disable this via `configureCodeHighlighting()`.
import type { HighlighterCore } from 'shiki/core';
type Loader = () => Promise<unknown>;
/**
* Minimal default language set — each a separate lazy chunk, loaded only on use.
* Kept deliberately small to keep the bundle lean; hosts add more at runtime via
* `configureCodeHighlighting({ languages })` (no rebuild needed — see that fn).
*/
const DEFAULT_LANGUAGES: Record<string, Loader> = {
bash: () => import('@shikijs/langs/bash'),
javascript: () => import('@shikijs/langs/javascript'),
typescript: () => import('@shikijs/langs/typescript'),
tsx: () => import('@shikijs/langs/tsx'),
html: () => import('@shikijs/langs/html'),
css: () => import('@shikijs/langs/css'),
json: () => import('@shikijs/langs/json'),
vue: () => import('@shikijs/langs/vue'),
svelte: () => import('@shikijs/langs/svelte'),
};
const DEFAULT_THEMES: Record<string, Loader> = {
'github-dark-dimmed': () => import('@shikijs/themes/github-dark-dimmed'),
'github-light': () => import('@shikijs/themes/github-light'),
};
const DEFAULT_ALIASES: Record<string, string> = {
js: 'javascript',
ts: 'typescript',
jsx: 'tsx',
sh: 'bash',
shell: 'bash',
};
const FALLBACK_THEME = 'github-dark-dimmed';
export interface CodeHighlightingOptions {
/** Turn highlighting on/off globally. When false, code renders as plain text. */
enabled?: boolean;
/** Register/override language loaders, e.g. `{ ruby: () => import('@shikijs/langs/ruby') }`. */
languages?: Record<string, Loader>;
/** Register/override theme loaders. */
themes?: Record<string, Loader>;
/** Map short names to canonical language keys, e.g. `{ vue: 'html' }`. */
aliases?: Record<string, string>;
}
let enabled = true;
let langLoaders: Record<string, Loader> = { ...DEFAULT_LANGUAGES };
let themeLoaders: Record<string, Loader> = { ...DEFAULT_THEMES };
let aliases: Record<string, string> = { ...DEFAULT_ALIASES };
let highlighterPromise: Promise<HighlighterCore> | null = null;
const loadedLangs = new Set<string>();
const loadedThemes = new Set<string>();
function getHighlighter(): Promise<HighlighterCore> {
if (!highlighterPromise) {
highlighterPromise = (async () => {
const [{ createHighlighterCore }, { createJavaScriptRegexEngine }] = await Promise.all([
import('shiki/core'),
import('shiki/engine/javascript'),
]);
return createHighlighterCore({
themes: [],
langs: [],
engine: createJavaScriptRegexEngine(),
});
})();
}
return highlighterPromise;
}
function resolveLang(lang: string): string {
return aliases[lang] ?? lang;
}
async function ensureLang(hl: HighlighterCore, lang: string): Promise<boolean> {
const name = resolveLang(lang);
if (loadedLangs.has(name)) return true;
const loader = langLoaders[name];
if (!loader) return false;
await hl.loadLanguage(loader() as never);
loadedLangs.add(name);
return true;
}
async function ensureTheme(hl: HighlighterCore, theme: string): Promise<boolean> {
if (loadedThemes.has(theme)) return true;
const loader = themeLoaders[theme];
if (!loader) return false;
await hl.loadTheme(loader() as never);
loadedThemes.add(theme);
return true;
}
function escapeHtml(s: string): string {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function plain(code: string): string {
return `<pre><code>${escapeHtml(code)}</code></pre>`;
}
/**
* Highlight `code` as `lang` with `theme`, returning HTML. Loads only what's
* needed, on demand. Falls back to escaped plain `<pre><code>` when highlighting
* is disabled, the language has no registered loader, or anything fails.
*/
export async function highlight(code: string, lang: string, theme: string): Promise<string> {
if (!enabled || !code) return plain(code);
try {
const hl = await getHighlighter();
const hasLang = await ensureLang(hl, lang);
if (!hasLang) return plain(code);
const useTheme = (await ensureTheme(hl, theme))
? theme
: (await ensureTheme(hl, FALLBACK_THEME)) ? FALLBACK_THEME : null;
if (!useTheme) return plain(code);
return hl.codeToHtml(code, { lang: resolveLang(lang), theme: useTheme });
} catch {
return plain(code);
}
}
/** Register additional languages/themes/aliases, or disable highlighting entirely. */
export function configureCodeHighlighting(options: CodeHighlightingOptions): void {
if (options.enabled !== undefined) enabled = options.enabled;
if (options.languages) langLoaders = { ...langLoaders, ...options.languages };
if (options.themes) themeLoaders = { ...themeLoaders, ...options.themes };
if (options.aliases) aliases = { ...aliases, ...options.aliases };
}
export function isCodeHighlightingEnabled(): boolean {
return enabled;
}
/** Test helper — reset the singleton and registries to defaults. */
export function __resetCodeHighlightingForTests(): void {
enabled = true;
langLoaders = { ...DEFAULT_LANGUAGES };
themeLoaders = { ...DEFAULT_THEMES };
aliases = { ...DEFAULT_ALIASES };
highlighterPromise = null;
loadedLangs.clear();
loadedThemes.clear();
}