mod: Санитайзер добавлен в конвейер типографа
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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>'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -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 = '<i>Текст «без» <b>HTML</b>, но с предлогом в доме.</i>'
|
expected_text = '<i>Текст «без» <b>HTML</b>, но с предлогом в доме.</i>'
|
||||||
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>Текст со «старой» разметкой.</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 = 'Текст с «кавычками» и жирным текстом.'
|
||||||
|
typo = Typographer(langs='ru', process_html=True, sanitizer=SANITIZE_ALL_HTML, mode='mixed')
|
||||||
|
actual_text = typo.process(input_html)
|
||||||
|
assert actual_text == expected_text
|
||||||
|
|||||||
Reference in New Issue
Block a user