Files
2025-etpgrf/etpgrf/quotes.py
erjemin 00c80b79f1 mod: Use node separators and placeholders for robust HTML processing
1. Защита тегов: Защищенные теги (<code>, <script> и т.д.) теперь физически заменяются на плейсхолдеры (\uFFFC) в DOM-дереве перед обработкой. Это предотвращает "протекание" контекста (например, склеивание слов через код) и защищает содержимое тегов от изменений.

2. Маркеры границ: При сборке "супер-строки" (для контекстной обработки) между всеми текстовыми узлами вставляются специальные разделители (\uFFFF). Это позволяет корректно восстанавливать текст по узлам, даже если длина текста изменилась (например, Unbreakables удалил лишние пробелы). Раньше мы полагались на карту длин (lengths_map), что приводило к смещению текста при любых изменениях длины.
2026-02-03 02:04:46 +03:00

79 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# etpgrf/quotes.py
# Модуль для расстановки кавычек в тексте
import regex
import logging
from .config import (LANG_RU, LANG_EN, CHAR_RU_QUOT1_OPEN, CHAR_RU_QUOT1_CLOSE, CHAR_EN_QUOT1_OPEN,
CHAR_EN_QUOT1_CLOSE, CHAR_RU_QUOT2_OPEN, CHAR_RU_QUOT2_CLOSE, CHAR_EN_QUOT2_OPEN,
CHAR_EN_QUOT2_CLOSE, CHAR_NODE_SEPARATOR)
from .comutil import parse_and_validate_langs
# --- Настройки логирования ---
logger = logging.getLogger(__name__)
# Определяем стили кавычек для разных языков
# Формат: (('открывающая_ур1', 'закрывающая_ур1'), ('открывающая_ур2', 'закрывающая_ур2'))
_QUOTE_STYLES = {
LANG_RU: ((CHAR_RU_QUOT1_OPEN, CHAR_RU_QUOT1_CLOSE), (CHAR_RU_QUOT2_OPEN, CHAR_RU_QUOT2_CLOSE)),
LANG_EN: ((CHAR_EN_QUOT1_OPEN, CHAR_EN_QUOT1_CLOSE), (CHAR_EN_QUOT2_OPEN, CHAR_EN_QUOT2_CLOSE)),
}
class QuotesProcessor:
"""
Обрабатывает прямые кавычки ("), превращая их в типографские
в зависимости от языка и контекста.
"""
def __init__(self, langs: str | list[str] | tuple[str, ...] | frozenset[str] | None = None):
self.langs = parse_and_validate_langs(langs)
# Выбираем стиль кавычек на основе первого поддерживаемого языка
self.open_quote = '"'
self.close_quote = '"'
for lang in self.langs:
if lang in _QUOTE_STYLES:
self.open_quote = _QUOTE_STYLES[lang][0][0]
self.close_quote = _QUOTE_STYLES[lang][0][1]
logger.debug(
f"QuotesProcessor: выбран стиль кавычек для языка '{lang}': '{self.open_quote}...{self.close_quote}'")
break # Используем стиль первого найденного языка
# Экранируем разделитель для использования в regex
sep = regex.escape(CHAR_NODE_SEPARATOR)
# Паттерн для открывающей кавычки: " перед буквой/цифрой,
# которой предшествует пробел, начало строки, открывающая скобка ИЛИ разделитель узлов.
# (?<=^|\s|[\(\[„\"\']|sep) - "просмотр назад" на начало строки... ищет пробел \s или знак из набора ([„"' или разделитель
# (?=\p{L}|sep) - "просмотр вперед" на букву \p{L} (но не цифру) ИЛИ разделитель узлов.
self._opening_quote_pattern = regex.compile(rf'(?<=^|\s|[\(\[„\"\']|{sep})\"(?=\p{{L}}|{sep})')
# self._opening_quote_pattern = regex.compile(r'(?<=^|\s|\p{Pi}|["\'\(\)])\"(?=\p{L})')
# Паттерн для закрывающей кавычки: " после буквы/цифры,
# за которой следует пробел, пунктуация, конец строки ИЛИ разделитель узлов.
# (?<=\p{L}|[?!…\.]|sep) - "просмотр назад" на букву или ?!… и точку ИЛИ разделитель узлов.
# (?=\s|[.,;:!?\)\"»”’]|\Z|sep) - "просмотр вперед" на пробел, пунктуацию, конец строки (\Z) или разделитель.
self._closing_quote_pattern = regex.compile(rf'(?<=\p{{L}}|[?!…\.]|{sep})\"(?=\s|[\.,;:!?\)\]»”’\"\']|\Z|{sep})')
# self._closing_quote_pattern = regex.compile(r'(?<=\p{L}|\p{N})\"(?=\s|[\.,;:!?\)\"»”’]|\Z)')
# self._closing_quote_pattern = regex.compile(r'(?<=\p{L}|[?!…])\"(?=\s|[\p{Po}\p{Pf}"\']|\Z)')
def process(self, text: str) -> str:
"""
Применяет правила замены кавычек к тексту.
"""
if '"' not in text:
# Быстрый выход, если в тексте нет прямых кавычек
return text
processed_text = text
# 1. Заменяем открывающие кавычки
# Заменяем только найденную кавычку, так как просмотр вперед не захватывает символы.
processed_text = self._opening_quote_pattern.sub(self.open_quote, processed_text)
# 2. Заменяем закрывающие кавычки
processed_text = self._closing_quote_pattern.sub(self.close_quote, processed_text)
return processed_text