mod: Конвейер типографа с рекурсивным обходом DOM
This commit is contained in:
@@ -131,12 +131,15 @@ SAFE_MODE_CHARS_TO_MNEMONIC = frozenset([
|
||||
# 3. СПИСОК ДЛЯ ЧИСЛОВОГО КОДИРОВАНИЯ: Символы без стандартного имени.
|
||||
ALWAYS_ENCODE_TO_NUMERIC_CHARS = frozenset([
|
||||
'\u058F', # Знак армянского драма (֏)
|
||||
'\u20BD', # Знак русского рубля (₽)
|
||||
'\u20B4', # Знак украинской гривны (₴)
|
||||
'\u20B8', # Знак казахстанского тенге (₸)
|
||||
'\u20B9', # Знак индийской рупии (₹)
|
||||
'\u20BA', # Знак турецкой лиры (₺)
|
||||
'\u20BB', # Знак итальянской лиры (₻)
|
||||
'\u20BC', # Знак азербайджанского маната
|
||||
'\u20BD', # Знак русского рубля (₽)
|
||||
'\u20BE', # Знак грузинский лари (₾)
|
||||
'\u20BF', # Знак биткоина (₿)
|
||||
])
|
||||
|
||||
# 4. СЛОВАРЬ ПРИОРИТЕТОВ: Кастомные и/или предпочитаемые мнемоники.
|
||||
@@ -649,7 +652,8 @@ DEFAULT_POST_UNITS = [
|
||||
'in', 'ft', 'yd', 'mi', 'oz', 'lb', 'st', 'pt', 'qt', 'gal', 'mph', 'rpm', 'hp', 'psi', 'cal',
|
||||
]
|
||||
# Пред-позиционные (№ 5, $ 10)
|
||||
DEFAULT_PRE_UNITS = ['№', '$', '€', '£', '₽', '#', '§']
|
||||
DEFAULT_PRE_UNITS = ['№', '$', '€', '£', '₽', '#', '§', '¤', '₴', '₿', '₺', '₦', '₩', '₪', '₫', '₲', '₡', '₵',
|
||||
'ГОСТ', 'ТУ', 'ИСО', 'DIN', 'ASTM', 'EN', 'IEC', 'IEEE'] # технические стандарты перед числом работают как единицы измерения
|
||||
|
||||
# Операторы, которые могут стоять между единицами измерения (км/ч)
|
||||
# Сложение и вычитание здесь намеренно отсутствуют.
|
||||
@@ -665,9 +669,9 @@ ABBR_COMMON_FINAL = [
|
||||
]
|
||||
|
||||
ABBR_COMMON_PREPOSITION = [
|
||||
'т. е.', 'т. к.', 'т. о.',
|
||||
'т. е.', 'т. к.', 'т. о.', 'т. ч.',
|
||||
'и. о.', 'ио', 'вр. и. о.', 'врио',
|
||||
'тов.', 'г-н.', 'г-жа.', 'им.',
|
||||
'д. о. с.', 'д. о. н.', 'д. м. н.', 'к. т. д.', 'к. т. п.',
|
||||
'АО', 'ООО', 'ЗАО', 'ПАО', 'НКО', 'ОАО', 'ФГУП',
|
||||
'АО', 'ООО', 'ЗАО', 'ПАО', 'НКО', 'ОАО', 'ФГУП', 'НИИ', 'ПБОЮЛ', 'ИП',
|
||||
]
|
@@ -1,3 +1,6 @@
|
||||
# etpgrf/typograph.py
|
||||
# Основной класс Typographer, который объединяет все модули правил и предоставляет единый интерфейс.
|
||||
# Поддерживает обработку текста внутри HTML-тегов с помощью BeautifulSoup.
|
||||
import logging
|
||||
import html
|
||||
try:
|
||||
@@ -117,12 +120,28 @@ class Typographer:
|
||||
processed_text = self.hyphenation.hyp_in_text(processed_text)
|
||||
# ... вызовы других активных модулей правил ...
|
||||
|
||||
return processed_text
|
||||
# Финальный шаг: кодируем результат в соответствии с выбранным режимом
|
||||
return encode_from_unicode(processed_text, self.mode)
|
||||
|
||||
def _walk_tree(self, node):
|
||||
"""
|
||||
Рекурсивно обходит DOM-дерево, находя и обрабатывая все текстовые узлы.
|
||||
"""
|
||||
# Список "детей" узла, который мы будем изменять.
|
||||
# Копируем в список, так как будем изменять его во время итерации.
|
||||
for child in list(node.children):
|
||||
if isinstance(child, NavigableString):
|
||||
# Если это текстовый узел, обрабатываем его
|
||||
# Пропускаем пустые или состоящие из пробелов узлы
|
||||
if not child.string.strip():
|
||||
continue
|
||||
|
||||
processed_node_text = self._process_text_node(child.string)
|
||||
child.replace_with((processed_node_text))
|
||||
elif child.name not in ['style', 'script', 'pre', 'code', 'kbd', 'samp', 'math']:
|
||||
# Если это "безопасный" тег, рекурсивно заходим в него
|
||||
self._walk_tree(child)
|
||||
|
||||
|
||||
# Конвейер для обработки текста
|
||||
def process(self, text: str) -> str:
|
||||
"""
|
||||
Обрабатывает текст, применяя все активные правила типографики.
|
||||
@@ -134,28 +153,16 @@ class Typographer:
|
||||
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: str = 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)
|
||||
|
||||
# Запускаем рекурсивный обход дерева, начиная с корневого элемента
|
||||
self._walk_tree(soup)
|
||||
# Получаем измененный HTML. BeautifulSoup по умолчанию выводит без тегов <html><body>
|
||||
# если их не было в исходной строке.
|
||||
processed = str(soup)
|
||||
processed_html = str(soup)
|
||||
|
||||
# Финальный шаг: BeautifulSoup по умолчанию экранирует амперсанды (& -> &).
|
||||
# Но наш кодек encode_from_unicode() тоже это делает. Так что мы получаем двойное экранирование.
|
||||
# Чтобы избежать этого, мы просто заменяем & обратно на &.
|
||||
return processed_html.replace('&', '&')
|
||||
else:
|
||||
# Если HTML-режим выключен
|
||||
processed = self._process_text_node(text)
|
||||
# Возвращаем
|
||||
return encode_from_unicode(processed, self.mode)
|
||||
return self._process_text_node(text)
|
||||
|
Reference in New Issue
Block a user