diff --git a/etpgrf/comutil.py b/etpgrf/comutil.py
index 0b1baae..6fb0e94 100644
--- a/etpgrf/comutil.py
+++ b/etpgrf/comutil.py
@@ -4,7 +4,7 @@ import os
import regex
-def parce_and_validate_mode(
+def parse_and_validate_mode(
mode_input: str | None = None,
) -> str:
"""
diff --git a/etpgrf/config.py b/etpgrf/config.py
index cd46634..310845e 100644
--- a/etpgrf/config.py
+++ b/etpgrf/config.py
@@ -9,9 +9,11 @@ MODE_MIXED = "mixed"
DEFAULT_MODE = MODE_MIXED
# Языки, поддерживаемые библиотекой
-SUPPORTED_LANGS = frozenset(['ru', 'en'])
+LANG_RU = 'ru' # Русский
+LANG_EN = 'en' # Английский
+SUPPORTED_LANGS = frozenset([LANG_RU, LANG_EN])
# Язык(и) по умолчанию, если не указаны пользователем и не заданы через ETPGRF_DEFAULT_LANGS_MODULE
-DEFAULT_LANGS = 'ru'
+DEFAULT_LANGS = LANG_RU
#
# ----------------- соответствия `unicode` и `mnemonic` для типографа
diff --git a/etpgrf/hyphenation.py b/etpgrf/hyphenation.py
index a817dbf..80dc2f0 100755
--- a/etpgrf/hyphenation.py
+++ b/etpgrf/hyphenation.py
@@ -1,13 +1,13 @@
import regex
-from etpgrf.config import DEFAULT_MODE, DEFAULT_LANGS, SHY_ENTITIES, MODE_UNICODE
-from etpgrf.comutil import parce_and_validate_mode, parse_and_validate_langs
+from etpgrf.config import LANG_RU, LANG_EN, DEFAULT_MODE, DEFAULT_LANGS, SHY_ENTITIES, MODE_UNICODE
+from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ'])
_RU_J_SOUND_UPPER = frozenset(['Й'])
_RU_SIGNS_UPPER = frozenset(['Ь', 'Ъ'])
-_EN_VOWELS_UPPER = frozenset(['A', 'E', 'I', 'O', 'U'])
+_EN_VOWELS_UPPER = frozenset(['A', 'E', 'I', 'O', 'U', 'Æ', 'Œ'])
_EN_CONSONANTS_UPPER = frozenset(['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'])
@@ -15,12 +15,12 @@ class Hyphenator:
"""Правила расстановки переносов для разных языков.
"""
def __init__(self,
- langs: frozenset[str] = None, # Языки, которые обрабатываем в переносе слов
+ langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
mode: str = None, # Режим обработки текста
max_unhyphenated_len: int = 14, # Максимальная длина непереносимой группы
min_chars_per_part: int = 3): # Минимальная длина после переноса (хвост, который разрешено переносить)
self.langs: frozenset[str] = parse_and_validate_langs(langs)
- self.mode: str = parce_and_validate_mode(mode)
+ self.mode: str = parse_and_validate_mode(mode)
self.max_unhyphenated_len = max_unhyphenated_len
self.min_chars_per_part = min_chars_per_part
@@ -29,6 +29,8 @@ class Hyphenator:
self._consonants: frozenset = frozenset()
self._j_sound_upper: frozenset = frozenset()
self._signs_upper: frozenset = frozenset()
+ self._ru_alphabet_upper: frozenset = frozenset()
+ self._en_alphabet_upper: frozenset = frozenset()
# Загружает наборы символов на основе self.langs
self._load_language_resources_for_hyphenation()
# Определяем символ переноса в зависимости от режима
@@ -37,14 +39,16 @@ class Hyphenator:
def _load_language_resources_for_hyphenation(self):
# Определяем наборы гласных, согласных и т.д. в зависимости языков.
- if "ru" in self.langs:
+ if LANG_RU in self.langs:
self._vowels |= _RU_VOWELS_UPPER
self._consonants |= _RU_CONSONANTS_UPPER
self._j_sound_upper |= _RU_J_SOUND_UPPER
self._signs_upper |= _RU_SIGNS_UPPER
- if "en" in self.langs:
+ self._ru_alphabet_upper |= _RU_VOWELS_UPPER | _RU_CONSONANTS_UPPER | _RU_SIGNS_UPPER | _RU_J_SOUND_UPPER
+ if LANG_EN in self.langs:
self._vowels |= _EN_VOWELS_UPPER
self._consonants |= _EN_CONSONANTS_UPPER
+ self._en_alphabet_upper |= _EN_VOWELS_UPPER | _EN_CONSONANTS_UPPER
# ... и для других языков, если они поддерживаются переносами
# --- Сюда переносятся все методы, связанные с переносами ---
@@ -79,67 +83,119 @@ class Hyphenator:
:param word: Слово, в котором надо расставить переносы
:return: Слово с расставленными переносами
"""
-
- # Поиск допустимой позиции для переноса около заданного индекса
- def find_hyphen_point(word_segment: str, start_idx: int) -> int:
- vow_indices = [i for i, char_w in enumerate(word_segment) if self._is_vow(char_w)]
- if not vow_indices:
- # Если в слове нет гласных, то перенос невозможен
- return -1
- # Ищем ближайшую гласную до или после start_idx
- for i in vow_indices:
- if i >= start_idx - self.min_chars_per_part and i + self.min_chars_per_part < len(word_segment):
- # Проверяем, что после гласной есть минимум символов "хвоста"
- ind = i + 1
- if (self._is_cons(word_segment[ind]) or self._is_j_sound(word_segment[ind])) and not self._is_vow(word_segment[ind + 1]):
- # Й -- полугласная. Перенос после неё только в случае, если дальше идет согласная
- # (например, "бой-кий"), но запретить, если идет гласная (например, "ма-йка" не переносится).
- ind += 1
- if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part:
- # Не отделяем 3 символ с начала или конца (это некрасиво)
- continue
- if self._is_sign(word_segment[ind]) or self._is_sign(word_segment[-1]):
- # Пропускаем мягкий/твердый знак. Согласно правилам русской типографики (например, ГОСТ 7.62-2008
- # или рекомендации по набору текста), перенос не должен разрывать слово так, чтобы мягкий или
- # твердый знак оказывался в начале или конце строки
- continue
- return ind
- return -1 # Не нашли подходящую позицию
-
- # Рекурсивное деление слова
- def split_word(word_to_split: str) -> str:
- if len(word_to_split) <= self.max_unhyphenated_len: # Если длина укладывается в лимит, перенос не нужен
- return word_to_split
-
- hyphen_idx = find_hyphen_point(word_to_split, len(word_to_split) // 2) # Ищем точку переноса около середины
-
- if hyphen_idx == -1: # Если не нашли точку переноса
- return word_to_split
-
- left_part = word_to_split[:hyphen_idx]
- right_part = word_to_split[hyphen_idx:]
-
- # Рекурсивно делим левую и правую части и соединяем их через символ переноса
- return split_word(left_part) + self._split_code + split_word(right_part)
-
- # Основная логика
+ # 1. ОБЩИЕ ПРОВЕРКИ
+ # TODO: возможно, для скорости, надо сделать проверку на пробелы и другие разделители, которых не должно быть
+ if not word:
+ # Добавим явную проверку на пустую строку
+ return ""
if len(word) <= self.max_unhyphenated_len or not any(self._is_vow(c) for c in word):
- # Короткое слово или без гласных "делению не подлежит", выходим из рекурсии
+ # Если слово короткое или не содержит гласных, перенос не нужен
+ return word
+
+ # 2. ОБНАРУЖЕНИЕ ЯЗЫКА И ПОДКЛЮЧЕНИЕ ЯЗЫКОВОЙ ЛОГИКИ
+ # Поиск вхождения букв строки (слова) через `frozenset` -- O(1). Это быстрее регулярного выражения -- O(n)
+ # 2.1. Проверяем RU
+ if LANG_RU in self.langs and frozenset(word.upper()) <= self._ru_alphabet_upper:
+ # Пользователь подключил русскую логику, и слово содержит только русские буквы
+ # Поиск допустимой позиции для переноса около заданного индекса
+ def find_hyphen_point_ru(word_segment: str, start_idx: int) -> int:
+ vow_indices = [i for i, char_w in enumerate(word_segment) if self._is_vow(char_w)]
+ # Если в слове нет гласных, то перенос невозможен
+ if not vow_indices:
+ return -1
+ # Ищем ближайшую гласную до или после start_idx
+ for i in vow_indices:
+ if i >= start_idx - self.min_chars_per_part and i + self.min_chars_per_part < len(word_segment):
+ # Проверяем, что после гласной есть минимум символов "хвоста"
+ ind = i + 1
+ if (self._is_cons(word_segment[ind]) or self._is_j_sound(word_segment[ind])) and not self._is_vow(word_segment[ind + 1]):
+ # Й -- полугласная. Перенос после неё только в случае, если дальше идет согласная
+ # (например, "бой-кий"), но запретить, если идет гласная (например, "ма-йка" не переносится).
+ ind += 1
+ if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part:
+ # Не отделяем 3 символ с начала или конца (это некрасиво)
+ continue
+ if self._is_sign(word_segment[ind]) or (ind > 0 and self._is_sign(word_segment[ind-1])):
+ # Пропускаем мягкий/твердый знак, если перенос начинается или заканчивается на них (ГОСТ 7.62-2008)
+ continue
+ return ind
+ return -1 # Не нашли подходящую позицию
+
+ # Рекурсивное деление слова
+ def split_word_ru(word_to_split: str) -> str:
+ # Если длина укладывается в лимит, перенос не нужен
+ if len(word_to_split) <= self.max_unhyphenated_len:
+ return word_to_split
+ # Ищем точку переноса около середины
+ hyphen_idx = find_hyphen_point_ru(word_to_split, len(word_to_split) // 2)
+ # Если не нашли точку переноса
+ if hyphen_idx == -1:
+ return word_to_split
+ # Разделяем слово на две части (до и после точки переноса)
+ left_part = word_to_split[:hyphen_idx]
+ right_part = word_to_split[hyphen_idx:]
+ # Рекурсивно делим левую и правую части и соединяем их через символ переноса
+ return split_word_ru(left_part) + self._split_code + split_word_ru(right_part)
+
+ # Основная логика
+ return split_word_ru(word) # Рекурсивно делим слово на части с переносами
+
+ # 2.2. Проверяем EN
+ elif LANG_EN in self.langs and frozenset(word.upper()) <= self._en_alphabet_upper:
+ # Пользователь подключил английскую логику, и слово содержит только английские буквы
+ print(f"#### Applying English rules to: {word}") # Для отладки
+ # --- Начало логики для английского языка (заглушка) ---
+ # ПРИМЕЧАНИЕ: Это очень упрощенная заглушка.
+ def find_hyphen_point_en(word_segment: str) -> int:
+ for i in range(self.min_chars_per_part, len(word_segment) - self.min_chars_per_part):
+ if self._is_vow(word_segment[i - 1]) and self._is_cons(word_segment[i]):
+ if len(word_segment[:i]) >= self.min_chars_per_part and \
+ len(word_segment[i:]) >= self.min_chars_per_part:
+ return i
+ return -1
+
+ def split_word_en(word_to_split: str) -> str:
+ if len(word_to_split) <= self.max_unhyphenated_len:
+ return word_to_split
+ hyphen_idx = find_hyphen_point_en(word_to_split)
+ if hyphen_idx != -1:
+ return word_to_split[:hyphen_idx] + self._split_code + word_to_split[hyphen_idx:]
+ return word_to_split
+ # --- Конец логики для английского языка ---
+ return split_word_en(word)
+ else:
+ # кстати "слова" в которых есть пробелы или другие разделители, тоже попадают сюда
+ print("!!!!ФИГНЯ")
return word
- return split_word(word) # Рекурсивно делим слово на части с переносами
def hyp_in_text(self, text: str) -> str:
""" Расстановка переносов в тексте
:param text: Строка, которую надо обработать (главный аргумент).
- :return: str:
+ :return: str: Строка с расставленными переносами.
"""
- rus_worlds = regex.findall(r'\b[а-яА-Я]+\b', text) # ищем все русскоязычные слова в тексте
- for word in rus_worlds:
- if len(word) > self.max_unhyphenated_len:
- hyphenated_word = self.hyp_in_word(word)
- print(f'{word} -> {hyphenated_word}')
- text = text.replace(word, hyphenated_word)
- return text
+
+ # 1. Определяем функцию, которая будет вызываться для каждого найденного слова
+ def replace_word_with_hyphenated(match_obj):
+ # Модуль regex автоматически передает сюда match_obj для каждого совпадения.
+ # Чтобы получить `слово` из 'совпадения' делаем .group() или .group(0).
+ word_to_process = match_obj.group(0)
+ # И оправляем это слово на расстановку переносов (внутри hyp_in_word уже есть все проверки).
+ hyphenated_word = self.hyp_in_word(word_to_process)
+
+ # ============= Для отладки (слова в которых появились переносы) ==================
+ if word_to_process != hyphenated_word:
+ print(f"hyp_in_text: '{word_to_process}' -> '{hyphenated_word}'")
+
+ return hyphenated_word
+
+ # 2. regex.sub() -- поиск с заменой. Ищем по паттерну `r'\b\p{L}+\b'` (`\b` - граница слова;
+ # `\p{L}` - любая буква Unicode; `+` - одно или более вхождений).
+ # Второй аргумент - это наша функция replace_word_with_hyphenated.
+ # regex.sub вызовет ее для каждого найденного слова, передав match_obj.
+ processed_text = regex.sub(pattern=r'\b\p{L}+\b', repl=replace_word_with_hyphenated, string=text)
+
+ return processed_text
+
diff --git a/etpgrf/typograph.py b/etpgrf/typograph.py
index fa9906e..850ad97 100644
--- a/etpgrf/typograph.py
+++ b/etpgrf/typograph.py
@@ -1,4 +1,4 @@
-from etpgrf.comutil import parce_and_validate_mode, parse_and_validate_langs
+from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
from etpgrf.hyphenation import Hyphenator
import copy
@@ -17,7 +17,7 @@ class Typographer:
self.langs: frozenset[str] = parse_and_validate_langs(langs)
# --- Обработка и валидация параметра mode ---
- self.mode: str = parce_and_validate_mode(mode)
+ self.mode: str = parse_and_validate_mode(mode)
# Сохраняем переданные модули правил
if hyphenation_rule is not None:
diff --git a/main.py b/main.py
index 159aae2..219be73 100644
--- a/main.py
+++ b/main.py
@@ -3,20 +3,25 @@ import etpgrf
if __name__ == '__main__':
# --- Пример использования ---
- ETPGRF_DEFAULT_LANGS = "ru"
print("\n--- Пример использования класса---\n")
# Определяем пользовательские правила переносов
- hyphen_settings = etpgrf.Hyphenator(langs=frozenset(['ru']), max_unhyphenated_len=8)
+ hyphen_settings = etpgrf.Hyphenator(langs='ru', max_unhyphenated_len=8)
# Определяем пользовательские правила типографа
- typo = etpgrf.Typographer(langs='ru', mode='mnemonic', hyphenation_rule=hyphen_settings)
result = hyphen_settings.hyp_in_text("Бармалейщина")
print(result, "\n\n")
result = hyphen_settings.hyp_in_word("Длинношеевый жираф")
print(result, "\n\n")
+
+ hyphen_settings2 = etpgrf.Hyphenator(langs='en', max_unhyphenated_len=8)
+ result = hyphen_settings2.hyp_in_text("frozenseter")
+ print(result, "\n\n")
+
+ typo = etpgrf.Typographer(langs='ru', mode='mnemonic', hyphenation_rule=hyphen_settings)
result = typo.process(text="Какой-то длинный текст для проверки переносов. Перпердикюляция!")
print(result, "\n\n")
- result = typo.process(text="Привет, World! Это тестовый текст для проверки расстановки переносов в словах. Миллион 100-метровошеих жирножирафов.")
+ result = typo.process(text="Привет, frozenseter! Это тестовый текст для проверки расстановки переносов"
+ " в словах. Миллион 100-метровошеих жирножирафов.")
print(result, "\n\n")
txt = ("Каждое пальто, которое мы создаём — это не просто одежда. Это"