mod: Санитайзер добавлен в конвейер типографа

This commit is contained in:
2025-12-19 14:33:46 +03:00
parent 48c90409b8
commit cd1be6bf27
3 changed files with 83 additions and 22 deletions

View File

@@ -13,8 +13,9 @@ from etpgrf.unbreakables import Unbreakables
from etpgrf.quotes import QuotesProcessor from etpgrf.quotes import QuotesProcessor
from etpgrf.layout import LayoutProcessor from etpgrf.layout import LayoutProcessor
from etpgrf.symbols import SymbolsProcessor from etpgrf.symbols import SymbolsProcessor
from etpgrf.sanitizer import SanitizerProcessor
from etpgrf.codec import decode_to_unicode, encode_from_unicode from etpgrf.codec import decode_to_unicode, encode_from_unicode
from etpgrf.config import PROTECTED_HTML_TAGS from etpgrf.config import PROTECTED_HTML_TAGS, SANITIZE_ALL_HTML
# --- Настройки логирования --- # --- Настройки логирования ---
@@ -32,6 +33,7 @@ class Typographer:
quotes: QuotesProcessor | bool | None = True, # Правила для обработки кавычек quotes: QuotesProcessor | bool | None = True, # Правила для обработки кавычек
layout: LayoutProcessor | bool | None = True, # Правила для тире и спецсимволов layout: LayoutProcessor | bool | None = True, # Правила для тире и спецсимволов
symbols: SymbolsProcessor | bool | None = True, # Правила для псевдографики symbols: SymbolsProcessor | bool | None = True, # Правила для псевдографики
sanitizer: SanitizerProcessor | str | bool | None = None, # Правила очистки
# ... другие модули правил ... # ... другие модули правил ...
): ):
@@ -87,7 +89,12 @@ class Typographer:
elif isinstance(layout, LayoutProcessor): elif isinstance(layout, LayoutProcessor):
self.layout = layout self.layout = layout
# I. --- Конфигурация других правил--- # I. --- Конфигурация санитайзера ---
self.sanitizer: SanitizerProcessor | None = None
if isinstance(sanitizer, SanitizerProcessor):
self.sanitizer = sanitizer
elif sanitizer: # Если передана строка режима или True
self.sanitizer = SanitizerProcessor(mode=sanitizer)
# Z. --- Логирование инициализации --- # Z. --- Логирование инициализации ---
logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, " logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, "
@@ -96,6 +103,7 @@ class Typographer:
f"quotes: {self.quotes is not None}, " f"quotes: {self.quotes is not None}, "
f"layout: {self.layout is not None}, " f"layout: {self.layout is not None}, "
f"symbols: {self.symbols is not None}, " f"symbols: {self.symbols is not None}, "
f"sanitizer: {self.sanitizer is not None}, "
f"process_html: {self.process_html}") f"process_html: {self.process_html}")
@@ -153,6 +161,26 @@ class Typographer:
soup = BeautifulSoup(text, 'lxml') soup = BeautifulSoup(text, 'lxml')
except Exception: except Exception:
soup = BeautifulSoup(text, 'html.parser') soup = BeautifulSoup(text, 'html.parser')
# --- ЭТАП 0: Санитизация (Очистка) ---
if self.sanitizer:
result = self.sanitizer.process(soup)
# Если режим SANITIZE_ALL_HTML, то результат - это строка (чистый текст)
if isinstance(result, str):
# Переключаемся на обработку обычного текста
text = result
# ВАЖНО: Мы выходим из ветки process_html и идем в ветку else,
# но так как мы внутри if, нам нужно явно вызвать логику для текста.
# Проще всего рекурсивно вызвать process с выключенным process_html,
# но чтобы не менять состояние объекта, просто выполним логику "else" блока здесь.
# Или, еще проще: присвоим text = result и пойдем в блок else? Нет, мы уже внутри if.
# Решение: Выполняем логику обработки простого текста прямо здесь
return self._process_plain_text(text)
# Если результат - soup, продолжаем работу с ним
soup = result
# 1.1. Создаем "токен-стрим" из текстовых узлов, которые мы будем обрабатывать. # 1.1. Создаем "токен-стрим" из текстовых узлов, которые мы будем обрабатывать.
# soup.descendants возвращает все дочерние узлы (теги и текст) в порядке их следования. # soup.descendants возвращает все дочерние узлы (теги и текст) в порядке их следования.
text_nodes = [node for node in soup.descendants text_nodes = [node for node in soup.descendants
@@ -194,20 +222,24 @@ class Typographer:
# в _process_text_node. Возвращаем их обратно. # в _process_text_node. Возвращаем их обратно.
return processed_html.replace('&', '&') return processed_html.replace('&', '&')
else: else:
# Если HTML-режим выключен, используем полный конвейер для простого текста. return self._process_plain_text(text)
# Шаг 0: Нормализация
processed_text = decode_to_unicode(text)
# Шаг 1: Применяем все правила последовательно
if self.quotes:
processed_text = self.quotes.process(processed_text)
if self.unbreakables:
processed_text = self.unbreakables.process(processed_text)
if self.symbols:
processed_text = self.symbols.process(processed_text)
if self.layout:
processed_text = self.layout.process(processed_text)
if self.hyphenation:
processed_text = self.hyphenation.hyp_in_text(processed_text)
# Шаг 2: Финальное кодирование
return encode_from_unicode(processed_text, self.mode)
def _process_plain_text(self, text: str) -> str:
"""
Логика обработки обычного текста (вынесена из process для переиспользования).
"""
# Шаг 0: Нормализация
processed_text = decode_to_unicode(text)
# Шаг 1: Применяем все правила последовательно
if self.quotes:
processed_text = self.quotes.process(processed_text)
if self.unbreakables:
processed_text = self.unbreakables.process(processed_text)
if self.symbols:
processed_text = self.symbols.process(processed_text)
if self.layout:
processed_text = self.layout.process(processed_text)
if self.hyphenation:
processed_text = self.hyphenation.hyp_in_text(processed_text)
# Шаг 2: Финальное кодирование
return encode_from_unicode(processed_text, self.mode)

View File

@@ -55,6 +55,11 @@ ETPGRF_SANITIZE_TEST_CASES = [
'<p>Hello <span class="user-class">world</span></p>', '<p>Hello <span class="user-class">world</span></p>',
'<p>Hello <span class="user-class">world</span></p>' '<p>Hello <span class="user-class">world</span></p>'
), ),
(
"keep_user_span", "Не трогаем span'ы с пользовательскими etp-классами",
'<p>Hello <span class="etp-user-class">world</span></p>',
'<p>Hello <span class="etp-user-class">world</span></p>'
),
( (
"keep_other_tags", "Не трогаем другие теги", "keep_other_tags", "Не трогаем другие теги",
'<div><b>Bold</b> and <i>italic</i></div>', '<div><b>Bold</b> and <i>italic</i></div>',
@@ -62,8 +67,8 @@ ETPGRF_SANITIZE_TEST_CASES = [
), ),
( (
"complex_case", "Сложный случай с несколькими разными span'ами", "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><span class="etp-laquo">«</span>Title<span class="etp-raquo">»</span></h1>\n<p>And <span class="note">note</span>.</p>',
'<h1>«Title»</h1><p>And <span class="note">note</span>.</p>' '<h1>«Title»</h1>\n<p>And <span class="note">note</span>.</p>'
), ),
] ]

