Files
VA.ASPEKTER/docs/applyNowrap.md
Sergey Zotov c090bfcf47 Initial commit — Aspekter VA email builder
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>
2026-04-13 01:21:00 +05:00

169 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# applyNowrap — защита от висячих предлогов в email
## Назначение
Функция `applyNowrap(html)` предотвращает «висячие» предлоги и короткие слова (≤3 букв) в email-рассылках. Оборачивает короткое слово + следующее слово в `<span style="white-space:nowrap">`, чтобы они не разрывались на разные строки.
**Почему нужна:** Mail.ru и некоторые другие почтовые клиенты игнорируют `&nbsp;` при переносе строк. Единственный надёжный способ — `white-space:nowrap` на `<span>`.
## Полный код
```javascript
function applyNowrap(html) {
function wrapShort(text) {
return text.replace(
/(?<![a-zA-Zа-яА-ЯёЁ])([a-zA-Zа-яА-ЯёЁ]{1,3})(?:\s|&nbsp;)+(\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 → реальные теги
```javascript
.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|&nbsp;)+(\S+)/gu
```
Разбор по частям:
| Часть | Значение |
|-------|----------|
| `(?<![a-zA-Zа-яА-ЯёЁ])` | Negative lookbehind: перед словом НЕ должна стоять буква (иначе это часть длинного слова) |
| `([a-zA-Zа-яА-ЯёЁ]{1,3})` | Группа 1: короткое слово от 1 до 3 букв (предлоги, союзы, частицы) |
| `(?:\s\|&nbsp;)+` | Пробел ИЛИ `&nbsp;` (типограф заменяет пробелы на `&nbsp;`) |
| `(\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>` |
## Результат на примере
**Вход:**
```html
<span class="font h3 blackText" style="font-size: 18px;">С&nbsp;первыми весенними днями дизайнеры и&nbsp;стилисты</span></td>
```
**Выход:**
```html
<span class="font h3 blackText" style="font-size: 18px;">
<span style="white-space:nowrap">С первыми</span> весенними днями
дизайнеры <span style="white-space:nowrap">и стилисты</span>
</span></td>
```
## Исправленные баги
### Баг 1: `&nbsp;` не матчился (2026-03-20)
**Симптом:** Nowrap-спаны не появляются в HTML, хотя функция вызывается.
**Причина:** Типограф (Артемий Лебедев / любой другой) вставляет `&nbsp;` между предлогами и словами. Оригинальный regex искал только `\s+` и не находил `&nbsp;`.
**Фикс:** Заменить `\s+` на `(?:\s|&nbsp;)+`.
### Баг 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.
## Адаптация для другого проекта
1. **Целевой CSS-класс:** Заменить `h3` в regex шага 1 на класс текстовых блоков вашего шаблона
2. **Закрывающий паттерн:** `</span></td>` — подстроить под структуру вашего HTML (может быть `</p></td>`, `</div>` и т.д.)
3. **Типограф:** Если используется типограф, regex уже обрабатывает `&nbsp;`. Если нет — `(?:\s|&nbsp;)+` всё равно будет работать корректно (просто `&nbsp;`-ветка не сработает)
4. **Вызов:** Применять к финальному HTML **после** типографа, но **до** отдачи клиенту
5. **Тестирование:** Обязательно проверять в Mail.ru — это самый строгий клиент по обработке пробелов
## Где вызывается
В бэкенде (vite.config.js), в endpoint рендера email — после получения HTML из email-gen-api и обработки Mindbox-тегов:
```javascript
const nowrapHtml = applyNowrap(rawHtml)
const nowrapPreview = applyNowrap(mindbox.html)
```
Результаты отдаются клиенту:
- `nowrapHtml` — финальный HTML для экспорта/отправки
- `nowrapPreview` — HTML с подставленными товарами для превью