add: проверки на диграммы/триграммы,квадрограммы в английских словах
This commit is contained in:
22
README.md
22
README.md
@@ -7,3 +7,25 @@
|
|||||||
Экранная типографика для веба -- способствует повышению читабельности текста в интернете,
|
Экранная типографика для веба -- способствует повышению читабельности текста в интернете,
|
||||||
приближая его к печатной типографике.
|
приближая его к печатной типографике.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Переносы слов
|
||||||
|
|
||||||
|
В основе переносов слов лежит фонетический принцип — деление по слогам. И этот принцип типограф решает.
|
||||||
|
|
||||||
|
Однако регение не идеально, т.к. на эту основу накладывается множество других факторов:
|
||||||
|
|
||||||
|
1. Морфологический принцип -- переносы стараются не разрывать значащие части слова (морфемы: приставки, корни, суффиксы, окончания).
|
||||||
|
2. Устоявшиеся практики (традиция) -- Для многих слов существуют традиционные, общепринятые варианты переноса, которые могут не всегда строго следовать фонетическим или морфологическим правилам. Эти традиции формировались десятилетиями и закреплены в словарях и справочниках по типографике.
|
||||||
|
3. Удобочитаемость и эстетика -- иногда правила переноса корректируются для улучшения внешнего вида текста и легкости чтения. Например, стараются не оставлять или переносить слишком короткие части слова (в типографе-etpgrf, это решается с помощью настроек по умолчанию:
|
||||||
|
* `defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN` -- длинна части слова не подлежащего переносам;
|
||||||
|
* `defaults.etpgrf_settings.hyphenation.MIN_TAIL_LEN` -- длинна части слова которое недопустимо переносить или оставлять на строке ("хвост", "сироты").
|
||||||
|
4. Избегают переносов, создающих неблагозвучные или двусмысленные сочетания букв на стыке строк.
|
||||||
|
5. А еще, любые правила, особенно в языке, имеют исключения и существуют слова, которые переносятся вопреки общим закономерностям.
|
||||||
|
|
||||||
|
Etpgrf -- работает только на эвристических правилах, основанных на буквенных паттернах. Этого недостаточно, особенно для английского языка. Возможные пути улучшения (TODO):
|
||||||
|
* Использование словарей переносов (hyphenation dictionaries): Это наиболее точный подход. Существуют готовые словари (часто в формате, используемом TeX), где для каждого слова указаны возможные точки переноса. Интеграция такого словаря — сложная, но самая надежная задача.
|
||||||
|
* Алгоритмы на основе паттернов (например, алгоритм Франклина Ляна, используемый в TeX): Эти алгоритмы не хранят все слова, а используют набор паттернов и их "весов" для определения оптимальных точек переноса. Они очень эффективны и дают высокое качество, но их реализация или адаптация — нетривиальная задача.
|
||||||
|
* Гибридный подход: Комбинация эвристических правил для простых случаев и обращение к словарю или более сложному алгоритму для неоднозначных или длинных слов.
|
||||||
|
* Расширение набора эвристических правил: Можно продолжать добавлять более специфичные правила (например, для диграфов, триграфов, более сложных приставок и суффиксов), но это путь с убывающей отдачей — правил становится много, они могут конфликтовать, а покрытие всех случаев все равно не гарантировано.
|
||||||
|
* Учет морфологии: Более глубокий анализ морфемной структуры слова.
|
@@ -3,6 +3,10 @@ from etpgrf.config import MODE_UNICODE, MODE_MNEMONIC, MODE_MIXED, SUPPORTED_LAN
|
|||||||
from etpgrf.defaults import etpgrf_settings
|
from etpgrf.defaults import etpgrf_settings
|
||||||
import os
|
import os
|
||||||
import regex
|
import regex
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# --- Настройки логирования ---
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def parse_and_validate_mode(
|
def parse_and_validate_mode(
|
||||||
@@ -89,4 +93,48 @@ def parse_and_validate_langs(
|
|||||||
if not validated_langs_set:
|
if not validated_langs_set:
|
||||||
raise ValueError("etpgrf: не предоставлено ни одного валидного кода языка.")
|
raise ValueError("etpgrf: не предоставлено ни одного валидного кода языка.")
|
||||||
|
|
||||||
return frozenset(validated_langs_set)
|
return frozenset(validated_langs_set)
|
||||||
|
|
||||||
|
|
||||||
|
def is_inside_unbreakable_segment(
|
||||||
|
word_segment: str,
|
||||||
|
split_index: int,
|
||||||
|
unbreakable_set: frozenset[str] | list[str] | set[str],
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, находится ли позиция разбиения внутри неразрывного сегмента.
|
||||||
|
|
||||||
|
:param word_segment: -- Сегмент слова, в котором мы ищем позицию разбиения.
|
||||||
|
:param split_index: -- Индекс (позиция внутри сегмента), по которому мы хотим проверить разбиение.
|
||||||
|
:param unbreakable_set: -- Набор неразрывных сегментов (например: диграфы, триграфы, акронимы...).
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
segment_len = len(word_segment)
|
||||||
|
# Проверяем, что позиция разбиения не выходит за границы сегмента
|
||||||
|
if not (0 < split_index < segment_len):
|
||||||
|
return False
|
||||||
|
# Пер образуем все в верхний регистр, чтобы сравнения строк работали
|
||||||
|
word_segment_upper = word_segment.upper()
|
||||||
|
# unbreakable_set_upper = (unit.upper() for unit in unbreakable_set) # <-- С помощью генератора
|
||||||
|
|
||||||
|
# Отсортируем unbreakable_set по длине лексем (чем короче, тем больше шансов на "ранний выход")
|
||||||
|
# и заодно превратим в list
|
||||||
|
sorted_units = sorted(unbreakable_set, key=len)
|
||||||
|
# sorted_units = sorted(unbreakable_set_upper, key=len)
|
||||||
|
for unbreakable in sorted_units:
|
||||||
|
unit_len = len(unbreakable)
|
||||||
|
if unit_len < 2:
|
||||||
|
continue
|
||||||
|
# Спорно, что преобразование в верхний регистр эффективнее делать тут, но благодаря возможному
|
||||||
|
# "раннему выходу" это может быть быстрее с помощью генератора (см. выше комментарии)
|
||||||
|
unbreakable_upper = unbreakable.upper()
|
||||||
|
for offset in range(1, unit_len):
|
||||||
|
position_start_in_segment = split_index - offset
|
||||||
|
position_end_in_segment = position_start_in_segment + unit_len
|
||||||
|
# Убедимся, что предполагаемое положение 'unit' не выходит за границы word_segment
|
||||||
|
if position_start_in_segment >= 0 and position_end_in_segment <= segment_len and \
|
||||||
|
word_segment_upper[position_start_in_segment:position_end_in_segment] == unbreakable_upper:
|
||||||
|
# Нашли 'unbreakable', и split_index находится внутри него.
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@@ -1,8 +1,14 @@
|
|||||||
|
# etpgrf/hyphenation.py
|
||||||
|
# Представленные здесь алгоритмы реализуют упрощенные правила. Но эти правила лучше, чем их полное отсутствие.
|
||||||
|
# Тем более что пользователь может отключить переносы из типографа.
|
||||||
|
# Для русского языка правила реализованы лучше. Для английского дают "разумные" переносы во многих случаях, но из-за
|
||||||
|
# большого числа беззвучных согласных и их сочетаний, могут давать не совсем корректный результат.
|
||||||
|
|
||||||
import regex
|
import regex
|
||||||
import logging
|
import logging
|
||||||
from etpgrf.config import LANG_RU, LANG_RU_OLD, LANG_EN, SHY_ENTITIES, MODE_UNICODE
|
from etpgrf.config import LANG_RU, LANG_RU_OLD, LANG_EN, SHY_ENTITIES, MODE_UNICODE
|
||||||
from etpgrf.defaults import etpgrf_settings
|
from etpgrf.defaults import etpgrf_settings
|
||||||
from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
|
from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs, is_inside_unbreakable_segment
|
||||||
|
|
||||||
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
|
_RU_VOWELS_UPPER = frozenset(['А', 'О', 'И', 'Е', 'Ё', 'Э', 'Ы', 'У', 'Ю', 'Я'])
|
||||||
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х',
|
_RU_CONSONANTS_UPPER = frozenset(['Б', 'В', 'Г', 'Д', 'Ж', 'З', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х',
|
||||||
@@ -25,6 +31,10 @@ _EN_SUFFIXES_WITHOUT_HYPHENATION_UPPER = frozenset([
|
|||||||
"SION", "TION", # decision, action (часто покрываются C-C или V-C-V)
|
"SION", "TION", # decision, action (часто покрываются C-C или V-C-V)
|
||||||
# "ING", "ED", "ER", "EST", "LY" # совсем короткие, но распространенные, не рассматриваем.
|
# "ING", "ED", "ER", "EST", "LY" # совсем короткие, но распространенные, не рассматриваем.
|
||||||
])
|
])
|
||||||
|
_EN_UNBREAKABLE_X_GRAPHS_UPPER = frozenset(["SH", "CH", "TH", "PH", "WH", "CK", "NG", "AW", # диграфы с согласными
|
||||||
|
"TCH", "DGE", "IGH", # триграфы
|
||||||
|
"EIGH", "OUGH"]) # квадрографы
|
||||||
|
|
||||||
|
|
||||||
# --- Настройки логирования ---
|
# --- Настройки логирования ---
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -143,16 +153,18 @@ class Hyphenator:
|
|||||||
if i >= start_idx - self.min_chars_per_part and i + self.min_chars_per_part < len(word_segment):
|
if i >= start_idx - self.min_chars_per_part and i + self.min_chars_per_part < len(word_segment):
|
||||||
# Проверяем, что после гласной есть минимум символов "хвоста"
|
# Проверяем, что после гласной есть минимум символов "хвоста"
|
||||||
ind = i + 1
|
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]):
|
# 1. Не отделяем "хвостов" с начала или конца (это некрасиво)
|
||||||
# Й -- полугласная. Перенос после неё только в случае, если дальше идет согласная
|
|
||||||
# (например, "бой-кий"), но запретить, если идет гласная (например, "ма-йка" не переносится).
|
|
||||||
ind += 1
|
|
||||||
if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part:
|
if ind <= self.min_chars_per_part or ind >= len(word_segment) - self.min_chars_per_part:
|
||||||
# Не отделяем 3 символ с начала или конца (это некрасиво)
|
|
||||||
continue
|
continue
|
||||||
|
# 2. Пропускаем мягкий/твердый знак, если перенос начинается или заканчивается
|
||||||
|
# на них (правило из ГОСТ 7.62-2008)
|
||||||
if self._is_sign(word_segment[ind]) or (ind > 0 and self._is_sign(word_segment[ind-1])):
|
if self._is_sign(word_segment[ind]) or (ind > 0 and self._is_sign(word_segment[ind-1])):
|
||||||
# Пропускаем мягкий/твердый знак, если перенос начинается или заканчивается на них (ГОСТ 7.62-2008)
|
|
||||||
continue
|
continue
|
||||||
|
# 3. Провека на `Й` (полугласная). Перенос после неё только в случае, если дальше идет
|
||||||
|
# согласная (например, "бой-кий"), но запретить, если идет гласная (например,
|
||||||
|
# "ма-йка" не переносится).
|
||||||
|
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
|
||||||
return ind
|
return ind
|
||||||
return -1 # Не нашли подходящую позицию
|
return -1 # Не нашли подходящую позицию
|
||||||
|
|
||||||
@@ -209,13 +221,26 @@ class Hyphenator:
|
|||||||
# Проверяем каждый потенциальный индекс переноса по упрощенным правилам
|
# Проверяем каждый потенциальный индекс переноса по упрощенным правилам
|
||||||
for i in valid_split_indices:
|
for i in valid_split_indices:
|
||||||
# Упрощенные правила английского переноса (основаны на частых паттернах, не на слогах):
|
# Упрощенные правила английского переноса (основаны на частых паттернах, не на слогах):
|
||||||
# 1. Перенос между двумя согласными (C-C), например, 'but-ter', 'subjec-tive'
|
# 1. Запрет переноса между гласными
|
||||||
|
if self._is_vow(word_segment[i - 1]) and self._is_vow(word_segment[i]):
|
||||||
|
logger.debug(
|
||||||
|
f"Skipping V-V split point at index {i} in '{word_segment}' ({word_segment[i - 1]}{word_segment[i]})")
|
||||||
|
continue # Переходим к следующему кандидату i
|
||||||
|
|
||||||
|
# 2. Запрет переноса ВНУТРИ неразрывных диграфов/триграфов и т.д.
|
||||||
|
if is_inside_unbreakable_segment(word_segment=word_segment,
|
||||||
|
split_index=i,
|
||||||
|
unbreakable_set=_EN_UNBREAKABLE_X_GRAPHS_UPPER):
|
||||||
|
logger.debug(f"Skipping unbreakable segment at index {i} in '{word_segment}'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3. Перенос между двумя согласными (C-C), например, 'but-ter', 'subjec-tive'
|
||||||
# Точка переноса - индекс i. Проверяем символы word[i-1] и word[i].
|
# Точка переноса - индекс i. Проверяем символы word[i-1] и word[i].
|
||||||
if self._is_cons(word_segment[i - 1]) and self._is_cons(word_segment[i]):
|
if self._is_cons(word_segment[i - 1]) and self._is_cons(word_segment[i]):
|
||||||
logger.debug(f"Found C-C split point at index {i} in '{word_segment}'")
|
logger.debug(f"Found C-C split point at index {i} in '{word_segment}'")
|
||||||
return i
|
return i
|
||||||
|
|
||||||
# 2. Перенос перед одиночной согласной между двумя гласными (V-C-V), например, 'ho-tel', 'ba-by'
|
# 4. Перенос перед одиночной согласной между двумя гласными (V-C-V), например, 'ho-tel', 'ba-by'
|
||||||
# Точка переноса - индекс i (перед согласной). Проверяем word[i-1], word[i], word[i+1].
|
# Точка переноса - индекс i (перед согласной). Проверяем word[i-1], word[i], word[i+1].
|
||||||
# Требуется как минимум 3 символа для этого паттерна.
|
# Требуется как минимум 3 символа для этого паттерна.
|
||||||
if i < word_len - 1 and \
|
if i < word_len - 1 and \
|
||||||
@@ -224,7 +249,7 @@ class Hyphenator:
|
|||||||
logger.debug(f"Found V-C-V (split before C) split point at index {i} in '{word_segment}'")
|
logger.debug(f"Found V-C-V (split before C) split point at index {i} in '{word_segment}'")
|
||||||
return i
|
return i
|
||||||
|
|
||||||
# 3. Перенос после одиночной согласной между двумя гласными (V-C-V), например, 'riv-er', 'fin-ish'
|
# 5. Перенос после одиночной согласной между двумя гласными (V-C-V), например, 'riv-er', 'fin-ish'
|
||||||
# Точка переноса - индекс i (после согласной). Проверяем word[i-2], word[i-1], word[i].
|
# Точка переноса - индекс i (после согласной). Проверяем word[i-2], word[i-1], word[i].
|
||||||
# Требуется как минимум 3 символа для этого паттерна.
|
# Требуется как минимум 3 символа для этого паттерна.
|
||||||
if i < word_len and \
|
if i < word_len and \
|
||||||
@@ -233,7 +258,7 @@ class Hyphenator:
|
|||||||
logger.debug(f"Found V-C-V (split after C) split point at index {i} in '{word_segment}'")
|
logger.debug(f"Found V-C-V (split after C) split point at index {i} in '{word_segment}'")
|
||||||
return i
|
return i
|
||||||
|
|
||||||
# 4. Правила для распространенных суффиксов (перенос ПЕРЕД суффиксом)
|
# 6. Правила для распространенных суффиксов (перенос ПЕРЕД суффиксом)
|
||||||
if word_segment[i:].upper() in _EN_SUFFIXES_WITHOUT_HYPHENATION_UPPER:
|
if word_segment[i:].upper() in _EN_SUFFIXES_WITHOUT_HYPHENATION_UPPER:
|
||||||
logger.debug(f"Found suffix '-{word_segment[i:]}' split point at index {i} in '{word_segment}'")
|
logger.debug(f"Found suffix '-{word_segment[i:]}' split point at index {i} in '{word_segment}'")
|
||||||
return i
|
return i
|
||||||
|
58
main.py
58
main.py
@@ -38,11 +38,7 @@ if __name__ == '__main__':
|
|||||||
" в словах. Миллион 100-метровошеих жирножирафов.")
|
" в словах. Миллион 100-метровошеих жирножирафов.")
|
||||||
print(result, "\n\n")
|
print(result, "\n\n")
|
||||||
|
|
||||||
# меняем настройки логирования
|
|
||||||
etpgrf.defaults.etpgrf_settings.logging_settings.LEVEL = logging.DEBUG
|
|
||||||
etpgrf.logger.update_etpgrf_log_level_from_settings() # Обновляем уровень логирования из настроек
|
|
||||||
etpgrf.defaults.etpgrf_settings.logging_settings.FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
||||||
etpgrf.logger.update_etpgrf_log_format_from_settings() # Обновляем формат логирования из настроек
|
|
||||||
|
|
||||||
# Меняем настройки по умолчанию для переносов
|
# Меняем настройки по умолчанию для переносов
|
||||||
etpgrf.defaults.etpgrf_settings.LANGS = "ru"
|
etpgrf.defaults.etpgrf_settings.LANGS = "ru"
|
||||||
@@ -63,19 +59,49 @@ if __name__ == '__main__':
|
|||||||
print(result, "\n-----\n\n-----")
|
print(result, "\n-----\n\n-----")
|
||||||
|
|
||||||
# Проверяем переносы в смешанном тексте (русский + английский)
|
# Проверяем переносы в смешанном тексте (русский + английский)
|
||||||
typo_en = etpgrf.Typographer(langs='en', mode='mixed', hyphenation=True)
|
|
||||||
etpgrf.defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN = 6
|
etpgrf.defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN = 6
|
||||||
txt = ("As the sun set—casting long shadows across the meadow—a remarkably sophisticated individual pondered"
|
typo_en = etpgrf.Typographer(langs='en', mode='mixed', hyphenation=True)
|
||||||
" life’s complexities. \"Is it possible,\" they wondered aloud, \"that such an inconsequential event"
|
txt = ("It was a chilly autumn afternoon when Anna finally received her custom-made KATEBLASH coat."
|
||||||
" could hold deeper meaning?\n"
|
" “I can’t believe how perfectly it fits!” she exclaimed, wrapping the soft, woolen fabric tightly"
|
||||||
|
" around her shoulders.\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Their notebook (a leather-bound relic from 1923) contained hastily scribbled observations:"
|
"The coat - designed with unique check patterns and a detachable hood - was more than just a garment."
|
||||||
" \"Interdisciplinary collaboration requires mutual understanding—not just technical expertise.\""
|
" It was a statement of style and comfort, crafted with care and precision. Anna remembered the"
|
||||||
" Nearby, an unfinished cup of coffee sat atop a stack of papers titled \"The Phenomenology of"
|
" fitting session vividly: “The tailor said, ‘This coat will keep you style through even the coldest"
|
||||||
" Subjective Experience in Modern Literature.\n"
|
" things winter throws at you.’”\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Suddenly, a deafening noise—like thunder, yet mechanical—echoed in the distance."
|
"Her friend Mark raised an eyebrow: “Only you would get a coat with such an elaborate"
|
||||||
" \"What on Earth…?\" they muttered, peering through binoculars. Was it an airplane?"
|
" design - and those fancy oughtstanding stitches! Sounds like your coat has more personality"
|
||||||
" A construction vehicle? Or something entirely different?")
|
" than some people I know!”\n"
|
||||||
|
"\n"
|
||||||
|
"As they walked down the street, Anna noticed how the coat’s tailored cut moved gracefully with her."
|
||||||
|
" The consideration of every detail - from the choice of fabric to the delicate embroidery - made it"
|
||||||
|
" clear that this was no ordinary coat.\n"
|
||||||
|
"\n"
|
||||||
|
"Later, over coffee, Anna joked, “I told the tailor, ‘Make it so I never want to take it off.’ "
|
||||||
|
"Looks like they succeeded!\n"
|
||||||
|
"\n"
|
||||||
|
"Mark nodded, “Well, with KATEBLASH, it’s not just about fashion - it’s about craftsmanship, comfort,"
|
||||||
|
" and a little bit of magic.”")
|
||||||
result = typo_en.process(text=txt)
|
result = typo_en.process(text=txt)
|
||||||
print(result, "\n\n")
|
print(result, "\n\n")
|
||||||
|
|
||||||
|
# Спасибо. Для английского текста, для проверки типографа, мне не хватает неразрывных диграфов-квадрографов -- sh, ch, th, ph, wh, ck, ng, aw, tch, dge, igh, eigh, ough и неразрывных суффиксов -- ation, ition, ution, osity, able, ible, ment, ness, less, ship, hood, tive, sion, tion в длинный словах (8 символов и более). и пусть тескт тоже будет про пальто KATEBLASH. Справишься??
|
||||||
|
|
||||||
|
# меняем настройки логирования
|
||||||
|
etpgrf.defaults.etpgrf_settings.logging_settings.LEVEL = logging.DEBUG
|
||||||
|
etpgrf.logger.update_etpgrf_log_level_from_settings() # Обновляем уровень логирования из настроек
|
||||||
|
# etpgrf.defaults.etpgrf_settings.logging_settings.FORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
# etpgrf.logger.update_etpgrf_log_format_from_settings() # Обновляем формат логирования из настроек
|
||||||
|
# Определяем пользовательские правила переносов
|
||||||
|
hyphen_settings = etpgrf.Hyphenator(langs='en', max_unhyphenated_len=6)
|
||||||
|
|
||||||
|
# Проверяем переносы в словах
|
||||||
|
result = hyphen_settings.hyp_in_text("oughtstanding")
|
||||||
|
print(result, "==\n\n")
|
||||||
|
result = hyphen_settings.hyp_in_text("blacksmithing")
|
||||||
|
print(result, "==\n\n")
|
||||||
|
result = hyphen_settings.hyp_in_text("dccadckpoooughremawgreen")
|
||||||
|
print(result, "==\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user