import { useMemo, type ReactNode } from 'react' /** * Лёгкий JSON viewer с token-based syntax highlight + diff-подсветкой * top-level ключей. Никаких внешних зависимостей: regex-проход по * `JSON.stringify(..., null, 2)`. * *

Syntax: ключи (aurora), строки (pink), числа (warn), true/false/null * (accent), пунктуация (mute). * *

Diff (опционально): передай diff — массив { kind, keys }. Строки * JSON где key в added/removed/changed получают bg-цвет (зелёный / * красный / жёлтый). Подсветка — top-level only (для глубокого diff'а * есть DraftDiffSummary). */ export type JsonDiff = { added?: Set | readonly string[] removed?: Set | readonly string[] changed?: Set | readonly string[] } type JsonViewProps = { value: unknown diff?: JsonDiff className?: string } const TOKEN_RE = /("(?:\\.|[^"\\])*")(\s*:)?|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(true|false|null)|([{}[\],])/g const toSet = (v: Set | readonly string[] | undefined): Set => v instanceof Set ? v : new Set(v ?? []) const tokenize = (line: string): ReactNode[] => { const out: ReactNode[] = [] let last = 0 let i = 0 for (const m of line.matchAll(TOKEN_RE)) { const idx = m.index ?? 0 if (idx > last) out.push(line.slice(last, idx)) const [whole, str, colon, num, lit, punct] = m if (str && colon) { out.push( {str} , ) out.push( {colon} , ) } else if (str) { out.push( {str} , ) } else if (num) { out.push( {num} , ) } else if (lit) { out.push( {lit} , ) } else if (punct) { out.push( {punct} , ) } else { out.push(whole) } last = idx + whole.length i++ } if (last < line.length) out.push(line.slice(last)) return out } const LINE_KEY_RE = /^ {2}"((?:\\.|[^"\\])*)":/ const lineDiffClass = ( line: string, added: Set, removed: Set, changed: Set, ): string => { const m = LINE_KEY_RE.exec(line) if (!m) return '' const k = m[1] if (added.has(k)) return 'bg-aurora/15 -mx-2 px-2 border-l-2 border-aurora' if (removed.has(k)) return 'bg-danger/15 -mx-2 px-2 border-l-2 border-danger' if (changed.has(k)) return 'bg-warn/15 -mx-2 px-2 border-l-2 border-warn' return '' } export const JsonView = ({ value, diff, className }: JsonViewProps) => { const added = useMemo(() => toSet(diff?.added), [diff?.added]) const removed = useMemo(() => toSet(diff?.removed), [diff?.removed]) const changed = useMemo(() => toSet(diff?.changed), [diff?.changed]) const lines = useMemo(() => { try { return JSON.stringify(value, null, 2).split('\n') } catch { return [String(value)] } }, [value]) return (

      {lines.map((line, i) => {
        const dc = lineDiffClass(line, added, removed, changed)
        return (
          
{tokenize(line)}
) })}
) }