diff --git a/etpgrf/typograph.py b/etpgrf/typograph.py index 07ab5a9..f59eda3 100644 --- a/etpgrf/typograph.py +++ b/etpgrf/typograph.py @@ -13,8 +13,9 @@ from etpgrf.unbreakables import Unbreakables from etpgrf.quotes import QuotesProcessor from etpgrf.layout import LayoutProcessor from etpgrf.symbols import SymbolsProcessor +from etpgrf.sanitizer import SanitizerProcessor 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, # Правила для обработки кавычек layout: LayoutProcessor | bool | None = True, # Правила для тире и спецсимволов symbols: SymbolsProcessor | bool | None = True, # Правила для псевдографики + sanitizer: SanitizerProcessor | str | bool | None = None, # Правила очистки # ... другие модули правил ... ): @@ -87,7 +89,12 @@ class Typographer: elif isinstance(layout, LayoutProcessor): 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. --- Логирование инициализации --- 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"layout: {self.layout is not None}, " f"symbols: {self.symbols is not None}, " + f"sanitizer: {self.sanitizer is not None}, " f"process_html: {self.process_html}") @@ -153,6 +161,26 @@ class Typographer: soup = BeautifulSoup(text, 'lxml') except Exception: 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. Создаем "токен-стрим" из текстовых узлов, которые мы будем обрабатывать. # soup.descendants возвращает все дочерние узлы (теги и текст) в порядке их следования. text_nodes = [node for node in soup.descendants @@ -194,20 +222,24 @@ class Typographer: # в _process_text_node. Возвращаем их обратно. return processed_html.replace('&', '&') else: - # Если HTML-режим выключен, используем полный конвейер для простого текста. - # Шаг 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) + return self._process_plain_text(text) + 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) diff --git a/tests/test_sanitizer.py b/tests/test_sanitizer.py index 5ec3c0b..f007b9e 100644 --- a/tests/test_sanitizer.py +++ b/tests/test_sanitizer.py @@ -55,6 +55,11 @@ ETPGRF_SANITIZE_TEST_CASES = [ '
Hello world
', 'Hello world
' ), + ( + "keep_user_span", "Не трогаем span'ы с пользовательскими etp-классами", + 'Hello world
', + 'Hello world
' + ), ( "keep_other_tags", "Не трогаем другие теги", 'And note.
', - 'And note.
' + 'And note.
', + 'And note.
' ), ] diff --git a/tests/test_typograph.py b/tests/test_typograph.py index e80de64..72fa4da 100644 --- a/tests/test_typograph.py +++ b/tests/test_typograph.py @@ -3,7 +3,7 @@ import pytest 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 = [ # --- Базовая обработка без HTML --- @@ -124,4 +124,28 @@ def test_typographer_plain_text_processing(): input_text = 'Текст "без" HTML, но с предлогом в доме.' expected_text = '<i>Текст «без» <b>HTML</b>, но с предлогом в доме.</i>' actual_text = typo.process(input_text) - assert actual_text == expected_text \ No newline at end of file + assert actual_text == expected_text + + +def test_typographer_sanitizer_etpgrf_integration(): + """ + Интеграционный тест: проверяет, что Typographer вызывает Sanitizer для очистки ETP-разметки. + """ + input_html = 'Текст со "старой" разметкой.
' + # Ожидаем, что "старая" разметка будет удалена, а "новая" (кавычки-елочки) будет добавлена. + expected_html = 'Текст со «старой» разметкой.
' + 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 = 'Текст с "кавычками" и жирным текстом.
' + # Ожидаем, что все теги будут удалены, а к чистому тексту применится типографика. + 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