mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
Session 2026-09-13: unify directions (canon+LLM-fallback, 966/1019), publisher dry_run fix, git repo, archive 8 changes, candidates-mail-ui WIP, docs
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-11
|
||||
@@ -0,0 +1,61 @@
|
||||
## Дизайн
|
||||
|
||||
### 1. Зависимость
|
||||
|
||||
```bash
|
||||
/opt/vesti/.venv/bin/pip install "Markdown>=3.6"
|
||||
echo "Markdown>=3.6" >> /opt/vesti/requirements.txt
|
||||
```
|
||||
|
||||
### 2. Фильтр в web/app.py
|
||||
|
||||
Рядом с существующим `tpl.filters["from_json"]` (строка 29) добавить:
|
||||
|
||||
```python
|
||||
import markdown as md_lib
|
||||
from markupsafe import Markup
|
||||
|
||||
def md_filter(text: str) -> Markup:
|
||||
if not text:
|
||||
return Markup("")
|
||||
# 1) экранируем HTML (защита от XSS), 2) рендерим markdown, 3) переносы строк
|
||||
import markupsafe
|
||||
safe = markupsafe.escape(text)
|
||||
html = md_lib.markdown(safe, extensions=["nl2br", "sane_lists"])
|
||||
return Markup(html)
|
||||
|
||||
tpl.filters["markdown"] = md_filter
|
||||
```
|
||||
|
||||
(В Jinja2 по умолчанию `Markup` не экранируется повторно; `markupsafe` уже идёт с Jinja2.)
|
||||
|
||||
### 3. Шаблон candidates.html
|
||||
|
||||
Заменить строку 41:
|
||||
|
||||
```jinja
|
||||
<div class="post-text mt-2">{{ (p.text or '')[:500] }}</div>
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```jinja
|
||||
<div class="post-text mt-2">{{ (p.text or '')[:2000] | markdown }}</div>
|
||||
```
|
||||
|
||||
И в `base.html` для `.post-text` оставить `white-space: pre-wrap` (после nl2br переносы
|
||||
строк уже есть, но pre-wrap не помешает) — или сменить на `line-height: 1.5`.
|
||||
|
||||
### 4. Безопасность
|
||||
|
||||
- `markupsafe.escape` до markdown-парсера — ссылки `[x](javascript:...)` должны быть
|
||||
заблокированы. Python-Markdown сам экранирует опасные протоколы в ссылках, но
|
||||
предварительное экранирование — обязательный слой.
|
||||
- Не использовать `|safe` без `Markup`.
|
||||
|
||||
### 5. Проверка
|
||||
|
||||
- Пост с текстом `**жирный** [ссылка](https://x) - пункт` рендерится жирным/ссылкой/списком.
|
||||
- В HTML нет сырых `**`, `[`, `](` символов разметки (кроме намеренных).
|
||||
- Ввод `<script>alert(1)</script>` отображается как текст, не исполняется.
|
||||
- `published.html` — если там есть `p.text`/`p.summary`, применить тот же фильтр (grep).
|
||||
@@ -0,0 +1,34 @@
|
||||
## Why
|
||||
|
||||
В списке кандидатов (`web/templates/candidates.html:41`) текст поста выводится как есть:
|
||||
|
||||
```html
|
||||
<div class="post-text mt-2">{{ (p.text or '')[:500] }}</div>
|
||||
```
|
||||
|
||||
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] }}`, убрать фильтр.
|
||||
@@ -0,0 +1,17 @@
|
||||
## 1. Зависимость и фильтр
|
||||
|
||||
- [x] 1.1 `pip install Markdown` в .venv, добавить в requirements.txt
|
||||
- [x] 1.2 Добавить Jinja2-фильтр `markdown` в `web/app.py` (экранирование HTML + nl2br + sane_lists)
|
||||
- [x] 1.3 Проверка: `python -c "from web.app import tpl; print(tpl.filters['markdown']('**b**'))"` — фильтр есть
|
||||
|
||||
## 2. Шаблоны
|
||||
|
||||
- [x] 2.1 `candidates.html`: заменить `{{ (p.text or '')[:500] }}` на `{{ (p.text or '')[:2000] | markdown }}`
|
||||
- [x] 2.2 Проверить `published.html`/`metrics.html` — применить фильтр к тексту/анонсам при наличии
|
||||
- [x] 2.3 Проверка: пост с `**жирный**` и ссылкой отображается разметкой, а не сырым md
|
||||
|
||||
## 3. Безопасность и регресс
|
||||
|
||||
- [x] 3.1 Тест XSS: `<script>` в тексте поста не исполняется (отображается как текст)
|
||||
- [x] 3.2 Тест регресса: список кандидатов и published рендерятся без ошибок
|
||||
- [x] 3.3 `grep -rn "p.text\|\.text" web/templates/` — все места обработаны
|
||||
Reference in New Issue
Block a user