Add "Rendering"
127
Rendering.md
Normal file
127
Rendering.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# Рендер и постобработка
|
||||||
|
|
||||||
|
## Сборка PUG-кода
|
||||||
|
|
||||||
|
Функция `rebuildOutput()` конкатенирует блоки:
|
||||||
|
|
||||||
|
```
|
||||||
|
//Заголовок ← комментарий с именем блока
|
||||||
|
+spacerLine(40) ← спейсер (если addSpacing)
|
||||||
|
tr
|
||||||
|
td.paddingWrapper
|
||||||
|
span.font.h2 ТЕКСТ
|
||||||
|
|
||||||
|
//БАННЕР
|
||||||
|
+spacerLine(20)
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
a(href="https://...")
|
||||||
|
img(src="https://...")
|
||||||
|
```
|
||||||
|
|
||||||
|
Если сертификат включён (`certificateEnabled`), блоки сертификата добавляются в конец.
|
||||||
|
|
||||||
|
## Render Cache
|
||||||
|
|
||||||
|
- **Ключ**: MD5 от `slug + pug + gender + genderPaths`
|
||||||
|
- **Хранение**: Map в памяти + `render-cache.json` на диске
|
||||||
|
- **Размер**: LRU, максимум 30 записей
|
||||||
|
- **Инвалидация**: при изменении любого параметра рендера
|
||||||
|
|
||||||
|
## Preheader
|
||||||
|
|
||||||
|
### Формат
|
||||||
|
```
|
||||||
|
{текст прехедера} <vk-snippet-end>⠀×130
|
||||||
|
```
|
||||||
|
|
||||||
|
- Текст прехедера — основное содержимое
|
||||||
|
- `<vk-snippet-end>` — тег для Mail.ru (snippet boundary)
|
||||||
|
- `⠀×130` — невидимые символы-пустышки, чтобы почтовые клиенты не показывали текст письма после прехедера
|
||||||
|
|
||||||
|
### Цепочка обработки
|
||||||
|
1. В Pug: `+preheader("текст")` (миксин из mixins.pug)
|
||||||
|
2. Миксин генерирует `#MAILRU_PREHEADER_TAG#` в HTML
|
||||||
|
3. server.js заменяет `#MAILRU_PREHEADER_TAG#` → `<vk-snippet-end/>`
|
||||||
|
|
||||||
|
### Санитизация preheader (защита от инъекций)
|
||||||
|
```javascript
|
||||||
|
preheader.replace(/[\r\n`\\]/g, '').replace(/"/g, '\\"')
|
||||||
|
```
|
||||||
|
Убираются переносы строк (предотвращает выход из строки `+preheader("...")`), backtick и backslash.
|
||||||
|
|
||||||
|
## Nowrap (висячие предлоги)
|
||||||
|
|
||||||
|
Функция `applyNowrap(html)` — оборачивает короткие предлоги/союзы с следующим словом в `<span style="white-space:nowrap">`.
|
||||||
|
|
||||||
|
### Алгоритм
|
||||||
|
1. Находит только `<span>` элементы с классом `h3`, за которыми `</span></td>`
|
||||||
|
2. Для каждого совпадения вызывает `wrapShort(text)`:
|
||||||
|
- Regex: `(?<![a-zA-Zа-яА-ЯёЁ])([a-zA-Zа-яА-ЯёЁ]{1,3})(?:\s| )+(\S+)`
|
||||||
|
- Слово 1-3 буквы + пробел + следующее слово
|
||||||
|
3. Placeholder-подход: сначала вставляет `\u200B` (zero-width space), затем заменяет на `<span style="white-space:nowrap">...\u00A0...</span>`
|
||||||
|
4. Placeholder нужен чтобы не было двойной обработки
|
||||||
|
|
||||||
|
### Пример
|
||||||
|
```
|
||||||
|
Идеи на каждый день → Идеи <span style="white-space:nowrap">на каждый</span> день
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mindbox Tag Processing
|
||||||
|
|
||||||
|
Функция `processMindboxTags(html, feedUrl)`:
|
||||||
|
|
||||||
|
### Что обрабатывает
|
||||||
|
1. **Рекомендации** — удаляет `@{for...}@{end for}` блоки
|
||||||
|
2. **Переменные** — резолвит `@{set var = value}`
|
||||||
|
3. **Условия**:
|
||||||
|
- `@{if ...DiscountPercent > 0}` — скрывает блок если нет скидки
|
||||||
|
- `@{if ...OldPrice > ...Price}` — скрывает если нет реальной скидки
|
||||||
|
- Вложенные условия поддерживаются
|
||||||
|
4. **Подстановки**:
|
||||||
|
- `${Products...GetByValue('name')}` → название товара из фида
|
||||||
|
- `${formatDecimal(price)}` → форматированная цена с пробелами
|
||||||
|
- `${ResizeImage(url, width, height)}` → URL картинки
|
||||||
|
5. **Бейджи** — если товар не в наличии, на картинку накладывается полупрозрачный оверлей "Нет в наличии"
|
||||||
|
|
||||||
|
### Маппинг свойств Mindbox → Фид
|
||||||
|
| Mindbox | Фид |
|
||||||
|
|---------|-----|
|
||||||
|
| `name` | name |
|
||||||
|
| `vendorName` | vendor |
|
||||||
|
| `url` | url |
|
||||||
|
| `pictureUrl` | image |
|
||||||
|
| `price` | price |
|
||||||
|
| `oldPrice` | oldPrice |
|
||||||
|
| `description` | description |
|
||||||
|
| `customField.vendorCode` | vendorCode |
|
||||||
|
| `customField.discountPercent` | discountPercent |
|
||||||
|
| и другие... | |
|
||||||
|
|
||||||
|
## Concurrency Limit
|
||||||
|
|
||||||
|
Максимум 3 параллельных рендера (`MAX_CONCURRENT_RENDERS`).
|
||||||
|
|
||||||
|
При превышении — HTTP 429 "Слишком много параллельных рендеров, подождите".
|
||||||
|
|
||||||
|
Счётчик `activeRenders` обёрнут в `try/finally` для гарантии декремента.
|
||||||
|
|
||||||
|
## Preview в iframe
|
||||||
|
|
||||||
|
```html
|
||||||
|
<iframe sandbox="allow-same-origin" srcdoc={previewHtml} />
|
||||||
|
```
|
||||||
|
|
||||||
|
- `sandbox="allow-same-origin"` — блокирует выполнение скриптов (XSS), но позволяет доступ к DOM
|
||||||
|
- `fitPreviewFrame()` — масштабирует содержимое под viewport
|
||||||
|
- Zoom: 40-100%, сохраняется в профиле на сервере
|
||||||
|
- Click detection: через `contentDocument.addEventListener` (не инъекция скрипта)
|
||||||
|
|
||||||
|
## Quick Edit
|
||||||
|
|
||||||
|
Клик по элементу в превью → находит соответствующий блок и поле → показывает плавающий редактор.
|
||||||
|
|
||||||
|
1. `injectPreviewClickDetection()` — навешивает click listener на iframe DOM
|
||||||
|
2. `handlePreviewElementClick({text, rect})` — ищет блок по тексту (normalizeMatchText)
|
||||||
|
3. `openQuickEdit(block, field, iframeRect)` — позиционирует редактор у элемента
|
||||||
|
4. Изменения сразу применяются к блоку, помечают превью как stale
|
||||||
Reference in New Issue
Block a user