add: разделены правила для языков + провеки на языки-алфавиты
This commit is contained in:
parent
4d9f4a798e
commit
f0b9784737
@ -4,7 +4,7 @@ import os
|
||||
import regex
|
||||
|
||||
|
||||
def parce_and_validate_mode(
|
||||
def parse_and_validate_mode(
|
||||
mode_input: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
|
@ -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` для типографа
|
||||
|
@ -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,12 +83,25 @@ class Hyphenator:
|
||||
:param word: Слово, в котором надо расставить переносы
|
||||
:return: Слово с расставленными переносами
|
||||
"""
|
||||
# 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(word_segment: str, start_idx: int) -> int:
|
||||
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:
|
||||
# Если в слове нет гласных, то перенос невозможен
|
||||
if not vow_indices:
|
||||
return -1
|
||||
# Ищем ближайшую гласную до или после start_idx
|
||||
for i in vow_indices:
|
||||
@ -98,48 +115,87 @@ class Hyphenator:
|
||||
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
|
||||
# или рекомендации по набору текста), перенос не должен разрывать слово так, чтобы мягкий или
|
||||
# твердый знак оказывался в начале или конце строки
|
||||
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(word_to_split: str) -> str:
|
||||
if len(word_to_split) <= self.max_unhyphenated_len: # Если длина укладывается в лимит, перенос не нужен
|
||||
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(word_to_split, len(word_to_split) // 2) # Ищем точку переноса около середины
|
||||
|
||||
if hyphen_idx == -1: # Если не нашли точку переноса
|
||||
# Ищем точку переноса около середины
|
||||
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(left_part) + self._split_code + split_word(right_part)
|
||||
return split_word_ru(left_part) + self._split_code + split_word_ru(right_part)
|
||||
|
||||
# Основная логика
|
||||
if len(word) <= self.max_unhyphenated_len or not any(self._is_vow(c) for c in word):
|
||||
# Короткое слово или без гласных "делению не подлежит", выходим из рекурсии
|
||||
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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
13
main.py
13
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! Это <i>тестовый текст для проверки расстановки</i> переносов в словах. Миллион 100-метровошеих жирножирафов.")
|
||||
result = typo.process(text="Привет, frozenseter! Это <i>тестовый текст для проверки расстановки</i> переносов"
|
||||
" в словах. Миллион 100-метровошеих жирножирафов.")
|
||||
print(result, "\n\n")
|
||||
|
||||
txt = ("Каждое пальто, которое мы создаём — это не просто одежда. Это"
|
||||
|
Loading…
x
Reference in New Issue
Block a user