npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@chepchik/json-highlighters

v0.1.4

Published

Три подхода к живой редактируемой подсветке JSON в React: regex, AST-токенизатор с позициями и AST внутри contentEditable.

Readme

@chepchik/json-highlighters

Три самописных подхода к живой, редактируемой подсветке синтаксиса JSON в React. Поддерживает React 18 и 19 (peerDependencies: react ^18.0.0 || ^19.0.0).

npm install @chepchik/json-highlighters

CSS подключается отдельной строкой (один раз, где угодно до рендера компонентов — обычно в точке входа приложения):

import "@chepchik/json-highlighters/styles.css";
import {
  RegexHighlighter,
  AstPositionHighlighter,
  AstContentEditableHighlighter,
} from "@chepchik/json-highlighters";

function App() {
  return (
    <div style={{ display: "flex", gap: 16, height: 400 }}>
      <RegexHighlighter initialJson={'{\n  "a": 1\n}'} onChange={console.log} />
      <AstPositionHighlighter />
      <AstContentEditableHighlighter title={null} theme="dark" />
    </div>
  );
}

Каждый компонент неконтролируемый (хранит текст в собственном состоянии) и принимает четыре опциональных пропа:

| Проп | Тип | Описание | |---|---|---| | initialJson | string | Начальный текст. По умолчанию — маленький пример {"hello": "world"}. | | title | string \| null | Заголовок панели (<h3>). null — не рендерить вовсе. | | theme | "system" \| "light" \| "dark" | По умолчанию "system" — следует prefers-color-scheme страницы-хоста. "light"/"dark" — фиксирует тему явно, независимо от системной. | | onChange | (text: string) => void | Вызывается с текущим текстом на каждое изменение (ввод, Tab, форматирование). |

Каждый компонент занимает 100% высоты и ширины родителя (display: flex; flex-direction: column; height: 100%) — оберните в контейнер с заданной высотой.

Стили и темы

Все стили вынесены в один CSS-файл (src/styles.css → публикуется как dist/styles.css), доступный из пакета через subpath-экспорт:

"exports": {
  ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
  "./styles.css": "./dist/styles.css"
}

Внутри — обычные CSS custom properties (--jh-bg, --jh-key, --jh-string, --jh-border и т.д.), заскоупленные на класс .json-viewer (а не на :root), чтобы не протекать на остальную страницу хоста:

.json-viewer {
  --jh-bg: #1e1e1e;
  --jh-key: #79c0ff;
  /* ... */
}

@media (prefers-color-scheme: light) {
  .json-viewer { /* светлые значения */ }
}

.json-viewer[data-theme="dark"] { /* явные тёмные значения */ }
.json-viewer[data-theme="light"] { /* явные светлые значения */ }

Проп theme компонента просто проставляет (или не проставляет) data-theme на корневой <div>:

  • theme="system" (по умолчанию) — атрибут не ставится, работает @media (prefers-color-scheme: ...), то есть цвета следуют настройке ОС/браузера посетителя.
  • theme="light" / theme="dark" — ставится data-theme, который побеждает медиа-запрос за счёт более высокой специфичности атрибутного селектора (.json-viewer[data-theme="dark"] специфичнее, чем просто .json-viewer внутри @media), поэтому тема остаётся зафиксированной независимо от системной настройки.

Три варианта

Все три работают с одним и тем же типом входных данных — сырым JSON-текстом, как будто он только что пришёл по сети (fetch(url).then(r => r.text())), а не с уже готовым JS-объектом. Каждая панель редактируется независимо.

1. RegexHighlighter

Прогоняет по сырому тексту один regex и оборачивает каждое совпадение (строка, число, true/false, null) в цветной <span>. Ключ объекта отличается от строки-значения хитрым приёмом внутри самого regex: совпадение может «прихватить» с собой хвостовой \s*: — если прихватило, значит это ключ.

Поскольку подход только размечает текст и никогда не пересобирает его, подсвеченный результат всегда посимвольно совпадает по длине и позициям с тем, что находится в редакторе. Это делает живую редактируемую подсветку тривиальной: прозрачная <textarea> лежит поверх цветного <pre> — textarea владеет настоящим курсором и вводом текста, а <pre> снизу просвечивает цветом. Пересчитывается на каждое нажатие клавиши; текст не обязан быть валидным JSON — у regex вообще нет понятия «невалидно».

Компромисс: корректность зависит от паттерна regex; экзотические edge-cases в грамматике токенов теоретически можно классифицировать неверно.

