import { codeToHtml, createCSSVariablesTheme, type FileContents, type FileDiffMetadata, type FileDiffOptions, type FileOptions, getSharedHighlighter, getSingularPatch, parseDiffFromFile, registerCustomTheme, } from "@pierre/diffs"; import { preloadDiffHTML, preloadFile } from "@pierre/diffs/ssr"; import { bundledLanguagesInfo } from "shiki"; /** Shared Shiki theme. Token colors resolve to `--syntax-*` CSS variables * declared in `src/styles/syntax.css`, so light/dark switch with `.dark`. */ const theme = createCSSVariablesTheme({ name: "ace", variablePrefix: "--syntax-", variableDefaults: {}, fontStyle: true, }); registerCustomTheme("ace", () => Promise.resolve(theme)); /** Languages preloaded into the shared diffs highlighter. Consumers that route * through `codeToHtml` (Code, CodeView, ToolResult) lazy-load any bundled * language on demand via Shiki's own singleton, so this list only needs to * cover the rich-text editor preload path. */ const langs = [ "bash", "css", "diff", "html", "js", "json", "jsx", "markdown", "rust", "shell", "ts", "tsx", ]; /** Maps every Shiki language id and alias to its canonical id. Built once * from `bundledLanguagesInfo` so new Shiki languages are picked up * automatically without touching this file. */ const languages = new Map(); for (let info of bundledLanguagesInfo) { languages.set(info.id, info.id); for (let alias of info.aliases || []) languages.set(alias, info.id); } const LIMIT = 512; type Entry = { promise: Promise; html?: string; }; let cache = new Map(); const options = { diffStyle: "unified", disableFileHeader: true, hunkSeparators: "line-info", diffIndicators: "classic", expansionLineCount: 40, theme: "ace", unsafeCSS: ` [data-diff] { --diffs-grid-number-column-width: calc(3ch + 24px); --diffs-bg-separator-override: var(--color-background); } [data-gutter] [data-column-number] { padding-inline: 0.5rem; padding-inline-start: 1rem; } [data-gutter] [data-column-number][data-line-type='context'] { color: color-mix(in oklch, var(--color-muted-foreground), transparent 25%); } [data-code] { padding-block: 6px 0; } [data-diff-type='split'][data-overflow='scroll'] > [data-code] { min-width: 0; } [data-diff-type='split'] > [data-code][data-additions] { margin-inline-start: -1px; } [data-diff-type='split'] [data-content] { margin-inline-start: -1px; } [data-content] :is( [data-line-type='change-addition'], [data-line-type='change-deletion'] ) { padding-inline-start: calc(1.2em + 4px); } [data-content] :is( [data-line-type='change-addition'], [data-line-type='change-deletion'] )::before { inset-inline-start: 4px; } [data-separator='line-info'] { background-color: var(--diffs-bg-separator); color: var(--color-muted-foreground); } [data-separator='line-info'] [data-separator-content], [data-separator='line-info'] [data-expand-button] { background-color: var(--diffs-bg-separator); color: var(--color-muted-foreground); } [data-separator='line-info'] [data-separator-content] { padding-inline: 0.65rem; } [data-separator='line-info']:has([data-expand-button]) { background-color: transparent; } [data-separator='line-info']:has([data-expand-button]) [data-separator-wrapper] { cursor: pointer; margin-inline: 0; overflow: hidden; border-radius: 4px; } [data-separator='line-info']:has([data-expand-button]) [data-separator-content] { padding-inline: 0; transition: color 150ms ease-out; } [data-separator='line-info']:has([data-expand-button]) [data-unmodified-lines] { box-sizing: border-box; inline-size: 100%; block-size: 100%; padding-inline-start: 16px; align-content: center; } [data-separator='line-info']:has([data-expand-button]) [data-expand-button] svg { transition: transform 150ms ease-out; } [data-separator='line-info']:has([data-expand-button]) [data-separator-content]:hover, [data-separator='line-info']:has([data-expand-button]) [data-unmodified-lines] { text-decoration-line: none; } [data-separator='line-info']:has([data-expand-button]):hover { background-color: transparent; } [data-separator='line-info']:has([data-expand-button]):hover [data-separator-content], [data-separator='line-info']:has([data-expand-button]):hover [data-expand-button] { background-color: var(--color-muted); color: var(--color-muted-foreground); } [data-separator='line-info']:has([data-expand-button]) [data-expand-button]:active svg { transform: translateY(1px) scale(0.96); } [data-diff-type='split'] [data-deletions] [data-content] [data-separator='line-info']:has([data-expand-button]) [data-separator-wrapper] { display: flex; height: 100%; margin-inline: 0; border-radius: 0; background-color: var(--diffs-bg-separator); } [data-diff-type='split'] [data-deletions] [data-content] [data-separator='line-info']:has([data-expand-button]) [data-expand-button] { display: none; } [data-diff-type='split'] [data-deletions] [data-content] [data-separator='line-info']:has([data-expand-button]) [data-separator-content] { min-width: 0; padding-inline-start: 0.65rem; border-radius: 0; } `, } satisfies FileDiffOptions; const source = { disableFileHeader: true, overflow: "scroll", theme: "ace", unsafeCSS: ` [data-file] { --diffs-grid-number-column-width: minmax(min-content, max-content); } [data-file] [data-code], [data-file] [data-content], [data-file] [data-line] { -webkit-user-select: text; user-select: text; } `, } satisfies FileOptions; type DiffStyle = NonNullable["diffStyle"]>; type DiffOptionsOverride = Partial, "hunkSeparators">>; function diffOptions( style: DiffStyle = "unified", overrides?: DiffOptionsOverride, ): FileDiffOptions { return { ...options, ...overrides, diffStyle: style }; } function lang(value: string | undefined) { return (value && languages.get(value)) || "text"; } function key(language: string | undefined, source: string) { return lang(language) + "\0" + source; } type DiffContents = { oldFile?: FileContents; newFile?: FileContents; }; function hash(value: string) { let hash = 0x811c9dc5; for (let i = 0; i < value.length; i++) { hash ^= value.charCodeAt(i); hash = Math.imul(hash, 0x01000193); } return (hash >>> 0).toString(36); } function contentKey(contents?: DiffContents) { if (!contents?.oldFile || !contents.newFile) return ""; return [ contents.oldFile.name, contents.oldFile.contents.length, hash(contents.oldFile.contents), contents.newFile.name, contents.newFile.contents.length, hash(contents.newFile.contents), ].join("\0"); } function fileKey(file: FileContents) { return `file:${ file.cacheKey ?? [ file.name, file.lang ?? "", file.contents.length, hash(file.contents), ].join("\0") }`; } function diffKey( style: DiffStyle, patch: string, file?: FileDiffMetadata | null, overrides?: DiffOptionsOverride, ) { return `file-diff:${style}:${overrides?.hunkSeparators ?? options.hunkSeparators}\0${ file?.cacheKey ?? patch }`; } function get(key: string) { let hit = cache.get(key); if (!hit) return; cache.delete(key); cache.set(key, hit); return hit; } function set(key: string, entry: Entry) { if (cache.has(key)) cache.delete(key); while (cache.size >= LIMIT) cache.delete(cache.keys().next().value!); cache.set(key, entry); } function render(key: string, fn: () => string | Promise) { let hit = get(key); if (hit) return hit; let entry = {} as Entry; entry.promise = Promise.resolve(fn()).then( html => { entry.html = html; return html; }, error => { if (cache.get(key) === entry) cache.delete(key); throw error; }, ); set(key, entry); return entry; } /** Lazily returns a shared Shiki highlighter loaded with the "ace" theme and common languages. */ function getHighlighter() { return getSharedHighlighter({ themes: ["ace"], langs }); } function cached(source: string, language?: string) { return get(key(language, source))?.html; } /** Render `source` as highlighted HTML. Unknown languages fall back to `text`. */ function highlight(source: string, language?: string) { return render( key(language, source), () => codeToHtml(source, { lang: lang(language), theme }), ) .promise; } function cachedDiff( patch: string, style: DiffStyle = "unified", file?: FileDiffMetadata | null, overrides?: DiffOptionsOverride, ) { return get(diffKey(style, patch, file, overrides))?.html; } function cachedFile(file: FileContents) { return get(fileKey(file))?.html; } function complete(patch: string) { return ( /^diff --git\s+\S+\s+\S+/m.test(patch) || ( /^---\s+\S+/m.test(patch) && /^\+\+\+\s+\S+/m.test(patch) && /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?/m.test(patch) ) ); } function parseDiff(patch: string, contents?: DiffContents): FileDiffMetadata | null { if (!complete(patch)) return null; try { if (contents?.oldFile && contents.newFile) { let key = contentKey(contents); return parseDiffFromFile( { ...contents.oldFile, cacheKey: `${key}:old` }, { ...contents.newFile, cacheKey: `${key}:new` }, ); } return getSingularPatch(patch); } catch { try { return getSingularPatch(patch); } catch { return null; } } } function highlightDiff( patch: string, file = parseDiff(patch), style: DiffStyle = "unified", overrides?: DiffOptionsOverride, ) { if (!file) return Promise.resolve(""); return render( diffKey(style, patch, file, overrides), () => preloadDiffHTML({ fileDiff: file, options: diffOptions(style, overrides) }), ).promise; } function fileOptions(): FileOptions { return source; } function highlightFile(file: FileContents) { return render( fileKey(file), () => preloadFile({ file, options: fileOptions() }).then(result => result.prerenderedHTML), ).promise; } export { cached, cachedDiff, cachedFile, diffOptions, fileOptions, getHighlighter, highlight, highlightDiff, highlightFile, languages, LIMIT, parseDiff, theme, }; export type { DiffContents, DiffOptionsOverride, DiffStyle };