diff --git a/etpgrf/config.py b/etpgrf/config.py index 11bbdc7..609f39b 100644 --- a/etpgrf/config.py +++ b/etpgrf/config.py @@ -72,6 +72,7 @@ CHAR_ARROW_LR_LONG_DOUBLE = '\u27fa' # Длинная двойная двуна CHAR_MIDDOT = '\u00b7' # Средняя точка (· иногда используется как знак умножения) / · CHAR_UNIT_SEPARATOR = '\u25F0' # Символ временный разделитель для составных единиц (◰), чтобы не уходить # в "мертвый" цикл при замене на тонкий пробел. Можно взять любой редкий символом. +CHAR_PLACEHOLDER = '\uFFFC' # Символ-заполнитель (Object Replacement Character) для защищенных тегов. # === КОНСТАНТЫ ПСЕВДОГРАФИКИ === @@ -253,7 +254,7 @@ CUSTOM_ENCODE_MAP = { '\u201d': '”', # ” / ” / ” / ” '\u2019': '’', # ’ / ’ / ’ / ’ '\u2237': '∷', # ∷ / ∷ / ∷ - '\u2201': '∁', # ∁ / ∁ / ∁ + '\u2201': '∁', # / ∁ / ∁ '\u2218': '∘', # ∘ / ∘ / ∘ '\u2102': 'ℂ', # ℂ / ℂ / ℂ '\u222f': '∯', # ∯ / ∯ / ∯ diff --git a/etpgrf/typograph.py b/etpgrf/typograph.py index 1c5dd69..e92e78a 100644 --- a/etpgrf/typograph.py +++ b/etpgrf/typograph.py @@ -17,7 +17,7 @@ from etpgrf.symbols import SymbolsProcessor from etpgrf.sanitizer import SanitizerProcessor from etpgrf.hanging import HangingPunctuationProcessor from etpgrf.codec import decode_to_unicode, encode_from_unicode -from etpgrf.config import PROTECTED_HTML_TAGS, SANITIZE_ALL_HTML +from etpgrf.config import PROTECTED_HTML_TAGS, SANITIZE_ALL_HTML, CHAR_PLACEHOLDER # --- Настройки логирования --- @@ -156,6 +156,82 @@ class Typographer: # Если это "обычный" html-тег, рекурсивно заходим в него self._walk_tree(child) + def _hide_protected_tags(self, soup) -> list: + """ + Находит все защищенные теги, заменяет их на плейсхолдеры и возвращает список сохраненных тегов. + """ + protected_tags = [] + if not PROTECTED_HTML_TAGS: + return protected_tags + + # Формируем селектор для поиска + selector = ", ".join(PROTECTED_HTML_TAGS) + + # Находим все теги. Важно: find_all возвращает их в порядке появления в документе. + # Но если мы будем заменять их на лету, структура может измениться. + # Поэтому лучше собрать список, а потом заменить. + tags_to_replace = soup.select(selector) + + for tag in tags_to_replace: + # Сохраняем тег (он будет удален из дерева при replace_with, но объект останется в памяти) + protected_tags.append(tag) + # Заменяем на текстовый узел с плейсхолдером + tag.replace_with(NavigableString(CHAR_PLACEHOLDER)) + + return protected_tags + + def _restore_protected_tags(self, soup, protected_tags: list): + """ + Восстанавливает защищенные теги на места плейсхолдеров. + """ + if not protected_tags: + return + + # Ищем все текстовые узлы, содержащие плейсхолдер + # Используем список, так как будем менять дерево + text_nodes_with_placeholder = [ + node for node in soup.descendants + if isinstance(node, NavigableString) and CHAR_PLACEHOLDER in node + ] + + tag_index = 0 + for node in text_nodes_with_placeholder: + text = str(node) + # Если в узле есть плейсхолдеры, нам нужно его разбить + if CHAR_PLACEHOLDER in text: + parts = text.split(CHAR_PLACEHOLDER) + + # Создаем список новых узлов для замены + new_nodes = [] + for i, part in enumerate(parts): + # Добавляем текст (если он не пустой) + if part: + new_nodes.append(NavigableString(part)) + + # Если это не последняя часть, значит, здесь был плейсхолдер. + # Вставляем тег. + if i < len(parts) - 1: + if tag_index < len(protected_tags): + new_nodes.append(protected_tags[tag_index]) + tag_index += 1 + else: + logger.warning("Mismatch in protected tags count during restoration.") + + # Заменяем исходный узел на новые + if new_nodes: + first_node = new_nodes[0] + node.replace_with(first_node) + current_pos = first_node + for next_node in new_nodes[1:]: + current_pos.insert_after(next_node) + current_pos = next_node + else: + # Если узел состоял только из плейсхолдера и мы его заменили на тег, + # то new_nodes может быть пустым (если тег был один и текст пустой). + # Но split('') дает [''], так что parts не пустой. + # Логика выше должна работать. + pass + def process(self, text: str) -> str: """ Обрабатывает текст, применяя все активные правила типографики. @@ -195,19 +271,28 @@ class Typographer: # Если результат - soup, продолжаем работу с ним soup = result + # --- ЭТАП 2.5: Скрытие защищенных тегов --- + # Заменяем ,