feat(docs): enhance code presentation

This commit is contained in:
Maofeng
2026-09-28 16:00:01 +08:00
parent 91cfb3d068
commit 93373adb9a
6 changed files with 430 additions and 20 deletions
@@ -0,0 +1,141 @@
import type { HighlighterCore } from "@shikijs/core"
export type HighlightedToken = {
content: string
dark: string
light: string
offset: number
}
export type HighlightedLine = {
offset: number
tokens: HighlightedToken[]
}
export type HighlightedSource = HighlightedLine[]
const languageLoaders = {
bash: () => import("@shikijs/langs/bash"),
css: () => import("@shikijs/langs/css"),
html: () => import("@shikijs/langs/html"),
javascript: () => import("@shikijs/langs/javascript"),
json: () => import("@shikijs/langs/json"),
jsx: () => import("@shikijs/langs/jsx"),
markdown: () => import("@shikijs/langs/markdown"),
mdx: () => import("@shikijs/langs/mdx"),
tsx: () => import("@shikijs/langs/tsx"),
typescript: () => import("@shikijs/langs/typescript"),
yaml: () => import("@shikijs/langs/yaml"),
} as const
type SyntaxLanguage = keyof typeof languageLoaders
const languageByExtension: Readonly<Record<string, SyntaxLanguage>> = {
bash: "bash",
cjs: "javascript",
css: "css",
html: "html",
js: "javascript",
json: "json",
jsx: "jsx",
md: "markdown",
mdx: "mdx",
mjs: "javascript",
sh: "bash",
ts: "typescript",
tsx: "tsx",
yaml: "yaml",
yml: "yaml",
zsh: "bash",
}
const highlighterPromises = new Map<SyntaxLanguage, Promise<HighlighterCore>>()
export async function highlightSource(
source: string,
filename: string
): Promise<HighlightedSource> {
const extension = filename.split(".").pop()?.toLowerCase() ?? ""
const language = languageByExtension[extension]
if (!language) return createPlainSource(source)
const highlighter = await getHighlighter(language)
const lines = highlighter.codeToTokensWithThemes(source, {
lang: language,
themes: {
dark: "github-dark",
light: "github-light",
},
})
let lineOffset = 0
return lines.map((line) => {
const highlightedLine: HighlightedLine = {
offset: lineOffset,
tokens: line.map((token) => ({
content: token.content,
dark: token.variants.dark?.color ?? "currentColor",
light: token.variants.light?.color ?? "currentColor",
offset: token.offset,
})),
}
lineOffset += line.reduce(
(length, token) => length + token.content.length,
0
)
lineOffset += 1
return highlightedLine
})
}
function getHighlighter(language: SyntaxLanguage) {
const cached = highlighterPromises.get(language)
if (cached) return cached
const highlighter = Promise.all([
import("@shikijs/core"),
import("@shikijs/engine-javascript"),
import("@shikijs/themes/github-dark"),
import("@shikijs/themes/github-light"),
languageLoaders[language](),
]).then(
([
{ createHighlighterCore },
{ createJavaScriptRegexEngine },
{ default: githubDark },
{ default: githubLight },
{ default: grammar },
]) =>
createHighlighterCore({
engine: createJavaScriptRegexEngine(),
langs: grammar,
themes: [githubDark, githubLight],
})
)
highlighterPromises.set(language, highlighter)
return highlighter
}
function createPlainSource(source: string): HighlightedSource {
let offset = 0
return source.split("\n").map((content) => {
const line: HighlightedLine = {
offset,
tokens: [
{
content,
dark: "inherit",
light: "inherit",
offset,
},
],
}
offset += content.length + 1
return line
})
}
+124 -19
View File
@@ -5,6 +5,7 @@ import { useTranslate } from "../../lib/locale"
import { getSandboxDefinition } from "./registry"
import { SandboxErrorBoundary } from "./sandbox-error-boundary"
import { SandboxFrame } from "./sandbox-frame"
import { highlightSource, type HighlightedSource } from "./highlight-source"
import type {
SandboxDefinition,
SandboxPreviewModule,
@@ -13,6 +14,8 @@ import type {
type SandboxView = "code" | "preview"
const codeFontSizes = [12, 13, 14, 16] as const
export type SandboxProps = {
className?: string
example: string
@@ -87,14 +90,13 @@ function SandboxViewer({
[definition.files]
)
const [view, setView] = React.useState<SandboxView>("preview")
const [selectedFile, setSelectedFile] = React.useState(
definition.defaultFile ?? filenames[0] ?? ""
)
const [previewModule, setPreviewModule] =
React.useState<SandboxPreviewModule | null>(null)
const [selectedFile, setSelectedFile] = React.useState(definition.defaultFile ?? filenames[0] ?? "")
const [previewModule, setPreviewModule] = React.useState<SandboxPreviewModule | null>(null)
const [previewError, setPreviewError] = React.useState<Error | null>(null)
const [source, setSource] = React.useState<string | null>(null)
const [sourceError, setSourceError] = React.useState<Error | null>(null)
const [highlightedSource, setHighlightedSource] = React.useState<HighlightedSource | null>(null)
const [codeFontSizeIndex, setCodeFontSizeIndex] = React.useState(1)
const [reloadKey, setReloadKey] = React.useState(0)
const [copied, setCopied] = React.useState(false)
const resolvedTitle = title ?? definition.title ?? definition.id
@@ -131,6 +133,7 @@ function SandboxViewer({
let active = true
setSource(null)
setSourceError(null)
setHighlightedSource(null)
resolveSource(file).then(
(value) => {
if (active) setSource(value)
@@ -145,6 +148,25 @@ function SandboxViewer({
}
}, [definition.files, selectedFile, view])
React.useEffect(() => {
if (!source || view !== "code") return
let active = true
setHighlightedSource(null)
highlightSource(source, selectedFile).then(
(value) => {
if (active) setHighlightedSource(value)
},
() => {
if (active) setHighlightedSource(null)
}
)
return () => {
active = false
}
}, [selectedFile, source, view])
React.useEffect(() => {
setSelectedFile(definition.defaultFile ?? filenames[0] ?? "")
}, [definition.defaultFile, filenames])
@@ -182,16 +204,24 @@ function SandboxViewer({
<div className="ml-auto flex items-center gap-1">
{view === "code" ? (
<SandboxAction
disabled={!source}
label={t({ zh: "复制源码", us: "Copy source" })}
onClick={() => void copySource()}
>
<Icon
name={copied ? "lucide:check" : "lucide:copy"}
className="size-4"
<>
<CodeFontSizeControl
decreaseLabel={t({ zh: "减小字号", us: "Decrease font size" })}
increaseLabel={t({ zh: "增大字号", us: "Increase font size" })}
index={codeFontSizeIndex}
onIndexChange={setCodeFontSizeIndex}
/>
</SandboxAction>
<SandboxAction
disabled={!source}
label={t({ zh: "复制源码", us: "Copy source" })}
onClick={() => void copySource()}
>
<Icon
name={copied ? "lucide:check" : "lucide:copy"}
className="size-4"
/>
</SandboxAction>
</>
) : (
<SandboxAction
label={t({ zh: "重置预览", us: "Reset preview" })}
@@ -241,11 +271,21 @@ function SandboxViewer({
))}
</nav>
) : null}
<pre className="m-0 min-w-0 overflow-auto bg-slate-950 p-4 text-sm leading-6 text-slate-100">
<code>
{sourceError
? sourceError.message
: (source ?? t({ zh: "正在加载源码…", us: "Loading source…" }))}
<pre
className="m-0 min-w-0 overflow-auto bg-slate-50 p-4 font-mono text-slate-800 dark:bg-slate-950 dark:text-slate-100"
style={{
fontSize: `${codeFontSizes[codeFontSizeIndex]}px`,
lineHeight: 1.55,
}}
>
<code className="block min-w-max">
{sourceError ? (
sourceError.message
) : highlightedSource ? (
<HighlightedCode source={highlightedSource} />
) : (
(source ?? t({ zh: "正在加载源码…", us: "Loading source…" }))
)}
</code>
</pre>
</div>
@@ -254,6 +294,71 @@ function SandboxViewer({
)
}
function CodeFontSizeControl({
decreaseLabel,
increaseLabel,
index,
onIndexChange,
}: {
decreaseLabel: string
increaseLabel: string
index: number
onIndexChange: React.Dispatch<React.SetStateAction<number>>
}) {
return (
<div
className="inline-flex h-8 items-center rounded-md border border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900"
aria-label={`${codeFontSizes[index]}px`}
>
<SandboxAction
disabled={index === 0}
label={decreaseLabel}
onClick={() => onIndexChange((value) => Math.max(0, value - 1))}
>
<Icon name="lucide:minus" className="size-3.5" />
</SandboxAction>
<span className="w-9 text-center font-mono text-[11px] text-slate-500 tabular-nums dark:text-slate-400">
{codeFontSizes[index]}
</span>
<SandboxAction
disabled={index === codeFontSizes.length - 1}
label={increaseLabel}
onClick={() =>
onIndexChange((value) =>
Math.min(codeFontSizes.length - 1, value + 1)
)
}
>
<Icon name="lucide:plus" className="size-3.5" />
</SandboxAction>
</div>
)
}
function HighlightedCode({ source }: { source: HighlightedSource }) {
return source.map((line, lineIndex) => (
<React.Fragment key={line.offset}>
<span className="sandbox-code-line">
{line.tokens.map((token) => (
<span
key={token.offset}
className="sandbox-code-token"
style={
{
"--sandbox-token-dark": token.dark,
"--sandbox-token-light": token.light,
} as React.CSSProperties
}
>
{token.content}
</span>
))}
</span>
{lineIndex < source.length - 1 ? "\n" : null}
</React.Fragment>
))
}
function SandboxTab({
active,
children,