Архитектура

Один проход, шесть фаз

Вход читается один раз. Порядок фаз жёсткий, внутри фазы правила независимы.

# Фаза Что делает
1 Prepare метка порядка байт, табуляция, декодирование типографских сущностей
2 Protect вход делится на текстовые узлы, разметку и защищённые зоны
3 Scan посимвольный проход: кавычки, тире, апостроф, многоточие, символы, числа, пробелы
4 Bind словарный проход по словам: неразрывные пробелы, сокращения, инициалы, единицы
5 Layout неразрывные блоки, висячая пунктуация, переносы, абзацы
6 Emit кодирование по EntityMode и запись в приёмник

Protect не отдельный проход по документу: сегментация вызывается ВНУТРИ каждой фазы — фаза сама идёт по сегментам, копирует разметку байт в байт и применяет свою логику только к текстовым узлам.

Правило попадает в ту фазу, в которой его результат успевают увидеть остальные. Замена табуляции на пробелы, например, живёт в Prepare, а не среди правил пробелов: сделанная позже, она оставила бы новые пробелы невидимыми для фаз Scan и Bind — и второй прогон дал бы результат, отличный от первого.

Буфер, который можно править задним числом

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

Буфер — изменяемая структура, передаётся только по ссылке и никогда не боксируется: иначе обещание про ноль аллокаций не выполнялось бы.

Правила как биты

RuleSet — иммутабельная маска на 128 бит (два ulong). Проверка «включено ли правило» стоит одну инструкцию и не аллоцирует, поэтому её можно делать в горячем цикле для каждого символа. RuleId.Index — позиция бита; индекс 0 зарезервирован под default(RuleId), чтобы значение по умолчанию не совпадало ни с одним правилом.

Реестр закрыт: 107 правил, имена совпадают с именами JS-typograf.

Гарантии

  1. Время работы линейно от длины входа.
  2. Исключений нет ни на каком входе, кроме объявленного OutputTooLargeException.
  3. Разметка копируется байт в байт. Исключение — правило common/html/escape, которое для того и существует.
  4. RuleSet.Default никогда не превращает текст в разметку.
  5. Повторный прогон по собственному выводу ничего не меняет. Исключения объявлены: UseBr, MaxNobr, common/nbsp/replaceNbsp, common/html/nbr, common/html/escape.
  6. Если правок нет, Process(string) возвращает тот же экземпляр строки.
  7. Ломаный UTF-16 проходит насквозь без исключения и без потери символов.

Гарантии проверяются тестами, а не декларируются: см. tests/Typographer.Tests/Guarantees.

Как это проверяется

Уровень Что ловит
Unit и property правила поштучно и каждое правило реестра в одиночку
Golden-корпус связный текст целиком, пары «вход — эталон» в файлах
Справочник правил правило, добавленное в реестр мимо документации
Снимок оракула расхождения с typograf.artlebedev.ru, закреплённые таблицей
Живая сверка не устарел ли сам снимок (с сетью, вне CI)
Фаззинг падение конвейера на произвольном входе