# tests/test_typograph.py # Тестирует основной класс Typographer и его конвейер обработки. import pytest from etpgrf import Typographer from etpgrf.config import CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, SANITIZE_ETPGRF, SANITIZE_ALL_HTML TYPOGRAPHER_HTML_TEST_CASES = [ # --- Базовая обработка без HTML --- ('mnemonic', 'Простой текст с "кавычками".', f'Простой текст с «кавычками».'), ('mixed', 'Простой текст с "кавычками".', f'Простой текст с «кавычками».'), ('unicode', 'Простой текст с "кавычками".', f'Простой текст с{CHAR_NBSP}«кавычками».'), # --- Базовая обработка с HTML --- ('mnemonic', '

Простой параграф с «кавычками».

', '

Простой параграф с «кавычками».

'), ('mixed', '

Простой параграф с "кавычками".

', '

Простой параграф с «кавычками».

'), ('unicode', '

Простой параграф с "кавычками".

', f'

Простой параграф с{CHAR_NBSP}«кавычками».

'), # --- Рекурсивный обход --- ('mnemonic', '

Текст, а внутри для проверки "жирный" текст.

', '

Текст, а внутри для проверки «жирный» текст.

'), ('mixed', '

Текст, а внутри для проверки "жирный" текст.

', '

Текст, а внутри для проверки «жирный» текст.

'), ('unicode', '

Текст, а внутри для проверки "жирный" текст.

', f'

Текст, а{CHAR_NBSP}внутри для{CHAR_NBSP}проверки «жирный» текст.

'), # --- Вложенные теги с предлогом в тексте --- ('mnemonic', '

Текст с предлогом в доме.

', '

Текст с предлогом в доме.

'), ('mixed', '

Текст с предлогом в доме.

', '

Текст с предлогом в доме.

'), ('unicode', '

Текст с предлогом в доме.

', f'

Текст с{CHAR_NBSP}предлогом в{CHAR_NBSP}доме.

'), # --- Обработка соседних текстовых узлов --- ('mnemonic', '

Союз и слово и еще один союз а текст.

', '

Союз и слово и еще один союз а текст.

'), ('mixed', '

Союз и слово и еще один союз а текст.

', '

Союз и слово и еще один союз а текст.

'), ('unicode', '

Союз и слово и еще один союз а текст.

', f'

Союз и{CHAR_NBSP}слово и{CHAR_NBSP}еще один союз а{CHAR_NBSP}текст.

'), # --- Проверка тегов ', '

Текст «до».

'), ('mixed', '

Текст "до".

', '

Текст «до».

'), ('mixed', '

Текст "до".

Ctrl + C', '

Текст «до».

Ctrl + C'), ('mixed', '

Текст "до".

Sample "text"', '

Текст «до».

Sample "text"'), ('mixed', '

Текст "до".

x=5', '

Текст «до».

x=5'), # --- Проверка тегов с атрибутами --- ('mixed', 'Текст "снаружи"', 'Текст «снаружи»'), ('mixed', 'Текст "снаружи"', 'Текст «снаружи»'), ('mixed', 'Текст "снаружи"', 'Текст «снаружи»'), ('mnemonic', 'Текст "снаружи"', 'Текст «снаружи»'), # --- Комплексный интеграционный тест --- ('mnemonic', '

Он сказал: "В 1941-1945 гг. -- было 100 тыс. руб. и т. д."

', '

Он сказал: «В 1941–1945 гг. – было 100 тыс. руб.' ' и т. д.»

'), ('mixed', '

Он сказал: "В 1941-1945 гг. -- было 100 тыс. руб. и т. д."

', '

Он сказал: «В 1941–1945 гг. – было 100 тыс. руб.' ' и т. д.»

'), ('unicode', '

Он сказал: "В 1941-1945 гг. -- было 100 тыс. руб. и т. д."

', f'

Он{CHAR_NBSP}сказал: «В{CHAR_NBSP}1941{CHAR_NDASH}1945{CHAR_NBSP}гг.{CHAR_NBSP}{CHAR_NDASH} было' f' 100{CHAR_NBSP}тыс.{CHAR_THIN_SP}руб. и{CHAR_NBSP}т.{CHAR_THIN_SP}д.»

'), # --- Теги внутри кавычек --- ('mnemonic', '

"Почему", "зачем" и "кому это выгодно" -- вопросы требующие ответа.

', '

«Почему», «зачем» и «кому это выгодно' '» – вопросы требующие ответа.

'), ('mixed', '

"Почему", "зачем" и "кому это выгодно" -- вопросы требующие ответа.

', '

«Почему», «зачем» и «кому это выгодно» – вопросы требующие ответа.

'), ('unicode', '

"Почему", "зачем" и "кому это выгодно" -- вопросы требующие ответа.

', f'

