{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-highlighted-code",
  "title": "Highlighted code",
  "description": "Shiki syntax highlighting for Code Block, one React node per line, with incremental tokenizing while code streams.",
  "dependencies": [
    "shiki@^4.5.0"
  ],
  "files": [
    {
      "path": "registry/familis/ui/hooks/use-highlighted-code.ts",
      "content": "import * as React from \"react\"\n\nimport { loadHighlighter } from \"@/registry/familis/lib/highlighter\"\n\nexport function useHighlightedCode(value: string, language: string) {\n  // A trailing newline would render as an extra empty line.\n  const code = value.endsWith(\"\\n\") ? value.slice(0, -1) : value\n  const [loaded, setLoaded] = React.useState<{\n    language: string\n    highlighter: Awaited<ReturnType<typeof loadHighlighter>>\n  } | null>(null)\n  const highlighter = loaded?.language === language ? loaded.highlighter : null\n\n  React.useEffect(() => {\n    let cancelled = false\n\n    loadHighlighter(language)\n      .then((next) => {\n        if (!cancelled) {\n          setLoaded({ language, highlighter: next })\n        }\n      })\n      .catch(() => {})\n\n    return () => {\n      cancelled = true\n    }\n  }, [language])\n\n  // The complete lines, highlighted. While the code streams, only the new\n  // lines are tokenized, starting from the grammar state of the last one.\n  const [done, setDone] = React.useState<{\n    highlighter: typeof highlighter\n    text: string\n    lines: React.ReactNode[]\n    state?: Parameters<NonNullable<typeof highlighter>[\"highlight\"]>[1]\n  }>({ highlighter: null, text: \"\", lines: [] })\n\n  let next = done\n\n  if (next.highlighter !== highlighter || !code.startsWith(next.text)) {\n    next = { highlighter, text: \"\", lines: [] }\n  }\n\n  const end = code.lastIndexOf(\"\\n\") + 1\n\n  if (highlighter && end > next.text.length) {\n    const result = highlighter.highlight(code.slice(next.text.length, end - 1), next.state)\n\n    next = {\n      highlighter,\n      text: code.slice(0, end),\n      lines: [...next.lines, ...result.lines],\n      state: result.state,\n    }\n  }\n\n  if (next !== done) {\n    setDone(next)\n  }\n\n  // The last line is still being written, so it is tokenized on every render.\n  const tail = code.slice(end)\n\n  return React.useMemo(() => {\n    if (!highlighter) {\n      return code.split(\"\\n\")\n    }\n\n    return [...next.lines, highlighter.highlight(tail, next.state).lines[0]]\n  }, [code, highlighter, next, tail])\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/familis/lib/highlighter.tsx",
      "content": "import * as React from \"react\"\nimport { createHighlighterCore, type GrammarState, type HighlighterCore } from \"shiki/core\"\nimport { createJavaScriptRegexEngine } from \"shiki/engine/javascript\"\n\n// Add the languages you need. Each one loads the first time it is used.\nconst languages = {\n  bash: () => import(\"shiki/langs/shellscript.mjs\"),\n  css: () => import(\"shiki/langs/css.mjs\"),\n  diff: () => import(\"shiki/langs/diff.mjs\"),\n  html: () => import(\"shiki/langs/html.mjs\"),\n  javascript: () => import(\"shiki/langs/javascript.mjs\"),\n  json: () => import(\"shiki/langs/json.mjs\"),\n  jsx: () => import(\"shiki/langs/jsx.mjs\"),\n  markdown: () => import(\"shiki/langs/markdown.mjs\"),\n  python: () => import(\"shiki/langs/python.mjs\"),\n  sql: () => import(\"shiki/langs/sql.mjs\"),\n  tsx: () => import(\"shiki/langs/tsx.mjs\"),\n  typescript: () => import(\"shiki/langs/typescript.mjs\"),\n  yaml: () => import(\"shiki/langs/yaml.mjs\"),\n}\n\nconst aliases: Record<string, keyof typeof languages> = {\n  js: \"javascript\",\n  md: \"markdown\",\n  py: \"python\",\n  sh: \"bash\",\n  shell: \"bash\",\n  shellscript: \"bash\",\n  ts: \"typescript\",\n  yml: \"yaml\",\n  zsh: \"bash\",\n}\n\nlet highlighter: Promise<HighlighterCore> | undefined\n\nfunction getHighlighter() {\n  highlighter ??= createHighlighterCore({\n    themes: [import(\"shiki/themes/github-light.mjs\"), import(\"shiki/themes/github-dark.mjs\")],\n    langs: [],\n    engine: createJavaScriptRegexEngine({ forgiving: true }),\n  })\n\n  return highlighter\n}\n\nfunction getLanguage(language: string) {\n  const name = language.toLowerCase()\n\n  if (name === \"ansi\") {\n    return name\n  }\n\n  if (name in languages) {\n    return name as keyof typeof languages\n  }\n\n  return aliases[name] ?? null\n}\n\n// Loads a language and returns a highlighter for it, or null for a language\n// that is not listed.\nexport async function loadHighlighter(language: string) {\n  const lang = getLanguage(language)\n\n  if (!lang) {\n    return null\n  }\n\n  const shiki = await getHighlighter()\n\n  if (lang !== \"ansi\" && !shiki.getLoadedLanguages().includes(lang)) {\n    await shiki.loadLanguage(languages[lang])\n  }\n\n  return {\n    // Returns one React node per line, and the grammar state after the last\n    // line. Pass that state back to continue where the code left off.\n    highlight(code: string, state?: GrammarState) {\n      const tokenize = (text: string) =>\n        shiki.codeToTokens(text, {\n          lang,\n          themes: { light: \"github-light\", dark: \"github-dark\" },\n          defaultColor: false,\n          grammarState: state,\n        }).tokens\n\n      // ANSI has no grammar state, so each line is tokenized on its own and\n      // gets the same colors whether it arrives in one piece or streams.\n      const tokens =\n        lang === \"ansi\" ? code.split(\"\\n\").flatMap((line) => tokenize(line)) : tokenize(code)\n\n      return {\n        lines: tokens.map((line) =>\n          line.map((token, index) => (\n            <span\n              key={index}\n              style={token.htmlStyle as React.CSSProperties}\n              className=\"text-(--shiki-light) dark:text-(--shiki-dark)\"\n            >\n              {token.content}\n            </span>\n          )),\n        ),\n        state: shiki.getLastGrammarState(tokens),\n      }\n    },\n  }\n}\n\n// Returns one React node per line, or null for a language that is not listed.\nexport async function highlightCode(code: string, language: string) {\n  const highlighter = await loadHighlighter(language)\n\n  return highlighter ? highlighter.highlight(code).lines : null\n}\n",
      "type": "registry:lib"
    }
  ],
  "meta": {
    "storybookId": "ui-hooks-highlighted-code--default"
  },
  "type": "registry:hook"
}