mod: улучшен алгоритм висячей пунктуации для правого вывешивания. Изменения в левом вывешивании.

This commit is contained in:
2026-03-19 11:03:30 +03:00
parent 88b228050e
commit 465dd9e9e6
5 changed files with 399 additions and 58 deletions

View File

@@ -309,6 +309,9 @@ Safari), поэтому на него полагаться нельзя. Поэ
сохранить расстояние до соседнего слова. Поэтому типограф оборачивает не только сам висячий символ, но и ближайшее слово
(до пробела или границы узла), а также при необходимости, окружающий пробел. Сама визуальная компенсация оформляется через
отрицательные `margin`/`padding` в CSS-классах — никаких `position:absolute`, чтобы не нарушать поток текста.
Учтите, что набор символов, попадающих в `HANGING_PUNCTUATION_SPACE_CHARS`, помимо обычного пробела включает табуляции, переводы
строки и множество тонких/математических пробелов. Именно поэтому компенсирующие обёртки иногда захватывают
символы на границе узлов или переносов и сохраняют корректный визуальный зазор.
По умолчанию эта функция висячей типографики **отключена**. Чтобы её включить, нужно задать параметр
`hanging_punctuation` при конфигурировании типографа (по умолчанию `hanging_punctuation=None`):
@@ -332,33 +335,56 @@ typo = etpgrf.Typographer(hanging_punctuation='left')
### Как работает оборачивание
Процессор висячей типографики запускается после всех текстовых преобразований и работает с деревом BeautifulSoup. Он ищет
последовательности «пробел + висячий символ» для левого выравнивания и «слово + висячий символ + пробел» для правого,
последовательности «пробел + висячий символ» для левого выравнивания и «висячий символ + пробел» для правого,
чтобы обернуть нужные фрагменты в пары `<span>` и не допустить «сиротства» символов. Порядок действий можно описать так:
* Для `hanging_punctuation='left'`:
* если символ стоит в начале текстового узла (без пробелов слева), оборачивается только сам символ и следующее
слово (`<span class="etp-laquo">«АукЫон»</span>`);
* если перед символом внутри узла есть пробел, то пробел оборачивается в `<span class="etp-sp-laquo"> </span>`, а
символ вместе со словом — в `<span class="etp-laquo">...</span>`;
* если пробел оказался в соседнем узле, то он тоже оборачивается в `etp-sp-*`, чтобы не нарушить последовательность;
* если компенсирующий пробел является "непереносимым пробелом" (или любым другим: шпацией, em-пробелом и т.п.), то тогда, для правильного выравнивания, оборачивается он, например: `<span class="etp-sp-laquo">&nbsp;</span><span class="etp-laquo">«АукЫон»</span>`.
* если перед "висячим" символом внутри узла есть пробел, то пробел и слово слева от него оборачивается
в `<span class="etp-sp-laquo">слово </span>` (компенсирующий пробел), а сам "висячий" символ вместе со словом справа —
в `<span class="etp-laquo">...</span>`;
* если компенсирующий пробел оказался в соседнем узле (слева), то он тоже оборачивается в `etp-sp-*`, чтобы
не нарушить последовательность;
* если слева от "висячего" символа пробел является "неразрывным пробелом" (`&nbsp;`, нулевой неразрывный пробел,
узкий неразрывный пробел или любой "не пробельный" символ) — это означает, что "висячий" символ не может
"вывешиваться" в начале строки и оборачивания в `<span>` не проиходит.
* Для `hanging_punctuation='right'`:
* слово с висячим символом оборачивается в соответствующий класс (`.etp-raquo`, `.etp-rpar` и т.д.);
* пробел сразу после символа получает класс `etp-sp-raquo`, `etp-sp-rpar` и т.д., чтобы сохранить переносную ширину и
аккуратно компенсировать смещение;
* слово с "висячим символом" и слово слева оборачивается в соответствующий класс (`.etp-raquo`, `.etp-rpar` и т.д.);
* пробел сразу после символа (справа) получает класс `etp-sp-raquo`, `etp-sp-rpar` и т.д., чтобы сохранить
переносную ширину и аккуратно компенсировать смещение;
* если компенсирующий пробел оказался в соседнем узле (справа), то он тоже оборачивается в `etp-sp-*`, чтобы
не нарушить последовательность;
* если справа от "висячего" символа пробел является "неразрывным пробелом" (`&nbsp;`, нулевой неразрывный пробел,
узкий неразрывный пробел или любой "не пробельный" символ) — это означает, что "висячий" символ не может
"вывешиваться" в конце строки и оборачивания в `<span>` не проиходит.
Пример вывода для `'left'`:
```html
Завтра концерт группы<span class="etp-sp-laquo"> </span><span class="etp-laquo">«АукЫон»</span>
<span class="etp-laquo">«Все</span> обобщения опасны, включая это» (Дюма)
Завтра концерт <span class="etp-sp-laquo">группы</span><span class="etp-laquo">«Дайте</span> танк»
Если перед&nbsp;«висячим символом» стоит неразрывный пробел, он&nbsp;не&nbsp;может оказаться вначале строки.
```
Пример вывода для `'right'`:
```html
Right “long <span class="etp-rdquo">quote”</span><span class="etp-sp-rdquo">
Отсутствие смещения «висячей <span class="etp-raquo">пунктуации»</span><span class="etp-sp-raquo"> </span>внутри строки обеспечивает компенсирующий пробел справа <span class="etp-r-dot">от&nbsp;неё.</span>
Символ правой «висячей пунктуации»&nbsp;не может оказаться в конце строки, если за&nbsp;ним стоит неразрывный пробел.
```
### CSS для висячих символов
Предлагаемый, начиная с etpgrf v0.1.6, CSS теперь работает только с `margin` и `padding`, без `position:absolute`. Пробелы получают собственные
классы, поэтому их компенсация контролируется отдельно, а не встроена в сам висячий символ. Убедитесь, что эти стили
подключены к странице и не конфликтуют с `text-justify`, который вытягивает пробелы по всей строке и разрушает аккуратное
выравнивание.
Предлагаемый, начиная с etpgrf v0.1.6, CSS теперь работает только с `margin` и `padding`, без `position:absolute`.
Компенсирующие пробелы получают собственные классы, поэтому их компенсация контролируется отдельно, а не встроена
в сам висячий символ. Убедитесь, что эти стили подключены к странице и не конфликтуют с `text-justify`, который
увеличивает пробелы между словами по всей строке, делают текст менее удобным для чтения и не пригодны
для выравнивания.
```css
/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */
@@ -375,11 +401,17 @@ typo = etpgrf.Typographer(hanging_punctuation='left')
/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */
.etp-raquo { padding-right: 0.44em; margin-left: -0.44em; }
.etp-rdquo { padding-right: 0.4em; margin-left: -0.4em; }
.etp-r-comma { padding-right: 0.28em; margin-left: -0.28em; }
.etp-r-colon { padding-right: 0.32em; margin-left: -0.32em; }
.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-sp-raquo { margin-left: -0.44em; }
.etp-sp-rdquo { margin-left: -0.4em; }
.etp-sp-r-comma { margin-left: -0.28em; }
.etp-sp-r-colon { margin-left: -0.32em; }
.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; }
```