dotnet-typographer 0.1.0

dotnet tool install --global dotnet-typographer --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local dotnet-typographer --version 0.1.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=dotnet-typographer&version=0.1.0
                    
nuke :add-package dotnet-typographer --version 0.1.0
                    

Typographer

CI

Документация: https://gberikov.github.io/Typographer/ · справочник правил

Типограф для русского языка на .NET: кавычки-ёлочки, тире, неразрывные пробелы, работает и с обычным текстом, и с HTML-фрагментами.

Статус: реестр закрыт — 107 правил, шесть фаз конвейера, пять пакетов. Не реализованы три правила, которые не могут быть реализованы без нарушения гарантий (common/punctuation/quoteLink, common/html/stripTags, common/html/processingAttrs), и отложено ru/typo/switchingKeyboardLayout. Публикации в NuGet ещё не было.

Использование

Быстрый старт — статический фасад с настройками по умолчанию:

using Typographer;

string html = Typographer.Html("Он сказал: \"Привет!\" - и махнул рукой.");
string text = Typographer.PlainText("Он сказал: \"Привет!\" - и махнул рукой.");

Настраиваемый вариант — свой набор правил, кодирование сущностей, перенос строк и абзацы:

using Typographer;
using Typographer.Rules;

var typographer = new HtmlTypographer(new HtmlOptions
{
    Rules = RuleSet.Default.Without(RuleId.Ru.Nbsp.Initials),
    Entities = EntityMode.Named,
    UseBr = true,
    MaxNobr = 3,
});

string html = typographer.Process("Он сказал: \"Привет!\" - и махнул рукой.\nВторая строка.");

Для обычного текста — TextTypographer и TextOptions (без Entities, UseBr, UseP, MaxNobr: они имеют смысл только в HTML, поэтому в TextOptions их физически нет):

var typographer = new TextTypographer(new TextOptions { Rules = RuleSet.Minimal });
string text = typographer.Process("Он сказал: \"Привет!\" - и махнул рукой.");

Обе точки входа умеют писать результат прямо в приёмник без промежуточной строки:

using System.Buffers;

var writer = new ArrayBufferWriter<char>();
HtmlTypographer.Default.Process("Он сказал: \"Привет!\"".AsSpan(), writer);

Наборы правил

RuleSet — иммутабельное множество включённых правил с готовыми пресетами:

Пресет Смысл
RuleSet.Default безопасная типографика — пресет по умолчанию
RuleSet.Minimal только кавычки, тире и многоточие
RuleSet.All все зарегистрированные правила, включая ещё не реализованные
RuleSet.None ничего не менять
RuleSet.Lebedev, RuleSet.Gost, RuleSet.Typograf пока совпадают с Default — правила, которые должны их различать, ещё не реализованы (см. XML-комментарии на этих пресетах)
RuleSet rules = RuleSet.Default.With(RuleId.Ru.Dash.Years).Without(RuleId.Ru.Nbsp.Abbr);
bool enabled = rules.Contains(RuleId.Ru.Dash.Years);

Идемпотентность и разметка

Правила RuleSet идемпотентны: повторный прогон ничего не меняет. Исключение — опции UseBr и MaxNobr: они рассчитаны на однократное применение к исходному тексту. Прогон по СОБСТВЕННОМУ ВЫВОДУ типографа с той же опцией вложит разметку в саму себя (<nobr><nobr>текст</nobr></nobr>) — типограф не распознаёт свой прошлый вывод, это намеренное решение (см. docs/spec.md, гарантия 5).

UseP в это исключение не входит: абзацы размечаются по документу целиком, а не по каждому текстовому узлу, и во входе с готовой блочной разметкой (<p>, <ul>, <div>…) опция не применяется — границы абзацев там задаёт сама разметка. Незакрытые теги и защищённые области также отключают UseP, чтобы добавленный </p> не оказался внутри атрибута, комментария или скрипта. UseBr обрабатывает только текст вне тегов и защищённых областей.

BOM (U+FEFF) удаляется только в начале документа. Внутри текста этот символ сохраняется: его удаление могло бы превратить текст в HTML-тег или сущность.

Целевые платформы

TFM Зачем
netstandard2.0 .NET Framework 4.6.1+, Unity, Xamarin, старые библиотеки
net8.0 текущая LTS
net10.0 актуальная LTS, Span/SearchValues, AOT

