# tests/test_hanging.py # Тесты для модуля висячей пунктуации (HangingPunctuationProcessor). import pytest from bs4 import BeautifulSoup from etpgrf.hanging import HangingPunctuationProcessor from etpgrf.config import ( CHAR_RU_QUOT1_OPEN, CHAR_RU_QUOT1_CLOSE, CHAR_EN_QUOT1_OPEN, CHAR_EN_QUOT1_CLOSE, CHAR_LPAR, CHAR_LSQB, CHAR_LCUB, CHAR_RPAR, CHAR_RSQB, CHAR_RCUB, CHAR_SHY, CHAR_THIN_SP, CHAR_HAIR_SP, CHAR_MED_SP, CHAR_EN_SP, CHAR_EM_SP, CHAR_NULL_SP, CHAR_THIN_NBSP, CHAR_NBSP, CHAR_ZWNJ ) # Вспомогательная функция для создания soup def make_soup(html_str): return BeautifulSoup(html_str, 'html.parser') # Набор тестовых случаев в формате: # (режим, входной_html, ожидаемый_html) HANGING_TEST_CASES = [ # --- Режим 'left' (только левая висячая пунктуация) --- ('left', f'

{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}

', f'

{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}

'), ('left', f'

{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE} вначале текста

', f'

{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE} вначале текста

'), ('left', f'

