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)}
)
})}
)
}