Meridian Office

Meridian Write — спецификация приложения

Текстовый процессор пакета Meridian Office. Документ подчинён библии проекта (docs/00-foundation.md): имена, форматы, стек, каркас окна, фазы и приоритеты взяты оттуда и здесь не переопределяются. Версия 0.1 · 2026-10-03 · фаза 1 (Write 1.0 выходит вместе с Meridian Hub, 3–12 мес от старта). Связанные документы: 03-architecture.md (ядро, FFI, процессы), 04-file-formats.md (кодеки), 05-script-api.md (MeridianScript, общая часть), 10-licensing-accounts.md (редакции и флаги функций).

Содержание: 1. Назначение и аналоги · 2. Функции · 3. Экраны · 4. Модель данных · 5. Алгоритмы и скрипты · 6. MeridianScript API · 7. Форматы · 8. Горячие клавиши · 9. Критерии готовности и риски · 10. Вопросы к библии.


1. Назначение и аналоги

Meridian Write — текстовый процессор для документов любой длины: письма, договоры, отчёты, курсовые и диссертации, книги, техническая документация, рассылки. Главный потребитель Meridian Kernel: модель текста (text-model), раскладка (text-layout), стили, поля, поиск, undo/CRDT. Таблицы, диаграммы, фигуры и формулы в Write — те же модули, что в Sheets, Slides, Draw и Math (принцип «одна модель — много представлений»).

Формат по умолчанию — .mwx (шаблон .mwt, плоский .mwxf), пространство имён urn:meridian:office:write:1. Полная поддержка .docx .docm .dotx .doc .rtf .odt .ott .txt .md .html .fb2 .epub, чтение .pages .wpd .abw .wps DocBook .tex, экспорт .pdf .pdf/a .pdf/ua .epub .html .md .tex, страниц в .png/.svg (матрица в библии, 3.3).

1.1 Аналоги

Продукт Сильные стороны Слабые стороны, которые используем
Microsoft Word 365 эталон совместимости, ribbon, рецензирование, поля, слияние, огромный парк документов хрупкие многоуровневые списки, нет регулярных выражений, обновление полей вручную (F9), тормоза на 300+ страницах с правками, VBA без песочницы, закрытый формат истории, нет ГОСТ-библиографии, нет Кнута–Пласса, слабая навигация по объектам
LibreOffice Writer открытый ODF, регулярные выражения ICU, стили страниц, бесплатно устаревший интерфейс, неточный импорт docx (поля, списки, плавающие объекты, таблицы), нет совместной работы, слабая работа с комментариями и правками, Basic/Python-макросы без типов
Apple Pages красивая вёрстка, простота, iCloud только macOS/iOS, нет полей/ссылок/указателей уровня Word, экспорт docx с потерями
Google Docs совместная работа, комментарии облако обязательно, нет разделов/колонтитулов уровня Word, слабая печать, нет полей
WPS Writer совместимость с docx, лёгкость реклама, закрытость, слабые скрипты

1.2 Чем Write будет лучше Word и Writer — конкретно

# Отличие Что именно делаем Чего нет у Word / Writer
1 Точная раскладка «как в Word» + лучшая типографика по выбору режим совместимости повторяет метрики Word (интервалы, пробелы, висячие строки, «лишние» пробелы за полем); в собственных шаблонах — Кнут–Пласс, висячая пунктуация, оптические поля, OpenType-фичи Word: только жадный алгоритм; Writer: нет Кнута–Пласса, висячей пунктуации
2 Поля обновляются сами граф зависимостей полей; PAGE, NUMPAGES, REF, SEQ, формулы, оглавление пересчитываются при изменении источника (с троттлингом); «F9» остаётся для принудительного обновления Word: ручное обновление, устаревшие оглавления в половине документов
3 Списки как объекты список — сущность с панелью свойств: видно, какие абзацы в него входят, кнопка «Починить нумерацию», источник номера подсвечивается, перезапуск и продолжение — явные действия Word: нумерация «ломается» при копировании между документами; Writer: путаница «стиль списка/прямой список»
4 Поиск уровня IDE регулярные выражения (regex), подстановочные знаки Word (совместимость), поиск по форматированию, стилям, объектам (таблицы, рисунки, поля), сохранённые запросы, предпросмотр замен списком Word: только wildcards; Writer: нет сохранённых запросов и предпросмотра
5 Рецензирование без тормозов правки — отдельный слой спанов, не копии текста; фильтр по автору/типу/дате, принять «только форматирование», сравнение по словам и структуре (перемещения абзацев, строк таблиц), трёхстороннее объединение Word: один фильтр, медленно; Writer: сравнение теряет структуру
6 Навигация по всему документу панель «Навигация»: заголовки, страницы, таблицы, рисунки, формулы, сноски, комментарии, поля, закладки, правки — с перетаскиванием разделов Word: заголовки/страницы/результаты; Writer: есть «Навигатор», но без правок/комментариев
7 Длинные документы фоновая раскладка, 500 страниц открываются < 2 с, ввод < 16 мс; главный документ с вложенными (книги) без порчи стилей Word: 500 страниц с правками — секундные паузы; «главный документ» Word портит файлы
8 ГОСТ из коробки стили цитирования ГОСТ Р 7.0.5-2008 (и 7.0.100-2018), шаблоны отчёта по ГОСТ 7.32, рамки ЕСКД, нумерация формул справа, списки литературы по алфавиту/по порядку упоминания Word: только APA/MLA/Chicago/ISO 690; Writer: примитивная библиография
9 Скрипты вместо макросов TypeScript с типизированным API, песочница, транслятор VBA для типовых макросов, команды в ленте через манифест, события документа Word: VBA без песочницы и типов; Writer: Basic/Python без изоляции
10 Открытый формат с историей .mwx хранит журнал операций (history/), версии и авторов; плоский .mwxf для git с построчным diff Word: история только в OneDrive; Writer: плоский .fodt без истории
11 Доступность как функция проверка доступности (alt-текст, порядок заголовков, контраст, таблицы с заголовками), экспорт PDF/UA с тегами, режим чтения с настройками для дислексии Word: проверка есть, PDF/UA частично; Writer: PDF/UA есть, проверки нет
12 Комментарии как обсуждения ветки, упоминания @, статусы (открыт/решён), реакции, фильтр, экспорт в docx (w15) Writer: плоские комментарии
13 Один документ — несколько окон новое окно, разделение холста, «рядом» с синхронной прокруткой, разные режимы в окнах (структура + разметка) Writer: разделение окна отсутствует
14 Восстановление всегда журнал операций каждые 2 с, восстановление после сбоя с потерей ≤ 2 с правок; версии в документе Word: автосохранение 10 мин без OneDrive
15 Assist (Pro) переписать/сократить/перевести/подытожить выделение, «сделать таблицу из текста», проверка стиля — локальная модель или облако по выбору Word: только облако Copilot; Writer: нет

2. Функции

Приоритеты по библии: P0 — без этого нет 1.0; P1 — 1.x; P2 — позже. Колонка «Отличие» — улучшение или сознательное отступление от Word. Функции, зависящие от редакции (библия, 9.1), помечены [Pro], [Home], [Business] и включаются через Features::has(...).

2.1 Ввод и правка

Функция P Поведение Отличие от Word
Ввод текста, IME, мёртвые клавиши P0 каретка на границах графемных кластеров (UAX #29); композиция IME отображается подчёркиванием, кандидаты — у каретки; реконверсия по выделению —
Выделение: мышь, Shift+стрелки, двойной/тройной клик (слово/абзац), Ctrl+клик (предложение), выделение в поле выделения слева (строка/абзац/всё) P0 режимы как в Word; выделение «умное» (прихватывает пробел после слова, отключается в параметрах) добавлено Ctrl+Shift+клик — выделить всё с таким же форматированием прямо из контекста
Блочное (прямоугольное) выделение Alt+перетаскивание P1 столбец символов; копирование/удаление/ввод в каждую строку как Word
Множественное выделение (Ctrl+перетаскивание) P1 несколько несмежных диапазонов, общее форматирование как Word
Вырезать/копировать/вставить, буфер обмена с историей P0 форматы: MOP-фрагмент, OOXML-фрагмент (<w:document> в text/html-обёртке Word), RTF, HTML, plain text, PNG; «Параметры вставки» (сохранить исходное / объединить / только текст / рисунок) история буфера (24 элемента, панель) — у Word есть, у Writer нет
Специальная вставка P0 выбор формата, «вставить связь» (к .msx диапазону, .mvx) связи обновляются по событию файла, а не вручную
Перетаскивание текста и объектов P0 внутри документа, между окнами и приложениями; Ctrl — копия —
Undo/Redo без ограничения глубины P0 транзакции с коалесцированием ввода (слово/пауза 1 с), история с метками «Отменить: Вставка таблицы», ветвление не поддерживается Word ограничен 100 шагами
Повторить последнее действие (F4 / Ctrl+Y) P0 повтор транзакции над новым выделением —
Автозамена при вводе P0 таблица замен (с форматированием), кавычки «ёлочки»/“лапки” по языку, тире --→—, дроби, порядковые, первые буквы предложений, названия дней, двойные прописные, Caps Lock исключения — по языку абзаца; отмена автозамены Ctrl+Z сразу после
Автоформат при вводе P0 маркированные/нумерованные списки из *, 1.; линии из ---; таблицы из +--+; гиперссылки из URL; стили заголовков из # (markdown-подобно, опция) markdown-ввод (# , **, `) включается в параметрах
Автотекст / экспресс-блоки P1 библиотека фрагментов с форматированием, подсказка после 4 символов, вставка по F3 хранится в шаблоне .mwt и в профиле
Символ/эмодзи, Unicode по коду (Alt+X) P0 диалог WR-44, недавние, по категориям, поиск по имени Alt+X в обе стороны как в Word
Непечатаемые знаки (¶) P0 пробел ·, табуляция →, абзац ¶, разрыв строки ↵, неразрывный ° , мягкий перенос ¬, скрытый текст, якоря объектов ⚓ скрытые закладки и поля — цветные скобки по опции
Буквица P1 в тексте / на поле, высота в строках, шрифт —
Регистр: Shift+F3, «Как в предложениях», «все строчные», «ВСЕ ПРОПИСНЫЕ», «Каждое Слово», «иЗМЕНИТЬ рЕГИСТР» P0 учитывает язык (турецкое i, ß) ICU4X casing
Сортировка абзацев/строк таблицы P1 по тексту/числу/дате, до 3 ключей, по полям с разделителем —
Текстовое поле (надпись), связанные надписи P1 фигура с текстом; цепочка перетекания текста между надписями связи показаны стрелками в режиме «Макет»
Диктовка (речевой ввод) P2 через системный API ОС —
Рукописный ввод (перо) P2 аннотации пером поверх документа, преобразование в текст — через Notes-движок —

2.2 Форматирование символов

Функция P Поведение Отличие от Word
Гарнитура (по письменностям: ascii/hAnsi/eastAsia/cs), размер (1–1638 pt, шаг 0,5), цвет, выделение цветом, заливка символов P0 подстановка шрифтов по метрической таблице (Calibri→Carlito, Times→Liberation Serif, Arial→Liberation Sans, Cambria→Caladea) с показом «замещён» в диалоге панель «Шрифт» показывает, какой шрифт реально использован (fallback)
Полужирный, курсив, подчёркивание (18 стилей Word + цвет), зачёркивание (одинарное/двойное), верхний/нижний индекс, положение (смещение), масштаб (1–600 %), межсимвольный интервал (разрядка/уплотнение), кернинг от размера P0 семантика toggle-свойств как в OOXML (XOR между стилями) —
Регистр: все прописные, малые прописные; скрытый текст P0 скрытый текст печатается по опции —
OpenType-функции: лигатуры (стандартные/контекстные/исторические/дискреционные), формы цифр (старостильные/маюскульные), интервал цифр (пропорц./табличный), стилистические наборы 1–20, контекстные альтернативы, капитель через smcp P0 сохраняется в docx как w14:ligatures/numForm/numSpacing/stylisticSets/cntxtAlts произвольные OpenType-теги (frac, ordn, swsh) — наше расширение, в docx теряются с предупреждением
Текстовые эффекты (контур, тень, свечение, отражение, градиент, 3D) P1 рендер через Draw-сцену; импорт w14:textOutline/shadow/glow —
Эмфазис-знаки (CJK), вертикальные формы, комбинированные символы, «уложить текст» P1 — —
Язык текста (проверка правописания), «не проверять» P0 язык — атрибут символов, автоопределение по письменности и статистике автоопределение по абзацу с подтверждением (без молчаливой смены)
Очистить формат символов (Ctrl+Space), формат по образцу (одинарный/двойной клик) P0 — —
Стиль символов (связанные стили в режиме символов) P0 — —

2.3 Форматирование абзацев

Функция P Поведение Отличие от Word
Выравнивание: по левому, центру, правому, ширине, «распределённое» (CJK), «по ширине с последней строкой» (thai/CJK варианты) P0 семантика Word для последней строки и «лишних» пробелов (см. 5.6) —
Отступы: слева, справа, первая строка/выступ, зеркальные (для разворота), отрицательные P0 единицы: см/мм/дюйм/pt/символы (сетка CJK) линейка с привязкой к сетке 0,25 см (отключаемая)
Интервалы: перед/после (включая «авто» 14 pt), междустрочный: одинарный, 1,5, двойной, множитель, минимум, точно; «не добавлять интервал между абзацами одного стиля» P0 режим «как Word»: одинарный = метрики hhea/OS/2 win по правилам Word; подавление «интервала перед» в начале страницы (как Word, флаг совместимости) —
Границы и заливка абзаца (стороны, стиль, цвет, расстояние, тень) P0 слияние границ соседних абзацев с одинаковыми границами (как Word) —
Табуляции: по левому/центру/правому/разделителю/с чертой, заполнители (точки, дефисы, линия, средняя линия), интервал по умолчанию, позиции в стиле и прямые P0 — диалог WR-32 с живым предпросмотром
Положение на странице: «не отрывать от следующего», «не разрывать абзац», «с новой страницы», «запрет висячих строк» (2/2, настраиваемо), «запретить нумерацию строк», «запретить автоперенос» P0 Word разрешает только 2/2, мы — N/M в режиме не-совместимости —
Уровень структуры (1–9, основной текст) P0 независим от стиля заголовка, как в Word —
Направление абзаца (RTL/LTR), сетка документа, кинсоку (CJK), висячая пунктуация, автоинтервал CJK/латиница, CJK/цифры P1 — висячая пунктуация и для кириллицы/латиницы (кавычки, тире, точки) — наш режим
Рамки (frames) из старых .doc P1 импорт как плавающая надпись с якорем —
Нумерация строк (раздел), «запретить в абзаце» P1 — —
Очистить формат абзаца (Ctrl+Q) P0 — —

2.4 Стили

Функция P Поведение Отличие от Word
Типы стилей: абзаца, символов, связанный (абзац+символы), таблицы (с условными форматами: заголовок, первый/последний столбец, итоги, чередование), списка (нумерации) P0 наследование based_on, «следующий стиль», связь абзац↔︎символ (link) тип «страницы» как в Writer — не вводим; параметры страницы — свойство раздела (совместимость с Word)
Набор по умолчанию P0 «Обычный», «Без интервала», «Заголовок 1…9», «Название», «Подзаголовок», «Цитата», «Выделенная цитата», «Слабое/Сильное выделение», «Строгий», «Ссылка на книгу», «Оглавление 1…9», «Заголовок оглавления», «Текст сноски/Знак сноски», «Текст концевой сноски», «Название объекта», «Текст выноски», «Верхний/Нижний колонтитул», «Номер страницы», «Гиперссылка», «Абзац списка», «Маркированный/Нумерованный список 1–5», «Указатель 1–9», «Список литературы», «Обычная таблица», «Сетка таблицы», «Таблица-список 1–7 (6 цветов)», «Таблица-сетка 1–7», «Номер строки», «Текст комментария/Тема комментария», «Текст в блоке (code)» имена и styleId совпадают с Word RU/EN для чистого round-trip; добавлен «Код» (JetBrains Mono) и «ГОСТ. Основной текст» в русских шаблонах
Наборы стилей и темы (цвета, шрифты, эффекты темы) P0 тема документа как в OOXML (theme1.xml): +mj-lt/+mn-lt шрифты, 12 цветов темы, вариации оттенков в ленте — группа «Оформление» на вкладке «Макет» (вкладки «Дизайн» нет по библии, 5.3)
Галерея экспресс-стилей в ленте, панель стилей (WR-11), применение по Ctrl+Shift+S P0 фильтры: используемые / рекомендованные / все; предпросмотр инспектор стилей «откуда свойство»: для выделения показывает цепочку (документ → стиль абзаца → стиль символов → прямое) по каждому свойству
Создание/изменение стиля по образцу, автообновление, «только в этом документе / в шаблоне» P0 диалог WR-53 «Заменить все прямые форматы стилем» (Ctrl+клик по стилю → «Применить ко всем с таким форматом»)
Импорт/экспорт стилей между документами и шаблонами («Организатор») P1 — —
Скрытые/латентные стили, приоритет, блокировка стилей (защита) P1 как в OOXML latentStyles —
Стили для режима структуры: уровни P0 — —

2.5 Списки и нумерация

Функция P Поведение Отличие от Word
Маркированные (символ/картинка), нумерованные (арабские, римские, буквенные, порядковые, словами, нули в начале, по-русски «а), б)», «I.», с точкой/скобкой), 9 уровней P0 модель как в OOXML: абстрактное определение + экземпляр с переопределениями —
Многоуровневые списки, привязка уровней к стилям заголовков («1. Заголовок 1 / 1.1 Заголовок 2») P0 галерея 7 стандартных + своих; «Библиотека списков» —
Юридическая нумерация (legal: 1.1.1 без римских вверху), формат текста уровня %1.%2., «включить номер предыдущего уровня» P0 — —
Перезапуск / продолжить нумерацию / начать с N / перезапуск после уровня P0 явные команды в контекстном меню и панели списка панель «Список» (в «Свойствах объекта») показывает все абзацы списка и позволяет перетаскивать
Положение номера/текста: отступ, выступ, табуляция после номера, выравнивание номера, «следовать за номером: табуляция/пробел/ничего» P0 — —
Автоопределение списков при вводе P0 см. автоформат —
Повышение/понижение уровня Tab/Shift+Tab, Alt+Shift+←/→ P0 — —
Стиль списка (список как стиль, применимый к абзацам) P1 OOXML numStyleLink —
Нумерация заголовков/рисунков/таблиц/формул по главам («Рисунок 2.3») P0 через SEQ + STYLEREF авто-обновление (см. 1.2 п.2)
Проверка «сломанных» списков P1 команда «Починить нумерацию»: объединяет одинаковые определения, убирает лишние экземпляры нет у Word

2.6 Страницы и разделы

Функция P Поведение Отличие от Word
Размер страницы (A4, A5, A3, Letter, Legal, свои, из принтера), ориентация, поля (включая зеркальные, переплёт слева/сверху, «2 страницы на листе», «брошюра») P0 диалог WR-35, применить: ко всему документу / к текущему разделу / до конца / к выделенному тексту (создаёт разделы) —
Разделы: разрывы «со следующей страницы», «непрерывный», «с чётной», «с нечётной», «со следующей колонки» P0 свойства раздела — у последнего абзаца раздела (модель Word), в UI — панель «Раздел» в строке состояния — номер раздела, клик открывает параметры
Колонки: 1–12, равные/разные ширины, разделитель, промежуток, «начать новую колонку» P0 балансировка колонок в конце непрерывного раздела (как Word) —
Разрывы страницы, колонки, строки, обтекания текста P0 — —
Колонтитулы: верхний/нижний, «особый для первой», «разные для чётных/нечётных», «как в предыдущем разделе», расстояние от края, поля с номерами страниц, рисунки/таблицы в колонтитулах P0 контекстная вкладка «Колонтитулы»; по двойному клику — редактирование связи «как в предыдущем» видны в навигации разделов
Нумерация страниц: формат (1, i, a, А, 一), начать с, «включить номер главы» (Заголовок N + разделитель), поле PAGE \* ArabicDash и др. P0 — —
Вертикальное выравнивание раздела (сверху/центр/по ширине/снизу) P0 — —
Нумерация строк (каждые N, перезапуск по странице/разделу/сквозная, расстояние) P1 — —
Границы страницы (линии/рисунки), цвет/заливка/рисунок фона, водяной знак (текст/рисунок, только печать или и экран) P1 — —
Титульная страница (галерея), пустая страница P0 — —
Сетка документа (CJK): по строкам, по символам и строкам, привязка P1 — —
Направление текста раздела (lrTb, tbRl, tbRlV, lrTbV, tbLrV — японское вертикальное, монгольское) P1 — —
Форматы бумаги по ГОСТ, рамки ЕСКД (основная надпись) P1 в шаблонах: рамка — колонтитул с таблицей и полями DOCPROPERTY нет у Word

2.7 Таблицы

Функция P Поведение Отличие от Word
Вставка: сетка 10×8, диалог (строки/столбцы, авто-подбор), рисование таблицы, преобразование текста↔︎таблица, экспресс-таблицы, вставка таблицы Sheets (внедрение/связь) P0 — «Таблица Sheets» — живой лист с формулами внутри Write (общее grid-ядро)
Вложенные таблицы (без ограничения глубины), объединение/разделение ячеек (горизонтально и вертикально), разделение таблицы, объединение таблиц P0 модель как OOXML: gridSpan + vMerge, сетка колонок —
Повтор заголовочных строк на каждой странице, «разрешить перенос строки на следующую страницу», «не отрывать» (через абзацы) P0 алгоритм в 5.12 —
Авто-подбор: по содержимому, по ширине окна, фиксированная ширина; ширина таблицы в %, абс., авто; ширины ячеек; «выровнять ширину столбцов/высоту строк» P0 алгоритм авто-подбора Word (CSS automatic table layout + правила Word) —
Выравнивание таблицы (слева/центр/справа, отступ), обтекание текстом (плавающая таблица с якорем), положение P0 — —
Границы: стиль (24 вида Word), толщина, цвет; «нарисовать границу», «ластик», диагональные границы ячеек; заливка ячеек (цвет, узор), поля ячеек, интервал между ячейками P0 — —
Выравнивание в ячейке (9 положений), направление текста в ячейке (3), вертикальный текст P0 — —
Стили таблиц с условным форматированием (строка заголовка, итогов, первый/последний столбец, чередование строк/столбцов), галерея P0 — —
Сортировка, формулы в ячейках (=SUM(ABOVE), =A1*B2, функции Word: ABS, AND, AVERAGE, COUNT, DEFINED, FALSE, IF, INT, MAX, MIN, MOD, NOT, OR, PRODUCT, ROUND, SIGN, SUM, TRUE), ссылки на закладки P0 формулы — поле = ; формат \# "0,00 ₽" движок calc-engine общий с Sheets: кроме функций Word доступны все функции Sheets (=СУММ, =ЕСЛИ локализованные) — в docx сохраняются как результат + формула Word, если выразима
Свойства таблицы (WR-37): таблица/строка/столбец/ячейка/alt-текст; высота строк (минимум/точно); заголовок/описание для доступности P0 — —
Преобразование таблицы в текст, в Sheets-таблицу; копирование в Sheets без потерь P0 — —
Горизонтальное объединение таблиц из docx (tblpPr плавающие) P0 — —
Автоматическая нумерация строк (поле AUTONUM/список в ячейках) P1 — —

2.8 Изображения и фигуры

Функция P Поведение Отличие от Word
Вставка рисунка: из файла (png/jpg/gif/webp/bmp/tiff/svg/emf/wmf/heic/avif), из буфера, снимок экрана/области, со сканера (P2), стоковые (Hub, P2) P0 хранится в media/, большие изображения — с миниатюрой для экрана; EMF/WMF растрируем через Draw-импорт SVG остаётся векторным в .mwx; в docx — svg + png-резерв (как Word 2016+)
Встроенный (inline) / плавающий объект: 7 режимов обтекания (в тексте, вокруг рамки, по контуру, сквозное, сверху и снизу, за текстом, перед текстом), расстояние до текста, обтекание только слева/справа/с большей стороны P0 якорь к абзацу/символу/странице/полю; «переместить с текстом», «зафиксировать якорь», «разрешить перекрытие» якорь всегда виден при выделении, перетаскивается; панель «Свойства объекта» показывает все координаты и относительно чего
Положение: 9 стандартных, абс./отн. (поля, страница, колонка, символ, строка, абзац), в % от ширины P0 — —
Размер, обрезка (включая по фигуре и «заливкой/вписать»), поворот, отражение, сохранять пропорции, сброс P0 — —
Коррекция: яркость/контраст/резкость, насыщенность/оттенок/перекраска, прозрачность, художественные эффекты (P2), удаление фона (P2 — через Paint) P1 неразрушающие фильтры, хранятся как параметры (DrawingML a:lum, a:alphaModFix, …) —
Стили рисунков (рамки, тени, отражения, скосы), границы, эффекты P1 — —
Фигуры (библиотека Draw: линии, прямоугольники, стрелки, блок-схема, выноски, звёзды, формулы-фигуры), текст в фигуре, стили фигур, заливка/контур/эффекты, редактирование узлов P0 движок — scene (Draw), DrawingML preset geometry 187 фигур с формулами редактор узлов из Draw
Группировка, порядок (вперёд/назад/перед текстом/за текстом), выравнивание/распределение, область выделения (список объектов с видимостью), полотно (canvas) P0 — —
Подписи (названия) к рисункам/таблицам/формулам: «Рисунок {SEQ}», положение, автоназвание при вставке, нумерация по главам P0 — автоназвание предлагается всплывающей подсказкой после вставки
Alt-текст, «декоративный», заголовок/описание P0 — Assist предлагает alt-текст (Pro)
Значки (icons, SVG-библиотека), «3D-модели» P2 — 3D — только отображение из docx (растр)
Декоративный текст (WordArt) P1 фигура с текстовыми эффектами —
Сжатие изображений при сохранении (dpi), связанные (внешние) изображения, INCLUDEPICTURE P1 — —

2.9 Диаграммы (Meridian Chart)

Функция P Поведение Отличие от Word
Отображение диаграмм из docx/odt (DrawingML chart, cx:chart), печать, экспорт P0 фаза 1: рендер через Meridian Chart, если движок готов, иначе — кэшированное изображение из файла (fallback) —
Вставка диаграммы (все типы Chart: гистограмма, график, круговая, кольцевая, точечная, пузырьковая, с областями, биржевая, поверхность, лепестковая, каскадная, воронка, карта дерева, солнечные лучи, гистограмма распределения, ящик с усами, комбинированная), данные в мини-Sheets P1 данные хранятся во внедрённом листе objects/chart1.msx (как Word с xlsx) редактирование данных прямо в боковой панели, не в отдельном окне
Связь с Sheets-файлом (обновление по изменению) P1 — —
Стили/макеты/элементы (легенда, подписи данных, оси, линии тренда) P1 общие с Sheets —

2.10 Формулы (Meridian Math)

Функция P Поведение Отличие от Word
Отображение формул OMML (docx), MathML (odt), печать P0 фаза 1: математическая раскладка (подмножество Math-движка: дроби, индексы, радикалы, операторы, матрицы, скобки, акценты) — см. 10, вопрос 3 —
Редактор формул: визуальный, LaTeX, UnicodeMath, ввод с клавиатуры; встроенные и отдельные формулы; нумерация (2.1) справа (ГОСТ) P1 (редактор — фаза 4) контекстная вкладка «Формула» нумерация формул с перекрёстными ссылками — встроена (у Word — трюк с таблицей)
Шрифт формул STIX Two / Cambria Math (если установлен) P0 — —

2.11 Поля

