diff --git a/README.md b/README.md index 61c31e8..f9deb39 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,23 @@ typo = etpgrf.Typographer(process_units=False) result = typo.process("100 км/ч") # Останется без изменений ``` +#### Сокращения + +Типограф также обрабатывает распространённые русскоязычные сокращения, чтобы они корректно отображались и не разрывались +при переносе строк. Правила делятся на два типа: +* Финальные сокращения. Сокращения, которые обычно стоят в конце фразы (например, и т. д., и т. п.), + обрабатываются особым образом: их части «склеиваются» тонкой шпацией, а перед всей конструкцией ставится неразрывный + пробел, чтобы она не «повисла» на новой строке. `...и так далее, и т. д.` → `...и так далее, и т. д.` + Это правило работает независимо от того, как сокращение было написано в исходном тексте (т.д. или т. д.). +* Препозиционные сокращения. Сокращения, которые стоят перед другим словом (например, и. о. директора, т. е. сказать), + также «склеиваются» внутри, но неразрывный пробел ставится после них, чтобы привязать их к последующему слову. + `Назначить и. о. директора` → `Назначить и. о. директора` + +Библиотека знает небольшой набор самых распространённых сокращений. Но не все, а некоторые принципиально невозможны +к обработке. Например, сокращение `пр.` может оказаться как финальным (в значении «и так далее»), так и препозиционным +(в значении «профессор» или «проспект»). Так же типограф не обрабатывает сокращения, связанные с адресами (ул., д., +кв., пл., наб. ...) так как они могут быть как финальными, так и препозиционными. + ## P.S. diff --git a/etpgrf/config.py b/etpgrf/config.py index c717250..23b31da 100644 --- a/etpgrf/config.py +++ b/etpgrf/config.py @@ -659,9 +659,12 @@ UNIT_MATH_OPERATORS = ['/', '*', '×', CHAR_MIDDOT, '÷'] # Эти сокращения (обычно в конце фразы) будут "склеены" тонкой шпацией, а перед ними будет поставлен неразрывный пробел. # Важно, чтобы многосложные сокращения (типа "и т. д.") были в списке с разделителем пробелом (иначе мы не сможем их найти). ABBR_COMMON_FINAL = [ - 'т. д.', 'т. п.', 'др.', 'пр.', + # 'т. д.', 'т. п.', 'др.', 'пр.', + # УБРАНЫ из-за неоднозначности: др. -- "другой", "доктор", "драм" / пр. -- "прочие", "профессор", "проект", "проезд" ... + 'т. д.', 'т. п.', ] ABBR_COMMON_PREPOSITION = [ - 'т.е.', 'т.к.', 'т.о.', 'и.о.', 'ио', 'вр.и.о.', 'врио' + 'т. е.', 'т. к.', 'т. о.', 'и. о.', 'ио', 'вр. и. о.', 'врио', 'тов.', 'г-н.', 'г-жа.', 'им.', + 'д. о. с.', 'д. о. н.', 'д. м. н.', 'к. т. д.', 'к. т. п.', ] \ No newline at end of file diff --git a/etpgrf/layout.py b/etpgrf/layout.py index f6b1cce..4d37d09 100644 --- a/etpgrf/layout.py +++ b/etpgrf/layout.py @@ -5,7 +5,7 @@ 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_FINAL, ABBR_COMMON_PREPOSITION) from etpgrf.comutil import parse_and_validate_langs @@ -124,6 +124,39 @@ class LayoutProcessor: # По умолчанию (и для русского) — отбивка пробелами. 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: """Применяет правила компоновки к тексту.""" @@ -139,24 +172,9 @@ class LayoutProcessor: # 3. Обработка пробела перед отрицательными числами/минусом. processed_text = self._negative_number_pattern.sub(f'{CHAR_NBSP}-\\1', processed_text) - # 4. Обработка финальных сокращений (т.д., т.п. и т.д.) - # Шаг 1: "Склеиваем" многосоставные сокращения временным разделителем. - temp_processed_text = processed_text - for abbr in ABBR_COMMON_FINAL: - if ' ' in abbr: # Обрабатываем только многосоставные - pattern = regex.escape(abbr).replace(r'\ ', r'\s*') - replacement = abbr.replace(' ', CHAR_UNIT_SEPARATOR) - temp_processed_text = regex.sub(pattern, replacement, temp_processed_text, flags=regex.IGNORECASE) - - # Шаг 2: Ставим неразрывный пробел перед всеми финальными сокращениями (уже "склеенными"). - # Создаем паттерн из всех вариантов - и простых, и "склеенных". - glued_abbrs = [a.replace(' ', CHAR_UNIT_SEPARATOR) for a in ABBR_COMMON_FINAL] - all_final_abbrs_pattern = '|'.join(map(regex.escape, sorted(glued_abbrs, key=len, reverse=True))) - nbsp_pattern = regex.compile(r'(\s)(' + all_final_abbrs_pattern + r')(?=[.,!?]|\s|$)', flags=regex.IGNORECASE) - processed_text = nbsp_pattern.sub(fr'{CHAR_NBSP}\2', temp_processed_text) - - # Шаг 3: Заменяем временный разделитель на правильную тонкую шпацию. - processed_text = processed_text.replace(CHAR_UNIT_SEPARATOR, CHAR_THIN_SP) + # 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: diff --git a/tests/test_layout.py b/tests/test_layout.py index ccae6e1..168c090 100644 --- a/tests/test_layout.py +++ b/tests/test_layout.py @@ -127,20 +127,36 @@ LAYOUT_TEST_CASES = [ ('ru', "За окном 15 °C", f"За окном 15{CHAR_NBSP}°C"), ('ru', "HiFi 20 Гц - 20 кГц", f"HiFi 20{CHAR_NBSP}Гц - 20{CHAR_NBSP}кГц"), - # Финальные сокращения - ('ru', "1 и т.д.", f"1 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."), - ('ru', "2 и т. д.", f"2 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."), - ('ru', "3 и т.д., и др.", f"3 и{CHAR_NBSP}т.{CHAR_THIN_SP}д., и{CHAR_NBSP}др."), # Слитное написание - ('ru', "4 и т.п., и пр.", f"4 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}пр."), # Слитное написание - ('ru', "5 и т. п., и т.п., и пр.", f"5 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}пр."), # Слитное и раздельное написание - - # Сложные единицы (склеиваются тонкой шпацией, привязываются к числу неразрывным пробелом) ('ru', "Дом 120 кв.м. / Участок 6 сот.", f"Дом 120{CHAR_NBSP}кв.{CHAR_THIN_SP}м. / Участок 6{CHAR_NBSP}сот."), # ('ru', "Гробик кладут в ямку 2 кв. м.", f"Гробик кладут в ямку 2 кв. м."), ('ru', "500 до н. э.", f"500 до н.{CHAR_THIN_SP}э."), ('ru+en', "Хаммурапи (1792 - 1750 до н. э.)", f"Хаммурапи (1792 - 1750 до н.{CHAR_THIN_SP}э.)"), + # Финальные сокращения + ('ru', "1 и т.д.", f"1 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."), + ('ru', "2 и т. \n д.", f"2 и{CHAR_NBSP}т.{CHAR_THIN_SP}д."), + ('ru', "3 и т.д., и др.", f"3 и{CHAR_NBSP}т.{CHAR_THIN_SP}д., и др."), # Слитное написание + ('ru', "4 и т.п., и пр.", f"4 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и пр."), # Слитное написание + ('ru', "5 и т. п., и т.п., и пр.", f"5 и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и{CHAR_NBSP}т.{CHAR_THIN_SP}п., и пр."), # Слитное и раздельное написание + + # Препозиционные сокращения + ('ru', "Назначить и.о. директора", f"Назначить и.{CHAR_THIN_SP}о.{CHAR_NBSP}директора"), + ('ru', "Назначить ио директора", f"Назначить ио{CHAR_NBSP}директора"), # без точек + ('ru', "замечаний не было, т. е. не было и ошибок", f"замечаний не было, т.{CHAR_THIN_SP}е.{CHAR_NBSP}не было и ошибок"), + ('ru', "Назначить и. о. директора", f"Назначить и.{CHAR_THIN_SP}о.{CHAR_NBSP}директора"), # с пробелом + ('ru', "Назначить и.о. директора", f"Назначить и.{CHAR_THIN_SP}о.{CHAR_NBSP}директора"), # без пробела + ('ru', "Назначить ио директора", f"Назначить ио{CHAR_NBSP}директора"), # без точек + ('ru', "то есть т. е. сказать", f"то есть т.{CHAR_THIN_SP}е.{CHAR_NBSP}сказать"), + ('ru', "таким образом, т. о. мы видим", f"таким образом, т.{CHAR_THIN_SP}о.{CHAR_NBSP}мы видим"), + ('ru', "потому что т.к. это важно", f"потому что т.{CHAR_THIN_SP}к.{CHAR_NBSP}это важно"), + ('ru', "Назначить вр. и. о. начальника", f"Назначить вр.{CHAR_THIN_SP}и.{CHAR_THIN_SP}о.{CHAR_NBSP}начальника"), + ('ru', "Назначить врио начальника", f"Назначить врио{CHAR_NBSP}начальника"), + ('ru', "Выступает тов. Сухов", f"Выступает тов.{CHAR_NBSP}Сухов"), + ('ru', "Приехал г-н. Петров", f"Приехал г-н.{CHAR_NBSP}Петров"), + ('ru', "Институт им. Курчатова", f"Институт им.{CHAR_NBSP}Курчатова"), + ('ru', "собаку оперировал д. м. н. профессор Преображенский", f"собаку оперировал д.{CHAR_THIN_SP}м.{CHAR_THIN_SP}н.{CHAR_NBSP}профессор Преображенский"), + # --- Комбинированные случаи --- ('ru', f"Да — это так{CHAR_HELLIP} а может и нет. Счёт -10.", f"Да{CHAR_NBSP}— это так{CHAR_HELLIP}{CHAR_NBSP}а может и нет. Счёт{CHAR_NBSP}-10."),