@escapenavigator/utils 1.10.131 → 1.10.132
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paste-time matchers для ReactQuill (`modules.clipboard.matchers`).
|
|
3
|
+
*
|
|
4
|
+
* Вынесено в общий пакет, чтобы admin и app использовали один и тот же набор
|
|
5
|
+
* matcher-ов без дублирования. Делает две вещи на входе из буфера обмена:
|
|
6
|
+
*
|
|
7
|
+
* 1) Снимает inline-цвета и фоны (`color: false` / `background: false` через
|
|
8
|
+
* compose Delta). Это лечит вставку из Word/Google Docs, которая по
|
|
9
|
+
* умолчанию приходит с собственной палитрой и часто не читается на наших
|
|
10
|
+
* темах (особенно тёмной теме ордеров и контрастных формах админки).
|
|
11
|
+
*
|
|
12
|
+
* 2) Заменяет неразрывные пробелы (`\u00A0`) на обычные прямо в Delta до
|
|
13
|
+
* того, как Quill сохранит ноду. Без этого ` ` остаётся в HTML и
|
|
14
|
+
* ломает word-wrap длинных заголовков и абзацев на странице рендера
|
|
15
|
+
* (см. также `sanitizeQuillHtml` на стороне рендеринга — оно ловит то же
|
|
16
|
+
* самое, но уже на исторических данных, которые попали в БД до того,
|
|
17
|
+
* как paste-matcher появился).
|
|
18
|
+
*
|
|
19
|
+
* ELEMENT_NODE matcher срабатывает на всех элементах (страховой случай),
|
|
20
|
+
* SPAN/FONT — прицельно (Word любит оборачивать раскраску в `<span style="…">`
|
|
21
|
+
* и `<font color="…">`), TEXT_NODE — для нормализации пробелов в тексте.
|
|
22
|
+
*/
|
|
23
|
+
import Delta from 'quill-delta';
|
|
24
|
+
export type ClipboardMatcher = [number | string, (node: Node, delta: Delta) => Delta];
|
|
25
|
+
/**
|
|
26
|
+
* Полный набор matcher-ов: и color-strip, и nbsp-normalize. Подключается там,
|
|
27
|
+
* где редактор не должен принимать цветную раскраску из буфера (waiver, AGB,
|
|
28
|
+
* локации, контентные модалки).
|
|
29
|
+
*/
|
|
30
|
+
export declare function getStripColorClipboardMatchers(): ClipboardMatcher[];
|
|
31
|
+
/**
|
|
32
|
+
* Только нормализация пробельных сущностей. Подключается там, где цветную
|
|
33
|
+
* раскраску в буфере мы готовы пустить дальше (rich email-редактор админа),
|
|
34
|
+
* но nbsp всё равно надо чистить, чтобы не ломать word-wrap.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getNormalizeWhitespaceClipboardMatchers(): ClipboardMatcher[];
|
|
37
|
+
/**
|
|
38
|
+
* Готовые `modules` для прямой подстановки в `<ReactQuill modules={…} />`.
|
|
39
|
+
*
|
|
40
|
+
* Содержит два изменения относительно дефолтного snow-конфига:
|
|
41
|
+
*
|
|
42
|
+
* 1) Тулбар без пикеров `color` / `background`. У нас цветной email-редактор
|
|
43
|
+
* живёт ровно в одном месте (`admin/modals/email-send-form`); во всех
|
|
44
|
+
* остальных формах цвета только мешают — пользователи случайно красили
|
|
45
|
+
* текст оранжевым на белом фоне, а потом он становился нечитаемым в
|
|
46
|
+
* тёмной теме клиента (orders/widget) или в письме на чёрном фоне.
|
|
47
|
+
*
|
|
48
|
+
* 2) Clipboard-matchers, которые на вставку из буфера снимают inline-цвета
|
|
49
|
+
* и нормализуют `\u00A0` → ` ` (см. `getStripColorClipboardMatchers`).
|
|
50
|
+
*/
|
|
51
|
+
export declare const stripColorQuillModules: {
|
|
52
|
+
toolbar: (string[] | {
|
|
53
|
+
header: (number | boolean)[];
|
|
54
|
+
}[] | {
|
|
55
|
+
list: string;
|
|
56
|
+
}[] | {
|
|
57
|
+
indent: string;
|
|
58
|
+
}[] | {
|
|
59
|
+
align: any[];
|
|
60
|
+
}[])[];
|
|
61
|
+
clipboard: {
|
|
62
|
+
matchers: ClipboardMatcher[];
|
|
63
|
+
};
|
|
64
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Paste-time matchers для ReactQuill (`modules.clipboard.matchers`).
|
|
4
|
+
*
|
|
5
|
+
* Вынесено в общий пакет, чтобы admin и app использовали один и тот же набор
|
|
6
|
+
* matcher-ов без дублирования. Делает две вещи на входе из буфера обмена:
|
|
7
|
+
*
|
|
8
|
+
* 1) Снимает inline-цвета и фоны (`color: false` / `background: false` через
|
|
9
|
+
* compose Delta). Это лечит вставку из Word/Google Docs, которая по
|
|
10
|
+
* умолчанию приходит с собственной палитрой и часто не читается на наших
|
|
11
|
+
* темах (особенно тёмной теме ордеров и контрастных формах админки).
|
|
12
|
+
*
|
|
13
|
+
* 2) Заменяет неразрывные пробелы (`\u00A0`) на обычные прямо в Delta до
|
|
14
|
+
* того, как Quill сохранит ноду. Без этого ` ` остаётся в HTML и
|
|
15
|
+
* ломает word-wrap длинных заголовков и абзацев на странице рендера
|
|
16
|
+
* (см. также `sanitizeQuillHtml` на стороне рендеринга — оно ловит то же
|
|
17
|
+
* самое, но уже на исторических данных, которые попали в БД до того,
|
|
18
|
+
* как paste-matcher появился).
|
|
19
|
+
*
|
|
20
|
+
* ELEMENT_NODE matcher срабатывает на всех элементах (страховой случай),
|
|
21
|
+
* SPAN/FONT — прицельно (Word любит оборачивать раскраску в `<span style="…">`
|
|
22
|
+
* и `<font color="…">`), TEXT_NODE — для нормализации пробелов в тексте.
|
|
23
|
+
*/
|
|
24
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
25
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.stripColorQuillModules = void 0;
|
|
29
|
+
exports.getStripColorClipboardMatchers = getStripColorClipboardMatchers;
|
|
30
|
+
exports.getNormalizeWhitespaceClipboardMatchers = getNormalizeWhitespaceClipboardMatchers;
|
|
31
|
+
const quill_delta_1 = __importDefault(require("quill-delta"));
|
|
32
|
+
const stripColorFromDelta = (_node, delta) => delta.compose(new quill_delta_1.default().retain(delta.length(), {
|
|
33
|
+
color: false,
|
|
34
|
+
background: false,
|
|
35
|
+
}));
|
|
36
|
+
const normalizeWhitespaceInDelta = (_node, delta) => {
|
|
37
|
+
if (!delta?.ops?.length)
|
|
38
|
+
return delta;
|
|
39
|
+
let modified = false;
|
|
40
|
+
const ops = delta.ops.map((op) => {
|
|
41
|
+
if (typeof op.insert === 'string' && op.insert.indexOf('\u00A0') !== -1) {
|
|
42
|
+
modified = true;
|
|
43
|
+
return { ...op, insert: op.insert.replace(/\u00A0/g, ' ') };
|
|
44
|
+
}
|
|
45
|
+
return op;
|
|
46
|
+
});
|
|
47
|
+
return modified ? new quill_delta_1.default(ops) : delta;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Полный набор matcher-ов: и color-strip, и nbsp-normalize. Подключается там,
|
|
51
|
+
* где редактор не должен принимать цветную раскраску из буфера (waiver, AGB,
|
|
52
|
+
* локации, контентные модалки).
|
|
53
|
+
*/
|
|
54
|
+
function getStripColorClipboardMatchers() {
|
|
55
|
+
return [
|
|
56
|
+
[Node.ELEMENT_NODE, stripColorFromDelta],
|
|
57
|
+
['SPAN', stripColorFromDelta],
|
|
58
|
+
['FONT', stripColorFromDelta],
|
|
59
|
+
[Node.TEXT_NODE, normalizeWhitespaceInDelta],
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Только нормализация пробельных сущностей. Подключается там, где цветную
|
|
64
|
+
* раскраску в буфере мы готовы пустить дальше (rich email-редактор админа),
|
|
65
|
+
* но nbsp всё равно надо чистить, чтобы не ломать word-wrap.
|
|
66
|
+
*/
|
|
67
|
+
function getNormalizeWhitespaceClipboardMatchers() {
|
|
68
|
+
return [[Node.TEXT_NODE, normalizeWhitespaceInDelta]];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Готовые `modules` для прямой подстановки в `<ReactQuill modules={…} />`.
|
|
72
|
+
*
|
|
73
|
+
* Содержит два изменения относительно дефолтного snow-конфига:
|
|
74
|
+
*
|
|
75
|
+
* 1) Тулбар без пикеров `color` / `background`. У нас цветной email-редактор
|
|
76
|
+
* живёт ровно в одном месте (`admin/modals/email-send-form`); во всех
|
|
77
|
+
* остальных формах цвета только мешают — пользователи случайно красили
|
|
78
|
+
* текст оранжевым на белом фоне, а потом он становился нечитаемым в
|
|
79
|
+
* тёмной теме клиента (orders/widget) или в письме на чёрном фоне.
|
|
80
|
+
*
|
|
81
|
+
* 2) Clipboard-matchers, которые на вставку из буфера снимают inline-цвета
|
|
82
|
+
* и нормализуют `\u00A0` → ` ` (см. `getStripColorClipboardMatchers`).
|
|
83
|
+
*/
|
|
84
|
+
exports.stripColorQuillModules = {
|
|
85
|
+
toolbar: [
|
|
86
|
+
[{ header: [1, 2, 3, false] }],
|
|
87
|
+
['bold', 'italic', 'underline', 'strike'],
|
|
88
|
+
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
89
|
+
[{ indent: '-1' }, { indent: '+1' }],
|
|
90
|
+
['blockquote', 'code-block'],
|
|
91
|
+
[{ align: [] }],
|
|
92
|
+
['link'],
|
|
93
|
+
['clean'],
|
|
94
|
+
],
|
|
95
|
+
clipboard: {
|
|
96
|
+
matchers: getStripColorClipboardMatchers(),
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Чистим Quill-HTML, который был сохранён без `clearQuill` или с устаревшей
|
|
3
|
+
* его версией (исторические waiver/AGB/`importantInfo`/локации/маркетинг-письма).
|
|
4
|
+
*
|
|
5
|
+
* Делит ответственность на две задачи, обе доступны и по отдельности:
|
|
6
|
+
*
|
|
7
|
+
* 1) `stripQuillColors` — снимает цвета и фоны: `style="color:…/background:…/
|
|
8
|
+
* background-color:…"`, атрибуты `color="…"`/`data-color`/`data-background`
|
|
9
|
+
* и Quill-классы `ql-color-*` / `ql-background-*`. Нужно везде, где тема
|
|
10
|
+
* рендера отличается от темы редактора (тёмная тема ордеров на белом фоне
|
|
11
|
+
* админки, инвертированные письма и т.п.).
|
|
12
|
+
*
|
|
13
|
+
* 2) `normalizeQuillWhitespace` — лечит неразрывные пробелы (` `, U+00A0)
|
|
14
|
+
* и одиночные `<br>` внутри `<p>`/`<h*>`. nbsp браузер не считает точкой
|
|
15
|
+
* переноса, и длинный заголовок вроде `14. ФОТО-, ВИДЕОФИКСАЦИЯ…`
|
|
16
|
+
* превращается в одно «слово» и вылезает за контейнер. То же самое
|
|
17
|
+
* случалось из-за вырезанных `<br>` в старом `clearQuill` — соседние строки
|
|
18
|
+
* склеивались без разделителя. Меняем nbsp и одиночные `<br>` на пробел,
|
|
19
|
+
* пустые `<p>`/`<p><br></p>` удаляем, дубли пробелов в тексте схлопываем.
|
|
20
|
+
*
|
|
21
|
+
* Содержимое `<pre>`/`<code>`/`<style>`/`<script>` не трогаем — там пробелы
|
|
22
|
+
* значимы.
|
|
23
|
+
*
|
|
24
|
+
* Все функции — чистые и идемпотентные: повторный вызов на уже очищенном
|
|
25
|
+
* HTML возвращает тот же результат (важно для сравнения «изменился ли текст»
|
|
26
|
+
* на стороне клиентских настроек, см. `client-agreement.tsx`).
|
|
27
|
+
*/
|
|
28
|
+
export type SanitizeQuillHtmlOptions = {
|
|
29
|
+
/** Снять inline-цвета/фоны и `ql-color-*`/`ql-background-*` классы. */
|
|
30
|
+
stripColors?: boolean;
|
|
31
|
+
};
|
|
32
|
+
export declare function stripQuillColors(html: string): string;
|
|
33
|
+
export declare function normalizeQuillWhitespace(html: string): string;
|
|
34
|
+
export declare function sanitizeQuillHtml(html: string | null | undefined, options?: SanitizeQuillHtmlOptions): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Чистим Quill-HTML, который был сохранён без `clearQuill` или с устаревшей
|
|
4
|
+
* его версией (исторические waiver/AGB/`importantInfo`/локации/маркетинг-письма).
|
|
5
|
+
*
|
|
6
|
+
* Делит ответственность на две задачи, обе доступны и по отдельности:
|
|
7
|
+
*
|
|
8
|
+
* 1) `stripQuillColors` — снимает цвета и фоны: `style="color:…/background:…/
|
|
9
|
+
* background-color:…"`, атрибуты `color="…"`/`data-color`/`data-background`
|
|
10
|
+
* и Quill-классы `ql-color-*` / `ql-background-*`. Нужно везде, где тема
|
|
11
|
+
* рендера отличается от темы редактора (тёмная тема ордеров на белом фоне
|
|
12
|
+
* админки, инвертированные письма и т.п.).
|
|
13
|
+
*
|
|
14
|
+
* 2) `normalizeQuillWhitespace` — лечит неразрывные пробелы (` `, U+00A0)
|
|
15
|
+
* и одиночные `<br>` внутри `<p>`/`<h*>`. nbsp браузер не считает точкой
|
|
16
|
+
* переноса, и длинный заголовок вроде `14. ФОТО-, ВИДЕОФИКСАЦИЯ…`
|
|
17
|
+
* превращается в одно «слово» и вылезает за контейнер. То же самое
|
|
18
|
+
* случалось из-за вырезанных `<br>` в старом `clearQuill` — соседние строки
|
|
19
|
+
* склеивались без разделителя. Меняем nbsp и одиночные `<br>` на пробел,
|
|
20
|
+
* пустые `<p>`/`<p><br></p>` удаляем, дубли пробелов в тексте схлопываем.
|
|
21
|
+
*
|
|
22
|
+
* Содержимое `<pre>`/`<code>`/`<style>`/`<script>` не трогаем — там пробелы
|
|
23
|
+
* значимы.
|
|
24
|
+
*
|
|
25
|
+
* Все функции — чистые и идемпотентные: повторный вызов на уже очищенном
|
|
26
|
+
* HTML возвращает тот же результат (важно для сравнения «изменился ли текст»
|
|
27
|
+
* на стороне клиентских настроек, см. `client-agreement.tsx`).
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.stripQuillColors = stripQuillColors;
|
|
31
|
+
exports.normalizeQuillWhitespace = normalizeQuillWhitespace;
|
|
32
|
+
exports.sanitizeQuillHtml = sanitizeQuillHtml;
|
|
33
|
+
const COLOR_STYLE_PROPS = new Set(['color', 'background', 'background-color']);
|
|
34
|
+
const COLOR_CLASS_PATTERN = /^ql-(color|background)-/;
|
|
35
|
+
const PRESERVE_BLOCK_PATTERN = /<(pre|code|style|script)\b[^>]*>[\s\S]*?<\/\1>/gi;
|
|
36
|
+
const PRESERVE_TOKEN_PREFIX = '\u2603SANITIZE_QUILL_PRESERVE_';
|
|
37
|
+
const PRESERVE_TOKEN_SUFFIX = '\u2603';
|
|
38
|
+
const PRESERVE_TOKEN_PATTERN = /\u2603SANITIZE_QUILL_PRESERVE_(\d+)\u2603/g;
|
|
39
|
+
function cleanStyleAttribute(style) {
|
|
40
|
+
return style
|
|
41
|
+
.split(';')
|
|
42
|
+
.map((part) => part.trim())
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.filter((part) => {
|
|
45
|
+
const prop = part.split(':')[0]?.trim().toLowerCase() ?? '';
|
|
46
|
+
return prop && !COLOR_STYLE_PROPS.has(prop);
|
|
47
|
+
})
|
|
48
|
+
.join('; ');
|
|
49
|
+
}
|
|
50
|
+
function stripQuillColors(html) {
|
|
51
|
+
return html
|
|
52
|
+
.replace(/\s*style="([^"]*)"/gi, (_match, styleContent) => {
|
|
53
|
+
const cleaned = cleanStyleAttribute(styleContent);
|
|
54
|
+
return cleaned ? ` style="${cleaned}"` : '';
|
|
55
|
+
})
|
|
56
|
+
.replace(/\s*color="[^"]*"/gi, '')
|
|
57
|
+
.replace(/\s*class="([^"]*)"/gi, (_match, classContent) => {
|
|
58
|
+
const cleaned = classContent
|
|
59
|
+
.split(/\s+/)
|
|
60
|
+
.filter((token) => token && !COLOR_CLASS_PATTERN.test(token))
|
|
61
|
+
.join(' ');
|
|
62
|
+
return cleaned ? ` class="${cleaned}"` : '';
|
|
63
|
+
})
|
|
64
|
+
.replace(/\s*data-color="[^"]*"/gi, '')
|
|
65
|
+
.replace(/\s*data-background="[^"]*"/gi, '')
|
|
66
|
+
.replace(/<span>\s*<\/span>/gi, '')
|
|
67
|
+
.replace(/<font[^>]*>([\s\S]*?)<\/font>/gi, '$1');
|
|
68
|
+
}
|
|
69
|
+
function normalizeQuillWhitespace(html) {
|
|
70
|
+
const placeholders = [];
|
|
71
|
+
const withPlaceholders = html.replace(PRESERVE_BLOCK_PATTERN, (match) => {
|
|
72
|
+
const token = `${PRESERVE_TOKEN_PREFIX}${placeholders.length}${PRESERVE_TOKEN_SUFFIX}`;
|
|
73
|
+
placeholders.push(match);
|
|
74
|
+
return token;
|
|
75
|
+
});
|
|
76
|
+
const normalized = withPlaceholders
|
|
77
|
+
.replace(/ | | |\u00A0/gi, ' ')
|
|
78
|
+
.replace(/<br\s*\/?>(?!\s*<\/p>)/gi, ' ')
|
|
79
|
+
.replace(/<p><br\s*\/?><\/p>/gi, '')
|
|
80
|
+
.replace(/<p>\s*<\/p>/gi, '')
|
|
81
|
+
.replace(/<br\s*\/?>/gi, '')
|
|
82
|
+
.replace(/>([^<]+)</g, function (_match, text) {
|
|
83
|
+
return `>${text.replace(/[ \t]{2,}/g, ' ')}<`;
|
|
84
|
+
});
|
|
85
|
+
return normalized.replace(PRESERVE_TOKEN_PATTERN, function (_match, idx) {
|
|
86
|
+
return placeholders[Number(idx)] || '';
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Кэш результата по композитному ключу (опции + html). За один рендер
|
|
91
|
+
* страницы один и тот же `importantInfo`/`prepareText` может попасть в
|
|
92
|
+
* Typography.Text несколько раз (sidebar + основной блок), а контент
|
|
93
|
+
* договоров — это десятки КБ HTML. Ключи — конкретные строки полей из API,
|
|
94
|
+
* их за сессию ограниченное число, освобождаются с закрытием вкладки.
|
|
95
|
+
*/
|
|
96
|
+
const cache = new Map();
|
|
97
|
+
function sanitizeQuillHtml(html, options) {
|
|
98
|
+
if (!html)
|
|
99
|
+
return '';
|
|
100
|
+
const stripColors = options?.stripColors ? '1' : '0';
|
|
101
|
+
const key = `${stripColors}|${html}`;
|
|
102
|
+
const cached = cache.get(key);
|
|
103
|
+
if (cached !== undefined)
|
|
104
|
+
return cached;
|
|
105
|
+
const colorStripped = options?.stripColors ? stripQuillColors(html) : html;
|
|
106
|
+
const result = normalizeQuillWhitespace(colorStripped);
|
|
107
|
+
cache.set(key, result);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.132",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -14,11 +14,12 @@
|
|
|
14
14
|
"test": "jest"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@escapenavigator/types": "^1.10.
|
|
17
|
+
"@escapenavigator/types": "^1.10.128",
|
|
18
18
|
"axios": "^0.21.4",
|
|
19
19
|
"class-transformer": "^0.5.1",
|
|
20
20
|
"class-validator": "^0.13.2",
|
|
21
|
-
"i18next": "^21.6.4"
|
|
21
|
+
"i18next": "^21.6.4",
|
|
22
|
+
"quill-delta": "^5.1.0"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/jest": "^29.5.10",
|
|
@@ -26,5 +27,5 @@
|
|
|
26
27
|
"ts-jest": "^29.1.1",
|
|
27
28
|
"typescript": "^5.6"
|
|
28
29
|
},
|
|
29
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "f314b83d632fc068719426b472f60ab03213d533"
|
|
30
31
|
}
|