SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
4.4 KB · 110 lines tsx
Raw Blame History
1import * as React from "react";2import { cn } from "@/lib/utils";3import { CopyButton } from "./copy-button";45export type CodeLang = "bash" | "json" | "javascript" | "typescript" | "python" | "go" | "php" | "ruby" | "java" | "csharp" | "html" | "text";67/** Tiny, dependency-free tokenizer good enough for docs and playground snippets. */8export function highlight(code: string, lang: CodeLang): React.ReactNode[] {9  const esc = (s: string) => s;10  if (lang === "text") return [esc(code)];11  const rules: Array<[RegExp, string]> = [];12  if (lang === "json") {13    rules.push([/"(?:[^"\\]|\\.)*"(?=\s*:)/g, "tok-a"], [/"(?:[^"\\]|\\.)*"/g, "tok-s"], [/\b(?:true|false|null)\b/g, "tok-k"], [/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/gi, "tok-n"]);14  } else if (lang === "bash") {15    rules.push([/#.*$/gm, "tok-c"], [/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, "tok-s"], [/^\s*(curl|export|echo|pip|npm|pnpm|yarn|go|python|node|fetcha)\b/gm, "tok-k"], [/\s(-{1,2}[a-zA-Z-]+)/g, "tok-a"]);16  } else if (lang === "html") {17    rules.push([/<!--[\s\S]*?-->/g, "tok-c"], [/<\/?[a-zA-Z][^\s>]*/g, "tok-t"], [/"[^"]*"/g, "tok-s"]);18  } else {19    rules.push(20      [/\/\/.*$|#.*$/gm, "tok-c"],21      [/\/\*[\s\S]*?\*\//g, "tok-c"],22      [/`(?:[^`\\]|\\.)*`|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g, "tok-s"],23      [/\b(?:import|from|export|const|let|var|function|async|await|return|new|class|def|if|else|elif|for|while|try|except|catch|finally|throw|raise|print|package|func|use|require|public|static|void|string|var|namespace|using|end|do|puts|nil|None|True|False|true|false|null|undefined|with|as|in|not|and|or|fmt|err)\b/g, "tok-k"],24      [/\b\d+(?:\.\d+)?\b/g, "tok-n"],25      [/\b(?:Fetcha|FetchaClient|fetch|json|text|sessions|create|Println|Fatal|NewRequest|Marshal|env|process|os|getenv|environ|std|Console|WriteLine)\b(?=\s*[\(\.])/g, "tok-a"],26    );27  }28  type Span = { s: number; e: number; c: string };29  const spans: Span[] = [];30  for (const [re, c] of rules) {31    re.lastIndex = 0;32    let m: RegExpExecArray | null;33    while ((m = re.exec(code))) {34      if (!m[0]) {35        re.lastIndex++;36        continue;37      }38      const s = m.index;39      const e = s + m[0].length;40      if (spans.some((x) => s < x.e && e > x.s)) continue;41      spans.push({ s, e, c });42    }43  }44  spans.sort((a, b) => a.s - b.s);45  const out: React.ReactNode[] = [];46  let i = 0;47  for (const sp of spans) {48    if (sp.s > i) out.push(esc(code.slice(i, sp.s)));49    out.push(50      <span key={sp.s} className={sp.c}>51        {code.slice(sp.s, sp.e)}52      </span>,53    );54    i = sp.e;55  }56  if (i < code.length) out.push(esc(code.slice(i)));57  return out;58}5960export function CodeBlock({61  code,62  lang = "bash",63  title,64  className,65  maxHeight,66  lineNumbers,67  copy = true,68}: {69  code: string;70  lang?: CodeLang;71  title?: string;72  className?: string;73  maxHeight?: number | string;74  lineNumbers?: boolean;75  copy?: boolean;76}) {77  const lines = code.replace(/\n$/, "").split("\n");78  return (79    <div className={cn("group relative overflow-hidden rounded-lg border border-border bg-bg-subtle dark:bg-[#0b0f17] text-[12.5px] leading-[1.6]", className)}>80      {title ? (81        <div className="flex items-center justify-between border-b border-border px-3.5 py-1.5 text-[11.5px] text-fg-subtle">82          <span className="font-mono">{title}</span>83          {copy ? <CopyButton value={code} size="icon-sm" className="-mr-1.5 size-7" /> : null}84        </div>85      ) : copy ? (86        <div className="absolute right-1.5 top-1.5 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">87          <CopyButton value={code} size="icon-sm" className="size-7 bg-bg/80 backdrop-blur" />88        </div>89      ) : null}90      <pre className="overflow-auto p-3.5 scrollbar-thin font-mono" style={{ maxHeight }}>91        <code>92          {lineNumbers93            ? lines.map((l, i) => (94                <span key={i} className="block">95                  <span className="mr-4 inline-block w-6 select-none text-right text-fg-subtle/70">{i + 1}</span>96                  {highlight(l, lang)}97                  {"\n"}98                </span>99              ))100            : highlight(code, lang)}101        </code>102      </pre>103    </div>104  );105}106107export function InlineCode({ children, className }: { children: React.ReactNode; className?: string }) {108  return <code className={cn("rounded-[4px] border border-border bg-bg-muted px-1.5 py-0.5 font-mono text-[0.85em] text-fg", className)}>{children}</code>;109}110