Сингулярность Markdown: Почему генерация HTML — это тупик

 · 3 min read

В конце прошлой части мы сидели с идеально работающим, покрытым тестами пайплайном. Наш конвертер успешно брал XML от Doxygen и генерировал кастомные HTML-страницы для API-документации. Команда уже готовилась открывать шампанское и катить всё это в продакшен, когда к нам подошел Infrastructure Lead. Он задал один простой, но абсолютно разрушительный вопрос: «А зачем вы до сих пор носитесь с генерацией HTML, если существуют готовые вещи вроде Hugo, Docusaurus или VitePress?»

Ловушка чистого HTML

Ловушка HTML

Нам хватило ровно минуты, чтобы осознать всю масштабность своей ошибки. Генерация чистого HTML звучит классно только до того момента, пока ты не попытаешься сделать из него современный портал для разработчиков. Оказалось, что мы собирались заново реализовывать практически каждую стандартную веб-фичу. Глобальный поиск требовал написания собственного индексатора. Для навигации нам пришлось бы вручную поддерживать консистентный сайдбар на сотнях сгенерированных страниц, а синхронизация стилей с корпоративным CSS грозила превратиться в бесконечную работу. Вместо системы документирования мы зачем-то начали писать свой собственный веб-фреймворк.

Пробуждение SSG

Готовые генераторы статических сайтов вроде Hugo или Docusaurus решают все эти проблемы из коробки (у них есть поиск, встроенный роутинг и системы плагинов). Единственная проблема заключалась в том, что на вход они едят исключительно Markdown. Пришлось резко менять планы и переделывать пайплайн с Doxygen XML ➡️ HTML на Doxygen XML ➡️ Markdown.

Переписывание выходного слоя прошло на удивление безболезненно. Ещё на этапе генерации HTML мы жестко разделили логику парсинга и отрисовки, введя промежуточный слой данных, который мы назвали Intermediate Representation (или просто IR). Это гарантировало работоспособность парсера, пока мы откручивали генератор HTML и прикручивали вместо него Markdown. Да и практика TDD тоже сыграла свою роль: благодаря ей генератор Markdown был написан быстро и уверенно. В процессе работы над новым экспортером внезапно сложились куски пазла.

Сингулярность движка

Архитектура Flude

Наша архитектура теперь состояла из двух изолированных фаз. Сначала Parser читает исходники и собирает абстрактное дерево (тот самый IR), а затем Renderer берет готовое дерево и перегоняет его в Markdown. В этот момент до нас дошло: рендереру абсолютно наплевать, откуда вообще взялись эти данные.

Мы вспомнили ситуацию из второй статьи, когда ИИ попытался выбросить Doxygen и начал парсить питоновский код напрямую через tree-sitter. Тогда мы испугались потери контроля и откатились к парсингу XML (хотя ИИ предлагал рабочий вариант). Теперь же, при наличии жесткого TDD-контракта и промежуточного слоя IR, бояться стало нечего. Мы могли спокойно выбросить Doxygen и напрямую тянуть данные из исходников.

Использование IR бонусом открыло дорогу к кэшированию и ускорению генерации. Если на первом запуске мы полностью строим дерево в памяти, нет никакого смысла пересобирать его целиком при следующем прогоне. Достаточно использовать инкрементальный парсинг и обновлять только те куски, которые реально изменились. Ну а если мы сможем напрямую читать Python, ничто не мешает нам читать наш основной C++ SDK и другие наши языки. А может быть, даже — страшно сказать — и не наши языки.

Именно в этот момент наш узкоспециализированный скрипт превратился в полноценный Universal Documentation Engine (UDE). Правда, название пришлось немного удлинить из-за существующей торговой марки немецкого отладчика Universal Debug Engine. Мы добавили характеристики Fast и Layered, и так на свет появился Flude. Теперь мы можем воткнуть на вход любой парсер, а бэкенд пайплайна соберет из этого красивый Markdown для нашего SSG (с общим поиском и нормальным дизайном).

От локальной задачи к масштабному проекту

Масштабирование

Крошечная локальная утилита для парочки питоновских модулей внезапно дала нам шанс с нуля спроектировать масштабируемую архитектуру. Работа над таким продуктом означала, что мы больше не можем сидеть на ручных сборках или запускать скрипты локально на своих ноутбуках. Нам потребовался полноценный автоматизированный процесс CI/CD.

Разбираться с этим вручную не пришлось — львиную долю работы по настройке взял на себя мой ИИ-напарник. В следующем эпизоде я расскажу, почему мы решили строить пайплайны именно на GitHub Actions.