Files
demo/frontend-starter/lib/highlighter.tsx
T
AdminandClaude Opus 5.5 78c8b702f0 Galerie aller shadcn/ui-Komponenten auf der Startseite
- Alle 63 Komponenten aus dem shadcn-Register installiert (Stil base-luma)
- Galerie unter components/galerie mit 7 Kategorien und Sprungnavigation
- Inhaltsbreite auf max-w-6xl erweitert
- shadcn-Hauptfarbe auf E&P-Tiefblau, größter Radius eckig
- App-Titel "UI-Galerie"

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-05 18:07:19 +00:00

125 lines
3.5 KiB
TypeScript

import * as React from "react"
import {
createHighlighterCore,
type GrammarState,
type HighlighterCore,
} from "shiki/core"
import { createJavaScriptRegexEngine } from "shiki/engine/javascript"
// Add the languages you need. Each one loads the first time it is used.
const languages = {
bash: () => import("shiki/langs/shellscript.mjs"),
css: () => import("shiki/langs/css.mjs"),
diff: () => import("shiki/langs/diff.mjs"),
html: () => import("shiki/langs/html.mjs"),
javascript: () => import("shiki/langs/javascript.mjs"),
json: () => import("shiki/langs/json.mjs"),
jsx: () => import("shiki/langs/jsx.mjs"),
markdown: () => import("shiki/langs/markdown.mjs"),
python: () => import("shiki/langs/python.mjs"),
sql: () => import("shiki/langs/sql.mjs"),
tsx: () => import("shiki/langs/tsx.mjs"),
typescript: () => import("shiki/langs/typescript.mjs"),
yaml: () => import("shiki/langs/yaml.mjs"),
}
const aliases: Record<string, keyof typeof languages> = {
js: "javascript",
md: "markdown",
py: "python",
sh: "bash",
shell: "bash",
shellscript: "bash",
ts: "typescript",
yml: "yaml",
zsh: "bash",
}
let highlighter: Promise<HighlighterCore> | undefined
function getHighlighter() {
highlighter ??= createHighlighterCore({
themes: [
import("shiki/themes/github-light.mjs"),
import("shiki/themes/github-dark.mjs"),
],
langs: [],
engine: createJavaScriptRegexEngine({ forgiving: true }),
})
return highlighter
}
function getLanguage(language: string) {
const name = language.toLowerCase()
if (name === "ansi") {
return name
}
if (name in languages) {
return name as keyof typeof languages
}
return aliases[name] ?? null
}
// Loads a language and returns a highlighter for it, or null for a language
// that is not listed.
export async function loadHighlighter(language: string) {
const lang = getLanguage(language)
if (!lang) {
return null
}
const shiki = await getHighlighter()
if (lang !== "ansi" && !shiki.getLoadedLanguages().includes(lang)) {
await shiki.loadLanguage(languages[lang])
}
return {
// Returns one React node per line, and the grammar state after the last
// line. Pass that state back to continue where the code left off.
highlight(code: string, state?: GrammarState) {
const tokenize = (text: string) =>
shiki.codeToTokens(text, {
lang,
themes: { light: "github-light", dark: "github-dark" },
defaultColor: false,
grammarState: state,
}).tokens
// ANSI has no grammar state, so each line is tokenized on its own and
// gets the same colors whether it arrives in one piece or streams.
const tokens =
lang === "ansi"
? code.split("\n").flatMap((line) => tokenize(line))
: tokenize(code)
return {
lines: tokens.map((line) =>
line.map((token, index) => (
<span
key={index}
style={token.htmlStyle as React.CSSProperties}
className="text-(--shiki-light) dark:text-(--shiki-dark)"
>
{token.content}
</span>
))
),
state: shiki.getLastGrammarState(tokens),
}
},
}
}
// Returns one React node per line, or null for a language that is not listed.
export async function highlightCode(code: string, language: string) {
const highlighter = await loadHighlighter(language)
return highlighter ? highlighter.highlight(code).lines : null
}