@escapenavigator/utils 1.10.131 → 1.10.133
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.
- package/dist/booking-lead-time/index.d.ts +31 -0
- package/dist/booking-lead-time/index.js +52 -0
- package/dist/booking-lead-time/index.test.d.ts +1 -0
- package/dist/booking-lead-time/index.test.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/quill-clipboard-matchers.d.ts +64 -0
- package/dist/quill-clipboard-matchers.js +98 -0
- package/dist/sanitize-quill-html.d.ts +34 -0
- package/dist/sanitize-quill-html.js +109 -0
- package/dist/serialize-slot.js +2 -1
- package/package.json +5 -4
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const BOOKING_LEAD_TIME_MINUTE_STEP = 5;
|
|
2
|
+
/** Максимум в селекте «часы» (7 суток). */
|
|
3
|
+
export declare const BOOKING_LEAD_TIME_MAX_HOURS = 168;
|
|
4
|
+
export declare const BOOKING_LEAD_TIME_MAX_MINUTES: number;
|
|
5
|
+
export type BookingLeadTimeParts = {
|
|
6
|
+
hours: number;
|
|
7
|
+
minutes: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const roundBookingLeadMinutesToStep: (totalMinutes: number, step?: number) => number;
|
|
10
|
+
export declare const minutesToBookingLeadParts: (totalMinutes: number) => BookingLeadTimeParts;
|
|
11
|
+
export declare const bookingLeadPartsToMinutes: (hours: number, minutes: number) => number;
|
|
12
|
+
export declare const buildBookingLeadHourOptions: () => Array<{
|
|
13
|
+
key: number;
|
|
14
|
+
content: number;
|
|
15
|
+
}>;
|
|
16
|
+
export declare const buildBookingLeadMinuteOptions: () => Array<{
|
|
17
|
+
key: number;
|
|
18
|
+
content: number;
|
|
19
|
+
}>;
|
|
20
|
+
type RuleLeadTimeLegacy = {
|
|
21
|
+
blockingMinutes?: number | null;
|
|
22
|
+
blockingHours?: number | null;
|
|
23
|
+
minMinutesForBooking?: number | null;
|
|
24
|
+
minHoursForBooking?: number | null;
|
|
25
|
+
};
|
|
26
|
+
export declare const resolveBlockingMinutes: (rule?: RuleLeadTimeLegacy | null) => number;
|
|
27
|
+
export declare const resolveMinMinutesForBooking: (rule?: RuleLeadTimeLegacy | null) => number;
|
|
28
|
+
type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
|
|
29
|
+
/** Человекочитаемый интервал до начала игры для таблиц и описаний. */
|
|
30
|
+
export declare const formatBookingLeadTime: (totalMinutes: number, t: TranslateFn) => string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatBookingLeadTime = exports.resolveMinMinutesForBooking = exports.resolveBlockingMinutes = exports.buildBookingLeadMinuteOptions = exports.buildBookingLeadHourOptions = exports.bookingLeadPartsToMinutes = exports.minutesToBookingLeadParts = exports.roundBookingLeadMinutesToStep = exports.BOOKING_LEAD_TIME_MAX_MINUTES = exports.BOOKING_LEAD_TIME_MAX_HOURS = exports.BOOKING_LEAD_TIME_MINUTE_STEP = void 0;
|
|
4
|
+
exports.BOOKING_LEAD_TIME_MINUTE_STEP = 5;
|
|
5
|
+
/** Максимум в селекте «часы» (7 суток). */
|
|
6
|
+
exports.BOOKING_LEAD_TIME_MAX_HOURS = 168;
|
|
7
|
+
exports.BOOKING_LEAD_TIME_MAX_MINUTES = exports.BOOKING_LEAD_TIME_MAX_HOURS * 60 + 55;
|
|
8
|
+
const roundBookingLeadMinutesToStep = (totalMinutes, step = exports.BOOKING_LEAD_TIME_MINUTE_STEP) => Math.max(0, Math.round(totalMinutes / step) * step);
|
|
9
|
+
exports.roundBookingLeadMinutesToStep = roundBookingLeadMinutesToStep;
|
|
10
|
+
const minutesToBookingLeadParts = (totalMinutes) => {
|
|
11
|
+
const normalized = (0, exports.roundBookingLeadMinutesToStep)(Math.max(0, totalMinutes || 0));
|
|
12
|
+
const hours = Math.min(exports.BOOKING_LEAD_TIME_MAX_HOURS, Math.floor(normalized / 60));
|
|
13
|
+
const minutes = Math.min(55, normalized - hours * 60);
|
|
14
|
+
return { hours, minutes };
|
|
15
|
+
};
|
|
16
|
+
exports.minutesToBookingLeadParts = minutesToBookingLeadParts;
|
|
17
|
+
const bookingLeadPartsToMinutes = (hours, minutes) => Math.min(exports.BOOKING_LEAD_TIME_MAX_MINUTES, Math.max(0, hours) * 60 + (0, exports.roundBookingLeadMinutesToStep)(minutes));
|
|
18
|
+
exports.bookingLeadPartsToMinutes = bookingLeadPartsToMinutes;
|
|
19
|
+
const buildBookingLeadHourOptions = () => Array.from({ length: exports.BOOKING_LEAD_TIME_MAX_HOURS + 1 }, (_, hours) => ({
|
|
20
|
+
key: hours,
|
|
21
|
+
content: hours,
|
|
22
|
+
}));
|
|
23
|
+
exports.buildBookingLeadHourOptions = buildBookingLeadHourOptions;
|
|
24
|
+
const buildBookingLeadMinuteOptions = () => Array.from({ length: 60 / exports.BOOKING_LEAD_TIME_MINUTE_STEP }, (_, i) => {
|
|
25
|
+
const minutes = i * exports.BOOKING_LEAD_TIME_MINUTE_STEP;
|
|
26
|
+
return { key: minutes, content: minutes };
|
|
27
|
+
});
|
|
28
|
+
exports.buildBookingLeadMinuteOptions = buildBookingLeadMinuteOptions;
|
|
29
|
+
const resolveBlockingMinutes = (rule) => {
|
|
30
|
+
if (rule?.blockingMinutes != null)
|
|
31
|
+
return rule.blockingMinutes;
|
|
32
|
+
return (rule?.blockingHours ?? 0) * 60;
|
|
33
|
+
};
|
|
34
|
+
exports.resolveBlockingMinutes = resolveBlockingMinutes;
|
|
35
|
+
const resolveMinMinutesForBooking = (rule) => {
|
|
36
|
+
if (rule?.minMinutesForBooking != null)
|
|
37
|
+
return rule.minMinutesForBooking;
|
|
38
|
+
return (rule?.minHoursForBooking ?? 0) * 60;
|
|
39
|
+
};
|
|
40
|
+
exports.resolveMinMinutesForBooking = resolveMinMinutesForBooking;
|
|
41
|
+
/** Человекочитаемый интервал до начала игры для таблиц и описаний. */
|
|
42
|
+
const formatBookingLeadTime = (totalMinutes, t) => {
|
|
43
|
+
const { hours, minutes } = (0, exports.minutesToBookingLeadParts)(totalMinutes);
|
|
44
|
+
if (hours > 0 && minutes > 0) {
|
|
45
|
+
return `${hours} ${t('часов')} ${minutes} ${t('мин')}`;
|
|
46
|
+
}
|
|
47
|
+
if (hours > 0) {
|
|
48
|
+
return `${hours} ${t('часов')}`;
|
|
49
|
+
}
|
|
50
|
+
return `${minutes} ${t('мин')}`;
|
|
51
|
+
};
|
|
52
|
+
exports.formatBookingLeadTime = formatBookingLeadTime;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("./index");
|
|
4
|
+
describe('booking-lead-time', () => {
|
|
5
|
+
it('splits and merges hours and minutes', () => {
|
|
6
|
+
expect((0, index_1.minutesToBookingLeadParts)(90)).toEqual({ hours: 1, minutes: 30 });
|
|
7
|
+
expect((0, index_1.bookingLeadPartsToMinutes)(1, 30)).toBe(90);
|
|
8
|
+
});
|
|
9
|
+
it('rounds to 5-minute step', () => {
|
|
10
|
+
expect((0, index_1.roundBookingLeadMinutesToStep)(32)).toBe(30);
|
|
11
|
+
expect((0, index_1.roundBookingLeadMinutesToStep)(33)).toBe(35);
|
|
12
|
+
});
|
|
13
|
+
it('resolves minutes with legacy hours fallback', () => {
|
|
14
|
+
expect((0, index_1.resolveBlockingMinutes)({ blockingHours: 2 })).toBe(120);
|
|
15
|
+
expect((0, index_1.resolveMinMinutesForBooking)({ minHoursForBooking: 48 })).toBe(2880);
|
|
16
|
+
expect((0, index_1.resolveBlockingMinutes)({ blockingMinutes: 30, blockingHours: 2 })).toBe(30);
|
|
17
|
+
});
|
|
18
|
+
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./agreement-config"), exports);
|
|
18
|
+
__exportStar(require("./booking-lead-time"), exports);
|
|
18
19
|
__exportStar(require("./convert-hhmm-to-minutes"), exports);
|
|
19
20
|
__exportStar(require("./convert-minutes-to-hhmm"), exports);
|
|
20
21
|
__exportStar(require("./convert-to-options"), exports);
|
|
@@ -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/dist/serialize-slot.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.serializeSlot = exports.serializeSlotOrderData = void 0;
|
|
4
4
|
const defailt_rule_1 = require("@escapenavigator/types/dist/slot-rule/defailt-rule");
|
|
5
5
|
const buildRuleSummary = (rule) => {
|
|
6
|
-
const { prepayment, prepaymentType, title, minHoursForBooking, minHoursForFreeCanceling, cancelationRule, minHoursForFullFine, cancelationAmount, } = rule || defailt_rule_1.defaultRule;
|
|
6
|
+
const { prepayment, prepaymentType, title, minHoursForBooking, minMinutesForBooking, minHoursForFreeCanceling, cancelationRule, minHoursForFullFine, cancelationAmount, } = rule || defailt_rule_1.defaultRule;
|
|
7
7
|
return {
|
|
8
8
|
prepayment,
|
|
9
9
|
minHoursForFullFine,
|
|
@@ -11,6 +11,7 @@ const buildRuleSummary = (rule) => {
|
|
|
11
11
|
prepaymentType,
|
|
12
12
|
title,
|
|
13
13
|
minHoursForBooking,
|
|
14
|
+
minMinutesForBooking: minMinutesForBooking ?? (minHoursForBooking ?? 0) * 60,
|
|
14
15
|
minHoursForFreeCanceling,
|
|
15
16
|
cancelationRule,
|
|
16
17
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.133",
|
|
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.129",
|
|
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": "25c61a2bd7e70a03406358540c8b2b7b2d76bd3a"
|
|
30
31
|
}
|