Full project: Svelte 5 frontend, Vite 7 backend API, Pug email templates (email-gen), Docker deployment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8.9 KiB
applyNowrap — защита от висячих предлогов в email
Назначение
Функция applyNowrap(html) предотвращает «висячие» предлоги и короткие слова (≤3 букв) в email-рассылках. Оборачивает короткое слово + следующее слово в <span style="white-space:nowrap">, чтобы они не разрывались на разные строки.
Почему нужна: Mail.ru и некоторые другие почтовые клиенты игнорируют при переносе строк. Единственный надёжный способ — white-space:nowrap на <span>.
Полный код
function applyNowrap(html) {
function wrapShort(text) {
return text.replace(
/(?<![a-zA-Zа-яА-ЯёЁ])([a-zA-Zа-яА-ЯёЁ]{1,3})(?:\s| )+(\S+)/gu,
'\u200Bspan\u200Bnwr\u200B$1\u00A0$2\u200B/span\u200Bnwr\u200B'
)
}
// Шаг 1: Найти текстовые блоки (h3 spans)
const result = html.replace(
/(<span[^>]*class="[^"]*\bh3\b[^"]*"[^>]*>)([\s\S]*?)(<\/span><\/td>)/gi,
(_match, open, content, close) => {
// Шаг 2: Обработать текст МЕЖДУ тегами: >текст<
const processed = content.replace(
/>([^<]+)</g,
(m, t) => '>' + wrapShort(t) + '<'
)
// Шаг 3: Обработать текст В НАЧАЛЕ (до первого тега)
const firstText = processed.replace(
/^([^<]+)/,
(m) => wrapShort(m)
)
return open + firstText + close
}
)
// Шаг 4: Заменить placeholder'ы на реальные span-теги
return result
.replace(/\u200Bspan\u200Bnwr\u200B/g, '<span style="white-space:nowrap">')
.replace(/\u200B\/span\u200Bnwr\u200B/g, '</span>')
}
Алгоритм пошагово
Шаг 1 — Выбор целевых блоков
/(<span[^>]*class="[^"]*\bh3\b[^"]*"[^>]*>)([\s\S]*?)(<\/span><\/td>)/gi
Ищет <span> с классом h3 (текстовые блоки писем), закрытый </span></td>.
Три группы захвата:
open— открывающий тег<span class="... h3 ...">content— всё содержимое между открывающим и закрывающим тегамиclose— закрывающий</span></td>
Адаптация для другого проекта: заменить
h3на нужный CSS-класс текстовых блоков. Заменить</span></td>на ваш закрывающий паттерн.
Шаг 2 — Обработка текста между вложенными тегами
/>([^<]+)</g
Находит текстовые фрагменты между > и < (текст между HTML-тегами внутри блока). Например в <span style="font-weight:700">Что это дает</span>? поймает Что это дает.
Шаг 3 — Обработка текста в начале блока
/^([^<]+)/
Ловит текст от начала content до первого HTML-тега. Нужен отдельно, потому что regex из шага 2 ищет >текст<, а в начале блока нет > перед текстом.
Шаг 4 — Placeholder → реальные теги
.replace(/\u200Bspan\u200Bnwr\u200B/g, '<span style="white-space:nowrap">')
.replace(/\u200B\/span\u200Bnwr\u200B/g, '</span>')
Заменяет placeholder-маркеры на настоящие HTML-теги.
Зачем placeholder'ы? Если бы мы сразу вставляли <span>, то regex из шага 2 (>([^<]+)<) мог бы снова поймать текст внутри уже вставленного span'а и обработать его повторно. Маркеры \u200B (zero-width space) невидимы и не матчатся как < или >, поэтому двойной обработки не происходит.
Ключевой regex — wrapShort
/(?<![a-zA-Zа-яА-ЯёЁ])([a-zA-Zа-яА-ЯёЁ]{1,3})(?:\s| )+(\S+)/gu
Разбор по частям:
| Часть | Значение |
|---|---|
(?<![a-zA-Zа-яА-ЯёЁ]) |
Negative lookbehind: перед словом НЕ должна стоять буква (иначе это часть длинного слова) |
([a-zA-Zа-яА-ЯёЁ]{1,3}) |
Группа 1: короткое слово от 1 до 3 букв (предлоги, союзы, частицы) |
(?:\s| )+ |
Пробел ИЛИ (типограф заменяет пробелы на ) |
(\S+) |
Группа 2: следующее слово (любые непробельные символы) |
Флаг g |
Глобальный поиск |
Флаг u |
Unicode-режим |
Строка замены: '\u200Bspan\u200Bnwr\u200B$1\u00A0$2\u200B/span\u200Bnwr\u200B'
| Часть | Значение |
|---|---|
\u200Bspan\u200Bnwr\u200B |
Placeholder для <span style="white-space:nowrap"> |
$1 |
Короткое слово (предлог) |
\u00A0 |
Non-breaking space между словами (двойная защита) |
$2 |
Следующее слово |
\u200B/span\u200Bnwr\u200B |
Placeholder для </span> |
Результат на примере
Вход:
<span class="font h3 blackText" style="font-size: 18px;">С первыми весенними днями дизайнеры и стилисты</span></td>
Выход:
<span class="font h3 blackText" style="font-size: 18px;">
<span style="white-space:nowrap">С первыми</span> весенними днями
дизайнеры <span style="white-space:nowrap">и стилисты</span>
</span></td>
Исправленные баги
Баг 1: не матчился (2026-03-20)
Симптом: Nowrap-спаны не появляются в HTML, хотя функция вызывается.
Причина: Типограф (Артемий Лебедев / любой другой) вставляет между предлогами и словами. Оригинальный regex искал только \s+ и не находил .
Фикс: Заменить \s+ на (?:\s| )+.
Баг 2: \b не работает с кириллицей (2026-03-21)
Симптом: Regex вообще ничего не матчит для кириллических предлогов (С, и, в, на, от, из, но, до, по, за, ко).
Причина: В JavaScript \b (word boundary) работает ТОЛЬКО с ASCII-символами ([a-zA-Z0-9_]). Кириллица не считается «word character». Поэтому \b перед кириллическими буквами НИКОГДА не срабатывает.
Фикс: Заменить \b на (?<![a-zA-Zа-яА-ЯёЁ]) (negative lookbehind — «не предшествует буква»).
⚠️ Правило: Никогда не использовать
\bдля кириллицы в JavaScript. Всегда использовать lookbehind/lookahead.
Адаптация для другого проекта
- Целевой CSS-класс: Заменить
h3в regex шага 1 на класс текстовых блоков вашего шаблона - Закрывающий паттерн:
</span></td>— подстроить под структуру вашего HTML (может быть</p></td>,</div>и т.д.) - Типограф: Если используется типограф, regex уже обрабатывает
. Если нет —(?:\s| )+всё равно будет работать корректно (просто -ветка не сработает) - Вызов: Применять к финальному HTML после типографа, но до отдачи клиенту
- Тестирование: Обязательно проверять в Mail.ru — это самый строгий клиент по обработке пробелов
Где вызывается
В бэкенде (vite.config.js), в endpoint рендера email — после получения HTML из email-gen-api и обработки Mindbox-тегов:
const nowrapHtml = applyNowrap(rawHtml)
const nowrapPreview = applyNowrap(mindbox.html)
Результаты отдаются клиенту:
nowrapHtml— финальный HTML для экспорта/отправкиnowrapPreview— HTML с подставленными товарами для превью