А вот это {CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

А вот это {CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

'), ('left', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

'), ('left', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

'),\ ('left', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

'), ('left', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

А вот это {CHAR_RU_QUOT1_OPEN}Длинная цитата{CHAR_RU_QUOT1_CLOSE} внутри текста

'), # Английские кавычки "лапки" ('left', f'

This is some {CHAR_EN_QUOT1_OPEN}wisdom quote{CHAR_EN_QUOT1_CLOSE} for the test.

', f'

This is some {CHAR_EN_QUOT1_OPEN}wisdom quote{CHAR_EN_QUOT1_CLOSE} for the test.

'), # Неразрывные пробелы и символы, отменяющие перенос, не должны оборачиваться, но должны сохраняться в тексте ('left', f'

Неразрывный пробел перед{CHAR_NBSP}{CHAR_RU_QUOT1_OPEN}Цитатой{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

Неразрывный пробел перед{CHAR_NBSP}{CHAR_RU_QUOT1_OPEN}Цитатой{CHAR_RU_QUOT1_CLOSE} внутри текста

'), ('left', f'

Неразрывный пробел перед{CHAR_ZWNJ}{CHAR_RU_QUOT1_OPEN}Цитатой{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

Неразрывный пробел перед{CHAR_ZWNJ}{CHAR_RU_QUOT1_OPEN}Цитатой{CHAR_RU_QUOT1_CLOSE} внутри текста

'), ('left', f'

Неразрывный пробел перед{CHAR_THIN_NBSP}{CHAR_RU_QUOT1_OPEN}Цитатой{CHAR_RU_QUOT1_CLOSE} внутри текста

', f'

Неразрывный пробел перед{CHAR_THIN_NBSP}{CHAR_RU_QUOT1_OPEN}Цитатой{CHAR_RU_QUOT1_CLOSE} внутри текста

'), # Примеры "круглая скобка" ('left', '

(Скобки)

', '

(Скобки)

'), ('left', '

Висячая пунктуация оборачивает (Скобки)

', '

Висячая пунктуация оборачивает (Скобки)

'), ('left', '

Висячая пунктуация оборачивает (Круглые скобки) вот так.

', '

Висячая пунктуация оборачивает (Круглые скобки) вот так.

'), # Примеры "квадратная скобка" ('left', '

[Скобки]

', '

[Скобки]

'), ('left', '

Висячая пунктуация оборачивает [Скобки]

', '

Висячая пунктуация оборачивает [Скобки]

'), ('left', '

Висячая пунктуация оборачивает [Квадратные скобки] вот так.

', '

Висячая пунктуация оборачивает [Квадратные скобки] вот так.

'), # Примеры "фигурная скобка" ('left', '

{Скобки}

', '

{Скобки}

'), ('left', '

Висячая пунктуация оборачивает {Скобки}

', '

Висячая пунктуация оборачивает {Скобки}

'), ('left', '

Висячая пунктуация оборачивает {Квадратные скобки} вот так.

', '

Висячая пунктуация оборачивает {Квадратные скобки} вот так.

'), # Обычный текст, в котором нет символов для висячей пунктуации, не должен изменяться ('left', '

Текст.

', '

Текст.

'), # --- Режим 'right' (только правая пунктуация) --- #('right', f'

{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}

', # f'

{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}

'), #('right', '

Текст.

', '

Текст.

'), #('right', '

(Скобки)

', '

(Скобки)

'), #('right', '

3.14

', '

3.14

'), #('right', '

End.

', '

End.

'), # --- Режим None / False (отключено) --- (None, f'

{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

', f'

{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

'), (False, f'

{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

', f'

{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

'), ] @pytest.mark.parametrize("mode, input_html, expected_html", HANGING_TEST_CASES) def test_hanging_punctuation_processor(mode, input_html, expected_html): """ Проверяет работу HangingPunctuationProcessor в различных режимах. """ # Arrange processor = HangingPunctuationProcessor(mode=mode) soup = make_soup(input_html) # Act processor.process(soup) actual_html = str(soup) # Assert assert actual_html == expected_html def test_hanging_punctuation_target_tags(): """ Отдельный тест для проверки работы со списком целевых тегов. """ mode = ['blockquote', 'h1'] input_html = (f'
{CHAR_RU_QUOT1_OPEN}Игнор{CHAR_RU_QUOT1_CLOSE}
' f'
{CHAR_RU_QUOT1_OPEN}Обработка{CHAR_RU_QUOT1_CLOSE}
' f'

{CHAR_RU_QUOT1_OPEN}Заголовок{CHAR_RU_QUOT1_CLOSE}

') expected_html = (f'
{CHAR_RU_QUOT1_OPEN}Игнор{CHAR_RU_QUOT1_CLOSE}
' f'
{CHAR_RU_QUOT1_OPEN}Обработка{CHAR_RU_QUOT1_CLOSE}
' f'

{CHAR_RU_QUOT1_OPEN}Заголовок{CHAR_RU_QUOT1_CLOSE}

') processor = HangingPunctuationProcessor(mode=mode) soup = make_soup(input_html) processor.process(soup) assert str(soup) == expected_html LEFT_FULL_SYMBOLS = [ (CHAR_RU_QUOT1_OPEN, CHAR_RU_QUOT1_CLOSE, 'etp-laquo'), (CHAR_EN_QUOT1_OPEN, CHAR_EN_QUOT1_CLOSE, 'etp-ldquo'), (CHAR_LPAR, CHAR_RPAR, 'etp-lpar'), (CHAR_LSQB, CHAR_RSQB, 'etp-lsqb'), (CHAR_LCUB, CHAR_RCUB, 'etp-lcub'), ] @pytest.mark.parametrize("open_char, close_char, cls", LEFT_FULL_SYMBOLS) def test_hanging_left_mode_wraps_symbol_pairs(open_char, close_char, cls): """ Убедимся, что разные висячие символы полностью оборачиваются в левом режиме. open_char: символ, открывающий висячий знак. close_char: символ, закрывающий висячий знак. cls: CSS класс для обёртки висячих знаков. """ input_html = f'

{open_char}Текст{close_char}

' expected_html = f'

{open_char}Текст{close_char}

' processor = HangingPunctuationProcessor(mode='left') soup = make_soup(input_html) processor.process(soup) assert str(soup) == expected_html SPACE_VARIANTS = [ ' ', CHAR_SHY, CHAR_THIN_SP, CHAR_HAIR_SP, CHAR_MED_SP, CHAR_EN_SP, CHAR_EM_SP, CHAR_NULL_SP, ] @pytest.mark.parametrize("space", SPACE_VARIANTS) def test_hanging_left_mode_compensates_different_spaces(space): """Проверяем компенсацию для разных типов разрывных пробелов.""" text = f'

Проба{space}{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

' expected_html = (f'

Проба{space}' f'{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

') processor = HangingPunctuationProcessor(mode='left') soup = make_soup(text) processor.process(soup) assert str(soup) == expected_html @pytest.mark.parametrize("separator", [CHAR_NBSP, CHAR_THIN_NBSP, CHAR_ZWNJ]) def test_hanging_left_mode_honors_cancellation(separator): """Символы, отменяющие перенос, остаются без обёрток.""" input_html = f'

Неразрывный{separator}{CHAR_RU_QUOT1_OPEN}Текст{CHAR_RU_QUOT1_CLOSE}

' processor = HangingPunctuationProcessor(mode='left') soup = make_soup(input_html) processor.process(soup) assert str(soup) == input_html