{
  "name": "syntax",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "syntax.tsx",
      "content": "\"use client\";\r\n\r\nimport { css, insertRawCss, themeVars as theme } from \"@yugnex/core\";\r\nimport { useMemo, useRef, type ReactNode } from \"react\";\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Token model\r\n * ------------------------------------------------------------------ */\r\n\r\nexport type TokenType =\r\n  | \"keyword\"\r\n  | \"string\"\r\n  | \"number\"\r\n  | \"comment\"\r\n  | \"operator\"\r\n  | \"punctuation\"\r\n  | \"function\"\r\n  | \"type\"\r\n  | \"property\"\r\n  | \"constant\"\r\n  | \"regexp\"\r\n  | \"tag\"\r\n  | \"attribute\"\r\n  | \"variable\"\r\n  | \"plain\";\r\n\r\nexport interface Token {\r\n  type: TokenType;\r\n  value: string;\r\n}\r\n\r\nexport type SyntaxLanguage =\r\n  | \"ts\"\r\n  | \"tsx\"\r\n  | \"json\"\r\n  | \"css\"\r\n  | \"sql\"\r\n  | \"md\"\r\n  | \"yaml\"\r\n  | \"dockerfile\"\r\n  | \"env\"\r\n  | \"sh\";\r\n\r\n/**\r\n * Carry state between lines. Anything that can span a line break — block\r\n * comments, template literals, fenced code inside markdown — parks its state\r\n * here so the next line resumes mid-construct instead of restarting clean.\r\n */\r\nexport type ScanState = \"none\" | \"block-comment\" | \"template\" | \"md-fence\";\r\n\r\ninterface Rule {\r\n  type: TokenType;\r\n  /** Must be sticky (`y`) — the scanner anchors every attempt at `lastIndex`. */\r\n  re: RegExp;\r\n  /** Re-type the token when the text immediately after the match starts with `(`. */\r\n  callable?: boolean;\r\n  /** Enter this scan state and consume the rest of the line. */\r\n  enter?: ScanState;\r\n}\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Grammars\r\n *\r\n * Deliberately regex-per-token-class rather than a full parser: these drive\r\n * a *display* surface, and the failure mode of a heuristic lexer (one word\r\n * tinted wrong) costs nothing, while a real parser would be orders of\r\n * magnitude more code and still choke on the half-written, mid-stream input\r\n * this component exists to render.\r\n * ------------------------------------------------------------------ */\r\n\r\nconst TS_KEYWORDS =\r\n  \"as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|new|of|package|private|protected|public|readonly|return|satisfies|set|static|super|switch|this|throw|try|type|typeof|var|void|while|with|yield\";\r\n\r\nconst TS_TYPES = \"any|bigint|boolean|never|null|number|object|string|symbol|undefined|unknown\";\r\nconst TS_CONSTANTS = \"true|false|null|undefined|NaN|Infinity\";\r\n\r\nconst tsRules: Rule[] = [\r\n  { type: \"comment\", re: /\\/\\/[^\\n]*/y },\r\n  { type: \"comment\", re: /\\/\\*/y, enter: \"block-comment\" },\r\n  { type: \"string\", re: /`/y, enter: \"template\" },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?/y },\r\n  { type: \"string\", re: /'(?:[^'\\\\\\n]|\\\\.)*'?/y },\r\n  // Regex literals only after a position where a value can't already have ended\r\n  // — otherwise `a / b / c` lexes as a regex. Handled by the scanner's\r\n  // `prevMeaningful` check rather than the pattern itself.\r\n  { type: \"regexp\", re: /\\/(?![/*])(?:[^/\\\\\\n[]|\\\\.|\\[(?:[^\\]\\\\\\n]|\\\\.)*\\])+\\/[gimsuyd]*/y },\r\n  { type: \"number\", re: /0[xX][0-9a-fA-F_]+n?|0[bB][01_]+n?|0[oO][0-7_]+n?|\\d[\\d_]*(?:\\.[\\d_]*)?(?:[eE][+-]?\\d+)?n?/y },\r\n  { type: \"constant\", re: new RegExp(`(?:${TS_CONSTANTS})\\\\b`, \"y\") },\r\n  { type: \"keyword\", re: new RegExp(`(?:${TS_KEYWORDS})\\\\b`, \"y\") },\r\n  { type: \"type\", re: new RegExp(`(?:${TS_TYPES})\\\\b`, \"y\") },\r\n  { type: \"constant\", re: /[A-Z][A-Z0-9_]{2,}\\b/y },\r\n  { type: \"type\", re: /[A-Z][A-Za-z0-9_$]*\\b/y },\r\n  { type: \"variable\", re: /[A-Za-z_$][A-Za-z0-9_$]*/y, callable: true },\r\n  { type: \"operator\", re: /=>|\\.\\.\\.|\\?\\?=?|\\?\\.|[+\\-*/%=<>!&|^~?:]+/y },\r\n  { type: \"punctuation\", re: /[{}[\\]();,.@#]/y },\r\n];\r\n\r\nconst jsonRules: Rule[] = [\r\n  { type: \"property\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?(?=\\s*:)/y },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?/y },\r\n  { type: \"number\", re: /-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?/y },\r\n  { type: \"constant\", re: /(?:true|false|null)\\b/y },\r\n  { type: \"punctuation\", re: /[{}[\\]:,]/y },\r\n];\r\n\r\nconst cssRules: Rule[] = [\r\n  { type: \"comment\", re: /\\/\\*/y, enter: \"block-comment\" },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?|'(?:[^'\\\\\\n]|\\\\.)*'?/y },\r\n  { type: \"keyword\", re: /@[a-zA-Z-]+/y },\r\n  { type: \"number\", re: /#[0-9a-fA-F]{3,8}\\b/y },\r\n  { type: \"number\", re: /-?\\d*\\.?\\d+(?:px|rem|em|%|vh|vw|vmin|vmax|s|ms|deg|fr|ch|ex|pt|cm|mm|in|pc|turn|rad)?/y },\r\n  { type: \"property\", re: /--[a-zA-Z0-9-]+/y },\r\n  { type: \"property\", re: /[a-zA-Z-]+(?=\\s*:)/y },\r\n  { type: \"function\", re: /[a-zA-Z-]+(?=\\()/y },\r\n  { type: \"tag\", re: /[.#]?[a-zA-Z][a-zA-Z0-9_-]*/y },\r\n  { type: \"punctuation\", re: /[{}();:,]/y },\r\n  { type: \"operator\", re: /[>+~*=]/y },\r\n];\r\n\r\nconst SQL_KEYWORDS =\r\n  \"add|all|alter|and|any|as|asc|begin|between|by|case|cast|check|column|commit|constraint|create|cross|database|default|delete|desc|distinct|drop|else|end|exists|foreign|from|full|group|having|if|in|index|inner|insert|into|is|join|key|left|like|limit|not|null|offset|on|or|order|outer|primary|references|returning|right|rollback|select|set|table|then|transaction|union|unique|update|values|view|when|where|with\";\r\n\r\nconst sqlRules: Rule[] = [\r\n  { type: \"comment\", re: /--[^\\n]*/y },\r\n  { type: \"comment\", re: /\\/\\*/y, enter: \"block-comment\" },\r\n  { type: \"string\", re: /'(?:[^'\\\\\\n]|\\\\.|'')*'?/y },\r\n  { type: \"property\", re: /\"(?:[^\"\\n]|\"\")*\"?|`[^`\\n]*`?/y },\r\n  { type: \"number\", re: /\\d+(?:\\.\\d+)?/y },\r\n  { type: \"keyword\", re: new RegExp(`(?:${SQL_KEYWORDS})\\\\b`, \"iy\") },\r\n  { type: \"function\", re: /[a-zA-Z_][a-zA-Z0-9_]*(?=\\()/y },\r\n  { type: \"variable\", re: /[a-zA-Z_][a-zA-Z0-9_$]*/y },\r\n  { type: \"operator\", re: /<>|!=|>=|<=|[=<>+\\-*/%|]/y },\r\n  { type: \"punctuation\", re: /[();,.]/y },\r\n];\r\n\r\nconst yamlRules: Rule[] = [\r\n  { type: \"comment\", re: /#[^\\n]*/y },\r\n  { type: \"punctuation\", re: /^\\s*-\\s/y },\r\n  { type: \"property\", re: /[A-Za-z_][\\w.-]*(?=\\s*:)/y },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?|'(?:[^'\\n]|'')*'?/y },\r\n  { type: \"constant\", re: /\\b(?:true|false|null|yes|no|on|off|~)\\b/iy },\r\n  { type: \"number\", re: /-?\\d+(?:\\.\\d+)?\\b/y },\r\n  { type: \"keyword\", re: /[&*][\\w-]+|<</y },\r\n  { type: \"punctuation\", re: /[:{}[\\],|>]/y },\r\n];\r\n\r\nconst DOCKER_INSTRUCTIONS =\r\n  \"ADD|ARG|CMD|COPY|ENTRYPOINT|ENV|EXPOSE|FROM|HEALTHCHECK|LABEL|MAINTAINER|ONBUILD|RUN|SHELL|STOPSIGNAL|USER|VOLUME|WORKDIR\";\r\n\r\nconst dockerfileRules: Rule[] = [\r\n  { type: \"comment\", re: /#[^\\n]*/y },\r\n  { type: \"keyword\", re: new RegExp(`^\\\\s*(?:${DOCKER_INSTRUCTIONS})\\\\b`, \"iy\") },\r\n  { type: \"constant\", re: /\\bAS\\b/iy },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?|'(?:[^'\\\\\\n]|\\\\.)*'?/y },\r\n  { type: \"variable\", re: /\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?/y },\r\n  { type: \"number\", re: /\\b\\d+(?:\\.\\d+)*\\b/y },\r\n  { type: \"operator\", re: /[=:@\\\\]/y },\r\n  { type: \"punctuation\", re: /[[\\],]/y },\r\n];\r\n\r\nconst envRules: Rule[] = [\r\n  { type: \"comment\", re: /#[^\\n]*/y },\r\n  { type: \"keyword\", re: /^\\s*export\\b/y },\r\n  { type: \"property\", re: /^\\s*[A-Za-z_][A-Za-z0-9_]*(?=\\s*=)/y },\r\n  { type: \"operator\", re: /=/y },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?|'(?:[^'\\n])*'?/y },\r\n  { type: \"variable\", re: /\\$\\{?[A-Za-z_][A-Za-z0-9_]*\\}?/y },\r\n];\r\n\r\nconst SH_KEYWORDS =\r\n  \"if|then|else|elif|fi|for|while|until|do|done|case|esac|function|return|in|select|time|coproc|break|continue|local|export|readonly|declare|typeset|unset|shift|source|alias|trap|set\";\r\n\r\nconst SH_BUILTINS =\r\n  \"echo|printf|read|cd|pwd|ls|cat|grep|sed|awk|cut|sort|uniq|head|tail|wc|find|xargs|chmod|chown|mkdir|rm|cp|mv|touch|test|kill|ps|curl|wget|git|npm|pnpm|yarn|node|docker|make\";\r\n\r\nconst shRules: Rule[] = [\r\n  { type: \"comment\", re: /#[^\\n]*/y },\r\n  { type: \"string\", re: /\"(?:[^\"\\\\\\n]|\\\\.)*\"?/y },\r\n  { type: \"string\", re: /'[^'\\n]*'?/y },\r\n  { type: \"variable\", re: /\\$\\{[^}\\n]*\\}?|\\$[A-Za-z_][A-Za-z0-9_]*|\\$[?@#*!$0-9]/y },\r\n  { type: \"keyword\", re: new RegExp(`(?:${SH_KEYWORDS})\\\\b`, \"y\") },\r\n  { type: \"function\", re: new RegExp(`(?:${SH_BUILTINS})\\\\b`, \"y\") },\r\n  { type: \"attribute\", re: /(?:^|\\s)--?[A-Za-z][\\w-]*/y },\r\n  { type: \"number\", re: /\\b\\d+\\b/y },\r\n  { type: \"operator\", re: /&&|\\|\\||[|<>&;=!]/y },\r\n  { type: \"punctuation\", re: /[(){}[\\]]/y },\r\n  { type: \"plain\", re: /[A-Za-z_./-][\\w./-]*/y },\r\n];\r\n\r\n/** JSX/TSX adds tag + attribute classes on top of the TypeScript rules. */\r\nconst tsxRules: Rule[] = [\r\n  { type: \"comment\", re: /\\/\\/[^\\n]*/y },\r\n  { type: \"comment\", re: /\\/\\*/y, enter: \"block-comment\" },\r\n  { type: \"string\", re: /`/y, enter: \"template\" },\r\n  { type: \"tag\", re: /<\\/?[A-Za-z][\\w.]*|\\/?>/y },\r\n  ...tsRules.slice(3),\r\n];\r\n\r\nconst GRAMMARS: Record<Exclude<SyntaxLanguage, \"md\">, Rule[]> = {\r\n  ts: tsRules,\r\n  tsx: tsxRules,\r\n  json: jsonRules,\r\n  css: cssRules,\r\n  sql: sqlRules,\r\n  yaml: yamlRules,\r\n  dockerfile: dockerfileRules,\r\n  env: envRules,\r\n  sh: shRules,\r\n};\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Scanner\r\n * ------------------------------------------------------------------ */\r\n\r\n/** Tokens after which a `/` starts a regex literal rather than division. */\r\nconst REGEX_PRECEDING = new Set<TokenType>([\"operator\", \"punctuation\", \"keyword\"]);\r\n\r\n/**\r\n * `from` is where the emitted token starts; `scanFrom` is where the search for\r\n * the terminator starts. They differ when the delimiter is being *opened* on\r\n * this line — the opener itself must not be mistaken for the closer.\r\n *\r\n * Without the split, a slash-star-slash line reads as a complete comment (the\r\n * opener's own slash gets matched as the terminator's), and a backtick opening\r\n * a template literal immediately closes itself.\r\n */\r\nfunction closeBlockComment(line: string, from: number, scanFrom: number): { token: Token; next: number; state: ScanState } {\r\n  const end = line.indexOf(\"*/\", scanFrom);\r\n  if (end === -1) return { token: { type: \"comment\", value: line.slice(from) }, next: line.length, state: \"block-comment\" };\r\n  return { token: { type: \"comment\", value: line.slice(from, end + 2) }, next: end + 2, state: \"none\" };\r\n}\r\n\r\n/** Walks a template literal to its closing backtick, respecting escapes. */\r\nfunction closeTemplate(line: string, from: number, scanFrom: number): { token: Token; next: number; state: ScanState } {\r\n  let i = scanFrom;\r\n  while (i < line.length) {\r\n    const ch = line[i];\r\n    if (ch === \"\\\\\") {\r\n      i += 2;\r\n      continue;\r\n    }\r\n    if (ch === \"`\") return { token: { type: \"string\", value: line.slice(from, i + 1) }, next: i + 1, state: \"none\" };\r\n    i += 1;\r\n  }\r\n  return { token: { type: \"string\", value: line.slice(from) }, next: line.length, state: \"template\" };\r\n}\r\n\r\nexport interface LineTokens {\r\n  tokens: Token[];\r\n  /** Scan state to feed into the next line. */\r\n  endState: ScanState;\r\n}\r\n\r\n/**\r\n * Tokenizes exactly one line, resuming from `startState`.\r\n *\r\n * Line-at-a-time is what makes the whole thing incremental: a streamed file\r\n * only ever invalidates the line currently being appended to (and any\r\n * following lines *if* its end state changed), so re-highlighting a 2000-line\r\n * file as its last line grows costs one line of work, not 2000.\r\n */\r\nexport function tokenizeLine(line: string, language: SyntaxLanguage, startState: ScanState = \"none\"): LineTokens {\r\n  if (language === \"md\") return tokenizeMarkdownLine(line, startState);\r\n\r\n  const rules = GRAMMARS[language];\r\n  const tokens: Token[] = [];\r\n  let index = 0;\r\n  let state = startState;\r\n\r\n  // Resuming: the delimiter was opened on an earlier line, so token start and\r\n  // scan start are both 0 — there is no opener on this line to skip past.\r\n  if (state === \"block-comment\") {\r\n    const r = closeBlockComment(line, 0, 0);\r\n    tokens.push(r.token);\r\n    index = r.next;\r\n    state = r.state;\r\n  } else if (state === \"template\") {\r\n    const r = closeTemplate(line, 0, 0);\r\n    tokens.push(r.token);\r\n    index = r.next;\r\n    state = r.state;\r\n  }\r\n\r\n  let lastMeaningful: TokenType | null = null;\r\n\r\n  while (index < line.length) {\r\n    const ws = /\\s+/y;\r\n    ws.lastIndex = index;\r\n    const wsMatch = ws.exec(line);\r\n    if (wsMatch) {\r\n      tokens.push({ type: \"plain\", value: wsMatch[0] });\r\n      index = ws.lastIndex;\r\n      continue;\r\n    }\r\n\r\n    let matched = false;\r\n\r\n    for (const rule of rules) {\r\n      // Division vs. regex is genuinely ambiguous without a parser; use the\r\n      // preceding meaningful token as the tiebreak, which is what every\r\n      // regex-based JS lexer settles on.\r\n      if (rule.type === \"regexp\" && lastMeaningful !== null && !REGEX_PRECEDING.has(lastMeaningful)) continue;\r\n\r\n      rule.re.lastIndex = index;\r\n      const match = rule.re.exec(line);\r\n      if (!match || match.index !== index || match[0].length === 0) continue;\r\n\r\n      // Opening on this line: skip the opener before looking for the closer.\r\n      if (rule.enter === \"block-comment\") {\r\n        const r = closeBlockComment(line, index, index + 2);\r\n        tokens.push(r.token);\r\n        index = r.next;\r\n        state = r.state;\r\n        lastMeaningful = \"comment\";\r\n        matched = true;\r\n        break;\r\n      }\r\n\r\n      if (rule.enter === \"template\") {\r\n        const r = closeTemplate(line, index, index + 1);\r\n        tokens.push(r.token);\r\n        index = r.next;\r\n        state = r.state;\r\n        lastMeaningful = \"string\";\r\n        matched = true;\r\n        break;\r\n      }\r\n\r\n      let type = rule.type;\r\n      if (rule.callable && line[index + match[0].length] === \"(\") type = \"function\";\r\n\r\n      tokens.push({ type, value: match[0] });\r\n      index += match[0].length;\r\n      lastMeaningful = type;\r\n      matched = true;\r\n      break;\r\n    }\r\n\r\n    if (!matched) {\r\n      // Unknown character: emit it as plain and advance, so a grammar gap can\r\n      // never wedge the scanner in an infinite loop.\r\n      tokens.push({ type: \"plain\", value: line[index] as string });\r\n      index += 1;\r\n      lastMeaningful = \"plain\";\r\n    }\r\n  }\r\n\r\n  return { tokens, endState: state };\r\n}\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Markdown (its own scanner — line-oriented, not token-oriented)\r\n * ------------------------------------------------------------------ */\r\n\r\nfunction tokenizeMarkdownLine(line: string, startState: ScanState): LineTokens {\r\n  if (startState === \"md-fence\") {\r\n    if (/^\\s*```/.test(line)) return { tokens: [{ type: \"keyword\", value: line }], endState: \"none\" };\r\n    return { tokens: [{ type: \"plain\", value: line }], endState: \"md-fence\" };\r\n  }\r\n\r\n  if (/^\\s*```/.test(line)) return { tokens: [{ type: \"keyword\", value: line }], endState: \"md-fence\" };\r\n  if (/^\\s{0,3}#{1,6}\\s/.test(line)) return { tokens: [{ type: \"keyword\", value: line }], endState: \"none\" };\r\n  if (/^\\s{0,3}>/.test(line)) return { tokens: [{ type: \"comment\", value: line }], endState: \"none\" };\r\n  if (/^\\s{0,3}(?:[-*_]\\s*){3,}$/.test(line)) return { tokens: [{ type: \"punctuation\", value: line }], endState: \"none\" };\r\n\r\n  const tokens: Token[] = [];\r\n  let rest = line;\r\n\r\n  const bullet = /^(\\s*)([-*+]|\\d+\\.)(\\s+)/.exec(rest);\r\n  if (bullet) {\r\n    tokens.push({ type: \"plain\", value: bullet[1] as string });\r\n    tokens.push({ type: \"punctuation\", value: bullet[2] as string });\r\n    tokens.push({ type: \"plain\", value: bullet[3] as string });\r\n    rest = rest.slice(bullet[0].length);\r\n  }\r\n\r\n  const inline = /(`[^`]+`)|(\\*\\*[^*]+\\*\\*|__[^_]+__)|(\\*[^*]+\\*|_[^_]+_)|(\\[[^\\]]*\\]\\([^)]*\\))/g;\r\n  let cursor = 0;\r\n  let m: RegExpExecArray | null;\r\n\r\n  while ((m = inline.exec(rest)) !== null) {\r\n    if (m.index > cursor) tokens.push({ type: \"plain\", value: rest.slice(cursor, m.index) });\r\n    if (m[1]) tokens.push({ type: \"string\", value: m[1] });\r\n    else if (m[2]) tokens.push({ type: \"constant\", value: m[2] });\r\n    else if (m[3]) tokens.push({ type: \"type\", value: m[3] });\r\n    else if (m[4]) tokens.push({ type: \"function\", value: m[4] });\r\n    cursor = m.index + m[0].length;\r\n  }\r\n\r\n  if (cursor < rest.length) tokens.push({ type: \"plain\", value: rest.slice(cursor) });\r\n\r\n  return { tokens, endState: \"none\" };\r\n}\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Incremental document tokenizer\r\n * ------------------------------------------------------------------ */\r\n\r\ninterface CachedLine {\r\n  text: string;\r\n  inState: ScanState;\r\n  result: LineTokens;\r\n}\r\n\r\n/**\r\n * Tokenizes a whole document, reusing per-line results across calls.\r\n *\r\n * Keep one instance per file (see `useSyntax`). Re-tokenizing after an append\r\n * touches only the lines whose text *or* incoming scan state changed — the\r\n * common streaming case where the last line grows re-scans exactly one line.\r\n */\r\nexport class IncrementalTokenizer {\r\n  private cache: CachedLine[] = [];\r\n\r\n  constructor(private language: SyntaxLanguage) {}\r\n\r\n  setLanguage(language: SyntaxLanguage): void {\r\n    if (language === this.language) return;\r\n    this.language = language;\r\n    this.cache = [];\r\n  }\r\n\r\n  tokenize(source: string): Token[][] {\r\n    const lines = source.split(\"\\n\");\r\n    const out: Token[][] = [];\r\n    let state: ScanState = \"none\";\r\n\r\n    for (let i = 0; i < lines.length; i++) {\r\n      const text = lines[i] as string;\r\n      const cached = this.cache[i];\r\n\r\n      if (cached && cached.text === text && cached.inState === state) {\r\n        out.push(cached.result.tokens);\r\n        state = cached.result.endState;\r\n        continue;\r\n      }\r\n\r\n      const result = tokenizeLine(text, this.language, state);\r\n      this.cache[i] = { text, inState: state, result };\r\n      out.push(result.tokens);\r\n      state = result.endState;\r\n    }\r\n\r\n    if (this.cache.length > lines.length) this.cache.length = lines.length;\r\n    return out;\r\n  }\r\n}\r\n\r\n/** Guesses a grammar from a filename. Falls back to `ts` for unknown code-ish files. */\r\nexport function languageFromFilename(filename: string): SyntaxLanguage {\r\n  const name = filename.split(\"/\").pop() ?? filename;\r\n  const lower = name.toLowerCase();\r\n\r\n  if (lower === \"dockerfile\" || lower.startsWith(\"dockerfile.\")) return \"dockerfile\";\r\n  if (lower === \".env\" || lower.startsWith(\".env.\")) return \"env\";\r\n\r\n  const ext = lower.includes(\".\") ? (lower.split(\".\").pop() as string) : \"\";\r\n  switch (ext) {\r\n    case \"ts\":\r\n    case \"mts\":\r\n    case \"cts\":\r\n    case \"js\":\r\n    case \"mjs\":\r\n    case \"cjs\":\r\n      return \"ts\";\r\n    case \"tsx\":\r\n    case \"jsx\":\r\n      return \"tsx\";\r\n    case \"json\":\r\n    case \"jsonc\":\r\n      return \"json\";\r\n    case \"css\":\r\n    case \"scss\":\r\n    case \"less\":\r\n      return \"css\";\r\n    case \"sql\":\r\n      return \"sql\";\r\n    case \"md\":\r\n    case \"mdx\":\r\n    case \"markdown\":\r\n      return \"md\";\r\n    case \"yaml\":\r\n    case \"yml\":\r\n      return \"yaml\";\r\n    case \"sh\":\r\n    case \"bash\":\r\n    case \"zsh\":\r\n      return \"sh\";\r\n    default:\r\n      return \"ts\";\r\n  }\r\n}\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Palette\r\n * ------------------------------------------------------------------ */\r\n\r\nconst SYNTAX_CSS = `\r\n:root{\r\n--nx-syn-keyword:#8b5cf6;--nx-syn-string:#0f8a54;--nx-syn-number:#b4500a;\r\n--nx-syn-comment:#8b8b96;--nx-syn-operator:#5b5b66;--nx-syn-punct:#8b8b96;\r\n--nx-syn-function:#2563c9;--nx-syn-type:#0e7490;--nx-syn-property:#b0221d;\r\n--nx-syn-constant:#a21caf;--nx-syn-regexp:#0f8a54;--nx-syn-tag:#b0221d;\r\n--nx-syn-attribute:#b4500a;--nx-syn-variable:inherit;\r\n}\r\n[data-theme=\"dark\"]{\r\n--nx-syn-keyword:#c4b5fd;--nx-syn-string:#78e5a8;--nx-syn-number:#fbb324;\r\n--nx-syn-comment:#7b7b88;--nx-syn-operator:#b8b8c4;--nx-syn-punct:#8b8b99;\r\n--nx-syn-function:#8ab4f8;--nx-syn-type:#5ed3e8;--nx-syn-property:#f7a6a4;\r\n--nx-syn-constant:#f0abfc;--nx-syn-regexp:#78e5a8;--nx-syn-tag:#f7a6a4;\r\n--nx-syn-attribute:#fccc4d;--nx-syn-variable:inherit;\r\n}`;\r\n\r\nlet paletteInserted = false;\r\n\r\nfunction ensurePalette(): void {\r\n  if (paletteInserted) return;\r\n  insertRawCss(\"base\", \"nx-syntax-palette\", SYNTAX_CSS);\r\n  paletteInserted = true;\r\n}\r\n\r\nconst TOKEN_COLOR: Record<TokenType, string> = {\r\n  keyword: \"var(--nx-syn-keyword)\",\r\n  string: \"var(--nx-syn-string)\",\r\n  number: \"var(--nx-syn-number)\",\r\n  comment: \"var(--nx-syn-comment)\",\r\n  operator: \"var(--nx-syn-operator)\",\r\n  punctuation: \"var(--nx-syn-punct)\",\r\n  function: \"var(--nx-syn-function)\",\r\n  type: \"var(--nx-syn-type)\",\r\n  property: \"var(--nx-syn-property)\",\r\n  constant: \"var(--nx-syn-constant)\",\r\n  regexp: \"var(--nx-syn-regexp)\",\r\n  tag: \"var(--nx-syn-tag)\",\r\n  attribute: \"var(--nx-syn-attribute)\",\r\n  variable: \"var(--nx-syn-variable)\",\r\n  plain: \"inherit\",\r\n};\r\n\r\nconst commentClass = css({ fontStyle: \"italic\" });\r\n\r\n/** Renders one already-tokenized line. Exported so diff-view can reuse it per side. */\r\nexport function TokenLine({ tokens }: { tokens: Token[] }): ReactNode {\r\n  ensurePalette();\r\n  return (\r\n    <>\r\n      {tokens.map((token, i) => {\r\n        if (token.type === \"plain\") return <span key={i}>{token.value}</span>;\r\n        return (\r\n          <span\r\n            key={i}\r\n            style={{ color: TOKEN_COLOR[token.type] }}\r\n            className={token.type === \"comment\" ? commentClass : undefined}\r\n          >\r\n            {token.value}\r\n          </span>\r\n        );\r\n      })}\r\n    </>\r\n  );\r\n}\r\n\r\n/* ------------------------------------------------------------------ *\r\n * Hook + component\r\n * ------------------------------------------------------------------ */\r\n\r\n/** Tokenizes `code`, reusing one tokenizer instance so streaming stays incremental. */\r\nexport function useSyntax(code: string, language: SyntaxLanguage): Token[][] {\r\n  const ref = useRef<IncrementalTokenizer | null>(null);\r\n  if (ref.current === null) ref.current = new IncrementalTokenizer(language);\r\n  const tokenizer = ref.current;\r\n  tokenizer.setLanguage(language);\r\n  return useMemo(() => tokenizer.tokenize(code), [code, language, tokenizer]);\r\n}\r\n\r\nconst rootClass = css({\r\n  fontFamily: theme.fontFamily.mono,\r\n  fontSize: theme.fontSize.xs,\r\n  lineHeight: 1.65,\r\n  color: theme.color.foreground,\r\n  overflowX: \"auto\",\r\n  tabSize: 2,\r\n});\r\n\r\nconst lineClass = css({ display: \"flex\", whiteSpace: \"pre\" });\r\n\r\nconst gutterClass = css({\r\n  flexShrink: 0,\r\n  userSelect: \"none\",\r\n  textAlign: \"right\",\r\n  paddingRight: theme.space[3],\r\n  color: theme.color.mutedForeground,\r\n  fontVariantNumeric: \"tabular-nums\",\r\n  position: \"sticky\",\r\n  left: 0,\r\n  backgroundColor: \"inherit\",\r\n});\r\n\r\nexport interface SyntaxProps {\r\n  code: string;\r\n  language?: SyntaxLanguage;\r\n  /** Used to infer the language when `language` is omitted. */\r\n  filename?: string;\r\n  showLineNumbers?: boolean;\r\n  /** 1-based line numbers to tint as active — e.g. the line being written. */\r\n  highlightLines?: number[];\r\n  className?: string;\r\n}\r\n\r\n/**\r\n * Syntax-highlighted code, tokenized incrementally so it stays cheap while the\r\n * source is still streaming in.\r\n */\r\nexport function Syntax({\r\n  code,\r\n  language,\r\n  filename,\r\n  showLineNumbers = false,\r\n  highlightLines,\r\n  className,\r\n}: SyntaxProps) {\r\n  const resolved = language ?? (filename ? languageFromFilename(filename) : \"ts\");\r\n  const lines = useSyntax(code, resolved);\r\n  const highlighted = useMemo(() => new Set(highlightLines ?? []), [highlightLines]);\r\n  const width = `${String(lines.length).length}ch`;\r\n\r\n  return (\r\n    <pre className={className ? `${rootClass} ${className}` : rootClass}>\r\n      <code>\r\n        {lines.map((tokens, i) => (\r\n          <div\r\n            key={i}\r\n            className={lineClass}\r\n            style={highlighted.has(i + 1) ? { backgroundColor: theme.color.accent } : undefined}\r\n          >\r\n            {showLineNumbers ? (\r\n              <span className={gutterClass} style={{ width }}>\r\n                {i + 1}\r\n              </span>\r\n            ) : null}\r\n            <span>\r\n              <TokenLine tokens={tokens} />\r\n            </span>\r\n          </div>\r\n        ))}\r\n      </code>\r\n    </pre>\r\n  );\r\n}\r\n",
      "type": "registry:component"
    }
  ]
}