2. AstPositionHighlighter

Самописный рекурсивный парсер (src/lib/jsonTokenizer.ts), который вместо построения дерева JS-значений выдаёт плоский список токенов — каждый со своим реальным смещением start/end в исходной строке. Строка помечается как key не по угадыванию соседней пунктуации, а потому что парсер структурно находится внутри parseObject(), сразу после { или , — по грамматике там может быть только ключ.

Если разбор ломается на середине (например, во время редактирования текст временно невалиден), функция возвращает токены, накопленные до места ошибки, плюс текст ошибки — неразобранный хвост текста рендерится без подсветки, а не пропадает.

Поскольку позиции токенов сохраняются (ничего не переформатируется), здесь работает тот же приём наложения прозрачной textarea на <pre>, что и в regex-варианте — но раскраска теперь основана на грамматике, а не на догадке regex. Живая подсветка, пересчитывается на каждое нажатие (токенизация — это дешёвый линейный проход по строке).

Компромисс: кода больше, чем в regex-варианте (настоящий парсер вместо одного паттерна), хотя токенизатор переиспользуется и в 3-м варианте.

3. AstContentEditableHighlighter

Редактирование происходит прямо внутри раскрашенного элемента contentEditable, без второго наложенного слоя. Это вариант на случай, когда наложение слоёв в принципе не подходит (например, если логика подсветки сама по себе переформатирует текст, как это делает обычный AST-рендерер, пересобирающий значение с нуля) — здесь ничего не зависит от посимвольного совпадения позиций между двумя слоями, потому что слой всего один.

Загвоздка: замена innerHTML у contentEditable-элемента сбрасывает курсор браузера. Поэтому DOM этого компонента не управляется React после первого монтирования — обновления происходят императивно:

  • onInput просто запоминает текущий текст в ref и (пере)запускает таймер дебаунса на 400мс — так быстрый набор текста никогда не прерывается заменой DOM.
  • когда пауза в наборе достигнута, recolor() сохраняет символьную позицию курсора (через Selection/Range), заново токенизирует текст, строит HTML-строку с подсветкой, подменяет innerHTML, а затем возвращает курсор на то же смещение, обходя текстовые узлы нового DOM.
  • Tab вставляет два пробела через document.execCommand("insertText", …), потому что у contentEditable нет selectionStart/selectionEnd, как у <textarea>.

Компромисс: подсветка запаздывает на ~400мс после остановки набора текста (не мгновенно посимвольно, как в вариантах 1 и 2), и учёт курсора требует заметно больше кода — contentEditable в принципе не создан для того, чтобы им управлял фреймворк.

Форматирование (отступы: пробелы/таб)

В каждом компоненте есть тулбар над редактором: выбор стиля отступа (2 пробела / 4 пробела / Таб) и кнопка «Форматировать».

Логика общая для всех трёх, вынесена в src/lib/formatJson.ts:

export type IndentStyle = "2" | "4" | "tab";

export function formatJson(text: string, style: IndentStyle): string {
  const parsed = JSON.parse(text);
  return JSON.stringify(parsed, null, style === "tab" ? "\t" : Number(style));
}

JSON.parse превращает текущий текст в настоящий JS-объект, а JSON.stringify(parsed, null, indentParam) собирает его обратно в текст — третий аргумент stringify и есть «стиль отступа». Если JSON.parse бросает исключение (текст невалиден), formatJson пробрасывает его выше — форматирование принципиально не может работать с битым JSON, в отличие от подсветки, которая у вариантов 1 и 2 может работать и с временно невалидным текстом. Поэтому кнопка «Форматировать» задизейблена, пока текст невалиден (у варианта 1, которому подсветка не требует валидности, ошибка форматирования показывается отдельным сообщением рядом с кнопкой).

Общий код

src/lib/jsonTokenizer.ts — токенизатор с сохранением позиций, используемый AstPositionHighlighter и AstContentEditableHighlighter, чтобы не дублировать логику разбора между ними. Экспортируется и из пакета (tokenizeWithPositions) на случай, если нужен сам разбор без готового UI.

src/lib/formatJson.ts — форматирование текста с выбранным стилем отступа (formatJson, INDENT_OPTIONS), тоже экспортируется отдельно.

Разработка

Это часть монорепозитория; см. example/ в корне репозитория — рабочее демо всех трёх компонентов рядом друг с другом на одной странице.

npm install       # из корня репозитория (npm workspaces)
npm run dev        # в этом пакете — пересборка в watch-режиме