Марейн Хавербеке выпускает свой шестой редактор — полная переработка архитектуры с учётом девяти лет опыта. Дельта-формат изменений, компонуемые схемы и собственное управление выделением.
2 июля 2026 года Марейн Хавербеке — создатель ProseMirror и CodeMirror, двух самых влиятельных библиотек редактирования текста в экосистеме JavaScript — анонсировал новый проект под названием Wordgard. По его собственному подсчёту, это шестой нетривиальный редактор, который он построил.
Wordgard — это не ProseMirror 2.0. Это полная переработка с нуля, вобравшая всё, что Хавербеке узнал за девять лет поддержки ProseMirror, плюс архитектурные находки из переработки CodeMirror 6. Программный интерфейс построен заново, без оглядки на совместимость.
Эта статья — технический разбор архитектуры Wordgard: дельта-формат изменений, заменивший шаги ProseMirror, компонуемая система схем, отказ от регулярных выражений для ограничения контента, система расширений на фацетах из CodeMirror 6 и новый подход к управлению выделением независимо от браузера. Если вы работаете с редактированием форматированного текста или следите за дизайном JavaScript-библиотек, этот релиз стоит понимания.
ProseMirror никуда не уходит — Хавербеке продолжит его поддерживать. Но девять лет реального использования выявили архитектурные решения, которые он теперь считает ошибками. Вместо того чтобы пытаться исправить их обратно-совместимым способом (что дало бы, по его словам, «скомпрометированный win32-подобный бардак»), он выбрал чистый разрыв.
Ключевые проблемы, которые решает Wordgard:
ProseMirror представляет изменения как последовательность шагов (steps), где каждый шаг работает с документом, полученным после предыдущего. Шаги атомарны, каждый делает одно чёткое действие, но с ними «серьёзно неудобно работать», как выражается Хавербеке. Чтобы понять, какой диапазон документа был заменён, нужно пройти по последовательности шагов в обоих направлениях, маппируя позиции вперёд и назад.
Wordgard заменяет это дельта-форматом, заимствованным из ShareJS и доработанным в CodeMirror 6. Изменение — это плоская последовательность секций, каждая из которых либо:
Для плоского документа длиной 10 вставка буквы «L» в позиции 4 выглядит так:
[keep 4] [replace 0 with "L"] [keep 6]
Удаление первых двух символов:
[replace 2 with ""] [keep 8]
Секция update — новая концепция, отсутствующая в текстовом дельта-формате CodeMirror. Она сохраняет структуру секции, но добавляет или удаляет разметку (выделение жирным, стиль ссылки, alt-текст изображения). Сделать слово с 3 по 6 позиции жирным:
[keep 3] [update 3 +bold] [keep 4]
Поскольку Wordgard использует систему индексации на основе подсчёта токенов (как и ProseMirror), дельта-формат адресует документ как плоскую последовательность токенов — открывающие/закрывающие токены узлов и листовые токены — и вставляет в неё новые последовательности. Это работает для форматированного контента благодаря фиксирующим механизмам, обеспечивающим структурную валидность.
Такие дельты легко комбинировать: одна транзакция всегда имеет одно связанное изменение, которое легко инспектировать и анализировать. Они также поддерживают ограниченную форму операционной трансформации (ОТ), позволяя объединять изменения, описанные относительно одного и того же исходного документа. Это даёт:
Документ на самом деле не является плоской последовательностью токенов — токены имеют смысл только в组合и, образующей хорошо сформированное дерево. Если удалить закрывающий токен узла, документ становится несбалансированным, и изменение не может быть применено напрямую. Код создания изменений в Wordgard включает проверки и коррекции для обеспечения валидности всех изменений.
Для операционной трансформации задача сложнее: трансформация изменения A для применения после B должна давать тот же результат, что и трансформация B для применения после A (конвергенция). Wordgard решает это с помощью механизма фиксирующего изменения (fix-up). При трансформации изменений система выводит коррекцию, которая применяется к обоим результатам трансформации одинаково, гарантируя сходимость обоих путей к одному валидному документу.
В ProseMirror схемы документов задают отношения между узлами напрямую — типы узлов и разметки существуют только внутри конкретной схемы. Это затрудняет композицию схем и обмен функциональностью между ними.
Wordgard применяет принципиально иной подход: типы узлов и разметки — это независимые сущности, которые могут быть частью нескольких схем. Они служат типизированными дескрипторами с поддержкой автодополнения, представляющими элементы документа независимо от контекста схемы.
Композиция схемы — просто сбор нужных элементов:
import { schema } from "wordgard"
import { heading, paragraph } from "wordgard/block"
import { em, strong, link } from "wordgard/mark"
const mySchema = schema([heading, paragraph], [em, strong, link])
Когда нужно переопределить отношения существующих элементов, схема может задать эти переопределения. Определение узла или разметки указывает типы контента по умолчанию, но схема, которой нужно другое поведение, может изменить их, не затрагивая базовый тип.
Такой дизайн делает переиспользуемые расширения значительно более жизнеспособными. В ProseMirror схема была настолько общей, что код либо был привязан к конкретной схеме, либо не мог знать, что означают элементы. В Wordgard встроенные узлы полезны «из коробки», и расширения могут работать с ними напрямую.
Ещё одно улучшение: в ProseMirror атрибуты узлов (например, выравнивание текста или alt-текст) определяются непосредственно на типе узла. Добавление выравнивания ко всем текстовым блокам требовало модификации каждого узла. Wordgard обобщает разметку (marks) для таких целей, сохраняя типы узлов чистыми и позволяя модульно добавлять атрибуты стилизации.
Фирменная функция ProseMirror — возможность задавать разрешённое содержимое для родительского узла с помощью регулярного выражения — убрана в Wordgard. Описание контента может ограничивать только типы разрешённых потомков, но не их порядок.
Причина двоякая. Во-первых, ограничения на основе регулярных выражений делают универсальный код манипуляции документом чрезвычайно сложным. Код, не написанный для конкретной схемы, не может ничего предполагать о допустимых трансформациях, требуя проверки каждой операции против выражения контента. Даже сам Хавербеке признаётся, что допускал ошибки в таком коде ProseMirror.
Во-вторых, жёсткая фиксация формы документа часто вредит пользовательскому опыту. Документы редактируются через серию маленьких инкрементальных действий. Если редактор блокирует промежуточные состояния с нестандартной формой документа, пользователи испытывают frustration.
Wordgard заменяет это абстракцией под названием коррекция (correction) — программной функцией, исправляющей нежелательные формы документа. Коррекции лучше потому что:
import { correction } from "wordgard"
const tableCorrection = correction((doc) => {
// Обеспечить одинаковое количество ячеек во всех строках таблицы
// Выражения контента ProseMirror не могли этого выразить
})
Система расширений ProseMirror позволяет плагинам влиять на систему, но каждый плагин делает несколько вещей одновременно. Приоритет плагинов глобален — можно менять их порядок, но один плагин может нуждаться в высоком приоритете для одной своей функции и низком для другой.
Wordgard копирует систему фацетов из CodeMirror 6. Фацеты — это типизированные точки расширения, которые может определять любой код, а не только сама библиотека редактора. Каждое значение расширения задаёт свою категорию приоритета независимо от других функций, предоставляемых тем же набором расширений.
Конфигурация — это не массив плагинов, а дерево расширений, каждое из которых может определять обработчики событий, настраивать атрибуты редактора, добавлять состояние редактора или вводить новые точки расширения. Реализация функции обычно состоит из группы совместно работающих расширений:
import { extensionGroup, keyBinding, stateEffect } from "wordgard"
const boldFeature = extensionGroup(
keyBinding({ key: "Mod-b", run: toggleBold }),
stateEffect("bold", (state) => ({ ...state, bold: !state.bold })),
// Ещё расширения, которые автоматически компонуются
)
Примитивы, на которых строятся расширения, спроектированы для чистой композиции. Вы можете добавлять наборы расширений в конфигурацию и ожидать, что они будут работать вместе без конфликтов. Это значительное улучшение качества жизни по сравнению с системой плагинов ProseMirror.
Один из главных источников проблем ProseMirror — опора на нативную реализацию выделения браузера. Идея была разумной — движение курсора в двунаправленном тексте со стилизованным контентом сложно. Но на практике браузеры делают эту работу «недостаточно хорошо».
Wordgard применяет другой подход: он реализует почти всё управление выделением самостоятельно. Для этого потребовалось:
Единственное исключение — сенсорное выделение, которое остаётся нативным, потому что его переопределение ломает системное контекстное меню — критичное для телефонов и планшетов. К счастью, сенсорное выделение имеет меньше странных проблем, чем клавиатурное.
Поддержка браузерами событий редактирования — особенно beforeinput —
значительно улучшилась за последние девять лет. Wordgard обрабатывает события
beforeinput для всего, кроме композиционного ввода текста, избегая
целого класса обходных путей, необходимых ProseMirror для мониторинга и разбора
изменений DOM.
Wordgard 0.1 доступен в npm как wordgard. Основной интерфейс поддерживает
почти всё, что запланировал Хавербеке, а набор расширений подтверждает практичность
дизайна. Документация, включая полное справочное руководство, доступна на
wordgard.net.
Библиотека выпущена под лицензией MIT. Хавербеке серьёзно рассматривал более строгую лицензию, но решил, что его модель работы — это модель изобилия: предоставлять достаточно ценности, чтобы даже малая часть возвращалась к нему как доход. Он прямо осуждает использование своего кода для обучения ИИ, но признаёт, что мало что может с этим сделать, сохраняя систему доступной.
Важное отклонение от стандартной практики open-source: Wordgard не принимает пулл-реквесты. Хавербеке считает, что рецензирование и согласование крупных изменений требует больше работы, чем их самостоятельная реализация, а с ростом объёмов AI-генерируемого кода он предпочёл развивать проект самостоятельно.
План — оставаться на версиях 0.x как минимум год для сбора обратной связи, исправления ошибок и уточнения публичного API. Как и с любым релизом 0.1, к продакшену стоит подходить с осторожностью — но для экспериментов и раннего внедрения Wordgard продвинулся дальше, чем предыдущие проекты Хавербеке на момент анонса.
Wordgard — значительная веха в дизайне JavaScript-библиотек для редактирования текста. Хавербеке взял всё, чему научился на двух самых широко используемых редакторских библиотеках в экосистеме, и переплавил в более чистую, более компонуемую архитектуру.
Дельта-формат изменений, независимые элементы схем, система коррекций и фацеты из CodeMirror 6 — каждое из этих решений адресует конкретную болевую точку из девятилетней истории ProseMirror. Вместе они представляют зрелую дизайнерскую философию, ставящую эргономику разработчика и компонуемость выше теоретической чистоты.
Разработчикам, создающим приложения, которым нужно редактирование форматированного текста — системы управления контентом, платформы для совместной работы, базы знаний или структурированные редакторы документов — стоит присмотреться к Wordgard. Он ещё молод, но фундамент прочен, а дизайнерские решения хорошо документированы и обоснованы.
Если вы рассматриваете проект по разработке сайта или веб-приложения, связанный с редактированием текста или любой другой сложной фронтенд-задачей, свяжитесь со мной — я full-stack разработчик с опытом во всей экосистеме JavaScript и помогу выбрать правильные инструменты для ваших задач.
Расскажите о проекте — я помогу выбрать правильный подход и архитектуру. Бесплатная первичная консультация.