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
- WR-06 Режим чтения: лента скрыта, верхняя полоска с кнопками (Файл, Сервис: найти/поиск в интернете/ Assist; Вид: колонки 1/2, ширина, цвет страницы (белый/сепия/тёмный), шрифт, поля, озвучить (P2); Закрыть); страницы «листаются» как экран, а не как печатные страницы; изображения увеличиваются по двойному клику; комментарии можно оставлять; редактирование запрещено.
- WR-07 Режим структуры: лента заменяется вкладкой «Структура»: уровень (выпадающий), повысить/понизить, до основного текста, вверх/вниз, развернуть/свернуть, показать уровень (1–9/все), ☐ показать форматирование, ☐ только первую строку; группа «Главный документ»: показать документ, создать/вставить/разъединить/объединить/ разбить/заблокировать; закрыть структуру. Абзацы показаны с маркерами уровня (⊕/−/•), перетаскивание веток.
- WR-08 Фокус-режим: холст на весь экран, фон темнее, лента и панели скрыты, появляется при наведении на верх; «пишущая машинка» (текущая строка в центре), подсветка текущего абзаца/предложения (опция), счётчик слов и цель на день; выход — Esc.
- WR-09 Веб-документ / Черновик: веб — одна колонка на всю ширину, без полей и колонтитулов, фон страницы; черновик — только текст без плавающих объектов (их место обозначено), сноски в разделённой области, быстрый режим для очень длинных документов, разрывы страниц/разделов — пунктирные линии с подписью.
4. Модель данных
(Rust, крейт meridian-text-model)
4.1 Принципы
- Модель повторяет семантику OOXML там, где от этого зависит
точность импорта (абзац как единица, свойства раздела у
последнего абзаца, поля как маркеры начало/разделитель/конец, нумерация
«абстрактное определение
- экземпляр», toggle-свойства), но хранится компактнее и
типизированно. Так round-trip
.docxне теряет структуру, а.mwxостаётся простым.
- экземпляр», toggle-свойства), но хранится компактнее и
типизированно. Так round-trip
- Абзац — единица хранения, раскладки, проверки правописания, undo-коалесцирования и CRDT. Всё, что меняется при наборе, локализовано в одном абзаце; документ — упорядоченное дерево абзацев и таблиц.
- Свойства интернированы. Наборы символьных/абзацных
свойств — неизменяемые значения в таблице хэш-консинга
(
PropsInterner), спаны ссылаются наCharPropsId(u32). Сравнение — по id, память — O(уникальных наборов). - Позиции стабильны. Внешние ссылки (закладки,
комментарии, якоря, каретки соавторов, кэш раскладки) указывают на
ParaId+ смещение в символах с гравитацией, а не на индексы в векторах. - Все изменения — операции (
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; }Правила:
- Каждая
Opобратима (invert), хранит достаточно данных для отката без чтения модели (removed,prev). UndoStackхранит транзакции; коалесцирование набора — покаcoalesceсовпадает и прошло < 1 с или до границы слова/абзаца; максимум — не ограничен (память ограничивается размером журнала 256 МБ со сбросом старых вhistory/).- Операции позиционно независимы от других абзацев
(адресуются
ParaId), поэтому применимы к модели, изменённой в других абзацах — основа для CRDT и для совместного undo (каждый автор отменяет только своё). - Трансляция в Yrs (фаза 6):
Document↔︎YDoc { body: YXmlFragment },Paragraph↔︎YXmlTextс атрибутами (CharPropsсериализуются как JSON-патчи, не какCharPropsId), блоки/строки/ячейки ↔︎YXmlElement, свойства ↔︎ атрибуты элементов, закладки/комментарии ↔︎YMapс относительными позициями (RelativePositionYrs = нашиAnchor). ОперацииOpприменяются к модели и кYDocв одной транзакции; входящие удалённые изменения приходят как diffYDoc→ переводятся вOpбез записи в undo-стек автора.
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 Фоновая раскладка длинных документов
- Поток
layout-workerна документ (в процессе документа); UI-поток держит снимокArc<LayoutSnapshot>(persistent-структуры:im::Vector<PageLayout>), новый снимок публикуется атомарно после каждой страницы. - Приоритет: сначала видимая область ± 2 страницы (от каретки или
прокрутки), затем остаток документа вперёд, затем назад от видимой
области, если пользователь прыгнул в середину (стартуем от ближайшего
Checkpointили от начала раздела с оценочным состоянием и помечаем страницы «приблизительными»). - Высота ещё не разложенных блоков оценивается
(
chars / avg_chars_per_line × line_h) для корректного ползунка прокрутки; номер страницы в строке состояния — «≈ 312» до завершения. - Отмена:
CancelTokenпроверяется после каждого абзаца; правка во время фоновой раскладки →dirty+ рестарт от точки правки, без потери страниц до неё. - Открытие 500 стр: парсинг docx ≈ 150 мс, стили/нумерация ≈ 20 мс, первые 3 экрана ≈ 120 мс, полная раскладка в фоне ≈ 1,2 с (цель < 2 с до «все номера страниц точны»).
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», «Сохранить копию .mwx и .docx», «Отмена». Повторно — только при появлении новых потерь.
- Поля, которых нет в формате, записываются как кэшированный результат
(текст), в
.docx— ещё и какw:fldSimpleс нашим кодом вw:instrтолько если Word его не «сломает» (неизвестные поля Word показывает какError! Unknown op codeпосле F9 — поэтому для них пишем текст + customXml-метку для обратного импорта). - Все наши расширения в
.docxкладутся вcustomXml/meridian.xml(urn:meridian:office:write:1:ext), чтобы Word их сохранял, а мы — восстанавливали (OpenType-теги, типографика, статусы, ГОСТ-библиография).
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 | 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. Вопросы к библии
Ctrl+K. Библия (5.3) закрепляетCtrl/⌘+Kза командной палитрой, а в схеме Word это «Гиперссылка» (одна из самых частых команд). Предложение: в схеме «Word» по умолчанию — палитраCtrl+K(как в библии), гиперссылка —Ctrl+Alt+K; добавить вариант схемы «Word строго», гдеCtrl+K— гиперссылка, а палитра —Alt+Q(как «Что вы хотите сделать» в Office) иF1. Так же в этом документе (8.1). Нужно решение.- Вкладка «Дизайн». В каркасе окна (5.3) вкладок ribbon восемь, без «Дизайн/Конструктор». Темы, наборы стилей, водяной знак, цвет и границы страницы размещены в «Макет → Оформление / Фон страницы» (3.2.4). Если владелец хочет 1:1 с Word 2013+, нужно добавить вкладку «Дизайн» в библию.
- Формулы и диаграммы в фазе 1. Math — фаза 4, Chart
— фаза 2, но документы из Word с OMML-формулами и диаграммами должны
открываться точно уже в Write 1.0 (критерий 9.1-2). Предложение: в фазу
1 включить «Math-рендер» (раскладка OMML/MathML без редактора) и
«Chart-рендер» (отрисовка DrawingML-диаграмм без редактора) как
подмножества соответствующих модулей; редакторы — по плану фаз. Отразить
в
08-roadmap.md. - Сравнение/объединение документов. Библия относит «рецензирование» к фазе 1; в этом документе сравнение — P0, объединение — P1. Подтвердить.
- Шрифт и интервалы «Обычного» стиля по умолчанию. Библия задаёт 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).
- Терминология. В Word RU «Исправления» (track
changes), в этом документе в UI используется «Исправления», а в
технических разделах — «правки». Предлагается закрепить: UI —
«Исправления», документация/код —
revisions. - Диалоги
WR-62(редактор скриптов) — общее окно пакета; должно быть описано в05-script-api.md, здесь только ссылка. Подтвердить, что общий документ API существует под этим именем (в библии он не перечислен). - Расширение
history/в.mwx— библия описывает «журнал операций CRDT, опционально»; здесь добавленыhistory/snapshots/для именованных версий (2.21). Если это противоречит замыслу — заменить на отдельный файл версий рядом с документом. - Единицы раскладки. Библия единиц не задаёт; выбрано
Lu = 1/64 pt(5). Если Sheets/Slides выберут иное (например, EMU или 1/20 pt), нужно унифицировать в03-architecture.md. - Главный документ (2.24) требует части
subdoc/в манифесте.mwx— дополнить04-file-formats.md.