| 1 | import "./highlight.css"; |
| 2 | |
| 3 | export type CodeLanguage = "rust" | "toml" | "sh"; |
| 4 | |
| 5 | type Span = { text: string; kind: "plain" | "comment" | "string" | "keyword" | "number" | "macro" }; |
| 6 | |
| 7 | const RUST_KEYWORDS = new Set( |
| 8 | "as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while".split(" "), |
| 9 | ); |
| 10 | |
| 11 | /** Tiny dependency-free tinting for the small code samples on this page. */ |
| 12 | export function tokenize(code: string, language: CodeLanguage): Span[] { |
| 13 | const spans: Span[] = []; |
| 14 | let rest = code; |
| 15 | let plain = ""; |
| 16 | const flush = () => { if (plain) { spans.push({ text: plain, kind: "plain" }); plain = ""; } }; |
| 17 | const lineComment = language === "rust" ? "//" : "#"; |
| 18 | while (rest) { |
| 19 | let match: RegExpMatchArray | null = null; |
| 20 | if (rest.startsWith(lineComment)) { |
| 21 | const end = rest.indexOf("\n"); |
| 22 | flush(); |
| 23 | spans.push({ text: end < 0 ? rest : rest.slice(0, end), kind: "comment" }); |
| 24 | rest = end < 0 ? "" : rest.slice(end); |
| 25 | continue; |
| 26 | } |
| 27 | if (language === "rust" && rest.startsWith("/*")) { |
| 28 | const end = rest.indexOf("*/"); |
| 29 | flush(); |
| 30 | const cut = end < 0 ? rest.length : end + 2; |
| 31 | spans.push({ text: rest.slice(0, cut), kind: "comment" }); |
| 32 | rest = rest.slice(cut); |
| 33 | continue; |
| 34 | } |
| 35 | if (rest[0] === '"') { |
| 36 | let i = 1; |
| 37 | while (i < rest.length && (rest[i] !== '"' || rest[i - 1] === "\\")) { |
| 38 | if (rest[i] === "\n") break; |
| 39 | i += 1; |
| 40 | } |
| 41 | flush(); |
| 42 | spans.push({ text: rest.slice(0, Math.min(i + 1, rest.length)), kind: "string" }); |
| 43 | rest = rest.slice(Math.min(i + 1, rest.length)); |
| 44 | continue; |
| 45 | } |
| 46 | if ((match = rest.match(/^[0-9][0-9_]*(\.[0-9_]+)?/))) { |
| 47 | flush(); |
| 48 | spans.push({ text: match[0], kind: "number" }); |
| 49 | rest = rest.slice(match[0].length); |
| 50 | continue; |
| 51 | } |
| 52 | if ((match = rest.match(/^[A-Za-z_][A-Za-z0-9_]*/))) { |
| 53 | const word = match[0]; |
| 54 | const after = rest.slice(word.length); |
| 55 | flush(); |
| 56 | if (language === "rust" && RUST_KEYWORDS.has(word)) spans.push({ text: word, kind: "keyword" }); |
| 57 | else if (after.startsWith("!")) { |
| 58 | spans.push({ text: word, kind: "macro" }); |
| 59 | rest = after; |
| 60 | continue; |
| 61 | } else plain += word; |
| 62 | rest = after; |
| 63 | continue; |
| 64 | } |
| 65 | plain += rest[0]; |
| 66 | rest = rest.slice(1); |
| 67 | } |
| 68 | flush(); |
| 69 | return spans; |
| 70 | } |
| 71 | |
| 72 | /** A code sample with gentle tinting. Copy still takes the raw text. */ |
| 73 | export function CodeBlock({ code, language }: { code: string; language: CodeLanguage }) { |
| 74 | return ( |
| 75 | <pre tabIndex={0} dir="ltr"> |
| 76 | <code>{tokenize(code, language).map((span, index) => |
| 77 | span.kind === "plain" ? <span key={index}>{span.text}</span> |
| 78 | : <span key={index} className={`rat-tok-${span.kind}`}>{span.text}</span>, |
| 79 | )}</code> |
| 80 | </pre> |
| 81 | ); |
| 82 | } |
| 83 |