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