Функция P Поведение Отличие от Word
Поля даты/времени: DATE, TIME, CREATEDATE, SAVEDATE, PRINTDATE, EDITTIME, форматы \@ P0 — —
Нумерация: PAGE, NUMPAGES, SECTIONPAGES, SECTION, SEQ, AUTONUM*, LISTNUM, REVNUM P0 — —
Ссылки: REF (с \h \n \p \r \t \w), PAGEREF, NOTEREF, STYLEREF, HYPERLINK, TOC, INDEX, TC, XE, TA, TOA, CITATION, BIBLIOGRAPHY, RD P0 (TOA/RD — P1) — —
Сведения: AUTHOR, TITLE, SUBJECT, KEYWORDS, COMMENTS, DOCPROPERTY, FILENAME \p, FILESIZE, NUMCHARS, NUMWORDS, TEMPLATE, USERNAME, USERINITIALS, LASTSAVEDBY P0 — —
Слияние: MERGEFIELD, MERGEREC, MERGESEQ, NEXT, NEXTIF, SKIPIF, SET, ASK, FILLIN, IF, COMPARE, DATABASE (P2), ADDRESSBLOCK, GREETINGLINE P0 (DATABASE P2) — —
Формулы: = с операторами и функциями Word, ссылки на ячейки и закладки, \# форматы P0 — —
Прочие: SYMBOL, QUOTE, INCLUDETEXT, INCLUDEPICTURE, LINK, EMBED, MACROBUTTON (→ кнопка скрипта), PRINT, BARCODE/DISPLAYBARCODE (P2), GOTOBUTTON, FORMTEXT/FORMCHECKBOX/FORMDROPDOWN (устаревшие формы), ADVANCE, EQ (старые формулы) P1 неизвестные поля сохраняются как есть с кэшированным результатом —
Ключи: \* MERGEFORMAT, \* CHARFORMAT, \* Upper/Lower/FirstCap/Caps, \* Ordinal/CardText/OrdText/Hex/Roman/Alphabetic/DollarText, \# числовые, \@ даты P0 — —
Коды полей / результаты (Shift+F9, Alt+F9), затенение полей (никогда/всегда/при выделении), блокировка Ctrl+F11, разрыв связи Ctrl+Shift+F9, обновление F9 (и автообновление — п. 1.2.2), «обновлять поля перед печатью» P0 — диалог WR-43 строит код по форме с подсказкой синтаксиса; подсветка ошибок в коде поля
Элементы управления содержимым (SDT: текст, форматированный текст, дата, список, флажок, рисунок, повторяющийся раздел, группа) и привязка к XML (customXml) P1 — —

2.12 Оглавление, списки, указатель, библиография

Функция P Поведение Отличие от Word
Оглавление: по стилям заголовков/уровням структуры/полям TC, уровни 1–9, заполнитель, номера справа, гиперссылки, «веб-вид без номеров», свои стили «Оглавление N», несколько оглавлений в документе (\b закладка, \o, \t, \u, \h, \n, \z) P0 генерируется как абзацы с полями PAGEREF, обновление «только номера страниц / целиком» автообновление при изменении заголовков (с задержкой 2 с, без потери ручных правок внутри — предупреждение)
Списки иллюстраций/таблиц/формул (по подписям SEQ или стилю) P0 — —
Предметный указатель: пометка элементов (XE с подэлементами, перекрёстными ссылками «см.», диапазонами страниц по закладкам), автопометка по файлу соответствий, форматы (с отступом/без), колонки, буквы-разделители, выравнивание номеров P0 — панель «Указатель» со списком всех пометок и редактированием на месте
Библиография и цитаты: менеджер источников (типы: книга, статья, веб, отчёт, патент, ГОСТ-типы «нормативный документ», «диссертация»), стили ГОСТ Р 7.0.5-2008 (затекстовые/подстрочные, по алфавиту и по порядку упоминания с номерами в квадратных скобках), APA 7, MLA 9, Chicago, IEEE, ISO 690; поле CITATION, список BIBLIOGRAPHY; импорт BibTeX/RIS/CSL-JSON, стили CSL P0 (ГОСТ, APA, MLA), P1 (остальные, CSL) стили — CSL-движок (citeproc-подобный на Rust) + ГОСТ-правила вручную нет у Word
Таблица ссылок (юридическая, TA/TOA) P2 — —
«Исследование» (поиск источников в интернете, Assist) P2 — —

2.13 Сноски

Функция P Поведение Отличие от Word
Обычные и концевые сноски; формат номера (1, i, a, *, †, ‡, свой символ), начать с, нумерация: сквозная / по разделам / по страницам; положение: внизу страницы / под текстом; концевые — в конце раздела/документа; разделители (обычный, продолжения, уведомление о продолжении) P0 диалог WR-39 —
Перенос сносок на следующую страницу, разбиение длинной сноски, колонки в сносках (1–4) P0 алгоритм 5.13 —
Преобразование обычных ↔︎ концевых, навигация «Следующая сноска», просмотр во всплывающей подсказке P0 — —
Ссылка на одну сноску из нескольких мест (NOTEREF) P0 — —
Запрет концевых сносок в разделе («подавить») P1 — —

2.14 Перекрёстные ссылки, закладки, гиперссылки

Функция P Поведение Отличие от Word
Закладки: именованные диапазоны (точка/диапазон), скрытые _Toc…, закладки на столбцы таблицы, показ скобками, навигация, сортировка по имени/положению P0 — —
Перекрёстные ссылки на: заголовок, закладку, сноску, концевую сноску, название (рисунок/таблица/формула), нумерованный абзац; что вставлять: текст, номер страницы, номер абзаца (полный/без контекста/относительный), «выше/ниже», только подпись и номер; гиперссылка P0 диалог WR-40 обновляется живо при изменении целей автообновление (п. 1.2.2); «битые» ссылки подсвечиваются и перечислены в панели «Навигация → Поля»
Гиперссылки: URL, файл, место в документе (заголовок/закладка), e-mail, с подсказкой, целевой фрейм; Ctrl+клик или клик по опции; удаление/изменение P0 диалог WR-42 —
Автоматическое распознавание URL/e-mail при вводе P0 — —

2.15 Комментарии и рецензирование

Функция P Поведение Отличие от Word
Комментарии: к диапазону, ответы (ветки), решение (resolve), удаление, навигация, автор/дата/инициалы, упоминания @ (при совместной работе), форматированный текст и рисунки внутри P0 панель WR-12 и выноски на полях; docx comments.xml + commentsExtended/Ids/People фильтр по автору/статусу, «показать только нерешённые», экспорт в таблицу
Запись исправлений (track changes): вставка, удаление, перемещение, изменение формата (символов/абзацев/разделов/таблиц), вставка/удаление строк и ячеек таблиц; автор/время; цвета по автору P0 модель 4.9 не копирует текст — слой спанов, нет замедления на больших документах
Отображение: все исправления / простая разметка / без исправлений / исходный документ; выноски / в тексте / только в панели; фильтры по авторам и типам; область проверки (сводка) P0 — —
Принять/отклонить: текущее, все, все показанные, все от автора, только форматирование, «принять все вставки и отклонить все удаления» P0 — —
Сравнение документов (юридическое): по словам/символам, что сравнивать (форматирование, регистр, пробелы, таблицы, колонтитулы, сноски, поля, комментарии), результат в новый/исходный/изменённый документ P0 алгоритм 5.23 структурный diff (перемещения, таблицы), а не только текст
Объединение документов (несколько рецензентов → один) P1 трёхстороннее слияние с базой —
Защита: ограничить редактирование (только чтение / только комментарии / только исправления / заполнение форм), исключения для диапазонов и пользователей, пароль; защита паролем на открытие (AES-256, совместимо с docx Agile Encryption); «пометить как окончательный» P0 — —
Цифровая подпись документа: XAdES (как Word) + Ed25519 (MOP) + ГОСТ Р 34.10 (через системный криптопровайдер, [Enterprise]) и видимая строка подписи P1 диалог WR-50 ГОСТ-подпись
Инспектор документа (личные данные, скрытый текст, комментарии, свойства, скрипты) перед отправкой P0 — —
Версии документа (снимки в history/, именованные версии, сравнение версий) P0 — —

2.16 Правописание и языковые инструменты (Meridian Lingo)

Функция P Поведение Отличие от Word
Орфография в фоне (Hunspell, 40+ языков), волнистое подчёркивание, контекстное меню с вариантами, добавить в словарь (личный/документный/организации), пропустить/пропустить всё, автозамена P0 алгоритм 5.22 словарь организации через Business-политику
Грамматика и стиль (LanguageTool-совместимый локальный сервер, опционально; базовые правила — встроенные: двойные пробелы, пунктуация, «тся/ться», согласование простое) P1 синее подчёркивание —
Переносы: автоматические (TeX-шаблоны, 70 языков), ручные (диалог), зона переноса, максимум подряд, «переносить ПРОПИСНЫЕ», мягкие переносы P0 — —
Тезаурус (синонимы/антонимы; русский — на базе открытых словарей, английский — WordNet) P1 панель WR-17 —
Автозамена (п. 2.1), автоформат, исключения; «Математическая автозамена» (\alpha → α) P0 диалог WR-45 —
Статистика (слова, символы, абзацы, строки, страницы, с/без сносок, время чтения, частотный словарь) P0 диалог WR-51 и строка состояния —
Язык документа/абзаца/выделения, автоопределение, установка «по умолчанию для новых» P0 — —
Перевод (Assist или внешний сервис по настройке) P2 [Pro] — —
Чтение вслух P2 системный TTS —

2.17 Поиск и замена

