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>
This commit is contained in:
168
docs/applyNowrap.md
Normal file
168
docs/applyNowrap.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# applyNowrap — защита от висячих предлогов в email
|
||||
|
||||
## Назначение
|
||||
|
||||
Функция `applyNowrap(html)` предотвращает «висячие» предлоги и короткие слова (≤3 букв) в email-рассылках. Оборачивает короткое слово + следующее слово в `<span style="white-space:nowrap">`, чтобы они не разрывались на разные строки.
|
||||
|
||||
**Почему нужна:** Mail.ru и некоторые другие почтовые клиенты игнорируют ` ` при переносе строк. Единственный надёжный способ — `white-space:nowrap` на `<span>`.
|
||||
|
||||
## Полный код
|
||||
|
||||
```javascript
|
||||
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 → реальные теги
|
||||
|
||||
```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| )+(\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>` |
|
||||
|
||||
## Результат на примере
|
||||
|
||||
**Вход:**
|
||||
```html
|
||||
<span class="font h3 blackText" style="font-size: 18px;">С первыми весенними днями дизайнеры и стилисты</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: ` ` не матчился (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.
|
||||
|
||||
## Адаптация для другого проекта
|
||||
|
||||
1. **Целевой CSS-класс:** Заменить `h3` в regex шага 1 на класс текстовых блоков вашего шаблона
|
||||
2. **Закрывающий паттерн:** `</span></td>` — подстроить под структуру вашего HTML (может быть `</p></td>`, `</div>` и т.д.)
|
||||
3. **Типограф:** Если используется типограф, regex уже обрабатывает ` `. Если нет — `(?:\s| )+` всё равно будет работать корректно (просто ` `-ветка не сработает)
|
||||
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 с подставленными товарами для превью
|
||||
Reference in New Issue
Block a user