etpgrf — единый типограф для веба / effortless typography for web
Типограф для веба
Экранная типографика для веба — способствует повышению читабельности текста в интернете, приближая его к печатной типографике.
Репозитории / Repositories
Исходный код доступен на нескольких площадках:
Демострация / Demo
Работа etpgrf-типографа представлена по адресу: typograph.cube2.ru. Подготовьте верстку вашего текста, сайтов, статей и постов к публикации в интернете за один клик.
Установка
pip install etpgrf
Быстрый старт
import etpgrf
# Создаем типограф с настройками по умолчанию
typo = etpgrf.Typographer(langs='ru')
# Обрабатываем текст
result = typo.process(text="\"Пример текста для типографирования!\" - сказал он.")
print(result)
Кодировки и html-мнемоники
Внутри типографа используется кодировка UTF-8. Но при использовании может быть другие кодировки (например,
для русскоязычных текстов все ещё могут использовать Windows-1251). При таких кодировках, для отображения в браузерах
некоторых специфических символов (например, кавычек, тире, стрелочек, математических символов) используют
html-мнемоники (например, — для длинного тире, « для открывающей кавычки-ёлочки и т.д.).
tpgrf имеет три режима работы с кодировками:
- Режим
unicode— весь вывод осуществляется в кодировке UTF-8. ВЕСЬ! Включая невидимые символы, типа неразрывных и нулевых пробелов, мягких переносов и т.д. Это не всегда удобно зато типографированый текст (строки) будет максимально компактен и занимать меньше места в памяти. В этом режиме в html-мнемоники преобразуются только опасные символы:<— знак меньше<;>— знак больше>;&— амперсанд&;"— двойные кавычки";'— одинарные кавычки (апостроф)'.
- Режим
mixed— вывод осуществляется в кодировке UTF-8, но наиболее критичные символы заменяются на html-мнемоники. Они невидимы или неотличимы друг от друга на экране:­— мягкий перенос (Soft Hyphen); — неразрывный пробел (Non-Breaking Space); — полужирный пробел (En Space) — широкий пробел (Em Space) — цифровой пробел; — пунктуационный пробел; — межсимвольный пробел; — пробел "толщина волоса" (Hair Space);​— негативный пробел (Negative Space);‍— пробел нулевой ширины (без объединения) (Zero Width Non-Joiner);‌— нулевая ширина (с объединением) (Zero Width Joiner);‎— изменение направления текста на слева-направо (Left-to-Right Mark);‏— изменение направления текста на направо-налево (Right-to-Left Mark);‐— дефис (Hyphen); — средний пробел (Medium Mathematical Space);⁠— неразрывный пробел (No-Break Space);⁢— невидимый знак умножения (Invisible Times) для семантической разметки математических выражений;⁣— невидимая запятая (Invisible Comma) для семантической разметки математических выражений.
- Режим
mnemonic— применяются все возможные html-мнемоники (кроме русских букв) и символов первой половины ASCII (плюс, минус, знак равенства, знаки препинания и т.д.).
Переключение режимов осуществляется с помощью параметра mode при конфигурировании типографа:
# Задаем конфигурацию типографа
typo_mixed_mode = etpgrf.Typographer(mode='mixed')
# Обработка текста
result = typo_mixed_mode.process(text="Этот текст будет обработан в режиме mixed.")
ВАЖНО:
- Если в тексте уже есть html-мнемоники, они будут преобразованы в unicode, и после обработки типографом будут заменены на html-мнемоники, соответствующие текущему режиму работы типографа.
- Некоторым символам соответствуют несколько html-мнемоник. Например,
→(стрелочка влево) может кодироваться как→,→,&rightarrow,→и→. Типограф будет использовать самое короткое из них (для компактности), а значит:- если в исходном тексте были html-мнемоники, то они будут заменены на более короткие;
- если html-мнемоники использовались как элементы семантической разметки (например, для математических выражений),
то после замены на более короткие html-мнемоники, текст может потерять такую семантику. Например F = A ⋂ B:
F = A ⋂ Bбудет преобразовано вF = A ⋂ B;
- Мнемоники для русских букв не используются в типографе. Все мнемоники русских букв будут преобразованы в русские буквы и останутся в тексте в виде русских букв.
- Все исходные html-мнемоники, которые превращаются в два unicode-символа будут превращены обратно в мнемоники каждый
как отдельный символ. Например, множество собственное другого подмножества
⊊︀в unicode отображается двумя символами\u228a\ufe00и превратится в⊊\ufe00. Символ\ufe00— это невидимый символ, cелектор варианта начертания (Variant Selector), который изменяет начертание предыдущего символа и для него нет html-мнемоники. К счастью, в стандарте таких мнемоник (превращающихся в два символа) исчезающе мало и они крайне редко применляются в тексте, поэтому это не должно вызывать проблем.
Переносы слов
Обычно в основе переносов слов лежит фонетический принцип — деление по слогам и морфемный принцип — деление по морфемам (приставки, корни, суффиксы, окончания). В типографе etpgrf реализован эвристический подход к переносу слов, основанный на фонетических правилах. Он не является строгим и не учитывает все нюансы языка, но обеспечивает вполне приемлемое качество для большинства случаев. Особенно если "неразрывные" блоки задать достаточно длинными (и именно это и требуется от хорошего типографа, ведь перенос трех-четырех букв слова на новую строку почти не улучшит читабельность и внешний вид текста).
Настройки по умолчанию для переноса слов (в etpgrf.defaults):
- Длина слова которое не подлежит переносам (
MAX_UNHYPHENATED_LEN) — 12 символов. - Длина части слова, которое недопустимо переносить или оставлять на строке ("хвост", "сироты")
(
MIN_TAIL_LEN) — 5 символов
Управление этими параметрами осуществляется через переопределение. Например:
# Меняем настройки по умолчанию для переносов
etpgrf.defaults.etpgrf_settings.hyphenation.MAX_UNHYPHENATED_LEN = 8
etpgrf.defaults.etpgrf_settings.hyphenation.MIN_TAIL_LEN = 4
Или через параметры конфигурации переносов типографа:
# Определяем пользовательские правила переносов
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="Электрофоретическое исследование характеризуется квинтэссенциальной значимостью!")
Результат обработки текста с переносами будет выглядеть так:
Электрофо­ретическое исследование характе­ризуется квинтэс­сенциальной значимостью!
Предлоги, союзы и частицы
Правилом хорошего тона в любой типографике считается, когда короткие слова, такие как предлоги, союзы и частицы, не остаются в конце строки в одиночестве («висеть»). Это ухудшает читаемость.
Типограф etpgrf автоматически решает эту проблему, «приклеивая» такие слова к последующему слову с помощью
неразрывного пробела ( ).
в доме→в домеи сказал→и сказал
Это правило работает для коротких слов в русском, старорусском и английском языках.
Кроме того, обрабатываются и постпозитивные частицы (например, ли, же, бы), которые, наоборот, для улучшения
читабельности, «приклеиваются» к предыдущему слову:
сказал бы→сказал бы
Кавычки
В текстах кавычки бывают двух видов: «ёлочки» (для русского языка) и “лапки” (для английского языка). В типографе реализована автоматическая замена кавычек на соответствующие типографские символы в зависимости от языка текста.
Большинство типографов при обработке кавычек находят парные (и определяют вложенность). В etpgrf же реализован другой подход. Он ищет и обрабатывает кавычки, которые находятся рядом со словами. То есть какие-то буквы следуют слева или справа от кавычки.
Преобразование рядом с цифрами (например, когда обозначаются дюймы (17") или секунды (3' 25")) не производится. Также
не обрабатываются кавычки окруженные пробелами. Все кавычки которые в исходном тексте уже были оформлены в виде
«ёлочек» или “лапок” — тоже не обрабатываются.
ВАЖНО1: По правилам орфографии перед закрывающей кавычкой разрешены только определенные знаки препинания:
вопросительный (?), восклицательный (!) знаки и многоточие (…). Такие конструкции используются для цитат. Это учтено
в etpgrf, и кавычки будут обработаны: Она воскликнула: "Какая красота!" будет преобразовано в Она воскликнула: «Какая красота!». По правилам пунктуации, точка . перед закрывающей кавычкой не допускается, но существуют
исключения, когда перед кавычкой стоит сокращение (например, т. д., и т. п.). В таких случаях точка сохраняется:
Он сказал: "Это важно, и т. д." → Он сказал: «Это важно, и т. д.». Типограф допускает точку перед закрывающей
кавычкой.
ВАЖНО2: Если в настройке типографа указано несколько языков (langs='ru+en'), то кавычки будут преобразованы по правилам
для языка который идет первым в списке. Например, для langs='ru+en' кавычки будут преобразованы в «ёлочки»,
Если при типорафировании преобразование не требуется, то можно обработку кавычек можно отключить с помощью
параметра quotes=False:
# Задаем конфигурацию типографа без кавычек
typo_no_quotes = etpgrf.Typographer(langs='ru', quotes=False)
# Обработка текста без кавычек
result = typo_no_quotes.process(text='Этот "текст" будет обработан без кавычек.')
Компоновка (тире, диапазоны, инициалы, единицы измерения, сокращения и т.п.)
После того как псевдографика заменена на правильные символы, в дело вступает модуль компоновки (layout), который отвечает за расстановку неразрывных и тонких пробелов. Он применяет несколько важных правил для улучшения читаемости.
Тире
По правилам русской типографики, длинное тире (—) должно отбиваться пробелами от соседних слов. Чтобы тире не "повисло" в начале строки и визуально не смешивалось с диалогами, etpgrf заменяет пробел перед тире на неразрывный ( ).
слово — слово→слово — слово
В английской типографике, наоборот, тире пишется слитно. Типограф учитывает это при указании языка langs='en'.
word — word→word—word
Если минус или диапазон стоят между числами (арабскими или римскими), то это считается обозначением числового диапазона
(или отрицательным числом, или математическим выражением), и никаких изменений не производится. Неважно есть пробелы
вокруг тире/минуса или нет. Если между цифрами тире, то это тоже считается диапазоном и неразрывные пробелы не ставятся:
1941 — 1945 → 1941 — 1945, -10 — -5 → -10 — -5,
Если минус стоит перед числом (например, -5), то это считается отрицательным числом, и перед ним ставится неразрывный
пробел: от -5 до +5 → от -5 до +5.
Инициалы и акронимы
Чтобы инициалы не отрывались друг от друга и от фамилии при переносе строки, типограф расставляет между ними специальные пробелы:
- Неразрывный пробел (
) ставится между фамилией и инициалом/инициалами (А. Пушкин→А. Пушкин). Неважно стоят ли инициалы перед фамилией или после неё. Важно наличие точки и буквы (инициала), написанного с заглавной буквы. - Тонкая шпация ( ) ставится между самими инициалами, если они написаны слитно, для улучшения внешнего вида
(
Пушкин А. С.→Пушкин А. С.). Число инициалов не ограничено (J.R.R. Tolkien→J. R. R. Tolkien), наличие или отсутствие пробелов между инициалами в исходном тексте неважно. - Акронимы, написанные через точку (не слитно, например, Н.Л.О.), разделяются так же, как инициалы, через тонкую шпацию
(
Н.Л.О.→Н. Л. О.). Наличие или отсутствие пробелов между буквами в исходном тексте неважно.
Это правило может давать побочные эффекты (в частности, тонкая шпация не является неразрывным пробелом, и в длинных
акронимах может привести к разрыву строки). Поэтому его обработку можно отключить с помощью параметра
process_initials_and_acronyms:
typo = etpgrf.Typographer(process_initials_and_acronyms=False)
result = typo.process("А. С. Пушкин") # Останется без изменений
Единицы измерения
Типограф предотвращает отрыв единиц измерения от чисел, ставя между ним и предшествующей цифрой неразрывный пробел. Это работает для:
- Простых единиц:
100 км.→100 км.,-5 °C→-5 °C' - Составных единиц:
120 кв.м.→120 кв. м.,50 тыс. руб.→50 тыс. руб.Пробелы (есть они или нет) между составными частями единицы изменения не важны. Между частями составной единицы измерения ставится тонкая шпация ( ). - Единиц с предлогом:
№ 5→№ 5,§ 7→§ 7,$ 100→$ 100 - Чисел, записанных и арабскими, и римскими цифрами:
V в.н.э.→V в. н. э. - Если между единицами изменений есть математические символы (например, умножение или деление):
10 км / ч→10 км/ч(неважно есть пробелы вокруг/или нет). Распознаются и другие символы:·,*,×,÷.
Библиотека "знает" множество стандартных единиц для русского и английского языков. Но не все. Вы можете расширить этот
список, передав свои кастомные единицы через параметр process_units:
# Передаем список
typo = etpgrf.Typographer(process_units=['бочек', 'вёдер'])
# Можно передавать и с помощзью строки через пробелы
typo = etpgrf.Typographer(process_units='бочек вёдер аршин сажен')
result = typo.process("Нужно 10 бочек.") # -> "Нужно 10 бочек."
Если нужно отключить распознавание и обработку единиц измерения:
typo = etpgrf.Typographer(process_units=False)
result = typo.process("100 км/ч") # Останется без изменений
Сокращения
Типограф также обрабатывает распространённые русскоязычные сокращения, чтобы они корректно отображались и не разрывались при переносе строк. Правила делятся на два типа:
- Финальные сокращения. Сокращения, которые обычно стоят в конце фразы (например, и т. д., и т. п.),
обрабатываются особым образом: их части «склеиваются» тонкой шпацией, а перед всей конструкцией ставится неразрывный
пробел, чтобы она не «повисла» на новой строке.
...и так далее, и т. д.→...и так далее, и т. д.Это правило работает независимо от того, как сокращение было написано в исходном тексте (т.д. или т. д.). - Препозиционные сокращения. Сокращения, которые стоят перед другим словом (например, и. о. директора, т. е. сказать),
также «склеиваются» внутри, но неразрывный пробел ставится после них, чтобы привязать их к последующему слову.
Назначить и. о. директора→Назначить и. о. директора
Библиотека знает небольшой набор самых распространённых сокращений. Но не все, а некоторые принципиально невозможны
к обработке. Например, сокращение пр. может оказаться как финальным (в значении «и так далее»), так и препозиционным
(в значении «профессор» или «проспект»). Так же типограф не обрабатывает сокращения, связанные с адресами (ул., д.,
кв., пл., наб. ...) так как они могут быть как финальными, так и препозиционными.
Висячая типографика
Висячая типографика — это приём из классической вёрстки, когда некоторые знаки препинания (кавычки, скобки, иногда tире и маркеры списков) выносятся на левое (и иногда и по правому) поле текста. Это создаёт идеально ровный край не по формальным границам знаков, а по оптическому краю — по первым буквам строк. Текст выглядит гораздо аккуратнее и профессиональнее.
Интернет публикации (да и бумажные издания) практически игнорируют висячую типографику. Но иногда это отличный
инструмент для акцентной типографики: крупные заголовки, цитаты, выносы, подписи к иллюстрациям, оформленные с помощью
висячей типографики, выглядят гораздо эффектнее. В современном CSS есть свойство hanging-punctuation, которое должно
делать всё это автоматически. Но на сегодняшний день (конец 2025) его поддержка браузерами практически нулевая (кроме
Safari), поэтому на него полагаться нельзя. Поэтому в типографе etpgrf реализация висячей типографики осуществляется
через оборачивание висячих символов в специальные HTML-теги с CSS-классами.
Оборачивая "висячий" символ или слово в <span> и применяя к нему, например, отрицательный text-indent или
margin-left (<span style="margin-left:-0.44em">«</span>), мы можем сместить сам символ, но нужно ещё и
сохранить расстояние до соседнего слова. Поэтому типограф оборачивает не только сам висячий символ, но и ближайшее слово
(до пробела или границы узла), а также, при необходимости, окружающий пробел. Сама визуальная компенсация оформляется через
отрицательные margin/padding в CSS-классах — никаких position:absolute, чтобы не нарушать поток текста.
По умолчанию эта функция висячей типографики отключена. Чтобы её включить, нужно задать параметр
hanging_punctuation при конфигурировании типографа (по умолчанию hanging_punctuation=None):
typo = etpgrf.Typographer(hanging_punctuation='left')
Параметр hanging_punctuation может принимать следующие значения:
NoneилиFalse— функция отключена (по умолчанию);'left'илиTrue— включены только левые висячие символы (выравнивание по левому краю);'right'— включены только правые висячие символы (выравнивание по правому краю).
Значения 'both' недоступно, потому что совмещение левой и правой выверки одновременно приводит к конфликтам
с размещением пробелов и делает невозможным контролировать визуальное выравнивание (см. блок про text-justify).
Также через hanging_punctuation можно задать список тегов, внутри которых висячая типографика будет применяться
(всегда в режиме 'both'). Это нерекомендованный способ, потому что он предполагает знание структуры HTML и неизбежно
выпадает из общей логики вложенности и пробельных узлов.
Как работает оборачивание
Процессор висячей типографики запускается после всех текстовых преобразований и работает с деревом 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"> </span><span class="etp-laquo">«АукЫон»</span>.
- если символ стоит в начале текстового узла (без пробелов слева), оборачивается только сам символ и следующее
слово (
- Для
hanging_punctuation='right':- слово с висячим символом оборачивается в соответствующий класс (
.etp-raquo,.etp-rparи т.д.); - пробел сразу после символа получает класс
etp-sp-raquo,etp-sp-rparи т.д., чтобы сохранить переносную ширину и аккуратно компенсировать смещение;
- слово с висячим символом оборачивается в соответствующий класс (
Пример вывода для 'left':
Завтра концерт группы<span class="etp-sp-laquo"> </span><span class="etp-laquo">«АукЫон»</span>
CSS для висячих символов
Предлагаемый CSS теперь работает только с margin и padding, без position:absolute. Пробелы получают собственные
классы, поэтому их компенсация контролируется отдельно, а не встроена в сам висячий символ. Убедитесь, что эти стили
подключены к странице и не конфликтуют с text-justify, который вытягивает пробелы по всей строке и разрушает аккуратное
выравнивание.
/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */
.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; }
/* компенсирующие пробелы для левых висячих символов */
.etp-sp-laquo { padding-right: 0.44em; }
.etp-sp-ldquo, .etp-sp-bdquo { padding-right: 0.4em; }
.etp-sp-lsquo { padding-right: 0.22em; }
.etp-sp-lpar, .etp-sp-lsqb, .etp-sp-lcub { padding-right: 0.25em; }
/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ --- */
.etp-raquo { padding-right: 0.44em; margin-left: -0.44em; }
.etp-rdquo { padding-right: 0.4em; margin-left: -0.4em; }
.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-rsquo { margin-left: -0.22em; }
.etp-sp-rpar, .etp-sp-rsqb, .etp-sp-rcub { margin-left: -0.25em; }
Комментарии: Двухстороннее выравнивание текстового блока с помощью стиля text-justify в принципе плохо совместим концепцией типографики — он растягивает или сжимает пробелы по всей строке (а это пробелы между словами) и уже этими, переменными, пробелами, делает текст трудночитаемым. Если же вы используете text-justify для выравнивания текста по ширине, то, чтобы сохранить оптимальную читаемость текста, включать висячую типографику не рекомендуется.
Известные особенности и ограничения
При обработке сложного HTML-кода типограф стремится сохранить структуру документа, но некоторые пограничные случаи могут обрабатываться не так, как ожидается. В частности:
- Обработка на стыке тегов: Правила, требующие анализа контекста (например, расстановка неразрывных пробелов у тире или единиц измерения), могут работать некорректно, если анализируемые части текста разделены тегами . Например, конструкция
$<b>100</b>не будет обработана (между $ и 100 не будет вставлен неразрывный пробел), так как типограф не видит их как соседние элементы. - "Ремонт" HTML: Библиотека использует
BeautifulSoupдля парсинга, который может "чинить" невалидный HTML (например, закрывать незакрытые теги). Это может привести к неожиданным изменениям в структуре, если исходный код был некорректен. Так же может меняться порядок атрибутов тега.
Мы знаем об этих особенностях и работаем над улучшением алгоритмов для более точной обработки сложных случаев.
P.S.
Если вам нравится этот проект, можете поддержать отправив любую сумму на мой Т-банк
по ссылке или, для приверженцев децентрализованного будущего,
через Toncoin (TON) (адрес кошелька UQApEkzNMYOg5qesWwlyfGFf4ayFyki5Mrpcd2yadgS2_1cx)
Credits
Разработка: Проект разработан Sergei Erjemin при активном участии различных LLM в роли pair-programmer.