@escapenavigator/utils 1.10.133 → 1.10.135
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.js +3 -3
- package/dist/email-builder/from-html.d.ts +149 -0
- package/dist/email-builder/from-html.js +1017 -0
- package/dist/email-builder/from-html.test.d.ts +1 -0
- package/dist/email-builder/from-html.test.js +753 -0
- package/dist/email-builder/placeholders.d.ts +89 -0
- package/dist/email-builder/placeholders.js +176 -0
- package/dist/email-builder/strip-html.d.ts +31 -0
- package/dist/email-builder/strip-html.js +73 -0
- package/dist/email-builder/strip-html.test.d.ts +1 -0
- package/dist/email-builder/strip-html.test.js +99 -0
- package/dist/format-flex-duration-label/index.d.ts +7 -0
- package/dist/format-flex-duration-label/index.js +21 -0
- package/dist/format-flex-duration-label/index.test.d.ts +1 -0
- package/dist/format-flex-duration-label/index.test.js +24 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/promocode-nominal-rules.d.ts +29 -4
- package/dist/promocode-nominal-rules.js +38 -7
- package/dist/promocode-nominal-rules.spec.js +68 -6
- package/dist/sanitize-quill-html.d.ts +16 -0
- package/dist/sanitize-quill-html.js +17 -3
- package/dist/sanitize-quill-html.test.d.ts +8 -0
- package/dist/sanitize-quill-html.test.js +71 -0
- package/dist/strip-html-tags.d.ts +13 -0
- package/dist/strip-html-tags.js +24 -0
- package/package.json +4 -3
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.findNominalRule = findNominalRule;
|
|
4
4
|
exports.isPromocodeApplicableToNominal = isPromocodeApplicableToNominal;
|
|
5
|
+
exports.getCertificateMode = getCertificateMode;
|
|
5
6
|
exports.resolveEffectiveNominalDiscount = resolveEffectiveNominalDiscount;
|
|
7
|
+
exports.resolveEffectiveNominalBonus = resolveEffectiveNominalBonus;
|
|
8
|
+
const promocode_certificate_mode_enum_1 = require("@escapenavigator/types/dist/promocode/emun/promocode-certificate-mode.enum");
|
|
6
9
|
/**
|
|
7
10
|
* Поиск правила промокода для конкретного номинала. Возвращает запись из
|
|
8
11
|
* `certificateNominalRules` или `null`, если правила нет.
|
|
@@ -15,7 +18,7 @@ function findNominalRule(promocode, nominalId) {
|
|
|
15
18
|
/**
|
|
16
19
|
* Применим ли промокод к данному номиналу: scope охватывает сертификаты и есть
|
|
17
20
|
* явное правило для `nominal.id`. Если правил нет — промокод к номиналу не
|
|
18
|
-
* применяется (магазин показывает базовую скидку номинала).
|
|
21
|
+
* применяется (магазин показывает базовую скидку номинала и не начисляет бонус).
|
|
19
22
|
*/
|
|
20
23
|
function isPromocodeApplicableToNominal(promocode, nominal) {
|
|
21
24
|
if (!promocode?.availableForCertificates)
|
|
@@ -23,17 +26,45 @@ function isPromocodeApplicableToNominal(promocode, nominal) {
|
|
|
23
26
|
return findNominalRule(promocode, nominal.id) !== null;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
|
-
*
|
|
29
|
+
* Режим действия для сертификатов с дефолтом в `DISCOUNT` для старых промокодов
|
|
30
|
+
* без поля `certificateMode`.
|
|
31
|
+
*/
|
|
32
|
+
function getCertificateMode(promocode) {
|
|
33
|
+
return promocode?.certificateMode ?? promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Эффективная скидка номинала на цену покупки сертификата.
|
|
27
37
|
* Возвращает значение в минимальных единицах валюты.
|
|
28
38
|
*
|
|
29
|
-
* - Промокод применим к номиналу →
|
|
30
|
-
* -
|
|
39
|
+
* - Промокод применим к номиналу и режим = `DISCOUNT` → значение из правила.
|
|
40
|
+
* - Режим = `BONUS` или промокод не применим → базовый `nominal.discount` (или 0).
|
|
41
|
+
*
|
|
42
|
+
* Помните: для режима `BONUS` цена не уменьшается, бонус начисляется отдельно
|
|
43
|
+
* (см. `resolveEffectiveNominalBonus`).
|
|
31
44
|
*/
|
|
32
45
|
function resolveEffectiveNominalDiscount(promocode, nominal) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
const applicable = isPromocodeApplicableToNominal(promocode, nominal);
|
|
47
|
+
const isDiscountMode = getCertificateMode(promocode) === promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT;
|
|
48
|
+
const rule = applicable && isDiscountMode ? findNominalRule(promocode, nominal.id) : null;
|
|
36
49
|
if (rule)
|
|
37
50
|
return Math.max(0, rule.discount || 0);
|
|
38
51
|
return Math.max(0, nominal.discount || 0);
|
|
39
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Эффективный бонус к балансу сертификата от промокода.
|
|
55
|
+
* Возвращает значение в минимальных единицах валюты.
|
|
56
|
+
*
|
|
57
|
+
* - Промокод применим к номиналу и режим = `BONUS` → значение из правила.
|
|
58
|
+
* - В остальных случаях → 0.
|
|
59
|
+
*
|
|
60
|
+
* Бонус НЕ уменьшает цену покупки — он прибавляется к усваиваемому
|
|
61
|
+
* балансу сертификата (`nominal + bonus - usedAmount`).
|
|
62
|
+
*/
|
|
63
|
+
function resolveEffectiveNominalBonus(promocode, nominal) {
|
|
64
|
+
if (!isPromocodeApplicableToNominal(promocode, nominal))
|
|
65
|
+
return 0;
|
|
66
|
+
if (getCertificateMode(promocode) !== promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS)
|
|
67
|
+
return 0;
|
|
68
|
+
const rule = findNominalRule(promocode, nominal.id);
|
|
69
|
+
return Math.max(0, rule?.discount || 0);
|
|
70
|
+
}
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const promocode_certificate_mode_enum_1 = require("@escapenavigator/types/dist/promocode/emun/promocode-certificate-mode.enum");
|
|
3
4
|
const promocode_nominal_rules_1 = require("./promocode-nominal-rules");
|
|
4
5
|
const baseNominal = { id: 10, discount: 500 };
|
|
5
|
-
const
|
|
6
|
+
const promocodeDiscountWithRule = {
|
|
6
7
|
availableForCertificates: true,
|
|
8
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
|
|
9
|
+
certificateNominalRules: [
|
|
10
|
+
{ nominalId: 10, discount: 1500 },
|
|
11
|
+
{ nominalId: 20, discount: 700 },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
const promocodeLegacyNoMode = {
|
|
15
|
+
availableForCertificates: true,
|
|
16
|
+
certificateNominalRules: [{ nominalId: 10, discount: 1500 }],
|
|
17
|
+
};
|
|
18
|
+
const promocodeBonusWithRule = {
|
|
19
|
+
availableForCertificates: true,
|
|
20
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
|
|
7
21
|
certificateNominalRules: [
|
|
8
22
|
{ nominalId: 10, discount: 1500 },
|
|
9
23
|
{ nominalId: 20, discount: 700 },
|
|
@@ -11,21 +25,23 @@ const promocodeWithRule = {
|
|
|
11
25
|
};
|
|
12
26
|
const promocodeWithoutRule = {
|
|
13
27
|
availableForCertificates: true,
|
|
28
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
|
|
14
29
|
certificateNominalRules: [{ nominalId: 99, discount: 1500 }],
|
|
15
30
|
};
|
|
16
31
|
const promocodeBookingsOnly = {
|
|
17
32
|
availableForCertificates: false,
|
|
33
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
|
|
18
34
|
certificateNominalRules: [{ nominalId: 10, discount: 1500 }],
|
|
19
35
|
};
|
|
20
36
|
describe('findNominalRule', () => {
|
|
21
37
|
it('returns rule when nominal matches', () => {
|
|
22
|
-
expect((0, promocode_nominal_rules_1.findNominalRule)(
|
|
38
|
+
expect((0, promocode_nominal_rules_1.findNominalRule)(promocodeDiscountWithRule, 10)).toEqual({
|
|
23
39
|
nominalId: 10,
|
|
24
40
|
discount: 1500,
|
|
25
41
|
});
|
|
26
42
|
});
|
|
27
43
|
it('returns null when nominal not in rules', () => {
|
|
28
|
-
expect((0, promocode_nominal_rules_1.findNominalRule)(
|
|
44
|
+
expect((0, promocode_nominal_rules_1.findNominalRule)(promocodeDiscountWithRule, 999)).toBeNull();
|
|
29
45
|
});
|
|
30
46
|
it('returns null on empty/missing data', () => {
|
|
31
47
|
expect((0, promocode_nominal_rules_1.findNominalRule)(null, 10)).toBeNull();
|
|
@@ -36,7 +52,10 @@ describe('findNominalRule', () => {
|
|
|
36
52
|
});
|
|
37
53
|
describe('isPromocodeApplicableToNominal', () => {
|
|
38
54
|
it('true only when scope=certificates and rule exists', () => {
|
|
39
|
-
expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(
|
|
55
|
+
expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeDiscountWithRule, baseNominal)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('true for bonus-mode promocodes when scope=certificates and rule exists', () => {
|
|
58
|
+
expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeBonusWithRule, baseNominal)).toBe(true);
|
|
40
59
|
});
|
|
41
60
|
it('false when no rule for nominal', () => {
|
|
42
61
|
expect((0, promocode_nominal_rules_1.isPromocodeApplicableToNominal)(promocodeWithoutRule, baseNominal)).toBe(false);
|
|
@@ -49,8 +68,14 @@ describe('isPromocodeApplicableToNominal', () => {
|
|
|
49
68
|
});
|
|
50
69
|
});
|
|
51
70
|
describe('resolveEffectiveNominalDiscount', () => {
|
|
52
|
-
it('returns rule discount when applicable', () => {
|
|
53
|
-
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(
|
|
71
|
+
it('returns rule discount when applicable and mode=discount', () => {
|
|
72
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeDiscountWithRule, baseNominal)).toBe(1500);
|
|
73
|
+
});
|
|
74
|
+
it('returns rule discount for legacy promocode without certificateMode (defaults to DISCOUNT)', () => {
|
|
75
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeLegacyNoMode, baseNominal)).toBe(1500);
|
|
76
|
+
});
|
|
77
|
+
it('returns base discount (not rule) when mode=bonus', () => {
|
|
78
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeBonusWithRule, baseNominal)).toBe(500);
|
|
54
79
|
});
|
|
55
80
|
it('returns base nominal discount when no rule', () => {
|
|
56
81
|
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(promocodeWithoutRule, baseNominal)).toBe(500);
|
|
@@ -62,6 +87,7 @@ describe('resolveEffectiveNominalDiscount', () => {
|
|
|
62
87
|
it('clamps negative rule discount to 0', () => {
|
|
63
88
|
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)({
|
|
64
89
|
availableForCertificates: true,
|
|
90
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.DISCOUNT,
|
|
65
91
|
certificateNominalRules: [{ nominalId: 10, discount: -50 }],
|
|
66
92
|
}, baseNominal)).toBe(0);
|
|
67
93
|
});
|
|
@@ -72,3 +98,39 @@ describe('resolveEffectiveNominalDiscount', () => {
|
|
|
72
98
|
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalDiscount)(null, { id: 10 })).toBe(0);
|
|
73
99
|
});
|
|
74
100
|
});
|
|
101
|
+
describe('resolveEffectiveNominalBonus', () => {
|
|
102
|
+
it('returns rule value when applicable and mode=bonus', () => {
|
|
103
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(promocodeBonusWithRule, baseNominal)).toBe(1500);
|
|
104
|
+
});
|
|
105
|
+
it('returns 0 when mode=discount', () => {
|
|
106
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(promocodeDiscountWithRule, baseNominal)).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
it('returns 0 for legacy promocode without certificateMode (defaults to DISCOUNT)', () => {
|
|
109
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(promocodeLegacyNoMode, baseNominal)).toBe(0);
|
|
110
|
+
});
|
|
111
|
+
it('returns 0 when no rule for nominal', () => {
|
|
112
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)({
|
|
113
|
+
availableForCertificates: true,
|
|
114
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
|
|
115
|
+
certificateNominalRules: [{ nominalId: 99, discount: 1500 }],
|
|
116
|
+
}, baseNominal)).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
it('returns 0 when scope excludes certificates', () => {
|
|
119
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)({
|
|
120
|
+
availableForCertificates: false,
|
|
121
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
|
|
122
|
+
certificateNominalRules: [{ nominalId: 10, discount: 1500 }],
|
|
123
|
+
}, baseNominal)).toBe(0);
|
|
124
|
+
});
|
|
125
|
+
it('returns 0 on missing promocode', () => {
|
|
126
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(null, baseNominal)).toBe(0);
|
|
127
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)(undefined, baseNominal)).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
it('clamps negative rule value to 0', () => {
|
|
130
|
+
expect((0, promocode_nominal_rules_1.resolveEffectiveNominalBonus)({
|
|
131
|
+
availableForCertificates: true,
|
|
132
|
+
certificateMode: promocode_certificate_mode_enum_1.PromocodeCertificateModeEnum.BONUS,
|
|
133
|
+
certificateNominalRules: [{ nominalId: 10, discount: -100 }],
|
|
134
|
+
}, baseNominal)).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -28,7 +28,23 @@
|
|
|
28
28
|
export type SanitizeQuillHtmlOptions = {
|
|
29
29
|
/** Снять inline-цвета/фоны и `ql-color-*`/`ql-background-*` классы. */
|
|
30
30
|
stripColors?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Удалить `<a>`-обёртки, сохранив только текст внутри. Используется
|
|
33
|
+
* для полей с `noLinks={true}` (`howToFind` / `prepareText` локации,
|
|
34
|
+
* importantInfo квеста и пр.), где ссылка только мешает: рендерится
|
|
35
|
+
* в orders/widget на разных темах, ссылается куда попало, ломает
|
|
36
|
+
* автогенерируемые UTM. Сам ReactQuill кнопку link в toolbar'е
|
|
37
|
+
* прячет через `noLinks`, но paste из браузера/Word всё равно
|
|
38
|
+
* протаскивает `<a>` атомарно — этот пост-санитайзер закрывает дыру.
|
|
39
|
+
*/
|
|
40
|
+
stripLinks?: boolean;
|
|
31
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* Удаляет `<a>`-обёртки, сохраняя текст. Поддерживает вложенные форматы
|
|
44
|
+
* внутри ссылки (`<a><strong>text</strong></a>` → `<strong>text</strong>`).
|
|
45
|
+
* Идемпотентна: повторный вызов на уже очищенном HTML возвращает то же.
|
|
46
|
+
*/
|
|
47
|
+
export declare function stripQuillLinks(html: string): string;
|
|
32
48
|
export declare function stripQuillColors(html: string): string;
|
|
33
49
|
export declare function normalizeQuillWhitespace(html: string): string;
|
|
34
50
|
export declare function sanitizeQuillHtml(html: string | null | undefined, options?: SanitizeQuillHtmlOptions): string;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* на стороне клиентских настроек, см. `client-agreement.tsx`).
|
|
28
28
|
*/
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.stripQuillLinks = stripQuillLinks;
|
|
30
31
|
exports.stripQuillColors = stripQuillColors;
|
|
31
32
|
exports.normalizeQuillWhitespace = normalizeQuillWhitespace;
|
|
32
33
|
exports.sanitizeQuillHtml = sanitizeQuillHtml;
|
|
@@ -47,6 +48,14 @@ function cleanStyleAttribute(style) {
|
|
|
47
48
|
})
|
|
48
49
|
.join('; ');
|
|
49
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Удаляет `<a>`-обёртки, сохраняя текст. Поддерживает вложенные форматы
|
|
53
|
+
* внутри ссылки (`<a><strong>text</strong></a>` → `<strong>text</strong>`).
|
|
54
|
+
* Идемпотентна: повторный вызов на уже очищенном HTML возвращает то же.
|
|
55
|
+
*/
|
|
56
|
+
function stripQuillLinks(html) {
|
|
57
|
+
return html.replace(/<a\b[^>]*>([\s\S]*?)<\/a>/gi, '$1');
|
|
58
|
+
}
|
|
50
59
|
function stripQuillColors(html) {
|
|
51
60
|
return html
|
|
52
61
|
.replace(/\s*style="([^"]*)"/gi, (_match, styleContent) => {
|
|
@@ -98,12 +107,17 @@ function sanitizeQuillHtml(html, options) {
|
|
|
98
107
|
if (!html)
|
|
99
108
|
return '';
|
|
100
109
|
const stripColors = options?.stripColors ? '1' : '0';
|
|
101
|
-
const
|
|
110
|
+
const stripLinks = options?.stripLinks ? '1' : '0';
|
|
111
|
+
const key = `${stripColors}|${stripLinks}|${html}`;
|
|
102
112
|
const cached = cache.get(key);
|
|
103
113
|
if (cached !== undefined)
|
|
104
114
|
return cached;
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
let intermediate = html;
|
|
116
|
+
if (options?.stripColors)
|
|
117
|
+
intermediate = stripQuillColors(intermediate);
|
|
118
|
+
if (options?.stripLinks)
|
|
119
|
+
intermediate = stripQuillLinks(intermediate);
|
|
120
|
+
const result = normalizeQuillWhitespace(intermediate);
|
|
107
121
|
cache.set(key, result);
|
|
108
122
|
return result;
|
|
109
123
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Узкая проверка опций `sanitizeQuillHtml` (особенно `stripLinks`,
|
|
3
|
+
* добавленной для полей с `noLinks={true}` в location-форме).
|
|
4
|
+
*
|
|
5
|
+
* Полная регрессия `normalizeQuillWhitespace` / `stripQuillColors` —
|
|
6
|
+
* предмет отдельных рег-тестов; здесь ловим именно регрессию ссылок.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Узкая проверка опций `sanitizeQuillHtml` (особенно `stripLinks`,
|
|
4
|
+
* добавленной для полей с `noLinks={true}` в location-форме).
|
|
5
|
+
*
|
|
6
|
+
* Полная регрессия `normalizeQuillWhitespace` / `stripQuillColors` —
|
|
7
|
+
* предмет отдельных рег-тестов; здесь ловим именно регрессию ссылок.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const sanitize_quill_html_1 = require("./sanitize-quill-html");
|
|
11
|
+
describe('stripQuillLinks', () => {
|
|
12
|
+
it('убирает `<a>`-обёртку, оставляет текст', () => {
|
|
13
|
+
const html = 'click <a href="https://example.com">here</a> to donate';
|
|
14
|
+
expect((0, sanitize_quill_html_1.stripQuillLinks)(html)).toBe('click here to donate');
|
|
15
|
+
});
|
|
16
|
+
it('поддерживает вложенные форматы внутри ссылки', () => {
|
|
17
|
+
const html = '<strong><a href="https://x.com" rel="noopener" target="_blank">here</a></strong>';
|
|
18
|
+
expect((0, sanitize_quill_html_1.stripQuillLinks)(html)).toBe('<strong>here</strong>');
|
|
19
|
+
});
|
|
20
|
+
it('обрабатывает несколько ссылок', () => {
|
|
21
|
+
const html = '<a href="a">one</a> and <a href="b">two</a>';
|
|
22
|
+
expect((0, sanitize_quill_html_1.stripQuillLinks)(html)).toBe('one and two');
|
|
23
|
+
});
|
|
24
|
+
it('идемпотентна', () => {
|
|
25
|
+
const html = 'click <a href="x">here</a>';
|
|
26
|
+
expect((0, sanitize_quill_html_1.stripQuillLinks)((0, sanitize_quill_html_1.stripQuillLinks)(html))).toBe((0, sanitize_quill_html_1.stripQuillLinks)(html));
|
|
27
|
+
});
|
|
28
|
+
it('игнорирует пустой ввод', () => {
|
|
29
|
+
expect((0, sanitize_quill_html_1.stripQuillLinks)('')).toBe('');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('sanitizeQuillHtml — combo `stripLinks` + nbsp + colors', () => {
|
|
33
|
+
it('по умолчанию ссылки сохраняет, нормализует', () => {
|
|
34
|
+
const html = '<p><a href="x">link</a> text</p>';
|
|
35
|
+
expect((0, sanitize_quill_html_1.sanitizeQuillHtml)(html)).toBe('<p><a href="x">link</a> text</p>');
|
|
36
|
+
});
|
|
37
|
+
it('`stripLinks: true` убирает ссылку, оставляет текст и нормализует пробелы', () => {
|
|
38
|
+
const html = '<p>click <a href="https://x">here</a> to donate</p>';
|
|
39
|
+
expect((0, sanitize_quill_html_1.sanitizeQuillHtml)(html, { stripLinks: true })).toBe('<p>click here to donate</p>');
|
|
40
|
+
});
|
|
41
|
+
it('`stripColors: true` + `stripLinks: true` чистит и цвет, и ссылку', () => {
|
|
42
|
+
const html = '<p style="color: red;"><a href="x" style="color: red;">link</a> text</p>';
|
|
43
|
+
const result = (0, sanitize_quill_html_1.sanitizeQuillHtml)(html, { stripColors: true, stripLinks: true });
|
|
44
|
+
expect(result).not.toContain('<a');
|
|
45
|
+
expect(result).not.toContain('color');
|
|
46
|
+
expect(result).toContain('link');
|
|
47
|
+
});
|
|
48
|
+
it('кеш разделяет результаты для разных опций', () => {
|
|
49
|
+
const html = '<a href="x">y</a>';
|
|
50
|
+
const withLinks = (0, sanitize_quill_html_1.sanitizeQuillHtml)(html);
|
|
51
|
+
const stripped = (0, sanitize_quill_html_1.sanitizeQuillHtml)(html, { stripLinks: true });
|
|
52
|
+
expect(withLinks).toContain('<a');
|
|
53
|
+
expect(stripped).not.toContain('<a');
|
|
54
|
+
// Повторный вызов с теми же опциями возвращает тот же результат
|
|
55
|
+
expect((0, sanitize_quill_html_1.sanitizeQuillHtml)(html, { stripLinks: true })).toBe(stripped);
|
|
56
|
+
});
|
|
57
|
+
it('пустой/null/undefined ввод → пустая строка', () => {
|
|
58
|
+
expect((0, sanitize_quill_html_1.sanitizeQuillHtml)('', { stripLinks: true })).toBe('');
|
|
59
|
+
expect((0, sanitize_quill_html_1.sanitizeQuillHtml)(null, { stripLinks: true })).toBe('');
|
|
60
|
+
expect((0, sanitize_quill_html_1.sanitizeQuillHtml)(undefined, { stripLinks: true })).toBe('');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('stripQuillColors smoke', () => {
|
|
64
|
+
it('убирает inline color/background style', () => {
|
|
65
|
+
const html = '<p style="color: red; background: yellow;">text</p>';
|
|
66
|
+
const result = (0, sanitize_quill_html_1.stripQuillColors)(html);
|
|
67
|
+
expect(result).not.toContain('color');
|
|
68
|
+
expect(result).not.toContain('background');
|
|
69
|
+
expect(result).toContain('text');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Убираем HTML-теги из строки и схлопываем пробелы.
|
|
3
|
+
*
|
|
4
|
+
* Используется, когда поле раньше редактировалось через Quill (и в БД мог
|
|
5
|
+
* попасть HTML), а теперь рендерится в обычном `<Textarea>`/inline-тексте.
|
|
6
|
+
* Если не вычистить, пользователь видит `<p>текст</p>` буквально (см. `teaser`
|
|
7
|
+
* в форме редактирования квеста: на момент миграции с Quill на Textarea в
|
|
8
|
+
* локалях остались записи с открывающими/закрывающими тегами).
|
|
9
|
+
*
|
|
10
|
+
* Функция чистая и идемпотентная: повторный вызов на уже очищенной строке
|
|
11
|
+
* возвращает её же.
|
|
12
|
+
*/
|
|
13
|
+
export declare function stripHtmlTags(text: string | null | undefined): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.stripHtmlTags = stripHtmlTags;
|
|
4
|
+
/**
|
|
5
|
+
* Убираем HTML-теги из строки и схлопываем пробелы.
|
|
6
|
+
*
|
|
7
|
+
* Используется, когда поле раньше редактировалось через Quill (и в БД мог
|
|
8
|
+
* попасть HTML), а теперь рендерится в обычном `<Textarea>`/inline-тексте.
|
|
9
|
+
* Если не вычистить, пользователь видит `<p>текст</p>` буквально (см. `teaser`
|
|
10
|
+
* в форме редактирования квеста: на момент миграции с Quill на Textarea в
|
|
11
|
+
* локалях остались записи с открывающими/закрывающими тегами).
|
|
12
|
+
*
|
|
13
|
+
* Функция чистая и идемпотентная: повторный вызов на уже очищенной строке
|
|
14
|
+
* возвращает её же.
|
|
15
|
+
*/
|
|
16
|
+
function stripHtmlTags(text) {
|
|
17
|
+
if (!text)
|
|
18
|
+
return '';
|
|
19
|
+
return text
|
|
20
|
+
.replace(/<[^>]+>/g, ' ')
|
|
21
|
+
.replace(/ | | |\u00A0/gi, ' ')
|
|
22
|
+
.replace(/\s+/g, ' ')
|
|
23
|
+
.trim();
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@escapenavigator/utils",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.135",
|
|
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.131",
|
|
18
18
|
"axios": "^0.21.4",
|
|
19
19
|
"class-transformer": "^0.5.1",
|
|
20
20
|
"class-validator": "^0.13.2",
|
|
21
21
|
"i18next": "^21.6.4",
|
|
22
|
+
"node-html-parser": "^7.1.0",
|
|
22
23
|
"quill-delta": "^5.1.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
@@ -27,5 +28,5 @@
|
|
|
27
28
|
"ts-jest": "^29.1.1",
|
|
28
29
|
"typescript": "^5.6"
|
|
29
30
|
},
|
|
30
|
-
"gitHead": "
|
|
31
|
+
"gitHead": "cb2f3ab38ef5b8b2a38ab5b2c485e484a0cf9414"
|
|
31
32
|
}
|