Функция P Поведение Отличие от Word
Быстрый поиск (панель WR-15): по мере ввода, подсветка всех, счётчик 3 из 42, навигация, результаты списком с контекстом P0 — —
Параметры: учитывать регистр, только слово целиком, подстановочные знаки Word, регулярные выражения (синтаксис regex, именованные группы, $1 в замене), произношение/формы слова (P2), все формы слова (P2), игнорировать пробелы/пунктуацию, различать диакритику/ширину (CJK), направление, область (документ/выделение/колонтитулы/сноски/комментарии) P0 алгоритм 5.24 regex + предпросмотр замен
Поиск по форматированию (шрифт, абзац, стиль, выделение цветом, язык) и спецсимволам (^p ^t ^l ^m ^s ^- ^~ ^# ^$ ^? ^w ^g ^d ^f ^e) P0 — —
Поиск объектов: таблицы, рисунки, формулы, поля, комментарии, сноски, закладки (с фильтром) P0 — —
Замена: одно/все, с форматированием результата, «заменить и найти далее», отмена одним шагом P0 — —
Перейти к (F5): страница, раздел, строка, закладка, комментарий, сноска, поле, таблица, рисунок, формула, объект, заголовок P0 — —
Сохранённые запросы, история поиска P1 — нет у Word

2.18 Слияние (mail merge)

Функция P Поведение Отличие от Word
Мастер слияния (WR-49) и панель (WR-18): письма, конверты, наклейки, каталог/справочник, электронные письма (через Meridian Mail или mailto/SMTP) P0 (письма, каталог), P1 (конверты, наклейки, e-mail) — —
Источники: Sheets (.msx/.xlsx, лист/диапазон/именованный), Base (.mbx таблица/запрос), CSV/TSV, ODBC (P2), контакты Mail (P1), «новый список» (встроенный редактор) P0 отображение полей, фильтр и сортировка получателей, уникальные значения Base как первоклассный источник
Поля слияния, блок адреса, строка приветствия, правила IF/NEXT/SKIPIF/SET/ASK/FILLIN/MERGEREC/MERGESEQ, сопоставление полей P0 — —
Предпросмотр по записям, поиск получателя, проверка ошибок P0 — —
Результат: один документ / отдельные файлы (имя по полю) / печать / PDF (по одному файлу на запись) / e-mail P0 (PDF-пакет P0) фоновая задача с прогрессом PDF по одному файлу на запись — у Word нет без макросов

2.19 Макросы и скрипты (Meridian Script)

Функция P Поведение Отличие от Word
Выполнение скриптов TS/JS из документа, профиля, организации; песочница QuickJS; разрешения (файлы, сеть, буфер) по манифесту P0 [Home+] — нет «макро-вирусов»: скрипты без прав по умолчанию
Редактор скриптов с автодополнением по типам, консоль, отладчик (точки останова, P1) P0 (редактор), P1 (отладчик) общее окно Meridian Script —
Рекордер действий → скрипт P1 записывает транзакции модели и команды UI —
Команды в ленте/панелях/контекстных меню через манифест; горячие клавиши; триггеры событий (открытие, сохранение, печать, изменение поля) P0 см. 6 —
Транслятор VBA → MeridianScript при открытии .docm (предложение конвертировать, отчёт о непереведённом) P1 таблица 6.5 —
Python-скрипты (плагин CPython) P2 — —
Магазин скриптов и шаблонов P2 (фаза 7) — —

2.20 Режимы просмотра, масштаб, окна

Функция P Поведение Отличие от Word
Разметка страницы (по умолчанию), Режим чтения (колонки/страницы, тёмный фон, шрифт и ширина строки, озвучка P2), Веб-документ (без страниц, по ширине окна), Структура (уровни, свёртывание, перемещение, главный документ), Черновик (без плавающих объектов и колонтитулов, быстрый), Фокус (только холст, без ленты, скрытые панели, типографская «пишущая машинка»: текущая строка в центре) P0 (Фокус — P1) — Фокус-режим и «пишущая машинка» — у Word только в Mac-версии
Масштаб 10–500 %, «по ширине страницы/текста», «целая страница», «несколько страниц», Ctrl+колесо, щипок на трекпаде P0 — —
Линейки (горизонтальная/вертикальная), сетка, границы текста, «пустое пространство между страницами» (скрыть), непечатаемые P0 — —
Окна: новое окно для того же документа, разделение (горизонтально), «рядом» с синхронной прокруткой, упорядочить, переключение; разные режимы в окнах P0 один процесс — одно окно документа, второе окно — тот же процесс (библия, 6) —
Боковые панели (список в 3.3): перетаскивание, стыковка слева/справа, вкладки, плавающие P0 — —
Свёртывание заголовков в тексте (треугольник у заголовка) P1 — —
Тёмная тема интерфейса и «тёмная страница» (инверсия холста без изменения документа) P0 — —
Иммерсивное чтение: межсимвольный интервал, слоговое деление, подсветка строки, линейка чтения P2 — —

2.21 Автосохранение и версии

Функция P Поведение Отличие от Word
Журнал операций каждые 2 с (~/.local/share/meridian/journal/<docid>.jrnl), восстановление при запуске после сбоя, список восстановленных документов в Hub P0 алгоритм 5.27 потери ≤ 2 с правок
Автосохранение «в файл» по таймеру (опция, по умолчанию выкл. для чужих форматов, вкл. для .mwx в облаке) P0 — —
Версии: снимки при сохранении (опция), именованные версии, просмотр/сравнение/восстановление, очистка P0 в history/ пакета —
Резервная копия .bak при сохранении (опция) P0 — —

2.22 Печать и экспорт

Функция P Поведение Отличие от Word
Печать (WR-52): принтер, копии, диапазоны (страницы/разделы p1s2-p3s2, выделение, текущая), чёт/нечёт, разбор по копиям, двусторонняя, n на листе, масштаб по бумаге, брошюра, «печатать: документ/свойства/разметку/стили/комментарии/автотекст/клавиши», обновлять поля и связи, фоновая печать, в обратном порядке, с полями/без комментариев P0 Meridian Print; предпросмотр — живой холст —
Экспорт PDF: качество (печать/экран), диапазон, закладки из заголовков, ссылки, PDF/A-1b/2b/3b, PDF/UA, пароль/разрешения, встраивание шрифтов (subset), комментарии как аннотации P0 алгоритм 5.26 PDF/UA с тегами из структуры документа
Экспорт EPUB 3 (главы по заголовкам, обложка, метаданные, фиксированный/перетекающий), FB2 P0 (EPUB), P1 (FB2 запись) — —
Экспорт HTML (чистый, с CSS, одно/многофайловый), Markdown (CommonMark + таблицы GFM), LaTeX (подмножество), текст (кодировки, переносы строк) P0 (HTML, MD, TXT), P1 (LaTeX) — —
Страницы в PNG/SVG, «сохранить как изображение» выделение P1 — —
Отправить: по почте (Mail или системная), в облако Meridian, «поделиться ссылкой» (фаза 6) P1 — —

2.23 Доступность

Функция P Поведение Отличие от Word
Проверка доступности документа: отсутствующий alt-текст, пропуски уровней заголовков, таблицы без строки заголовка/с объединениями, низкий контраст, пустые абзацы для отступов, рисунки «не в тексте», текст в изображениях, язык не задан P0 панель с переходом к проблеме и «исправить» —
Alt-текст и «декоративный» для всех объектов, заголовок таблицы P0 — —
Поддержка экранных дикторов (UIA/AX/AT-SPI через Qt Accessibility): структура документа, чтение по абзацам, уведомления о правках P0 — —
Навигация с клавиатуры во всём UI, высокий контраст, масштаб UI, «уменьшить движение» P0 по MDS (библия, 5) —
Экспорт PDF/UA (теги, порядок чтения, alt, язык), EPUB с ARIA P0 — —

2.24 Длинные документы

Функция P Поведение Отличие от Word
Фоновая раскладка, оценка высоты непросчитанных страниц, прокрутка без ожидания P0 5.16 —
Главный документ: вложенные документы (.mwx/.docx) как разделы, развернуть/свернуть, блокировка, создание из структуры, объединение в один файл; стили — из главного P1 модель 4.12 не портит файлы: вложенные — read-through, изменения записываются в сами вложенные документы транзакционно
Книги: сквозная нумерация глав/рисунков/сносок через RD, общее оглавление/указатель P1 — —
Навигация по структуре с перетаскиванием глав P0 — —
Индекс полнотекстового поиска для документов > 200 страниц P1 — —

2.25 Шаблоны

Функция P Поведение Отличие от Word
Шаблоны .mwt (+ .dotx/.ott чтение/запись): стили, автотекст, скрипты, настройки ленты, свойства; «Обычный» шаблон Normal.mwt; шаблоны организации (папка/облако, политика) P0 — —
Галерея шаблонов в Hub (письмо, резюме, отчёт, курсовая по ГОСТ 7.32, диссертация, договор, счёт, книга, статья, инструкция, протокол); русские шаблоны на PT Serif/Sans (библия, 5.2) P0 — ГОСТ-шаблоны
Присоединение шаблона к документу, обновление стилей из шаблона, глобальные шаблоны (надстройки) P1 — —
Элементы управления содержимым и защита — для форм-бланков P1 — —

2.26 Восточные языки, RTL, вертикальный текст

Функция P Поведение Отличие от Word
BiDi (UAX #9): арабский, иврит, персидский, урду; направление абзаца, нейтральные символы, зеркальные скобки, кнопки RTL/LTR, цифры (контекст/хинди/арабские), каретка на границах направлений P0 5.3, 5.17 —
Сложные письменности (индийские, тайская, кхмерская, тибетская) — шейпинг HarfBuzz, разбиение строк для тайского/кхмерского по словарю (ICU4X) P0 — —
CJK: шрифты по письменности (eastAsia), сетка документа, кинсоку (запрет в начале/конце строки), висячая пунктуация, автоинтервал, фуригана/руби (w:ruby), комбинирование символов, эмфазис-знаки, полноширинные/полуширинные P1 — —
Вертикальный текст (tbRl для японского/китайского, tbLrV монгольский), вертикальные формы глифов (vert, vrt2), повёрнутые латинские фрагменты (tate-chu-yoko) P1 — —
Ввод: IME для всех платформ, окно кандидатов у каретки, реконверсия P0 — —
Локализованные форматы чисел/дат/сортировки (ICU4X) P0 — —

2.27 Совместная работа (фаза 6) [Pro+]

Функция P Поведение Отличие от Word
Одновременное редактирование по CRDT (Yrs) через Meridian Cloud / свой сервер; курсоры и выделения соавторов; присутствие P2 (фаза 6) модель операций 4.11 готова с фазы 1 офлайн-первый: правки сливаются без конфликтов после переподключения
Комментарии и упоминания в реальном времени, уведомления P2 — —
История версий с авторами, «кто изменил этот абзац» (blame) P2 — —
Общий доступ: ссылка, роли (чтение/комментарии/правка), срок P2 — —

2.28 Meridian Assist [Pro] (фаза 7, задел — P2)

Переписать (тон, длина), подытожить, продолжить, перевести, «сделать таблицу из текста», «предложить заголовки», alt-текст для рисунков, проверка стиля документа (канцелярит, пассив), «объяснить правку». Все действия — через панель WR-19, результат вставляется как исправление (track change) с автором «Assist», чтобы его можно было принять/отклонить. Локальная модель или облако — по настройке организации; без аккаунта — недоступно.


3. Экраны

Идентификаторы WR-xx используются в макетах mockups/write.html (<section class="screen" id="WR-xx">). Каркас окна — по библии 5.3 (заголовок, лента 92 px / компактная 40 px, навигационная панель слева, холст, боковая панель справа, строка состояния 24 px). Цвет приложения — --app-write #3B5BDB.

3.1 Сводный список экранов

ID Экран Тип P
WR-01 Главное окно, режим «Разметка страницы» окно P0
WR-02 Строка состояния элемент окна P0
WR-03 Мини-панель форматирования всплывающая панель P0
WR-04 Контекстные меню (12 контекстов) меню P0
WR-05 Backstage «Файл» полноэкранный режим P0
WR-06 Режим чтения режим холста P0
WR-07 Режим структуры режим холста P0
WR-08 Фокус-режим режим окна P1
WR-09 Веб-документ / Черновик режим холста P0
WR-10 Панель «Навигация» боковая панель P0
WR-11 Панель «Стили» боковая панель P0
WR-12 Панель «Комментарии» боковая панель P0
WR-13 Панель «Исправления» (правки) боковая панель P0
WR-14 Панель «Свойства объекта» боковая панель P0
WR-15 Панель «Поиск и замена» боковая панель P0
WR-16 Панель «Правописание» боковая панель P0
WR-17 Панель «Тезаурус» боковая панель P1
WR-18 Панель «Слияние» боковая панель P0
WR-19 Панель «Assist / Исследование» боковая панель P2
WR-20 Панель «Буфер обмена» боковая панель P1
WR-21 Панель «Доступность» боковая панель P0
WR-22 Панель «Указатель и ссылки» (XE, закладки, поля, битые ссылки) боковая панель P1
WR-30 … WR-64 Диалоги (3.6) модальные/немодальные окна —

3.2 WR-01 Главное окно и лента

┌──────────────────────────────────────────────────────────────────────────────┐
│ [W] Отчёт.mwx — Meridian Write                 [Ctrl+K Что сделать?] [☁] [👤] │
├──────────────────────────────────────────────────────────────────────────────┤
│ Файл  Главная  Вставка  Макет  Ссылки  Рецензирование  Вид  Скрипты  │Таблица│ │
│ ┌Буфер┐┌Шрифт──────────────┐┌Абзац────────────┐┌Стили───────────┐┌Правка┐    │
│ │ ⎘ ✂ ⧉ ││Carlito ▾ 11 ▾ A↑A↓││≡ ≡ ≡ ≡ ↕ ⋮≡ ¶  ││Обычный Заг.1 … ││🔍 ⇄ ▾│    │
│ └─────┘└───────────────────┘└─────────────────┘└────────────────┘└──────┘ ⌄  │
├────────────┬───────────────────────────────────────────────┬─────────────────┤
│ Навигация  │ ·····|··1··|··2··|··3··|···· линейка           │ Стили           │
│ ▸ Заголовки│ ┌───────────────────────────────────────────┐ │ ▸ Обычный       │
│ ▸ Страницы │ │                                           │ │ ▸ Заголовок 1   │
│ ▸ Объекты  │ │        страница (тень, поля пунктиром)    │ │ …               │
│ ▸ Результат│ │                                           │ │ [инспектор]     │
├────────────┴───────────────────────────────────────────────┴─────────────────┤
│ Стр. 3 из 12 · Разд. 1 · 1 254 слова · Русский · ¶ · Исправления: вкл · ⌧ ▭ ≣ · 100 % ─●─ │
└──────────────────────────────────────────────────────────────────────────────┘

Поведение окна: один процесс на документ (библия, 6); лента сворачивается двойным кликом по вкладке; «Компактная панель» — одна строка 40 px с самыми частыми командами и выпадающими группами; при ширине окна < 1100 px группы схлопываются в кнопки с меню (приоритет схлопывания задан в манифесте ленты). Все команды имеют стабильные идентификаторы write.<group>.<command> (используются палитрой, скриптами и настройкой клавиш).

3.2.1 Вкладка «Файл» (backstage, WR-05)

Полноэкранный backstage: слева — список разделов, справа — содержимое.

Раздел Содержимое
Создать пустой документ, шаблоны (локальные, организации, Hub-галерея), «из структуры»
Открыть недавние (с закреплением), этот компьютер, Meridian Cloud, добавленные облака, «восстановленные»
Сведения свойства (название, автор, теги, статистика, расширенные — DOCPROPERTY), защита (пароль, ограничения, подпись, окончательный), проверка (инспектор, доступность, совместимость с Word 2007/2010/2013+), версии, связи с внешними файлами, скрипты в документе (разрешить/запретить)
Сохранить / Сохранить как нативный диалог; формат по умолчанию из Hub-настроек; «сохранить копию»; при чужом формате — предупреждение о потерях (таблица 7.3) с кнопкой «Подробнее»
Экспорт PDF (WR-61), EPUB, HTML, Markdown, LaTeX, текст, изображения страниц; «изменить тип файла»
Печать WR-52 с живым предпросмотром
Поделиться ссылка (фаза 6), по почте (вложение .mwx/.docx/.pdf), в облако
Закрыть —
Аккаунт Meridian ID, лицензия/редакция, устройства (библия, 9)
Параметры WR-46

3.2.2 Вкладка «Главная»

Группа Команды
Буфер обмена Вставить ▾ (сохранить исходное форматирование, объединить форматирование, только текст, рисунок, специальная вставка…), Вырезать, Копировать, Формат по образцу (двойной клик — многократно), [◻ открыть панель WR-20]
Шрифт гарнитура ▾ (с предпросмотром, группы: тема/недавние/все; поиск), размер ▾, Увеличить/Уменьшить размер, Регистр ▾ (5 вариантов), Очистить всё форматирование; Ж К Ч▾ (стили и цвет подчёркивания), Зачёркнутый, Нижний индекс, Верхний индекс, Текстовые эффекты ▾, Цвет выделения ▾ (16 цветов Word + «нет»), Цвет текста ▾ (тема/стандартные/другие/пипетка); [◻ WR-30]
Абзац Маркеры ▾ (библиотека, «определить новый», «изменить уровень»), Нумерация ▾ (библиотека, «задать начальное значение», «продолжить»), Многоуровневый список ▾ (библиотека, «определить», «определить стиль списка»); Уменьшить/Увеличить отступ; Сортировка; Отобразить все знаки (¶); По левому краю, По центру, По правому краю, По ширине, Распределить (при CJK/настройке); Интервал ▾ (1,0 / 1,15 / 1,5 / 2,0 / 2,5 / 3,0; добавить/удалить интервал перед/после; другие варианты…); Заливка ▾; Границы ▾ (все стороны, «нарисовать», «границы и заливка…» WR-33); [◻ WR-31]
Стили галерея (прокручиваемая 3 ряда, раскрывается), Наборы стилей ▾ («Оформление» дублирует), Применить стили…, Инспектор стилей, [◻ панель WR-11]
Редактирование Найти ▾ (найти, расширенный поиск, перейти к), Заменить, Выделить ▾ (всё, объекты, все объекты, текст с таким же форматированием, область выделения)
Assist [Pro] Переписать ▾, Подытожить, Перевести ▾ (скрыта без лицензии — «замок»)

3.2.3 Вкладка «Вставка»

Группа Команды
Страницы Титульная страница ▾ (галерея, удалить), Пустая страница, Разрыв страницы
Таблицы Таблица ▾ (сетка 10×8 с живым предпросмотром, Вставить таблицу…, Нарисовать таблицу, Преобразовать в таблицу…, Таблица Sheets, Экспресс-таблицы ▾)
Иллюстрации Рисунки ▾ (это устройство, стоковые, из буфера, со сканера), Фигуры ▾ (категории Draw + «новое полотно»), Значки, Снимок экрана ▾ (окна, вырезка экрана), Диаграмма…, Схема (Draw-объект)
Надстройки Скрипты ▾ (установленные команды), Магазин (фаза 7)
Медиа Видео из интернета (ссылка-превью), Аудио (P2)
Ссылки Гиперссылка (WR-42), Закладка (WR-41), Перекрёстная ссылка (WR-40)
Примечания Комментарий
Колонтитулы Верхний колонтитул ▾ (галерея, изменить, удалить), Нижний колонтитул ▾, Номер страницы ▾ (вверху/внизу/на полях/текущее положение, формат номеров…, удалить)
Текст Текстовое поле ▾ (галерея, нарисовать), Экспресс-блоки ▾ (автотекст, свойство документа, поле… WR-43, организатор), Декоративный текст ▾, Буквица ▾ (WR-57), Строка подписи ▾ (WR-50), Дата и время…, Объект ▾ (внедрённый документ Meridian/OLE-импорт, текст из файла…), Элемент управления ▾ (SDT)
Символы Формула ▾ (галерея, вставить новую — Math, LaTeX-ввод), Символ ▾ (недавние 20, другие… WR-44), Эмодзи

3.2.4 Вкладка «Макет»

Группа Команды
Параметры страницы Поля ▾ (обычные/узкие/умеренные/широкие/зеркальные/последние/настраиваемые… WR-35), Ориентация ▾, Размер ▾ (список + «другие размеры…»), Колонки ▾ (1/2/3/слева/справа/другие… WR-36), Разрывы ▾ (страницы: страница/колонка/обтекание текста; разделы: следующая страница/непрерывный/чётная/нечётная), Номера строк ▾, Расстановка переносов ▾ (нет/авто/ручная/параметры…)
Абзац Отступ слева/справа (поля ввода со стрелками), Интервал перед/после
Упорядочение Положение ▾ (в тексте + 9 позиций + другие…), Обтекание текстом ▾ (7 режимов, изменить контур, переместить с текстом, зафиксировать якорь, другие…), Переместить вперёд ▾, Переместить назад ▾, Область выделения, Выровнять ▾, Группировать ▾, Повернуть ▾
Оформление Темы ▾, Наборы стилей (галерея), Цвета ▾, Шрифты ▾, Интервал между абзацами ▾, Эффекты ▾, По умолчанию (сохранить как тему документа по умолчанию)
Фон страницы Водяной знак ▾, Цвет страницы ▾, Границы страниц (WR-33, вкладка «Страница»)
Сетка документа (показывается при CJK-языке или по настройке) Тип сетки ▾, Параметры…

3.2.5 Вкладка «Ссылки»

Группа Команды
Оглавление Оглавление ▾ (галерея: автособираемое 1/2, ручное, «настраиваемое…» WR-38, удалить), Добавить текст ▾ (не включать, уровень 1–3), Обновить таблицу (только номера / целиком)
Сноски Вставить сноску, Вставить концевую сноску, Следующая сноска ▾ (след./пред. обычная, след./пред. концевая), Показать сноски, [◻ WR-39]
Ссылки и списки литературы Вставить ссылку ▾ (источники документа, «добавить новый источник…», «добавить заполнитель»), Управление источниками (WR-56), Стиль ▾ (ГОСТ Р 7.0.5 (по алфавиту), ГОСТ Р 7.0.5 (по упоминанию), APA 7, MLA 9, Chicago, IEEE, ISO 690, …CSL), Список литературы ▾ (галерея, вставить, обновить)
Названия Вставить название (WR-54), Список иллюстраций ▾ (рисунков/таблиц/формул/настраиваемый), Обновить таблицу, Перекрёстная ссылка (WR-40)
Предметный указатель Пометить элемент (WR-55), Предметный указатель…, Обновить указатель, Автопометка…
Таблица ссылок (P2) Пометить ссылку, Таблица ссылок, Обновить
Закладки Закладка (WR-41), Показать закладки (переключатель)

3.2.6 Вкладка «Рецензирование»

Группа Команды
Правописание Орфография и грамматика (панель WR-16), Тезаурус (WR-17), Статистика (WR-51), Параметры переносов
Доступность Проверить доступность (WR-21), Alt-текст
Язык Язык ▾ (язык проверки…, параметры языка), Перевод ▾ [Pro]
Примечания Создать примечание, Удалить ▾ (текущее/все показанные/все/все решённые), Предыдущее, Следующее, Показать примечания ▾ (в контексте/списком), Решить
Запись исправлений Исправления ▾ (вкл/выкл Ctrl+Shift+E, блокировать запись паролем, только для меня), Отображение для проверки ▾ (все исправления / простая разметка / без исправлений / исходный документ), Показать исправления ▾ (примечания, вставки и удаления, форматирование, выноски: в тексте/в выносках/только примечания и форматирование, авторы…, цвета…), Область проверки ▾ (панель WR-13 справа/внизу)
Изменения Принять ▾ (это исправление, все показанные, все, все и прекратить запись), Отклонить ▾ (те же), Назад, Далее
Сравнение Сравнить ▾ (сравнить… WR-47, объединить…, показать исходный документ)
Защита Ограничить редактирование (WR-48), Защитить паролем, Подписать (WR-50), Пометить как окончательный
Версии Версии документа (WR-63), Сравнить с версией

3.2.7 Вкладка «Вид»

Группа Команды
Режимы Разметка страницы, Режим чтения, Веб-документ, Структура, Черновик, Фокус
Показать ☐ Линейка, ☐ Сетка, ☐ Область навигации, ☐ Границы текста, ☐ Непечатаемые, ☐ Пространство между страницами, ☐ Свёртывание заголовков
Масштаб Масштаб… (диалог с процентами/по ширине/целая/несколько), 100 %, Одна страница, Несколько страниц, По ширине страницы
Окно Новое окно, Упорядочить все, Разделить, Рядом, Синхронная прокрутка, Восстановить расположение, Перейти в другое окно ▾
Панели ☐ Стили, ☐ Навигация, ☐ Комментарии, ☐ Исправления, ☐ Свойства, ☐ Слияние, ☐ Буфер обмена, ☐ Правописание, ☐ Доступность, Сбросить расположение
Тема Светлая / Тёмная / Системная; ☐ Тёмная страница; Плотность ленты ▾ (лента/компактная/фокус)

3.2.8 Вкладка «Скрипты»

Группа Команды
Выполнение Запустить ▾ (список скриптов документа/профиля/организации), Остановить, Записать действия ▾ (начать/остановить/сохранить как скрипт)
Редактор Редактор скриптов (окно Meridian Script), Новый скрипт, Консоль
Расширения Установленные, Магазин (фаза 7), Импорт манифеста…
Безопасность Доверие к документу (разрешить скрипты этого документа), Подписи, Журнал запусков
Автоматизация Назначить на кнопку ленты…, Назначить клавишу…, Триггеры (при открытии/сохранении/печати/изменении поля)
VBA Импорт макросов VBA (из .docm/.dotm, транслятор с отчётом)

3.2.9 Контекстные вкладки

Вкладка Группы и команды
Таблица → Конструктор Параметры стилей таблиц (☐ Строка заголовка, ☐ Строка итогов, ☐ Чередующиеся строки, ☐ Первый столбец, ☐ Последний столбец, ☐ Чередующиеся столбцы); Стили таблиц (галерея, изменить, очистить, создать); Заливка ▾; Обрамление (стиль линии ▾, толщина ▾, цвет ▾, Границы ▾, Нарисовать границу, Ластик, Копировать границу)
Таблица → Макет Таблица (Выделить ▾: ячейка/столбец/строка/таблица, Отобразить сетку, Свойства WR-37); Рисование (Нарисовать, Ластик); Строки и столбцы (Удалить ▾, Вставить сверху, Вставить снизу, Вставить слева, Вставить справа); Объединение (Объединить ячейки, Разделить ячейки, Разделить таблицу, Объединить таблицы); Размер ячейки (Высота, Ширина, Автоподбор ▾: по содержимому/по ширине окна/фиксированная, Выровнять высоту строк, Выровнять ширину столбцов); Выравнивание (9 кнопок, Направление текста, Поля ячейки); Данные (Сортировка, Повторить строки заголовков, Преобразовать в текст, Формула, Открыть в Sheets)
Рисунок → Формат Изменение (Удалить фон P2, Коррекция ▾, Цвет ▾, Художественные эффекты ▾ P2, Прозрачность ▾, Сжать рисунки, Заменить рисунок ▾, Восстановить ▾); Стили рисунков (галерея, Граница ▾, Эффекты ▾, Макет рисунка ▾); Доступность (Alt-текст); Упорядочение (как в «Макет»); Размер (Обрезка ▾: обрезать/по фигуре/пропорции/заливка/вписать, Высота, Ширина, [◻ WR-58])
Фигура → Формат Вставка фигур (галерея, Изменить фигуру ▾, Начать изменение узлов, Текстовое поле); Стили фигур (галерея, Заливка ▾, Контур ▾, Эффекты ▾, [◻ панель WR-14]); Стили текста (галерея, Заливка/Контур/Эффекты текста); Текст (Направление текста ▾, Выровнять текст ▾, Создать связь / Разорвать связь); Доступность; Упорядочение; Размер
Диаграмма → Конструктор / Формат Макеты (Добавить элемент ▾, Экспресс-макет ▾); Стили диаграмм (галерея, Цвета ▾); Данные (Строка/столбец, Выбрать данные, Изменить данные ▾: в панели / в Sheets, Обновить); Тип (Изменить тип); Формат — текущий фрагмент, стили фигур, WordArt, упорядочение, размер
Колонтитулы Колонтитулы (Верхний ▾, Нижний ▾, Номер страницы ▾); Вставка (Дата и время, Сведения о документе ▾, Экспресс-блоки ▾, Поле…, Рисунки, Фигуры); Переходы (Перейти к верхнему/нижнему, Предыдущий, Следующий, ☐ Как в предыдущем разделе); Параметры (☐ Особый колонтитул для первой страницы, ☐ Разные для чётных и нечётных, ☐ Показывать текст документа); Положение (Верхний колонтитул от края, Нижний от края, Вставить табуляцию с выравниванием); Закрыть окно колонтитулов
Формула (Math) Сервис (Формула ▾ галерея, Профессиональный, Линейный, Обычный текст, LaTeX/UnicodeMath переключатель, Параметры); Преобразования; Символы (палитры: основные, греческие, операторы, стрелки, отношения, логика, скрипты); Структуры (Дробь, Индекс, Радикал, Интеграл, Крупный оператор, Скобка, Функция, Диакритика, Предел и логарифм, Оператор, Матрица); Нумерация (Встроенная/Отдельная, Номер справа/слева, Перекрёстная ссылка)
Текстовое поле / SDT свойства элемента управления (заголовок, тег, стиль, блокировка, список значений, формат даты, привязка XML)

3.3 Боковые панели

Все панели: заголовок с кнопкой закрытия и меню (стыковка слева/справа, отстыковать, ширина), прокрутка, состояние сохраняется в профиле на документ-тип. Несколько панелей — вкладками в одной стойке.

ID Панель Элементы Поведение
WR-10 Навигация вкладки «Заголовки» (дерево по уровням структуры, свёртывание, перетаскивание разделов, контекст: повысить/понизить/добавить/удалить/выделить/печать), «Страницы» (миниатюры, виртуализированные), «Объекты» (фильтр: таблицы, рисунки, фигуры, диаграммы, формулы, сноски, комментарии, поля, закладки, исправления, гиперссылки — списком с превью), «Результаты» (поиск); поле поиска сверху клик — переход с подсветкой; текущее положение подсвечивается при прокрутке; перетаскивание главы перемещает блоки до следующего заголовка того же уровня (одна транзакция)
WR-11 Стили список стилей с предпросмотром, фильтр (используемые/рекомендуемые/все/типы), поиск; кнопки Создать/Изменить/Удалить/Организатор; инспектор (для каретки: стиль абзаца → стиль символов → прямое форматирование с перечнем свойств и «откуда»), «Показать все прямые форматы в документе» (список фрагментов) правый клик по стилю: применить, изменить, выделить все N вхождений, удалить, «заменить все прямые форматы на стиль», переименовать; перетаскивание — порядок в галерее
WR-12 Комментарии карточки (автор, аватар, время, текст, ответы, статус), фильтр (все/нерешённые/мои/автор), сортировка (по положению/по времени), поиск, «новый комментарий», экспорт в таблицу/печать карточка привязана к диапазону (подсветка цветом автора); редактирование на месте; @-упоминание (фаза 6); решённые свёрнуты
WR-13 Исправления сводка (вставки/удаления/перемещения/форматирование по авторам), список исправлений с контекстом, фильтр по автору/типу/дате, кнопки Принять/Отклонить/Далее/Назад; режим внизу (горизонтально) клик — переход; множественный выбор → принять выбранные
WR-14 Свойства объекта контекстные страницы: Абзац (отступы, интервалы, положение), Символы, Список (структура списка, абзацы, перезапуск), Таблица/Ячейка, Рисунок/Фигура (положение, обтекание, размер, поворот, заливка, контур, alt-текст), Диаграмма (данные, элементы), Поле (код, ключи, обновить, заблокировать), Сноска, Раздел (параметры страницы, колонки, колонтитулы), Закладка, Гиперссылка все значения с единицами; изменение применяется немедленно (одна транзакция на «отпускание»); «Расширенные…» открывает соответствующий диалог
WR-15 Поиск и замена поле «Найти», «Заменить», переключатели (Aa, слово, .* regex, *? Word, формат ▾, спецсимвол ▾, область ▾, направление), список результатов с контекстом и чекбоксами, «Заменить выбранные/все», «Предпросмотр» (строка «было → стало»), сохранённые запросы ▾ поиск по мере ввода (троттлинг 150 мс), подсветка всех совпадений, Enter — далее, Shift+Enter — назад; при regex — подсветка синтаксиса и ошибок
WR-16 Правописание текущая ошибка (контекст), варианты, кнопки Пропустить/Пропустить всё/Добавить в словарь/Заменить/Заменить всё/Автозамена, язык, объяснение правила (грамматика), счётчик движется по документу от каретки; при исправлении на холсте — панель обновляется
WR-17 Тезаурус слово, язык, список значений → синонимы/антонимы, история, «вставить/копировать» —
WR-18 Слияние шаги мастера в компактном виде: тип → источник (таблица получателей с фильтром/сортировкой/чекбоксами) → поля (вставка, сопоставление) → предпросмотр (◀ 3/120 ▶) → завершить (документ/файлы/печать/PDF/почта) состояние слияния хранится в settings.xml документа (источник, запрос)
WR-19 Assist / Исследование [Pro] чат-поле, подсказки действий над выделением, результаты как «предложение» с кнопками Вставить/Заменить/Как исправление без лицензии — «замок» и предложение редакции
WR-20 Буфер обмена 24 последних элемента с превью, «вставить всё», «очистить», закрепление —
WR-21 Доступность список проблем по категориям (ошибки/предупреждения/советы), «почему это важно», «исправить» (ввести alt-текст, назначить строку заголовка, заменить пустые абзацы интервалом), результат проверки в строке состояния фоновая проверка при изменениях (дебаунс 1 с)
WR-22 Указатель и ссылки вкладки: элементы XE (редактирование на месте), закладки, поля (с «битыми» REF/PAGEREF/NOTEREF красным), гиперссылки (проверка доступности URL — вручную) —

3.4 WR-02 Строка состояния, WR-03 мини-панель

Строка состояния (слева направо, каждый элемент — кнопка): Стр. 3 из 12 (клик — «Перейти к»), Разд. 1 (клик — параметры раздела), Стр. 3 · Строка 14 · Столбец 8 (опция), 1 254 слова (клик — WR-51; при выделении — 120 из 1 254), язык (клик — выбрать язык), значок правописания (✓/✗ с числом ошибок — клик — WR-16), значок доступности, Исправления: вкл/выкл, значок скриптов (если документ содержит скрипты), состояние облака/сохранения («Сохранено 12:01», «Сохранение…», «Офлайн»), режим вставки/замены (ЗАМ), выделение (РАСШ при F8), справа — кнопки режимов (разметка/чтение/веб/структура/черновик), ползунок масштаба с кнопками −/+ и значение (клик — диалог). Правый клик — выбор видимых элементов.

Мини-панель (WR-03): появляется при выделении мышью (полупрозрачная, проявляется при наведении, как в Word) и по правому клику над контекстным меню: гарнитура, размер, A↑ A↓, Ж К Ч, цвет текста, выделение, маркеры, нумерация, отступы, формат по образцу, стиль ▾, комментарий. Отключается в параметрах. Контекстные варианты: для таблицы — вставка/удаление строк, объединение, выравнивание; для рисунка — обтекание, положение, обрезка; для ссылки — открыть/изменить/копировать адрес.

3.5 WR-04 Контекстные меню

Контекст Пункты
Текст Вырезать, Копировать, Параметры вставки (4 значка), Assist ▾, Шрифт…, Абзац…, Поиск в интернете, Синонимы ▾, Перевести, Гиперссылка…, Создать комментарий, Стили ▾, Язык ▾
Слово с ошибкой варианты (до 5), Пропустить всё, Добавить в словарь, Автозамена ▾, Язык ▾, затем обычные пункты
Выделение с исправлениями Принять исправление, Отклонить исправление, Принять все показанные, затем обычные
Таблица/ячейка Вставить ▾ (строки сверху/снизу, столбцы слева/справа, ячейки…), Удалить ячейки…, Объединить/Разделить, Границы и заливка…, Выравнивание в ячейке ▾, Автоподбор ▾, Направление текста…, Свойства таблицы…, Формула…, Сортировка…, Открыть в Sheets
Рисунок/фигура Изменить рисунок/Изменить узлы, Группировать ▾, На передний/задний план ▾, Гиперссылка…, Вставить название…, Обтекание текстом ▾, Alt-текст…, Размер и положение…, Формат рисунка/фигуры… (панель WR-14), Сохранить как рисунок…
Поле Обновить поле, Коды/значения полей, Изменить поле…, Заблокировать, Разорвать связь
Гиперссылка Открыть, Изменить…, Копировать адрес, Удалить гиперссылку
Оглавление/указатель/список литературы Обновить (только номера/целиком), Изменить…, Удалить, Преобразовать в текст
Сноска Перейти к сноске/к ссылке, Преобразовать в концевую, Параметры сносок…
Комментарий (выноска) Ответить, Решить, Изменить, Удалить, Перейти к тексту
Список Продолжить нумерацию, Начать заново с 1, Задать начальное значение…, Изменить уровень ▾, Изменить отступы…, Починить нумерацию
Колонтитул Изменить колонтитул, Удалить, Как в предыдущем (переключатель)
Вкладка ленты / группа Свернуть ленту, Настроить ленту…, Добавить в компактную панель

3.6 Диалоги

Все диалоги — немодальные там, где это возможно (Поиск, Стили, Свойства), модальные — для Параметров и мастеров. Кнопки «ОК/Отмена/Применить», предпросмотр где уместно; Esc — отмена; Enter — ОК. Единицы измерения — по настройке (см, мм, дюйм, pt, пика). Каждый диалог имеет help-ссылку и доступен с клавиатуры.

ID Диалог Назначение, элементы, поведение
WR-30 Шрифт вкладки «Шрифт» (гарнитура по письменностям: латиница/кириллица, восточноазиатский, сложное письмо; начертание; размер; цвет; подчёркивание стиль/цвет; эффекты: зачёркнутый, двойной, верхний/нижний индекс, малые прописные, все прописные, скрытый) и «Дополнительно» (масштаб, интервал, положение, кернинг от N pt; OpenType: лигатуры, формы цифр, интервал цифр, стилистические наборы, контекстные альтернативы, произвольные теги); предпросмотр; «По умолчанию…» (в документ/в шаблон); показывает замещённый шрифт
WR-31 Абзац «Отступы и интервалы» (выравнивание, уровень структуры, ☐ свернут, отступы слева/справа/первая строка/выступ, ☐ зеркальные, интервалы перед/после, междустрочный + значение, ☐ не добавлять между абзацами одного стиля), «Положение на странице» (висячие строки, не отрывать от следующего, не разрывать, с новой страницы, ☐ запретить нумерацию строк, ☐ запретить переносы, направление, CJK-параметры), предпросмотр, «Табуляция…» (WR-32), «По умолчанию…»
WR-32 Табуляция список позиций, поле ввода, выравнивание (левый/центр/правый/разделитель/черта), заполнитель (нет/…/---/___/–·–), «По умолчанию» интервал, Установить/Удалить/Удалить все; предпросмотр строки
WR-33 Границы и заливка вкладки «Граница» (тип: нет/рамка/тень/объёмная/другая; стиль из 24; цвет; ширина; предпросмотр с кликабельными сторонами; применить к: абзацу/тексту/ячейке/таблице; «Параметры…» — расстояния до текста), «Страница» (то же + рисунки-рамки, применить к разделам, выравнивать по краю страницы/текста), «Заливка» (цвет, узор, цвет узора)
WR-34 Списки / Многоуровневый список «Определить новый маркер/формат номера» (символ/рисунок/шрифт, формат номера %1., стиль нумерации, выравнивание) и «Определить многоуровневый список»: уровни 1–9, формат текста, стиль номера, включить уровень, начать с, перезапуск после уровня, ☐ юридический, связь со стилем, уровень в галерее, «следовать за номером», положение номера/текста, «задать для всех уровней», имя ListNum-поля; предпросмотр
WR-35 Параметры страницы «Поля» (верх/низ/лево/право, переплёт + положение, ориентация, страницы: обычные/зеркальные/2 на листе/брошюра, применить к), «Бумага» (размер, источник первой/остальных страниц), «Макет» (начало раздела, колонтитулы: чётн/нечётн, первая; расстояния; вертикальное выравнивание; нумерация строк…; границы…), «Сетка документа» (CJK); предпросмотр; «По умолчанию…»
WR-36 Колонки предустановки (1/2/3/слева/справа), число, ☐ равной ширины, таблица ширин/промежутков, ☐ разделитель, применить к (документ/раздел/до конца/выделенный текст), ☐ новая колонка; предпросмотр
WR-37 Свойства таблицы «Таблица» (ширина + ед., выравнивание, отступ, обтекание + «Размещение…», «Границы и заливка…», «Параметры…» — поля ячеек, интервал, автоподбор), «Строка» (высота + режим, ☐ разрешить перенос, ☐ повторять как заголовок, переходы), «Столбец» (ширина), «Ячейка» (ширина, вертикальное выравнивание, «Параметры…» — поля, ☐ перенос текста, ☐ вписать), «Замещающий текст» (заголовок, описание)
WR-38 Оглавление предпросмотр (печать и веб), ☐ номера страниц, ☐ по правому краю, заполнитель, ☐ гиперссылки вместо номеров, форматы (из шаблона/классический/изысканный/…), уровни, «Параметры…» (стили → уровни, уровни структуры, поля TC), «Изменить…» (стили Оглавление 1–9); для списков иллюстраций — «название: Рисунок/Таблица/Формула», ☐ включать подпись и номер
WR-39 Сноски положение (сноски: внизу страницы/под текстом; концевые: в конце раздела/документа), колонки, формат номера, свой знак, начать с, нумерация (сквозная/в каждом разделе/на каждой странице), применить к; «Преобразовать…»
WR-40 Перекрёстная ссылка тип (нумерованный абзац, заголовок, закладка, сноска, концевая сноска, формула, рисунок, таблица + свои названия), «вставить ссылку на» (текст, номер страницы, номер абзаца полный/без контекста/относительный, «выше/ниже», только подпись и номер), ☐ гиперссылка, ☐ добавить «выше/ниже», ☐ разделитель номеров; список целей с поиском; немодальный — Вставить много раз
WR-41 Закладка имя (валидация: буквы/цифры/подчёркивание, без пробелов — для совместимости; наш формат позволяет любые имена с предупреждением), список (сортировка по имени/положению, ☐ скрытые), Добавить/Удалить/Перейти
WR-42 Гиперссылка связать с: файлом/веб-страницей (адрес, обзор, недавние), местом в документе (дерево заголовков/закладок), новым документом, e-mail (адрес, тема); текст, подсказка, целевой фрейм; «Удалить ссылку»
WR-43 Поле категории, список полей, описание, форма свойств (например, для DATE — формат из списка с примером), ключи с описанием, ☐ сохранять формат при обновлении, кнопка «Коды поля» (ручной ввод с подсветкой и проверкой синтаксиса)
WR-44 Символ вкладки «Символы» (шрифт, подмножество, сетка с виртуализацией, поиск по имени/коду, недавние, код и имя, клавиша…), «Специальные знаки» (тире, неразрывные, мягкий перенос, © ® ™ … с клавишами); «Автозамена…»
WR-45 Автозамена вкладки «Автозамена» (параметры + таблица замен с поиском, ☐ форматированный текст, исключения…), «Математическая автозамена», «Автоформат при вводе», «Автоформат», «Действия» (умные теги — P2)
WR-46 Параметры Write разделы: Общие (имя пользователя, инициалы, мини-панель, предпросмотр), Экран (непечатаемые, границы, затенение полей), Правописание (как Lingo, словари, грамматика), Сохранение (формат по умолчанию, автосохранение, версии, резервные копии, шрифты в документ), Язык (UI, редактирование, проверка), Дополнительно (правка: умное выделение, умная вырезка; вид; печать; совместимость: «режим Word 2007/2010/2013+/Writer/Meridian» с перечнем флагов), Лента и панели (настройка, сброс, экспорт/импорт), Клавиши (схема: Word/LibreOffice/своя, таблица, конфликты), Скрипты (доверие, песочница), Конфиденциальность (телеметрия по согласию, Assist)
WR-47 Сравнить документы исходный, изменённый (с обзором и недавними), «пометить изменения как» (автор), «Дополнительно»: параметры сравнения (вставки/удаления, перемещения, комментарии, форматирование, регистр, пробелы, таблицы, колонтитулы, сноски, текстовые поля, поля), уровень (символы/слова), результат в: новый/исходный/изменённый документ; для «Объединить» — то же + база
WR-48 Защита панель-диалог: 1) ☐ ограничить форматирование (набор разрешённых стилей, запрет тем/наборов), 2) ☐ ограничить редактирование: только чтение / комментарии / исправления / заполнение форм; исключения: диапазоны + пользователи/группы (все/Meridian ID), 3) «Да, включить защиту» → пароль (или аккаунт); «Снять защиту»
WR-49 Слияние (мастер) 6 шагов: тип документа → начальный документ → получатели (источник: Sheets/Base/CSV/Контакты/новый список; таблица с фильтром, сортировкой, поиском дубликатов, проверкой) → документ (блок адреса, приветствие, поля, сопоставление) → просмотр (навигация, исключить получателя, найти) → завершение (печать/документ/файлы/PDF/почта с именованием по полю); можно свернуть в панель WR-18
WR-50 Подпись «Строка подписи» (подписывающий, должность, e-mail, инструкции, ☐ дата, ☐ комментарии) и «Подписать»: сертификат (системное хранилище: Windows CNG / macOS Keychain / Linux NSS/PKCS#11; [Enterprise] ГОСТ через КриптоПро/ViPNet-провайдер), цель подписи, видимая/невидимая, изображение подписи; «Проверить подписи» со статусами
WR-51 Статистика страницы, слова, символы (без/с пробелами), абзацы, строки, ☐ учитывать сноски и колонтитулы, время чтения, частотный словарь (топ-50, экспорт CSV), по выделению
WR-52 Печать слева параметры (принтер + свойства, копии, ☐ разобрать, диапазон: все/текущая/выделение/страницы 1-3,5,p2s1, чёт/нечёт, печатать: документ/свойства/стили/исправления/примечания…, односторонняя/двусторонняя (по длинному/короткому краю), ориентация, бумага, поля, страниц на листе 1/2/4/6/8/16 + брошюра, масштаб по бумаге, ☐ в обратном порядке, ☐ печатать фон, ☐ обновить поля), справа — предпросмотр (навигация, масштаб); «Печать в PDF» — переход к WR-61
WR-53 Стиль (создание/изменение) имя, тип (абзац/символы/связанный/таблица/список), основан на, стиль следующего абзаца, мини-форматирование (шрифт, размер, Ж К Ч, цвет, выравнивание, интервалы, отступы), предпросмотр, описание свойств текстом, ☐ в галерее, ☐ обновлять автоматически, ☐ только в этом документе / в новых на основе шаблона, кнопка «Формат ▾» (Шрифт, Абзац, Табуляция, Граница, Язык, Рамка, Нумерация, Клавиша, Текстовые эффекты)
WR-54 Название (подпись) название (список: Рисунок/Таблица/Формула/свои + «Создать…»), положение (над/под), ☐ исключить подпись из названия, «Нумерация…» (формат, ☐ включить номер главы: стиль + разделитель), «Автоназвание…» (по типам вставляемых объектов)
WR-55 Указатель: пометить элемент основной элемент, дополнительный (подэлемент через :), параметры: перекрёстная ссылка «См.», текущая страница, диапазон страниц (закладка); формат номера (жирный/курсив); Пометить / Пометить все; немодальный; «Предметный указатель…» — формат (с отступом/без), тип, колонки, язык, ☐ номера по правому краю, заполнитель, форматы
WR-56 Управление источниками главный список (профиль) ↔︎ текущий список (документ), поиск, сортировка, предпросмотр в текущем стиле, Создать/Изменить/Копировать/Удалить; форма источника по типу (ГОСТ-типы: книга, статья в журнале, сборник, диссертация, автореферат, нормативный документ, патент, электронный ресурс, стандарт); импорт BibTeX/RIS/CSL-JSON/DOI-поиск (P2)
WR-57 Буквица положение (нет/в тексте/на поле), шрифт, высота в строках, расстояние до текста
WR-58 Макет объекта (размер и положение) «Положение» (горизонталь: выравнивание/книжный/абсолютное/относительное; вертикаль; ☐ переместить с текстом, ☐ зафиксировать якорь, ☐ разрешить перекрытие, ☐ в ячейке), «Обтекание» (режимы, стороны, расстояния), «Размер» (высота/ширина абс./отн., поворот, масштаб, ☐ пропорции, ☐ относительно исходного, исходный размер, сброс)
WR-59 Нумерация строк / Разрывы ☐ добавить нумерацию, начать с, от текста, шаг, перезапуск (на каждой странице/в разделе/непрерывно)
WR-60 Формула в таблице формула =SUM(ABOVE), формат числа, вставить функцию ▾, вставить закладку ▾
WR-61 Экспорт PDF диапазон, качество (печать/минимальный размер), ☐ закладки из заголовков/закладок, ☐ свойства документа, ☐ теги структуры (PDF/UA), ☐ комментарии как аннотации, стандарт (нет/PDF/A-1b/2b/2u/3b/PDF/UA-1), шрифты (встраивать все/подмножества), ☐ изображения: сжатие/dpi, безопасность (пароль открытия, пароль разрешений: печать/копирование/изменение), ☐ открыть после экспорта
WR-62 Редактор скриптов окно Meridian Script (общее для пакета): дерево скриптов, редактор с типами, консоль, отладчик, манифест; описано в 05-script-api.md
WR-63 Версии документа список снимков (время, автор, имя, размер, комментарий), Открыть копию / Сравнить с текущей / Восстановить / Удалить / Создать именованную версию; ☐ сохранять версию при каждом сохранении
WR-64 Восстановление документов после сбоя: список восстановленных (оригинал/автовосстановленный, время, размер), Открыть / Сохранить как / Удалить; показывается Hub и Write

3.7 Режимы холста WR-06…WR-09


4. Модель данных (Rust, крейт meridian-text-model)

4.1 Принципы

  1. Модель повторяет семантику OOXML там, где от этого зависит точность импорта (абзац как единица, свойства раздела у последнего абзаца, поля как маркеры начало/разделитель/конец, нумерация «абстрактное определение
    • экземпляр», toggle-свойства), но хранится компактнее и типизированно. Так round-trip .docx не теряет структуру, а .mwx остаётся простым.
  2. Абзац — единица хранения, раскладки, проверки правописания, undo-коалесцирования и CRDT. Всё, что меняется при наборе, локализовано в одном абзаце; документ — упорядоченное дерево абзацев и таблиц.
  3. Свойства интернированы. Наборы символьных/абзацных свойств — неизменяемые значения в таблице хэш-консинга (PropsInterner), спаны ссылаются на CharPropsId(u32). Сравнение — по id, память — O(уникальных наборов).
  4. Позиции стабильны. Внешние ссылки (закладки, комментарии, якоря, каретки соавторов, кэш раскладки) указывают на ParaId + смещение в символах с гравитацией, а не на индексы в векторах.
  5. Все изменения — операции (Op), применяемые через Transaction; та же операция служит для undo, журнала автосохранения, истории в history/ и (фаза 6) трансляции в Yrs.

4.2 Дерево документа, идентификаторы, позиции

// kernel/crates/text-model/src/document.rs

/// Стабильный идентификатор узла: старшие 32 бита — клиент (0 = локальный), младшие — счётчик.
/// Stable node id: high 32 bits — client id (0 = local), low 32 bits — counter. Совместим с Yrs (client, clock).
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct NodeId(pub u64);
pub type ParaId = NodeId;
pub type TableId = NodeId;
pub type ObjectId = NodeId;

pub struct Document {
    pub id: DocId,                       // UUID документа / document UUID (meta.xml)
    pub meta: Metadata,                  // Dublin Core + свои / Dublin Core + custom
    pub settings: DocSettings,           // совместимость, вид, защита, слияние / compat, view, protection, mail merge
    pub theme: Theme,                    // цвета/шрифты темы / theme colors & fonts
    pub fonts: FontTable,                // объявленные шрифты, встроенные подмножества / declared & embedded fonts
    pub styles: StyleSheet,
    pub numbering: NumberingDefs,
    pub body: Body,
    pub footnotes: NoteStore,            // тела сносок по id / footnote bodies by id
    pub endnotes: NoteStore,
    pub comments: CommentStore,
    pub bookmarks: BookmarkStore,
    pub revisions: RevisionStore,        // авторы, даты, перемещения / authors, dates, moves
    pub fields: FieldIndex,              // индекс полей для обновления / field index for updates
    pub media: MediaStore,               // blob-ы media/, objects/ / blobs
    pub glossary: Vec<GlossaryEntry>,    // автотекст / autotext (в шаблонах)
    pub scripts: ScriptManifest,
    pub sources: BibliographySources,    // источники цитирования / citation sources
    pub custom_xml: Vec<CustomXmlPart>,  // round-trip
    pub extensions: ExtensionBag,        // неизвестные элементы чужих форматов / unknown foreign elements
    pub interner: PropsInterner,         // хэш-консинг свойств / props hash-consing
    pub oplog: OpLog,                    // журнал операций (история, автосохранение) / operation journal
}

pub struct Body { pub sections: Vec<Section> }

pub struct Section {
    pub id: NodeId,
    pub props: SectionProps,             // см. 4.6
    pub blocks: BlockList,               // последовательность блоков / block sequence
    pub headers: HeaderFooterSet,        // default / first / even (каждый — BlockList)
    pub footers: HeaderFooterSet,
}

/// Список блоков с суммарными длинами для O(log n) поиска по позиции (B-дерево порядка 32).
/// Block list with prefix sums for O(log n) position lookup (B-tree, order 32).
pub struct BlockList { tree: SumTree<Block, BlockSummary> }
pub struct BlockSummary { pub chars: u64, pub paras: u32, pub blocks: u32 }

pub enum Block {
    Paragraph(Paragraph),
    Table(Table),
    Sdt(ContentControlBlock),            // элемент управления блочного уровня / block-level content control
}

pub struct Paragraph {
    pub id: ParaId,
    pub style: StyleId,                  // стиль абзаца (всегда есть; по умолчанию "Normal")
    pub props: ParaPropsId,              // прямое форматирование абзаца / direct paragraph formatting
    pub mark_props: CharPropsId,         // свойства знака абзаца ¶ / paragraph mark run properties
    pub text: ParaText,                  // см. 4.3
    pub runs: SpanList<CharPropsId>,     // прямое форматирование символов / direct run formatting
    pub rev: SpanList<RevSpan>,          // слой исправлений / revision layer (4.9)
    pub markers: MarkerMap,              // поля, объекты, сноски, разрывы (занимают 1 символ U+FFFC)
    pub anchors: AnchorList,             // закладки/комментарии (нулевой ширины) / zero-width anchors
    pub section_break: Option<Box<SectionProps>>, // только у последнего абзаца раздела при импорте docx
    pub cache: ParaCache,                // хэш текста, язык, номер списка, раскладка (не сериализуется)
}

/// Элементы, занимающие один символ-заполнитель U+FFFC в тексте абзаца.
/// Inline elements occupying one placeholder char U+FFFC in paragraph text.
pub enum Marker {
    FieldStart { field: FieldId },       // { начало поля / field begin
    FieldSeparate { field: FieldId },    // | разделитель код/результат
    FieldEnd { field: FieldId },         // } конец поля
    FootnoteRef { note: NoteId, custom_mark: Option<String> },
    EndnoteRef { note: NoteId, custom_mark: Option<String> },
    InlineObject { object: ObjectId },   // рисунок/фигура/формула/диаграмма в тексте / inline drawing
    AnchoredObject { object: ObjectId }, // плавающий объект, якорь здесь / floating object anchored here
    Break(BreakKind),                    // Line | Page | Column | TextWrapping{clear}
    Symbol { font: FontRef, code: u32 }, // w:sym
    Ruby { base: Box<ParaText>, text: Box<ParaText>, props: RubyProps },
    CommentRef { comment: CommentId },   // знак комментария / comment reference mark
    SdtStart { sdt: SdtId }, SdtEnd { sdt: SdtId }, // инлайновый элемент управления
    Separator(SeparatorKind),            // разделитель сносок / footnote separator (только в телах сносок)
    PageNumber,                          // устаревший w:pgNum
    Tab,                                 // '\t' хранится как символ; маркер только для w:ptab (абс. таб.)
}
// Обычная табуляция — символ '\t', разрыв строки — '\u{000B}', неразрывный пробел — U+00A0, мягкий перенос —
// U+00AD, неразрывный дефис — U+2011. Так поиск и правописание работают по «плоскому» тексту.

/// Позиция в документе (стабильная): абзац + смещение в скалярных значениях Unicode.
/// Stable document position: paragraph + offset in Unicode scalar values.
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Position { pub para: ParaId, pub offset: u32 }

/// Путь для навигации по дереву (вычисляется из ParaId через индекс документа за O(log n)).
/// Tree path (resolved from ParaId via document index in O(log n)).
pub struct BlockPath { pub section: u16, pub steps: SmallVec<[PathStep; 4]> }
pub enum PathStep { Block(u32), Cell { row: u16, col: u16 }, Note(NoteId), Header(HfKind), TextBox(ObjectId) }

Таблицы:

pub struct Table {
    pub id: TableId,
    pub style: StyleId,
    pub props: TablePropsId,             // ширина, выравнивание, отступ, границы, поля ячеек, плавающая позиция
    pub grid: Vec<Twips>,                // сетка колонок / column grid (как w:tblGrid)
    pub rows: Vec<TableRow>,
    pub look: TableLook,                 // флаги условного форматирования стиля / firstRow, lastRow, …
    pub caption: Option<String>, pub description: Option<String>, // доступность / accessibility
}
pub struct TableRow {
    pub id: NodeId,
    pub props: RowPropsId,               // высота + правило, ☐ заголовок, ☐ разрешить разрыв, выравнивание
    pub cells: Vec<TableCell>,
    pub rev: Option<RevId>,              // вставка/удаление строки как исправление / row ins/del revision
}
pub struct TableCell {
    pub id: NodeId,
    pub props: CellPropsId,              // ширина, grid_span, v_merge: Restart|Continue, границы, заливка,
                                         // поля, верт. выравнивание, направление, ☐ no_wrap, ☐ fit_text
    pub blocks: BlockList,               // вложенное содержимое, включая таблицы / nested content incl. tables
    pub rev: Option<RevId>,
}

4.3 Хранение текста абзаца: rope с атрибутными спанами (решение)

Выбор: текст абзаца хранится в ParaText — компактной строке (SmallString<64>) для абзацев до 4 КиБ и в rope (Rope на базе B-дерева чанков по 1–2 КиБ с суммами «байты / скаляры / графемы / UTF-16») для больших. Форматирование — отдельным слоем SpanList<T>: отсортированный вектор (len, value) для коротких абзацев (< 64 спанов) и B-дерево с суммами для длинных. Все слои (стили, исправления, язык, правописание) — независимые SpanList, разрезаемые по одним и тем же смещениям.

pub enum ParaText { Small(SmallString<64>), Rope(Rope) }   // переключение при > 4096 байт

pub struct SpanList<T: Copy + Eq> { spans: SpanStorage<T> }   // инвариант: Σ len == text.len_chars()
pub struct Span<T> { pub len: u32, pub value: T }

impl<T: Copy + Eq> SpanList<T> {
    pub fn value_at(&self, offset: u32) -> T;                       // O(log n)
    pub fn insert(&mut self, offset: u32, len: u32, value: T);      // расщепляет спан, сливает равных соседей
    pub fn remove(&mut self, range: Range<u32>);
    pub fn set(&mut self, range: Range<u32>, f: impl Fn(T) -> T);   // патч на диапазоне / patch over range
    pub fn iter(&self, range: Range<u32>) -> impl Iterator<Item = (Range<u32>, T)>;
}

Почему не piece table (как в Word и VS Code):

Критерий Piece table Rope + спаны (выбрано)
Локальность для раскладки нет: абзац — набор кусков из разных буферов, нужно собирать текст абзаца непрерывен (или чанки одного абзаца) → шейпинг прямо по срезу
Атрибуты piece table хранит атрибуты вместе с кусками → при смене формата куски дробятся навсегда, таблица растёт при долгом редактировании (известная проблема Word «раздувающихся» файлов) слои независимы, спаны сливаются при равенстве соседей; сложность не растёт со временем
Undo бесплатен (куски не удаляются) нужен журнал операций — он всё равно необходим для автосохранения и CRDT
CRDT (Yrs YText) плохое соответствие: Yrs сам хранит кусочную структуру 1:1 — YText на абзац + атрибуты как форматирующие спаны Yjs
Память на 500 стр (≈1,2 МБ текста) ~ текст + таблица кусков (после долгого редактирования — десятки тысяч) ~ текст + спаны (обычно 2–5 на абзац) ≈ 2 МБ
Поиск/правописание требует линеаризации работают по срезу абзаца

Инварианты: длины спанов всех слоёв в сумме равны длине текста; маркер U+FFFC всегда в единственном спане длины 1; граница графемного кластера не обязана совпадать с границей спана (шейпинг объединяет).

4.4 Свойства, стили и разрешение

// Все поля Option: None = «не задано, наследовать». All fields are Option: None = inherit.
#[derive(Clone, PartialEq, Eq, Hash, Default)]
pub struct CharProps {
    pub font: Option<FontSet>,           // ascii, h_ansi, east_asia, cs (+ hint)
    pub size: Option<HalfPoints>, pub size_cs: Option<HalfPoints>,
    pub bold: Option<bool>, pub bold_cs: Option<bool>,   // toggle-свойства / toggle properties
    pub italic: Option<bool>, pub italic_cs: Option<bool>,
    pub caps: Option<bool>, pub small_caps: Option<bool>, pub strike: Option<bool>, pub dstrike: Option<bool>,
    pub outline: Option<bool>, pub shadow: Option<bool>, pub emboss: Option<bool>, pub imprint: Option<bool>,
    pub vanish: Option<bool>, pub web_hidden: Option<bool>,
    pub underline: Option<Underline>,    // стиль + цвет / style + color
    pub color: Option<Color>, pub highlight: Option<Highlight>, pub shading: Option<Shading>,
    pub spacing: Option<Twips>,          // разрядка / tracking
    pub position: Option<HalfPoints>,    // смещение / raise-lower
    pub scale: Option<u16>,              // масштаб % / horizontal scale
    pub kern: Option<HalfPoints>,
    pub vert_align: Option<VertAlign>,   // Baseline | Superscript | Subscript
    pub lang: Option<LangSet>,           // val, east_asia, bidi
    pub rtl: Option<bool>, pub cs: Option<bool>,
    pub no_proof: Option<bool>, pub snap_to_grid: Option<bool>,
    pub em: Option<EmphasisMark>, pub east_asian_layout: Option<EastAsianLayout>, pub fit_text: Option<FitText>,
    pub ot_features: Option<OtFeatures>, // лигатуры, цифры, ss01..20, cntxt + произвольные теги / OpenType
    pub effects: Option<Arc<TextEffects>>, // w14: контур/тень/свечение/градиент (большой, редкий → Arc)
    pub style: Option<StyleId>,          // ссылка на стиль символов / character style ref (rStyle)
    pub ext: Option<Arc<ExtensionBag>>,
}

#[derive(Clone, PartialEq, Eq, Hash, Default)]
pub struct ParaProps {
    pub alignment: Option<Alignment>,    // Left|Center|Right|Both|Distribute|ThaiDistribute|…
    pub indent: Option<Indent>,          // left, right, first_line, hanging, (chars-варианты для CJK), mirror
    pub spacing: Option<ParaSpacing>,    // before, after, before_auto, after_auto, line, line_rule, contextual
    pub keep_next: Option<bool>, pub keep_lines: Option<bool>, pub widow_control: Option<bool>,
    pub page_break_before: Option<bool>, pub suppress_line_numbers: Option<bool>,
    pub suppress_auto_hyphens: Option<bool>,
    pub outline_level: Option<u8>,       // 0..=8, 9 = основной текст / body text
    pub borders: Option<Arc<ParaBorders>>, pub shading: Option<Shading>,
    pub tabs: Option<Arc<TabStops>>,     // включая «очищенные» позиции / incl. cleared stops
    pub numbering: Option<NumRef>,       // num_id + ilvl; num_id == 0 — «нет списка» явно
    pub bidi: Option<bool>, pub text_direction: Option<TextDirection>,
    pub frame: Option<Arc<FrameProps>>,  // рамки .doc / legacy frames
    pub cjk: Option<CjkParaProps>,       // kinsoku, overflow_punct, top_line_punct, auto_space_*, snap_to_grid
    pub mirror_indents: Option<bool>, pub text_alignment: Option<TextAlignment>, // baseline/center/…
    pub div_id: Option<u32>,             // HTML-импорт / html div
    pub ext: Option<Arc<ExtensionBag>>,
}

pub struct Style {
    pub id: StyleId,                     // интернированный styleId (как в docx: "Heading1")
    pub name: String,                    // отображаемое имя, локализуется по таблице Word RU/EN
    pub kind: StyleKind,                 // Paragraph | Character | Linked | Table | Numbering
    pub based_on: Option<StyleId>, pub next: Option<StyleId>, pub link: Option<StyleId>,
    pub para: ParaPropsId, pub chars: CharPropsId,
    pub table: Option<Arc<TableStyle>>,  // базовые свойства + условные форматы / conditional formats
    pub numbering: Option<NumberingId>,  // для StyleKind::Numbering
    pub ui_priority: u16, pub hidden: bool, pub semi_hidden: bool, pub unhide_when_used: bool,
    pub quick_style: bool, pub locked: bool, pub auto_redefine: bool, pub custom: bool,
}

pub struct StyleSheet {
    pub styles: IndexMap<StyleId, Style>,
    pub doc_defaults: (ParaPropsId, CharPropsId),   // w:docDefaults
    pub latent: LatentStyles,
    cache: ResolveCache,                 // (para_style, char_style, direct ids…) → ResolvedProps
}

Порядок разрешения (совпадает с OOXML §17.7.2, что гарантирует одинаковый результат с Word):

docDefaults → стиль таблицы (базовый, затем условный по положению ячейки) → свойства уровня нумерации
(pPr/rPr уровня) → цепочка стилей абзаца (от корня basedOn к листу) → цепочка стилей символов →
прямые свойства абзаца → свойства знака абзаца (для ¶) / прямые свойства run.
pub struct ResolveCtx<'a> { sheet: &'a StyleSheet, numbering: &'a NumberingDefs, table: Option<TableCtx> }

impl StyleSheet {
    /// Разрешить свойства символов для run. / Resolve effective character properties for a run.
    pub fn resolve_char(&self, cx: &ResolveCtx, para: &Paragraph, run_props: CharPropsId) -> &ResolvedChar {
        let key = (cx.table_key(), para.style, para.props, run_props);
        self.cache.get_or_insert(key, || {
            let mut acc = ResolvedChar::from(self.doc_defaults.1);
            if let Some(t) = cx.table_style_chain() { for s in t { acc.apply_char(s.chars) } }
            if let Some(num) = para_num_level(cx, para) { acc.apply_char(num.chars) }
            let pchain = self.chain(para.style);        // корень → лист / root → leaf
            let mut toggles = ToggleAcc::default();      // XOR-аккумулятор toggle-свойств
            for s in &pchain { acc.apply_char_with_toggles(s.chars, &mut toggles) }
            let rp = cx.interner.chars(run_props);
            if let Some(cs) = rp.style { for s in self.chain(cs) { acc.apply_char_with_toggles(s.chars, &mut toggles) } }
            acc.apply_direct_char(rp, &toggles);         // прямое: toggle задаёт абсолютное значение
            acc.finish(cx)                               // шрифт по письменности, размер cs, язык по умолчанию
        })
    }
}

Toggle-свойства (bold, italic, caps, small_caps, strike, dstrike, outline, shadow, emboss, imprint, vanish): внутри цепочки стилей значение XOR-ится (стиль символов «полужирный» на абзаце «Заголовок» с полужирным даёт обычный текст — так делает Word), прямое форматирование — абсолютно. Кэш разрешения инвалидируется при изменении любого стиля или docDefaults (версия StyleSheet.generation).

4.5 Нумерация

pub struct NumberingDefs {
    pub abstract_nums: IndexMap<AbstractNumId, AbstractNum>,
    pub instances: IndexMap<NumId, NumInstance>,
}
pub struct AbstractNum {
    pub id: AbstractNumId,
    pub multi_level: MultiLevelType,     // Single | Multilevel | HybridMultilevel
    pub levels: [Level; 9],
    pub style_link: Option<StyleId>,     // этот abstractNum определяет стиль списка / defines list style
    pub num_style_link: Option<StyleId>, // ссылается на стиль списка / references list style
    pub name: Option<String>,            // имя списка в UI / list name shown in UI
}
pub struct Level {
    pub start: i32,
    pub fmt: NumFmt,                     // Decimal | UpperRoman | LowerRoman | UpperLetter | LowerLetter | Ordinal |
                                         // CardinalText | OrdinalText | Bullet | DecimalZero | RussianLower | RussianUpper |
                                         // Chicago | IdeographDigital | JapaneseCounting | … | Custom(String) | None
    pub text: LevelText,                 // шаблон "%1.%2." → [Lit("…"), LevelRef(0), Lit("."), …]
    pub justify: Alignment,              // выравнивание номера / number alignment
    pub para: ParaPropsId,               // отступы/табуляции уровня / level indents & tabs
    pub chars: CharPropsId,              // шрифт номера/маркера / number run props
    pub suffix: LevelSuffix,             // Tab | Space | Nothing
    pub restart_after: Option<u8>,       // перезапуск после уровня (lvlRestart; 0 = никогда) / restart after level
    pub legal: bool,                     // isLgl
    pub pic_bullet: Option<MediaId>,
    pub tentative: bool,
}
pub struct NumInstance {
    pub id: NumId,
    pub abstract_id: AbstractNumId,
    pub overrides: [Option<LevelOverride>; 9],   // startOverride и/или полная замена уровня / level override
}
pub struct LevelOverride { pub start: Option<i32>, pub level: Option<Box<Level>> }
pub struct NumRef { pub num_id: NumId, pub ilvl: u8 }

Результат вычисления (5.21) кэшируется в ParaCache.list_label: Option<ListLabel { text, counters: [i32;9], props }>. «Список как объект» в UI — это все абзацы с одним num_id (или с abstract_id, если экземпляры отличаются только start), собираемые индексом NumberingIndex при обходе.

4.6 Раздел и страница

pub struct SectionProps {
    pub break_kind: SectionBreak,        // NextPage | Continuous | EvenPage | OddPage | NextColumn
    pub page: PageSize,                  // width, height (twips), orientation, paper code
    pub margins: PageMargins,            // top, bottom, left, right, header, footer, gutter, gutter_at_top
    pub columns: Columns,                // count, space, equal_width, cols: Vec<(width, space)>, separator
    pub title_page: bool,                // особый колонтитул первой / different first page
    pub page_numbering: PageNumbering,   // fmt, start: Option<i32>, chapter_style: Option<u8>, chapter_sep
    pub line_numbering: Option<LineNumbering>, // count_by, start, distance, restart: NewPage|NewSection|Continuous
    pub vertical_align: VAlign,          // Top | Center | Both | Bottom
    pub text_direction: TextDirection,   // LrTb | TbRl | BtLr | LrTbV | TbRlV | TbLrV
    pub bidi: bool, pub rtl_gutter: bool,
    pub footnote_props: NoteProps,       // pos, num_fmt, num_start, num_restart, columns
    pub endnote_props: NoteProps,
    pub doc_grid: DocGrid,               // Default | Lines | LinesAndChars | SnapToChars; line_pitch, char_space
    pub borders: Option<Arc<PageBorders>>,
    pub paper_source: Option<PaperSource>,
    pub protection: Option<SectionProtection>, // form protection
    pub ext: Option<Arc<ExtensionBag>>,
}
pub struct HeaderFooterSet { pub default: Option<BlockList>, pub first: Option<BlockList>, pub even: Option<BlockList> }
// Отсутствующий колонтитул = «как в предыдущем разделе» (linkToPrevious) / missing = link to previous.

Документ в целом: DocSettings { even_odd_headers: bool, mirror_margins: bool, compat: CompatMode { word_version: Option<u16>, flags: BTreeSet<CompatFlag> }, default_tab_stop: Twips, hyphenation: HyphenationSettings, view: ViewSettings, track_revisions: bool, protection: Option<DocProtection>, mail_merge: Option<MailMergeSettings>, footnote_props_default, endnote_props_default, update_fields_on_open: bool, auto_update_fields: bool (наше), typography: TypographyMode { WordCompatible | Meridian } }.

4.7 Плавающие объекты и якоря

pub struct DrawingObject {
    pub id: ObjectId,
    pub kind: DrawingKind,               // Picture(PictureProps) | Shape(SceneNodeId) | Group(Vec<ObjectId>) |
                                         // TextBox(TextBoxProps + BlockList) | Chart(ChartId) | Math(MathId) |
                                         // Canvas(Vec<ObjectId>) | Ole(OleRef) | Sheet(EmbeddedSheet)
    pub size: Size,                      // EMU / extent
    pub transform: Transform2D,          // поворот, отражение / rotation, flip
    pub placement: Placement,            // Inline(InlineProps) | Anchored(AnchorProps)
    pub alt: AltText,                    // title, description, decorative
    pub name: String, pub hidden: bool, pub locked: bool,
    pub hyperlink: Option<Hyperlink>,
    pub style: ShapeStyle,               // заливка, контур, эффекты (общие с Draw) / fill, line, effects
    pub ext: Option<Arc<ExtensionBag>>,
}
pub struct AnchorProps {
    pub h: HPos,                         // rel: Margin|Page|Column|Character|LeftMargin|RightMargin|InsideMargin|OutsideMargin
                                         // + Align(Left|Center|Right|Inside|Outside) | Offset(Emu) | PctOffset(f32)
    pub v: VPos,                         // rel: Margin|Page|Paragraph|Line|TopMargin|BottomMargin|Inside|Outside
                                         // + Align(Top|Center|Bottom|Inside|Outside) | Offset(Emu) | PctOffset
    pub wrap: Wrap,                      // None{behind: bool} | Square{sides} | Tight{polygon, sides} |
                                         // Through{polygon, sides} | TopAndBottom
    pub dist: Insets,                    // расстояние до текста / distance from text (EMU)
    pub z: i32, pub behind_text: bool, pub allow_overlap: bool, pub layout_in_cell: bool,
    pub locked: bool,                    // зафиксировать якорь / lock anchor
    pub move_with_text: bool,            // v.rel == Paragraph|Line ⇒ true
    pub rel_size: Option<RelSize>,       // ширина/высота в % от поля/страницы / relative size
    pub simple_pos: Option<Point>,       // старые документы / legacy
}

Якорь — маркер Marker::AnchoredObject в абзаце; при удалении абзаца объект удаляется (как в Word); при перетаскивании объекта якорь переносится к ближайшему абзацу на новой странице (если не locked).

4.8 Поля

pub struct Field {
    pub id: FieldId,
    pub code: FieldCode,                 // разобранный код / parsed instruction
    pub instr_text: String,              // исходная строка кода (для round-trip и неизвестных полей)
    pub switches: Switches,              // \* \# \@ \! \h и специфичные / general & field-specific switches
    pub para: ParaId,                    // абзац маркера FieldStart (поля не пересекают абзацы, кроме
                                         // устаревших — тогда хранится цепочка маркеров по абзацам)
    pub dirty: bool, pub locked: bool,   // требует обновления / заблокировано
    pub nested_in: Option<FieldId>,
    pub kind: FieldKind,                 // Simple (w:fldSimple) | Complex (маркеры) — влияет только на запись docx
    pub cached_result: Option<String>,   // для полей без Separate (fldSimple) и для сравнения
}
pub enum FieldCode {
    Page, NumPages, SectionPages, Section, Date(DateFmt), Time(DateFmt), CreateDate, SaveDate, PrintDate, EditTime,
    Seq { ident: String, bookmark: Option<String>, reset_level: Option<u8>, reset_to: Option<i32>, hide: bool, next: bool },
    Ref { bookmark: String, hyperlink: bool, number: RefNumber, above_below: bool, suppress_nondelim: bool },
    PageRef { bookmark: String, hyperlink: bool, above_below: bool },
    NoteRef { bookmark: String, hyperlink: bool, footnote_fmt: bool, above_below: bool },
    StyleRef { style: String, number: RefNumber, from_bottom: bool, raw_number: bool },
    Toc(TocOptions), Index(IndexOptions), Tc { text: String, level: u8, omit_page: bool, ident: Option<String> },
    Xe { entry: String, sub: Vec<String>, see: Option<String>, range_bookmark: Option<String>, bold: bool, italic: bool },
    Hyperlink { target: String, location: Option<String>, tooltip: Option<String>, frame: Option<String>, new_window: bool },
    MergeField { name: String, before: Option<String>, after: Option<String>, mapped: bool, vertical: bool },
    MergeRec, MergeSeq, Next, NextIf(Compare), SkipIf(Compare), If { cond: Compare, then: Expr, else_: Expr },
    Set { bookmark: String, value: Expr }, Ask { bookmark: String, prompt: String, default: Option<String>, once: bool },
    FillIn { prompt: String, default: Option<String>, once: bool }, Compare(Compare),
    Formula(FormulaAst),                 // = SUM(ABOVE) * 2
    DocProperty(String), Author, Title, Subject, Keywords, Comments, Template, FileName { with_path: bool },
    FileSize { kb: bool, mb: bool }, NumChars, NumWords, UserName, UserInitials, UserAddress, LastSavedBy, RevNum,
    Citation { tag: String, locator: Option<String>, prefix: Option<String>, suffix: Option<String>, suppress: SuppressFlags },
    Bibliography { style: Option<String>, filter_lang: Option<u32> },
    Symbol { code: u32, font: Option<String>, size: Option<HalfPoints> }, Quote(Expr), IncludeText(IncludeOpts),
    IncludePicture { path: String, data: bool }, Link(LinkOpts), Embed(EmbedOpts), MacroButton { script: String, label: String },
    ListNum { name: Option<String>, level: Option<u8>, start: Option<i32> }, AutoNum(AutoNumKind), AddressBlock(AddrOpts),
    GreetingLine(GreetOpts), Database(DbOpts), Print(String), GotoButton { target: String, label: String },
    FormText(FormTextOpts), FormCheckBox(FormCheckOpts), FormDropDown(FormDropOpts), Advance(AdvanceOpts), Eq(String),
    Barcode(BarcodeOpts), Rd { path: String, relative: bool }, Ta(TaOpts), Toa(ToaOpts),
    Unknown { name: String },
}
pub struct FieldIndex {
    pub by_id: HashMap<FieldId, Field>,
    pub by_kind: HashMap<Discriminant<FieldCode>, Vec<FieldId>>,
    pub deps: DepGraph<FieldId, Dep>,    // ребра: поле → источник (закладка, стиль, SEQ-счётчик, страница) / 5.20
}

Результат поля — обычный текст между FieldSeparate и FieldEnd, с форматированием (при \* MERGEFORMAT — сохраняется при обновлении посимвольно по старым спанам; при \* CHARFORMAT — формат первого символа кода).

4.9 Исправления (правки, track changes)

Правки — слой спанов rev: SpanList<RevSpan> в абзаце и ссылки RevId у блоков. Текст с пометкой «удалено» остаётся в text и участвует в поиске/раскладке по режиму отображения; принятие удаляет его физически, отклонение снимает метку.

#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct RevSpan {
    pub ins: Option<RevId>,              // вставлено в исправлении / inserted by revision
    pub del: Option<RevId>,              // удалено в исправлении / deleted by revision
    pub moved_from: Option<RevId>, pub moved_to: Option<RevId>, // перемещение (пара с общим move_id)
    pub fmt: Option<RevId>,              // изменение формата (старые свойства — в RevisionStore) / format change
}
pub struct Revision {
    pub id: RevId, pub author: AuthorId, pub date: Timestamp,
    pub kind: RevKind,
}
pub enum RevKind {
    Insert, Delete, MoveFrom { move_id: MoveId }, MoveTo { move_id: MoveId },
    CharFormat { old: CharPropsId }, ParaFormat { old: ParaPropsId, old_style: StyleId },
    ParaMarkInsert, ParaMarkDelete,      // вставка/удаление ¶ (слияние абзацев) / paragraph mark ins/del
    SectionProps { old: Box<SectionProps> }, TableProps { old: TablePropsId }, RowProps, CellProps,
    RowInsert, RowDelete, CellInsert, CellDelete, CellMerge { old_grid: Vec<Twips> },
    NumberingChange { old: Option<NumRef> }, ObjectInsert, ObjectDelete, ObjectProps { old: Arc<AnchorProps> },
}
pub struct RevisionStore { pub revisions: HashMap<RevId, Revision>, pub authors: IndexMap<AuthorId, Author>, pub moves: HashMap<MoveId, (RevId, RevId)> }

