12k
All articles

Первый взгляд на Wordgard — новый текстовый редактор

Wordgard — новая библиотека rich-text редактора Марийна Хавербеке: одинарные изменения, corrections, facets и собственное выделение.

OpenReplay Team
OpenReplay Team
Первый взгляд на Wordgard — новый текстовый редактор

Wordgard — это JavaScript-библиотека Марейна Хавербеке (Marijn Haverbeke), автора ProseMirror и CodeMirror, предназначенная для создания редакторов форматированного текста, документы которых соответствуют схеме. В комплекте идёт UI-компонент редактора, но это не универсальный WYSIWYG-редактор произвольной формы и не HTML-редактор.

Поддержка интеграции с ProseMirror нередко означает проброс позиций через список шагов или написание «универсальной» команды, которой на каждом шагу приходится проверять content expressions. Wordgard — это ответ того же автора на подобные претензии, написанный с нуля, а не пристроенный поверх ProseMirror.

В статье разбирается, что именно меняет библиотека: модель изменений, отказ от ограничений на содержимое, систему расширений на основе фасетов и работу с выделением внутри самой библиотеки, а также то, какое место первый релиз этого автора занимает рядом с ProseMirror, TipTap и Lexical.

Ключевые выводы

  • Wordgard впервые вышел как версия 0.1.0 2 июля 2026 года под лицензией MIT и устанавливается из npm как wordgard; на момент релиза автор заявил, что проект останется на версиях 0.x, вероятно, как минимум год.
  • Транзакция в Wordgard несёт ровно одно изменение, составленное из секций, которые сохраняют диапазон токенов, заменяют его либо добавляют или удаляют на нём метки (marks), так что затронутый диапазон можно прочитать напрямую, а не восстанавливать по списку шагов.
  • Схемы Wordgard могут ограничивать, какие типы узлов допустимы внутри родителя, но не их порядок; за инварианты вроде прямоугольности таблиц отвечают коррекции (corrections) — функции-наблюдатели, возвращающие спецификации исправляющих изменений.
  • Конфигурация представляет собой дерево расширений с приоритетом на уровне отдельного значения и определяемыми пользователем фасетами — этот подход заимствован из CodeMirror 6.
  • Wordgard обрабатывает выделение клавиатурой и указателем внутри библиотеки и рисует собственный курсор; сенсорное выделение оставлено браузеру.

Что такое Wordgard?

Wordgard — это система редактирования форматированного текста для содержимого, укладывающегося в конкретную схему, а не готовый WYSIWYG-компонент и не приложение. Согласно System Guide, поверхность редактирования должна ощущаться как WYSIWYG, но содержимое и действия редактирования именуются по смыслу (заголовки, списки, выделение), а не по внешнему виду (гарнитура, отступ абзаца, полужирное начертание). Главный экспорт библиотеки — UI-класс Wordgard. Под ним располагаются типы для документов, состояния редактора и действий редактирования, и большинство из них работают вообще без браузера.

В анонсе версии 0.1 от 2 июля 2026 года указаны лицензия MIT, имя npm-пакета wordgard и то, что исходники размещены на собственном инстансе Forgejo автора. Домашняя страница проекта подтверждает лицензию и добавляет, что сообщения об ошибках приветствуются, а pull request’ы не принимаются. Там же перечислены возможности: документы на основе схем, модульные расширения, двунаправленный текст, структурированное содержимое вроде таблиц и вложенных списков, а также совместное редактирование — воспринимайте эти пункты как заявления самого проекта.

Как настроить редактор Wordgard?

Минимальный редактор Wordgard — это один вызов Wordgard.create с документом, конфигурацией и родительским элементом. Вот пример настройки из руководства:

import {Wordgard, menuBar} from "wordgard/editor"
import {fullSchema} from "wordgard/schema"
import {history} from "wordgard/history"

let editor = Wordgard.create({
  doc: `<p>Starting content</p>`,
  config: [
    fullSchema(), // A predefined document schema
    history(),    // Enable the undo history
    menuBar()     // Show a menu
  ],
  parent: document.body
})

Массив config — это дерево расширений, и каждая из трёх записей представляет собой набор расширений, а не объект схемы, плагин и виджет. fullSchema() подтягивает весь набор элементов схемы из wordgard/schema, причём в его собственной документации предупреждается, что набор может пополняться по мере развития библиотеки; в последующих примерах руководства используется basicSchema(), объединяющая блочный документ, абзацы, заголовки, переводы строк и метки strong, emphasis и link. Строка doc разбирается как HTML в соответствии с этой схемой. Пакет разделён на модули: wordgard/doc, wordgard/state, wordgard/editor, wordgard/command, wordgard/history, wordgard/schema и wordgard/types, и руководство рекомендует TypeScript из-за того, насколько плотно эти части связаны между собой.

Чем модель изменений Wordgard отличается от модели ProseMirror?

