Первый взгляд на Wordgard — новый текстовый редактор
Wordgard — новая библиотека rich-text редактора Марийна Хавербеке: одинарные изменения, corrections, facets и собственное выделение.
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
| ProseMirror | TipTap | Lexical | Wordgard | |
|---|---|---|---|---|
| Модель изменений | Упорядоченные шаги | Наследует от 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.
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