Пакеты

Пакет Зачем Зависимости Платформы
Typographer ядро: HTML и обычный текст, правила, пресеты нет на net8.0 и net10.0; System.Memory на netstandard2.0 netstandard2.0, net8.0, net10.0
Typographer.DependencyInjection AddTypographer() Microsoft.Extensions.DependencyInjection.Abstractions netstandard2.0, net8.0, net10.0
Typographer.AspNetCore тег-хелпер <typographer> и IHtmlContent ASP.NET Core net8.0, net10.0
Typographer.Markdig типографика Markdown Markdig netstandard2.0, net8.0, net10.0
dotnet-typographer утилита командной строки ядро net10.0

Контейнер

services.AddTypographer();                                        // настройки по умолчанию
services.AddTypographer(new HtmlOptions { Entities = EntityMode.Named });

Регистрируются одиночками HtmlTypographer и TextTypographer. Своя регистрация, сделанная раньше, побеждает: внутри TryAddSingleton.

ASP.NET Core

@addTagHelper *, Typographer.AspNetCore

<typographer><p>Он - человек и "цитата"</p></typographer>

Тег-хелпер типографирует содержимое и исчезает сам. Требует services.AddTypographer(). Там, где удобнее вызов, а не элемент:

@inject HtmlTypographer Typographer
@Typographer.ToHtmlContent(Model.Text)

Markdown

MarkdownPipeline pipeline = new MarkdownPipelineBuilder().UseTypographer().Build();
string html = Markdown.ToHtml(source, pipeline);

Правки вносятся в дерево документа после разбора, а не в готовый HTML: рендерер кодирует прямую кавычку в &quot;, и в отрендеренном HTML ёлочки уже не появились бы. Поэтому код, адреса ссылок и встроенный HTML остаются нетронутыми, а кавычки вокруг разметки — "**слово**" — смотрят в разные стороны.

Командная строка

dotnet tool install --global dotnet-typographer
echo 'Он - человек' | dotnet-typographer
dotnet-typographer --entities named --in-place статья.html
dotnet-typographer --help

Разработка

dotnet build
dotnet test

Четыре уровня проверки, три из них в CI:

Уровень Что проверяет Команда
Unit и property правила поштучно, гарантии, каждое правило реестра в одиночку dotnet test
Golden-корпус tests/Typographer.Corpus — пары «вход — эталон» dotnet test
Снимок оракула наши расхождения с typograf.artlebedev.ru (без сети) dotnet test
Живая сверка не устарел ли сам снимок (с сетью, вне CI) TYPOGRAPHER_ORACLE=1 dotnet test tests/Typographer.Oracle
Фаззинг конвейер не падает ни на каком входе workflow Fuzz, по расписанию

Эталоны корпуса и список расхождений перегенерируются переменными TYPOGRAPHER_UPDATE_CORPUS=1 и TYPOGRAPHER_UPDATE_ORACLE=1. Тест при этом падает намеренно: перезапись эталона — не проверка.

Производительность

Бенчмарки — bench/Typographer.Bench (BenchmarkDotNet). Запуск: dotnet run -c Release --project bench/Typographer.Bench.

Последний замер — docs/perf.md. Коротко, на входе в 22 200 символов:

Сценарий Пропускная способность Аллокации
Html(string) 28,0 млн симв./с сама возвращаемая строка
Html(span, IBufferWriter) 27,3 млн симв./с 0 байт

Обещание про ноль аллокаций выполнено. Обещание про 50 млн символов в секунду — нет: получается 28. Разрыв записан и объяснён в docs/perf.md; там же сравнение с замером плана 1, когда правил было четырнадцать вместо ста семи.

Ветвление — git flow

  • master — релизы, тег вида 1.2.3 на каждый релиз (версия пакета — MinVer);
  • develop — интеграционная ветка;
  • feature/*, release/*, hotfix/* — как в git flow.

master, develop и теги *.*.* защищены правилами репозитория: только PR, без force-push и удаления.

Релиз: тег вида 0.1.0 (без префикса v) на master запускает workflow Release — он собирает, прогоняет тесты, упаковывает пять пакетов, публикует их в NuGet и создаёт релиз на GitHub. Для публикации нужен секрет репозитория NUGET_API_KEY; без него workflow останавливается с понятной ошибкой, а не публикует половину.

Лицензия

MIT

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.1.0 104 9/10/2026