В Wordgard транзакция несёт ровно один объект изменения, составленный из секций, которые сохраняют участок документа, заменяют его либо добавляют или удаляют на нём метки, поэтому диапазон, затронутый правкой, можно прочитать напрямую, а не восстанавливать по списку шагов. В ProseMirror транзакция — это упорядоченный список атомарных шагов, каждый из которых работает с документом, полученным на предыдущем шаге, что вынуждает выполнять арифметику позиций и разбор диапазонов, проходя по всей цепочке.

Обоснование в анонсе таково: формат дельт из CodeMirror, сам восходящий к ShareJS, одновременно проще и функциональнее. Изменение — это плоская последовательность над старым документом. Возьмём документ длиной десять токенов: добавление одного токена в позицию 4 выражается как «сохранить 4, заменить 0 на токен, сохранить 6», а выделение позиций с 3 по 6 полужирным — как «сохранить 3, обновить 3, добавив метку, сохранить 4». Секция обновления меток — это расширение модели CodeMirror, добавленное в Wordgard.

С деревом это работает потому, что позиции считаются в токенах. В описанной в руководстве системе индексов каждое открытие plot, закрытие plot, нетекстовый лист и UTF-16-символ увеличивают позицию на единицу, позиция 0 находится непосредственно перед первым потомком, а собственные токены открытия и закрытия узла документа не учитываются. Это позволяет изменению вставлять новые последовательности токенов в документ так, будто он плоский, при этом задача проверки того, что результат по-прежнему является корректным деревом, ложится на код создания изменений.

Когда в ChangeSet.create передаётся сразу несколько изменений, все позиции интерпретируются относительно исходного документа, а смещения библиотека вычисляет автоматически. Изменение меток вообще не затрагивает содержимое:

let makeStrong = ChangeSet.create(doc, {
  from: 1, to: 5,
  add: Strong
})

Те же объекты поддерживают трансформацию изменений друг относительно друга — именно на этом строятся история отмен и совместное редактирование.

Что пришло на смену content expressions из ProseMirror?

Схемы Wordgard могут ограничивать, какие типы узлов допустимы внутри родителя и может ли блочный plot быть пустым, но не порядок следования потомков; у content expressions на основе регулярных выражений из ProseMirror здесь нет аналога. В анонсе приводятся две причины: универсальный код манипуляции документом невозможно написать под произвольные ограничения порядка, не проверяя каждую операцию, а жёсткие ограничения блокируют промежуточные «грязные» состояния, через которые проходит реальное редактирование.

Правила, которые схема выразить не может, обрабатываются коррекциями. Коррекция — это наблюдатель, привязанный к запросу по узлам; он срабатывает всякий раз, когда подходящий узел изменяется или появляется, и может вернуть спецификацию изменения, которую библиотека добавит в транзакцию. Поскольку коррекция — это код, она может учитывать, что пользователь находится в процессе какого-то действия, вместо того чтобы механически отвергать получившуюся структуру. В примере из руководства Correction.onChildList(Doc, ...) вставляет заголовок первого уровня, если документ не начинается с него; в анонсе прямоугольные таблицы названы тем случаем, который content expressions из ProseMirror выразить в принципе не могли.

Почему Wordgard использует фасеты вместо плагинов?

Wordgard заменяет плагин ProseMirror как единицу конфигурации и приоритета деревом мелкогранулярных значений-расширений, каждое из которых может иметь собственный приоритет. Претензия в анонсе сформулирована точно: плагин ProseMirror объединяет несколько хуков под одной позицией приоритета, поэтому плагин, которому для одного хука нужен высокий приоритет, а для другого — низкий, не может получить и то, и другое.

В разделе о конфигурации руководства расширением называется одно из трёх: значение одного из встроенных типов расширений библиотеки, любой объект с расширением в поле extension или массив, содержащий то же самое. Явный приоритет задаётся функциями из GardState.prec; внутри одного уровня порядок определяется положением в дереве. Фасеты — это типизированные точки расширения, которые может определить любой код, с необязательной функцией combine для сведения входных значений к одному выходному, а компартменты позволяют заменять части конфигурации, не теряя состояние. Слово «плагин» никуда не исчезло: Wordgard.Plugin.define по-прежнему на месте — для объектов, которые хранят собственное состояние и должны находиться близко к DOM; именно так устроены поставляемые с библиотекой тултипы и панели.

Выделение, отрисованное библиотекой

Wordgard обрабатывает выделение клавиатурой и указателем внутри библиотеки и скрывает нативную каретку, чтобы рисовать собственный курсор, при этом нативная подсветка выделения остаётся видимой. В анонсе это объясняется ненадёжным поведением браузеров: курсор, который отказывается двигаться дальше определённого содержимого, оказывается не в том месте или вообще не отрисовывается, а также сбои при выделении мышью с перетаскиванием. Поэтому библиотека строит собственное представление о раскладке содержимого, самостоятельно обрабатывает двунаправленный текст и сама размещает курсор. В примере DOM-структуры из руководства показан отдельный элемент-слой курсора, наложенный поверх содержимого, а в документе о миграции говорится, что нативная подсветка сохраняется, поскольку так возникает меньше проблем.

