# etpgrf/layout.py # Модуль для обработки тире, специальных символов и правил их компоновки. import regex import logging from etpgrf.config import (LANG_RU, LANG_EN, CHAR_NBSP, CHAR_THIN_SP, CHAR_NDASH, CHAR_MDASH, CHAR_HELLIP, CHAR_UNIT_SEPARATOR, DEFAULT_POST_UNITS, DEFAULT_PRE_UNITS, UNIT_MATH_OPERATORS, ABBR_COMMON_FINAL, ABBR_COMMON_PREPOSITION) from etpgrf.comutil import parse_and_validate_langs # -- # --- Настройки логирования --- logger = logging.getLogger(__name__) class LayoutProcessor: """ Обрабатывает тире, псевдографику (например, … -> © и тому подобные) и применяет правила расстановки пробелов в зависимости от языка (для тире язык важен, так как. Правила типографики различаются для русского и английского языков). Предполагается, что на вход уже поступает текст с правильными типографскими символами тире (— и –). """ def __init__(self, langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None, process_initials_and_acronyms: bool = True, process_units: bool | str | list[str] = True): self.langs = parse_and_validate_langs(langs) self.main_lang = self.langs[0] if self.langs else LANG_RU self.process_initials_and_acronyms = process_initials_and_acronyms self.process_units = process_units # 1. Паттерн для длинного (—) или среднего (–) тире, окруженного пробелами. # (?<=[\p{L}\p{Po}\p{Pf}"\']) - просмотр назад на букву, пунктуацию или кавычку. self._dash_pattern = regex.compile(rf'(?<=[\p{{L}}\p{{Po}}\p{{Pf}}"\'])\s+([{CHAR_MDASH}{CHAR_NDASH}])\s+(?=\S)') # 2. Паттерн для многоточия, за которым следует пробел и слово. # Ставит неразрывный пробел после многоточия, чтобы не отрывать его от следующего слова. # (?=[\p{L}\p{N}]) - просмотр вперед на букву или цифру. self._ellipsis_pattern = regex.compile(rf'({CHAR_HELLIP})\s+(?=[\p{{L}}\p{{N}}])') # 3. Паттерн для отрицательных чисел. # Ставит неразрывный пробел перед знаком минус, если за минусом идет цифра (неразрывный пробел # заменяет обычный). Это предотвращает "отлипание" знака от числа при переносе строки. # (? str: """Callback-функция для расстановки пробелов вокруг тире с учетом языка.""" dash = match.group(1) # Получаем сам символ тире (— или –) if self.main_lang == LANG_EN: # Для английского языка — слитно, без пробелов. return dash # По умолчанию (и для русского) — отбивка пробелами. return f'{CHAR_NBSP}{dash} ' def _process_abbreviations(self, text: str, abbreviations: list[str], mode: str) -> str: """ Универсальный обработчик для разных типов сокращений. :param text: Входной текст. :param abbreviations: Список сокращений для обработки. :param mode: 'final' (NBSP ставится перед) или 'prepositional' (NBSP ставится после). :return: Обработанный текст. """ processed_text = text # Шаг 1: "Склеиваем" многосоставные сокращения временным разделителем CHAR_UNIT_SEPARATOR for abbr in sorted(abbreviations, key=len, reverse=True): if ' ' in abbr: pattern = regex.escape(abbr).replace(r'\ ', r'\s*') replacement = abbr.replace(' ', CHAR_UNIT_SEPARATOR) processed_text = regex.sub(pattern, replacement, processed_text, flags=regex.IGNORECASE) # Шаг 2: Ставим неразрывный пробел. glued_abbrs = [a.replace(' ', CHAR_UNIT_SEPARATOR) for a in abbreviations] all_abbrs_pattern = '|'.join(map(regex.escape, sorted(glued_abbrs, key=len, reverse=True))) if mode == 'final': # Ставим nbsp перед сокращением, если перед ним есть пробел nbsp_pattern = regex.compile(r'(\s)(' + all_abbrs_pattern + r')(?=[.,!?]|\s|$)', flags=regex.IGNORECASE) processed_text = nbsp_pattern.sub(fr'{CHAR_NBSP}\2', processed_text) elif mode == 'prepositional': # Ставим nbsp после сокращения, если после него есть пробел nbsp_pattern = regex.compile(r'(' + all_abbrs_pattern + r')(\s)', flags=regex.IGNORECASE) processed_text = nbsp_pattern.sub(fr'\1{CHAR_NBSP}', processed_text) # Шаг 3: Заменяем временный разделитель на правильную тонкую шпацию return processed_text.replace(CHAR_UNIT_SEPARATOR, CHAR_THIN_SP) def process(self, text: str) -> str: """Применяет правила компоновки к тексту.""" # Порядок применения правил важен. processed_text = text # 1. Обработка пробелов вокруг тире. processed_text = self._dash_pattern.sub(self._replace_dash_spacing, processed_text) # 2. Обработка пробела после многоточия. processed_text = self._ellipsis_pattern.sub(f'\\1{CHAR_NBSP}', processed_text) # 3. Обработка пробела перед отрицательными числами/минусом. processed_text = self._negative_number_pattern.sub(f'{CHAR_NBSP}-\\1', processed_text) # 4. Обработка сокращений. processed_text = self._process_abbreviations(processed_text, ABBR_COMMON_FINAL, 'final') processed_text = self._process_abbreviations(processed_text, ABBR_COMMON_PREPOSITION, 'prepositional') # 5. Обработка инициалов и акронимов (если включено). if self.process_initials_and_acronyms: # Сначала вставляем тонкие пробелы там, где пробелов не было. processed_text = self._initial_to_initial_ns_pattern.sub(f'\\1{CHAR_THIN_SP}', processed_text) processed_text = self._initial_to_surname_ns_pattern.sub(f'\\1{CHAR_THIN_SP}', processed_text) # Затем заменяем существующие пробелы на неразрывные. processed_text = self._initial_to_initial_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text) processed_text = self._initial_to_surname_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text) processed_text = self._surname_to_initial_ws_pattern.sub(f'\\1{CHAR_NBSP}', processed_text) # 6. Обработка единиц измерения (если включено). if self.process_units: if self._complex_unit_pattern: # Шаг 1: "Склеиваем" все составные единицы с помощью временного разделителя. # Цикл безопасен, так как мы заменяем пробелы на непробельный символ, и паттерн не найдет себя снова. while self._complex_unit_pattern.search(processed_text): processed_text = self._complex_unit_pattern.sub( fr'\1.{CHAR_UNIT_SEPARATOR}\3', processed_text, count=1) if self._math_unit_pattern: # processed_text = self._math_unit_pattern.sub(r'\1/\2', processed_text) processed_text = self._math_unit_pattern.sub(r'\1\2\3', processed_text) # И только потом привязываем простые единицы к числам if self._post_units_pattern: processed_text = self._post_units_pattern.sub(f'\\1{CHAR_NBSP}\\2', processed_text) if self._pre_units_pattern: processed_text = self._pre_units_pattern.sub(f'\\1{CHAR_NBSP}\\2', processed_text) # Шаг 2: Заменяем все временные разделители на правильную тонкую шпацию. processed_text = processed_text.replace(CHAR_UNIT_SEPARATOR, CHAR_THIN_SP) return processed_text