@escapenavigator/utils 1.10.142 → 1.10.144

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.
@@ -66,6 +66,23 @@ const SKIP_AXIOS_CODES = new Set(['ERR_CANCELED', 'CANCELED']);
66
66
  * ошибкой только потому что код != 200.
67
67
  */
68
68
  const SKIP_HTTP_STATUSES = new Set([401, 403, 304]);
69
+ /**
70
+ * Бизнес-коды 4xx, которые НЕ нужно репортить вообще: это ожидаемое
71
+ * состояние, а не баг кода, и юзер уже видит понятное сообщение.
72
+ *
73
+ * widgetUnavailable — 404 на /openapi/widget/info для несуществующего /
74
+ * удалённого/переименованного widgetUid. Это «протухший» embed-сниппет
75
+ * на хост-сайте партнёра (его надо обновить — задача поддержки/данных,
76
+ * не кода). Один битый embed генерил 145 событий / 97 юзеров в день
77
+ * (ESCAPE-NAVIGATOR-WIDGET-E). Раньше эта же ситуация прилетала как 500
78
+ * (WIDGET-7) — мы починили бэк на чистый 404, теперь глушим и фронт-репорт.
79
+ *
80
+ * orderFinalizedUseCancel — 400 на DELETE /openapi/orders/preorder, когда
81
+ * юзер удаляет уже подтверждённую бронь (надо отменять, а не чистить
82
+ * draft). Ожидаемое бизнес-состояние, юзер видит понятное сообщение
83
+ * (ESCAPE-NAVIGATOR-WIDGET-B). Не баг кода — глушим фронт-репорт.
84
+ */
85
+ const SKIP_BUSINESS_CODES = new Set(['widgetUnavailable', 'orderFinalizedUseCancel']);
69
86
  function classifyApiError(ctx) {
70
87
  const { status, code, apiKey, errorCode, originalErrorCode } = ctx;
71
88
  if (code && SKIP_AXIOS_CODES.has(code)) {
@@ -74,6 +91,10 @@ function classifyApiError(ctx) {
74
91
  if (typeof status === 'number' && SKIP_HTTP_STATUSES.has(status)) {
75
92
  return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
76
93
  }
94
+ if ((errorCode && SKIP_BUSINESS_CODES.has(errorCode)) ||
95
+ (originalErrorCode && SKIP_BUSINESS_CODES.has(originalErrorCode))) {
96
+ return { severity: 'skip', level: 'info', fingerprint: null, shouldReport: false };
97
+ }
77
98
  // 5xx → critical. Включаем status в fingerprint, чтобы 502/503/504 не
78
99
  // схлопывались в один issue с 500 — у них разные источники проблем
79
100
  // (proxy/CDN vs само приложение vs gateway-timeout).
@@ -22,6 +22,20 @@
22
22
  */
23
23
  import Delta from 'quill-delta';
24
24
  export type ClipboardMatcher = [number | string, (node: Node, delta: Delta) => Delta];
25
+ /**
26
+ * Гарантированно убирает неразрывные пробелы из Quill-HTML: и литеральные
27
+ * сущности (` `/` `/` `), и сам символ U+00A0. Применяется
28
+ * на `onChange` КАЖДОГО редактора — это финальная страховка поверх
29
+ * clipboard-matcher'ов (которые ловят только вставку): что бы ни оказалось
30
+ * в редакторе (вставка, набор двойных пробелов, загрузка легаси-контента),
31
+ * наружу уходит текст без nbsp.
32
+ *
33
+ * Зачем: nbsp (1) запрещает перенос строки — длинные абзацы вылезают за
34
+ * контейнер письма и рвутся посреди слова, (2) ломает разбор Handlebars
35
+ * `{{#if X}}` на стороне рендера (`transformText`) и валидатор
36
+ * `IsIfIncorrect`, которым нужен обычный пробел после `#if`.
37
+ */
38
+ export declare function stripNonBreakingSpaces(html?: string | null): string;
25
39
  /**
26
40
  * Полный набор matcher-ов: и color-strip, и nbsp-normalize. Подключается там,
27
41
  * где редактор не должен принимать цветную раскраску из буфера (waiver, AGB,
@@ -26,9 +26,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  };
27
27
  Object.defineProperty(exports, "__esModule", { value: true });
28
28
  exports.stripColorQuillModules = void 0;
29
+ exports.stripNonBreakingSpaces = stripNonBreakingSpaces;
29
30
  exports.getStripColorClipboardMatchers = getStripColorClipboardMatchers;
30
31
  exports.getNormalizeWhitespaceClipboardMatchers = getNormalizeWhitespaceClipboardMatchers;
31
32
  const quill_delta_1 = __importDefault(require("quill-delta"));
33
+ /**
34
+ * Гарантированно убирает неразрывные пробелы из Quill-HTML: и литеральные
35
+ * сущности (` `/` `/` `), и сам символ U+00A0. Применяется
36
+ * на `onChange` КАЖДОГО редактора — это финальная страховка поверх
37
+ * clipboard-matcher'ов (которые ловят только вставку): что бы ни оказалось
38
+ * в редакторе (вставка, набор двойных пробелов, загрузка легаси-контента),
39
+ * наружу уходит текст без nbsp.
40
+ *
41
+ * Зачем: nbsp (1) запрещает перенос строки — длинные абзацы вылезают за
42
+ * контейнер письма и рвутся посреди слова, (2) ломает разбор Handlebars
43
+ * `{{#if X}}` на стороне рендера (`transformText`) и валидатор
44
+ * `IsIfIncorrect`, которым нужен обычный пробел после `#if`.
45
+ */
46
+ function stripNonBreakingSpaces(html) {
47
+ if (!html)
48
+ return html ?? '';
49
+ return html.replace(/ | | |\u00A0/gi, ' ');
50
+ }
32
51
  const stripColorFromDelta = (_node, delta) => delta.compose(new quill_delta_1.default().retain(delta.length(), {
33
52
  color: false,
34
53
  background: false,
@@ -25,6 +25,18 @@ const BOOKING_BUTTON_STYLE = `
25
25
  .trim();
26
26
  const transformText = (text, obj) => {
27
27
  let res = text || '';
28
+ // Quill-сохранённый HTML (особенно вставленный из Word/Docs или старого
29
+ // редактора) приходит с неразрывными пробелами (` `/U+00A0) между
30
+ // словами. Они ломают два места ниже:
31
+ // 1) разбор `{{#if X}}`-блоков — regex ждёт обычный `\s` после `#if`,
32
+ // а в тексте стоит литеральная сущность ` ` (`{{#if X}}`),
33
+ // из-за чего блок не матчится и теги печатаются в письмо как есть;
34
+ // 2) перенос строки — nbsp браузер/почтовый клиент не считают точкой
35
+ // переноса, поэтому длинные абзацы не переносятся и вылезают за
36
+ // контейнер (рвутся посреди слова).
37
+ // Нормализуем в обычный пробел — тот же контракт, что у `sanitizeQuillHtml`
38
+ // на фронте и у clipboard-matcher'ов редактора.
39
+ res = res.replace(/ | | |\u00A0/gi, ' ');
28
40
  const bookingButtonRegex = /\{\{#manageBooking\}\}([\s\S]*?)\{\{\/manageBooking\}\}/g;
29
41
  res = res.replace(bookingButtonRegex, (_match, buttonText) => {
30
42
  const link = obj.bookingManagementLink;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escapenavigator/utils",
3
- "version": "1.10.142",
3
+ "version": "1.10.144",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -14,7 +14,7 @@
14
14
  "test": "jest"
15
15
  },
16
16
  "dependencies": {
17
- "@escapenavigator/types": "^1.10.138",
17
+ "@escapenavigator/types": "^1.10.140",
18
18
  "axios": "^0.21.4",
19
19
  "class-transformer": "^0.5.1",
20
20
  "class-validator": "^0.13.2",
@@ -28,5 +28,5 @@
28
28
  "ts-jest": "^29.1.1",
29
29
  "typescript": "^5.6"
30
30
  },
31
- "gitHead": "4592522583b49fd2c99c8c916e3713f11eb3cf61"
31
+ "gitHead": "3a47865b15ba6211c53a0b9892ae0e618ada4c25"
32
32
  }
@@ -1,8 +0,0 @@
1
- /**
2
- * Email clients (Gmail и др.) плохо переваривают WebP — PNG лежит рядом
3
- * с тем же basename. В БД храним только WebP URL; при отправке письма
4
- * подменяем расширение.
5
- */
6
- export declare function profileLogoUrlForEmail(logoUrl: string | null | undefined): string;
7
- /** TipTap/Maily doc: image-ноды с profile logo WebP → PNG перед send-render. */
8
- export declare function replaceProfileLogoUrlsInDoc<T>(doc: T): T;
@@ -1,38 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.profileLogoUrlForEmail = profileLogoUrlForEmail;
4
- exports.replaceProfileLogoUrlsInDoc = replaceProfileLogoUrlsInDoc;
5
- const PROFILE_LOGO_PATH = 'profile_logos/';
6
- /**
7
- * Email clients (Gmail и др.) плохо переваривают WebP — PNG лежит рядом
8
- * с тем же basename. В БД храним только WebP URL; при отправке письма
9
- * подменяем расширение.
10
- */
11
- function profileLogoUrlForEmail(logoUrl) {
12
- if (!logoUrl)
13
- return logoUrl ?? '';
14
- if (!logoUrl.includes(PROFILE_LOGO_PATH) || !/\.webp(\?.*)?$/i.test(logoUrl)) {
15
- return logoUrl;
16
- }
17
- return logoUrl.replace(/\.webp(\?.*)?$/i, '.png$1');
18
- }
19
- /** TipTap/Maily doc: image-ноды с profile logo WebP → PNG перед send-render. */
20
- function replaceProfileLogoUrlsInDoc(doc) {
21
- if (!doc || typeof doc !== 'object')
22
- return doc;
23
- if (Array.isArray(doc)) {
24
- return doc.map((item) => replaceProfileLogoUrlsInDoc(item));
25
- }
26
- const node = { ...doc };
27
- if (node.type === 'image' && node.attrs && typeof node.attrs === 'object') {
28
- const attrs = { ...node.attrs };
29
- if (typeof attrs.src === 'string') {
30
- attrs.src = profileLogoUrlForEmail(attrs.src);
31
- }
32
- node.attrs = attrs;
33
- }
34
- if (Array.isArray(node.content)) {
35
- node.content = node.content.map((child) => replaceProfileLogoUrlsInDoc(child));
36
- }
37
- return node;
38
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,34 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const profile_logo_url_1 = require("./profile-logo-url");
4
- describe('profileLogoUrlForEmail', () => {
5
- it('replaces webp with png for profile logos', () => {
6
- const url = 'https://escapenavigator-images.s3.eu-central-1.amazonaws.com/dev/profile_logos/123-imgABC.webp';
7
- expect((0, profile_logo_url_1.profileLogoUrlForEmail)(url)).toBe('https://escapenavigator-images.s3.eu-central-1.amazonaws.com/dev/profile_logos/123-imgABC.png');
8
- });
9
- it('preserves query string', () => {
10
- const url = 'https://cdn.example.com/profile_logos/x.webp?v=1';
11
- expect((0, profile_logo_url_1.profileLogoUrlForEmail)(url)).toBe('https://cdn.example.com/profile_logos/x.png?v=1');
12
- });
13
- it('leaves non-profile and non-webp URLs unchanged', () => {
14
- expect((0, profile_logo_url_1.profileLogoUrlForEmail)('https://cdn.example.com/inside/logo.png')).toBe('https://cdn.example.com/inside/logo.png');
15
- expect((0, profile_logo_url_1.profileLogoUrlForEmail)(null)).toBe('');
16
- });
17
- });
18
- describe('replaceProfileLogoUrlsInDoc', () => {
19
- it('walks nested image nodes', () => {
20
- const doc = {
21
- type: 'doc',
22
- content: [
23
- {
24
- type: 'image',
25
- attrs: {
26
- src: 'https://cdn.example.com/profile_logos/a.webp',
27
- },
28
- },
29
- ],
30
- };
31
- const result = (0, profile_logo_url_1.replaceProfileLogoUrlsInDoc)(doc);
32
- expect(result.content[0].attrs.src).toBe('https://cdn.example.com/profile_logos/a.png');
33
- });
34
- });