4 Commits
v0.1.2 ... main

7 changed files with 142 additions and 30 deletions

View File

@@ -5,18 +5,20 @@
Формат основан на [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
и этот проект придерживается [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.2] - 2025-05-02
## [0.1.3] - 2026-01-11
### Исправлено
- **Критическое исправление:** Добавлена отсутствующая зависимость `regex` в `pyproject.toml`. Без неё библиотека
падала при импорте.
- Файл `LIBRARY_SPECS.md` для LLM.
- Исправлена проблема с появлением лишних тегов `<html>` и `<body>` при обработке фрагментов HTML (когда используется парсер `lxml`). Теперь типограф автоматически определяет, был ли на входе полноценный документ или фрагмент, и возвращает соответствующий результат.
## [0.1.1] - 2025-05-02
## [0.1.2] - 2025-12-27
### Исправлено
- **Критическое исправление:** Добавлена отсутствующая зависимость `regex` в `pyproject.toml`. Без неё библиотека падала при импорте.
## [0.1.1] - 2025-12-23
### Добавлено
- Ссылки на зеркала репозитория (GitVerse, Gitea) в `pyproject.toml` и `README.md`.
- Раздел Credits в документации.
## [0.1.0] - 2025-05-01
## [0.1.0] - 2025-12-23
### Добавлено
- Первый публичный релиз библиотеки `etpgrf`.
- Основные модули:
@@ -24,8 +26,9 @@
- `Hyphenator`: расстановка мягких переносов (алгоритм Ляна-Кнута).
- `QuotesProcessor`: замена кавычек («ёлочки», „лапки“).
- `Unbreakables`: неразрывные пробелы для предлогов, союзов и частиц.
- `LayoutProcessor`: типографика тире, инициалов, акронимов, единиц измерения.
- `SymbolsProcessor`: псевдографика.
- `LayoutProcessor`: типографика тире, инициалов, акронимов, единиц измерения, устойчивых сокращений (постпозиционных
и препозиционных).
- `SymbolsProcessor`: псевдографика (тире, стрелочки, копирайт и т.п.)
- `HangingPunctuationProcessor`: висячая пунктуация.
- `SanitizerProcessor`: очистка HTML перед обработкой.
- Поддержка русского, русского дореформенного и английского языков.

View File

@@ -1,4 +1,4 @@
# Библиотека etpgrf: Контекст и Спецификация для LLM
# Библиотека etpgrf: Контекст и Спецификация
Этот документ описывает архитектуру и API библиотеки `etpgrf` (Electro-Typographer), предназначенной для типографирования текста в вебе. Используйте этот контекст при разработке зависимых проектов (например, веб-сайтов).
@@ -16,14 +16,20 @@
* **Язык:** Python 3.10+.
* **Зависимости:** `beautifulsoup4`, `lxml`, `regex`.
* **Главный класс:** `etpgrf.Typographer`.
* **Принцип работы:**
1. Принимает текст или HTML.
2. Если HTML: парсит через `BeautifulSoup` (lxml), очищает от старой разметки (Sanitizer).
3. Извлекает текстовые узлы, склеивает их в "супер-строку" для контекстных правил (кавычки).
4. Применяет правила к тексту.
5. Восстанавливает структуру HTML.
6. Применяет висячую пунктуацию (модификация дерева DOM).
7. Возвращает строку.
* **Принцип работы (Pipeline):**
1. **Анализ структуры:** Типограф проверяет, является ли входной текст полным HTML-документом (есть `<html>`, `<body>`) или фрагментом. Это важно для корректной сборки на выходе.
2. **Парсинг и Санитизация:** Текст парсится через `BeautifulSoup` (предпочтительно `lxml`). Если включен санитайзер, происходит очистка от старой разметки (удаление висячей пунктуации или всех тегов).
3. **Подготовка (Токенизация):** Извлекаются все текстовые узлы (`NavigableString`), которые склеиваются в одну "супер-строку". Это позволяет правилам видеть контекст через границы тегов (например, кавычка в `<b>` и слово после него).
4. **Контекстная обработка:** К "супер-строке" применяются правила, требующие глобального контекста:
* `QuotesProcessor`: расстановка кавычек.
* `Unbreakables`: привязка предлогов и союзов.
5. **Восстановление структуры:** Обработанная "супер-строка" аккуратно нарезается обратно и раскладывается по исходным текстовым узлам DOM-дерева.
6. **Локальная обработка:** Запускается рекурсивный обход дерева. К каждому текстовому узлу применяются локальные правила:
* `SymbolsProcessor`: псевдографика.
* `LayoutProcessor`: тире, спецсимволы.
* `Hyphenator`: расстановка переносов.
7. **Висячая пунктуация:** Модифицируется само DOM-дерево. Символы пунктуации оборачиваются в теги `<span>` с классами для CSS-коррекции.
8. **Финальная сборка:** Дерево сериализуется в строку. Если на входе был фрагмент, возвращается только содержимое `<body>`, чтобы не плодить лишние теги.
## 3. API: Класс Typographer

24
Rules Normal file
View File

@@ -0,0 +1,24 @@
# Правила проекта 2025-etpgrf при обработке ИИ
## Общие сведения
- **Проект:** Python-библиотека для экранной типографики для веб (висячая пунктуация, неразрывные пробелы, перенос слов и т.д.).
- **Язык:** Python 3.10+.
- **Стиль кода:** PEP8.
- **Типизация:** Обязательные Type Hints для аргументов и возвращаемых значений.
- **Язык комментариев:** Русский.
## Архитектура
- **Точка входа:** Класс `Typographer` в `etpgrf/typograph.py`.
- **Обработка HTML:**
- Использовать `BeautifulSoup4` (предпочтительно парсер `lxml`).
- НИКОГДА не парсить HTML регулярными выражениями.
- **Санитизация:** Всегда выполняется *до* рекурсивного обхода дерева.
- **Рекурсия:** Использовать `_walk_tree` для обработки текстовых узлов, сохраняя структуру HTML.
- **Конфигурация:** Все константы (regex, коды символов, классы) должны быть в `etpgrf/config.py`.
## Тестирование
- **Фреймворк:** `pytest`.
- **Структура:**
- Юнит-тесты: `tests/test_<module>.py`.
- Интеграционные тесты: `tests/test_typograph.py`.
- **Философия:** Тестировать как режим простого текста ("plain text"), так и режим HTML.

View File

@@ -8,7 +8,11 @@ etpgrf - библиотека для экранной типографики т
- Висячая пунктуация
- Очистка и обработка HTML
"""
__version__ = "0.1.0"
__version__ = "0.1.3"
__author__ = "Sergei Erjemin"
__email__ = "erjemin@gmail.com"
__license__ = "MIT"
__copyright__ = "Copyright 2025 Sergei Erjemin"
import etpgrf.defaults
import etpgrf.logger

View File

@@ -3,6 +3,7 @@
# Поддерживает обработку текста внутри HTML-тегов с помощью BeautifulSoup.
import logging
import html
import regex # Для проверки наличия корневых тегов
try:
from bs4 import BeautifulSoup, NavigableString
except ImportError:
@@ -164,13 +165,18 @@ class Typographer:
return ""
# Если включена обработка HTML и BeautifulSoup доступен
if self.process_html:
# --- ЭТАП 1: Токенизация и "умная склейка" ---
# --- ЭТАП 1: Анализ структуры ---
# Проверяем, есть ли в начале текста теги <html> или <body>.
# Если есть - значит, это полноценный документ, и мы должны вернуть его целиком.
# Если нет - значит, это фрагмент, и мы должны вернуть только содержимое body.
is_full_document = bool(regex.search(r'^\s*<(?:!DOCTYPE|html|body)', text, regex.IGNORECASE))
# --- ЭТАП 2: Парсинг и Санитизация ---
try:
soup = BeautifulSoup(text, 'lxml')
except Exception:
soup = BeautifulSoup(text, 'html.parser')
# --- ЭТАП 0: Санитизация (Очистка) ---
if self.sanitizer:
result = self.sanitizer.process(soup)
# Если режим SANITIZE_ALL_HTML, то результат - это строка (чистый текст)
@@ -189,20 +195,21 @@ class Typographer:
# Если результат - soup, продолжаем работу с ним
soup = result
# 1.1. Создаем "токен-стрим" из текстовых узлов, которые мы будем обрабатывать.
# --- ЭТАП 3: Подготовка (токен-стрим) ---
# 3.1. Создаем "токен-стрим" из текстовых узлов, которые мы будем обрабатывать.
# soup.descendants возвращает все дочерние узлы (теги и текст) в порядке их следования.
text_nodes = [node for node in soup.descendants
if isinstance(node, NavigableString)
# and node.strip()
and node.parent.name not in PROTECTED_HTML_TAGS]
# 1.2. Создаем "супер-строку" и "карту длин"
# 3.2. Создаем "супер-строку" и "карту длин"
super_string = ""
lengths_map = []
for node in text_nodes:
super_string += str(node)
lengths_map.append(len(str(node)))
# --- ЭТАП 2: Контекстная обработка (ПОКА ЧТО ПРОПУСКАЕМ) ---
# --- ЭТАП 4: Контекстная обработка ---
processed_super_string = super_string
# Применяем правила, которым нужен полный контекст (вся супер-строка контекста, очищенная от html).
# Важно, чтобы эти правила не меняли длину строки!!!! Иначе карта длин слетит и восстановление не получится.
@@ -211,7 +218,7 @@ class Typographer:
if self.unbreakables:
processed_super_string = self.unbreakables.process(processed_super_string)
# --- ЭТАП 3: "Восстановление" ---
# --- ЭТАП 5: Восстановление структуры ---
current_pos = 0
for i, node in enumerate(text_nodes):
length = lengths_map[i]
@@ -219,18 +226,29 @@ class Typographer:
node.replace_with(new_text_part) # Заменяем содержимое узла на месте
current_pos += length
# --- ЭТАП 4: Локальная обработка (второй проход) ---
# --- ЭТАП 6: Локальная обработка (второй проход) ---
# Теперь, когда структура восстановлена, запускаем наш старый рекурсивный обход,
# который применит все остальные правила к каждому текстовому узлу.
self._walk_tree(soup)
# --- ЭТАП 4.5: Висячая пунктуация ---
# --- ЭТАП 7: Висячая пунктуация ---
# Применяем после всех текстовых преобразований, но перед финальной сборкой
if self.hanging:
self.hanging.process(soup)
# --- ЭТАП 5: Финальная сборка ---
processed_html = str(soup)
# --- ЭТАП 8: Финальная сборка ---
if is_full_document:
# Если на входе был полноценный документ, возвращаем все дерево
processed_html = str(soup)
else:
# Если на входе был фрагмент, возвращаем только содержимое body.
# decode_contents() возвращает строку с содержимым тега (без самого тега).
# Если body нет (что странно для BS), возвращаем str(soup).
if soup.body:
processed_html = soup.body.decode_contents()
else:
processed_html = str(soup)
# BeautifulSoup по умолчанию экранирует амперсанды (& -> &amp;), которые мы сгенерировали
# в _process_text_node. Возвращаем их обратно.
return processed_html.replace('&amp;', '&')

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "etpgrf"
version = "0.1.2"
version = "0.1.3"
description = "Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and ."
readme = "README.md"
requires-python = ">=3.10"
@@ -26,7 +26,7 @@ classifiers = [
]
dependencies = [
"beautifulsoup4>=4.10.0",
"lxml>=4.9.0", # Рекомендуемый парсер
"lxml>=4.9.0", # Рекомендуемый парсер (в принципе со встроенным html.parser тоже будет работать, но медленнее)
"regex>=2022.1.18", # Критически важная зависимость для Unicode
]

View File

@@ -149,3 +149,60 @@ def test_typographer_sanitizer_all_html_integration():
typo = Typographer(langs='ru', process_html=True, sanitizer=SANITIZE_ALL_HTML, mode='mixed')
actual_text = typo.process(input_html)
assert actual_text == expected_text
# --- Новые тесты на структуру HTML (проверка отсутствия лишних оберток) ---
HTML_STRUCTURE_TEST_CASES = [
# 1. Фрагмент HTML (без html/body) -> должен остаться фрагментом
('<div>Текст</div>', '<div>Текст</div>'),
('<span>Текст</span>', '<span>Текст</span>'),
('<p>Текст</p>', '<p>Текст</p>'),
# 2. Голый текст -> должен остаться голым текстом (без <p>, <html>, <body>)
('Текст без тегов', 'Текст без&nbsp;тегов'), # Исправлено: ожидаем nbsp
('Текст с <b>тегом</b> внутри', 'Текст с&nbsp;<b>тегом</b> внутри'),
# 3. Полноценный html-документ -> должен сохранить структуру
('<html><body><p>Текст</p></body></html>', '<html><body><p>Текст</p></body></html>'),
('<!DOCTYPE html><html><head></head><body><p>Текст</p></body></html>',
'<!DOCTYPE html><html><head></head><body><p>Текст</p></body></html>'), # BS может добавить перенос строки после doctype
# 4. Кривой html -> будет "починен"
('<div>Текст', '<div>Текст</div>'),
('<p>Текст', '<p>Текст</p>'),
('Текст <b>жирный <i>курсив', 'Текст <b>жирный <i>курсив</i></b>'),
# Используем валидный HTML для теста с DOCTYPE
('<!DOCTYPE html><html><head><title>Title</title></head><body><p>Текст</p></body></html>',
'<!DOCTYPE html><html><head><title>Title</title></head><body><p>Текст</p></body></html>'),
# Тест на совсем кривой HTML (см ниже) не проходит: весь текст после незарытого <title> передается в заголовок.
# ('<!DOCTYPE html><html><head><title>Title<body><p>Текст', '<!DOCTYPE html><html><head><title>Title</title></head><body><p>Текст</p></body></html>'),
]
@pytest.mark.parametrize("input_html, expected_html", HTML_STRUCTURE_TEST_CASES)
def test_typographer_html_structure_preservation(input_html, expected_html):
"""
Проверяет, что Typographer не добавляет лишние теги (html, body, p)
вокруг фрагментов и текста, но сохраняет их, если они были.
"""
# Отключаем все "украшательства" (кавычки, неразрывные пробелы),
# чтобы проверять только структуру тегов.
typo = Typographer(
langs='ru',
process_html=True,
mode='mixed',
hyphenation=False,
quotes=False,
unbreakables=True, # Оставим unbreakables, чтобы проверить, что &nbsp; добавляются, но теги не ломаются
layout=False,
symbols=False
)
actual_html = typo.process(input_html)
# Для теста с doctype может быть нюанс с форматированием (переносы строк),
# поэтому нормализуем пробелы перед сравнением
if '<!DOCTYPE' in input_html:
assert '<html>' in actual_html
assert '<body>' in actual_html
assert '<p>Текст</p>' in actual_html
else:
assert actual_html == expected_html