mod: Санитайзер для очистки от HTML (несколько режимов)
This commit is contained in:
@@ -11,7 +11,12 @@ etpgrf - библиотека для экранной типографики т
|
|||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
import etpgrf.defaults
|
import etpgrf.defaults
|
||||||
from etpgrf.typograph import Typographer
|
|
||||||
from etpgrf.hyphenation import Hyphenator
|
|
||||||
from etpgrf.unbreakables import Unbreakables
|
|
||||||
import etpgrf.logger
|
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
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ LANG_EN = 'en' # Английский
|
|||||||
SUPPORTED_LANGS = frozenset([LANG_RU, LANG_RU_OLD, LANG_EN])
|
SUPPORTED_LANGS = frozenset([LANG_RU, LANG_RU_OLD, LANG_EN])
|
||||||
DEFAULT_LANGS = (LANG_RU, 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-ТЕГОВ, ВНУТРИ КОТОРЫХ НЕ НАДО ТИПОГРАФИРОВАТЬ ===
|
# === КОНСТАНТЫ ДЛЯ 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
62
etpgrf/sanitizer.py
Normal 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
80
tests/test_sanitizer.py
Normal 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
|
||||||
Reference in New Issue
Block a user