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