etpgrf 0.1.4
Electro-Typographer: Python library for advanced web typography (non-breaking spaces, hyphenation, hanging punctuation and .
License: MIT
Requires Python: >=3.10
Keywords: typography, html, non-breaking-spaces, hanging-punctuation, hyphenation, russian, english

# etpgrf — единый типограф для веба / effortless typography for web

[![PyPI version](https://badge.fury.io/py/etpgrf.svg)](https://badge.fury.io/py/etpgrf)
[![Python Version](https://img.shields.io/pypi/pyversions/etpgrf.svg)](https://pypi.org/project/etpgrf/)
[![License](https://img.shields.io/pypi/l/etpgrf.svg)](https://pypi.org/project/etpgrf/)


# Типограф для веба 

Экранная типографика для веба — способствует повышению читабельности текста в интернете,
приближая его к печатной типографике.

## Репозитории / Repositories

Исходный код доступен на нескольких площадках:
*   [Gitea](https://git.cube2.ru/erjemin/2025-etpgrf) (Основной self-hosted)
*   [GitHub](https://github.com/erjemin/etpgrf) (Главное зеркало & homepage/issues)
*   [GitVerse](https://gitverse.ru/erjemin/etpgrf) (Зеркало на GitVerse)

## Демострация / Demo

Работа etpgrf-типографа представлена по адресу: [typograph.cube2.ru](https://typograph.cube2.ru/). Подготовьте верстку 
вашего текста, сайтов, статей и постов к публикации в интернете за один клик.

## Установка

```bash
pip install etpgrf
```
## Быстрый старт

```python
import etpgrf
# Создаем типограф с настройками по умолчанию
typo = etpgrf.Typographer(langs='ru')
# Обрабатываем текст
result = typo.process(text="\"Пример текста для типографирования!\" - сказал он.")
print(result)
```

### Кодировки и html-мнемоники

Внутри типографа используется кодировка UTF-8. Но при использовании может быть другие кодировки (например,
для русскоязычных текстов все ещё могут использовать Windows-1251). При таких кодировках, для отображения в браузерах
некоторых специфических символов (например, кавычек, тире, стрелочек, математических символов) используют
html-мнемоники (например, `&mdash;` для длинного тире, `&laquo;` для открывающей кавычки-ёлочки и т.д.). 

tpgrf имеет три режима работы с кодировками:
- Режим `unicode` — весь вывод осуществляется в кодировке UTF-8. ВЕСЬ! Включая невидимые символы, типа неразрывных и нулевых
  пробелов, мягких переносов и т.д. Это не всегда удобно зато типографированый текст (строки) будет максимально
  компактен и занимать меньше места в памяти. В этом режиме в html-мнемоники преобразуются только опасные символы:
  * `&lt;` — знак меньше `<`;
  * `&gt;` — знак больше `>`;
  * `&amp;` — амперсанд `&`;
  * `&quot;` — двойные кавычки `"`;
  * `&apos;` — одинарные кавычки (апостроф) `'`.
- Режим `mixed` — вывод осуществляется в кодировке UTF-8, но наиболее критичные символы заменяются на html-мнемоники.
  Они невидимы или неотличимы друг от друга на экране:
  * `&shy;` — мягкий перенос (Soft Hyphen); 
  * `&nbsp;` — неразрывный пробел (Non-Breaking Space); 
  * `&ensp;` — полужирный пробел (En Space)
  * `&emsp;` — широкий пробел (Em Space) 
  * `&numsp;` — цифровой пробел;
  * `&puncsp;` — пунктуационный пробел; 
  * `&thinsp;` — межсимвольный пробел;
  * `&hairsp;` — пробел "толщина волоса" (Hair Space); 
  * `&NegativeThinSpace;` — негативный пробел (Negative Space); 
  * `&zwj;` — пробел нулевой ширины (без объединения) (Zero Width Non-Joiner);
  * `&zwnj;` — нулевая ширина (с объединением) (Zero Width Joiner);
  * `&lrm;` — изменение направления текста на слева-направо (Left-to-Right Mark);
  * `&rlm;` — изменение направления текста на направо-налево (Right-to-Left Mark);
  * `&dash;` — дефис (Hyphen);
  * `&MediumSpace;` — средний пробел (Medium Mathematical Space); 
  * `&NoBreak;` — неразрывный пробел (No-Break Space);
  * `&InvisibleTimes;` — невидимый знак умножения (Invisible Times) для семантической разметки математических
    выражений;
  * `&InvisibleComma;` — невидимая запятая (Invisible Comma) для семантической разметки математических выражений.
- Режим `mnemonic` — применяются все возможные html-мнемоники (кроме русских букв) и символов первой половины ASCII
  (плюс, минус, знак равенства, знаки препинания и т.д.).

Переключение режимов осуществляется с помощью параметра `mode` при конфигурировании типографа:
```python
# Задаем конфигурацию типографа
typo_mixed_mode = etpgrf.Typographer(mode='mixed')
# Обработка текста 
result = typo_mixed_mode.process(text="Этот текст будет обработан в режиме mixed.")
```

### ВАЖНО:

1. Если в тексте уже есть html-мнемоники, они будут преобразованы в unicode, и после обработки типографом
   будут заменены на html-мнемоники, соответствующие текущему режиму работы типографа.
2. Некоторым символам соответствуют несколько html-мнемоник. Например, `→` (стрелочка влево) может кодироваться
   как `&rarr;`, `&srarr;`, `&rightarrow`, `&RightArrow;` и `&ShortRightArrow;`. Типограф будет использовать самое
   короткое из них (для компактности), а значит:
   * если в исходном тексте были html-мнемоники, то они будут заменены на более короткие;
   * если html-мнемоники использовались как элементы семантической разметки (например, для математических выражений),
     то после замены на более короткие html-мнемоники, текст может потерять такую семантику. Например _F = A ⋂ B_:
     `F = A &Intersection; B`  будет преобразовано в `F = A &xcap; B`;
3. Мнемоники для русских букв не используются в типографе. Все мнемоники русских букв будут преобразованы в русские
   буквы и останутся в тексте в виде русских букв.
4. Все исходные html-мнемоники, которые превращаются в два unicode-символа будут превращены обратно в мнемоники каждый 
   как отдельный символ. Например, множество собственное другого подмножества `&varsubsetneq;` в unicode отображается 
   двумя символами `\u228a\ufe00` и превратится в `&subne;\ufe00`. Символ `\ufe00` — это невидимый символ, cелектор
   варианта начертания (Variant Selector), который изменяет начертание предыдущего символа и для него нет
   html-мнемоники. К счастью, в стандарте таких мнемоник (превращающихся в два символа) исчезающе мало и они крайне 
   редко применяются в тексте, поэтому это не должно вызывать проблем.


### Переносы слов

Обычно в основе переносов слов лежит фонетический принцип — деление по слогам и морфемный принцип — деление по морфемам
(приставки, корни, суффиксы, окончания). В типографе etpgrf реализован эвристический подход к переносу слов,
основанный на фонетических правилах. Он не является строгим и не учитывает все нюансы языка, но обеспечивает вполне
приемлемое качество для большинства случаев. Особенно если "неразрывные" блоки задать достаточно длинными (и именно
это и требуется от хорошего типографа, ведь перенос трех-четырех букв слова на новую строку почти не улучшит
читабельность и внешний вид текста).

Настройки по умолчанию для переноса слов (в `etpgrf.defaults`):
* Длина слова которое не подлежит переносам (`MAX_UNHYPHENATED_LEN`) — 12 символов.
* Длина части слова, которое недопустимо переносить или оставлять на строке ("хвост", "сироты")
  (`MIN_TAIL_LEN`) — 5 символов

Управление этими параметрами осуществляется через переопределение. Например:
```python
# Меняем настройки по умолчанию для переносов
etpgrf.defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN = 8
etpgrf.defaults.etpgrf_settings.hyphenation.MIN_TAIL_LEN = 4
```
Или через параметры конфигурации переносов типографа:
```python
# Определяем пользовательские правила переносов
hyphen_settings = etpgrf.Hyphenator(langs='ru', max_unhyphenated_len=8)
# Передаем их в типограф
typo_hyp = etpgrf.Typographer(langs='ru', mode='mnemonic', hyphenation=hyphen_settings)
# Обработка текста с переносами
result = typo_hyp.process(text="Электрофоретическое исследование характеризуется квинтэссенциальной значимостью!")
```

Результат обработки текста с переносами будет выглядеть так:
```html
Электрофо&shy;ретическое исследование характе&shy;ризуется квинтэс&shy;сенциальной значимостью!
```

### Предлоги, союзы и частицы

Правилом хорошего тона в любой типографике считается, когда короткие слова, такие как предлоги, союзы и частицы, 
не остаются в конце строки в одиночестве («висеть»). Это ухудшает читаемость.

Типограф `etpgrf` автоматически решает эту проблему, «приклеивая» такие слова к последующему слову с помощью
неразрывного пробела (`&nbsp;`).

*   `в доме` → `в&nbsp;доме`
*   `и сказал` → `и&nbsp;сказал`

Это правило работает для коротких слов в русском, старорусском и английском языках.

Кроме того, обрабатываются и постпозитивные частицы (например, `ли`, `же`, `бы`), которые, наоборот, для улучшения
читабельности, «приклеиваются» к предыдущему слову:

*   `сказал бы` → `сказал&nbsp;бы`


### Кавычки

В текстах кавычки бывают двух видов: «ёлочки» (для русского языка) и “лапки” (для английского языка). В типографе
реализована автоматическая замена кавычек на соответствующие типографские символы в зависимости от языка текста.

Большинство типографов при обработке кавычек находят парные (и определяют вложенность). В etpgrf же реализован
другой подход. Он ищет и обрабатывает кавычки, которые находятся рядом со словами. То есть какие-то буквы следуют
слева или справа от кавычки.

Преобразование рядом с цифрами (например, когда обозначаются дюймы (`17"`) или секунды (`3' 25"`)) не производится. Также
не обрабатываются кавычки окруженные пробелами. Все кавычки которые в исходном тексте уже были оформлены в виде
«ёлочек» или “лапок” — тоже не обрабатываются.

ВАЖНО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='Этот "текст" будет обработан без кавычек.')
```


### Компоновка (тире, диапазоны, инициалы, единицы измерения, сокращения и т.п.)

После того как псевдографика заменена на правильные символы, в дело вступает модуль компоновки (layout), который
отвечает за расстановку неразрывных и тонких пробелов. Он применяет несколько важных правил для улучшения читаемости.

#### Тире

По правилам русской типографики, длинное тире (—) должно отбиваться пробелами от соседних слов. Чтобы тире не "повисло"
в начале строки и визуально не смешивалось с диалогами, etpgrf заменяет пробел перед тире на неразрывный (&nbsp;).
  
* `слово — слово` → `слово&nbsp;— слово`
 
В английской типографике, наоборот, тире пишется слитно. Типограф учитывает это при указании языка `langs='en'`.

* `word — word` → `word—word`
 
Если минус или диапазон стоят между числами (арабскими или римскими), то это считается обозначением числового диапазона
(или отрицательным числом, или математическим выражением), и никаких изменений не производится. Неважно есть пробелы
вокруг тире/минуса или нет. Если между цифрами тире, то это тоже считается диапазоном и неразрывные пробелы не ставятся:
`1941 — 1945` → `1941 — 1945`, `-10 — -5` → `-10 — -5`,

Если минус стоит перед числом (например, `-5`), то это считается отрицательным числом, и перед ним ставится неразрывный
пробел: `от -5 до +5` → `от&nbsp;-5 до +5`.
 
#### Инициалы и акронимы

Чтобы инициалы не отрывались друг от друга и от фамилии при переносе строки, типограф расставляет между ними
специальные пробелы:
  
* Неразрывный пробел (`&nbsp;`) ставится между фамилией и инициалом/инициалами (`А. Пушкин` → `А.&nbsp;Пушкин`).
  Неважно стоят ли инициалы перед фамилией или после неё. Важно наличие точки и буквы (инициала), написанного
  с заглавной буквы.
* Тонкая шпация (&thinsp;) ставится между самими инициалами, если они написаны слитно, для улучшения внешнего вида
  (`Пушкин А. С. ` → `Пушкин&nbsp;А.&thinsp;С.`). Число инициалов не ограничено (`J.R.R. Tolkien` →
  `J.&thinsp;R.&thinsp;R.&nbsp;Tolkien`), наличие или отсутствие пробелов между инициалами в исходном тексте неважно.
* Акронимы, написанные через точку (не слитно, например, **Н.Л.О.**), разделяются так же, как инициалы, через тонкую шпацию
  (`Н.Л.О.` → `Н.&thinsp;Л.&thinsp;О.`). Наличие или отсутствие пробелов между буквами в исходном тексте неважно.

Это правило может давать побочные эффекты (в частности, тонкая шпация не является неразрывным пробелом, и в длинных
акронимах может привести к разрыву строки). Поэтому его обработку можно отключить с помощью параметра
`process_initials_and_acronyms`:
```python
typo = etpgrf.Typographer(process_initials_and_acronyms=False)
result = typo.process("А. С. Пушкин") # Останется без изменений
```

#### Единицы измерения

Типограф предотвращает отрыв единиц измерения от чисел, ставя между ним и предшествующей цифрой неразрывный пробел.
Это работает для:

*   **Простых единиц:** `100 км.` → `100&nbsp;км.`, `-5 °C` → `-5&nbsp;°C`'
*   **Составных единиц:** `120 кв.м.` → `120&nbsp;кв.&thinsp;м.`, `50 тыс. руб.` → `50&nbsp;тыс.&thinsp;руб.` Пробелы
    (есть они или нет) между составными частями единицы изменения не важны. Между частями составной единицы измерения
    ставится тонкая шпация (`&thinsp;`).
*   **Единиц с предлогом:** `№ 5` → `№&nbsp;5`, `§ 7` → `§&nbsp;7`, `$ 100` → `$&nbsp;100`
*   **Чисел, записанных и арабскими, и римскими цифрами:** `V в.н.э.` → `V&nbsp;в.&thinsp;н.&thinsp;э.`
*   Если между единицами изменений есть математические символы (например, умножение или деление):
    `10 км / ч` → `10&nbsp;км/ч` (неважно есть пробелы вокруг `/` или нет). Распознаются и другие символы:
    `·`, `*`, `×`, `÷`.

Библиотека "знает" множество стандартных единиц для русского и английского языков. Но не все. Вы можете расширить этот
список, передав свои кастомные единицы через параметр `process_units`:

```python
# Передаем список
typo = etpgrf.Typographer(process_units=['бочек', 'вёдер'])
# Можно передавать и с помощзью строки через пробелы 
typo = etpgrf.Typographer(process_units='бочек вёдер аршин сажен')
result = typo.process("Нужно 10 бочек.")       # -> "Нужно 10&nbsp;бочек."
```

Если нужно отключить распознавание и обработку единиц измерения:

```python
typo = etpgrf.Typographer(process_units=False)
result = typo.process("100 км/ч")               # Останется без изменений
```

#### Сокращения

Типограф также обрабатывает распространённые русскоязычные сокращения, чтобы они корректно отображались и не разрывались
при переносе строк. Правила делятся на два типа:
*   Финальные сокращения. Сокращения, которые обычно стоят в конце фразы (например, и т. д., и т. п.),
    обрабатываются особым образом: их части «склеиваются» тонкой шпацией, а перед всей конструкцией ставится неразрывный
    пробел, чтобы она не «повисла» на новой строке. `...и так далее, и т. д.` → `...и так далее, и&nbsp;т.&thinsp;д.`
    Это правило работает независимо от того, как сокращение было написано в исходном тексте (т.д. или т. д.).
*   Препозиционные сокращения. Сокращения, которые стоят перед другим словом (например, и. о. директора, т. е. сказать), 
    также «склеиваются» внутри, но неразрывный пробел ставится после них, чтобы привязать их к последующему слову.
    `Назначить и. о. директора` → `Назначить и.&thinsp;о.&nbsp;директора`

Библиотека знает небольшой набор самых распространённых сокращений. Но не все, а некоторые принципиально невозможны
к обработке. Например, сокращение `пр.` может оказаться как финальным (в значении «и так далее»), так и препозиционным
(в значении «профессор» или «проспект»). Так же типограф не обрабатывает сокращения, связанные с адресами (ул., д.,
кв., пл., наб. ...) так как они могут быть как финальными, так и препозиционными.

### Висячая типографика

Висячая типографика — это приём из классической вёрстки, когда некоторые знаки препинания (кавычки, скобки, иногда
тире и маркеры списков) выносятся на левое (и иногда и по правому, при выравнивании текст по правому краю) поле текста.
Это создаёт идеально ровный край не по формальным границам знаков, а по оптическому краю — по первым буквам строк.
Текст выглядит гораздо аккуратнее и профессиональнее.

Интернет публикации (да и бумажные издания) практически игнорируют висячую типографику. Но иногда это отличный
инструмент для акцентной типографики: крупные заголовки, цитаты, выносы, подписи к иллюстрациям, оформленные с помощью
висячей типографики, выглядят гораздо эффектнее. В современном CSS есть свойство `hanging-punctuation`, которое должно
делать всё это автоматически. Но на сегодняшний день (конец 2025) его поддержка браузерами практически нулевая (кроме
Safari), поэтому на него полагаться нельзя. Поэтому в типографе etpgrf реализация висячей типографики осуществляется
через оборачивание висячих символов в специальные HTML-теги с CSS-классами.

Оборачивая "висячий" символ или слово в `<span>` и применяя к нему, например, отрицательный `text-indent` или
`margin-left` (`<span style="margin-left:-0.44em;">&laquo;</span>`). Важный нюанс: степень "вывешивания" у символов
разная. И не только потому, что кавычка, скобка или точка имеют разную ширину, но еще и потому, что кавычка, например,
может "висеть" на 100% своей ширины, а точка или запятая — только на 50-70%, чтобы не отрываться от слова совсем.

По умолчанию эта функция висячей типографики **отключена**. Чтобы её включить, нужно задать параметр
`hanging_punctuation` при конфигурировании типографа (по умолчанию `hanging_punctuation=None`):

```python
typo = etpgrf.Typographer(hanging_punctuation='left')
```

Параметр `hanging_punctuation` может принимать следующие значения:
*   `None`или `False` — функция отключена (по умолчанию);
*   `'left'` — включены только левые висячие символы (выравнивание по левому краю);
*   `'right'` — включены только правые висячие символы (выравнивание по правому краю);
*   `'both'` или `True` — включены и левые, и правые висячие символы.

Так же через `hanging_punctuation` можно задать список тегов, внутри которых висячая типографика будет применяться
(всегда в режиме `'both'`). Это нерекомендованный способ (так как предполагает знание об исходном тексте, и не сработает
для сточных тегов, а поведение "блочных" и "строчных" может быть переназначено через CSS). Тем не менее управление
висячей пунктуацией на уровне тегов может быть полезно в некоторых, редких случаях:
```python
typo = etpgrf.Typographer(hanging_punctuation=['blockquote', 'h2', 'h3'])
```

Рекомендуемый CSS для этих классов выглядит так (возможно, вам придется подкорректировать значения `em` в зависимости
от используемого шрифта, его толщины и начертания):

```css
/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
.etp-laquo { margin-left: -0.44em; }                /* « */
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; }  /* “ „ */
.etp-lsquo { margin-left: -0.22em; }                /* ‘ */
.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.25em; } /* ( [ { */

/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по правому краю) --- */
/* Общая механика: "вырываем" символ из потока для идеального выравнивания текста */
[class^="etp-r"], [class*=" etp-r"]  { position: absolute; }
/* Точечная настройка смещения для каждого символа */
.etp-raquo { right: -0.44em; }                /* » */
.etp-rdquo { right: -0.4em; }                /* ” */
.etp-rsquo { right: -0.22em; }                /* ’ */
.etp-rpar, .etp-rsqb, .etp-rcub { right: -0.25em; } /* ) ] } */
.etp-r-dot, .etp-r-comma, .etp-r-colon { right: -0.15em; } /* . , : */
```


## P.S.

Если вам нравится этот проект, можете поддержать отправив любую сумму на мой Т-банк
[по ссылке](https://tbank.ru/cf/27hMw1BTFMs) или, для приверженцев децентрализованного будущего,
через Toncoin (TON) (адрес кошелька `UQApEkzNMYOg5qesWwlyfGFf4ayFyki5Mrpcd2yadgS2_1cx`)


## Credits

**Разработка:**
Проект разработан Sergei Erjemin при активном участии Gemini Assistant (LLM) в роли pair-programmer.