---
title: mdfocus
section: tools
desc: Рідер сирого markdown: line focus, bionic reading, ембіент під швидкість скролу, вузька колонка й повернення до місця читання. URL-проксі з SSRF-гардом.
locked: false
order: 4
featured: false
---

# mdfocus

Я скімлю (читаю по діагоналі, вихоплюючи тільки «цікаве»). Довгий технічний текст відкриваю з наміром прочитати, а очі вже летять униз — «де тут цікаве», — і оцінне судження вимикається десь на другому екрані. Для статей це терпимо. Для першоджерел і документації, де половина сенсу в абзаці, який не кричить, — ні. mdfocus я зробив під рівно цю діру: рідер сирого markdown, який не дає тексту проскочити повз.

Три входи: вставити текст, завантажити `.md`, або кинути лінк на зовнішній `.md` через `?url=`. Далі `marked.js` рендерить, а зверху лягає шар, який я назвав «токенами» — не LLM-токени і не auth-токени, а одиниці утримання уваги. Назва збігається з двома іншими «токенами», тож одразу уточнюю.

## Що всередині

Line focus підсвічує вікно з п'яти блоків навколо рядка читання (по два з кожного боку). Решта тексту лишається з повним контрастом: підсвітка показує, де я зараз, а не карає все поза цією точкою. Progress rail і лічильник секцій по h1/h2 дають масштаб документа; спільний `mandrock0-ambient.js` reseed-иться лише на зміні секції, не на таймері.

Тепер ембіент реагує і на швидкість скролу. Коли я пролітаю текст, canvas приглушується аж до 25% opacity; після паузи починає повертатись. Швидкість — лише proxy для скіму, не детектор того, що я зрозумів абзац (було б зручно, але браузер такого не знає). При `prefers-reduced-motion` цю реакцію вимкнено.

Bionic reading теж дійшов до рідера: перемикач робить жирним початок слів у звичайному тексті, не чіпаючи код, заголовки й лінки. Він off за замовчуванням. Незалежного заміру, що такий прийом справді прискорює моє читання, у мене нема — тому не записую його в «доведені покращення», а лишаю ручкою, яку можна вимкнути за секунду. Вузька колонка на 58ch — друга така ручка: коли очі стрибають по рядку, прибираю зайву ширину, не переписуючи сам markdown.

Ще один токен майже невидимий: після скролу позиція зберігається локально для цього тексту. При наступному відкритті з'являється «продовжити читання», але рідер не перекидає мене туди сам — іноді я відкриваю документ саме з початку. Brown/pink noise лишився процедурним через Web Audio API, без аудіофайлів і без автозапуску. Налаштування й позиція живуть у localStorage; бекенд бачить тільки URL, якщо я завантажую зовнішній `.md` через проксі.

## Що я викинув

Було мікро-поле рефлексії після секції — «запиши, що зрозумів». Воно щоразу розривало потік. Викинув 11.09, і ось правило, що з цього випало: токен не має переривати того, кого він тримає. Bionic reading у старій версії був у беклозі саме тому, що я не хотів вмикати спірний прийом усім підряд; тепер він повернувся як opt-in, а поле рефлексії — ні.

## Бекенд: проксі й гард, який ніхто не просив

Браузер не вміє fetch-ити чужий `.md` без CORS, тож `?url=` ходить через тонкий проксі на stdlib Python, 0 залежностей, `127.0.0.1:3466` за nginx. Сам проксі — одна функція, але SSRF-гард я туди вклав як окрему роботу: «сходи на будь-який URL від імені сервера» — класична діра у внутрішню мережу й cloud metadata.

Що закриває кожен пункт:

- тільки `http`/`https` — відсікає `file://`, `gopher://` і решту схем, якими дістають локальні ресурси;
- резолв хоста і відмова, якщо хоч одна адреса private/loopback/link-local/reserved/multicast/unspecified — стіна перед `169.254.169.254` і внутрішніми IP;
- конект на вже перевірений IP, не на ім'я — закриває DNS-rebind, те вікно між перевіркою і запитом, за яке домен встигає перепризначитись на localhost;
- редіректи не слідуються (3xx → 400) — інакше валідний публічний хост редіректить на внутрішній, і вся перевірка нічого не варта;
- 2 MB cap і 10 с таймаут — щоб проксі не став помпою.

Живе: [tools.mandrock.me/mdfocus/](https://tools.mandrock.me/mdfocus/). Репо, MIT: [github.com/0mandrock1/mdfocus](https://github.com/0mandrock1/mdfocus).
