diff --git a/README.md b/README.md index 3202085..c3fd889 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,7 @@ typo = etpgrf.Typographer(hanging_punctuation='left') ### CSS для висячих символов -Предлагаемый CSS теперь работает только с `margin` и `padding`, без `position:absolute`. Пробелы получают собственные +Предлагаемый, начиная с etpgrf v0.1.6, CSS теперь работает только с `margin` и `padding`, без `position:absolute`. Пробелы получают собственные классы, поэтому их компенсация контролируется отдельно, а не встроена в сам висячий символ. Убедитесь, что эти стили подключены к странице и не конфликтуют с `text-justify`, который вытягивает пробелы по всей строке и разрушает аккуратное выравнивание. diff --git a/etpgrf/config.py b/etpgrf/config.py index e8e6549..e9e2c7a 100644 --- a/etpgrf/config.py +++ b/etpgrf/config.py @@ -690,6 +690,13 @@ PROTECTED_HTML_TAGS = ['style', 'script', 'pre', 'code', 'kbd', 'samp', 'math'] # === КОНСТАНТЫ ДЛЯ ВИСЯЧЕЙ ТИПОГРАФИКИ === +HANGING_PUNCTUATION_MODE_LEFT = 'left' +HANGING_PUNCTUATION_MODE_RIGHT = 'right' +HANGING_PUNCTUATION_MODES = frozenset([ + HANGING_PUNCTUATION_MODE_LEFT, + HANGING_PUNCTUATION_MODE_RIGHT, +]) + # 1. Набор символов, которые могут "висеть" слева HANGING_PUNCTUATION_LEFT_CHARS = frozenset([ CHAR_RU_QUOT1_OPEN, # « @@ -722,4 +729,5 @@ HANGING_PUNCTUATION_CLASSES = { '.': 'etp-r-dot', ',': 'etp-r-comma', ':': 'etp-r-colon', -} \ No newline at end of file +} + diff --git a/etpgrf/hanging.py b/etpgrf/hanging.py index 681b6c9..46cda0b 100644 --- a/etpgrf/hanging.py +++ b/etpgrf/hanging.py @@ -6,7 +6,9 @@ from bs4 import BeautifulSoup, NavigableString, Tag from .config import ( HANGING_PUNCTUATION_LEFT_CHARS, HANGING_PUNCTUATION_RIGHT_CHARS, - HANGING_PUNCTUATION_CLASSES + HANGING_PUNCTUATION_CLASSES, + HANGING_PUNCTUATION_MODE_LEFT, + HANGING_PUNCTUATION_MODE_RIGHT, ) logger = logging.getLogger(__name__) @@ -21,30 +23,27 @@ class HangingPunctuationProcessor: """ :param mode: Режим работы: - None / False: отключено. - - 'left': только левая пунктуация. - - 'right': только правая пунктуация. - - 'both' / True: и левая, и правая. + - 'left': левая висячая пунктуация. + - 'right': правая висячая пунктуация. - list[str]: список тегов (например, ['p', 'blockquote']), - внутри которых применять 'both'. + внутри которых применять висячую пунктуацию в обе стороны. + - True эквивалентно 'left'. """ self.mode = mode self.target_tags = None self.active_chars = set() - # Определяем, какие символы будем обрабатывать if isinstance(mode, list): self.target_tags = set(t.lower() for t in mode) - # Если передан список тегов, включаем полный режим ('both') внутри них self.active_chars.update(HANGING_PUNCTUATION_LEFT_CHARS) self.active_chars.update(HANGING_PUNCTUATION_RIGHT_CHARS) - elif mode == 'left': - self.active_chars.update(HANGING_PUNCTUATION_LEFT_CHARS) - elif mode == 'right': - self.active_chars.update(HANGING_PUNCTUATION_RIGHT_CHARS) - elif mode == 'both' or mode is True: - self.active_chars.update(HANGING_PUNCTUATION_LEFT_CHARS) - self.active_chars.update(HANGING_PUNCTUATION_RIGHT_CHARS) - + else: + normalized_mode = HANGING_PUNCTUATION_MODE_LEFT if mode is True else mode + if normalized_mode == HANGING_PUNCTUATION_MODE_LEFT: + self.active_chars.update(HANGING_PUNCTUATION_LEFT_CHARS) + elif normalized_mode == HANGING_PUNCTUATION_MODE_RIGHT: + self.active_chars.update(HANGING_PUNCTUATION_RIGHT_CHARS) + # Предварительно фильтруем карту классов, оставляя только активные символы self.char_to_class = { char: cls diff --git a/pyproject.toml b/pyproject.toml index 58c8d1d..314e15d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "Homepage" = "https://github.com/erjemin/etpgrf" "Bug Tracker" = "https://github.com/erjemin/etpgrf/issues" "Mirror1 (GitVerse)" = "https://gitverse.ru/erjemin/etpgrf" -"Nirror2 (Gitea Selfhosted)" = "https://git.cube2.ru/erjemin/2025-etpgrf" +"Mirror2 (Gitea Selfhosted)" = "https://git.cube2.ru/erjemin/2025-etpgrf" [tool.setuptools.packages.find] where = ["."] # Искать пакеты в корне (найдет папку etpgrf) diff --git a/tests/test_hanging.py b/tests/test_hanging.py index 88614b0..5d2baae 100644 --- a/tests/test_hanging.py +++ b/tests/test_hanging.py @@ -19,60 +19,34 @@ 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'(Скобки)
', - f'(Скобки)
'), - # Правая пунктуация игнорируется - ('left', f'Текст.
', f'Текст.
'), + ('left', '(Скобки)
', '(Скобки)
'), + ('left', 'Текст.
', 'Текст.
'), # --- Режим 'right' (только правая пунктуация) --- ('right', f'{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}
', f'{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}
'), - ('right', f'Текст.
', - f'Текст.
'), - # Левая пунктуация игнорируется - ('right', f'(Скобки)
', f'(Скобки)
'), - - # --- Режим 'both' (и левая, и правая) --- - ('both', f'{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}
', - f'{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}
'), - ('both', f'Текст.
', - f'Текст.
'), - # Последовательность символов (точка + кавычка) - ('both', f'Текст.{CHAR_RU_QUOT1_CLOSE}
', - f'Текст.{CHAR_RU_QUOT1_CLOSE}
'), - # Вложенные теги - ('both', f'{CHAR_RU_QUOT1_OPEN}Жирный{CHAR_RU_QUOT1_CLOSE}
', - f'{CHAR_RU_QUOT1_OPEN}Жирный{CHAR_RU_QUOT1_CLOSE}
'), - # Смешанный контент - ('both', 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}
'), +] - # --- Отсутствие висячих символов --- - ('both', 'Простой текст без спецсимволов!
', 'Простой текст без спецсимволов!
'), - - # --- Проверка контекста (пробелы) --- - # 1. Левая кавычка внутри слова (не должна висеть) - ('both', f'func{CHAR_RU_QUOT1_OPEN}arg{CHAR_RU_QUOT1_CLOSE}
', - f'func{CHAR_RU_QUOT1_OPEN}arg{CHAR_RU_QUOT1_CLOSE}
'), # Правая висит, т.к. конец узла - # 2. Правая кавычка внутри слова (не должна висеть) - ('both', f'1{CHAR_RU_QUOT1_CLOSE}2
', - f'1{CHAR_RU_QUOT1_CLOSE}2
'), - # 3. Левая кавычка после пробела (должна висеть) - ('both', f'func {CHAR_RU_QUOT1_OPEN}arg
', - f'func {CHAR_RU_QUOT1_OPEN}arg
'), - # 4. Правая кавычка перед пробелом (должна висеть) - ('both', f'arg{CHAR_RU_QUOT1_CLOSE} next
', - f'arg{CHAR_RU_QUOT1_CLOSE} next
'), - # 5. Точка внутри числа (не должна висеть) - ('both', '3.14
', '3.14
'), - # 6. Точка в конце предложения (должна висеть) - ('both', 'End.
', 'End.
'), +# --- Режим list[str] (список тегов с обеими сторонами) --- +HANGING_LIST_MODE_CASES = [ + (['p'], f'{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}
', + f'{CHAR_RU_QUOT1_OPEN}Цитата{CHAR_RU_QUOT1_CLOSE}
'), + (['p'], f'Текст.{CHAR_RU_QUOT1_CLOSE}
', + f'Текст.{CHAR_RU_QUOT1_CLOSE}
'), + (['p'], f'func {CHAR_RU_QUOT1_OPEN}arg
', + f'func {CHAR_RU_QUOT1_OPEN}arg
'), + (['p'], f'arg{CHAR_RU_QUOT1_CLOSE} next
', + f'arg{CHAR_RU_QUOT1_CLOSE} next
'), ] @@ -112,3 +86,13 @@ def test_hanging_punctuation_target_tags(): processor.process(soup) assert str(soup) == expected_html + + +@pytest.mark.parametrize("mode, input_html, expected_html", HANGING_LIST_MODE_CASES) +def test_hanging_punctuation_processor_list_mode(mode, input_html, expected_html): + """Проверяет, что list-режим работает и для левой, и для правой стороны внутри указанного тега.""" + processor = HangingPunctuationProcessor(mode=mode) + soup = make_soup(input_html) + + processor.process(soup) + assert str(soup) == expected_html