mod: разделение функциональности по файлам
This commit is contained in:
parent
c4262bb1d1
commit
bc20296729
@ -10,5 +10,5 @@ Typography - библиотека для экранной типографики
|
|||||||
"""
|
"""
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
import regex
|
from etpgrf.typograph import Typographer
|
||||||
from etpgrf import processor, hyphenation
|
from etpgrf.hyphenation import Hyphenator
|
65
etpgrf/comutil.py
Normal file
65
etpgrf/comutil.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
from etpgrf.config import DEFAULT_LANGS, SUPPORTED_LANGS
|
||||||
|
import os
|
||||||
|
import regex
|
||||||
|
# Общие функции для типографа etpgrf
|
||||||
|
|
||||||
|
def parse_and_validate_langs(
|
||||||
|
langs_input: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
|
||||||
|
) -> frozenset[str]:
|
||||||
|
"""
|
||||||
|
Обрабатывает и валидирует входной параметр языков.
|
||||||
|
Если langs_input не предоставлен (None), используются языки по умолчанию
|
||||||
|
(сначала из переменной окружения ETPGRF_DEFAULT_LANGS, затем внутренний дефолт).
|
||||||
|
|
||||||
|
:param langs_input: Язык(и) для обработки. Может быть строкой (например, "ru+en"),
|
||||||
|
списком, кортежем или frozenset.
|
||||||
|
:return: Frozenset валидированных кодов языков в нижнем регистре.
|
||||||
|
:raises TypeError: Если langs_input имеет неожиданный тип.
|
||||||
|
:raises ValueError: Если langs_input пуст после обработки или содержит неподдерживаемые коды.
|
||||||
|
"""
|
||||||
|
_langs_input = langs_input
|
||||||
|
|
||||||
|
if _langs_input is None:
|
||||||
|
# Если langs_input не предоставлен явно, будем выкручиваться и искать в разных местах
|
||||||
|
# 1. Попытка получить языки из переменной окружения системы
|
||||||
|
env_default_langs = os.environ.get('ETPGRF_DEFAULT_LANGS')
|
||||||
|
if env_default_langs:
|
||||||
|
# Нашли язык для библиотеки в переменных окружения
|
||||||
|
_langs_input = env_default_langs
|
||||||
|
# print(f"Using ETPGRF_DEFAULT_LANGS from environment: {env_default_langs}") # Для отладки
|
||||||
|
else:
|
||||||
|
# Если в переменной окружения нет, используем то что есть в конфиге `etpgrf/config.py`
|
||||||
|
_langs_input = DEFAULT_LANGS
|
||||||
|
# print(f"Using library internal default langs: {DEFAULT_LANGS}") # Для отладки
|
||||||
|
|
||||||
|
if isinstance(_langs_input, str):
|
||||||
|
# Разделяем строку по любым небуквенным символам, приводим к нижнему регистру
|
||||||
|
# и фильтруем пустые строки
|
||||||
|
parsed_lang_codes_list = [lang.lower() for lang in regex.split(r'[^a-zA-Z]+', _langs_input) if lang]
|
||||||
|
elif isinstance(_langs_input, (list, tuple, frozenset)): # frozenset тоже итерируемый
|
||||||
|
# Приводим к строке, нижнему регистру и проверяем, что строка не пустая
|
||||||
|
parsed_lang_codes_list = [str(lang).lower() for lang in _langs_input if str(lang).strip()]
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
f"etpgrf: параметр 'langs' должен быть строкой, списком, кортежем или frozenset. Получен: {type(_langs_input)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not parsed_lang_codes_list:
|
||||||
|
raise ValueError(
|
||||||
|
"etpgrf: параметр 'langs' не может быть пустым или приводить к пустому списку языков после обработки."
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_langs_set = set()
|
||||||
|
for code in parsed_lang_codes_list:
|
||||||
|
if code not in SUPPORTED_LANGS:
|
||||||
|
raise ValueError(
|
||||||
|
f"etpgrf: код языка '{code}' не поддерживается. Поддерживаемые языки: {list(SUPPORTED_LANGS)}"
|
||||||
|
)
|
||||||
|
validated_langs_set.add(code)
|
||||||
|
|
||||||
|
# Эта проверка на случай, если parsed_lang_codes_list был не пуст, но все коды оказались невалидными
|
||||||
|
# (хотя предыдущее исключение должно было сработать раньше для каждого невалидного кода).
|
||||||
|
if not validated_langs_set:
|
||||||
|
raise ValueError("etpgrf: не предоставлено ни одного валидного кода языка.")
|
||||||
|
|
||||||
|
return frozenset(validated_langs_set)
|
12
etpgrf/config.py
Normal file
12
etpgrf/config.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# etpgrf/conf.py
|
||||||
|
# Настройки по умолчанию для типографа etpgrf
|
||||||
|
|
||||||
|
UTF = frozenset(['utf-8', 'utf-16', 'utf-32'])
|
||||||
|
MNEMO_CODE = frozenset(['mnemo', '&'])
|
||||||
|
SUPPORTED_LANGS = frozenset(['ru', 'en'])
|
||||||
|
|
||||||
|
|
||||||
|
# Язык(и) по умолчанию, если не указаны пользователем и не заданы через ETPGRF_DEFAULT_LANGS_MODULE
|
||||||
|
DEFAULT_LANGS = 'ru'
|
||||||
|
#
|
||||||
|
DEFAULT_CODE = 'utf-8'
|
@ -1,9 +1,7 @@
|
|||||||
|
from os.path import exists
|
||||||
|
|
||||||
import regex
|
import regex
|
||||||
|
from etpgrf.comutil import parse_and_validate_langs
|
||||||
UTF = frozenset(['utf-8', 'utf-16', 'utf-32'])
|
|
||||||
MNEMO_CODE = frozenset(['mnemo', '&'])
|
|
||||||
|
|
||||||
LANGS = frozenset(['ru', 'en'])
|
|
||||||
|
|
||||||
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
|
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
|
||||||
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ'])
|
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ'])
|
||||||
@ -14,14 +12,14 @@ _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'])
|
_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'])
|
||||||
|
|
||||||
|
|
||||||
class HyphenationRule:
|
class Hyphenator:
|
||||||
"""Правила расстановки переносов для разных языков.
|
"""Правила расстановки переносов для разных языков.
|
||||||
"""
|
"""
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
langs: frozenset[str], # Языки, которые обрабатываем в переносе слов
|
langs: frozenset[str], # Языки, которые обрабатываем в переносе слов
|
||||||
max_unhyphenated_len: int = 14, # Максимальная длина непереносимой группы
|
max_unhyphenated_len: int = 14, # Максимальная длина непереносимой группы
|
||||||
min_chars_per_part: int = 3): # Минимальная длина после переноса (хвост, который разрешено переносить)
|
min_chars_per_part: int = 3): # Минимальная длина после переноса (хвост, который разрешено переносить)
|
||||||
self.langs = langs
|
self.langs: frozenset[str] = parse_and_validate_langs(langs)
|
||||||
self.max_unhyphenated_len = max_unhyphenated_len
|
self.max_unhyphenated_len = max_unhyphenated_len
|
||||||
self.min_chars_per_part = min_chars_per_part
|
self.min_chars_per_part = min_chars_per_part
|
||||||
|
|
||||||
@ -30,10 +28,12 @@ class HyphenationRule:
|
|||||||
self._consonants: frozenset = frozenset()
|
self._consonants: frozenset = frozenset()
|
||||||
self._j_sound_upper: frozenset = frozenset()
|
self._j_sound_upper: frozenset = frozenset()
|
||||||
self._signs_upper: frozenset = frozenset()
|
self._signs_upper: frozenset = frozenset()
|
||||||
|
|
||||||
self._load_language_resources_for_hyphenation() # Загружает наборы символов на основе self.langs
|
self._load_language_resources_for_hyphenation() # Загружает наборы символов на основе self.langs
|
||||||
|
|
||||||
self._split_memo: dict[str, str] = {} # Кеш для этого экземпляра
|
self._split_memo: dict[str, str] = {} # Кеш для этого экземпляра
|
||||||
|
|
||||||
|
|
||||||
def _load_language_resources_for_hyphenation(self):
|
def _load_language_resources_for_hyphenation(self):
|
||||||
# Определяем наборы гласных, согласных и т.д. в зависимости языков.
|
# Определяем наборы гласных, согласных и т.д. в зависимости языков.
|
||||||
if "ru" in self.langs:
|
if "ru" in self.langs:
|
||||||
@ -71,7 +71,7 @@ class HyphenationRule:
|
|||||||
return char.upper() in self._signs_upper
|
return char.upper() in self._signs_upper
|
||||||
|
|
||||||
|
|
||||||
def hyphenation_in_word(self, word: str) -> str:
|
def hyp_in_word(self, word: str) -> str:
|
||||||
""" Расстановка переносов в русском слове с учетом максимальной длины непереносимой группы.
|
""" Расстановка переносов в русском слове с учетом максимальной длины непереносимой группы.
|
||||||
Переносы ставятся половинным делением слова, рекурсивно.
|
Переносы ставятся половинным делением слова, рекурсивно.
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ class HyphenationRule:
|
|||||||
return split_word(word) # Рекурсивно делим слово на части с переносами
|
return split_word(word) # Рекурсивно делим слово на части с переносами
|
||||||
|
|
||||||
|
|
||||||
def apply(self, text: str) -> str:
|
def hyp_in_text(self, text: str) -> str:
|
||||||
""" Расстановка переносов в тексте
|
""" Расстановка переносов в тексте
|
||||||
|
|
||||||
:param text: Строка, которую надо обработать (главный аргумент).
|
:param text: Строка, которую надо обработать (главный аргумент).
|
||||||
@ -137,68 +137,8 @@ class HyphenationRule:
|
|||||||
rus_worlds = regex.findall(r'\b[а-яА-Я]+\b', text) # ищем все русскоязычные слова в тексте
|
rus_worlds = regex.findall(r'\b[а-яА-Я]+\b', text) # ищем все русскоязычные слова в тексте
|
||||||
for word in rus_worlds:
|
for word in rus_worlds:
|
||||||
if len(word) > self.max_unhyphenated_len:
|
if len(word) > self.max_unhyphenated_len:
|
||||||
hyphenated_word = self.hyphenation_in_word(word)
|
hyphenated_word = self.hyp_in_word(word)
|
||||||
print(f'{word} -> {hyphenated_word}')
|
print(f'{word} -> {hyphenated_word}')
|
||||||
text = text.replace(word, hyphenated_word)
|
text = text.replace(word, hyphenated_word)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
# --- Основной класс Typographer ---
|
|
||||||
class Typographer:
|
|
||||||
def __init__(self,
|
|
||||||
langs: str | list[str] | tuple[str, ...] | frozenset[str] = 'ru',
|
|
||||||
code_out: str = 'mnemo',
|
|
||||||
hyphenation_rule: HyphenationRule | None = None, # Перенос слов и параметры расстановки переносов
|
|
||||||
# glue_prepositions_rule: GluePrepositionsRule | None = None, # Для других правил
|
|
||||||
# ... другие модули правил ...
|
|
||||||
):
|
|
||||||
|
|
||||||
# --- Обработка и валидация параметра langs ---
|
|
||||||
# Параметр langs может быть строкой, списком или кортежем ("ru+en", ["ru", "en"] или ("ru", "en"))
|
|
||||||
# Для удобств в классе буду использовать только frozenset, чтобы не дублировать коды языков
|
|
||||||
self.langs = set()
|
|
||||||
if isinstance(langs, str):
|
|
||||||
# Разделяем строку по любым небуквенным символам, приводим к нижнему регистру
|
|
||||||
# и фильтруем пустые строки, которые могут появиться, если разделители идут подряд
|
|
||||||
lang_codes = [lang.lower() for lang in regex.split(r'[^a-zA-Z]+', langs) if lang]
|
|
||||||
elif isinstance(langs, (list, tuple)):
|
|
||||||
lang_codes = [str(lang).lower() for lang in langs] # Приводим к строке и нижнему регистру
|
|
||||||
else:
|
|
||||||
raise TypeError(f"etpgrf: 'langs' parameter must be a string, list, or tuple. Got {type(langs)}")
|
|
||||||
if not lang_codes:
|
|
||||||
raise ValueError("etpgrf: 'langs' parameter cannot be empty or result in an empty list of languages after parsing.")
|
|
||||||
for code in lang_codes:
|
|
||||||
if code not in LANGS: # LANGS = frozenset(['ru', 'en'])
|
|
||||||
raise ValueError(f"etpgrf: langs code '{code}' is not supported. Supported languages: {list(LANGS)}")
|
|
||||||
self.langs.add(code)
|
|
||||||
self.langs: frozenset[str] = frozenset(self.langs)
|
|
||||||
|
|
||||||
# --- Обработка и валидация параметра code_out ---
|
|
||||||
if code_out not in MNEMO_CODE | UTF:
|
|
||||||
raise ValueError(f"etpgrf: code_out '{code_out}' is not supported. Supported codes: {MNEMO_CODE | UTF}")
|
|
||||||
|
|
||||||
# Сохраняем переданные модули правил
|
|
||||||
self.hyphenation_rule = hyphenation_rule
|
|
||||||
|
|
||||||
# TODO: вынести все соответствия UTF ⇄ MNEMO_CODE в отдельный класс
|
|
||||||
# self.hyphen_char = "" if code_out in UTF else "­" # Мягкий перенос по умолчанию
|
|
||||||
|
|
||||||
# Конвейер для обработки текста
|
|
||||||
def process(self, text: str) -> str:
|
|
||||||
processed_text = text
|
|
||||||
if self.hyphenation_rule:
|
|
||||||
# Передаем активные языки и символ переноса, если модуль HyphenationRule
|
|
||||||
# не получает их в своем __init__ напрямую от пользователя,
|
|
||||||
# а конструируется с настройками по умолчанию, а потом конфигурируется.
|
|
||||||
# В нашем примере HyphenationRule уже получает их в __init__.
|
|
||||||
processed_text = self.hyphenation_rule.apply(processed_text)
|
|
||||||
|
|
||||||
# if self.glue_prepositions_rule:
|
|
||||||
# processed_text = self.glue_prepositions_rule.apply(processed_text, non_breaking_space_char=self._get_nbsp())
|
|
||||||
|
|
||||||
# ... вызовы других активных модулей правил ...
|
|
||||||
return processed_text
|
|
||||||
|
|
||||||
# def _get_nbsp(self): # Пример получения неразрывного пробела
|
|
||||||
# return "\u00A0" if self.code_out in UTF else " "
|
|
||||||
|
|
||||||
|
17
main.py
17
main.py
@ -1,19 +1,28 @@
|
|||||||
import etpgrf
|
import etpgrf
|
||||||
from etpgrf.hyphenation import HyphenationRule, Typographer
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# --- Пример использования ---
|
# --- Пример использования ---
|
||||||
|
ETPGRF_DEFAULT_LANGS = "ru"
|
||||||
print("\n--- Пример использования класса---\n")
|
print("\n--- Пример использования класса---\n")
|
||||||
# Определяем пользовательские правила переносов
|
# Определяем пользовательские правила переносов
|
||||||
hyphen_settings = HyphenationRule(langs=frozenset(['ru']), max_unhyphenated_len=8)
|
hyphen_settings = etpgrf.Hyphenator(langs=frozenset(['ru']), max_unhyphenated_len=8)
|
||||||
# Определяем пользовательские правила типографа
|
# Определяем пользовательские правила типографа
|
||||||
typo = Typographer(langs='ru', code_out='utf-8', hyphenation_rule=hyphen_settings)
|
typo = etpgrf.Typographer(langs='ru', code_out='utf-8', hyphenation_rule=hyphen_settings)
|
||||||
|
|
||||||
result = hyphen_settings.apply(text="Бармалейщина")
|
result = hyphen_settings.hyp_in_text("Бармалейщина")
|
||||||
|
print(result, "\n\n")
|
||||||
|
result = hyphen_settings.hyp_in_word("Длинношеевый жираф")
|
||||||
print(result, "\n\n")
|
print(result, "\n\n")
|
||||||
result = typo.process(text="Какой-то длинный текст для проверки переносов. Перпердикюляция!")
|
result = typo.process(text="Какой-то длинный текст для проверки переносов. Перпердикюляция!")
|
||||||
print(result, "\n\n")
|
print(result, "\n\n")
|
||||||
result = typo.process(text="Привет, World! Это <i>тестовый текст для проверки расстановки</i> переносов в словах. Миллион 100-метровошеих жирножирафов.")
|
result = typo.process(text="Привет, World! Это <i>тестовый текст для проверки расстановки</i> переносов в словах. Миллион 100-метровошеих жирножирафов.")
|
||||||
print(result, "\n\n")
|
print(result, "\n\n")
|
||||||
|
|
||||||
|
txt = ("Каждое пальто, которое мы создаём — это не просто одежда. Это"
|
||||||
|
" вещь, в которой должно быть удобно жить: ходить, ждать, ехать, молчать и — главное —"
|
||||||
|
" чувствовать себя собой. <b>Мы не шьём одина­ковые пальто. Мы шьём ваше. </b> Ниже —"
|
||||||
|
" как устроен процесс заказа.</p>")
|
||||||
|
|
||||||
|
result = typo.process(text=txt)
|
||||||
|
print(result, "\n\n")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user