У абзаца: Paragraph.mark_rev: Option<RevId> (вставлен/удалён знак абзаца), Paragraph.props_rev. Принять/отклонить — операции Op::AcceptRevision/Op::RejectRevision, которые раскрываются в примитивные операции (удаление текста, снятие метки, восстановление old), поэтому они отменяемы и попадают в CRDT.

4.10 Закладки и комментарии

pub struct Anchor { pub pos: Position, pub gravity: Gravity } // Gravity::Left | Right: куда «тянется» при вставке в точку
pub struct Bookmark {
    pub id: BookmarkId, pub name: String,
    pub start: Anchor, pub end: Anchor,  // start == end — точечная закладка / point bookmark
    pub hidden: bool,                    // _Toc12345 и подобные / auto-generated
    pub table_cols: Option<(u16, u16)>,  // закладка на столбцы таблицы / column bookmark (w:colFirst/colLast)
}
pub struct Comment {
    pub id: CommentId, pub author: AuthorId, pub initials: String, pub date: Timestamp,
    pub range: (Anchor, Anchor),         // диапазон; для «точечного» — ref-маркер / range or point
    pub body: BlockList,                 // форматированный текст, рисунки / rich body
    pub parent: Option<CommentId>,       // ответ в ветке / reply
    pub resolved: bool,                  // w15:done
    pub durable_id: u32,                 // w16cid для round-trip
}

