mod: Санитайзер для очистки от HTML (несколько режимов)

This commit is contained in:
2025-10-28 23:46:38 +03:00
parent 57fb4914d8
commit 48c90409b8
4 changed files with 193 additions and 4 deletions

View File

@@ -11,7 +11,12 @@ etpgrf - библиотека для экранной типографики т
__version__ = "0.1.0"
import etpgrf.defaults
from etpgrf.typograph import Typographer
from etpgrf.hyphenation import Hyphenator
from etpgrf.unbreakables import Unbreakables
import etpgrf.logger
from etpgrf.hyphenation import Hyphenator
from etpgrf.layout import LayoutProcessor
from etpgrf.quotes import QuotesProcessor
from etpgrf.sanitizer import SanitizerProcessor
from etpgrf.symbols import SymbolsProcessor
from etpgrf.typograph import Typographer
from etpgrf.unbreakables import Unbreakables

View File

@@ -15,6 +15,12 @@ LANG_EN = 'en' # Английский
SUPPORTED_LANGS = frozenset([LANG_RU, LANG_RU_OLD, LANG_EN])
DEFAULT_LANGS = (LANG_RU, LANG_EN) # Языки по умолчанию
# Виды санитизации (очистки) входного текста
SANITIZE_ALL_HTML = "html" # Полная очистка от HTML-тегов
SANITIZE_ETPGRF = "etp" # Очистка от "span-оберток" символов висячей пунктуации (если она была расставлена
# при предыдущих проходах типографа)
SANITIZE_NONE = None # Без очистки (режим по умолчанию). False тоже можно использовать.
# === ИСТОЧНИК ПРАВДЫ ===
# --- Базовые алфавиты: Эти константы используются как для правил переноса, так и для правил кодирования ---
@@ -677,4 +683,40 @@ ABBR_COMMON_PREPOSITION = [
]
# === КОНСТАНТЫ ДЛЯ HTML-ТЕГОВ, ВНУТРИ КОТОРЫХ НЕ НАДО ТИПОГРАФИРОВАТЬ ===
PROTECTED_HTML_TAGS = ['style', 'script', 'pre', 'code', 'kbd', 'samp', 'math']
PROTECTED_HTML_TAGS = ['style', 'script', 'pre', 'code', 'kbd', 'samp', 'math']
# === КОНСТАНТЫ ДЛЯ ВИСЯЧЕЙ ТИПОГРАФИКИ ===
# 1. Набор символов, которые могут "висеть" слева
HANGING_PUNCTUATION_LEFT_CHARS = frozenset([
CHAR_RU_QUOT1_OPEN, # «
CHAR_EN_QUOT1_OPEN, # “
'(', '[', '{',
])
# 2. Набор символов, которые могут "висеть" справа
HANGING_PUNCTUATION_RIGHT_CHARS = frozenset([
CHAR_RU_QUOT1_CLOSE, # »
CHAR_EN_QUOT1_CLOSE, # ”
')', ']', '}',
'.', ',', ':',
])
# 3. Словарь, сопоставляющий символ с его CSS-классом
HANGING_PUNCTUATION_CLASSES = {
# Левая пунктуация: все классы начинаются с 'etp-l'
CHAR_RU_QUOT1_OPEN: 'etp-laquo',
CHAR_EN_QUOT1_OPEN: 'etp-ldquo',
'(': 'etp-lpar',
'[': 'etp-lsqb',
'{': 'etp-lcub',
# Правая пунктуация: все классы начинаются с 'etp-r'
CHAR_RU_QUOT1_CLOSE: 'etp-raquo',
CHAR_EN_QUOT1_CLOSE: 'etp-rdquo',
')': 'etp-rpar',
']': 'etp-rsqb',
'}': 'etp-rcub',
'.': 'etp-r-dot',
',': 'etp-r-comma',
':': 'etp-r-colon',
}

62
etpgrf/sanitizer.py Normal file
View File

