add: правило для ×

This commit is contained in:
2026-03-19 13:16:40 +03:00
parent 465dd9e9e6
commit d092cbdb5c
5 changed files with 33 additions and 5 deletions

View File

@@ -7,7 +7,9 @@
## [0.1.6] - 2024-03-19 ## [0.1.6] - 2024-03-19
### Изменено ### Изменено
- Новый алгоритм "висячей пунктуации" (HangingPunctuationProcessor). Добавлены компенсирующие пробелы для висячих символов, чтобы избежать наложения на соседние слова. Теперь "висячие символы" (кавычки, тире) оборачиваются в `<span>` вместе с ближайшим словом и пробелом, что обеспечивает корректное визуальное выравнивание внутри сторки без наложения. Режим `both` для одновременного вывешивания в обе стороны отключен из-за потенциальных конфликтов компенсирующих пробелов и проблем с выравниванием при использовании CSS text-justify. - Новый алгоритм «висячей пунктуации» (HangingPunctuationProcessor). Добавлены компенси­рующие пробелы для висячих символов, чтобы избежать наложения на соседние слова. Теперь «висячие символы» (кавычки, скобки и т. п.) обора­чиваются в `<span>` вместе с ближайшим словом и пробелом, что обеспечивает отсутствие визуальных смещений внутри строки. Режим `both` (для одновре­менного вывешивания в обе стороны) отключен из-за потенци­альных конфликтов компенси­рующих пробелов и проблем с выравни­ванием при исполь­зовании CSS `text-justify`.</span>
### Добавлено
- Автоматическая замена символов `x`, `X`, `х`, `Х`, стоящих между числами, на знак умножения `×`, чтобы выражения вида `100x100` или `100 х 100` корректно обрабатывались и выглядели типографски правильными (`100×100` или `100&nbsp;×&nbsp;100`).
## [0.1.5] - 2024-02-18 ## [0.1.5] - 2024-02-18

View File

@@ -254,6 +254,7 @@ result = typo.process("А. С. Пушкин") # Останется без изм
* Если между единицами изменений есть математические символы (например, умножение или деление): * Если между единицами изменений есть математические символы (например, умножение или деление):
`10 км / ч``10&nbsp;км/ч` (неважно есть пробелы вокруг `/` или нет). Распознаются и другие символы: `10 км / ч``10&nbsp;км/ч` (неважно есть пробелы вокруг `/` или нет). Распознаются и другие символы:
`·`, `*`, `×`, `÷`. `·`, `*`, `×`, `÷`.
* Символы `x`, `X`, `х`, `Х`, стоящие между двумя числами, заменяются на знак умножения `×`, чтобы выражения вида `100x100` или `100 х 100` корректно обрабатывались и выглядели типографски правильными (`100×100` или `100&nbsp;×&nbsp;100`).
Библиотека "знает" множество стандартных единиц для русского и английского языков. Но не все. Вы можете расширить этот Библиотека "знает" множество стандартных единиц для русского и английского языков. Но не все. Вы можете расширить этот
список, передав свои кастомные единицы через параметр `process_units`: список, передав свои кастомные единицы через параметр `process_units`:
@@ -401,18 +402,18 @@ Right “long <span class="etp-rdquo">quote”</span><span class="etp-sp-rdquo">
/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */ /* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */
.etp-raquo { padding-right: 0.44em; margin-left: -0.44em; } .etp-raquo { padding-right: 0.44em; margin-left: -0.44em; }
.etp-rdquo { padding-right: 0.4em; margin-left: -0.4em; } .etp-rdquo { padding-right: 0.4em; margin-left: -0.4em; }
.etp-rsquo { padding-right: 0.22em; margin-left: -0.22em; }
.etp-r-comma { padding-right: 0.28em; margin-left: -0.28em; } .etp-r-comma { padding-right: 0.28em; margin-left: -0.28em; }
.etp-r-colon { padding-right: 0.32em; margin-left: -0.32em; } .etp-r-colon { padding-right: 0.32em; margin-left: -0.32em; }
.etp-r-dot { padding-right: 0.12em; margin-left: -0.12em; } .etp-r-dot { padding-right: 0.12em; margin-left: -0.12em; }
.etp-rsquo { padding-right: 0.22em; margin-left: -0.22em; }
.etp-rpar, .etp-rsqb, .etp-rcub { padding-right: 0.25em; margin-left: -0.25em; } .etp-rpar, .etp-rsqb, .etp-rcub { padding-right: 0.25em; margin-left: -0.25em; }
/* компенсирующие пробелы для правых висячих символов */ /* компенсирующие пробелы для правых висячих символов */
.etp-sp-raquo { margin-left: -0.44em; } .etp-sp-raquo { margin-left: -0.44em; }
.etp-sp-rdquo { margin-left: -0.4em; } .etp-sp-rdquo { margin-left: -0.4em; }
.etp-sp-rsquo { margin-left: -0.22em; }
.etp-sp-r-comma { margin-left: -0.28em; } .etp-sp-r-comma { margin-left: -0.28em; }
.etp-sp-r-colon { margin-left: -0.32em; } .etp-sp-r-colon { margin-left: -0.32em; }
.etp-sp-r-dot { margin-left: -0.12em; } .etp-sp-r-dot { margin-left: -0.12em; }
.etp-sp-rsquo { margin-left: -0.22em; }
.etp-sp-rpar, .etp-sp-rsqb, .etp-sp-rcub { margin-left: -0.25em; } .etp-sp-rpar, .etp-sp-rsqb, .etp-sp-rcub { margin-left: -0.25em; }
``` ```