View File

@@ -3,7 +3,7 @@
import pytest import pytest
from etpgrf import Typographer from etpgrf import Typographer
from etpgrf.config import CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH from etpgrf.config import CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, SANITIZE_ETPGRF, SANITIZE_ALL_HTML
TYPOGRAPHER_HTML_TEST_CASES = [ TYPOGRAPHER_HTML_TEST_CASES = [
# --- Базовая обработка без HTML --- # --- Базовая обработка без HTML ---
@@ -124,4 +124,28 @@ def test_typographer_plain_text_processing():
input_text = '<i>Текст "без" <b>HTML</b>, но с предлогом в доме.</i>' input_text = '<i>Текст "без" <b>HTML</b>, но с предлогом в доме.</i>'
expected_text = '&lt;i&gt;Текст «без» &lt;b&gt;HTML&lt;/b&gt;, но&nbsp;с&nbsp;предлогом в&nbsp;доме.&lt;/i&gt;' expected_text = '&lt;i&gt;Текст «без» &lt;b&gt;HTML&lt;/b&gt;, но&nbsp;с&nbsp;предлогом в&nbsp;доме.&lt;/i&gt;'
actual_text = typo.process(input_text) actual_text = typo.process(input_text)
assert actual_text == expected_text assert actual_text == expected_text
def test_typographer_sanitizer_etpgrf_integration():
"""
Интеграционный тест: проверяет, что Typographer вызывает Sanitizer для очистки ETP-разметки.
"""
input_html = '<p>Текст со <span class="etp-laquo">"старой"</span> разметкой.</p>'
# Ожидаем, что "старая" разметка будет удалена, а "новая" (кавычки-елочки) будет добавлена.
expected_html = '<p>Текст со&nbsp;«старой» разметкой.</p>'
typo = Typographer(langs='ru', process_html=True, sanitizer=SANITIZE_ETPGRF, mode='mixed')
actual_html = typo.process(input_html)
assert actual_html == expected_html
def test_typographer_sanitizer_all_html_integration():
"""
Интеграционный тест: проверяет, что Typographer вызывает Sanitizer для полной очистки HTML.
"""
input_html = '<p>Текст с "кавычками" и <b>жирным</b> текстом.</p>'
# Ожидаем, что все теги будут удалены, а к чистому тексту применится типографика.
expected_text = 'Текст с&nbsp;«кавычками» и&nbsp;жирным текстом.'
typo = Typographer(langs='ru', process_html=True, sanitizer=SANITIZE_ALL_HTML, mode='mixed')
actual_text = typo.process(input_html)
assert actual_text == expected_text