@@ -0,0 +1,62 @@
# etpgrf/sanitizer.py
# Модуль для очистки и нормализации HTML-кода перед типографикой.
import logging
from bs4 import BeautifulSoup, NavigableString
from .config import (SANITIZE_ALL_HTML, SANITIZE_ETPGRF, SANITIZE_NONE,
HANGING_PUNCTUATION_CLASSES, PROTECTED_HTML_TAGS)
logger = logging.getLogger(__name__)
class SanitizerProcessor:
"""
Выполняет очистку HTML-кода в соответствии с заданным режимом.
"""
def __init__(self, mode: str | bool | None = SANITIZE_NONE):
"""
:param mode: Режим очистки:
- 'etp' (SANITIZE_ETPGRF): удаляет только разметку висячей пунктуации.
- 'html' (SANITIZE_ALL_HTML): удаляет все HTML-теги.
- None или False: ничего не делает.
"""
if mode is False:
mode = SANITIZE_NONE
self.mode = mode
self._etp_classes_to_clean = frozenset(HANGING_PUNCTUATION_CLASSES.values())
logger.debug(f"SanitizerProcessor `__init__`. Mode: {self.mode}")
def process(self, soup: BeautifulSoup) -> BeautifulSoup | str:
"""
Применяет правила очистки к `soup`-объекту.
:param soup: Объект BeautifulSoup для обработки.
:return: Обработанный объект BeautifulSoup или строка (в режиме 'html').
"""
if self.mode == SANITIZE_ETPGRF:
# Находим все span'ы, у которых есть <span> с хотя бы одним из наших классов висячей пунктуации
spans_to_clean = soup.find_all(
name='span',
class_=lambda c: c and any(etp_class in c.split() for etp_class in self._etp_classes_to_clean)
)
# "Агрессивная" очистка: просто "разворачиваем" все найденные теги,
# заменяя их своим содержимым.
for span in spans_to_clean:
span.unwrap()
return soup
elif self.mode == SANITIZE_ALL_HTML:
# Возвращаем только текст, удаляя все теги
# При этом уважаем защищенные теги, не извлекая текст из них.
text_parts = [
str(node) for node in soup.descendants
if isinstance(node, NavigableString) and node.parent.name not in PROTECTED_HTML_TAGS
]
return "".join(text_parts)
# Если режим не задан, ничего не делаем
return soup

80
tests/test_sanitizer.py Normal file
View File

@@ -0,0 +1,80 @@
# tests/test_sanitizer.py
# Тестирует модуль SanitizerProcessor.
import pytest
from bs4 import BeautifulSoup
from etpgrf.sanitizer import SanitizerProcessor
from etpgrf.config import SANITIZE_NONE, SANITIZE_ETPGRF, SANITIZE_ALL_HTML
def test_sanitizer_mode_none():
"""
Проверяет, что в режиме SANITIZE_NONE (по умолчанию) ничего не происходит.
"""
html_input = '<p><span class="etp-laquo">«</span>Hello<span class="user-class"> world</span>.</p>'
soup = BeautifulSoup(html_input, 'html.parser')
# Тестируем с mode=None и mode=False
processor_none = SanitizerProcessor(mode=SANITIZE_NONE)
processor_false = SanitizerProcessor(mode=False)
result_soup_none = processor_none.process(soup)
result_soup_false = processor_false.process(soup)
assert str(result_soup_none) == html_input
assert str(result_soup_false) == html_input
def test_sanitizer_mode_all_html():
"""
Проверяет, что в режиме SANITIZE_ALL_HTML удаляются все теги.
"""
html_input = '<p>Hello <b>world</b>! <a href="#">Click me</a>.</p>'
soup = BeautifulSoup(html_input, 'html.parser')
processor = SanitizerProcessor(mode=SANITIZE_ALL_HTML)
result_text = processor.process(soup)
assert result_text == "Hello world! Click me."
ETPGRF_SANITIZE_TEST_CASES = [
# ID, Описание, Входной HTML, Ожидаемый HTML
(
"simple_unwrap", "Простое разворачивание span'а с одним etp-классом",
'<p><span class="etp-laquo">«</span>Hello</p>',
'<p>«Hello</p>'
),
(
"aggressive_unwrap", "Агрессивное разворачивание span'а со смешанными классами",
'<p>Hello<span class="user-class etp-raquo">»</span></p>',
'<p>Hello»</p>'
),
(
"keep_user_span", "Не трогаем span'ы с пользовательскими классами",
'<p>Hello <span class="user-class">world</span></p>',
'<p>Hello <span class="user-class">world</span></p>'
),
(
"keep_other_tags", "Не трогаем другие теги",
'<div><b>Bold</b> and <i>italic</i></div>',
'<div><b>Bold</b> and <i>italic</i></div>'
),
(
"complex_case", "Сложный случай с несколькими разными span'ами",
'<h1><span class="etp-laquo">«</span>Title<span class="etp-raquo">»</span></h1><p>And <span class="note">note</span>.</p>',
'<h1>«Title»</h1><p>And <span class="note">note</span>.</p>'
),
]
@pytest.mark.parametrize("case_id, description, html_input, expected_html", ETPGRF_SANITIZE_TEST_CASES)
def test_sanitizer_mode_etpgrf(case_id, description, html_input, expected_html):
"""
Проверяет, что в режиме SANITIZE_ETPGRF удаляется только разметка висячей пунктуации.
"""
soup = BeautifulSoup(html_input, 'html.parser')
processor = SanitizerProcessor(mode=SANITIZE_ETPGRF)
result_soup = processor.process(soup)
assert str(result_soup) == expected_html