Анкеры хранятся в Paragraph.anchors: AnchorList (отсортированный вектор (offset, AnchorRef)), чтобы операции над абзацем обновляли их за O(k) без обращения к глобальным хранилищам; BookmarkStore и CommentStore держат карты id → (ParaId start, ParaId end) для навигации.

4.11 Операции, транзакции, undo и CRDT

pub enum Op {
    // текст / text
    InsertText { para: ParaId, at: u32, text: Arc<str>, props: CharPropsId, rev: RevSpan },
    DeleteText { para: ParaId, range: Range<u32>, removed: Arc<Fragment> }, // Fragment: text + все слои + маркеры + анкеры
    SetCharSpans { para: ParaId, range: Range<u32>, patch: CharPatch, prev: Arc<[(Range<u32>, CharPropsId)]> },
    SetRevSpans { para: ParaId, range: Range<u32>, new: RevSpan, prev: Arc<[(Range<u32>, RevSpan)]> },
    // абзац / paragraph
    SetParaProps { para: ParaId, new: ParaPropsId, prev: ParaPropsId },
    SetParaStyle { para: ParaId, new: StyleId, prev: StyleId },
    SplitParagraph { para: ParaId, at: u32, new_para: ParaId, inherit: SplitInherit },
    MergeParagraphs { first: ParaId, second: ParaId, removed_second: Arc<ParaShell> },
    // блоки / blocks
    InsertBlock { parent: ContainerRef, index: u32, block: Arc<Block> },
    RemoveBlock { parent: ContainerRef, index: u32, block: Arc<Block> },
    MoveBlocks { from: (ContainerRef, Range<u32>), to: (ContainerRef, u32) },
    // таблицы / tables
    InsertRow { table: TableId, index: u16, row: Arc<TableRow> }, RemoveRow { .. },
    InsertColumn { table: TableId, grid_index: u16, width: Twips, cells: Arc<[TableCell]> }, RemoveColumn { .. },
    MergeCells { table: TableId, rect: CellRect, prev: Arc<CellsSnapshot> }, SplitCell { .. },
    SetTableProps { .. }, SetRowProps { .. }, SetCellProps { .. }, SetTableGrid { table: TableId, new: Arc<[Twips]>, prev: Arc<[Twips]> },
    // разделы, объекты, поля, заметки, закладки, комментарии / sections, objects, fields, notes, bookmarks, comments
    SetSectionProps { section: NodeId, new: Arc<SectionProps>, prev: Arc<SectionProps> },
    InsertSection { index: u16, section: Arc<Section> }, RemoveSection { .. },
    SetHeaderFooter { section: NodeId, slot: HfSlot, new: Option<Arc<BlockList>>, prev: Option<Arc<BlockList>> },
    InsertObject { object: Arc<DrawingObject> }, RemoveObject { .. }, SetObjectProps { id: ObjectId, patch: ObjectPatch, prev: Arc<ObjectPatch> },
    SetField { id: FieldId, new: Arc<Field>, prev: Option<Arc<Field>> }, RemoveField { .. },
    InsertNote { id: NoteId, body: Arc<BlockList>, kind: NoteKind }, RemoveNote { .. },
    SetBookmark { id: BookmarkId, new: Option<Arc<Bookmark>>, prev: Option<Arc<Bookmark>> },
    SetComment { id: CommentId, new: Option<Arc<Comment>>, prev: Option<Arc<Comment>> },
    // стили, нумерация, настройки / styles, numbering, settings
    SetStyle { id: StyleId, new: Option<Arc<Style>>, prev: Option<Arc<Style>> },
    SetDocDefaults { .. }, SetNumbering { id: NumId, new: Option<Arc<NumInstance>>, prev: .. }, SetAbstractNum { .. },
    SetSettings { patch: SettingsPatch, prev: SettingsPatch }, SetMeta { .. },
    // составные (раскрываются в примитивы при применении, хранятся для метки undo) / composite
    AcceptRevision { id: RevId }, RejectRevision { id: RevId },
}

pub struct Transaction {
    pub id: TxId, pub label: Cow<'static, str>,   // «Ввод текста», «Вставка таблицы» / undo label
    pub ops: Vec<Op>, pub time: Timestamp, pub author: AuthorId,
    pub coalesce: Option<CoalesceKey>,            // (para, kind) — слияние набора текста / typing coalescing
    pub selection_before: SelectionSnapshot, pub selection_after: SelectionSnapshot,
}

pub trait Apply { fn apply(&mut self, op: &Op) -> Result<(), ModelError>; fn invert(op: &Op) -> Op; }

Правила:

4.12 Главный документ и вложенные

pub struct SubDocument { pub id: NodeId, pub path: RelPath, pub locked: bool, pub expanded: bool,
                         pub range: (ParaId, ParaId), pub last_hash: Blake3Hash }

Вложенный документ открывается как отдельный Document в том же процессе; его блоки отображаются в главном через прокси-раздел (стили разрешаются по таблице главного документа, конфликты имён — по правилу Word: главный побеждает). Правки во вложенном через главный — транзакции пробрасываются в его OpLog и сохраняются в его файл; главный хранит только ссылки (subdoc/ в manifest.xml, RD-поля для сквозных указателей).

4.13 Сериализация в .mwx (кратко; схема — 04-file-formats.md)

content.xml (body: разделы, абзацы <p>, <r> с @cp ссылкой на интернированные наборы свойств в styles.xml/props, маркеры как элементы), styles.xml (стили, docDefaults, нумерация, тема), settings.xml, meta.xml, comments.xml, notes.xml, fields.xml (индекс и кэш результатов), bookmarks.xml, media/*, objects/* (.mvx фигуры, .msx листы, .meq формулы), history/ops-*.cbor (журнал транзакций CBOR, сжатый zstd), history/snapshots/*.xml, thumbnail.png, scripts/*.ts + scripts/manifest.json, signatures/*.xml. Плоский .mwxf — те же части, вложенные в один корневой элемент, медиа — base64.


5. Алгоритмы и скрипты (крейты text-layout, fields, search, lingo, diff)

Единицы: модель — twips (1/20 pt, как OOXML); раскладка — Lu(i32) = 1/64 pt (26.6 fixed point): детерминированный результат на всех ОС и в WASM, без накопления ошибок float; глифовые advance из HarfBuzz (font units) переводятся в Lu один раз при измерении. Все алгоритмы однопоточны по разделу, параллельны между разделами и фоновыми задачами (правописание, поля, миниатюры).

5.1 Конвейер раскладки — обзор

Paragraph(text, runs, markers) + ResolvedProps
  │ 5.2 итемизация: по шрифту/письменности/направлению/языку/свойствам → Vec<Item>
  │ 5.3 BiDi (UAX #9): уровни вложенности на абзац → Item.level
  │ 5.4 шейпинг (HarfBuzz) каждого Item → ShapedRun { glyphs, clusters, advances }      (кэш 5.15)
  │ 5.5 измерение: advance в Lu, кернинг/разрядка/масштаб, табуляции-заглушки, inline-объекты
  │ 5.6 разбиение на строки: кандидаты (UAX #14 + переносы 5.7 + табуляции 5.8) → жадный | Кнут–Пласс
  │ 5.9 высота строки (метрики Word) → LineBox { x, y, w, h, baseline, visual runs (reordered) }
  ▼
ParagraphLayout { lines, height, floats: Vec<ObjectId>, break_opportunities }
  │ 5.10 поток: абзац → колонка → страница; keep/widow/break; 5.11 плавающие и обтекание;
  │ 5.12 таблицы; 5.13 сноски; колонтитулы; нумерация строк
  ▼
PageLayout { columns: [ColumnLayout { fragments: [Fragment] }], floats, header, footer, footnotes }
  │ Display list (Meridian Render): текст (глифы), линии, заливки, изображения → экран / печать / PDF

Ключевые структуры:

pub struct Item { pub range: Range<u32>, pub font: FontId, pub script: Script, pub lang: Lang,
                  pub level: u8, pub props: ResolvedCharId, pub kind: ItemKind } // Text | Tab | Object | Marker
pub struct ShapedRun { pub item: u32, pub glyphs: Vec<Glyph>, pub clusters: Vec<u32> /* glyph→char */,
                       pub advances: Vec<Lu>, pub offsets: Vec<(Lu, Lu)>, pub width: Lu, pub rtl: bool }
pub struct LineBox { pub y: Lu, pub height: Lu, pub ascent: Lu, pub x: Lu, pub width: Lu,
                     pub text_range: Range<u32>, pub visual: Vec<VisualRun>, pub justify_extra: Lu,
                     pub segment: u8 /* индекс сегмента обтекания */, pub trailing_ws: Lu }
pub struct ParagraphLayout { pub para: ParaId, pub width: Lu, pub lines: Vec<LineBox>, pub height: Lu,
                             pub label: Option<LabelBox>, pub fingerprint: u64 }

5.2 Итемизация

/// Разбить абзац на участки с постоянными (шрифт, письменность, направление, язык, свойства).
/// Split paragraph into runs of constant (font, script, direction, language, props).
fn itemize(p: &Paragraph, cx: &LayoutCtx) -> Vec<Item> {
    let text = p.text.as_str();
    let levels = bidi_levels(text, para_direction(p, cx));                  // 5.3
    let mut out = Vec::new();
    for (range, props_id) in p.runs.iter(0..p.text.len()) {                 // спаны прямого форматирования
        let props = cx.styles.resolve_char(cx, p, props_id);
        // Письменность: UAX #24 + разрешение Common/Inherited по соседям / script resolution
        for (srange, script) in script_runs(&text[range.clone()]) {
            // Выбор шрифта по письменности как в Word: ascii / hAnsi / eastAsia / cs по правилам
            // «hint», с резервом через fontdb fallback (Noto) по покрытию символов
            for (frange, font) in font_runs(&text[srange.clone()], props.font_for(script), cx.fonts) {
                for (lrange, level) in level_runs(&levels[frange.clone()]) {
                    out.push(Item { range: lrange, font, script, lang: props.lang_for(script),
                                    level, props: props.id(), kind: ItemKind::Text });
                }
            }
        }
    }
    split_at_markers_and_tabs(&mut out, &p.markers, text);  // '\t', U+FFFC → отдельные Item
    out
}

Правила выбора шрифта по Word: символ относится к ascii (U+0000–U+007F), hAnsi (прочая латиница/кириллица), eastAsia (CJK, кана, хангыль; при hint=eastAsia — также U+00A1–U+00FF и др.) или cs (арабский, иврит, тайский, сирийский; cs=true принудительно); отсутствующие глифы → метрически совместимая подстановка (таблица fonts/substitutions.toml), затем fallback по покрытию. Сложность O(n) по символам.

