
В типографике существует понятие “висячих слов” — это короткие слова (предлоги, союзы, местоимения), которые остаются в конце строки при переносе текста. Такие переносы нарушают удобочитаемость и эстетику текста. В русской типографике принято избегать переносов после коротких слов длиной 1-2 символа.
Решение проблемы с помощью JavaScript
Для автоматического предотвращения переносов после коротких слов можно использовать JavaScript, который заменяет обычные пробелы на неразрывные пробелы ( ) после определённых слов.
// Перенос висячих слов
document.addEventListener('DOMContentLoaded', function () {
// 1. Находим все текстовые элементы, которые нужно обработать
const textElements = document.querySelectorAll('p, span, h1, h2, h3, h4, h5, h6, li, dt, dd');
// 2. Список предлогов и союзов, которые нельзя переносить
const prepositions = ['в', 'без', 'до', 'из', 'к', 'на', 'по', 'о', 'от', 'перед', 'при', 'через', 'для', 'с', 'у', 'и', 'а', 'но', 'да', 'или', 'либо', 'что', 'чтобы', 'как', 'когда', 'если', 'вы'];
// 3. Функция для обработки каждого элемента
textElements.forEach(element => {
// Получаем все текстовые узлы внутри элемента
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
const textNodes = [];
while (walker.nextNode()) {
textNodes.push(walker.currentNode);
}
// Обрабатываем каждый текстовый узел
textNodes.forEach(textNode => {
let text = textNode.nodeValue;
// Заменяем пробелы после коротких слов на неразрывные пробелы
text = text.replace(/(^|\s)([а-яё]{1,2})\s/gi, (match, prefix, word) => {
// Проверяем, есть ли слово в списке предлогов
if (prepositions.includes(word.toLowerCase())) {
return prefix + word + '\u00A0'; // \u00A0 - это неразрывный пробел
}
return match;
});
// Обновляем содержимое текстового узла
textNode.nodeValue = text;
});
});
});
Разбор кода по частям
1. Инициализация скрипта
document.addEventListener('DOMContentLoaded', function () {
Код выполняется после полной загрузки DOM-дерева страницы. Это гарантирует, что все элементы будут доступны для обработки.
2. Поиск текстовых элементов
const textElements = document.querySelectorAll('p, span, h1, h2, h3, h4, h5, h6, li, dt, dd');
Скрипт находит все основные текстовые элементы на странице: параграфы, заголовки, элементы списков и другие. Вы можете расширить этот список, добавив другие селекторы.
3. Список коротких слов
const prepositions = ['в', 'без', 'до', 'из', 'к', 'на', 'по', 'о', 'от', 'перед', 'при', 'через', 'для', 'с', 'у', 'и', 'а', 'но', 'да', 'или', 'либо', 'что', 'чтобы', 'как', 'когда', 'если', 'вы'];
Массив содержит наиболее распространённые предлоги, союзы и другие служебные слова русского языка, после которых нежелательны переносы.
4. Обход текстовых узлов
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
TreeWalker позволяет пройти по всем текстовым узлам внутри элемента, включая вложенные теги. Это важно, так как текст может содержать форматирование (, и т.д.).
5. Регулярное выражение для замены
text = text.replace(/(^|\s)([а-яё]{1,2})\s/gi, (match, prefix, word) => {
Регулярное выражение ищет:
· (^|\s) — начало строки или пробел (группа 1);
· ([а-яё]{1,2}) — слово из 1-2 русских букв (группа 2);
· \s — пробел после слова;
· Флаги gi означают глобальный поиск без учёта регистра.
6. Условная замена
if (prepositions.includes(word.toLowerCase())) {
return prefix + word + '\u00A0'; // \u00A0 - это неразрывный пробел
}
Если найденное короткое слово есть в списке предлогов, обычный пробел заменяется на неразрывный пробел (\u00A0). Это предотвращает разрыв строки между предлогом и следующим словом.
Результат работы
После выполнения скрипта текст “Я иду в магазин” не будет разорван на строки как “Я иду в” и “магазин”. Предлог “в” всегда останется на одной строке со следующим словом благодаря неразрывному пробелу.
Настройки скрипта
Вы можете легко адаптировать скрипт под свои нужды:
Добавить новые селекторы в querySelectorAll() для обработки других элементов;
Расширить список слов в массиве prepositions;
Изменить длину слов в регулярном выражении с {1,2} на другое значение;
Добавить обработку других языков, изменив диапазон символов в регулярном; выражении.
Этот простой скрипт значительно улучшает типографику русскоязычных веб-страниц, делая текст более читаемым и профессионально оформленным.
P.S.: это JS решение иногда не во всех случаях срабатывает + бывают сложности с адаптивностью: на смартфонах может переносить совсем не так, как хотелось бы, текст может «уехать» за пределы контейнера, нужно будет «фиксить» эти моменты в CSS.
А как вы решаете вопрос с переносом «висячих» слов?
Подписывайся на мой телеграм-канал, чтобы узнать ещё больше полезных фишек верстки и веб-дизайна.
Комментарии (10)
winkyBrain
20.06.2025 08:59Итого получаем:
Перебор ВСЕГО дом-дерева в поисках указанных текстовых элементов;
Перебор получившегося массива текстовых элементов с последующим перебором текстовых элементов внутри него, т.е. цикл в цикле;
И внутри этого цикла у нас replace, внутри коллбэка которого ещё и includes по массиву, т.е. ещё несколько переборов внутри переборов.
В плане производительности получается дичь, даже с сотней-другой текстовых элементов небольшого текстового содержания. На большом проекте скорее всего может просто повесить вкладку)
Как можно сделать быстрее/лучше/проще:
Вынести текст в константы, и уже по ним устроить перебор - как минимум не придётся перебирать DOM в поисках текстовых элементов, а в элементах искать текстовые узлы. Можно отдать в них уже форматированный текст, а не наоборот. Предлоги вынести в Set вместо массива, чтобы исключить ещё один перебор.
Неразрывные пробелы нужны не во всех случаях. Я бы даже сказал что они нужны реже, чем не нужны) текстовый блок относительно короткий или нет в нём коротких предлогов. Соответственно и перебирать их просто нет нужды. Здесь можно, используя описанный выше подход, итерироваться не по всем текстовым константам, а только по тем, где вам неразрывный пробел необходим. Например собирать из них отдельный массив/объект. Но так как при таком подходе уже придётся что-то отделять самостоятельно, то:
Самое простое - в тех же текстовых константах(или в разметке на месте) ставить неразрывный пробел руками. Это делается один раз при вставке с макета, глаз обычно уже намётан. На текущем месте так и делаем, никто не пострадал.
А ещё изредка бывает так, что для мобильной вёрстки в конкретном месте перед коротким предлогом НЕ НУЖЕН неразрывный пробел, чтобы текст встал красиво. И в таком случае автоматика тоже будет мешать
Adyavor
20.06.2025 08:59Ну без тестов нельзя утверждать. У меня подобная вещь работает на странице. Текст длинной около 19К обрабатывается примерно за 110 мс.
Браузер Хром, Проц i5 12100
На мозиле немного дольше - 150 мс.
Adyavor
20.06.2025 08:59Там даже еще ставятся неразрывные пробелы нулевой длины между скобками и содержимым внутри, знаки минус или дефис между буквами заменяются на неразрывный дефис, ставится неразрывный пробел перед тире.
DjUmnik
Можно также улучшить читаемость текста с помощью CSS-свойства text-wrap: pretty; оно правда не устоявшееся.
Была статья на хабре https://habr.com/ru/articles/899194/
m-timshin Автор
Видел, но, если не ошибаюсь, вопрос с "висячими" словами же не решает?
nikolayshabalin
Да-да,
text-wrap
не решает задачу избавления от висячих предлоговwinkyBrain
Решает. Другой момент, что именно вы называете висячим словом. Вижу у вас в массиве предлог "перед", это целых 5 букв. Не удивлюсь, если text-wrap: pretty не переносит слова подобных размеров и не должен
nikolayshabalin
Автор этой статьи в самом начале определяет, что он имеет ввиду под "висячими словами"
> В типографике существует понятие “висячих слов” — это короткие слова (предлоги, союзы, местоимения), которые остаются в конце строки при переносе текста. Такие переносы нарушают удобочитаемость и эстетику текста. В русской типографике принято избегать переносов после коротких слов длиной 1-2 символа.
В самом коде все предлоги и союзы определяются в это понятие "висячего слова". Вот по такой формулировке
text-wrap
ничего не решает.