add: обработка кавычек
This commit is contained in:
30
README.md
30
README.md
@@ -87,7 +87,8 @@ result = typo_mixed_mode.process(text="Этот текст будет обраб
|
||||
|
||||
Настройки по умолчанию для переноса слов (в `etpgrf.defaults`):
|
||||
* Длина слова которое не подлежит переносам (`MAX_UNHYPHENATED_LEN`) — 12 символов.
|
||||
* Длина части слова, которое недопустимо переносить или оставлять на строке ("хвост", "сироты") (`MIN_TAIL_LEN`) — 7 символ
|
||||
* Длина части слова, которое недопустимо переносить или оставлять на строке ("хвост", "сироты")
|
||||
(`MIN_TAIL_LEN`) — 5 символов
|
||||
|
||||
Управление этими параметрами осуществляется через переопределение. Например:
|
||||
```python
|
||||
@@ -114,9 +115,30 @@ result = typo_hyp.process(text="Электрофоретическое иссл
|
||||
|
||||
### Кавычки
|
||||
|
||||
В текстах кавычки бывают двух видов: «ёлочки» (для русского языка) и “лапки” (для английского языка). В типографе
|
||||
реализована автоматическая замена кавычек на соответствующие типографские символы в зависимости от языка текста.
|
||||
|
||||
Теперь хочу обсудить модуль работы с кавычками. Большинство изсестных мне типографов разибают праные кавчки и превращают их в спец-сиволы. Мне каежтся это неверно, ведь ковычки моут быть не парными и вложенность может быть произвольной. Мне кажется нужен другой подход. Обнаруживать только конструкции `"слово` или `слово"` и преобразовывать только их. Соответсвнно в елочки или лапки в зависимоати от языка. При этом если уже в тескте есть лапки, елочки или другие типы кавычек — ничего не менять (автор текстьа сам знает, какие кавычки лучшк). Кавычки окруженные пробелами или буквать с дрвух сторон превоащать в " ...
|
||||
Большинство типографов при обработке кавычек находят парные (и определяют вложенность). В etpgrf же реализован
|
||||
другой подход. Он ищет и обрабатывает кавычки, которые находятся рядом со словами. То есть какие-то буквы следуют
|
||||
слева или справа от кавычки.
|
||||
|
||||
Как считаешь, это рабочий подход??
|
||||
Преобразование рядом с цифрами (например, когда обозначаются дюймы (`17"`) или секунды (`3' 25"`)) не производится. Также
|
||||
не обрабатываются кавычки окруженные пробелами. Все кавычки которые в исходном тексте уже были оформлены в виде
|
||||
«ёлочек» или “лапок” — тоже не обрабатываются.
|
||||
|
||||
1.Надежность (Robustness): Вы правильно заметили, что символ " используется не только для кавычек. Это могут быть дюймы (17"), секунды (3' 25"), обозначения в коде (print("Hello")) и т.д. Ваш метод, который ищет кавычку рядом со словом ("слово или слово"), с гораздо меньшей вероятностью сломает такие конструкции.2.Уважение к авторскому замыслу: Правило "если уже есть ёлочки или лапки — ничего не менять" — это золотой стандарт. Типограф не должен "исправлять" то, что уже было отформатировано автором вручную. Это предотвращает двойное преобразование и сохраняет особые случаи, задуманные автором.3.Простота и производительность: Вместо сложного и медленного парсера, который отслеживает уровни вложенности (и часто ошибается), ваш подход можно реализовать с помощью нескольких быстрых регулярных выражений.4.Безопасность: Идея превращать оставшиеся "одинокие" кавычки в " — это отличный механизм защиты. Он гарантирует, что на выходе не останется "сырых" кавычек, которые могут сломать HTML-разметку.
|
||||
ВАЖНО1: По правилам орфографии перед закрывающей кавычкой разрешены только определенные знаки препинания:
|
||||
вопросительный (?), восклицательный (!) знаки и многоточие (…). Такие конструкции используются для цитат. Это учтено
|
||||
в etpgrf, и кавычки будут обработаны: `Она воскликнула: "Какая красота!"` будет преобразовано в `Она воскликнула:
|
||||
«Какая красота!»`. В неправильны конструкциях (например, `"Какая красота."`) закрывающая кавычка не будет обработана.
|
||||
|
||||
ВАЖНО2: Если в настройке типографа указано несколько языков (`langs='ru+en'`), то кавычки будут преобразованы по правилам
|
||||
для языка который идет первым в списке. Например, для `langs='ru+en'` кавычки будут преобразованы в «ёлочки»,
|
||||
|
||||
Если при типорафировании преобразование не требуется, то можно обработку кавычек можно отключить с помощью
|
||||
параметра `quotes=False`:
|
||||
```python
|
||||
# Задаем конфигурацию типографа без кавычек
|
||||
typo_no_quotes = etpgrf.Typographer(langs='ru', quotes=False)
|
||||
# Обработка текста без кавычек
|
||||
result = typo_no_quotes.process(text='Этот "текст" будет обработан без кавычек.')
|
||||
```
|
||||
|
74
etpgrf/quotes.py
Normal file
74
etpgrf/quotes.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# etpgrf/quotes.py
|
||||
# Модуль для расстановки кавычек в тексте
|
||||
|
||||
import regex
|
||||
import logging
|
||||
from .config import LANG_RU, LANG_EN, RU_QUOT1_OPEN, RU_QUOT1_CLOSE, EN_QUOT1_OPEN, EN_QUOT1_CLOSE, \
|
||||
RU_QUOT2_OPEN, RU_QUOT2_CLOSE, EN_QUOT2_OPEN, EN_QUOT2_CLOSE
|
||||
from .comutil import parse_and_validate_langs
|
||||
|
||||
# --- Настройки логирования ---
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Определяем стили кавычек для разных языков
|
||||
# Формат: (('открывающая_ур1', 'закрывающая_ур1'), ('открывающая_ур2', 'закрывающая_ур2'))
|
||||
_QUOTE_STYLES = {
|
||||
LANG_RU: ((RU_QUOT1_OPEN, RU_QUOT1_CLOSE), (RU_QUOT2_OPEN, RU_QUOT2_CLOSE)),
|
||||
LANG_EN: ((EN_QUOT1_OPEN, EN_QUOT1_CLOSE), (EN_QUOT2_OPEN, 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 # Используем стиль первого найденного языка
|
||||
|
||||
# Паттерн для открывающей кавычки: " перед буквой/цифрой,
|
||||
# которой предшествует пробел, начало строки или открывающая скобка.
|
||||
# (?<=^|\s|[\(\[„\"‘\']) - "просмотр назад" на начало строки... ищет пробел \s или знак из набора ([„"‘'
|
||||
# (?=\p{L}) - "просмотр вперед" на букву \p{L} (но не цифру).
|
||||
self._opening_quote_pattern = regex.compile(r'(?<=^|\s|[\(\[„\"‘\'])\"(?=\p{L})')
|
||||
# self._opening_quote_pattern = regex.compile(r'(?<=^|\s|\p{Pi}|["\'\(\)])\"(?=\p{L})')
|
||||
|
||||
# Паттерн для закрывающей кавычки: " после буквы/цифры,
|
||||
# за которой следует пробел, пунктуация или конец строки.
|
||||
# (?<=\p{L}|[?!…]) - "просмотр назад" на букву или ?!…
|
||||
# (?=\s|[.,;:!?\)\"»”’]|\Z) - "просмотр вперед" на пробел, пунктуацию или конец строки (\Z).
|
||||
self._closing_quote_pattern = regex.compile(r'(?<=\p{L}|[?!…])\"(?=\s|[\.,;:!?\)\]»”’\"\']|\Z)')
|
||||
# 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
|
@@ -7,6 +7,7 @@ except ImportError:
|
||||
from etpgrf.comutil import parse_and_validate_mode, parse_and_validate_langs
|
||||
from etpgrf.hyphenation import Hyphenator
|
||||
from etpgrf.unbreakables import Unbreakables
|
||||
from etpgrf.quotes import QuotesProcessor
|
||||
from etpgrf.codec import decode_to_unicode, encode_from_unicode
|
||||
|
||||
|
||||
@@ -22,6 +23,7 @@ class Typographer:
|
||||
process_html: bool = False, # Флаг обработки HTML-тегов
|
||||
hyphenation: Hyphenator | bool | None = True, # Перенос слов и параметры расстановки переносов
|
||||
unbreakables: Unbreakables | bool | None = True, # Правила для предотвращения разрыва коротких слов
|
||||
quotes: QuotesProcessor | bool | None = True, # Правила для обработки кавычек
|
||||
# ... другие модули правил ...
|
||||
):
|
||||
|
||||
@@ -56,12 +58,20 @@ class Typographer:
|
||||
# D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode)
|
||||
self.unbreakables = unbreakables
|
||||
|
||||
# F. --- Конфигурация других правил---
|
||||
# F. --- Конфигурация правил обработки кавычек ---
|
||||
self.quotes: QuotesProcessor | None = None
|
||||
if quotes is True or quotes is None:
|
||||
self.quotes = QuotesProcessor(langs=self.langs)
|
||||
elif isinstance(quotes, QuotesProcessor):
|
||||
self.quotes = quotes
|
||||
|
||||
# G. --- Конфигурация других правил---
|
||||
|
||||
# Z. --- Логирование инициализации ---
|
||||
logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, "
|
||||
f"hyphenation: {self.hyphenation is not None}, "
|
||||
f"unbreakables: {self.unbreakables is not None}"
|
||||
f"unbreakables: {self.unbreakables is not None}, "
|
||||
f"quotes: {self.quotes is not None}, "
|
||||
f"process_html: {self.process_html}")
|
||||
|
||||
|
||||
@@ -75,6 +85,8 @@ class Typographer:
|
||||
# processed_text = text # ВРЕМЕННО: используем текст как есть
|
||||
|
||||
# Шаг 2: Применяем правила к чистому Unicode-тексту
|
||||
if self.quotes is not None:
|
||||
processed_text = self.quotes.process(processed_text)
|
||||
if self.unbreakables is not None:
|
||||
processed_text = self.unbreakables.process(processed_text)
|
||||
if self.hyphenation is not None:
|
||||
|
63
tests/test__typograph.py
Normal file
63
tests/test__typograph.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# tests/test__typograph.py
|
||||
# Тесты для модуля Typographer. Проверяют отключение модулей обработки текста.
|
||||
|
||||
import pytest
|
||||
from etpgrf.typograph import Typographer
|
||||
from etpgrf.config import SHY_CHAR, NBSP_CHAR
|
||||
|
||||
|
||||
def test_typographer_disables_quotes_processor():
|
||||
"""
|
||||
Проверяет, что при quotes=False модуль обработки кавычек отключается.
|
||||
"""
|
||||
# Arrange
|
||||
input_string = 'Текст "в кавычках", который не должен измениться.'
|
||||
# Создаем два экземпляра: с None и с False для полной проверки
|
||||
typo_false = Typographer(langs='ru', quotes=False)
|
||||
|
||||
# Act
|
||||
output_false = typo_false.process(input_string)
|
||||
|
||||
# Assert
|
||||
# 1. Проверяем внутреннее состояние: модуль действительно отключен
|
||||
assert typo_false.quotes is None
|
||||
|
||||
# 2. Проверяем результат: типографские кавычки НЕ появились в тексте.
|
||||
# Это главная и самая надежная проверка.
|
||||
assert '«' not in output_false and '»' not in output_false
|
||||
|
||||
|
||||
def test_typographer_disables_hyphenation():
|
||||
"""
|
||||
Проверяет, что при hyphenation=False модуль переносов отключается.
|
||||
"""
|
||||
# Arrange
|
||||
input_string = "Длинноесловодляпроверкипереносов"
|
||||
typo = Typographer(langs='ru', hyphenation=False)
|
||||
|
||||
# Act
|
||||
output_string = typo.process(input_string)
|
||||
|
||||
# Assert
|
||||
# 1. Проверяем внутреннее состояние
|
||||
assert typo.hyphenation is None
|
||||
# 2. Проверяем результат: в тексте не появилось символов мягкого переноса
|
||||
assert SHY_CHAR not in output_string
|
||||
|
||||
|
||||
def test_typographer_disables_unbreakables():
|
||||
"""
|
||||
Проверяет, что при unbreakables=False модуль неразрывных пробелов отключается.
|
||||
"""
|
||||
# Arrange
|
||||
input_string = "Он сказал: в дом вошла она."
|
||||
typo = Typographer(langs='ru', unbreakables=False)
|
||||
|
||||
# Act
|
||||
output_string = typo.process(input_string)
|
||||
|
||||
# Assert
|
||||
# 1. Проверяем внутреннее состояние
|
||||
assert typo.unbreakables is None
|
||||
# 2. Проверяем результат: в тексте не появилось неразрывных пробелов
|
||||
assert NBSP_CHAR not in output_string
|
74
tests/test_quotes.py
Normal file
74
tests/test_quotes.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# tests/test_quotes.py
|
||||
# Тесты для модуля QuotesProcessor. Проверяют обработку кавычек в тексте.
|
||||
|
||||
import pytest
|
||||
from etpgrf.quotes import QuotesProcessor
|
||||
|
||||
# Набор тестовых случаев в формате:
|
||||
# (язык, входная_строка, ожидаемый_результат)
|
||||
QUOTES_TEST_CASES = [
|
||||
# --- Базовые случаи ---
|
||||
('ru', 'Текст \"в кавычках\"', 'Текст «в кавычках»'),
|
||||
('en', 'Text \"in quotes\"', 'Text “in quotes”'),
|
||||
('ru', '(\"в скобках\")', '(«в скобках»)'),
|
||||
|
||||
# --- Случаи, которые не должны меняться ---
|
||||
('ru', 'Текст без кавычек.', 'Текст без кавычек.'),
|
||||
('ru', 'Уже есть «ёлочки» и „лапки“', 'Уже есть «ёлочки» и „лапки“'),
|
||||
('en', 'Already have “curly” and ‘single’ quotes', 'Already have “curly” and ‘single’ quotes'),
|
||||
|
||||
# --- Сложные случаи и исключения (QuotesProcessor должен их игнорировать) ---
|
||||
('ru', 'Размер экрана 17"', 'Размер экрана 17"'),
|
||||
('en', 'Screen size 17"', 'Screen size 17"'),
|
||||
('ru', 'Код print("Hello, World!")', 'Код print(«Hello, World!»)'), # Скобки помогают определить контекст
|
||||
('ru', ' " одинокая кавычка " ', ' " одинокая кавычка " '), # Кавычки окружены пробелами
|
||||
('ru', 'слово"вплотную', 'слово"вплотную'),
|
||||
('ru', 'вплотную"слово', 'вплотную"слово'),
|
||||
|
||||
# --- Вложенность и несколько пар ---
|
||||
('ru', 'Он сказал: "Привет, мир!"', 'Он сказал: «Привет, мир!»'),
|
||||
('ru', 'Она ответила: "И тебе "привет"!"', 'Она ответила: «И тебе «привет»!»'),
|
||||
|
||||
# --- Обработка пунктуации ---
|
||||
# Точка СНАРУЖИ кавычек - правильная пунктуация, корректно обрабатывается
|
||||
('ru', 'Текст "в кавычках".', 'Текст «в кавычках».'),
|
||||
('en', '"Word".', '“Word”.'),
|
||||
# Точка ВНУТРИ кавычек - неправильная пунктуация, закрывающая кавычка не обрабатывается (согласно README)
|
||||
('ru', 'Текст "в кавычках."', 'Текст «в кавычках."'),
|
||||
('en', '"Word."', '“Word."'),
|
||||
# Знаки ?, !, … ВНУТРИ кавычек - правильная пунктуация, корректно обрабатывается
|
||||
('ru', '"Слово?"', '«Слово?»'),
|
||||
('en', '"Word?"', '“Word?”'),
|
||||
('ru', '"Слово!"', '«Слово!»'),
|
||||
('en', '"Word!"', '“Word!”'),
|
||||
('ru', '"Слово…"', '«Слово…»'),
|
||||
('en', '"Word…"', '“Word…”'),
|
||||
# Запятая СНАРУЖИ кавычек
|
||||
('ru', '"Слово", - сказал он.', '«Слово», - сказал он.'),
|
||||
# Цифры в кавычках не обрабатываются
|
||||
('ru', 'Цифры "123" в кавычках', 'Цифры "123" в кавычках'),
|
||||
('en', 'Numbers "123" in quotes', 'Numbers "123" in quotes'),
|
||||
|
||||
# --- Языки и приоритеты ---
|
||||
(None, 'Текст "в кавычках"', 'Текст «в кавычках»'), # Язык не указан -> используется DEFAULT_LANGS[0] ('ru')
|
||||
('ru-en', 'Текст "в кавычках"', 'Текст «в кавычках»'), # 'ru' первый в списке -> используются русские кавычки
|
||||
('en-ru', 'Text "in quotes"', 'Text “in quotes”'), # 'en' первый в списке -> используются английские кавычки
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lang, input_string, expected_output", QUOTES_TEST_CASES)
|
||||
def test_quotes_processor(lang, input_string, expected_output):
|
||||
"""
|
||||
Проверяет работу QuotesProcessor в изоляции.
|
||||
"""
|
||||
# Arrange (подготовка)
|
||||
# Создаем экземпляр процессора только для этого теста
|
||||
processor = QuotesProcessor(langs=lang)
|
||||
|
||||
# Act (действие)
|
||||
# Выполняем обработку
|
||||
actual_output = processor.process(input_string)
|
||||
|
||||
# Assert (проверка)
|
||||
# Сравниваем результат с ожидаемым
|
||||
assert actual_output == expected_output
|
Reference in New Issue
Block a user