129 lines
8.0 KiB
Python
129 lines
8.0 KiB
Python
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
|
||
|
||
|
||
# --- Настройки логирования ---
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
# --- Основной класс Typographer ---
|
||
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, # Правила для предотвращения разрыва коротких слов
|
||
# ... другие модули правил ...
|
||
):
|
||
|
||
# A. --- Обработка и валидация параметра langs ---
|
||
self.langs: frozenset[str] = parse_and_validate_langs(langs)
|
||
# B. --- Обработка и валидация параметра mode ---
|
||
self.mode: str = parse_and_validate_mode(mode)
|
||
# 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
|
||
if hyphenation is True or hyphenation is None:
|
||
# C1. Создаем новый объект Hyphenator с заданными языками и режимом, а все остальное по умолчанию
|
||
self.hyphenation = Hyphenator(langs=self.langs, mode=self.mode)
|
||
elif isinstance(hyphenation, Hyphenator):
|
||
# C2. Если hyphenation - это объект Hyphenator, то просто сохраняем его (и используем его langs и mode)
|
||
self.hyphenation = hyphenation
|
||
|
||
# E. --- Конфигурация правил неразрывных слов ---
|
||
self.unbreakables: Unbreakables | None = None
|
||
if unbreakables is True or unbreakables is None:
|
||
# D1. Создаем новый объект Unbreakables с заданными языками и режимом, а все остальное по умолчанию
|
||
self.unbreakables = Unbreakables(langs=self.langs, mode=self.mode)
|
||
elif isinstance(unbreakables, Unbreakables):
|
||
# D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode)
|
||
self.unbreakables = unbreakables
|
||
|
||
# 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"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:
|
||
"""
|
||
Обрабатывает текст, применяя все активные правила типографики.
|
||
Поддерживает обработку текста внутри 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)
|
||
|
||
# Возвращаем измененный 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 " "
|
||
|