5.3 BiDi (UAX #9)

unicode-bidi::BidiInfo::new(text, Some(base_level)); базовый уровень — из ParaProps.bidi (RTL=1) или автоопределение (первый сильный символ) при TextDirection::Auto (наш режим). Уровни хранятся на абзац; визуальный порядок применяется на строку после разбиения (reorder_line(levels[line_range]) — правила L1–L4: сброс уровней концевых пробелов и табуляций до базового, зеркальные скобки через Bidi_Mirrored). Цифры: CharProps.numeral (Context | Hindi | Arabic | System) переводит ASCII-цифры в арабско-индийские при шейпинге в RTL-контексте — как Word. Сложность O(n).

5.4 Шейпинг (HarfBuzz / rustybuzz)

fn shape(item: &Item, text: &str, cx: &LayoutCtx) -> Arc<ShapedRun> {
    let key = ShapeKey { font: item.font, size: props.size, script: item.script, lang: item.lang,
                         rtl: item.level & 1 == 1, features: props.ot_features, text_hash: hash(&text[item.range]) };
    cx.shape_cache.get_or_insert(key, || {
        let mut buf = rustybuzz::UnicodeBuffer::new();
        buf.push_str(&text[item.range.clone()]);
        buf.set_direction(if key.rtl { RTL } else { LTR }); buf.set_script(item.script); buf.set_language(item.lang);
        let features = word_features(&props);   // kern=1 если kern>0 и размер ≥ порога; liga по умолчанию как Word:
                                                 // 'liga' вкл для шрифтов с настройкой ligatures, 'clig' — по OpenType-опции
        let out = rustybuzz::shape(cx.fonts.face(item.font), &features, buf);
        Arc::new(ShapedRun::from_hb(out, item, scale_to_lu(props.size)))
    })
}

Особенности: разрядка (spacing) добавляется к advance каждого кластера после шейпинга (Word добавляет к каждому символу, включая последний); масштаб (scale) умножает advances и матрицу глифов; caps/small_caps — преобразование текста до шейпинга (small caps — уменьшенный размер ×0,8 при отсутствии smcp, как Word); vanish — пропуск при не-показе скрытого текста; верхний/нижний индекс — размер ×0,65 и смещение по метрикам шрифта (OS/2 ySuperscriptYOffset), как Word. Повторный шейпинг для переносов: при переносе внутри кластера (лигатуры) участок до/после дефиса шейпится заново (HarfBuzz не поддерживает разрыв лигатуры).

5.5 Измерение

Ширина кластера = Σ advances его глифов; табуляция и объекты — ширина определяется при разбиении (табуляция зависит от позиции). Для каждой строки хранится trailing_ws — ширина конечных пробелов (не входит в «занятую» ширину: Word позволяет пробелам «висеть» за правым полем). Высота/глубина глифов для exactly-обрезки и для рамок выделения берутся из метрик шрифта, не из глифов (как Word), кроме математических объектов.

5.6 Разбиение на строки

Кандидаты разрыва: UAX #14 (unicode-linebreak) с Word-поправками (compat-флаги): разрыв после «/» запрещён (кроме URL в Meridian-режиме), после дефиса — разрешён, перед закрывающей пунктуацией — запрещён, кинсоку для CJK (CjkParaProps), «не переносить» для no_break спанов, неразрывный пробел/дефис — запрет, мягкий перенос — кандидат с дефисом, разрывы строки (\u{000B}) — обязательные. Переносы (5.7) добавляют кандидаты внутри слов.

Жадный алгоритм (по умолчанию для документов из Word и для TypographyMode::WordCompatible):

fn break_lines_greedy(items: &[MeasuredItem], opps: &[BreakOpp], segs: &SegmentProvider, hy: &Hyphenator) -> Vec<LineBox> {
    let mut lines = vec![]; let mut start = 0; let mut y = Lu(0);
    while start < items.total_chars() {
        let seg = segs.segment_for(y, lines.len() == 0);       // 5.11: ширина строки с учётом обтекания
        let avail = seg.width - first_line_indent_if(lines.is_empty()) - right_indent;
        let mut last_ok: Option<BreakOpp> = None; let mut w = Lu(0);
        for opp in opps.iter_from(start) {
            // ширина до кандидата без концевых пробелов / width up to candidate, trailing whitespace excluded
            let (w_to, trailing) = measure(start..opp.pos, items);
            if w_to <= avail || last_ok.is_none() && opp.is_mandatory_fallback() { last_ok = Some(*opp); w = w_to; }
            if w_to > avail { break; }
            if opp.mandatory { break; }
        }
        let brk = match last_ok {
            Some(b) => b,
            None => emergency_break(start, avail, items)            // слово шире строки → разрыв по графемам (как Word)
        };
        if brk.hyphen { /* перешейпить хвост с '-' / reshape tail with hyphen */ }
        lines.push(make_line(start..brk.pos, seg, w, trailing_ws(start..brk.pos)));
        y += lines.last().height; start = brk.pos_after();       // пробелы после разрыва «съедаются» в trailing
    }
    lines
}

Правила Word для «лишних» пробелов: конечные пробелы строки не участвуют в проверке w_to <= avail, при выравнивании по ширине не растягиваются и рисуются за правым полем; при центрировании/вправо считаются с нулевой шириной; каретка в них ставится (Word показывает за полем). Последняя строка абзаца при Both — по левому краю; при Distribute — растягивается. Разрыв строки Shift+Enter в абзаце по ширине: в Word-режиме строка растягивается (известное поведение Word), в Meridian-режиме — нет (как Writer).

Кнут–Пласс (для TypographyMode::Meridian и при выравнивании по ширине):

struct Node { pos: u32, kind: Kind /* Box(w) | Glue(w, stretch, shrink) | Penalty(w, p, flagged) */ }

fn break_lines_kp(nodes: &[Node], widths: &dyn Fn(usize /*line idx*/) -> Lu, tol: f32, params: &KpParams) -> Vec<usize> {
    // Три прохода как в TeX: 1) без переносов, tol = 1; 2) с переносами, tol = params.tolerance (2..4);
    // 3) emergency_stretch. Активные узлы: список кандидатов начала строки с (demerits, fitness, line).
    let mut active = vec![Active { at: 0, line: 0, demerits: 0.0, fitness: Fit::Normal, prev: None }];
    for (i, n) in nodes.iter().enumerate() {
        if !n.is_legal_break() { continue; }
        let mut candidates: [Option<Active>; 4] = Default::default();
        active.retain(|a| {
            let (w, st, sh) = sum_between(nodes, a.at, i);
            let r = adjustment_ratio(w, st, sh, widths(a.line));          // (W − w)/stretch или /shrink
            if r < -1.0 || n.is_forced() { return false; }               // строка не влезает → узел мёртв
            if r <= tol {
                let badness = 100.0 * r.abs().powi(3);
                let d = demerits(badness, n.penalty(), a, n.flagged, params); // + штраф за два переноса подряд,
                                                                               // + за смену класса fitness
                let fit = Fit::from_ratio(r);
                if candidates[fit as usize].map_or(true, |c| d < c.demerits) {
                    candidates[fit as usize] = Some(Active { at: i, line: a.line + 1, demerits: d, fitness: fit, prev: Some(a.clone_ref()) });
                }
            }
            true
        });
        active.extend(candidates.into_iter().flatten());
        if active.is_empty() { active.push(emergency_restart(i)); }     // как TeX: «Overfull», ставим разрыв здесь
    }
    backtrace(active.iter().min_by(|a, b| a.demerits.total_cmp(&b.demerits)).unwrap())
}

Glue: пробел — (w, w/2, w/3), после точки — (w, w, w/3) при включённом «французском интервале» выкл.; между CJK-иероглифами — (0, w/8, 0); висячая пунктуация — отрицательная ширина последнего бокса. Ширины строк widths(i) — из провайдера сегментов (обтекание), поэтому К–П работает с «рваными» строками. Сложность O(n·a), a — активные узлы (обычно < 10) → абзац 1000 слов ≈ 0,3 мс. Результат может отличаться от Word по переносам — поэтому режим включается только для документов и шаблонов Meridian; при сохранении в docx предупреждаем (7.3).

5.7 Переносы

Крейт hyphenation с TeX-шаблонами (ru, en-US/GB, de, fr, uk, …, 70 языков) + словари исключений; параметры: hyphenation.zone (Word: расстояние от правого поля, в котором перенос не нужен — 0,63 см по умолчанию), min_left/min_right (Word: 2/2 для ru — ГОСТ рекомендует 2/3, задаём в русских шаблонах), max_consecutive (Word: без ограничения, по умолчанию 3 у нас в Meridian-режиме), hyphenate_caps (по умолчанию нет), мягкие переносы U+00AD — приоритетнее шаблонов, «запретить автоперенос» (абзац) и no_proof спаны. Кандидаты строятся лениво: только для слова, пересекающего правую границу (жадный) или для всех слов абзаца (К–П), с кэшем по (lang, word).

5.8 Табуляции

fn tab_advance(x: Lu, stops: &TabStops, default: Lu, avail: Lu, next_text_width: Lu, kind_override: Option<TabKind>) -> (Lu, TabFill) {
    // Позиции стопов — от левого края абзаца (Word: от левого отступа; при compat-флаге — от поля).
    match stops.next_after(x) {
        Some(stop) => match stop.kind {
            Left    => (stop.pos - x, stop.leader),
            Center  => (stop.pos - x - next_text_width / 2, stop.leader),
            Right   => (stop.pos - x - next_text_width, stop.leader),       // next_text_width — до следующей табуляции/конца
            Decimal => (stop.pos - x - width_before_decimal_sep(next_text), stop.leader),
            Bar     => (Lu(0), TabFill::Bar(stop.pos)),                      // черта, позиция не меняется
        },
        None => { let n = ((x / default) + 1) * default; if n > avail { (Lu(0), TabFill::None) /* перенос строки */ } else { (n - x, TabFill::None) } }
    }
}

Особенности Word: табуляции по умолчанию действуют только после последней пользовательской; табуляция за правым полем — Word разрешает текст до правого поля (compat); в нумерованных абзацах первая табуляция после номера — позиция текста уровня (Level.para.indent.left), если suffix == Tab.

5.9 Высота строки и интервалы

fn line_height(line: &LineRuns, sp: &ParaSpacing, grid: Option<&DocGrid>) -> (Lu /*height*/, Lu /*ascent*/) {
    // natural: max по run'ам (ascent_i, descent_i) с метриками «как Word» / Word-like font metrics
    let (asc, desc) = line.runs.iter().fold((Lu(0), Lu(0)), |(a, d), r| {
        let m = word_metrics(r.font, r.size);  // asc = usWinAscent, desc = usWinDescent (+ external leading:
                                               // max(0, hhea.asc + |hhea.desc| + hhea.lineGap − (winAsc + winDesc)) делится
                                               // как у GDI; при fsSelection.USE_TYPO_METRICS — typo-метрики)
        (a.max(m.ascent + r.raise), d.max(m.descent + m.ext_leading - r.raise))
    });
    let natural = asc + desc;
    let (h, a) = match sp.line_rule {
        LineRule::Auto    => { let h = natural * sp.line / 240; (h, asc + (h - natural) /* доп. интервал Word кладёт сверху */) },
        LineRule::AtLeast => if sp.line > natural { (sp.line, asc + (sp.line - natural)) } else { (natural, asc) },
        LineRule::Exact   => (sp.line, asc - (natural - sp.line).max(Lu(0)) * asc / natural.max(Lu(1))), // обрезка сверху/снизу пропорционально
    };
    match grid { Some(g) if g.snap_lines() => snap_to_pitch(h, a, g.line_pitch), _ => (h, a) }
}

Семантика множителя как в Word: line=240 — одинарный, 276 — 1,15, 360 — 1,5, 480 — двойной; доп. высота добавляется над текстом (базовая линия смещается вниз) — так располагается Word, и это видно при выделении. Интервалы между абзацами: after(prev) + before(cur) складываются; contextual_spacing при одинаковом стиле обнуляет оба; before подавляется в верхней части страницы/колонки при автоматическом переходе (кроме первого абзаца раздела и абзацев после явного разрыва — флаг doNotSuppressTopSpacing инвертирует); before_auto/after_auto = 14 pt (HTML-режим). Пустой абзац — высота по свойствам знака ¶.

5.10 Поток: абзац → колонка → страница

struct FlowState { page: u32, column: u8, y: Lu, col_rect: Rect, floats: FloatSet, fn_reserve: Lu,
                   pending_keep: Vec<BlockRef> /* цепочка keep_with_next */, line_no: u32, num_state: NumState }

fn flow_section(sec: &Section, st: &mut FlowState, out: &mut Pages, cache: &LayoutCache) -> Result<(), Cancelled> {
    for block in sec.blocks.iter() {
        match block {
            Block::Paragraph(p) => flow_paragraph(p, st, out, cache)?,
            Block::Table(t)     => flow_table(t, st, out, cache)?,          // 5.12
            Block::Sdt(s)       => flow_blocks(&s.blocks, st, out, cache)?,
        }
    }
    if sec.props.break_kind == Continuous { balance_columns(st, out); }        // как Word: уравнять колонки
    finish_section(st, out, sec.props.break_kind);                              // Even/OddPage → пустая страница
    Ok(())
}

fn flow_paragraph(p: &Paragraph, st: &mut FlowState, out: &mut Pages, cache: &LayoutCache) -> Result<(), Cancelled> {
    let props = resolve_para(p);
    if props.page_break_before || has_marker(p, Break::Page) && at_start(p) { new_page(st, out); }
    place_anchored_objects_of(p, st, out);                                      // 5.11, до раскладки строк
    let pl = cache.layout_paragraph(p, SegmentProvider::new(st), /*first_line_y*/ st.y)?;  // строки с учётом обтекания
    let space_before = if st.at_top_of_column() && st.auto_break_here && !first_in_section { Lu(0) } else { props.spacing.before };
    let mut lines = pl.lines.as_slice();
    // Разбиение абзаца по страницам с учётом keep_lines / widow_control / keep_next
    loop {
        let avail = st.col_rect.bottom() - st.y - st.fn_reserve;
        let fits = count_lines_that_fit(lines, avail - space_before, &st.footnotes_for(lines));
        let total = lines.len();
        let (take, action) = decide_split(fits, total, &props, st);
        match action {
            Split::All => { emit(lines, st); break; }
            Split::MoveWhole => { new_column_or_page(st, out); continue; }   // не влезает ни одна строка/keep_lines
            Split::Partial(n) => { emit(&lines[..n], st); lines = &lines[n..]; new_column_or_page(st, out); }
        }
    }
    if props.keep_next { st.pending_keep.push(p.into()); } else { st.pending_keep.clear(); }
    Ok(())
}

fn decide_split(fits: usize, total: usize, pr: &ResolvedPara, st: &FlowState) -> (usize, Split) {
    if fits >= total { return (total, Split::All); }
    if pr.keep_lines || fits == 0 { return (0, Split::MoveWhole); }
    let mut n = fits;
    if pr.widow_control { if n < 2 { return (0, Split::MoveWhole); } if total - n < 2 { n = total - 2; } if n < 2 { return (0, Split::MoveWhole); } }
    (n, Split::Partial(n))
}

keep_with_next: если абзац с keep_next оказался последним в колонке, а следующий не поместился — цепочка pending_keep переносится целиком на новую колонку (Word ограничивает цепочку страницей: если вся цепочка выше страницы — разрывает). Разрыв колонки — как страницы в пределах колонок; «со следующей страницы» — new_page. Сложность потока O(строк), раскладка абзаца O(n) (жадный) / O(n·a) (К–П).

5.11 Плавающие объекты и обтекание

struct FloatSet { items: Vec<PlacedFloat> }           // на страницу: прямоугольник/контур + wrap + z
struct PlacedFloat { id: ObjectId, bbox: Rect, wrap: Wrap, exclusion: Exclusion /* Rect | Polygon */ }

/// Свободные сегменты строки высотой [y0, y1) внутри колонки. / Free segments for a line band.
fn free_segments(col: &Rect, y0: Lu, y1: Lu, floats: &FloatSet, min_w: Lu) -> SmallVec<[Segment; 4]> {
    let mut blocked: SmallVec<[(Lu, Lu); 8]> = SmallVec::new();
    for f in &floats.items {
        if f.bbox.bottom <= y0 || f.bbox.top >= y1 { continue; }
        match (&f.wrap, &f.exclusion) {
            (Wrap::None { .. }, _) => continue,
            (Wrap::TopAndBottom, _) => return smallvec![],                       // строка пропускается, y → bbox.bottom
            (Wrap::Square { sides }, Exclusion::Rect(r)) => push_sides(&mut blocked, r.left, r.right, sides, col),
            (Wrap::Tight { sides, .. }, Exclusion::Polygon(poly)) => {
                // пересечение полосы [y0,y1) с полигоном: min/max x по всем рёбрам, попадающим в полосу
                let (l, r) = poly.x_extent_in_band(y0, y1);
                push_sides(&mut blocked, l - f.dist.left, r + f.dist.right, sides, col);
            }
            (Wrap::Through { sides, .. }, Exclusion::Polygon(poly)) => {
                for (l, r) in poly.x_intervals_in_band(y0, y1) { push_sides(&mut blocked, l, r, sides, col); } // дырки допустимы
            }
            _ => unreachable!(),
        }
    }
    blocked.sort_by_key(|b| b.0); merge_overlapping(&mut blocked);
    complement(col.left, col.right, &blocked).into_iter().filter(|s| s.width() >= min_w).collect()
}

push_sides реализует sides: Both | Left | Right | Largest: при Left блокируется всё справа от объекта и т.д. Полоса строки берётся по предполагаемой высоте (высота предыдущей строки или одинарный интервал абзаца); если фактическая высота строки больше и пересекает новый объект — строка пересчитывается с новой полосой (не более 2 итераций). min_w = 1 em текущего шрифта (Word пропускает слишком узкие сегменты). Строка с несколькими сегментами — несколько LineBox с одним y и segment 0..k; жадный алгоритм последовательно заполняет сегменты, К–П получает их как widths(i).

Размещение объекта:

place_anchored(obj, anchor_para, st):
  x = h_pos(obj.h, rel: page|margin|column|character(x якоря)|…)
  y = v_pos(obj.v, rel: page|margin|paragraph(st.y абзаца)|line(y строки якоря)|…)
  if obj.v.rel in {Paragraph, Line} and y + h > col.bottom and not obj.allow_overflow:
       перенести якорный абзац (и объект) на следующую колонку/страницу (Word: объект «тянет» абзац)
  if obj.v.rel in {Page, Margin} and y + h > page.bottom: прижать к низу (Word) 
  if not obj.allow_overlap: сдвинуть вниз, пока не пересекает другие объекты страницы (с таким же флагом)
  st.floats.push(PlacedFloat{ bbox, wrap, exclusion: contour_or_rect(obj) })

Контур для Tight/Through — из wrap_polygon объекта (DrawingML wp:wrapPolygon, 21600-координаты) или вычисляется из альфа-канала изображения/геометрии фигуры (трассировка → упрощение Дугласа–Пекера, ≤ 64 вершин). Объекты в колонтитулах участвуют в обтекании текста страницы (как Word). За текстом/перед текстом — не влияют. Якорь в таблице (layout_in_cell) — обтекание внутри ячейки.

5.12 Таблицы

Ширины столбцов (алгоритм Word ≈ CSS 2.1 automatic layout с правилами Word):

compute_columns(table, avail_width):
  for each cell: (min_c, max_c) = content widths (мин: самое длинное неразрывное слово/объект; макс: без переносов),
                 + cell margins + borders; вложенные таблицы — рекурсивно; fixed cell width → min=max=w
  for span=1 cells: col.min = max(min_c), col.max = max(max_c); preferred (из tcW/gridCol) учитывается как max
  for span>1 cells (по возрастанию span): если Σ col.min < min_c — распределить дефицит пропорционально max (Word: равномерно, compat)
  layout = table.props.layout
  if layout == Fixed: widths = grid (tblGrid/tcW); лишнее/нехватка — не корректируется (Word обрезает содержимое)
  else:  // AutoFit
     T = requested table width (абс / % от avail / auto)
     if T is auto: W = clamp(Σ max, Σ min, avail)
     else: W = T
     if Σ max <= W: widths = max + (W − Σ max) распределить пропорционально max (tblW задан) или не распределять (auto → таблица уже)
     elif Σ min <= W: widths = min + (W − Σ min) * (max − min) / Σ(max − min)      // Word сжимает пропорционально «гибкости»
     else: widths = min (таблица выходит за поле — как Word; в Meridian-режиме: равномерное сжатие с переносом по символам)
  вертикальные объединения не влияют на ширины; высота строки = max высоты ячеек (vMerge Continue — 0 в своей строке,
  общий блок проверяется после раскладки всех строк диапазона: если контента больше — увеличить последнюю строку)

Разбиение по страницам:

flow_table(t, st):
  header_rows = rows with row.props.header (только ведущие подряд; Word игнорирует иные)
  for row in rows:
    h = layout_row(row, widths)                       // ячейки — отдельные потоки блоков (рекурсивно 5.10)
    if st.y + h > bottom:
      if row.props.allow_break and h > line_min and not row.has_vmerge_restart_conflict:
         (top_part, rest) = split_row_at(row, bottom − st.y)   // каждая ячейка режется по границе строки текста
         emit(top_part); new_page(); emit(header_rows); row = rest; continue
      else:
         if h > page_body_height: // строка выше страницы: Word обрезает; Meridian-режим — принудительное разбиение
         new_page(); emit(header_rows)
    emit(row)
  плавающая таблица (tblpPr): как объект с обтеканием Square, остальное — как у 5.11

«Не отрывать от следующего» у абзацев в ячейках Word игнорирует для строк, мы — тоже (compat); «не разрывать абзац» внутри ячейки учитывается. Вложенные таблицы разбиваются по тем же правилам внутри родительской ячейки. Сложность: ширины O(cells), раскладка O(cells·text).

5.13 Сноски

place_line_with_notes(line, st):
  notes = footnote refs in line
  need = Σ (layout_note(n, width=col or page by NoteProps.columns).height) + (st.fn_reserve == 0 ? separator_h : 0)
  if st.y + line.h + need <= bottom − st.fn_reserve:
      st.fn_reserve += need; emit(line); st.page.footnotes.extend(notes); return Ok
  // не влезает: 1) попытаться разбить последнюю сноску (min 2 строки здесь, остальное — continuation на следующей)
  if can_split(notes.last, avail=bottom − st.fn_reserve − st.y − line.h, min_lines=2):
      split → первая часть здесь (+ «уведомление о продолжении» из разделителя), остаток в st.carry_notes
      emit(line); return Ok
  // 2) иначе — вся строка на следующую страницу вместе со своими сносками
  return NeedNewPage
new_page(): сначала размещаются st.carry_notes (продолжение) вверху области сносок

Концевые сноски собираются в EndnoteBlock и раскладываются после последнего абзаца раздела (или документа) как обычный поток с разделителем; нумерация — 5.21 (сквозная/по разделу/по странице — последняя требует знания страницы и вычисляется при потоке с перераскладкой ссылки при изменении ширины номера). Область сносок: ширина колонки (если columns==0 — по колонке, иначе — вся страница, с заданным числом колонок); положение pos=BeneathText — сразу под текстом вместо низа страницы.

5.14 Инкрементальная перераскладка

Состояние потока фиксируется в начале каждой колонки/страницы как Checkpoint:

#[derive(PartialEq, Eq, Hash, Clone)]
struct Checkpoint { first_block: BlockRef, first_line: u32 /* строка абзаца, если абзац продолжается */,
                    page_no: u32, column: u8, y: Lu, floats: FloatSetHash, fn_reserve: Lu, carry_notes: NotesHash,
                    pending_keep_len: u16, num_state: u64 /* хэш счётчиков 5.21 */, line_no: u32,
                    section_props: u64, page_fmt: u64 /* влияющие на колонтитулы поля */ }

fn relayout_after_edit(doc: &Document, dirty: &DirtySet, lay: &mut Layout) -> Result<RelayoutStats, Cancelled> {
    // 1. Точка старта: первая страница, содержащая самый ранний «грязный» блок, минус цепочки неразрывности
    let first_dirty = dirty.min_block();
    let start_page = lay.page_of(first_dirty).saturating_sub(backtrack_pages(first_dirty, lay)); // keep_next/висячие
    let mut st = FlowState::from_checkpoint(&lay.checkpoints[start_page]);
    let old_checkpoints = lay.checkpoints.split_off(start_page);   // старые границы для сравнения
    let mut pages = lay.pages.split_off(start_page);
    let mut new_pages = Pages::new();
    for block in doc.blocks_from(st.first_block) {
        flow_block(block, &mut st, &mut new_pages, &lay.cache)?;    // 5.10; абзацы из кэша, если не dirty и ширина/сегменты те же
        if let Some(cp) = st.checkpoint_if_boundary() {              // мы на границе страницы/колонки
            if !dirty.any_after(cp.first_block) {                     // дальше грязных блоков нет
                if let Some(k) = old_checkpoints.iter().position(|o| o.same_flow(&cp)) { // те же блок/строка/y/флоаты/сноски/счётчики
                    let delta = cp.page_no as i64 - old_checkpoints[k].page_no as i64;
                    reuse_pages(&mut new_pages, pages.drain(k..), delta);  // ранний выход: страницы переиспользуются с перенумерацией
                    break;
                }
            }
            lay.checkpoints.push(cp);
        }
    }
    lay.pages.extend(new_pages);
    invalidate_fields_if(delta != 0 || page_count_changed);           // PAGE/NUMPAGES/PAGEREF → 5.20 (отложенно)
    Ok(stats)
}

same_flow сравнивает всё, кроме page_no: совпадение означает, что остаток документа лёг бы точно так же (при delta ≠ 0 лишь номера страниц сдвигаются; колонтитулы с полем PAGE перерисовываются без перераскладки тела, если ширина номера не изменила их высоту — иначе страница помечается грязной). Для ввода символа внутри абзаца на странице k: перераскладка абзаца (кэш шейпинга по неизменённым спанам), затем поток до ближайшей границы — обычно та же страница → ранний выход на k+1. Худший случай (изменилось число строк на каждой странице, например смена размера шрифта стиля «Обычный») — полный проход, в фоне (5.16).

5.15 Кэш раскладки

Кэш Ключ Значение Инвалидация Размер
Шейпинг (font, size, script, lang, rtl, features, text_hash) Arc<ShapedRun> LRU по памяти (64 МБ) ~ 50 байт/глиф
Абзац (ParaId, text_hash, runs_hash, resolved_para_hash, width, segments_hash, first_line_y_class) Arc<ParagraphLayout> при правке абзаца, смене стилей (generation), ширины колонки все абзацы документа
Метрики шрифтов (font, size) ascent/descent/leading/x-height, ширины цифр никогда мал
Переносы (lang, word) позиции никогда (LRU 100k) мал
Страницы page_no PageLayout + display list + растровая миниатюра (для панели «Страницы», 160 px) 5.14 все страницы
Разрешение стилей (table_ctx, para_style, para_props, run_props) ResolvedChar/Para StyleSheet.generation тысячи записей

first_line_y_class — строка абзаца зависит от y только через сегменты обтекания; если на колонке нет плавающих объектов, класс = 0 и кэш переиспользуется при любом y.

5.16 Фоновая раскладка длинных документов

5.17 Hit-testing и каретка

pub struct Caret { pub pos: Position, pub affinity: Affinity /* Upstream | Downstream */, pub x_goal: Option<Lu> }

fn hit_test(lay: &Layout, pt: PointPage) -> Hit {
    let page = &lay.pages[pt.page];
    if let Some(obj) = page.floats.top_most_at(pt.xy) { return Hit::Object(obj); }      // по z, перед текстом
    let col = page.column_at(pt.x).unwrap_or(nearest);
    let frag = col.fragment_at(pt.y);                                                     // бинарный поиск по y
    match frag {
        Fragment::Para(pl, line_range) => {
            let line = pl.line_at_y(pt.y, line_range);                                     // ближайшая, если между
            let (seg, x) = line.segment_containing(pt.x);
            // визуальные run'ы слева направо; внутри run — кластеры по накопленным advance
            let vr = seg.visual_run_at(x).unwrap_or(edge_run(seg, x));
            let (cluster, frac) = vr.cluster_at(x);                                        // frac ∈ [0,1) внутри кластера
            let offset = if vr.rtl { if frac < 0.5 { cluster.end } else { cluster.start } }
                         else      { if frac < 0.5 { cluster.start } else { cluster.end } };
            // внутри лигатуры: Word делит ширину кластера на число символов
            let offset = ligature_subdivide(cluster, frac, offset);
            Hit::Text(Caret { pos: Position { para: pl.para, offset }, affinity: affinity_for(line, offset), x_goal: None })
        }
        Fragment::Table(t) => hit_test_table(t, pt),                                        // рекурсивно в ячейку
        Fragment::Note(n) => hit_test_note(n, pt),
        Fragment::Empty => caret_at_end_of_last_line(col, pt),
    }
}

/// Координаты каретки для логической позиции с учётом BiDi. / Caret x for logical position, BiDi-aware.
fn caret_rect(lay: &Layout, c: &Caret) -> CaretRect {
    let (pl, line) = lay.line_for(c.pos, c.affinity);   // affinity выбирает строку на границе переноса
    // позиция может быть на стыке run'ов разного направления → два кандидата; выбираем по affinity:
    // Downstream — край run'а, в который «входит» следующий символ; Upstream — предыдущего
    let vr = line.run_for_offset(c.pos.offset, c.affinity);
    let x = vr.x_at_offset(c.pos.offset);                 // для RTL: x = run.right − advance_to(offset)
    CaretRect { page, x, y: line.y, h: line.height, rtl: vr.rtl }   // rtl → каретка с «флажком» направления
}

Перемещение: ←/→ — логическое (по умолчанию, как в Word; опция «визуальное» переключает на обход визуального порядка), по графемным кластерам (ICU4X GraphemeClusterSegmenter); Ctrl+←/→ — по словам (UAX #29 word с правилами Word: пунктуация — отдельное «слово», пробелы прилипают к предыдущему); ↑/↓ — по x_goal в соседней строке (через таблицы — в ячейку по x; через объекты — пропуск); Home/End — начало/конец визуальной строки (для RTL-абзаца — зеркально). Каретка не ставится внутрь маркеров полей/объектов, перед FieldSeparate, внутри удалённого текста в режиме «без исправлений».

5.18 Выделение

pub enum Selection {
    Text { anchor: Position, focus: Position },                     // логический диапазон
    Block { anchor: PointPage, focus: PointPage },                   // прямоугольное (Alt)
    Cells { table: TableId, r0: u16, c0: u16, r1: u16, c1: u16 },   // ячейки
    Objects(SmallVec<[ObjectId; 4]>),
    Multi(Vec<Selection>),                                           // Ctrl-выделение
}
/// Визуальные прямоугольники выделения на странице. / Selection rectangles per page.
fn selection_rects(lay: &Layout, sel: &Selection, page: u32) -> Vec<Rect> {
    // для каждой строки, пересекающей [start, end): пересечь логический диапазон с run'ами строки,
    // для каждого run'а взять [x_at(max(start, run.start)), x_at(min(end, run.end))] — для RTL концы меняются местами;
    // объединить соседние; концевые пробелы и ¶ — выделяются (как Word), пустые строки — полоса ширины 1 em
}

Расширение выделения двойным кликом — слово (с «умным» захватом пробела), тройным — абзац; клик в поле выделения (левое поле) — строка, двойной — абзац, тройной — всё; Shift+клик — расширить; F8 — режим расширения (слово → предложение → абзац → раздел → документ). Выделение таблицы: протягивание через ячейки переключает Text → Cells; Cells, покрывающие все ячейки, трактуются как таблица.

5.19 IME-композиция

Qt присылает QInputMethodEvent { preedit, attributes, commit, replacement } → оболочка вызывает ядро:

pub struct Composition { pub base: Position, pub text: String, pub cursor: u32, pub segments: Vec<(Range<u32>, ImeStyle)> }
// Ядро не изменяет модель: композиция — оверлей контроллера документа; раскладка абзаца получает
// «виртуальную вставку» (текст + стиль подчёркивания) по base; хит-тест и каретка учитывают оверлей.
// Commit → одна Transaction { InsertText(commit) } с CoalesceKey набора; отмена → оверлей снимается.
// inputMethodQuery: SurroundingText (абзац до/после, ≤ 2 КиБ), CursorPosition, AnchorPosition, CursorRectangle
// (из caret_rect), Hints (без автозамены в полях паролей — не применимо). Реконверсия: выделение → preedit.

Автозамена и автоформат не срабатывают до commit; проверка правописания не трогает абзац с активной композицией.

5.20 Обновление полей и оглавления

enum Dep { Bookmark(BookmarkId), Style(StyleId), SeqCounter(String), Note(NoteId), Layout /* страницы */,
           DocProp(String), Field(FieldId) /* вложенность */, External(PathBuf), MergeSource, Now /* DATE/TIME */ }

fn update_fields(doc: &mut Document, lay: &Layout, scope: UpdateScope, reason: UpdateReason) -> UpdateReport {
    let graph = &doc.fields.deps;                       // ребра строятся при разборе кода поля
    let mut order = graph.topo_order(scope.fields());    // вложенные поля — после внутренних; TOC/INDEX — последними
    let mut iterations = 0;
    loop {
        let mut layout_changed = false;
        for f in &order {
            if !f.dirty(reason) || f.locked { continue; }
            let new_result = evaluate(doc, lay, f)?;                       // по FieldCode; Layout-зависимые читают lay
            if new_result != current_result(doc, f) {
                replace_result(doc, f, new_result, keep_format(f));         // \* MERGEFORMAT: спаны старого результата
                layout_changed |= f.affects_layout();                       // текст изменил длину → перераскладка
            }
        }
        if !layout_changed || iterations == 3 { break; }                   // Word обновляет дважды; нам до 3
        relayout_after_edit(doc, &dirty_from_fields, lay)?; iterations += 1;
        order.retain(|f| f.depends_on_layout());                           // повторно только PAGE/PAGEREF/NUMPAGES/TOC
    }
    report
}

Автообновление (наше отличие): после транзакции планировщик помечает поля, чьи зависимости затронуты (закладка сдвинулась — не повод; изменился текст внутри закладки/заголовка/подписи — повод), и через 2 с простоя (или перед сохранением/печатью/экспортом) запускает update_fields(scope = dirty); DATE/TIME — только по явному F9/печати (как Word, иначе документ «меняется сам»). TOC:

build_toc(opts): entries = заголовки по стилям (opts.styles → уровни) ∪ абзацы с outline_level (\u) ∪ TC-поля (\f)
  в порядке документа; для каждого: текст без номеров полей/сносок (с номером списка — \n не задан),
  уровень, скрытая закладка _TocNNN на абзац (создать, если нет);
  результат — абзацы стиля «Оглавление L»: [номер+текст][таб с заполнителем][PAGEREF _TocNNN \h]
  (\h → весь элемент — HYPERLINK), \n L-L' — без номеров для уровней, \z — скрыть номера в веб-виде;
  ручные правки внутри оглавления сохраняются, если пользователь выбрал «обновить только номера страниц».

INDEX — сбор XE (с группировкой по буквам, объединением страниц 3, 5–7, «см.» ссылками), SEQ — счётчики по идентификатору с \r, \s (сброс по уровню заголовка), REF к SEQ — номер подписи; формулы = — вычисление через calc-engine с таблицей ячеек текущей таблицы (A1, ABOVE/LEFT/BELOW/RIGHT) и закладками. MERGEFIELD — значения из текущей записи (5.25). Неизвестные/внешние (INCLUDETEXT) — по явному обновлению, с подтверждением доступа к файлу.

5.21 Нумерация списков

fn compute_numbering(doc: &Document) -> NumberingResult {
    let mut counters: HashMap<AbstractNumId, [i32; 9]> = HashMap::new();   // счётчики общие для экземпляров одного abstractNum
    let mut started: HashSet<(NumId, u8)> = HashSet::new();                 // startOverride применяется один раз
    let mut result = NumberingResult::default();
    for p in doc.paragraphs_in_order() {                                    // тело, затем колонтитулы/сноски/надписи отдельно
        let Some(nref) = resolve_para(p).numbering else { continue };
        if nref.num_id.0 == 0 { continue; }                                 // явное «нет списка»
        let inst = &doc.numbering.instances[&nref.num_id];
        let abs = &doc.numbering.abstract_nums[&inst.abstract_id];
        let lvl = nref.ilvl as usize;
        let level = inst.overrides[lvl].as_ref().and_then(|o| o.level.as_deref()).unwrap_or(&abs.levels[lvl]);
        if level.fmt == NumFmt::Bullet { result.set(p.id, bullet_label(level)); continue; }
        let c = counters.entry(inst.abstract_id).or_insert_with(|| abs.levels.map(|l| l.start - 1));
        // перезапуск по startOverride (один раз на экземпляр и уровень)
        if let Some(o) = &inst.overrides[lvl] { if let Some(s) = o.start { if started.insert((nref.num_id, lvl as u8)) { c[lvl] = s - 1; } } }
        c[lvl] += 1;
        // уровни глубже сбрасываются; уровни выше — не трогаем; restart_after: сброс, только если встретился уровень ≤ restart_after
        for deeper in lvl + 1..9 { c[deeper] = abs.levels[deeper].start - 1; }
        for upper in 0..lvl { if c[upper] < abs.levels[upper].start { c[upper] = abs.levels[upper].start; } } // Word: «невидимые» верхние = start
        let text = render_level_text(level, c, abs, lvl);                   // "%1.%2" → "2.3"; legal → все уровни decimal
        result.set(p.id, ListLabel { text, counters: *c, props: level.chars, suffix: level.suffix, indent: level.para });
    }
    result
}

Level.restart_after = Some(k): счётчик уровня сбрасывается только после появления абзаца уровня ≤ k (при 0 — никогда не сбрасывается). Ширина номера влияет на раскладку (табуляция после номера), поэтому нумерация вычисляется до раскладки инкрементально: при правке пересчитываются абзацы от первого изменённого абзаца списка до конца документа с ранним выходом при совпадении counters с кэшем (ParaCache.list_label). Сложность O(n) по абзацам, обычно ≪ раскладки.

5.22 Проверка правописания в фоне

fn spellcheck_worker(rx: Receiver<ParaId>, doc: DocSnapshot, lingo: &Lingo, cache: &WordCache) {
    for para_id in rx {                                         // очередь «грязных» абзацев, приоритет — видимые
        let p = doc.paragraph(para_id);
        if p.has_active_composition() { continue; }
        let mut errors = Vec::new();
        for (range, lang) in p.lang_spans() {                   // по разрешённому CharProps.lang (+ no_proof → пропуск)
            if lang.is_none() || lang.no_proof { continue; }
            for w in WordSegmenter::new(lang).segment(&p.text[range]) {    // UAX #29 (ICU4X) с правилами Lingo: дефисные слова целиком,
                                                                           // апострофы, URL/e-mail/числа/аббревиатуры с цифрами — пропуск
                if should_skip(w, &doc.settings.spelling) { continue; }    // ПРОПИСНЫЕ, слова с цифрами, повторы
                let ok = cache.get_or_insert((lang, w.text), || lingo.spell(lang, w.text) || doc.dictionaries.contains(w.text));
                if !ok { errors.push(SpellError { range: w.range, kind: Misspelled }); }
                if is_double_word(w, prev) { errors.push(SpellError { range, kind: Repeated }); }
            }
        }
        publish(para_id, p.text_hash, errors);                   // UI применяет, если хэш совпадает с актуальным
    }
}

Подсказки (suggest) — лениво, при открытии контекстного меню/панели (Hunspell suggest ≈ 5–50 мс на слово). Грамматика (P1): предложения (ICU4X SentenceSegmenter), контекст ±1 абзац, запрос к локальному LanguageTool (HTTP, batched, debounce 1 с), результаты — спаны с категорией и объяснением правила. Автозамена «при вводе» применяется в UI-потоке после ввода разделителя (пробел/пунктуация/Enter) к последнему слову — одна дополнительная операция в той же транзакции (отменяется отдельно Ctrl+Z). Полный прогон 500 стр (≈150 тыс. слов, попадание в кэш ≈ 90 %) — < 2 с на фоне.

5.23 Сравнение и объединение документов

compare(a: Document, b: Document, opts) -> Document (результат с исправлениями автора «Сравнение»):
 1. Линеаризация: последовательности «единиц верхнего уровня» для тела, каждого колонтитула, сносок, надписей:
    Unit = Para{hash(norm_text)} | TableRow{hash(cells)} | Object{hash(content)}; norm_text = NFC, без скрытого текста,
    без результатов полей (код поля вместо результата), по opts: casefold, схлопнуть пробелы.
 2. Diff абзацев: patience-diff по уникальным хэшам (якоря) + Myers O(ND) в промежутках (крейт `similar`-подобный свой).
    Равные → без изменений; удалённые/вставленные → попытка спаривания по сходству:
    sim = Jaccard по 3-граммам слов; пары с sim ≥ 0.5 (жадно по убыванию) → «изменённые», иначе Delete/Insert.
    Идентичные хэши среди удалённых и вставленных на расстоянии → MoveFrom/MoveTo (если opts.moves).
 3. Внутри изменённой пары: diff по словам (UAX #29, пунктуация — отдельные токены) Myers; при opts.level=Chars —
    по графемам внутри изменённых слов. Результат → RevSpan ins/del на тексте результата (берётся b с вставками a-удалений).
 4. Форматирование (opts.formatting): для равных слов сравнить ResolvedChar; различие → CharFormat{old} исправление;
    абзацные свойства/стиль → ParaFormat; свойства раздела → SectionProps.
 5. Таблицы: строки — как единицы (хэш), ячейки изменённых строк — рекурсивно по блокам; изменение сетки → TableProps.
 6. Комментарии (opts.comments): по durable_id/тексту; поля, сноски, колонтитулы — те же шаги в своих потоках.
Сложность: O((N+M)·D) по абзацам + O(слов·D') внутри пар; 500 стр × 10 % изменений ≈ 0,5–1,5 с.

merge(base, mine, theirs) -> Document: diff3 по абзацам (base↔mine, base↔theirs); неконфликтные — применяются,
конфликтные абзацы — обе версии как исправления разных авторов + комментарий «Конфликт объединения»;
исправления и комментарии из обоих документов сохраняются с исходными авторами (как Word «Объединить»).

5.24 Поиск и замена

pub struct Query { pub pattern: String, pub mode: Mode /* Plain | Wildcards | Regex */, pub case: bool, pub whole_word: bool,
                   pub diacritics: bool, pub width: bool /* CJK ширина */, pub kashida: bool, pub prefix: bool, pub suffix: bool,
                   pub format: Option<FormatPredicate>, pub scope: Scope, pub direction: Dir, pub ignore_ws: bool, pub ignore_punct: bool }

fn compile(q: &Query) -> Result<Matcher, QueryError> {
    let src = match q.mode {
        Mode::Plain     => regex::escape(&normalize(&q.pattern)),
        Mode::Wildcards => wildcards_to_regex(&q.pattern)?,   // таблица ниже
        Mode::Regex     => q.pattern.clone(),
    };
    let src = expand_special(&src);                           // ^p→\n (граница абзаца), ^t→\t, ^l→\x0B, ^s→\u{A0}, ^~→\u{2011},
                                                              // ^-→\u{AD}, ^#→\d, ^$→\p{L}, ^?→., ^w→\s+, ^g→\u{FFFC}(graphic), ^d→FIELD, ^f→NOTE, ^e→ENDNOTE
    let re = regex::RegexBuilder::new(&src).case_insensitive(!q.case).unicode(true).build()?;   // гарантированно линейное время
    Ok(Matcher { re, word: q.whole_word, fmt: q.format.clone(), fold: Fold::new(q) })
}
fn search(doc: &Document, m: &Matcher, scope: Scope) -> impl Iterator<Item = Match> {
    // по абзацам области; строка поиска — «складка» текста: casefold (ICU4X), снятие диакритики (NFD + strip Mn) если !diacritics,
    // полуширинные↔полноширинные если !width, с картой смещений fold→orig; маркеры → спецсимволы (U+FFFC и т.п.);
    // ^p соединяет соседние абзацы: окно из текущего + следующего абзаца с '\n' между ними, совпадения, пересекающие
    // границу, разрешены только при наличии ^p в шаблоне; whole_word — проверка границ UAX #29;
    // предикат формата — по спанам в найденном диапазоне (все символы удовлетворяют); результат — Match { range, groups }
}

Подстановочные знаки Word → regex: ?→., *→.*? (ленивый, как Word), [a-z]→[a-z], [!x]→[^x], {n},{n,},{n,m}→те же, @→+, <→\b(?=\w), >→(?<=\w)\b (через fancy-regex при необходимости), (…)→группы, \1→$1 в замене, ^13→\n. Замена: шаблон с $1/\1, ^& (найденное), ^c (буфер обмена, с форматом); форматирование результата из FormatPredicate замены; «Заменить всё» — одна транзакция, обход с конца абзаца, чтобы смещения не сдвигались. Предпросмотр — список «было → стало» без применения. Производительность: 500 стр, простой шаблон — ≈ 20 мс; regex — ≈ 50–100 мс; построчный индекс не нужен.

5.25 Слияние

mail_merge(doc, source: DataSource, opts) -> MergeOutput:
  rows = source.open(filter, sort)                       // Sheets: лист/диапазон → Vec<Record>; Base: запрос; CSV: кодировка/разделитель авто
  mapping = resolve_mapping(doc.fields.by_kind[MergeField], source.columns, doc.settings.mail_merge.map)   // ADDRESSBLOCK/GREETINGLINE
  outputs = []
  for (i, rec) in rows.enumerate():
      d = doc.clone_for_merge()                          // структурное копирование (Arc-шаринг медиа/стилей, O(абзацев))
      ctx = MergeCtx { rec, i, next_rec: rows.peek(), vars: {} }
      evaluate_merge_fields(d, ctx)                      // MERGEFIELD→значение, IF/COMPARE/SET/ASK/FILLIN, NEXT/NEXTIF/SKIPIF (управление записями)
      update_fields(d, scope=All)                        // формулы, даты, PAGE после раскладки
      strip_merge_markup(d) if opts.flatten              // поля → текст
      match opts.output:
          Document      => append_as_section(outputs.doc, d, break=NextPage)   // «каталог» — без разрыва
          Files{pattern} => save(d, format(pattern, rec))                      // «{Фамилия}_{№}.docx»
          Pdf{pattern}   => export_pdf(d, path)
          Print          => print(d)
          Email{to, subj, body_mode} => mail.send(rec[to], subj, d as pdf|html|body)
  return report (ошибки записей, пропущенные, итоги)

Выполняется фоновой задачей с прогрессом и отменой; 1000 писем по 2 страницы — ≈ 20–40 с в PDF.

5.26 Экспорт PDF

export_pdf(doc, lay, opts):
  w = PdfWriter(version=1.7 | PDF/A profile, compress=flate)
  fonts: для каждого использованного (font, glyph set) → подмножество (TrueType glyf / CFF charstrings), Type0 + CIDFontType2/0,
         Identity-H, CIDToGIDMap, ToUnicode CMap из кластеров шейпинга (лигатуры → несколько кодов); PDF/A: все шрифты встроены
  for page in lay.pages:
      dl = render_page_display_list(page, PrintMode)    // тот же список, что для экрана: Text(glyph ids, font, matrix), Path, Image, Clip
      if opts.tagged: открыть структуру: /Document → /Sect (раздел) → /H1../H6 (по outline_level), /P, /L /LI /Lbl /LBody (списки),
                      /Table /TR /TH /TD (строка заголовка → TH, Scope), /Figure (+ /Alt из AltText; decorative → /Artifact),
                      /Link (+ /Alt), /Note (сноски), /TOC /TOCI, /Formula (Math + /Alt как MathML/LaTeX), /Span с /Lang при смене языка;
                      колонтитулы, номера строк, разделители сносок, фон → /Artifact; порядок чтения = порядок потока модели
      emit content stream: BDC /Tag <</MCID n>> … EMC вокруг каждого фрагмента; изображения — XObject (JPEG passthrough, иначе Flate; dpi по opts)
      аннотации: /Link для гиперссылок и PAGEREF (\h); комментарии → /Text/Highlight при opts.comments
  outline: закладки из заголовков (дерево по уровням) и/или закладок документа; /Dests по _Toc закладкам
  метаданные: Info + XMP (dc:title/creator/…; pdfaid для PDF/A; pdfuaid:part=1 для UA); OutputIntent sRGB ICC для PDF/A
  безопасность: AES-256 (R6) при пароле; PDF/A запрещает шифрование → предупреждение
  PDF/A-1: без прозрачности (сплющивание через растр 300 dpi для фрагментов с alpha), без JPEG2000; A-2/3: прозрачность ок, A-3 — вложения (.mwx-источник по опции)

Проверка: автотест через veraPDF на корпусе; 500 стр ≈ 2–4 с (шрифты один раз, страницы параллельно по разделам, запись потоком).

5.27 Автосохранение и журнал операций

JournalWriter (поток документа):
  файл <appdata>/journal/<doc-uuid>.jrnl: заголовок {magic, version, doc path, base snapshot id} + последовательность фреймов
  фрейм = {len u32, crc32c, zstd(cbor(Transaction))}; транзакции накапливаются в буфере, сброс + fdatasync каждые 2 с
  (или немедленно для «тяжёлых» операций: вставка таблицы, объекта) — библия, 6
  снимок: при журнале > 32 МБ или 5 мин → <doc-uuid>.snap (полная модель CBOR zstd, атомарная запись через rename) и обрезка журнала
  при Save: журнал помечается «base = сохранённый файл» (хэш), старые фреймы отбрасываются
  при закрытии без ошибок: журнал удаляется
Recovery (Hub/Write при запуске): для каждого незакрытого .jrnl → загрузить base (файл или .snap), replay фреймов до первого
  битого (crc) → документ «восстановленный» в WR-64; пользователь выбирает сохранить/сравнить/удалить
Версии в документе (history/): при сохранении (опция) — транзакции с последнего снимка пишутся в history/ops-<n>.cbor,
  снимки — history/snapshots/<ts>.xml (ограничение по размеру: по умолчанию 50 МБ или 20 версий, старые сжимаются до снимков)

5.28 Печать

Meridian Print получает PrintJob { pages: Vec<PageRef>, settings }; холст тот же — display list → бэкенд: Windows — XPS/GDI через Qt QPrinter с нашим растеризатором глифов (или «печать в PDF» → Windows PDF-принтер); macOS — CoreGraphics (PDF-контекст → NSPrintOperation); Linux — CUPS, отправляем PDF (5.26, без тегов) напрямую через lp/IPP. Общие функции: «n на листе», брошюра (переупорядочивание страниц 1-N, 2-(N-1) с разворотом), масштаб по бумаге (матрица), диапазоны p3s2, чёт/нечёт, обратный порядок, двусторонняя (через драйвер или эмуляция с паузой), обновление полей перед печатью (PRINTDATE), печать скрытого текста/кодов полей/фона по опциям. Предпросмотр = обычный рендер страницы в масштабе; растровый fallback 600 dpi для проблемных драйверов.

5.29 Сложность и целевые показатели

Операция Сложность Цель (500 стр., ≈150 тыс. слов, 40 рисунков, 30 таблиц) Как измеряем
Открытие .docx до первого экрана O(размер файла) парсинг + O(видимых абзацев) < 0,5 с criterion, корпус 10 000 файлов, p95
Полная раскладка в фоне O(символов) + O(n·a) для К–П < 2 с (все номера страниц точны) p95
Ввод символа → перерисовка O(абзац) + поток до границы страницы < 16 мс p99 (< 8 мс p50) трассировка кадров
Прокрутка O(видимых страниц) из кэша 60 fps (16 мс) на 4K —
Применение стиля ко всему документу O(n) перераскладка (фон) интерактивность < 100 мс, завершение < 2 с —
Поиск простой / regex O(n) 20 мс / 100 мс —
Замена всех (1000 вхождений) O(n + k) одна транзакция < 300 мс —
Обновление полей (без TOC / с TOC) O(полей) + ≤ 3 перераскладки 50 мс / 500 мс —
Нумерация списков O(абзацев) инкрементально < 5 мс на правку —
Правописание полного документа O(слов), кэш < 2 с в фоне, видимый экран < 100 мс —
Сравнение двух версий (10 % изменений) O((N+M)D) < 1,5 с —
Экспорт PDF O(страниц) параллельно по разделам < 4 с (PDF/UA < 6 с) —
Сохранение .mwx / .docx O(модели) < 0,5 с / < 1 с —
Память — < 400 МБ RSS (без 4K-изображений в оригинале — миниатюры в памяти, оригиналы — lazily) —
Восстановление после сбоя O(журнала) потеря ≤ 2 с правок; replay 10 МБ журнала < 1 с kill -9 тест
2000 стр. (книга) — открытие до интерактивности < 1 с, полная раскладка < 8 с —

6. MeridianScript API для Write

Общая часть (песочница QuickJS-ng, манифест, разрешения, модули meridian/*, транспиляция TS) — в 05-script-api.md. Здесь — объектная модель Write (meridian/write), события и соответствие VBA. Принципы: синхронный доступ к модели внутри doc.transact() (одна отменяемая транзакция), асинхронные (Promise) — ввод-вывод, экспорт, слияние, Assist; все коллекции — 0-based, итерируемые; объекты — «живые» ссылки по NodeId (не инвалидируются при правках в других местах, бросают StaleRangeError, если их узел удалён). Типы поставляются как meridian-write.d.ts.

6.1 Объектная модель

Application (meridian/app)
 ├─ documents: Documents            open(path|url, opts) / create(template?) / active / [i] / on(...)
 ├─ selection: Selection            (активного окна)
 ├─ windows: Windows                активное окно, split, zoom, viewMode
 ├─ ui: UI                          commands, ribbon (через манифест), dialogs: alert/prompt/confirm/pickFile, sidebar.show()
 ├─ clipboard, fs (по разрешению), net (по разрешению), sheets/base (мост в другие приложения, если открыты/доступны)
 └─ Document
     ├─ body: Body                   корневой контейнер (sections)       Body ⊂ Container
     ├─ sections: Section[]          pageSetup, headers/footers (default/first/even), columns, breakKind
     ├─ paragraphs: Paragraph[]      (все, рекурсивно по телу), tables: Table[], shapes: Shape[], inlineObjects
     ├─ styles: Styles               get(name|id), add(), builtIn, defaults
     ├─ lists: Lists                 списки как объекты: paragraphs, restart(), continueFrom()
     ├─ fields: Fields               update(), add(code, range), по типу
     ├─ bookmarks, hyperlinks, footnotes, endnotes, comments, revisions, sources (библиография), properties (meta + custom)
     ├─ range(start?, end?): Range   весь документ или подмножество; content = body.range()
     ├─ find(query): Match[]  /  replaceAll(query, replacement)
     ├─ transact(label, fn): T       одна транзакция undo; вложенные объединяются
     ├─ save(), saveAs(path, {format}), export(path, {format, pdf?: {...}}), print(opts), close({save})
     ├─ compare(other, opts) → Document,  mailMerge: MailMerge,  protection, signatures, versions
     ├─ trackRevisions: boolean, view: {mode, zoom, markup}
     └─ on(event, handler) / off
 Container (Body | Cell | TextBox | HeaderFooter | NoteBody | CommentBody)
     ├─ blocks: Block[] (Paragraph | Table | ContentControl)
     ├─ insertParagraph(text?, {style, at}), insertTable(rows, cols, {style, at}), insertBreak(kind), clear()
 Paragraph: text, style, format: ParagraphFormat, font: Font (общий для всех run'ов — агрегат), runs: Run[], range,
            listItem?: ListItem {level, label, numId}, outlineLevel, insertBefore/After(), delete(), split(at), merge(next)
 Run: text, font: Font, style (символьный), range, isField, isHidden, revision?
 Range: start, end (Position), text (get/set), paragraphs, runs, tables, fields, inlineObjects, comments, revisions,
        font, paragraphFormat, style, insertText(text, where: 'before'|'after'|'replace'), insertParagraph(), insertTable(),
        insertInlinePicture(blob|path), insertField(code), insertFootnote(text), insertComment(text), insertBookmark(name),
        insertHyperlink(url, {text, tooltip}), delete(), select(), expandTo('word'|'sentence'|'paragraph'|'cell'|'table'),
        find(query), replace(query, repl), highlight(color), getHtml(), getOoxml(), getMarkdown(), compareTo(range)
 Table: rows, columns, cell(r, c), style, look {headerRow, totalRow, bandedRows, ...}, width, alignment, autoFit(mode),
        addRow(at?, count?), addColumn(at?), deleteRow/Column, merge(r0,c0,r1,c1), split(), sort(keys), toText(sep),
        borders, shading, caption, altText, nested parent?: Cell
 Row: cells, height {value, rule}, isHeader, allowBreak, index;  Cell: content: Container, text, width, vAlign, span, shading, borders
 Shape (плавающий/встроенный объект): kind, name, size, position {h, v}, wrap, zOrder, altText, rotation, fill/line (Draw API),
        textFrame?: Container, picture?: {source, crop, adjustments}, chart?: Chart (Meridian Chart API), group?: Shape[]
 Field: code, type, result (Range), update(), lock, unlock, unlink(), switches
 Comment: author, date, text, body: Container, replies, resolved, range, reply(text), resolve(), delete()
 Revision: type ('insert'|'delete'|'format'|'move'|...), author, date, range, accept(), reject()
 Style: id, name, type, basedOn, next, font, paragraphFormat, tableStyle?, numbering?, builtIn, inUse, delete()
 Selection ⊂ Range + { kind, collapse(), move(unit, count), extend(unit, count), typeText(), paste(), cut(), copy(),
                       insertAtEnd(), goTo(what, which, count|name), activeWindow }
 Font: name, nameEastAsia/CS, size, bold, italic, underline, color, highlight, strike, superscript, subscript, caps,
       smallCaps, hidden, spacing, scale, position, kerning, language, ligatures, numberForm, stylisticSets
 ParagraphFormat: alignment, leftIndent, rightIndent, firstLineIndent, spaceBefore, spaceAfter, lineSpacing, lineRule,
       keepWithNext, keepTogether, widowControl, pageBreakBefore, outlineLevel, borders, shading, tabs: TabStops, direction
 PageSetup: paperSize, orientation, margins {top,bottom,left,right,header,footer,gutter}, columns, differentFirstPage,
       oddAndEvenPages, pageNumbering {format, start}, lineNumbering, verticalAlignment, textDirection
 MailMerge: source(set from Sheets/Base/CSV/array), fields, preview(i), execute({output: 'document'|'files'|'pdf'|'print'|'email', ...})
 Events: documentOpen, documentBeforeSave, documentAfterSave, documentBeforeClose, documentBeforePrint, selectionChange,
         contentChange {transactions}, fieldBeforeUpdate, fieldAfterUpdate, commentAdded/Resolved, revisionAccepted/Rejected,
         windowActivate, viewModeChange, mailMergeBeforeRecord, mailMergeAfterMerge, autoSaveTick

6.2 Типы (фрагмент meridian-write.d.ts)

declare module "meridian/write" {
  export const app: Application;
  export interface Document {
    readonly path: string | null; readonly name: string; readonly isDirty: boolean; readonly format: FileFormat;
    readonly body: Body; readonly sections: ReadonlyArray<Section>; readonly paragraphs: ReadonlyArray<Paragraph>;
    readonly tables: ReadonlyArray<Table>; readonly styles: Styles; readonly fields: Fields; readonly comments: Comments;
    readonly revisions: Revisions; readonly bookmarks: Bookmarks; readonly properties: DocProperties;
    trackRevisions: boolean;
    range(start?: Position, end?: Position): Range;
    find(q: string | Query): Match[];
    replaceAll(q: string | Query, replacement: string | ((m: Match) => string), opts?: ReplaceOptions): number;
    transact<T>(label: string, fn: (doc: Document) => T): T;
    save(): Promise<void>; saveAs(path: string, opts?: { format?: FileFormat }): Promise<void>;
    export(path: string, opts: ExportOptions): Promise<void>;
    print(opts?: PrintOptions): Promise<void>;
    compare(other: Document, opts?: CompareOptions): Promise<Document>;
    readonly mailMerge: MailMerge;
    on<K extends keyof DocumentEvents>(ev: K, h: (e: DocumentEvents[K]) => void | Promise<void>): Disposable;
  }
  export interface Query { text: string; mode?: "plain" | "wildcards" | "regex"; matchCase?: boolean; wholeWord?: boolean;
                           format?: Partial<Font & ParagraphFormat & { style: string }>; scope?: "document" | "selection" | "headers" | "notes" | "comments"; }
  export interface Match { readonly range: Range; readonly groups: string[]; readonly text: string; }
  export type FileFormat = "mwx" | "mwt" | "mwxf" | "docx" | "docm" | "dotx" | "doc" | "rtf" | "odt" | "ott" | "txt" | "md" | "html" | "fb2" | "epub" | "pdf" | "tex";
}

6.3 Примеры

1. Замена во всём документе регулярным выражением с сохранением форматирования

// Нормализуем даты «01.02.2026» → «1 февраля 2026 г.», не трогая колонтитулы / normalize dates in body only
import { app } from "meridian/write";
const doc = app.documents.active!;
const months = ["января","февраля","марта","апреля","мая","июня","июля","августа","сентября","октября","ноября","декабря"];
const n = doc.transact("Нормализация дат", d =>
  d.replaceAll({ text: String.raw`\b(\d{2})\.(\d{2})\.(\d{4})\b`, mode: "regex", scope: "document" },
    m => `${Number(m.groups[1])} ${months[Number(m.groups[2]) - 1]} ${m.groups[3]} г.`,
    { keepFormatting: true })       // формат первого символа совпадения сохраняется / first-char formatting kept
);
app.ui.statusBar.flash(`Заменено: ${n}`);

2. Генерация отчёта из Sheets (таблица + диаграмма + поля)

import { app } from "meridian/write";
import { sheets } from "meridian/sheets";            // мост доступен, если Sheets установлен (Free+)
const wb = await sheets.open("~/Отчёты/продажи-2026.msx");
const data = wb.sheet("Итоги").range("A1:D13").values as (string | number)[][];
const doc = await app.documents.create({ template: "Отчёт (ГОСТ 7.32)" });
doc.transact("Отчёт из Sheets", d => {
  d.properties.title = "Продажи за 2026 год";
  d.body.insertParagraph("Продажи по кварталам", { style: "Heading1" });
  const t = d.body.insertTable(data.length, data[0].length, { style: "TableGrid" });
  data.forEach((row, r) => row.forEach((v, c) => { t.cell(r, c).text = String(v); }));
  t.rows[0].isHeader = true; t.autoFit("contents"); t.caption = "Таблица 1 — Продажи по кварталам";
  const p = d.body.insertParagraph("", { style: "Normal" });
  const chart = p.range.insertChart({ type: "column", data: wb.sheet("Итоги").range("A1:D13"), size: { w: "16cm", h: "9cm" } });
  chart.title = "Продажи, млн ₽";
  d.body.insertParagraph("Всего продано: ", { style: "Normal" }).range.insertField(`=SUM(Table1 D2:D13) \\# "0,00"`);
  d.fields.update();
});
await doc.saveAs("~/Отчёты/Продажи-2026.mwx");

3. Обход таблиц: строка «Итого» в каждой таблице с числовым последним столбцом

import { app } from "meridian/write";
const doc = app.documents.active!;
doc.transact("Итоги по таблицам", d => {
  for (const t of d.tables) {
    const last = t.columns.length - 1;
    const nums = t.rows.slice(t.rows[0].isHeader ? 1 : 0).map(r => parseFloat(r.cells[last].text.replace(/\s/g, "").replace(",", ".")));
    if (nums.some(isNaN) || nums.length === 0) continue;
    const row = t.addRow();                               // в конец / append
    row.cells[0].text = "Итого";
    row.cells[last].content.clear();
    row.cells[last].content.insertParagraph("").range.insertField(`=SUM(ABOVE) \\# "# ##0,00"`);
    row.cells.forEach(c => { c.content.paragraphs[0].font.bold = true; c.shading = { fill: "#F2A541" }; });
  }
  d.fields.update();
});

4. Пакетная конвертация папки в PDF/A и .mwx (фоновая задача с прогрессом)

import { app } from "meridian/write";
import { fs } from "meridian/fs";                      // требует разрешения "fs:read", "fs:write" в манифесте
const dir = await app.ui.dialogs.pickFolder({ title: "Папка с документами" });
if (!dir) throw new Error("Отменено");
const files = (await fs.list(dir)).filter(f => /\.(docx?|rtf|odt)$/i.test(f.name));
const task = app.tasks.start("Конвертация", files.length);
for (const f of files) {
  const doc = await app.documents.open(f.path, { visible: false, readOnly: true });
  try {
    await doc.export(f.path.replace(/\.\w+$/, ".pdf"), { format: "pdf", pdf: { standard: "pdf/a-2b", tagged: true, bookmarks: "headings" } });
    await doc.saveAs(f.path.replace(/\.\w+$/, ".mwx"));
  } catch (e) { task.log(`${f.name}: ${(e as Error).message}`); }
  finally { await doc.close({ save: false }); task.advance(); }
}
task.finish();

5. Команда в ленте через манифест + обработчик

// scripts/manifest.json (в документе, шаблоне или в профиле ~/.meridian/scripts/<id>/)
{
  "id": "ru.example.gost-check", "name": "Проверка по ГОСТ", "version": "1.2.0", "apps": ["write"],
  "permissions": ["document:write"],
  "contributes": {
    "commands": [{ "id": "gostCheck.run", "title": "Проверить по ГОСТ 7.32", "icon": "check-circle" }],
    "ribbon": [{ "tab": "review", "group": "gost", "groupTitle": "ГОСТ", "commands": ["gostCheck.run"] }],
    "keybindings": [{ "command": "gostCheck.run", "key": "ctrl+alt+g", "mac": "cmd+alt+g" }],
    "menus": [{ "context": "text", "command": "gostCheck.run" }],
    "events": [{ "event": "documentBeforeSave", "handler": "onBeforeSave" }]
  },
  "main": "main.ts"
}
// main.ts
import { app, commands, Document } from "meridian/write";
commands.register("gostCheck.run", async () => {
  const doc = app.documents.active!; const issues: string[] = [];
  for (const s of doc.sections) {
    const m = s.pageSetup.margins;
    if (m.left < "30mm" || m.right < "15mm" || m.top < "20mm" || m.bottom < "20mm") issues.push("Поля не по ГОСТ 7.32 (30/15/20/20 мм)");
  }
  for (const p of doc.paragraphs) if (p.style.id === "Normal" && p.font.size !== 14) { issues.push(`Размер шрифта ${p.font.size} pt в абзаце «${p.text.slice(0, 30)}…»`); if (issues.length > 50) break; }
  app.ui.sidebar.show("script-results", { title: "Проверка по ГОСТ", items: issues.length ? issues : ["Замечаний нет"] });
});
export function onBeforeSave(e: { document: Document; cancel(reason: string): void }) {
  if (e.document.shapes.some(s => !s.altText.decorative && !s.altText.description)) e.document.properties.custom["a11y"] = "alt-missing";
}

6. Эвристическое назначение стилей заголовков (чистка «прямого форматирования»)

import { app } from "meridian/write";
const doc = app.documents.active!;
doc.transact("Стили заголовков по формату", d => {
  for (const p of d.paragraphs) {
    if (p.style.id !== "Normal" || p.runs.length === 0 || p.text.trim().length === 0 || p.text.length > 120) continue;
    const f = p.font;                                     // агрегат: undefined, если run'ы различаются / mixed → undefined
    if (f.bold === true && f.size !== undefined && f.size >= 16) p.style = "Heading1";
    else if (f.bold === true && f.size !== undefined && f.size >= 13) p.style = "Heading2";
    else continue;
    p.runs.forEach(r => { r.font.bold = undefined; r.font.size = undefined; });   // снять прямое форматирование / clear direct formatting
  }
});

7. Слияние из CSV в отдельные PDF-файлы по одному на запись

import { app } from "meridian/write";
const doc = app.documents.active!;
const mm = doc.mailMerge;
await mm.setSource({ kind: "csv", path: "~/Рассылка/клиенты.csv", encoding: "auto", delimiter: "auto" });
mm.map({ "Имя": "first_name", "Фамилия": "last_name", "Сумма": "amount" });   // поле документа → столбец источника
const report = await mm.execute({ output: "pdf", folder: "~/Рассылка/out", namePattern: "{Фамилия}_{Имя}.pdf",
                                  filter: r => Number(r["amount"]) > 0, pdf: { standard: "pdf/a-2b" } });
console.log(`Готово: ${report.produced}, пропущено: ${report.skipped}, ошибок: ${report.errors.length}`);

8. Реакция на события: автообновление оглавления и предупреждение о битых ссылках

import { app } from "meridian/write";
const doc = app.documents.active!;
doc.on("contentChange", e => {                            // e.transactions: метки и затронутые абзацы
  if (e.transactions.some(t => t.touchedStyles.some(s => s.startsWith("Heading")))) doc.fields.ofType("TOC").forEach(f => f.update());
});
doc.on("documentBeforePrint", e => {
  const broken = doc.fields.ofType("REF").filter(f => f.result.text.startsWith("Ошибка! Источник ссылки не найден"));
  if (broken.length) e.cancel(`Есть ${broken.length} битых перекрёстных ссылок`);
});

6.4 Соответствие объектам VBA Word (для транслятора)

VBA (Word) MeridianScript (meridian/write) Примечание
Application app свойства Version, UserName → app.version, app.user.name
ActiveDocument / Documents(i) / Documents.Open(path) / Documents.Add app.documents.active / app.documents[i-1] / await app.documents.open(path) / app.documents.create() коллекции 0-based, транслятор вычитает 1; Open → await
Selection app.selection Selection.TypeText → typeText, TypeParagraph → insertParagraph('after'), MoveDown wdLine, 2 → move('line', 2)
Range(start, end) / Document.Content doc.range(start, end) / doc.body.range() позиции — Position, не числа; транслятор использует doc.range.fromOffsets(a, b) для совместимости
Paragraphs(i) / .Range.Text / .Style / .Format.* doc.paragraphs[i-1] / .text / .style / .format.*
Words(i), Sentences(i), Characters(i) range.words[i-1], range.sentences[i-1], range.chars[i-1] сегментация UAX #29, как в Word с поправками
Tables(i), Table.Cell(r, c), Rows.Add, Columns.Add, Cell.Merge doc.tables[i-1], t.cell(r-1, c-1), t.addRow(), t.addColumn(), t.merge(...)
Font (.Bold = True, .Size, .Name, .Color = wdColorRed) font.bold = true, font.size, font.name, font.color = "#FF0000" константы wdColor* → hex; .Bold = wdToggle → !font.bold
ParagraphFormat (.Alignment = wdAlignParagraphCenter, .SpaceAfter) paragraphFormat.alignment = "center", .spaceAfter единицы pt как в VBA
Find.Execute(FindText:=, ReplaceWith:=, Replace:=wdReplaceAll, MatchWildcards:=True) range.replace({ text, mode: "wildcards" }, repl, { all: true }) Find.Execute в цикле Do While .Found → итератор range.find(q)
Styles(name), Styles.Add doc.styles.get(name), doc.styles.add({...}) имена локализуются по таблице Word
Bookmarks.Add(name, range), Bookmarks.Exists, Bookmarks(name).Range doc.bookmarks.add(name, range), .has(name), .get(name).range
Fields.Update, Fields.Add(range, wdFieldPage), Field.Code.Text, Field.Result doc.fields.update(), range.insertField("PAGE"), field.code, field.result wdField* → строка кода
InlineShapes.AddPicture(path), Shapes.AddShape(type, l, t, w, h) range.insertInlinePicture(path), doc.shapes.add({ kind: "shape", preset, position, size }) единицы pt
Comments.Add(range, text), Comment.Author range.insertComment(text), comment.author
Revisions.AcceptAll, TrackRevisions = True doc.revisions.acceptAll(), doc.trackRevisions = true
Sections(i).PageSetup.*, Headers(wdHeaderFooterPrimary).Range doc.sections[i-1].pageSetup.*, .headers.default.range()
Hyperlinks.Add(anchor, address, , , text) range.insertHyperlink(address, { text })
Footnotes.Add(range, , text) range.insertFootnote(text)
MailMerge.OpenDataSource, .Execute doc.mailMerge.setSource({...}), await doc.mailMerge.execute({...})
PrintOut, SaveAs2 FileFormat:=wdFormatPDF, ExportAsFixedFormat await doc.print(), await doc.export(path, { format: "pdf" })
MsgBox, InputBox, Application.FileDialog await app.ui.dialogs.alert/confirm/prompt, pickFile синхронный VBA → await; транслятор оборачивает процедуру в async
With … End With, For Each, On Error Resume Next, GoTo блочная переменная, for…of, try/catch с продолжением (эмуляция через обёртку), GoTo — не транслируется (отчёт)
UserForm не транслируется; предлагается app.ui.dialogs.form(schema) отчёт о непереведённом
CreateObject("Excel.Application") import { sheets } from "meridian/sheets" для простых случаев; иначе — отчёт
Shell, Kill, реестр, WinAPI (Declare) запрещено песочницей — не транслируется, отчёт безопасность по умолчанию

7. Форматы

Матрица чтения/записи — библия 3.3; устройство кодеков, схемы XML и тестовые корпуса — 04-file-formats.md. Ниже — только то, что специфично для Write.

7.1 Особенности по форматам

Формат Чтение Запись Особенности для Write
.mwx/.mwt/.mwxf полная полная родной; история history/, скрипты, подписи; mwxf для git — детерминированная сериализация (стабильный порядок атрибутов, \n после каждого блока)
.docx/.docm/.dotx полная (WordprocessingML + DrawingML + OMML + charts + VML legacy + w14/w15/w16 расширения + customXml + glossary) полная; неизвестные элементы — round-trip через ExtensionBag; .docm — VBA-проект сохраняется как blob без изменений (не исполняется), скрипты Meridian — в customXml + scripts/ нашей части режим совместимости из w:compat/w:compatSetting (Word 2007/2010/2013+) определяет флаги раскладки; шрифты по fontTable с подстановкой; fldSimple ↔︎ маркеры; w:ins/w:del/w:rPrChange ↔︎ слой правок; цели: 10 000 документов корпуса — визуальное совпадение ≥ 98 %
.doc (Word 97–2003, бинарный) полная (FIB, piece table → абзацы, CHPX/PAPX/SEPX, таблицы, поля, сноски, рисунки Escher, OLE) базовая (Word 97 структура, без новых объектов: диаграммы и SmartArt → изображения) парсер в песочном процессе (библия, 6); фаззинг обязателен
.rtf полная (включая \object, \shp, \field, \footnote, \revised, \upr Unicode) полная RTF 1.9.1; кодировки ANSI-страниц; «старые» таблицы
.odt/.ott полная (ODF 1.3: text:*, table:*, draw:*, стили страниц, автостили, text:tracked-changes, office:annotation) полная стили страниц ODF ↔︎ разделы: каждый разрыв со сменой стиля страницы → новый раздел; обратно — стиль страницы на раздел; списки ODF (text:list-style) ↔︎ нумерация
.txt кодировки авто (UTF-8/16/1251/866/KOI8-R, BOM) выбор кодировки/переносов строк абзац = строка; опция «абзац = пустая строка»
.md CommonMark + GFM (таблицы, задачи, зачёркивание, сноски, формулы $…$, front-matter) да стили ↔︎ синтаксис: заголовки, цитаты, код, списки; остальное — потери (7.3)
.html чтение с CSS (подмножество: блочная модель, шрифты, таблицы, списки, изображения data:/файлы) чистый HTML5 + CSS (классы по стилям) или «фильтрованный» для почты импорт из Word-HTML (mso-*) распознаётся
.fb2 полная (секции → заголовки, стихи, эпиграфы, сноски <a type="note">, изображения base64) P1 книжные шаблоны
.epub 2/3 (spine → разделы, CSS-подмножество, сноски epub:type) EPUB 3 (главы по «Заголовок 1», nav, обложка, метаданные, вертикальный текст, MathML) фиксированный макет — только чтение
.pdf через Meridian PDF (текст → абзацы по эвристике, таблицы по линиям, изображения), «как рисунок страницы» 5.26 импорт — «с ограничениями» (R*)
.pages, .wpd, .abw, .wps, DocBook, .tex (подмножество) чтение — .pages — IWA/protobuf (новые) и XML (старые); .tex — структура, формулы в Math, без макросов

7.2 Правила совместимости при сохранении в чужой формат

7.3 Что теряется при сохранении в .docx и как предупреждаем

Возможность Write Что происходит в .docx Предупреждение / смягчение
Кнут–Пласс, висячая пунктуация, оптические поля (TypographyMode::Meridian) Word разложит жадно → другие переносы и число страниц «Макет в Word может отличаться (переносы, страницы)»; предлагаем переключить документ в режим совместимости перед отправкой
Произвольные OpenType-теги (frac, ordn, swsh, salt) нет аналога (есть только ligatures/numForm/numSpacing/stylisticSets/cntxtAlts) сохраняются в customXml; в Word видны без фич; предупреждение «Типографские функции X не поддерживаются Word»
Стили библиографии ГОСТ Р 7.0.5, CSL-стили Word знает только свои XSL-стили; CITATION/BIBLIOGRAPHY сохраняем с результатом; Word при F9 переформатирует в свой стиль результат остаётся текстом, стиль — в customXml; предупреждение «Список литературы в Word обновится в другом стиле — заблокировать поля?» (кнопка блокирует)
Нумерация формул с перекрёстными ссылками (Math) OMML + SEQ в таблице-обёртке (приём Word) автоматически конвертируется в «таблицу 3 колонки без границ» при экспорте — визуально идентично
Статусы комментариев (решён), ветки w15:commentsExtended (поддерживается Word 2013+) без потерь; для Word 2007/2010 — предупреждение
Реакции/упоминания в комментариях (фаза 6) нет теряются; предупреждение
История операций и версии (history/) нет «История изменений не сохраняется в .docx» (один раз на документ)
Скрипты MeridianScript в документе нет (VBA не генерируем) «Скрипты не будут работать в Word»; customXml хранит их для обратного импорта
Фигуры Draw с умными коннекторами/узлами DrawingML: коннекторы как cxnSp, нестандартные геометрии → custGeom без потерь визуально, теряется «умная» маршрутизация
Диаграммы новых типов (каскад, воронка, treemap, sunburst, box-plot) cx:chart (Word 2016+) — есть; для старых Word → рисунок PNG + данные предупреждение только при целевой версии < 2016
Вложенная таблица Sheets с формулами внедрённый xlsx + изображение (как Word) без потерь (Word открывает в Excel)
Висячие строки N/M (не 2/2), макс. переносов подряд, min_left/right ≠ 2 нет аналога применяются 2/2 и без ограничений — предупреждение «Параметры переносов упрощены»
Направление абзаца «авто» фиксируется по первому сильному символу без предупреждения
Автообновление полей Word обновляет вручную перед сохранением — обновить все поля (опция вкл.)
Подпись Ed25519 (MOP) / ГОСТ XAdES в docx — только с сертификатом X.509; Ed25519 Word не проверит; ГОСТ — проверит только с КриптоПро «Подпись не будет проверяться в Word» + возможность подписать повторно X.509
Любые имена закладок (с пробелами, кириллицей) Word требует [A-Za-z_][A-Za-z0-9_]* автоматически переименовываем (Моя закладка → _Моя_закладка → транслитерация _Moya_zakladka), ссылки обновляются; предупреждение со списком
Плоский .mwxf, миниатюра, Dublin Core расширения есть только docProps/core.xml подмножество теряются свои метаданные → customXml
Фокус-режим, настройки вида, тёмная страница нет не предупреждаем (настройки окна)
Контурное обтекание > 64 вершин, градиентные границы абзаца wrapPolygon без ограничений; градиентные границы → сплошная предупреждение только для границ
Защита диапазонов «по Meridian ID» исключения Word принимают только e-mail/группы исключения — по e-mail аккаунта; предупреждение, если аккаунта нет
Комментарии к объектам в колонтитулах, несколько оглавлений с одинаковыми \b Word поддерживает ограниченно предупреждение при обнаружении

Для .odt: теряются условные стили таблиц (сплющиваются в прямое форматирование ячеек), связанные стили (разделяются на два), некоторые ключи полей (\* MERGEFORMAT), w14-эффекты; для .md/.txt/.html — показывается краткая сводка «сохраняется только структура/текст». Для .rtf — OpenType, эффекты, темы.


8. Горячие клавиши

Схема по умолчанию — Microsoft Word (библия, 5.3). В настройках: «LibreOffice» и «Своя» (редактор с проверкой конфликтов, экспорт/импорт JSON). На macOS Ctrl → ⌘, Alt → ⌥, кроме указанных отличий (8.2).

8.1 Основные (схема Word)

Действие Windows / Linux Действие Windows / Linux
Создать документ Ctrl+N Открыть Ctrl+O
Сохранить / Сохранить как Ctrl+S / F12 Печать Ctrl+P
Закрыть документ Ctrl+W (Ctrl+F4) Командная палитра Ctrl+K (см. 10.1)
Отменить / Вернуть / Повторить Ctrl+Z / Ctrl+Y / F4 Вырезать / Копировать / Вставить Ctrl+X / Ctrl+C / Ctrl+V
Специальная вставка Ctrl+Alt+V Вставить только текст Ctrl+Shift+V
Выделить всё Ctrl+A Режим расширения выделения F8 (повторно — слово/предложение/абзац), Shift+F8 — уменьшить
Найти / Расширенный поиск Ctrl+F / Ctrl+H (панель с заменой) Перейти к Ctrl+G / F5
Найти далее / назад Enter в поле / Shift+Enter; в тексте Ctrl+PageDown / Ctrl+PageUp по объекту обзора Повторить поиск Shift+F4
Полужирный / Курсив / Подчёркнутый Ctrl+B / Ctrl+I / Ctrl+U Двойное подчёркивание / только слова Ctrl+Shift+D / Ctrl+Shift+W
Верхний / Нижний индекс Ctrl+Shift+= / Ctrl+= Все прописные / Малые прописные Ctrl+Shift+A / Ctrl+Shift+K
Регистр по кругу Shift+F3 Скрытый текст Ctrl+Shift+H
Увеличить / уменьшить размер (список) Ctrl+Shift+> / Ctrl+Shift+< На 1 pt Ctrl+] / Ctrl+[
Диалог «Шрифт» Ctrl+D Очистить формат символов / абзаца Ctrl+Space / Ctrl+Q
Формат по образцу: копировать / применить Ctrl+Shift+C / Ctrl+Shift+V (при активном образце) Стиль «Обычный» Ctrl+Shift+N
Заголовок 1 / 2 / 3 Ctrl+Alt+1 / 2 / 3 Применить стиль (панель) Ctrl+Shift+S
Выровнять: влево / центр / вправо / по ширине Ctrl+L / Ctrl+E / Ctrl+R / Ctrl+J Интервал 1,0 / 1,5 / 2,0 Ctrl+1 / Ctrl+5 / Ctrl+2
Интервал перед абзацем 12 pt вкл/выкл Ctrl+0 Отступ абзаца увеличить / уменьшить Ctrl+M / Ctrl+Shift+M
Выступ увеличить / уменьшить Ctrl+T / Ctrl+Shift+T Маркированный список Ctrl+Shift+L
Понизить / повысить уровень списка Tab / Shift+Tab (в начале абзаца), Alt+Shift+→ / ← Переместить абзац вверх / вниз Alt+Shift+↑ / ↓
Разрыв страницы / колонки / строки Ctrl+Enter / Ctrl+Shift+Enter / Shift+Enter Неразрывный пробел / дефис / мягкий перенос Ctrl+Shift+Space / Ctrl+Shift+- / Ctrl+-
Непечатаемые знаки Ctrl+Shift+8 (Ctrl+*) Символ по коду (туда и обратно) Alt+X
Сноска / концевая сноска Ctrl+Alt+F / Ctrl+Alt+D Комментарий Ctrl+Alt+M
Запись исправлений вкл/выкл Ctrl+Shift+E Принять / отклонить текущее исправление (панель) Ctrl+Shift+Alt+A / R (наше)
Гиперссылка Ctrl+Alt+K (см. 10.1; в схеме «Word строго» — Ctrl+K) Закладка Ctrl+Shift+F5
Вставить поле / Обновить поля Ctrl+F9 / F9 Коды полей: текущее / все Shift+F9 / Alt+F9
Заблокировать поле / Разорвать связь Ctrl+F11 / Ctrl+Shift+F9 Следующее / предыдущее поле F11 / Shift+F11
Дата / Время (поля) Alt+Shift+D / Alt+Shift+T Номер страницы (поле) Alt+Shift+P
Автотекст (подстановка) F3 Тезаурус Shift+F7
Правописание F7 Статистика Ctrl+Shift+G
Оглавление: обновить F9 на оглавлении; Ctrl+Alt+Shift+T — вставить (наше) Пометить элемент указателя Alt+Shift+X
Пометить элемент оглавления (TC) Alt+Shift+O Вставить название Alt+Ctrl+Shift+C (наше)
Таблица: следующая / предыдущая ячейка Tab / Shift+Tab Выделить столбец / таблицу Shift+Alt+PageDown (от верхней) / Alt+5 (NumLock выкл.)
Начало / конец строки Home / End Начало / конец документа Ctrl+Home / Ctrl+End
На слово влево / вправо Ctrl+← / Ctrl+→ На абзац вверх / вниз Ctrl+↑ / Ctrl+↓
Удалить слово слева / справа Ctrl+Backspace / Ctrl+Delete К последней правке (3 позиции) Shift+F5
Новое окно / Разделить окно без клавиши (меню «Вид → Окно», палитра) / Ctrl+Alt+S Переключение окон Ctrl+F6 / Ctrl+Shift+F6
Масштаб + / − / 100 % Ctrl+колесо, Ctrl+Shift+= / Ctrl+Shift+- (наше), Ctrl+Alt+0 — 100 % (наше) Режим чтения / Фокус Ctrl+Alt+R (наше) / Ctrl+Shift+F11 (наше)
Режим структуры / разметки Ctrl+Alt+O / Ctrl+Alt+P Черновик Ctrl+Alt+N (как в Word)
Свернуть ленту Ctrl+F1 Контекстное меню Shift+F10 / Menu
Выход Alt+F4 Справка F1

Прочие (без изменений относительно Word): Ctrl+Shift+F3 (вставить копилку), Ctrl+F3 (вырезать в копилку), Alt+Shift+F7 (словарь), Ctrl+Shift+F7 (обновить связи), Alt+Ctrl+Z (обход позиций правки), Alt+Shift+C (закрыть область проверки), Ctrl+Alt+Home (объект обзора), Ctrl+Shift+F8 (блочное выделение), Alt+Ctrl+Y (повторить поиск), Ctrl+Shift+F12 (печать), Alt+F8 (скрипты), Alt+F11 (редактор скриптов).

8.2 Отличия на macOS

Действие macOS Примечание
Все Ctrl+… ⌘+… кроме перечисленных ниже
Все Alt+… ⌥+…
Сохранить как ⌘+Shift+S F12 занят системой
Параметры ⌘+, как у всех приложений macOS
Закрыть окно / выход ⌘+W / ⌘+Q
Повторить ⌘+Y или ⌘+Shift+Z обе
Найти / заменить ⌘+F / ⌘+Shift+H Ctrl+H недоступно
Перейти к ⌘+⌥+G
Правописание / тезаурус ⌘+⌥+L / ⌘+⌥+Ctrl+R F7 требует fn
Очистить формат символов Ctrl+Space (если не занят переключением раскладки) или ⌘+\ отображаем доступную в меню
Начало / конец строки ⌘+← / ⌘+→ Home/End — прокрутка (системно); в настройках можно вернуть Windows-поведение
Начало / конец документа ⌘+↑ / ⌘+↓
По словам ⌥+← / ⌥+→
Удалить слово ⌥+Backspace / ⌥+fn+Backspace
Delete fn+Backspace
Сноска / комментарий ⌘+⌥+F / ⌘+⌥+A
Исправления вкл/выкл ⌘+Shift+E
Разрыв страницы ⌘+Enter
Неразрывный пробел ⌥+Space (системный) и ⌘+Shift+Space
Полноэкранный режим Ctrl+⌘+F системный
Эмодзи и символы Ctrl+⌘+Space системная панель, также ⌘+⌥+T — WR-44
Функциональные клавиши F1–F12 требуют fn (или настройка в системе) дублируем всё в меню
Контекстное меню Ctrl+клик / правый клик
Переключение окон документа `⌘+``
Скрытый текст только через диалог «Шрифт» (WR-30) ⌘+H занято системной командой «Скрыть», ⌘+Shift+H — замена (как в Word для Mac)

9. Критерии готовности 1.0 и риски

9.1 Критерии готовности (измеримые)

# Критерий Метрика Порог
1 Функции P0 реализованы и покрыты автотестами (unit/UI) 100 % P0 из раздела 2, 0 открытых дефектов S1/S2
2 Точность импорта .docx корпус 10 000 реальных документов: снимочное сравнение страниц с эталоном Word 365 (рендер в PNG 150 dpi, эталонные шрифты Liberation/Carlito), метрика «доля страниц с pixel-diff < 1 %» ≥ 98 % страниц; ≥ 99,5 % документов открываются без ошибок
3 Round-trip .docx открыть → сохранить → XML-diff нормализованный (без порядка атрибутов/ rsid) ≥ 99 % документов без потери элементов; визуально 100 % идентично после пересохранения
4 .doc, .rtf, .odt аналогично п.2 на корпусах 2 000 / 1 000 / 2 000 ≥ 95 % / 97 % / 97 % страниц
5 Производительность таблица 5.29, p95 на референсных машинах (Win: i5-8250U; mac: M1; Linux: Ryzen 5 3500U) все цели выполнены
6 Стабильность crash-free sessions (телеметрия по согласию + внутренняя), фаззинг парсеров docx/doc/rtf/odt/html ≥ 99,5 %; 72 ч фаззинга без падений/утечек
7 Автосохранение kill -9 в случайный момент набора (1000 прогонов) 100 % восстановлений, потеря ≤ 2 с
8 Печать 6 принтеров (лазер/струйный/сетевой PostScript) × 3 ОС + PDF-принтеры; тест-набор 50 документов 100 % совпадение с предпросмотром (визуальная проверка по чек-листу)
9 PDF veraPDF на 500 экспортах (PDF/A-2b и PDF/UA-1) 0 ошибок валидации; PAC 2024 — 0 ошибок UA на 100 документах
10 Доступность UI WCAG 2.1 AA чек-лист, NVDA/JAWS (Win), VoiceOver (mac), Orca (Linux) — сценарии «набор, навигация по заголовкам, таблица, комментарии» 100 % сценариев проходят; все элементы ленты доступны с клавиатуры
11 Локализация UI: ru, en — 100 %; de, fr, es, pt-BR, zh-CN — ≥ 95 %; проверка правописания: 40 языков —
12 Совместимость горячих клавиш таблица 8.1 100 % реализованы, автотест
13 MeridianScript API раздела 6 покрыт тестами; примеры 6.3 выполняются; транслятор VBA на корпусе 500 макросов покрытие API ≥ 90 %; ≥ 70 % макросов транслируются без ручной правки
14 Лицензирование флаги [Home]/[Pro] по библии 9: Free-режим без аккаунта работает полностью в рамках редакции 100 % зависимых функций через Features::has (lint-правило в CI)
15 Документация и шаблоны справка на ru/en, 12 шаблонов (включая ГОСТ 7.32, диссертация, договор), галерея в Hub —
16 Размер и установка установщики для 3 ОС (библия, 4), размер Write + Hub + ядро ≤ 250 МБ с шрифтами; холодный запуск ≤ 1,5 с

9.2 Риски и меры

Риск Вероятность / влияние Меры
Объём спецификации OOXML (6 000+ стр.) и недокументированные «compat»-особенности Word: сроки импорта растут высокая / высокое корпус 10 000 документов с частотным анализом элементов — реализуем по частоте; снимочные тесты с первого месяца; таблица compat-флагов по версиям Word; эксперт по OOXML в команде; «неизвестное сохраняем как есть»
Метрики шрифтов и интервалы «как в Word» (win/typo/hhea, external leading, exactly-обрезка) высокая / высокое формулы 5.9 калибруются по корпусу на эталонных шрифтах; тест-набор «интервалы» (200 документов) с pixel-порогом; флаги compat
Производительность холста в Qt Quick (текстуры, HiDPI, Wayland), задержка ввода средняя / высокое собственный QQuickItem с рендером в отдельном потоке (библия, 4), бюджет кадра 8 мс, профилирование на референсных машинах с фазы 0, fallback на CPU-растеризацию Skia
IME на Linux/Wayland (fcitx5, ibus) и реконверсия на macOS средняя / среднее ранние тесты с носителями CJK; Qt 6.8 LTS + патчи; оверлей-композиция 5.19 не зависит от модели
Кнут–Пласс/типографика Meridian vs ожидание «как в Word» средняя / среднее по умолчанию — Word-режим для импортированных документов; Meridian-режим — только в своих шаблонах с явным переключателем; предупреждение при сохранении в docx
Сложные письменности (индийские, тайский, кхмерский) и вертикальный CJK — ошибки шейпинга/разбиения средняя / среднее HarfBuzz + ICU4X сегментация; тест-корпуса Unicode (UDHR на 100 языках); вертикальный текст — P1
Правки/комментарии docx с расширениями w15/w16 — потеря данных при round-trip средняя / высокое ExtensionBag и durable_id; тесты round-trip на документах с 1 000+ правок
Доступность Qt на Linux (AT-SPI) слабее, чем на Windows/macOS средняя / среднее собственный accessibility-мост для холста (виртуальное дерево абзацев), тесты с Orca; участие в апстриме Qt
Печать на Linux (разнородные CUPS-драйверы) средняя / низкое путь «PDF в CUPS» как основной; растровый fallback
Hunspell для русского: слабая морфология, нет грамматики высокая / низкое словарь AOT/ru_RU расширенный; LanguageTool локально (P1); пользовательские словари организации
Фаза 6 (CRDT) потребует переделки модели, если сейчас не предусмотреть средняя / высокое модель 4.11 спроектирована под Yrs (операции по ParaId, относительные позиции); прототип синхронизации двух клиентов — в фазе 1 как внутренний тест
Транслятор VBA покрывает мало реальных макросов высокая / низкое ограничиваем обещание: «типовые макросы»; отчёт о непереведённом; рекордер действий как альтернатива
Юридические: шрифты (лицензии OFL/Apache — ок), патенты MS на отдельные UI-решения (ribbon — лицензия Office UI отозвана в 2010-х, ribbon общеупотребим), ГОСТ-криптопровайдеры (лицензии КриптоПро) низкая / среднее юридическая проверка перед релизом; ГОСТ-подпись только через системный провайдер пользователя
Нехватка людей: ядро текста + кодеки + UI одновременно высокая / высокое фазирование: сначала раскладка и docx-импорт (чтение), затем запись, затем рецензирование; еженедельные снимочные отчёты как объективная метрика прогресса
Размер документа в памяти при тысячах изображений низкая / среднее ленивые миниатюры, оригиналы с диска по требованию, лимиты кэшей 5.15

10. Вопросы к библии

  1. Ctrl+K. Библия (5.3) закрепляет Ctrl/⌘+K за командной палитрой, а в схеме Word это «Гиперссылка» (одна из самых частых команд). Предложение: в схеме «Word» по умолчанию — палитра Ctrl+K (как в библии), гиперссылка — Ctrl+Alt+K; добавить вариант схемы «Word строго», где Ctrl+K — гиперссылка, а палитра — Alt+Q (как «Что вы хотите сделать» в Office) и F1. Так же в этом документе (8.1). Нужно решение.
  2. Вкладка «Дизайн». В каркасе окна (5.3) вкладок ribbon восемь, без «Дизайн/Конструктор». Темы, наборы стилей, водяной знак, цвет и границы страницы размещены в «Макет → Оформление / Фон страницы» (3.2.4). Если владелец хочет 1:1 с Word 2013+, нужно добавить вкладку «Дизайн» в библию.
  3. Формулы и диаграммы в фазе 1. Math — фаза 4, Chart — фаза 2, но документы из Word с OMML-формулами и диаграммами должны открываться точно уже в Write 1.0 (критерий 9.1-2). Предложение: в фазу 1 включить «Math-рендер» (раскладка OMML/MathML без редактора) и «Chart-рендер» (отрисовка DrawingML-диаграмм без редактора) как подмножества соответствующих модулей; редакторы — по плану фаз. Отразить в 08-roadmap.md.
  4. Сравнение/объединение документов. Библия относит «рецензирование» к фазе 1; в этом документе сравнение — P0, объединение — P1. Подтвердить.
  5. Шрифт и интервалы «Обычного» стиля по умолчанию. Библия задаёт Carlito 11 pt (= Calibri). Для совместимости предлагается полностью повторить Word 2013+: интервал 1,08, после абзаца 8 pt; для русских шаблонов — PT Serif 12 pt, 1,15, 0/6 pt; для ГОСТ — Times New Roman→Liberation Serif 14 pt, 1,5. Закрепить в библии (5.2).
  6. Терминология. В Word RU «Исправления» (track changes), в этом документе в UI используется «Исправления», а в технических разделах — «правки». Предлагается закрепить: UI — «Исправления», документация/код — revisions.
  7. Диалоги WR-62 (редактор скриптов) — общее окно пакета; должно быть описано в 05-script-api.md, здесь только ссылка. Подтвердить, что общий документ API существует под этим именем (в библии он не перечислен).
  8. Расширение history/ в .mwx — библия описывает «журнал операций CRDT, опционально»; здесь добавлены history/snapshots/ для именованных версий (2.21). Если это противоречит замыслу — заменить на отдельный файл версий рядом с документом.
  9. Единицы раскладки. Библия единиц не задаёт; выбрано Lu = 1/64 pt (5). Если Sheets/Slides выберут иное (например, EMU или 1/20 pt), нужно унифицировать в 03-architecture.md.
  10. Главный документ (2.24) требует части subdoc/ в манифесте .mwx — дополнить 04-file-formats.md.