diff --git a/etpgrf/__init__.py b/etpgrf/__init__.py index 346cb13..895603e 100644 --- a/etpgrf/__init__.py +++ b/etpgrf/__init__.py @@ -10,5 +10,5 @@ Typography - библиотека для экранной типографики """ __version__ = "0.1.0" -import regex -from etpgrf import processor, hyphenation \ No newline at end of file +from etpgrf.typograph import Typographer +from etpgrf.hyphenation import Hyphenator \ No newline at end of file diff --git a/etpgrf/comutil.py b/etpgrf/comutil.py new file mode 100644 index 0000000..6ef4967 --- /dev/null +++ b/etpgrf/comutil.py @@ -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) \ No newline at end of file diff --git a/etpgrf/config.py b/etpgrf/config.py new file mode 100644 index 0000000..6ca6332 --- /dev/null +++ b/etpgrf/config.py @@ -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' diff --git a/etpgrf/hyphenation.py b/etpgrf/hyphenation.py index ed5d67c..df85452 100755 --- a/etpgrf/hyphenation.py +++ b/etpgrf/hyphenation.py @@ -1,9 +1,7 @@ +from os.path import exists + import regex - -UTF = frozenset(['utf-8', 'utf-16', 'utf-32']) -MNEMO_CODE = frozenset(['mnemo', '&']) - -LANGS = frozenset(['ru', 'en']) +from etpgrf.comutil import parse_and_validate_langs _RU_VOWELS_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']) -class HyphenationRule: +class Hyphenator: """Правила расстановки переносов для разных языков. """ def __init__(self, langs: frozenset[str], # Языки, которые обрабатываем в переносе слов max_unhyphenated_len: int = 14, # Максимальная длина непереносимой группы 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.min_chars_per_part = min_chars_per_part @@ -30,10 +28,12 @@ class HyphenationRule: self._consonants: frozenset = frozenset() self._j_sound_upper: frozenset = frozenset() self._signs_upper: frozenset = frozenset() + self._load_language_resources_for_hyphenation() # Загружает наборы символов на основе self.langs self._split_memo: dict[str, str] = {} # Кеш для этого экземпляра + def _load_language_resources_for_hyphenation(self): # Определяем наборы гласных, согласных и т.д. в зависимости языков. if "ru" in self.langs: @@ -71,7 +71,7 @@ class HyphenationRule: 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) # Рекурсивно делим слово на части с переносами - def apply(self, text: str) -> str: + def hyp_in_text(self, text: str) -> str: """ Расстановка переносов в тексте :param text: Строка, которую надо обработать (главный аргумент). @@ -137,68 +137,8 @@ class HyphenationRule: rus_worlds = regex.findall(r'\b[а-яА-Я]+\b', text) # ищем все русскоязычные слова в тексте for word in rus_worlds: 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}') text = text.replace(word, hyphenated_word) 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 " " - diff --git a/etpgrf/processor.py b/etpgrf/processor.py deleted file mode 100644 index e69de29..0000000 diff --git a/main.py b/main.py index 52d14cd..c8e57db 100644 --- a/main.py +++ b/main.py @@ -1,19 +1,28 @@ import etpgrf -from etpgrf.hyphenation import HyphenationRule, Typographer if __name__ == '__main__': # --- Пример использования --- + ETPGRF_DEFAULT_LANGS = "ru" 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") result = typo.process(text="Какой-то длинный текст для проверки переносов. Перпердикюляция!") print(result, "\n\n") result = typo.process(text="Привет, World! Это тестовый текст для проверки расстановки переносов в словах. Миллион 100-метровошеих жирножирафов.") print(result, "\n\n") + txt = ("Каждое пальто, которое мы создаём — это не просто одежда. Это" + " вещь, в которой должно быть удобно жить: ходить, ждать, ехать, молчать и — главное —" + " чувствовать себя собой. Мы не шьём одина­ковые пальто. Мы шьём ваше. Ниже —" + " как устроен процесс заказа.

") + + result = typo.process(text=txt) + print(result, "\n\n")