{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "highlight",
  "title": "Highlight",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A small dependency-free tokenizer for the snippets developer UIs show: JSON, shell and curl, JavaScript and TypeScript, Python, Go, HTTP and .env, with theme-aware token colors.",
  "files": [
    {
      "path": "registry/ballmac/lib/highlight.ts",
      "content": "// Ballmac UI: Highlight. https://ui.ballmac.com/components/highlight\n// A tiny, dependency-free tokenizer for the snippets developer components show: JSON, shell and curl, JS/TS, Python, Go, HTTP and .env.\n\nexport type TokenType = \"plain\" | \"comment\" | \"string\" | \"number\" | \"keyword\" | \"property\" | \"function\" | \"flag\" | \"punctuation\" | \"literal\"\n\nexport type Token = { type: TokenType; text: string }\n\nexport type HighlightLanguage = \"json\" | \"bash\" | \"javascript\" | \"typescript\" | \"python\" | \"go\" | \"http\" | \"env\" | \"text\"\n\n/**\n * Tailwind classes per token type. Each color is a chart token mixed into the foreground,\n * so contrast holds on light and dark surfaces and the palette follows the theme.\n */\nexport const tokenClass: Record<TokenType, string> = {\n  plain: \"\",\n  comment: \"text-muted-foreground italic\",\n  string: \"text-[color-mix(in_oklab,var(--chart-2)_60%,var(--foreground))]\",\n  number: \"text-[color-mix(in_oklab,var(--chart-3)_58%,var(--foreground))]\",\n  keyword: \"text-[color-mix(in_oklab,var(--chart-4)_62%,var(--foreground))] font-medium\",\n  property: \"text-[color-mix(in_oklab,var(--chart-1)_62%,var(--foreground))]\",\n  function: \"text-[color-mix(in_oklab,var(--chart-5)_58%,var(--foreground))]\",\n  flag: \"text-[color-mix(in_oklab,var(--chart-5)_58%,var(--foreground))]\",\n  punctuation: \"text-muted-foreground\",\n  literal: \"text-[color-mix(in_oklab,var(--chart-3)_58%,var(--foreground))] font-medium\",\n}\n\nconst JS_KEYWORDS =\n  \"const let var function return if else for while do switch case break continue new class extends import from export default async await try catch finally throw typeof instanceof of in as interface type enum implements public private readonly void\"\nconst PY_KEYWORDS =\n  \"def return if elif else for while in not and or is import from as class try except finally raise with lambda pass yield async await global del assert\"\nconst GO_KEYWORDS =\n  \"package import func return if else for range switch case default break continue go defer select chan map struct interface type var const\"\nconst BASH_KEYWORDS = \"if then else fi for do done while case esac function in export\"\nconst LITERALS = \"true false null undefined None True False nil NaN\"\n\nconst set = (words: string) => new RegExp(`^(?:${words.trim().split(/\\s+/).join(\"|\")})$`)\n\ntype Rule = [TokenType, RegExp]\n\nconst common = {\n  json: [\n    [\"property\", /\"(?:\\\\.|[^\"\\\\\\n])*\"(?=\\s*:)/y],\n    [\"string\", /\"(?:\\\\.|[^\"\\\\\\n])*\"/y],\n    [\"number\", /-?\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b/y],\n    [\"literal\", /\\b(?:true|false|null)\\b/y],\n    [\"punctuation\", /[{}[\\],:]/y],\n  ],\n} as const\n\n/** Splits `code` into tokens. Words are classified afterwards so keywords, literals and calls can be told apart. */\nexport function tokenize(code: string, language: HighlightLanguage = \"text\"): Token[] {\n  if (language === \"text\") return [{ type: \"plain\", text: code }]\n  const out: Token[] = []\n  const push = (type: TokenType, text: string) => {\n    const last = out[out.length - 1]\n    if (last && last.type === type && type !== \"plain\") last.text += text\n    else out.push({ type, text })\n  }\n\n  if (language === \"json\") {\n    scan(code, common.json as unknown as Rule[], push)\n    return out\n  }\n\n  if (language === \"env\") {\n    for (const line of code.split(\"\\n\")) {\n      const m = line.match(/^(\\s*(?:export\\s+)?)([A-Za-z_][A-Za-z0-9_]*)(=)(.*)$/)\n      if (/^\\s*#/.test(line)) push(\"comment\", line + \"\\n\")\n      else if (m) {\n        push(\"plain\", m[1]!)\n        push(\"property\", m[2]!)\n        push(\"punctuation\", m[3]!)\n        push(/^[\"'].*[\"']$/.test(m[4]!.trim()) ? \"string\" : \"plain\", m[4]! + \"\\n\")\n      } else push(\"plain\", line + \"\\n\")\n    }\n    const last = out[out.length - 1]\n    if (last && !code.endsWith(\"\\n\")) last.text = last.text.replace(/\\n$/, \"\")\n    return out\n  }\n\n  if (language === \"http\") {\n    code.split(\"\\n\").forEach((line, i, all) => {\n      const nl = i < all.length - 1 ? \"\\n\" : \"\"\n      let m: RegExpMatchArray | null\n      if ((m = line.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)(\\s+)(\\S+)(.*)$/))) {\n        push(\"keyword\", m[1]!)\n        push(\"plain\", m[2]!)\n        push(\"function\", m[3]!)\n        push(\"punctuation\", m[4]! + nl)\n      } else if ((m = line.match(/^(HTTP\\/[\\d.]+)(\\s+)(\\d{3})(.*)$/))) {\n        push(\"punctuation\", m[1]!)\n        push(\"plain\", m[2]!)\n        push(\"number\", m[3]!)\n        push(\"plain\", m[4]! + nl)\n      } else if ((m = line.match(/^([\\w-]+)(:)(.*)$/))) {\n        push(\"property\", m[1]!)\n        push(\"punctuation\", m[2]!)\n        push(\"string\", m[3]! + nl)\n      } else push(\"plain\", line + nl)\n    })\n    return out\n  }\n\n  const words =\n    language === \"python\" ? set(PY_KEYWORDS) : language === \"go\" ? set(GO_KEYWORDS) : language === \"bash\" ? set(BASH_KEYWORDS) : set(JS_KEYWORDS)\n  const literals = set(LITERALS)\n\n  const rules: Rule[] =\n    language === \"bash\"\n      ? [\n          [\"comment\", /(?:^|(?<=\\s))#[^\\n]*/y],\n          [\"string\", /\"(?:\\\\[\\s\\S]|[^\"\\\\])*\"|'[^']*'/y],\n          [\"flag\", /(?<=^|\\s)--?[A-Za-z][\\w-]*/y],\n          [\"property\", /\\$\\{?[A-Za-z_]\\w*\\}?/y],\n          [\"number\", /\\b\\d+(?:\\.\\d+)?\\b/y],\n          [\"plain\", /\\s+/y],\n          [\"string\", /https?:\\/\\/[^\\s\"'\\\\]+/y],\n          [\"plain\", /[A-Za-z_][\\w./-]*/y],\n          [\"punctuation\", /[|&;<>()=\\\\]+/y],\n        ]\n      : language === \"python\"\n        ? [\n            [\"comment\", /#[^\\n]*/y],\n            [\"string\", /\"\"\"[\\s\\S]*?\"\"\"|'''[\\s\\S]*?'''|[rbf]?\"(?:\\\\.|[^\"\\\\\\n])*\"|[rbf]?'(?:\\\\.|[^'\\\\\\n])*'/y],\n            [\"number\", /\\b\\d[\\d_]*(?:\\.\\d+)?\\b/y],\n            [\"plain\", /\\s+/y],\n            [\"plain\", /[A-Za-z_]\\w*/y],\n            [\"punctuation\", /[{}()[\\],.:=+\\-*/%<>!&|@]+/y],\n          ]\n        : [\n            [\"comment\", /\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\//y],\n            [\"string\", /`(?:\\\\[\\s\\S]|[^`\\\\])*`|\"(?:\\\\.|[^\"\\\\\\n])*\"|'(?:\\\\.|[^'\\\\\\n])*'/y],\n            [\"number\", /\\b\\d[\\d_]*(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b/y],\n            [\"plain\", /\\s+/y],\n            [\"plain\", /[A-Za-z_$][\\w$]*/y],\n            [\"punctuation\", /[{}()[\\];,.:<>=+\\-*/%&|!?]+/y],\n          ]\n\n  scan(code, rules, (type, text) => {\n    if (type !== \"plain\" || !/^[A-Za-z_$]/.test(text)) return push(type, text)\n    if (words.test(text)) return push(\"keyword\", text)\n    if (literals.test(text)) return push(\"literal\", text)\n    return push(\"plain\", text)\n  })\n\n  if (language === \"bash\") {\n    // The first word of each command is the program name; a trailing backslash continues the command.\n    let atStart = true\n    let continued = false\n    for (const t of out) {\n      if (t.type === \"plain\" && !t.text.trim()) {\n        if (t.text.includes(\"\\n\") && !continued) atStart = true\n        continue\n      }\n      continued = t.type === \"punctuation\" && t.text.endsWith(\"\\\\\")\n      if (atStart && t.type === \"plain\" && /^[A-Za-z_]/.test(t.text)) t.type = \"function\"\n      atStart = t.type === \"punctuation\" && /[|&;]/.test(t.text)\n    }\n  } else {\n    // Identifiers directly followed by \"(\" are calls.\n    for (let i = 0; i < out.length - 1; i++) {\n      const t = out[i]!\n      if (t.type === \"plain\" && /^[A-Za-z_$][\\w$]*$/.test(t.text) && out[i + 1]!.text.startsWith(\"(\")) t.type = \"function\"\n    }\n  }\n  return out\n}\n\nfunction scan(code: string, rules: Rule[], push: (type: TokenType, text: string) => void) {\n  let i = 0\n  while (i < code.length) {\n    let matched = false\n    for (const [type, re] of rules) {\n      re.lastIndex = i\n      const m = re.exec(code)\n      if (m && m.index === i && m[0].length > 0) {\n        push(type, m[0])\n        i += m[0].length\n        matched = true\n        break\n      }\n    }\n    if (!matched) {\n      push(\"plain\", code[i]!)\n      i++\n    }\n  }\n}\n\n/** Tokenizes `code` and groups the tokens by line, ready to render one row at a time. */\nexport function highlightLines(code: string, language: HighlightLanguage = \"text\"): Token[][] {\n  const lines: Token[][] = [[]]\n  for (const token of tokenize(code, language)) {\n    const parts = token.text.split(\"\\n\")\n    parts.forEach((part, index) => {\n      if (index > 0) lines.push([])\n      if (part) lines[lines.length - 1]!.push({ type: token.type, text: part })\n    })\n  }\n  return lines\n}\n\n/** Maps a file name or label such as \"app.ts\", \"curl\" or \"Python\" to a highlight language. */\nexport function languageFromName(name: string): HighlightLanguage {\n  const n = name.toLowerCase()\n  if (/\\.(json|jsonc)$/.test(n) || n === \"json\") return \"json\"\n  if (/\\.(ts|tsx|mts|cts)$/.test(n) || n === \"typescript\" || n === \"ts\") return \"typescript\"\n  if (/\\.(js|jsx|mjs|cjs)$/.test(n) || /^(javascript|js|node|node\\.js)$/.test(n)) return \"javascript\"\n  if (/\\.py$/.test(n) || n === \"python\") return \"python\"\n  if (/\\.go$/.test(n) || n === \"go\") return \"go\"\n  if (/\\.(sh|bash|zsh)$/.test(n) || /^(bash|shell|sh|curl|zsh|cli)$/.test(n)) return \"bash\"\n  if (/(^|\\.)env(\\..*)?$/.test(n)) return \"env\"\n  if (n === \"http\") return \"http\"\n  return \"text\"\n}\n",
      "type": "registry:lib",
      "target": "@lib/ballmac/highlight.ts"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "syntax highlighting",
      "tokenizer",
      "code",
      "snippet"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Import highlightLines(code, language) from @/lib/ballmac/highlight to get tokens grouped by line, and map each token to tokenClass[token.type]. languageFromName('app.ts') picks a language from a file name or label. Not a full grammar: use Shiki for exact highlighting of large files.",
      "whenToUse": [
        "Showing short request examples, logs and config without shipping a highlighter",
        "Server or client rendering with no async loading"
      ],
      "whenNotToUse": [
        "Editors or long source files that need exact grammar (use Shiki)",
        "Languages outside the list"
      ],
      "composesWith": [],
      "a11y": [],
      "customization": [
        "tokenClass: change the Tailwind classes per token type",
        "language: json | bash | javascript | typescript | python | go | http | env | text"
      ]
    },
    "examples": [],
    "url": "https://ui.ballmac.com/components/highlight"
  },
  "categories": [
    "foundation"
  ],
  "type": "registry:lib"
}