Serv 0.3: локальный веб-сервер с поддержкой Markdown
Всем привет!
Как часто у вас бывало так, что у вас есть ворох markdown файлов в одной папке с кучей документации и её надо прочитать? Приходилось ли вам читать сырые markdown файлы в вашем любимом редакторе? Если так, то этот пост для вас.
Версия serv получила обновление v0.3, в котором мы научили его отдавать такую папку как сайт. Без конфига, без генератора статики, без npm install. Один бинарник, флаг -m, и документация читается в браузере.
serv — это маленький сервер для локальной разработки: показываете ему папку, открываете адрес. Раньше он умел отдавать HTML, картинки и всё остальное, а .md честно отдавал как text/markdown — то есть браузер предлагал его скачать. Теперь не предлагает.
Установка и первый запуск
Homebrew ставит готовый бинарник, компилировать нечего:
brew install jwo1f/tap/serv
Или через cargo, если так привычнее:
cargo install --git https://github.com/JWo1F/serv
Исходники — https://github.com/JWo1F/serv. В репозитории лежит папка example, специально набитая всеми видами блоков; дальше все картинки в статье — это она.
serv -m example
serv▌ 0.3.0
╭──────────────────────────╮
│ http://127.0.0.1:8099/ │
╰──────────────────────────╯
root ~/work/jwo1f/serv/example
contents 2 folders · 2 files · 4.2 kB
index no index.html · folders get a generated listing
not found built-in page
urls clean · /about serves about.html
markdown rendered · folders fall back to index.md, then README.md · code plain
encoding gzip · text between 1 KiB and 8 MiB
logs on
ctrl-c to stop
Открываете адрес и видите README — сверстанный, а не в виде решёток и звёздочек.

Файл читается с диска на каждый запрос — ничего не кэшируется внутри сервера. Поправили абзац, нажали F5, увидели правку. Это тот же принцип, по которому serv отдаёт и все остальные файлы, просто теперь он касается и документации.
Флаг -m не добавляет режим, он расширяет правила
Самое важное решение тут — не рендерер, а маршрутизация. serv и раньше умел чистые URL: /about отдаёт about.html, а /about.html редиректит на /about, чтобы у страницы был один адрес, а не два. Markdown не получил собственных правил — он встал в те же самые.
| Запрос | Что отдаётся |
|---|---|
/about |
about.html, а если его нет — отрендеренный about.md |
/about.md |
301 на /about |
/docs/ |
index.html, затем index.md, затем README.md, затем листинг папки |
/docs |
301 на /docs/ |
HTML выигрывает всегда. Если рядом лежат about.html и about.md, отдаётся первый: собранный артефакт конкретнее исходника.
301 GET /guide/pages.md 45 µs
200 GET /guide/pages 15.6 kB 296 µs
Третья строка таблицы — та, ради которой всё затевалось. Папка с README.md открывается этим README, как репозиторий на GitHub. Показали serv -m на проект — получили его документацию, а не список файлов.
За это приходится платить, и плата прямая: у такой папки больше нет листинга. Совсем. Никакого ?listing, никакого запасного адреса — я не стал придумывать URL-параметры, которых у serv никогда не было. Папка либо документ, либо оглавление.
А папка, в которой документа нет, по-прежнему рисует своё оглавление:

Без -m не меняется ничего. .md так и остаётся файлом, который стримится с диска с ETag и range-запросами.
Ссылки
В документации ссылки написаны на файлы: [гайд](guide.md). Если отдать такую ссылку как есть, читатель уедет на /guide.md, оттуда получит редирект на /guide и в итоге попадёт куда надо — просто через лишний запрос и с миганием в адресной строке.
Поэтому ссылки переписываются на этапе рендера: guide.md превращается в guide, docs/index.md сворачивается в docs/, а #якорь и ?запрос едут дальше нетронутыми.

