mirror of
https://gitverse.ru/kpa39l/vesti.git
synced 2026-09-29 09:55:03 +00:00
rss-formatting-preserve: сохранять абзацы и разметку из RSS (html_to_text/extract_full_text normalize; safe_html bleach для предпросмотра; пересборка старых постов)
This commit is contained in:
+43
@@ -17,6 +17,11 @@ from fastapi.staticfiles import StaticFiles
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from markupsafe import Markup, escape
|
||||
|
||||
try: # bleach — санитайзер HTML из RSS (allowlist); см. requirements.txt
|
||||
import bleach
|
||||
except ImportError:
|
||||
bleach = None
|
||||
|
||||
# CRUD источников: работа с sources.yaml (источник истины) + БД синк
|
||||
from sources.sources import (
|
||||
db_source_to_yaml,
|
||||
@@ -51,6 +56,43 @@ def md_filter(text: str) -> Markup:
|
||||
return Markup(html)
|
||||
|
||||
|
||||
# Теги/атрибуты, разрешённые в текстах из RSS (безопасное подмножество HTML).
|
||||
_SAFE_TAGS = [
|
||||
"p", "br", "b", "strong", "i", "em", "u", "s", "strike", "del",
|
||||
"a", "ul", "ol", "li", "blockquote", "pre", "code", "h1", "h2", "h3",
|
||||
"h4", "h5", "h6", "table", "thead", "tbody", "tr", "th", "td",
|
||||
]
|
||||
_SAFE_ATTRS = {"a": ["href", "title", "rel"]}
|
||||
_SAFE_PROTOCOLS = ["http", "https", "mailto"]
|
||||
|
||||
|
||||
def safe_html_filter(text: str) -> Markup:
|
||||
"""Исходный текст из RSS → безопасный HTML (allowlist-санитайзер).
|
||||
|
||||
Сохраняет «заложенное форматирование» (жирный, ссылки, списки, абзацы),
|
||||
вырезает всё опасное (script/style/on*/iframe/svg/...) и делает ссылки
|
||||
кликабельными (linkify). Для markdown-пересказов/комментариев НЕ используется —
|
||||
они идут через md_filter.
|
||||
"""
|
||||
if not text:
|
||||
return Markup("")
|
||||
if bleach is None:
|
||||
# запасной вариант без bleach — экранируем (безопасно, но без разметки)
|
||||
return Markup(md_lib.markdown(escape(text), extensions=["nl2br", "sane_lists"]))
|
||||
try:
|
||||
cleaned = bleach.clean(
|
||||
text,
|
||||
tags=_SAFE_TAGS,
|
||||
attributes=_SAFE_ATTRS,
|
||||
protocols=_SAFE_PROTOCOLS,
|
||||
strip=True,
|
||||
)
|
||||
cleaned = bleach.linkify(cleaned, parse_email=True)
|
||||
return Markup(cleaned)
|
||||
except Exception:
|
||||
return Markup(escape(text))
|
||||
|
||||
|
||||
def dt_filter(value) -> str:
|
||||
"""2026-08-15T15:53 → 15:53 15.08.2026; None/мусор → ''."""
|
||||
if not value:
|
||||
@@ -64,6 +106,7 @@ def dt_filter(value) -> str:
|
||||
|
||||
|
||||
tpl.filters["markdown"] = md_filter
|
||||
tpl.filters["safe_html"] = safe_html_filter
|
||||
tpl.filters["dt"] = dt_filter
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<style>
|
||||
body { padding-top: 4.5rem; background: #f5f6fa; }
|
||||
.card { box-shadow: 0 1px 4px rgba(0,0,0,.08); }
|
||||
.post-text { white-space: pre-wrap; max-height: 140px; overflow-y: auto; font-size: .9rem; }
|
||||
.post-text { white-space: pre-wrap; font-size: .9rem; }
|
||||
/* список кандидатов: выбранный пост — светло-голубая заливка карточки */
|
||||
.list-group-item.active-row { background-color: #dcecfc; }
|
||||
.list-group-item.active-row:hover { background-color: #d0e4f8; }
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<h6 class="text-muted flex-shrink-0">Оригинальный пост</h6>
|
||||
<div class="flex-grow-1 overflow-auto" style="min-height:0;">
|
||||
{% if selected.text %}
|
||||
<div class="post-text mt-1">{{ selected.text | markdown }}</div>
|
||||
<div class="post-text mt-1">{{ selected.text | safe_html }}</div>
|
||||
{% endif %}
|
||||
{% if selected.media_path %}
|
||||
{% set media_src = '/media/' ~ selected.media_path.split('/')[-1] %}
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
<h6 class="text-muted">Оригинальный пост</h6>
|
||||
<div class="border rounded p-2 bg-light small overflow-auto flex-grow-1" style="min-height:0;">
|
||||
{% if selected.text %}
|
||||
<div class="post-text">{{ selected.text | markdown }}</div>
|
||||
<div class="post-text">{{ selected.text | safe_html }}</div>
|
||||
{% endif %}
|
||||
{% if selected.media_path %}
|
||||
{% set media_src = '/media/' ~ selected.media_path.split('/')[-1] %}
|
||||
|
||||
Reference in New Issue
Block a user