В экосистеме CLI на Rust тихо бушует эпидемия. Симптоматика: справочные тексты, от которых хочется проверить себя на дальтонизм, сообщения об ошибках, настолько плоские, что их можно подсунуть под дверь, и документация, чья стилистическая выразительность уступает кассовому чеку. При этом любой современный эмулятор терминала поддерживает жирный шрифт, курсив, цвет и Unicode-символы рисования рамок. Разрыв между тем, что терминал умеет показывать, и тем, что большинство CLI-инструментов соизволяют показывать, по масштабу сопоставим с Гранд-Каньоном и по глубине — с Марианским колодцем безразличия к пользователю.
Marcli этот разрыв закрывает. Берёт CommonMark Markdown, отдаёт стилизованный ANSI-вывод для терминала. Вы пишете Markdown. Ваши пользователи видят красиво оформленный текст. Вся идея — в одном предложении, и этого ровно достаточно.
Что оно делает
Marcli парсит Markdown через comrak — тот же парсер, что приводит в движение рендеринг на GitHub, — и обходит AST, порождая ANSI escape-последовательности для каждого поддерживаемого элемента. Подсветка синтаксиса в огороженных блоках кода обеспечивается syntect — те же грамматики, что стоят за Sublime Text. Никакого вызова Pygments, никаких загрузок в рантайме, никаких церемоний конфигурации. Просто работает — концепция, которую остальная экосистема, кажется, считает радикальной.
Один вызов функции:
let output = marcli::render("# Hello\n\nSome **bold** text.", &Default::default());println!("{}", output);
Это весь публичный API для типичного случая. Функция render принимает строку с Markdown и структуру RenderOptions, возвращает String с вшитыми ANSI-последовательностями. Никакого паттерна «строитель» с семнадцатью вызовами по цепочке. Никакого трейта, который нужно реализовать предварительно. Никакого асинхронного рантайма, затаившегося в дереве зависимостей и ждущего удобного случая сожрать вашу сборку.
Поддерживаемые элементы
Marcli обрабатывает полную спецификацию CommonMark плюс несколько расширений:
-
Заголовки — h1 отображается жёлтым жирным, h2 — голубым жирным, h3 и ниже — белым жирным. Три уровня визуальной иерархии без единой строчки конфигурации.
-
Строчное форматирование — жирный, курсив, зачёркнутый и строчный код, каждый со своим стилем ANSI.
-
Маркированные списки — с треугольными маркерами (▸) вместо привычного ASCII-дефиса. Мелочь, но читаемость подскакивает непропорционально.
-
Нумерованные списки — обведённые числа-глифы (①, ②, ③, …) до двадцати пунктов. После двадцати — числа в скобках. Пока никто не жаловался, впрочем, никто и не составляет списков из двадцати одного пункта, если он в здравом уме.
-
Огороженные блоки кода — в рамке из Unicode-символов рисования с заголовком языка. При указании языка подсветка синтаксиса включается автоматически.
-
Цитаты — с тусклой вертикальной чертой слева, визуально отличимые от окружающего текста без истерики.
-
Таблицы — полноценные рамки из символов рисования с корректным выравниванием столбцов (по левому краю, по правому, по центру). Заголовки выделены жирным.
-
Списки задач — маркеры с чекбоксами (☑ / ☐).
-
Тематические разделители — тусклая горизонтальная линия из символа рисования.
-
Ссылки — текст подчёркнут синим, за ним URL приглушённым шрифтом.
-
Изображения — альтернативный текст в скобках с URL, поскольку терминалы, вопреки нашим отчаянным усилиям, всё ещё не являются программами просмотра изображений.
Это не подмножество для бедных. Это всё, что разумный человек захотел бы отобразить в терминале, и ничего сверх того.
Подсветка синтаксиса, которая работает без конфигурации
Огороженные блоки кода с указанием языка автоматически подсвечиваются встроенными грамматиками syntect. Rust, Python, Elixir, JavaScript, Go, C, SQL, TOML, YAML, сам Markdown — если syntect знает язык, Marcli его подсветит. Если не знает — блок отрисовывается как простой стилизованный текст в той же рамке. Никаких падений, предупреждений и деградации опыта — просто код без цветов. Революционная концепция «не падать от незнакомого входа», к которой индустрия идёт, судя по всему, со скоростью геологических процессов.
fn main() { let md = "```rust\nfn greet(name: &str) {\n println!(\"Hello, {}!\", name);\n}\n```"; let output = marcli::render(md, &Default::default()); println!("{}", output);}
Ключевые слова окрашиваются в один цвет, строки — в другой, комментарии приглушены и выделены курсивом, имена функций выделяются на общем фоне. Маппинг из TextMate-скоупов syntect в ANSI-последовательности встроен и покрывает все основные категории токенов: ключевые слова, строки, числа, комментарии, операторы, имена, модификаторы хранения, имена сущностей, макросы поддержки, константы, переменные и маркеры диффов. Каждая категория имеет разумное значение по умолчанию, и каждую можно переопределить.
Подсветку синтаксиса можно выключить целиком:
let mut theme = marcli::Theme::default();theme.syntax_highlight = false;let opts = marcli::RenderOptions { theme, ..Default::default() };
Полная настройка цветовых тем через TOML
Каждый визуальный аспект вывода Marcli контролируется структурой Theme: цвета заголовков, маркеры списков, рамки блоков кода, символы таблиц, стили токенов подсветки синтаксиса, ширина тематического разделителя, префикс цитат — всё. Значения по умолчанию разумны, но если вам нужны корпоративные цвета или высококонтрастная тема для доступности, вы переопределяете ровно те поля, которые вас интересуют. Остальные — не трогаете. Marcli не из тех библиотек, которые при изменении одного параметра требуют пересобрать всю конфигурацию с нуля, бормоча что-то про «целостность».
Темы загружаются из TOML-файлов:
let theme = marcli::Theme::load(".marcli.toml").unwrap_or_default();let opts = marcli::RenderOptions { theme, ..Default::default() };let output = marcli::render(markdown, &opts);
Частичный TOML-файл переопределяет только указанные поля; всё остальное остаётся по умолчанию. Неизвестные ключи молча игнорируются, так что темы остаются совместимыми с будущими версиями.
Система тематизации — не костыль, прикрученный задним числом через feature-флаги. Это архитектура. Рендерер не содержит ни одной захардкоженной ANSI-последовательности — каждый стиль читается из структуры Theme в момент рендеринга. Это означает, что можно породить полностью нестилизованный вывод, выставив все стили в пустые строки, или подстроить вывод под возможности конкретного терминала, изменив только нужные последовательности. Дизайн настолько очевидный, что удивительно, почему его приходится объяснять.
Поддержка CRLF для xterm.js и веб-терминалов
Если ваш CLI-инструмент работает ещё и в веб-терминале (xterm.js, к примеру), переводы строк имеют значение. Marcli позволяет задать последовательность переноса:
let opts = marcli::RenderOptions { newline: "\r\n".into(), ..Default::default()};let output = marcli::render(markdown, &opts);
Каждый внутренний перенос строки — между элементами списка, внутри блоков кода, между абзацами — использует сконфигурированную последовательность. Никакого постобработочного регулярного выражения. Никакого «заменим \n на \r\n и будем молиться». Корректно по построению — подход, который вызывает у некоторых разработчиков состояние, близкое к культурному шоку.
Удаление ANSI для чистого текста
Иногда нужен структурированный рендеринг без цветов — для логирования, для перенаправления в файл, для инструментов доступности, которые давятся escape-последовательностями. Опция escape_sequences решает это:
let opts = marcli::RenderOptions { escape_sequences: false, ..Default::default()};let output = marcli::render(markdown, &opts);// output содержит структурированный текст// с маркерами и отступами, но без ANSI-кодов
Marcli также экспортирует strip_ansi как публичную функцию, если нужно очистить от escape-последовательностей произвольный текст:
let plain = marcli::strip_ansi(some_ansi_string);
История зависимостей
Marcli зависит от шести крейтов: comrak (парсинг Markdown), syntect (подсветка синтаксиса), serde и toml (сериализация тем), regex (удаление ANSI) и once_cell (ленивые статики). Все с отключёнными по возможности фичами по умолчанию. Никакого tokio. Никакого hyper. Никакого tower. Никакого serde_json. Никакого комбинаторного взрыва feature-флагов. Никаких процедурных макросов сверх того, что требует serde.
Дерево зависимостей узко по замыслу. CLI-инструмент, добавляющий Marcli для рендеринга вывода --help или диагностики ошибок, не наследует внезапно HTTP-клиент и TLS-стек. В мире Rust, где добавление крейта для форматирования даты иногда утягивает за собой половину сетевого стека, это заслуживает отдельного упоминания — и, пожалуй, стаканчика шампанского.
Почему Marcli, а не альтернативы
Экосистема Rust предлагает несколько подходов к стилизованному выводу в терминал, и каждый решает свою задачу. Беда в том, что большинство из них решают не вашу.
Крейты для «покраски» (например, colored, owo-colors, ansi_term) — это кисточки. Они позволяют раскрасить отдельные строки. Они не парсят структуру, не обрабатывают списки, не рендерят таблицы, не подсвечивают код. Если ваш контент уже структурирован, всю логику форматирования вы пишете сами. Marcli берёт неструктурированный Markdown и порождает полностью структурированный терминальный вывод. Совершенно другой слой абстракции. Сравнивать их — всё равно что спрашивать, что лучше: молоток или дом.
TUI-фреймворки (например, ratatui, tui-rs) — это полноценные инструментарии терминального UI. Они управляют компоновкой, виджетами, циклами событий, альтернативными экранными буферами. Если вы строите интерактивное приложение — они превосходны. Если вам нужно напечатать стилизованную справку и выйти — это кувалда для канцелярской кнопки. Marcli — инструмент размером с канцелярскую кнопку. Никаких претензий на большее, и слава богу.
termimad — ближайшая альтернатива. Тоже рендерит Markdown в терминале. Но termimad использует собственный парсер (не совместимый с CommonMark), имеет другую (и более непрозрачную) модель тематизации и не предоставляет подсветку синтаксиса через syntect. Marcli использует comrak для парсинга (CommonMark, совместимый с GitHub), syntect для подсветки (грамматики Sublime Text) и выставляет каждый визуальный параметр через плоскую, сериализуемую структуру Theme. Если вы цените соответствие спецификации и конфигурируемость — выбор прозрачен.
mdcat — самостоятельный инструмент командной строки, а не библиотека. Вы не можете встроить его в свой бинарник и вызвать render(). Он или запускается как внешний процесс, или требует наличия своего бинарника на машине пользователя. Marcli — библиотека: добавьте в Cargo.toml, вызовите функцию, получите строку. Без обязательств по совместному проживанию.
Аргумент в пользу лёгкости
Каждая Rust-библиотека для CLI, которая общается с людьми, сталкивается с одним и тем же вопросом: как подать структурированный текст? Ответ почти всегда один из трёх: ⓐ выплюнуть голый текст и надеяться, что пользователь его прочтёт (оптимизм, граничащий с клиническим), ⓑ вручную накатать ANSI-форматирование десятками вызовов format! с захардкоженными escape-последовательностями (ремесленничество с примесью мазохизма), ⓒ подтянуть фреймворк, который весит больше самого приложения (архитектурное решение из мира node modules).
Marcli предлагает вариант ⓓ: писать сообщения, справочные тексты, пояснения к ошибкам, журналы изменений и диагностику на Markdown — формате, который ваша команда уже знает, который ваш редактор уже подсвечивает, который ваш документационный конвейер уже рендерит, — и одним вызовом функции превращать это в терминальный вывод, который люди хотят читать.
Цена — один вызов функции и шесть транзитивных зависимостей. Выгода — каждый фрагмент текста, который ваш CLI выдаёт в мир, может быть жирным, курсивным, с подсветкой синтаксиса, маркированным, табличным и цитированным, с полной поддержкой тем и нулевым ручным жонглированием ANSI-последовательностями.
Ваш терминал заслуживает лучшего, чем println!. Marcli — способ дать ему это лучшее. А если не заслуживает — ну, хотя бы ваши пользователи заслуживают.
Ссылки
▸ Репозиторий: github.com/Oeditus/marcli-rust
▸ Документация: docs.rs/marcli
▸ Крейт: crates.io/crates/marcli
ссылка на оригинал статьи https://habr.com/ru/articles/1038706/