add: обработка кавычек
This commit is contained in:
30
README.md
30
README.md
@@ -87,7 +87,8 @@ result = typo_mixed_mode.process(text="Этот текст будет обраб
|
|||||||
|
|
||||||
Настройки по умолчанию для переноса слов (в `etpgrf.defaults`):
|
Настройки по умолчанию для переноса слов (в `etpgrf.defaults`):
|
||||||
* Длина слова которое не подлежит переносам (`MAX_UNHYPHENATED_LEN`) — 12 символов.
|
* Длина слова которое не подлежит переносам (`MAX_UNHYPHENATED_LEN`) — 12 символов.
|
||||||
* Длина части слова, которое недопустимо переносить или оставлять на строке ("хвост", "сироты") (`MIN_TAIL_LEN`) — 7 символ
|
* Длина части слова, которое недопустимо переносить или оставлять на строке ("хвост", "сироты")
|
||||||
|
(`MIN_TAIL_LEN`) — 5 символов
|
||||||
|
|
||||||
Управление этими параметрами осуществляется через переопределение. Например:
|
Управление этими параметрами осуществляется через переопределение. Например:
|
||||||
```python
|
```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.comutil import parse_and_validate_mode, parse_and_validate_langs
|
||||||
from etpgrf.hyphenation import Hyphenator
|
from etpgrf.hyphenation import Hyphenator
|
||||||
from etpgrf.unbreakables import Unbreakables
|
from etpgrf.unbreakables import Unbreakables
|
||||||
|
from etpgrf.quotes import QuotesProcessor
|
||||||
from etpgrf.codec import decode_to_unicode, encode_from_unicode
|
from etpgrf.codec import decode_to_unicode, encode_from_unicode
|
||||||
|
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ class Typographer:
|
|||||||
process_html: bool = False, # Флаг обработки HTML-тегов
|
process_html: bool = False, # Флаг обработки HTML-тегов
|
||||||
hyphenation: Hyphenator | bool | None = True, # Перенос слов и параметры расстановки переносов
|
hyphenation: Hyphenator | bool | None = True, # Перенос слов и параметры расстановки переносов
|
||||||
unbreakables: Unbreakables | bool | None = True, # Правила для предотвращения разрыва коротких слов
|
unbreakables: Unbreakables | bool | None = True, # Правила для предотвращения разрыва коротких слов
|
||||||
|
quotes: QuotesProcessor | bool | None = True, # Правила для обработки кавычек
|
||||||
# ... другие модули правил ...
|
# ... другие модули правил ...
|
||||||
):
|
):
|
||||||
|
|
||||||
@@ -56,12 +58,20 @@ class Typographer:
|
|||||||
# D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode)
|
# D2. Если unbreakables - это объект Unbreakables, то просто сохраняем его (и используем его langs и mode)
|
||||||
self.unbreakables = unbreakables
|
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. --- Логирование инициализации ---
|
# Z. --- Логирование инициализации ---
|
||||||
logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, "
|
logger.debug(f"Typographer `__init__`: langs: {self.langs}, mode: {self.mode}, "
|
||||||
f"hyphenation: {self.hyphenation is not None}, "
|
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}")
|
f"process_html: {self.process_html}")
|
||||||
|
|
||||||
|
|
||||||
@@ -75,6 +85,8 @@ class Typographer:
|
|||||||
# processed_text = text # ВРЕМЕННО: используем текст как есть
|
# processed_text = text # ВРЕМЕННО: используем текст как есть
|
||||||
|
|
||||||
# Шаг 2: Применяем правила к чистому Unicode-тексту
|
# Шаг 2: Применяем правила к чистому Unicode-тексту
|
||||||
|
if self.quotes is not None:
|
||||||
|
processed_text = self.quotes.process(processed_text)
|
||||||
if self.unbreakables is not None:
|
if self.unbreakables is not None:
|
||||||
processed_text = self.unbreakables.process(processed_text)
|
processed_text = self.unbreakables.process(processed_text)
|
||||||
if self.hyphenation is not None:
|
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