import { useCallback, useMemo, useState } from "react"; import { getHighlighter, useResolvedShikiTheme, type ShikiThemeProp, type SupportedTheme, } from "../lib/shiki"; import { cn } from "../lib/utils"; import { Button } from "./button"; import type { ThemedToken } from "shiki/core"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type Definition = { readonly name: string; /** The full type body string, e.g. `{ login: string; id: number }` */ readonly code: string; }; // --------------------------------------------------------------------------- // Simple TypeScript formatter — expands single-line types into readable form // --------------------------------------------------------------------------- const formatTypeScript = (code: string): string => { let result = ""; let indent = 0; let i = 0; const len = code.length; // Track brace depth so we only break `|` and `&` at the top level of a union let braceDepth = 0; while (i < len) { const ch = code[i]!; if (ch === "{") { braceDepth++; indent++; result += "{\n" + " ".repeat(indent); i++; while (i < len && code[i] === " ") i++; } else if (ch === "}") { braceDepth = Math.max(0, braceDepth - 1); indent = Math.max(0, indent - 1); result += "\n" + " ".repeat(indent) + "}"; i++; } else if (ch === ";" && i + 1 < len && code[i + 1] === " ") { result += ";\n" + " ".repeat(indent); i += 2; } else if (ch === ";" && indent > 0) { result += ";\n" + " ".repeat(indent); i++; while (i < len && code[i] === " ") i++; } else if (ch === "|" && i > 0 && code[i - 1] === " " && i + 1 < len && code[i + 1] === " ") { // Union operator ` | ` — break onto new line result += "\n" + " ".repeat(indent) + "| "; i += 2; // skip the operator and trailing space } else { result += ch; i++; } } return result; }; // --------------------------------------------------------------------------- // Copy icons // --------------------------------------------------------------------------- const CopyIcon = () => ( ); const CheckIcon = () => ( ); // --------------------------------------------------------------------------- // Shiki tokenization hook — non-blocking // --------------------------------------------------------------------------- function useTokens(code: string, theme: SupportedTheme): ThemedToken[][] { const highlighter = getHighlighter(); const result = highlighter.codeToTokens(code, { lang: "typescript", theme, }); return result.tokens; } // --------------------------------------------------------------------------- // Inline-expand logic // // When a user clicks a ref name like `NonEmptyTrimmedString`, we replace // that name with the resolved body inline. So `firstName: NonEmptyTrimmedString` // becomes `firstName: string`. // // We rebuild the full code string with expansions applied, then re-tokenize. // --------------------------------------------------------------------------- /** * Given a code string, a set of ref names to expand, and their definitions, * produce a new code string with each expanded ref replaced by its body. * Applies recursively — if an expanded body itself contains expanded refs, * those get replaced too. Tracks ancestors to prevent infinite loops. */ const applyExpansions = ( code: string, expanded: ReadonlySet, definitions: ReadonlyMap, ancestors: ReadonlySet, ): string => { if (expanded.size === 0) return code; let result = ""; let i = 0; // Sort expanded names longest-first to match greedily const sortedNames = [...expanded] .filter((n) => !ancestors.has(n)) .sort((a, b) => b.length - a.length); while (i < code.length) { let matched = false; for (const name of sortedNames) { if (code.startsWith(name, i)) { const body = definitions.get(name); if (body) { // Recursively expand the body, adding this name to ancestors const childAncestors = new Set(ancestors); childAncestors.add(name); result += applyExpansions(body, expanded, definitions, childAncestors); i += name.length; matched = true; break; } } } if (!matched) { result += code[i]; i++; } } return result; }; // --------------------------------------------------------------------------- // Split tokens to find ref names that can be clicked // --------------------------------------------------------------------------- type RenderToken = | { kind: "text"; content: string; color?: string } | { kind: "ref"; name: string; color?: string }; const splitToken = (token: ThemedToken, clickableNames: ReadonlySet): RenderToken[] => { if (clickableNames.size === 0) { return [{ kind: "text", content: token.content, color: token.color }]; } const text = token.content; const results: RenderToken[] = []; let remaining = text; while (remaining.length > 0) { let earliest = -1; let matchedName = ""; for (const name of clickableNames) { const idx = remaining.indexOf(name); if ( idx !== -1 && (earliest === -1 || idx < earliest || (idx === earliest && name.length > matchedName.length)) ) { earliest = idx; matchedName = name; } } if (earliest === -1) { results.push({ kind: "text", content: remaining, color: token.color }); break; } if (earliest > 0) { results.push({ kind: "text", content: remaining.slice(0, earliest), color: token.color }); } results.push({ kind: "ref", name: matchedName, color: token.color }); remaining = remaining.slice(earliest + matchedName.length); } return results; }; // --------------------------------------------------------------------------- // HighlightedCode — renders tokenized code with clickable unexpanded refs // --------------------------------------------------------------------------- function HighlightedCode(props: { tokens: ThemedToken[][]; /** Ref names that are NOT yet expanded and can be clicked */ clickableNames: ReadonlySet; onToggle: (name: string) => void; expanded: ReadonlySet; }) { const { tokens, clickableNames, onToggle, expanded } = props; return ( <> {tokens.map((line, li) => ( {li > 0 && "\n"} {line.map((token, ti) => { const parts = splitToken(token, clickableNames); return parts.map((part, pi) => { if (part.kind === "text") { return ( {part.content} ); } const isExpanded = expanded.has(part.name); return ( { e.stopPropagation(); onToggle(part.name); }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onToggle(part.name); } }} className={cn( "cursor-pointer underline underline-offset-2 hover:opacity-80", isExpanded ? "decoration-current/50" : "decoration-current/30", )} style={part.color ? { color: part.color } : undefined} title={isExpanded ? `Collapse ${part.name}` : `Expand ${part.name}`} > {part.name} ); }); })} ))} ); } // --------------------------------------------------------------------------- // ExpandableCodeBlock — main export // --------------------------------------------------------------------------- export function ExpandableCodeBlock(props: { code: string; definitions?: readonly Definition[]; className?: string; theme?: ShikiThemeProp; }) { const { code, definitions = [], className, theme } = props; const resolvedTheme = useResolvedShikiTheme(theme); // Auto-expand trivial aliases (primitives, simple unions, string literals) const trivialNames = useMemo(() => { const trivial = new Set(); const trivialPattern = /^(?:string|number|boolean|null|undefined|void|never|unknown|any|true|false|(?:"[^"]*"(?:\s*\|\s*"[^"]*")*))$/; for (const d of definitions) { const body = d.code.trim(); if (trivialPattern.test(body)) { trivial.add(d.name); } } return trivial; }, [definitions]); const [expanded, setExpanded] = useState>(() => new Set()); const [copied, setCopied] = useState(false); const definitionMap = useMemo( () => new Map(definitions.map((d) => [d.name, d.code])), [definitions], ); const definitionNames = useMemo(() => new Set(definitions.map((d) => d.name)), [definitions]); // Names that can be clicked — exclude trivial aliases (already inlined) const clickableNames = useMemo(() => { const names = new Set(definitionNames); for (const name of trivialNames) names.delete(name); return names; }, [definitionNames, trivialNames]); const emptyAncestors = useMemo(() => new Set(), []); // Merge user-expanded + trivial auto-expanded const allExpanded = useMemo(() => { const merged = new Set(expanded); for (const name of trivialNames) merged.add(name); return merged; }, [expanded, trivialNames]); // Build the display code: start with raw code, apply expansions, then format const displayCode = useMemo(() => { const withExpansions = applyExpansions(code, allExpanded, definitionMap, emptyAncestors); return formatTypeScript(withExpansions); }, [code, allExpanded, definitionMap, emptyAncestors]); const tokens = useTokens(displayCode, resolvedTheme); const handleToggle = useCallback((name: string) => { setExpanded((prev) => { const next = new Set(prev); if (next.has(name)) { next.delete(name); } else { next.add(name); } return next; }); }, []); const handleCopy = useCallback(() => { void navigator.clipboard.writeText(displayCode).then(() => { setCopied(true); setTimeout(() => setCopied(false), 1500); }); }, [displayCode]); return (
          
            
          
        
); }