add: обработка html (теги исклчены из типографа).

This commit is contained in:
2025-07-19 23:00:41 +03:00
parent 6b72da89c4
commit 1c5fe77706
4 changed files with 108 additions and 54 deletions

View File

@@ -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', '₽'), # Российский рубль (₽)
}
# Другие символы (пример для расширения)

View File

@@ -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 по умолчанию выводит без тегов <html><body>
# если их не было в исходной строке.
return str(soup)
else:
# Если HTML-режим выключен, работаем как раньше
return self._process_text_node(text)
# def _get_nbsp(self): # Пример получения неразрывного пробела
# return "\u00A0" if self.mode in UTF else "&nbsp;"