На момент анонса 0.1 сенсорное выделение было единственным исключением и оставалось нативным, так как его переизобретение ломает системное контекстное меню. С тех пор ситуация изменилась: в changelog отмечено сенсорное выделение в 0.5.0 для позиций, недостижимых нативным выделением, а в 0.5.1 — дополнительная позиция курсора на краях inline-plot’ов, что даёт сенсорному выделению перетаскиванием точку остановки. Обработку ввода анонс тоже описывает как временное решение: Wordgard обрабатывает beforeinput для всего, кроме композиции, и отказывается от разбора мутаций DOM, как в ProseMirror, — до проверки на практике. Матрица поддержки браузеров не опубликована.

Wordgard рядом с ProseMirror, TipTap и Lexical

ProseMirrorTipTapLexicalWordgard
Модель измененийУпорядоченные шагиНаследует от ProseMirrorСобственная модельОдно изменение на основе секций
Структура содержимогоContent expressions на регулярных выраженияхНаследует от ProseMirrorСобственная модельНаборы типов потомков плюс коррекции
КонфигурацияПлагиныРасширения поверх плагинов ProseMirrorСобственная модельФасетные расширения с приоритетом на уровне значения
ВыделениеНативное браузерноеНативное браузерноеСобственная модельКурсор, отрисованный библиотекой, нативные касания

TipTap — это фреймворк-надстройка над ProseMirror, наследующая его базовую модель; Lexical — отдельный редакторный фреймворк от Meta. Ни один из них не имеет общих интерфейсов с Wordgard.

Кому стоит подождать: большинству команд — пока да. Wordgard впервые вышел как 0.1.0, и пакет в npm с тех пор прошёл через несколько релизов; самая свежая запись в changelog — 0.5.2 от 6 сентября 2026 года, причём в changelog зафиксированы ломающие изменения в 0.2.0, 0.3.0, 0.4.0 и 0.5.0. Автор рассчитывает переосмыслить части публичного интерфейса и остаться на 0.x, вероятно, год или дольше. Пути обновления с ProseMirror нет: документ проекта Migrating from ProseMirror сопоставляет каждый пакет ProseMirror с модулем Wordgard и прямо указывает, что совместимость интерфейсов не ставилась целью.

Вывод

Wordgard — первый редактор из линии ProseMirror, который в рамках одного дизайна отказывается от шагов, упорядоченных content expressions и выделения, управляемого браузером, и внимания он заслуживает благодаря именно этим трём решениям, а не имени автора. Если вы сопровождаете продукт на основе ProseMirror, прочитайте документ о миграции и разделы руководства Changes и Corrections, а затем попробуйте реализовать в прототипе один неудобный инвариант схемы в виде коррекции; это упражнение скажет вам о применимости больше, чем любой список возможностей.

Часто задаваемые вопросы

Включает ли Wordgard совместное редактирование или нужно писать собственный сервер?

Wordgard поставляет клиентское расширение для совместного редактирования в wordgard/collab, но без сервера. Расширение collab() отслеживает неподтверждённые локальные изменения; collab.sendableUpdate и collab.receive обмениваются обновлениями с центральным авторитетом, который вы реализуете сами, а collab.transformUpdate (добавлен в 0.2.0) позволяет этому серверу выполнять rebase устаревших обновлений. Коррекции пропускают удалённые транзакции, поэтому задайте в конфигурации клиента и в трансформации на сервере одни и те же коррекции, перечисленные в одном порядке.

В чём разница между plot и leaf в Wordgard?

Plot — это узел с содержимым, например абзац, список, таблица или сам документ; leaf — узел без содержимого, например текст, изображение или перевод строки. Это отдельные классы, Plot и Leaf, а свойства isPlot и isLeaf сужают тип между ними в TypeScript. Leaf сам по себе является тегом (тип, параметр, метки), тогда как plot содержит тег плюс массив содержимого.

Можно ли создавать или изменять документы Wordgard вне браузера, например в Node?

Для модели документа — да, для редактора — нет. Модули wordgard/doc, wordgard/state и wordgard/types спроектированы для работы без DOM, так что на сервере можно строить документы, применять наборы изменений, выполнять коррекции и сериализовать в JSON; wordgard/types зависит только от wordgard/doc. Модуль wordgard/editor вне браузера загружается, но пользы не приносит, а документ в виде HTML-строки требует браузерного парсера, поэтому передавайте JSON или используйте jsdom.

Поддерживает ли Wordgard таблицы и как он сохраняет их прямоугольность?

Да. Модуль wordgard/table экспортирует набор расширений tables(), который добавляет элементы схемы для таблиц, тип CellSelection для выделения прямоугольных областей ячеек, обработчики вставки и перетаскивания, меню таблицы и tables.correction — встроенную коррекцию, восстанавливающую таблицы, ячейки которых не выстраиваются в аккуратный прямоугольник. Её параметры — headerCells, cellSpanning и cellContent (inline или block). Объединённые ячейки используют метки RowSpan и ColSpan.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.