// Dependency-free syntax highlighter for the marketing hero code tabs. // // The original Rivet site rendered these snippets with Shiki at build time and // styled the resulting `.shiki` / `.line` markup. Shiki is not resolvable from // this package, so we tokenize the (TS/JS) source ourselves and emit the same // `.shiki` > `.line` structure with inline-colored token spans. The hero code // block styles the container (`[&_.shiki]:!bg-transparent`, muted base text), so // only the token foregrounds matter here. // // Kept async + same signature as the prior `highlightCodeHtml` so callers // (Astro pages) do not change. function escapeHtml(code: string): string { return code .replace(/&/g, "&") .replace(//g, ">"); } // Light-theme palette: purple keywords, orange strings, gray-italic comments, // blue function calls. Tuned to read on the light (`bg-zinc-50`) code block. const STYLES: Record = { comment: "color:#8b949e;font-style:italic", keyword: "color:#8250df", string: "color:#b45309", number: "color:#0550ae", fn: "color:#0550ae", }; const KEYWORDS = new Set([ "import", "export", "from", "const", "let", "var", "await", "async", "function", "return", "new", "class", "extends", "implements", "interface", "type", "enum", "if", "else", "for", "while", "do", "switch", "case", "break", "continue", "default", "try", "catch", "finally", "throw", "typeof", "instanceof", "in", "of", "void", "yield", "this", "super", "static", "public", "private", "protected", "readonly", "as", "satisfies", "namespace", "declare", "true", "false", "null", "undefined", ]); interface Token { type: keyof typeof STYLES | "text"; value: string; } function tokenize(code: string): Token[] { const tokens: Token[] = []; const n = code.length; let i = 0; let text = ""; const flushText = () => { if (text) { tokens.push({ type: "text", value: text }); text = ""; } }; const isIdentStart = (c: string) => /[A-Za-z_$]/.test(c); const isIdent = (c: string) => /[\w$]/.test(c); while (i < n) { const c = code[i]; const next = code[i + 1]; // Line comment if (c === "/" && next === "/") { flushText(); let j = i + 2; while (j < n && code[j] !== "\n") j++; tokens.push({ type: "comment", value: code.slice(i, j) }); i = j; continue; } // Block comment if (c === "/" && next === "*") { flushText(); let j = i + 2; while (j < n && !(code[j] === "*" && code[j + 1] === "/")) j++; j = Math.min(n, j + 2); tokens.push({ type: "comment", value: code.slice(i, j) }); i = j; continue; } // Strings and template literals if (c === '"' || c === "'" || c === "`") { flushText(); const quote = c; let j = i + 1; while (j < n) { if (code[j] === "\\") { j += 2; continue; } if (code[j] === quote) { j++; break; } j++; } tokens.push({ type: "string", value: code.slice(i, j) }); i = j; continue; } // Numbers if (/[0-9]/.test(c) && !(text && isIdent(text[text.length - 1]))) { flushText(); let j = i; while (j < n && /[0-9._a-fxA-FXn]/.test(code[j])) j++; tokens.push({ type: "number", value: code.slice(i, j) }); i = j; continue; } // Identifiers / keywords / function calls if (isIdentStart(c)) { flushText(); let j = i + 1; while (j < n && isIdent(code[j])) j++; const word = code.slice(i, j); if (KEYWORDS.has(word)) { tokens.push({ type: "keyword", value: word }); } else { // Function call if the next non-space char is "(" let k = j; while (k < n && (code[k] === " " || code[k] === "\t")) k++; tokens.push({ type: code[k] === "(" ? "fn" : "text", value: word }); } i = j; continue; } text += c; i++; } flushText(); return tokens; } function renderTokens(tokens: Token[]): string { // Build per-line markup, splitting token values that span newlines so each // `.line` stays self-contained (the container styles `.line` for wrapping). const lines: string[][] = [[]]; for (const token of tokens) { const parts = token.value.split("\n"); parts.forEach((part, idx) => { if (idx > 0) lines.push([]); if (part === "") return; const escaped = escapeHtml(part); const style = token.type === "text" ? undefined : STYLES[token.type]; lines[lines.length - 1].push( style ? `${escaped}` : escaped, ); }); } return lines .map((parts) => `${parts.join("") || " "}`) .join("\n"); } export async function highlightCodeHtml( code: string, _lang = "ts", _theme?: string, ): Promise { const html = renderTokens(tokenize(code)); return `
${html}
`; }