«Почему», «зачем» и{CHAR_NBSP}«кому это выгодно»{CHAR_NBSP}{CHAR_NDASH} вопросы требующие ответа.

'), # --- Проверка пустого текста и узлов с пробелами --- ('mnemonic', '

\n\t

Слово

', '

\n

Слово

'), ('mixed', '

\n\t

Слово

', '

\n

Слово

'), ('unicode', '

\n\t

Слово

', '

\n

Слово

'), # --- Самозакрывающиеся теги и теги с атрибутами --- # ВАЖНО: 1. Порядок атрибутов в типографированном тексте может быть произвольным # 2. Любое число пробельных символов внутри "пустых" тегов будут редуцированы до одного пробела или # перевода строки. # 3. Самозакрывающиеся теги будут приведены к единому виду с косой чертой в конце. Типа
# 4. Все это "проделки" связаны с использованием библиотеки BeautifulSoup для парсинга HTML, # так что может произойти и другой "улучшайзинг". ('mnemonic', '

Текст с картинкой image и текстом.

', '

Текст с картинкой image и текстом.

'), ('mnemonic', '

Текст с <br>
А это новая строка.

', '

Текст с <br>
А это новая строка.

'), ('mixed', '

Текст с картинкой image и текстом.

', '

Текст с картинкой image и текстом.

'), ('mixed', '

Текст с <br>
А это новая строка.

', '

Текст с <br>
А это новая строка.

'), ('unicode', '

Текст с картинкой image и текстом.

', f'

Текст с{CHAR_NBSP}картинкой image и{CHAR_NBSP}текстом.

'), ('unicode', '

Текст с <br>
А это новая строка.

', f'

Текст с{CHAR_NBSP}<br>
А{CHAR_NBSP}это новая строка.

'), ] @pytest.mark.parametrize("mode, input_html, expected_html", TYPOGRAPHER_HTML_TEST_CASES) def test_typographer_html_processing(mode, input_html, expected_html): """ Проверяет полный конвейер Typographer при обработке HTML. """ typo = Typographer(langs='ru', process_html=True, mode=mode) actual_html = typo.process(input_html) assert actual_html == expected_html def test_typographer_plain_text_processing(): """ Проверяет, что в режиме process_html=False типограф маскирует HTML-теги и обрабатывает весь текст. """ typo = Typographer(langs='ru', process_html=False) input_text = 'Текст "без" HTML, но с предлогом в доме.' expected_text = '<i>Текст «без» <b>HTML</b>, но с предлогом в доме.</i>' actual_text = typo.process(input_text) 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 # --- Новые тесты на структуру HTML (проверка отсутствия лишних оберток) --- HTML_STRUCTURE_TEST_CASES = [ # 1. Фрагмент HTML (без html/body) -> должен остаться фрагментом ('
Текст
', '
Текст
'), ('Текст', 'Текст'), ('

Текст

', '

Текст

'), # 2. Голый текст -> должен остаться голым текстом (без

, , ) ('Текст без тегов', 'Текст без тегов'), # Исправлено: ожидаем nbsp ('Текст с тегом внутри', 'Текст с тегом внутри'), # 3. Полноценный html-документ -> должен сохранить структуру ('

Текст

', '

Текст

'), ('

Текст

', '

Текст

'), # BS может добавить перенос строки после doctype # 4. Кривой html -> будет "починен" ('
Текст', '
Текст
'), ('

Текст', '

Текст

'), ('Текст жирный курсив', 'Текст жирный курсив'), # Используем валидный HTML для теста с DOCTYPE ('Title

Текст

', 'Title

Текст

'), # Тест на совсем кривой HTML (см ниже) не проходит: весь текст после незарытого передается в заголовок. # ('<!DOCTYPE html><html><head><title>Title<body><p>Текст', '<!DOCTYPE html><html><head><title>Title

Текст

'), ] @pytest.mark.parametrize("input_html, expected_html", HTML_STRUCTURE_TEST_CASES) def test_typographer_html_structure_preservation(input_html, expected_html): """ Проверяет, что Typographer не добавляет лишние теги (html, body, p) вокруг фрагментов и текста, но сохраняет их, если они были. """ # Отключаем все "украшательства" (кавычки, неразрывные пробелы), # чтобы проверять только структуру тегов. typo = Typographer( langs='ru', process_html=True, mode='mixed', hyphenation=False, quotes=False, unbreakables=True, # Оставим unbreakables, чтобы проверить, что   добавляются, но теги не ломаются layout=False, symbols=False ) actual_html = typo.process(input_html) # Для теста с doctype может быть нюанс с форматированием (переносы строк), # поэтому нормализуем пробелы перед сравнением if '' in actual_html assert '' in actual_html assert '

Текст

' in actual_html else: assert actual_html == expected_html