diff --git a/etpgrf/config.py b/etpgrf/config.py index 01d21f8..3f15098 100644 --- a/etpgrf/config.py +++ b/etpgrf/config.py @@ -30,7 +30,7 @@ SHY_ENTITIES = { SPACE_ENTITIES = { 'NBSP': ('\u00A0', ' '), # Неразрывный пробел 'THINSP': ('\u2009', ' '), # Тонкий пробел - 'ENSP': ('\u2002', ' '), # Полуширокий пробел + 'ENSP': ('\u2002', ' '), # Полу-широкий пробел 'EMSP': ('\u2003', ' '), # Широкий пробел 'ZWNJ': ('\u200C', '‌'), # Разрывный пробел нулевой ширины (без пробела) 'ZWJ': ('\u200D', '‍'), # Неразрывный пробел нулевой ширины @@ -38,9 +38,10 @@ SPACE_ENTITIES = { # Тире и дефисы DASH_ENTITIES = { - 'NDASH': ('\u2013', '–'), # Короткое тире - 'MDASH': ('\u2014', '—'), # Длинное тире - # 'HYPHEN': ('\u2010', '‐'), # Обычный дефис (если нужно отличать от минуса) + 'NDASH': ('\u2013', '–'), # Cреднее тире (En dash) + 'MDASH': ('\u2014', '—'), # Длинное тире + 'HYPHEN': ('\u2010', '‐'), # Обычный дефис (если нужно отличать от минуса) + 'HORBAR': ('\u2015', '―'), # Горизонтальная линия (длинная черта) } # Кавычки @@ -57,7 +58,16 @@ QUOTE_ENTITIES = { 'SBQUO': ('\u201A', '‚'), # Нижняя одинарная кавычка -- ‚ 'LSAQUO': ('\u2039', '‹'), # Открывающая французская угловая кавычка -- › 'RSAQUO': ('\u203A', '›'), # Закрывающая французская угловая кавычка -- ‹ +} +CURRENCY_ENTITIES = { + 'DOLLAR': ('\u0024', '$'), # Доллар + 'CENT': ('\u00A2', '¢'), # Цент + 'POUND': ('\u00A3', '£'), # Фунт стерлингов + 'CURREN': ('\u00A4', '¤'), # Знак валюты (обычно используется для обозначения "без конкретной валюты") + 'YEN': ('\u00A5', '¥'), # Йена + 'EURO': ('\u20AC', '€'), # Евро + 'RUBLE': ('\u20BD', '₽'), # Российский рубль (₽) } # Другие символы (пример для расширения) diff --git a/etpgrf/typograph.py b/etpgrf/typograph.py index 0c01e19..b95bbd4 100644 --- a/etpgrf/typograph.py +++ b/etpgrf/typograph.py @@ -1,7 +1,12 @@ +import logging +try: + from bs4 import BeautifulSoup, NavigableString +except ImportError: + BeautifulSoup = None from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs from etpgrf.hyphenation import Hyphenator from etpgrf.unbreakables import Unbreakables -import logging + # --- Настройки логирования --- logger = logging.getLogger(__name__) @@ -12,6 +17,7 @@ class Typographer: def __init__(self, langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None, mode: str | None = None, + process_html: bool = False, # Флаг обработки HTML-тегов hyphenation: Hyphenator | bool | None = True, # Перенос слов и параметры расстановки переносов unbreakables: Unbreakables | bool | None = True, # Правила для предотвращения разрыва коротких слов # ... другие модули правил ... @@ -21,7 +27,14 @@ class Typographer: self.langs: frozenset[str] = parse_and_validate_langs(langs) # B. --- Обработка и валидация параметра mode --- self.mode: str = parse_and_validate_mode(mode) - # C. --- Инициализация правила переноса --- + # C. --- Настройка режима обработки HTML --- + self.process_html = process_html + if self.process_html and BeautifulSoup is None: + logger.warning("Параметр 'process_html=True', но библиотека BeautifulSoup не установлена. " + "HTML не будет обработан. Установите ее: `pip install beautifulsoup4`") + self.process_html = False + + # D. --- Инициализация правила переноса --- # Предпосылка: если вызвали типограф, значит, мы хотим обрабатывать текст и переносы тоже нужно расставлять. # А для специальных случаев, когда переносы не нужны, пусть не ленятся и делают `hyphenation=False`. self.hyphenation: Hyphenator | None = None @@ -31,13 +44,8 @@ class Typographer: elif isinstance(hyphenation, Hyphenator): # C2. Если hyphenation - это объект Hyphenator, то просто сохраняем его (и используем его langs и mode) self.hyphenation = hyphenation - elif hyphenation is False: - # C3. Если hyphenation - False, то правило переноса выключено. - self.hyphenation = None - else: - # D4. Если hyphenation что-то неведомое, то игнорируем его и правило переноса выключено - self.hyphenation = None - # D. --- Конфигурация правил неразрывных слов --- + + # E. --- Конфигурация правил неразрывных слов --- self.unbreakables: Unbreakables | None = None if unbreakables is True or unbreakables is None: # D1. Создаем новый объект Unbreakables с заданными языками и режимом, а все остальное по умолчанию @@ -45,37 +53,75 @@ class Typographer: elif isinstance(unbreakables, Unbreakables): # D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode) self.unbreakables = unbreakables - elif unbreakables is False: - # D3. Если unbreakables - False, то правило неразрывных слов выключено. - self.unbreakables = None - else: - # D4. Если unbreakables что-то неведомое, то игнорируем его и правило неразрывных слов выключено - self.unbreakables = None - # E. --- Конфигурация других правил--- + + # F. --- Конфигурация других правил--- # Z. --- Логирование инициализации --- logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, " f"hyphenation: {self.hyphenation is not None}, " - f"unbreakables: {self.unbreakables is not None}") + f"unbreakables: {self.unbreakables is not None}" + f"process_html: {self.process_html}") + + + def _process_text_node(self, text: str) -> str: + """ + Внутренний конвейер, который работает с чистым текстом. + """ + # Шаг 1: Декодируем весь входящий текст в канонический Unicode + # (здесь можно использовать html.unescape, но наш кодек тоже подойдет) + # processed_text = decode_to_unicode(text) + processed_text = text # ВРЕМЕННО: используем текст как есть + + # Шаг 2: Применяем правила к чистому Unicode-тексту + if self.unbreakables is not None: + processed_text = self.unbreakables.process(processed_text) + if self.hyphenation is not None: + processed_text = self.hyphenation.hyp_in_text(processed_text) + # ... вызовы других активных модулей правил ... + + # Шаг 3: Кодируем результат в запрошенный формат (mnemonic или mixed) + # final_text = encode_from_unicode(processed_text, self.mode) + final_text = processed_text # ВРЕМЕННО: используем текст как есть + return final_text + # Конвейер для обработки текста def process(self, text: str) -> str: - processed_text = text - # Применяем правила в определенном порядке. - # Неразрывные конструкции лучше применять до переносов. - if self.unbreakables is not None: - processed_text = self.unbreakables.process(processed_text) - if self.hyphenation is not None: - # Обработчик переносов (Hyphenator) активен. Обрабатываем текст... - processed_text = self.hyphenation.hyp_in_text(processed_text) + """ + Обрабатывает текст, применяя все активные правила типографики. + Поддерживает обработку текста внутри HTML-тегов. + """ + if not text: + return "" + # Если включена обработка HTML и BeautifulSoup доступен + if self.process_html: + # Мы передаем 'html.parser', он быстрый и встроенный. + soup = BeautifulSoup(markup=text, features='html.parser') + text_nodes = soup.find_all(string=True) + for node in text_nodes: + # Пропускаем пустые или состоящие из пробелов узлы и узлы внутри тегов, где не нужно обрабатывать текст + if not node.string.strip() or node.parent.name in ['style', 'script', 'pre', 'code']: + continue + # К каждому текстовому узлу применяем "внутренний" процессор + processed_node_text = self._process_text_node(node.string) + # Отладочная печать, чтобы видеть, что происходит + if node.string != processed_node_text: + logger.info(f"Processing node: '{node.string}' -> '{processed_node_text}'") + # Заменяем узел в дереве на обработанный текст. + # BeautifulSoup сама позаботится об экранировании, если нужно. + # Важно: мы не можем просто заменить строку, нужно создать новый объект NavigableString, + # чтобы BeautifulSoup правильно обработал символы вроде '<' и '>'. + # Однако, replace_with достаточно умен, чтобы справиться с этим. + node.replace_with(processed_node_text) - # if self.glue_prepositions_rule: - # processed_text = self.glue_prepositions_rule.hyp_in_text(processed_text, non_breaking_space_char=self._get_nbsp()) - - # ... вызовы других активных модулей правил ... - return processed_text + # Возвращаем измененный HTML. BeautifulSoup по умолчанию выводит без тегов + # если их не было в исходной строке. + return str(soup) + else: + # Если HTML-режим выключен, работаем как раньше + return self._process_text_node(text) # def _get_nbsp(self): # Пример получения неразрывного пробела # return "\u00A0" if self.mode in UTF else " " diff --git a/main.py b/main.py index c809bc5..5aebb84 100644 --- a/main.py +++ b/main.py @@ -60,8 +60,6 @@ if __name__ == '__main__': print(result, "\n-----\n\n-----") # Проверяем переносы в смешанном тексте (русский + английский) - etpgrf.defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN = 6 - typo_en = etpgrf.Typographer(langs='en', mode='mixed', hyphenation=True) txt = ("It was a chilly autumn afternoon when Anna finally received her custom-made KATEBLASH coat." " “I can’t believe how perfectly it fits!” she exclaimed, wrapping the soft, woolen fabric tightly" " around her shoulders.\n" @@ -81,25 +79,19 @@ if __name__ == '__main__': "\n" "Later, over coffee, Anna joked, “I told the tailor, ‘Make it so I never want to take it off.’ " "Looks like they succeeded!") + etpgrf.defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN = 6 + typo_en = etpgrf.Typographer(langs='en', mode='mixed', hyphenation=True) result = typo_en.process(text=txt) - print(result, "\n\n") + print(result, "\n\n--------------\n\n") + + # Проверяем если есть HTML-тегов + txt = ("

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.

") + typo_en = etpgrf.Typographer(langs='en', mode='mixed', process_html=True, hyphenation=True) + result = typo_en.process(text=txt) + print(result, "\n\n--------------\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") diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000..f4e1d3e --- /dev/null +++ b/requirement.txt @@ -0,0 +1,6 @@ +regex==2024.11.6 + +beautifulsoup4==4.13.4 +soupsieve==2.7 +typing_extensions==4.14.1 +