# 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 с подставленными товарами для превью