Ссылка наружу открывается в своей вкладке, с rel="noopener". А mailto: и tel: не получают ни того, ни другого: они передают управление почтовику или телефону, и вкладка, открытая под них, остаётся пустой висеть рядом с документом.
Под -e, где serv отдаёт пути буквально, ссылки не трогаются вообще — расширение там ничего не теряет, значит и подменять его не на что.
Диаграммы
Блок ```mermaid рисуется как диаграмма, а не как код.

На картинке, кстати, ровно та логика, которую я описал в таблице выше — это диаграмма из example/README.md, отрисованная самим serv.
Дальше — то, чем за неё пришлось заплатить. Mermaid существует только как браузерная библиотека: порта на Rust, который превращал бы graph TD в SVG, нет, так что рисовать диаграмму может исключительно JavaScript в самой странице. Минифицированный mermaid.min.js весит 2.62 МБ при бинарнике serv в 1.38 МБ, и зашивать его внутрь ради одного вида блока я не стал.
Значит, страница тянет mermaid с jsdelivr — и это единственное место во всём serv, которое ходит в сеть. Ходит только та страница, на которой диаграмма действительно есть; документ без диаграммы не запрашивает ничего. Пока mermaid не приехал, и если он не приедет вовсе, в блоке видно исходник диаграммы, а не пустое место.
До 0.3 в README было написано, что serv не ходит в сеть. Пришлось переписать.
Подсветка кода
В готовых бинарниках блоки кода — просто моноширинный текст. Настоящая подсветка живёт за флагом сборки:
cargo install --git https://github.com/JWo1F/serv --features highlight
Внутри — syntect с настоящими TextMate-грамматиками. Цена:
| Сборка | Размер |
|---|---|
| 0.1.1, до markdown | 1.11 МБ |
| 0.3.0, по умолчанию | 1.38 МБ |
0.3.0, --features highlight |
2.97 МБ |
Сам markdown обошёлся в 258 КиБ — это pulldown-cmark. Подсветка добавляет ещё полтора мегабайта: дамп грамматик плюс движок регулярок. Для инструмента, который запускают, чтобы почитать документацию, это многовато по умолчанию, но вполне разумно по требованию — поэтому флаг, а не зависимость.
Вывод syntect идёт классами, а не инлайновыми цветами из темы, так что код раскрашен палитрой самой страницы и уезжает в тёмную тему вместе с ней. Язык, которого syntect не знает, остаётся обычным блоком.
Где это ломается
Несколько мест, которые я знаю и не чиню.
Сырой HTML в markdown не переписывается: если вы написали <a href="guide.md"> руками, ссылка так и останется на .md. До неё рендерер не доходит — она никогда не становится событием ссылки. Сам HTML при этом проходит насквозь и работает, потому что это ваш файл на вашей машине.
README.md не сворачивается в папку. Ссылка на него ведёт на /README, адрес рабочий, страница откроется. Но свернуть его в ./, как index.md, значит начать угадывать — и угадывание сломается в тот день, когда рядом появится index.md.
Сгенерированные страницы — листинг, 404 и документ — отдаются без gzip и без ETag, то есть рендерятся заново на каждый запрос. На локалхосте это доли миллисекунды, а кэш противоречил бы главному обещанию: то, что вы сохранили, — это то, что уедет в браузер.
Зачем это, если есть предпросмотр в редакторе
Возражение справедливое: в любом редакторе есть панель предпросмотра, и для одного файла она удобнее — не надо ничего запускать.
Разница начинается на второй странице. Предпросмотр показывает файл, а не папку: ссылки между документами в нём либо не работают, либо открывают исходник, относительные картинки находятся не всегда, с якорями как повезёт. Набор документов — это маленький сайт, и читать его удобнее как сайт, с переходами и адресной строкой. Плюс то, что показывает браузер, можно показать коллеге, дав ему адрес.
Что под капотом
pulldown-cmark с расширениями GitHub: таблицы, задачи, сноски, зачёркивание. Плюс умная пунктуация — страницы serv свёрстаны как печатный лист, и прямые кавычки на нём смотрелись неправильно. Front matter выбрасывается, а не печатается заголовком. Каждый заголовок получает якорь, чтобы #ссылки, которыми набит любой README, куда-то вели.
Тестов — 311 в сборке по умолчанию и 316 с --features highlight, и CI гоняет обе конфигурации: флаг, который никто не собирает, ломается незаметно.
Что дальше
Установите, покажите ему свою папку с документацией и попробуйте её прочитать.
serv -m ./docs
Если что-то отрендерится не так — issue или pull request на https://github.com/JWo1F/serv будет кстати. Особенно интересны markdown-файлы, которые рендерятся без ошибки, но неправильно: такие я нахожу только глазами.
Комментарии 0
Комментариев пока нет.