add: обработка html (теги исклчены из типографа).
This commit is contained in:
@@ -30,7 +30,7 @@ SHY_ENTITIES = {
|
|||||||
SPACE_ENTITIES = {
|
SPACE_ENTITIES = {
|
||||||
'NBSP': ('\u00A0', ' '), # Неразрывный пробел
|
'NBSP': ('\u00A0', ' '), # Неразрывный пробел
|
||||||
'THINSP': ('\u2009', ' '), # Тонкий пробел
|
'THINSP': ('\u2009', ' '), # Тонкий пробел
|
||||||
'ENSP': ('\u2002', ' '), # Полуширокий пробел
|
'ENSP': ('\u2002', ' '), # Полу-широкий пробел
|
||||||
'EMSP': ('\u2003', ' '), # Широкий пробел
|
'EMSP': ('\u2003', ' '), # Широкий пробел
|
||||||
'ZWNJ': ('\u200C', '‌'), # Разрывный пробел нулевой ширины (без пробела)
|
'ZWNJ': ('\u200C', '‌'), # Разрывный пробел нулевой ширины (без пробела)
|
||||||
'ZWJ': ('\u200D', '‍'), # Неразрывный пробел нулевой ширины
|
'ZWJ': ('\u200D', '‍'), # Неразрывный пробел нулевой ширины
|
||||||
@@ -38,9 +38,10 @@ SPACE_ENTITIES = {
|
|||||||
|
|
||||||
# Тире и дефисы
|
# Тире и дефисы
|
||||||
DASH_ENTITIES = {
|
DASH_ENTITIES = {
|
||||||
'NDASH': ('\u2013', '–'), # Короткое тире
|
'NDASH': ('\u2013', '–'), # Cреднее тире (En dash)
|
||||||
'MDASH': ('\u2014', '—'), # Длинное тире
|
'MDASH': ('\u2014', '—'), # Длинное тире
|
||||||
# 'HYPHEN': ('\u2010', '‐'), # Обычный дефис (если нужно отличать от минуса)
|
'HYPHEN': ('\u2010', '‐'), # Обычный дефис (если нужно отличать от минуса)
|
||||||
|
'HORBAR': ('\u2015', '―'), # Горизонтальная линия (длинная черта)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Кавычки
|
# Кавычки
|
||||||
@@ -57,7 +58,16 @@ QUOTE_ENTITIES = {
|
|||||||
'SBQUO': ('\u201A', '‚'), # Нижняя одинарная кавычка -- ‚
|
'SBQUO': ('\u201A', '‚'), # Нижняя одинарная кавычка -- ‚
|
||||||
'LSAQUO': ('\u2039', '‹'), # Открывающая французская угловая кавычка -- ›
|
'LSAQUO': ('\u2039', '‹'), # Открывающая французская угловая кавычка -- ›
|
||||||
'RSAQUO': ('\u203A', '›'), # Закрывающая французская угловая кавычка -- ‹
|
'RSAQUO': ('\u203A', '›'), # Закрывающая французская угловая кавычка -- ‹
|
||||||
|
}
|
||||||
|
|
||||||
|
CURRENCY_ENTITIES = {
|
||||||
|
'DOLLAR': ('\u0024', '$'), # Доллар
|
||||||
|
'CENT': ('\u00A2', '¢'), # Цент
|
||||||
|
'POUND': ('\u00A3', '£'), # Фунт стерлингов
|
||||||
|
'CURREN': ('\u00A4', '¤'), # Знак валюты (обычно используется для обозначения "без конкретной валюты")
|
||||||
|
'YEN': ('\u00A5', '¥'), # Йена
|
||||||
|
'EURO': ('\u20AC', '€'), # Евро
|
||||||
|
'RUBLE': ('\u20BD', '₽'), # Российский рубль (₽)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Другие символы (пример для расширения)
|
# Другие символы (пример для расширения)
|
||||||
|
@@ -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.comutil import parse_and_validate_mode, parse_and_validate_langs
|
||||||
from etpgrf.hyphenation import Hyphenator
|
from etpgrf.hyphenation import Hyphenator
|
||||||
from etpgrf.unbreakables import Unbreakables
|
from etpgrf.unbreakables import Unbreakables
|
||||||
import logging
|
|
||||||
|
|
||||||
# --- Настройки логирования ---
|
# --- Настройки логирования ---
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -12,6 +17,7 @@ class Typographer:
|
|||||||
def __init__(self,
|
def __init__(self,
|
||||||
langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
|
langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None,
|
||||||
mode: str | None = None,
|
mode: str | None = None,
|
||||||
|
process_html: bool = False, # Флаг обработки HTML-тегов
|
||||||
hyphenation: Hyphenator | bool | None = True, # Перенос слов и параметры расстановки переносов
|
hyphenation: Hyphenator | bool | None = True, # Перенос слов и параметры расстановки переносов
|
||||||
unbreakables: Unbreakables | bool | None = True, # Правила для предотвращения разрыва коротких слов
|
unbreakables: Unbreakables | bool | None = True, # Правила для предотвращения разрыва коротких слов
|
||||||
# ... другие модули правил ...
|
# ... другие модули правил ...
|
||||||
@@ -21,7 +27,14 @@ class Typographer:
|
|||||||
self.langs: frozenset[str] = parse_and_validate_langs(langs)
|
self.langs: frozenset[str] = parse_and_validate_langs(langs)
|
||||||
# B. --- Обработка и валидация параметра mode ---
|
# B. --- Обработка и валидация параметра mode ---
|
||||||
self.mode: str = parse_and_validate_mode(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`.
|
# А для специальных случаев, когда переносы не нужны, пусть не ленятся и делают `hyphenation=False`.
|
||||||
self.hyphenation: Hyphenator | None = None
|
self.hyphenation: Hyphenator | None = None
|
||||||
@@ -31,13 +44,8 @@ class Typographer:
|
|||||||
elif isinstance(hyphenation, Hyphenator):
|
elif isinstance(hyphenation, Hyphenator):
|
||||||
# C2. Если hyphenation - это объект Hyphenator, то просто сохраняем его (и используем его langs и mode)
|
# C2. Если hyphenation - это объект Hyphenator, то просто сохраняем его (и используем его langs и mode)
|
||||||
self.hyphenation = hyphenation
|
self.hyphenation = hyphenation
|
||||||
elif hyphenation is False:
|
|
||||||
# C3. Если hyphenation - False, то правило переноса выключено.
|
# E. --- Конфигурация правил неразрывных слов ---
|
||||||
self.hyphenation = None
|
|
||||||
else:
|
|
||||||
# D4. Если hyphenation что-то неведомое, то игнорируем его и правило переноса выключено
|
|
||||||
self.hyphenation = None
|
|
||||||
# D. --- Конфигурация правил неразрывных слов ---
|
|
||||||
self.unbreakables: Unbreakables | None = None
|
self.unbreakables: Unbreakables | None = None
|
||||||
if unbreakables is True or unbreakables is None:
|
if unbreakables is True or unbreakables is None:
|
||||||
# D1. Создаем новый объект Unbreakables с заданными языками и режимом, а все остальное по умолчанию
|
# D1. Создаем новый объект Unbreakables с заданными языками и режимом, а все остальное по умолчанию
|
||||||
@@ -45,37 +53,75 @@ class Typographer:
|
|||||||
elif isinstance(unbreakables, Unbreakables):
|
elif isinstance(unbreakables, Unbreakables):
|
||||||
# D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode)
|
# D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode)
|
||||||
self.unbreakables = unbreakables
|
self.unbreakables = unbreakables
|
||||||
elif unbreakables is False:
|
|
||||||
# D3. Если unbreakables - False, то правило неразрывных слов выключено.
|
# F. --- Конфигурация других правил---
|
||||||
self.unbreakables = None
|
|
||||||
else:
|
|
||||||
# D4. Если unbreakables что-то неведомое, то игнорируем его и правило неразрывных слов выключено
|
|
||||||
self.unbreakables = None
|
|
||||||
# E. --- Конфигурация других правил---
|
|
||||||
|
|
||||||
# Z. --- Логирование инициализации ---
|
# Z. --- Логирование инициализации ---
|
||||||
logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, "
|
logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, "
|
||||||
f"hyphenation: {self.hyphenation is not None}, "
|
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:
|
def process(self, text: str) -> str:
|
||||||
processed_text = text
|
"""
|
||||||
# Применяем правила в определенном порядке.
|
Обрабатывает текст, применяя все активные правила типографики.
|
||||||
# Неразрывные конструкции лучше применять до переносов.
|
Поддерживает обработку текста внутри HTML-тегов.
|
||||||
if self.unbreakables is not None:
|
"""
|
||||||
processed_text = self.unbreakables.process(processed_text)
|
if not text:
|
||||||
if self.hyphenation is not None:
|
return ""
|
||||||
# Обработчик переносов (Hyphenator) активен. Обрабатываем текст...
|
# Если включена обработка HTML и BeautifulSoup доступен
|
||||||
processed_text = self.hyphenation.hyp_in_text(processed_text)
|
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:
|
# Возвращаем измененный HTML. BeautifulSoup по умолчанию выводит без тегов <html><body>
|
||||||
# processed_text = self.glue_prepositions_rule.hyp_in_text(processed_text, non_breaking_space_char=self._get_nbsp())
|
# если их не было в исходной строке.
|
||||||
|
return str(soup)
|
||||||
# ... вызовы других активных модулей правил ...
|
else:
|
||||||
return processed_text
|
# Если HTML-режим выключен, работаем как раньше
|
||||||
|
return self._process_text_node(text)
|
||||||
|
|
||||||
# def _get_nbsp(self): # Пример получения неразрывного пробела
|
# def _get_nbsp(self): # Пример получения неразрывного пробела
|
||||||
# return "\u00A0" if self.mode in UTF else " "
|
# return "\u00A0" if self.mode in UTF else " "
|
||||||
|
32
main.py
32
main.py
@@ -60,8 +60,6 @@ if __name__ == '__main__':
|
|||||||
print(result, "\n-----\n\n-----")
|
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."
|
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"
|
" “I can’t believe how perfectly it fits!” she exclaimed, wrapping the soft, woolen fabric tightly"
|
||||||
" around her shoulders.\n"
|
" around her shoulders.\n"
|
||||||
@@ -81,25 +79,19 @@ if __name__ == '__main__':
|
|||||||
"\n"
|
"\n"
|
||||||
"Later, over coffee, Anna joked, “I told the tailor, ‘Make it so I never want to take it off.’ "
|
"Later, over coffee, Anna joked, “I told the tailor, ‘Make it so I never want to take it off.’ "
|
||||||
"Looks like they succeeded!")
|
"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)
|
result = typo_en.process(text=txt)
|
||||||
print(result, "\n\n")
|
print(result, "\n\n--------------\n\n")
|
||||||
|
|
||||||
|
# Проверяем если есть HTML-тегов
|
||||||
|
txt = ("<p>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 <i>choice of fabric</i> to the delicate embroidery - made it"
|
||||||
|
" clear that this was no ordinary coat.</p><style>body { font-family: Arial; }</style>")
|
||||||
|
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")
|
|
||||||
|
|
||||||
|
|
||||||
|
6
requirement.txt
Normal file
6
requirement.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
regex==2024.11.6
|
||||||
|
|
||||||
|
beautifulsoup4==4.13.4
|
||||||
|
soupsieve==2.7
|
||||||
|
typing_extensions==4.14.1
|
||||||
|
|
Reference in New Issue
Block a user