View File

@@ -797,6 +797,7 @@ HANGING_PUNCTUATION_SYMBOLS_CLASSES = {
# Левая пунктуация: все классы начинаются с 'etp-l' # Левая пунктуация: все классы начинаются с 'etp-l'
CHAR_RU_QUOT1_OPEN: 'etp-laquo', # ` «` -- левая открывающая кавычка-ёлочка CHAR_RU_QUOT1_OPEN: 'etp-laquo', # ` «` -- левая открывающая кавычка-ёлочка
CHAR_EN_QUOT1_OPEN: 'etp-ldquo', # ` “` -- левая открывающая кавычка-лапка CHAR_EN_QUOT1_OPEN: 'etp-ldquo', # ` “` -- левая открывающая кавычка-лапка
CHAR_EN_QUOT2_OPEN: 'etp-lsquo', # ` ` -- левая открывающая кавычка-апостроф (одинарная)
CHAR_LPAR: 'etp-lpar', # ` (` -- левая открывающая скобка CHAR_LPAR: 'etp-lpar', # ` (` -- левая открывающая скобка
CHAR_LSQB: 'etp-lsqb', # ` [` -- левая открывающая квадратная скобка CHAR_LSQB: 'etp-lsqb', # ` [` -- левая открывающая квадратная скобка
CHAR_LCUB: 'etp-lcub', # ` {` -- левая открывающая фигурная скобка CHAR_LCUB: 'etp-lcub', # ` {` -- левая открывающая фигурная скобка
@@ -805,6 +806,7 @@ HANGING_PUNCTUATION_SYMBOLS_CLASSES = {
# Правая пунктуация: все классы начинаются с 'etp-r' # Правая пунктуация: все классы начинаются с 'etp-r'
CHAR_RU_QUOT1_CLOSE: 'etp-raquo', # `» ` -- правая закрывающая кавычка-ёлочка CHAR_RU_QUOT1_CLOSE: 'etp-raquo', # `» ` -- правая закрывающая кавычка-ёлочка
CHAR_EN_QUOT1_CLOSE: 'etp-rdquo', # `” ` -- правая закрывающая кавычка-лапка CHAR_EN_QUOT1_CLOSE: 'etp-rdquo', # `” ` -- правая закрывающая кавычка-лапка
CHAR_EN_QUOT2_CLOSE: 'etp-rsquo', # ` ` -- правая закрывающая кавычка-апостроф (одинарная)
CHAR_RPAR: 'etp-rpar', # `) ` -- правая закрывающая скобка CHAR_RPAR: 'etp-rpar', # `) ` -- правая закрывающая скобка
CHAR_RSQB: 'etp-rsqb', # `] ` -- правая закрывающая квадратная скобка CHAR_RSQB: 'etp-rsqb', # `] ` -- правая закрывающая квадратная скобка
CHAR_RCUB: 'etp-rcub', # `} ` -- правая закрывающая фигурная скобка CHAR_RCUB: 'etp-rcub', # `} ` -- правая закрывающая фигурная скобка
@@ -824,6 +826,7 @@ HANGING_PUNCTUATION_SPACE_CLASSES = {
# Для левой пунктуации (компенсационный пробел слева от висячей пунктуации) # Для левой пунктуации (компенсационный пробел слева от висячей пунктуации)
CHAR_RU_QUOT1_OPEN: 'etp-sp-laquo', # ` «` -- для пробела пред открывающей кавычкой-ёлочкой CHAR_RU_QUOT1_OPEN: 'etp-sp-laquo', # ` «` -- для пробела пред открывающей кавычкой-ёлочкой
CHAR_EN_QUOT1_OPEN: 'etp-sp-ldquo', # ` “` -- для пробела пред открывающей кавычкой-лапкой CHAR_EN_QUOT1_OPEN: 'etp-sp-ldquo', # ` “` -- для пробела пред открывающей кавычкой-лапкой
CHAR_EN_QUOT2_OPEN: 'etp-sp-lsquo', # ` ` -- для пробела пред открывающей кавычкой-апострофом (одинарной)
CHAR_LPAR: 'etp-sp-lpar', # ` (` -- для пробела пред левой открывающей скобкой CHAR_LPAR: 'etp-sp-lpar', # ` (` -- для пробела пред левой открывающей скобкой
CHAR_LSQB: 'etp-sp-lsqb', # ` [` -- для пробела пред левой открывающей квадратной скобкой CHAR_LSQB: 'etp-sp-lsqb', # ` [` -- для пробела пред левой открывающей квадратной скобкой
CHAR_LCUB: 'etp-sp-lcub', # ` {` -- для пробела пред левой открывающей фигурной скобкой CHAR_LCUB: 'etp-sp-lcub', # ` {` -- для пробела пред левой открывающей фигурной скобкой
@@ -832,6 +835,7 @@ HANGING_PUNCTUATION_SPACE_CLASSES = {
# Для правой пунктуации (компенсационный пробел справа от висячей пунктуации) # Для правой пунктуации (компенсационный пробел справа от висячей пунктуации)
CHAR_RU_QUOT1_CLOSE: 'etp-sp-raquo', # `» ` -- для пробела после закрывающей кавычки-ёлочки CHAR_RU_QUOT1_CLOSE: 'etp-sp-raquo', # `» ` -- для пробела после закрывающей кавычки-ёлочки
CHAR_EN_QUOT1_CLOSE: 'etp-sp-rdquo', # `” ` -- для пробела после закрывающей кавычки-лапки CHAR_EN_QUOT1_CLOSE: 'etp-sp-rdquo', # `” ` -- для пробела после закрывающей кавычки-лапки
CHAR_EN_QUOT2_CLOSE: 'etp-sp-rsquo', # ` ` -- для пробела после закрывающей кавычки-апострофом (одинарной)
CHAR_RPAR: 'etp-sp-rpar', # `) ` -- для пробела после правой закрывающей скобки CHAR_RPAR: 'etp-sp-rpar', # `) ` -- для пробела после правой закрывающей скобки
CHAR_RSQB: 'etp-sp-rsqb', # `] ` -- для пробела после правой закрывающей квадратной скобки CHAR_RSQB: 'etp-sp-rsqb', # `] ` -- для пробела после правой закрывающей квадратной скобки
CHAR_RCUB: 'etp-sp-rcub', # `} ` -- для пробела после правой закрывающей фигурной скобки CHAR_RCUB: 'etp-sp-rcub', # `} ` -- для пробела после правой закрывающей фигурной скобки

View File

@@ -3,7 +3,7 @@
import regex import regex
import logging import logging
from .config import CHAR_NDASH, STR_TO_SYMBOL_REPLACEMENTS from .config import CHAR_NDASH, CHAR_NBSP, CHAR_TIMES, STR_TO_SYMBOL_REPLACEMENTS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -20,6 +20,7 @@ class SymbolsProcessor:
# Паттерн для диапазонов: цифра-дефис-цифра -> цифра–цифра (среднее тире). # Паттерн для диапазонов: цифра-дефис-цифра -> цифра–цифра (среднее тире).
# Обрабатываем арабские и римские цифры. # Обрабатываем арабские и римские цифры.
self._range_pattern = regex.compile(pattern=r'(\d)-(\d)|([IVXLCDM]+)-([IVXLCDM]+)', flags=regex.IGNORECASE) self._range_pattern = regex.compile(pattern=r'(\d)-(\d)|([IVXLCDM]+)-([IVXLCDM]+)', flags=regex.IGNORECASE)
self._times_pattern = regex.compile(pattern=r'(?<=\d)(?P<pre>\s*)(?P<letter>[xхXХ])(?P<post>\s*)(?=\d)')
logger.debug("SymbolsProcessor `__init__`") logger.debug("SymbolsProcessor `__init__`")
@@ -31,6 +32,14 @@ class SymbolsProcessor:
return f'{match.group(3)}{CHAR_NDASH}{match.group(4)}' return f'{match.group(3)}{CHAR_NDASH}{match.group(4)}'
return match.group(0) # На всякий случай return match.group(0) # На всякий случай
def _replace_times(self, match: regex.Match) -> str:
# Встраивает CHAR_TIMES между цифрами и защищает его от переноса
pre = match.group('pre')
post = match.group('post')
before = CHAR_NBSP if pre else ''
after = CHAR_NBSP if post else ''
return f'{before}{CHAR_TIMES}{after}'
def process(self, text: str) -> str: def process(self, text: str) -> str:
# Шаг 1: Выполняем простые замены из списка `STR_TO_SYMBOL_REPLACEMENTS` (см. config.py). # Шаг 1: Выполняем простые замены из списка `STR_TO_SYMBOL_REPLACEMENTS` (см. config.py).
@@ -45,6 +54,7 @@ class SymbolsProcessor:
# Шаг 2: Обрабатываем диапазоны с помощью регулярного выражения. # Шаг 2: Обрабатываем диапазоны с помощью регулярного выражения.
# Эта замена более специфична и требует контекста (цифры вокруг дефиса). # Эта замена более специфична и требует контекста (цифры вокруг дефиса).
processed_text = self._range_pattern.sub(self._replace_range, processed_text) processed_text = self._range_pattern.sub(self._replace_range, processed_text)
processed_text = self._times_pattern.sub(self._replace_times, processed_text)
return processed_text return processed_text

View File

@@ -8,6 +8,7 @@ from etpgrf.config import (
CHAR_TRADE, CHAR_AP, CHAR_ARROW_L, CHAR_ARROW_R, CHAR_ARROW_LR, CHAR_TRADE, CHAR_AP, CHAR_ARROW_L, CHAR_ARROW_R, CHAR_ARROW_LR,
CHAR_ARROW_L_DOUBLE, CHAR_ARROW_R_DOUBLE, CHAR_ARROW_LR_DOUBLE, CHAR_ARROW_L_DOUBLE, CHAR_ARROW_R_DOUBLE, CHAR_ARROW_LR_DOUBLE,
CHAR_ARROW_L_LONG_DOUBLE, CHAR_ARROW_R_LONG_DOUBLE, CHAR_ARROW_LR_LONG_DOUBLE, CHAR_ARROW_L_LONG_DOUBLE, CHAR_ARROW_R_LONG_DOUBLE, CHAR_ARROW_LR_LONG_DOUBLE,
CHAR_NBSP,
) )
SYMBOLS_TEST_CASES = [ SYMBOLS_TEST_CASES = [
@@ -50,7 +51,17 @@ SYMBOLS_TEST_CASES = [
("I-V век", f"I{CHAR_NDASH}V век"), ("I-V век", f"I{CHAR_NDASH}V век"),
("ix-vi до н.э.", f"ix{CHAR_NDASH}vi до н.э."), ("ix-vi до н.э.", f"ix{CHAR_NDASH}vi до н.э."),
# 3. --- Комбинированные и пограничные случаи --- # 3. --- Проверка замены `x`, `X`, `х` и `Х` на `×` ---
("222 x 333 = 73926", f"222{CHAR_NBSP}×{CHAR_NBSP}333 = 73926"),
("222 X 333 = 73926", f"222{CHAR_NBSP}×{CHAR_NBSP}333 = 73926"),
("222 х 333 = 73926", f"222{CHAR_NBSP}×{CHAR_NBSP}333 = 73926"), # русская х
("222 Х 333 = 73926", f"222{CHAR_NBSP}×{CHAR_NBSP}333 = 73926"), # русская Х
("Размер 5x10 см", f"Размер 5×10 см"),
("Размер 5X10 см", f"Размер 5×10 см"),
("Размер 5х10 см", f"Размер 5×10 см"), # русская х
("Размер 5Х10 см", f"Размер 5×10 см"), # русская Х
# 4. --- Комбинированные и пограничные случаи ---
# Сначала сработает простая замена '---' -> '—', потом диапазон '1-5' -> '15' # Сначала сработает простая замена '---' -> '—', потом диапазон '1-5' -> '15'
("1-5 --- это диапазон (c)", f"1{CHAR_NDASH}5 {CHAR_MDASH} это диапазон {CHAR_COPY}"), ("1-5 --- это диапазон (c)", f"1{CHAR_NDASH}5 {CHAR_MDASH} это диапазон {CHAR_COPY}"),
# Простая замена '--' -> '' не должна мешать диапазону '1-5' # Простая замена '--' -> '' не должна мешать диапазону '1-5'