## Why В списке кандидатов (`web/templates/candidates.html:41`) текст поста выводится как есть: ```html
{{ (p.text or '')[:500] }}
``` Jinja2 экранирует HTML-сущности (`{{ }}` — автоэскейп), но **не парсит markdown**: жирный текст, ссылки, списки, заголовки в исходных постах (телеграм-посты с markdown-разметкой) отображаются сырыми символами `**`, `[text](url)`, `- item`. Пользователь видит «сырой markdown, а не красивый». ## What Changes - Рендерить текст поста из markdown в HTML перед выводом в списке кандидатов. - Добавить Jinja2-фильтр `markdown` (или `md`): `{{ (p.text or '')[:2000] | markdown }}`. - Использовать локальную Python-библиотеку (не JS/CDN!): `markdown` (Python-Markdown) — уже покрывает жирный/курсив/ссылки/списки/заголовки. Безопасный вывод: экранирование HTML-тегов в исходном тексте (вход — непроверенный текст из TG), `nl2br`/`pre`-обёртка для переносов строк. - Только серверный рендер, без внешних JS-библиотек (в духе deexternalize-web-assets). - Превратить `.post-text` в блок с классом `post-text` и `white-space` нормальным (не `pre-wrap` над сырым md) или оставить, но уже с HTML. ## Impact - Файлы: `web/app.py` (зарегистрировать фильтр), `web/templates/candidates.html` (заменить вывод), возможно `published.html` (если там тоже текст). - Зависимость: добавить `Markdown>=3.6` в `requirements.txt` (pip, локально). - Безопасность: важно экранировать HTML до передачи в markdown-парсер (иначе XSS из telegram-постов). - Минимальная правка; поведение страниц не меняется, кроме вида текста. - Rollback: вернуть `{{ (p.text or '')[:500] }}`, убрать фильтр.