From 6b098f161def2f790532b0b1b8c3beee8379e09d Mon Sep 17 00:00:00 2001 From: erjemin Date: Thu, 26 Feb 2026 17:46:51 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=B1=D0=BE=D0=BB=D0=B5=D0=B5=20=D0=B1?= =?UTF-8?q?=D1=8B=D1=81=D1=82=D1=80=D0=B0=D1=8F=20=D1=81=D1=85=D0=B5=D0=BC?= =?UTF-8?q?=D0=B0=20(=D0=BF=D1=80=D0=BE=D1=85=D0=BE=D0=B4=20=D0=B2=20?= =?UTF-8?q?=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D0=B9?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20=D0=B4?= =?UTF-8?q?=D0=B2=D1=83=D1=85,=20=D0=B8=D0=B7=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=D0=B8=D1=81=D1=8C=20=D0=BE=D1=82=20=D1=81=D0=BB=D0=BE?= =?UTF-8?q?=D0=B6=D0=BD=D0=BE=D0=B9=20=D0=B8=20=D1=85=D1=80=D1=83=D0=BF?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20?= =?UTF-8?q?=D1=81=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D0=B9=20=D0=B4=D0=BB?= =?UTF-8?q?=D0=B8=D0=BD=20=D0=B1=D0=BB=D0=B0=D0=B3=D0=BE=D0=B4=D0=B0=D1=80?= =?UTF-8?q?=D1=8F=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=BF=D0=BB=D0=B5=D0=B9=D1=81=D1=85=D0=BE=D0=BB=D0=B4?= =?UTF-8?q?=D0=B5=D1=80=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=BE=D0=B2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etpgrf/typograph.py | 169 ++++---------------------------------------- 1 file changed, 13 insertions(+), 156 deletions(-) diff --git a/etpgrf/typograph.py b/etpgrf/typograph.py index 68cb312..5f20474 100644 --- a/etpgrf/typograph.py +++ b/etpgrf/typograph.py @@ -116,46 +116,6 @@ class Typographer: 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.symbols is not None: - processed_text = self.symbols.process(processed_text) - if self.layout is not None: - processed_text = self.layout.process(processed_text) - if self.hyphenation is not None: - processed_text = self.hyphenation.hyp_in_text(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 PROTECTED_HTML_TAGS: - # Если это "обычный" html-тег, рекурсивно заходим в него - self._walk_tree(child) - def _hide_protected_tags(self, soup) -> list: """ Находит все защищенные теги, заменяет их на плейсхолдеры и возвращает список сохраненных тегов. @@ -164,18 +124,11 @@ class Typographer: 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 @@ -187,8 +140,6 @@ class Typographer: if not protected_tags: return - # Ищем все текстовые узлы, содержащие плейсхолдер - # Используем список, так как будем менять дерево text_nodes_with_placeholder = [ node for node in soup.descendants if isinstance(node, NavigableString) and CHAR_PLACEHOLDER in node @@ -197,19 +148,14 @@ class Typographer: 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]) @@ -217,7 +163,6 @@ class Typographer: else: logger.warning("Mismatch in protected tags count during restoration.") - # Заменяем исходный узел на новые if new_nodes: first_node = new_nodes[0] node.replace_with(first_node) @@ -225,12 +170,6 @@ class Typographer: 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: """ @@ -240,20 +179,11 @@ class Typographer: if not text: return "" - # --- ЭТАП 0: Защита & --- - # Заменяем & на временный плейсхолдер, чтобы он не был декодирован в & - # и не был повторно закодирован в &amp; text = text.replace('&', CHAR_AMP_PLACEHOLDER) - # Если включена обработка HTML и BeautifulSoup доступен if self.process_html: - # --- ЭТАП 1: Анализ структуры --- - # Проверяем, есть ли в начале текста теги или . - # Если есть - значит, это полноценный документ, и мы должны вернуть его целиком. - # Если нет - значит, это фрагмент, и мы должны вернуть только содержимое body. is_full_document = bool(regex.search(r'^\s*<(?:!DOCTYPE|html|body)', text, regex.IGNORECASE)) - # --- ЭТАП 2: Парсинг и Санитизация --- try: soup = BeautifulSoup(text, 'lxml') except Exception: @@ -261,138 +191,65 @@ class Typographer: if self.sanitizer: result = self.sanitizer.process(soup) - # Если режим SANITIZE_ALL_HTML, то результат - это строка (чистый текст) if isinstance(result, str): - # Переключаемся на обработку обычного текста - text = result - # ВАЖНО: Мы выходим из ветки process_html и идем в ветку else, - # но так как мы внутри if, нам нужно явно вызвать логику для текста. - # Проще всего рекурсивно вызвать process с выключенным process_html, - # но чтобы не менять состояние объекта, просто выполним логику "else" блока здесь. - # Или, еще проще: присвоим text = result и пойдем в блок else? Нет, мы уже внутри if. - - # Решение: Выполняем логику обработки простого текста прямо здесь - return self._process_plain_text(text) - - # Если результат - soup, продолжаем работу с ним + return self._process_plain_text(result).replace(CHAR_AMP_PLACEHOLDER, '&') soup = result - # --- ЭТАП 2.5: Скрытие защищенных тегов --- - # Заменяем ,