все посты
Логотип serv: слово «serv» моноширинным шрифтом и красный блок-курсор после него, на тёмном фоне.

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 — сверстанный, а не в виде решёток и звёздочек.

Страница serv с отрендеренным README: шапка «serv · local development server», хлебные крошки «example/», заголовок «serv, reading markdown», абзац прозы и блок кода.

Файл читается с диска на каждый запрос — ничего не кэшируется внутри сервера. Поправили абзац, нажали 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 никогда не было. Папка либо документ, либо оглавление.

А папка, в которой документа нет, по-прежнему рисует своё оглавление:

Сгенерированный serv листинг папки: хлебные крошки «example/ guide/ nested/», строки с иконками для notes.txt и style.css, размер и дата справа.

Без -m не меняется ничего. .md так и остаётся файлом, который стримится с диска с ETag и range-запросами.

Ссылки

В документации ссылки написаны на файлы: [гайд](guide.md). Если отдать такую ссылку как есть, читатель уедет на /guide.md, оттуда получит редирект на /guide и в итоге попадёт куда надо — просто через лишний запрос и с миганием в адресной строке.

Поэтому ссылки переписываются на этапе рендера: guide.md превращается в guide, docs/index.md сворачивается в docs/, а #якорь и ?запрос едут дальше нетронутыми.

Раздел «Links» на странице serv: таблица из двух колонок, слева исходная запись ссылки в markdown, справа — во что она превращается: сворачивается в папку, теряет расширение, сохраняет фрагмент, открывается в новой вкладке.

Ссылка наружу открывается в своей вкладке, с rel="noopener". А mailto: и tel: не получают ни того, ни другого: они передают управление почтовику или телефону, и вкладка, открытая под них, остаётся пустой висеть рядом с документом.

Под -e, где serv отдаёт пути буквально, ссылки не трогаются вообще — расширение там ничего не теряет, значит и подменять его не на что.

Диаграммы

Блок ```mermaid рисуется как диаграмма, а не как код.

Раздел «A diagram» на странице serv: блок-схема mermaid с ромбами решений «On disk?», «index.html?», «index.md or README.md?», «–spa?» и прямоугольниками «Send it», «Render the document», «Draw the listing», «404».

На картинке, кстати, ровно та логика, которую я описал в таблице выше — это диаграмма из 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

Комментариев пока нет.