# 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'
{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