@capillarytech/creatives-library 8.0.271 → 8.0.273

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.
Files changed (153) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +2 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +10 -0
  7. package/services/tests/api.test.js +34 -0
  8. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +17 -35
  9. package/tests/integration/TemplateCreation/api-response.js +31 -1
  10. package/tests/integration/TemplateCreation/msw-handler.js +2 -0
  11. package/utils/common.js +5 -0
  12. package/utils/commonUtils.js +28 -5
  13. package/utils/imageUrlUpload.js +13 -14
  14. package/utils/tests/commonUtil.test.js +224 -0
  15. package/utils/tests/imageUrlUpload.test.js +298 -0
  16. package/utils/transformTemplateConfig.js +0 -10
  17. package/v2Components/CapDeviceContent/index.js +61 -56
  18. package/v2Components/CapTagList/index.js +6 -1
  19. package/v2Components/CapTagListWithInput/index.js +5 -1
  20. package/v2Components/CapTagListWithInput/messages.js +1 -1
  21. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  22. package/v2Components/ErrorInfoNote/constants.js +1 -0
  23. package/v2Components/ErrorInfoNote/index.js +402 -72
  24. package/v2Components/ErrorInfoNote/messages.js +32 -6
  25. package/v2Components/ErrorInfoNote/style.scss +278 -6
  26. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  27. package/v2Components/HtmlEditor/HTMLEditor.js +418 -99
  28. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +870 -0
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1882 -133
  30. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  31. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  32. package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
  33. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +23 -102
  34. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -140
  35. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  36. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  37. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -1
  38. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +31 -6
  39. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  40. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  41. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  42. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  43. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  44. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  45. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +7 -10
  46. package/v2Components/HtmlEditor/components/PreviewPane/index.js +22 -43
  47. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  48. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +18 -0
  49. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +36 -31
  50. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +46 -34
  51. package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
  52. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +52 -46
  53. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +277 -0
  54. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +295 -0
  55. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  56. package/v2Components/HtmlEditor/constants.js +45 -20
  57. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  58. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +351 -16
  59. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  60. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  61. package/v2Components/HtmlEditor/hooks/useValidation.js +213 -56
  62. package/v2Components/HtmlEditor/index.js +1 -1
  63. package/v2Components/HtmlEditor/messages.js +102 -94
  64. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +214 -45
  65. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +134 -0
  66. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  67. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  68. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +158 -124
  69. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  70. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  71. package/v2Components/HtmlEditor/utils/validationConstants.js +38 -0
  72. package/v2Components/MobilePushPreviewV2/constants.js +6 -0
  73. package/v2Components/MobilePushPreviewV2/index.js +33 -7
  74. package/v2Components/TemplatePreview/_templatePreview.scss +55 -24
  75. package/v2Components/TemplatePreview/index.js +47 -32
  76. package/v2Components/TemplatePreview/messages.js +4 -0
  77. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  78. package/v2Containers/BeeEditor/index.js +172 -90
  79. package/v2Containers/BeePopupEditor/_beePopupEditor.scss +14 -0
  80. package/v2Containers/BeePopupEditor/constants.js +10 -0
  81. package/v2Containers/BeePopupEditor/index.js +194 -0
  82. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  83. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  84. package/v2Containers/CreativesContainer/SlideBoxFooter.js +156 -13
  85. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  86. package/v2Containers/CreativesContainer/constants.js +1 -0
  87. package/v2Containers/CreativesContainer/index.js +251 -47
  88. package/v2Containers/CreativesContainer/messages.js +8 -0
  89. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +103 -0
  92. package/v2Containers/Email/actions.js +7 -0
  93. package/v2Containers/Email/constants.js +5 -1
  94. package/v2Containers/Email/index.js +234 -29
  95. package/v2Containers/Email/messages.js +32 -0
  96. package/v2Containers/Email/reducer.js +12 -1
  97. package/v2Containers/Email/sagas.js +61 -7
  98. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  99. package/v2Containers/Email/tests/reducer.test.js +46 -0
  100. package/v2Containers/Email/tests/sagas.test.js +320 -29
  101. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1246 -0
  102. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +212 -21
  103. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  104. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2614 -0
  105. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  106. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  107. package/v2Containers/EmailWrapper/constants.js +2 -0
  108. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +627 -79
  109. package/v2Containers/EmailWrapper/index.js +103 -23
  110. package/v2Containers/EmailWrapper/messages.js +65 -1
  111. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +955 -0
  112. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +596 -82
  113. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  114. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  115. package/v2Containers/InApp/actions.js +7 -0
  116. package/v2Containers/InApp/constants.js +20 -4
  117. package/v2Containers/InApp/index.js +802 -360
  118. package/v2Containers/InApp/index.scss +4 -3
  119. package/v2Containers/InApp/messages.js +7 -3
  120. package/v2Containers/InApp/reducer.js +21 -3
  121. package/v2Containers/InApp/sagas.js +29 -9
  122. package/v2Containers/InApp/selectors.js +25 -5
  123. package/v2Containers/InApp/tests/index.test.js +154 -50
  124. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  125. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  126. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  127. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +151 -0
  128. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  129. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -0
  130. package/v2Containers/InAppWrapper/constants.js +16 -0
  131. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  132. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  133. package/v2Containers/InAppWrapper/index.js +148 -0
  134. package/v2Containers/InAppWrapper/messages.js +49 -0
  135. package/v2Containers/InappAdvance/index.js +1099 -0
  136. package/v2Containers/InappAdvance/index.scss +10 -0
  137. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  138. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  139. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  140. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  141. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  142. package/v2Containers/MobilePush/Create/index.js +1 -1
  143. package/v2Containers/MobilePush/Edit/index.js +10 -6
  144. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  145. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  146. package/v2Containers/TagList/index.js +62 -19
  147. package/v2Containers/Templates/_templates.scss +60 -1
  148. package/v2Containers/Templates/index.js +89 -4
  149. package/v2Containers/Templates/messages.js +4 -0
  150. package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
  151. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  152. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  153. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
@@ -24,7 +24,7 @@ const defaultMessageFormatter = (messageKey, values = {}) => {
24
24
  'sanitizer.productionValidHtml': 'Provide valid HTML content before deploying to production',
25
25
  'sanitizer.productionSanitized': 'Content has been sanitized for security - review changes before deploying',
26
26
  'sanitizer.productionInlineCss': 'Consider inlining CSS for better email client compatibility',
27
- 'sanitizer.productionLargeContent': `Content is large (${values.size || 'unknown'} characters) - consider optimizing for mobile performance`
27
+ 'sanitizer.productionLargeContent': `Content is large (${values.size || 'unknown'} characters) - consider optimizing for mobile performance`,
28
28
  };
29
29
 
30
30
  return fallbackMessages[messageKey] || messageKey;
@@ -33,17 +33,17 @@ const defaultMessageFormatter = (messageKey, values = {}) => {
33
33
  // Constants for better maintainability
34
34
  const SANITIZER_VARIANTS = {
35
35
  EMAIL: 'email',
36
- INAPP: 'inapp'
36
+ INAPP: 'inapp',
37
37
  };
38
38
 
39
39
  const SECURITY_LEVELS = {
40
40
  STANDARD: 'standard',
41
- STRICT: 'strict'
41
+ STRICT: 'strict',
42
42
  };
43
43
 
44
44
  const CONTENT_LIMITS = {
45
45
  LARGE_CONTENT_SIZE: 50000,
46
- MIN_CONTENT_LENGTH: 0
46
+ MIN_CONTENT_LENGTH: 0,
47
47
  };
48
48
 
49
49
  const DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:'];
@@ -51,7 +51,7 @@ const DANGEROUS_PROTOCOLS = ['javascript:', 'data:', 'vbscript:'];
51
51
  const EVENT_HANDLERS = [
52
52
  'onclick', 'onload', 'onerror', 'onmouseover', 'onmouseout',
53
53
  'onmousedown', 'onmouseup', 'onkeydown', 'onkeyup', 'onkeypress',
54
- 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset'
54
+ 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset',
55
55
  ];
56
56
 
57
57
  // Email-specific sanitization config
@@ -62,18 +62,18 @@ const EMAIL_CONFIG = {
62
62
  'a', 'img', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'tfoot',
63
63
  'ul', 'ol', 'li', 'strong', 'b', 'em', 'i', 'u', 'center',
64
64
  'font', 'small', 'big', 'sup', 'sub', 'pre', 'code',
65
- 'blockquote', 'cite', 'abbr', 'acronym', 'address'
65
+ 'blockquote', 'cite', 'abbr', 'acronym', 'address',
66
66
  ],
67
67
  ALLOWED_ATTR: [
68
68
  'src', 'alt', 'title', 'href', 'target', 'rel',
69
69
  'width', 'height', 'style', 'class', 'id',
70
70
  'align', 'valign', 'bgcolor', 'color', 'border',
71
71
  'cellpadding', 'cellspacing', 'colspan', 'rowspan',
72
- 'type', 'charset', 'content', 'name', 'http-equiv'
72
+ 'type', 'charset', 'content', 'name', 'http-equiv',
73
73
  ],
74
74
  FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'input'],
75
75
  FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover'],
76
- ALLOW_DATA_ATTR: false
76
+ ALLOW_DATA_ATTR: false,
77
77
  };
78
78
 
79
79
  // InApp-specific sanitization config
@@ -83,29 +83,29 @@ const INAPP_CONFIG = {
83
83
  'div', 'span', 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
84
84
  'a', 'img', 'button', 'ul', 'ol', 'li', 'strong', 'b', 'em', 'i', 'u',
85
85
  'small', 'big', 'sup', 'sub', 'pre', 'code', 'blockquote', 'cite',
86
- 'video', 'audio', 'source', 'canvas' // Mobile-friendly multimedia
86
+ 'video', 'audio', 'source', 'canvas', // Mobile-friendly multimedia
87
87
  ],
88
88
  ALLOWED_ATTR: [
89
89
  'src', 'alt', 'title', 'href', 'target', 'rel',
90
90
  'width', 'height', 'style', 'class', 'id',
91
91
  'type', 'controls', 'autoplay', 'loop', 'muted',
92
- 'poster', 'preload'
92
+ 'poster', 'preload',
93
93
  ],
94
94
  FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'input'],
95
95
  FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover'],
96
- ALLOW_DATA_ATTR: true
96
+ ALLOW_DATA_ATTR: true,
97
97
  };
98
98
 
99
99
  // Strict sanitization config for production
100
100
  const STRICT_CONFIG = {
101
101
  ALLOWED_TAGS: [
102
102
  'div', 'span', 'p', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
103
- 'a', 'img', 'strong', 'b', 'em', 'i', 'u', 'ul', 'ol', 'li'
103
+ 'a', 'img', 'strong', 'b', 'em', 'i', 'u', 'ul', 'ol', 'li',
104
104
  ],
105
105
  ALLOWED_ATTR: ['src', 'alt', 'title', 'href', 'style', 'class'],
106
106
  FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'style'],
107
107
  FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover'],
108
- ALLOW_DATA_ATTR: false
108
+ ALLOW_DATA_ATTR: false,
109
109
  };
110
110
 
111
111
  /**
@@ -126,8 +126,9 @@ export const sanitizeHTML = (html, variant = SANITIZER_VARIANTS.EMAIL, level = S
126
126
  warnings: html === null || html === undefined ? [] : [{
127
127
  type: 'warning',
128
128
  message: formatMessage('sanitizer.invalidInput'),
129
- source: 'sanitizer'
130
- }]
129
+ source: 'sanitizer',
130
+ rule: 'sanitizer.invalidInput', // Rule Group #1 – blocking error for UI gating
131
+ }],
131
132
  };
132
133
  }
133
134
 
@@ -137,7 +138,7 @@ export const sanitizeHTML = (html, variant = SANITIZER_VARIANTS.EMAIL, level = S
137
138
  sanitized: '',
138
139
  isClean: true,
139
140
  removedElements: [],
140
- warnings: []
141
+ warnings: [],
141
142
  };
142
143
  }
143
144
 
@@ -159,7 +160,7 @@ export const sanitizeHTML = (html, variant = SANITIZER_VARIANTS.EMAIL, level = S
159
160
  sanitized: '',
160
161
  isClean: true,
161
162
  removedElements: [],
162
- warnings: []
163
+ warnings: [],
163
164
  };
164
165
 
165
166
  try {
@@ -188,8 +189,8 @@ export const sanitizeHTML = (html, variant = SANITIZER_VARIANTS.EMAIL, level = S
188
189
  CUSTOM_ELEMENT_HANDLING: {
189
190
  tagNameCheck: null,
190
191
  attributeNameCheck: null,
191
- allowCustomizedBuiltInElements: false
192
- }
192
+ allowCustomizedBuiltInElements: false,
193
+ },
193
194
  };
194
195
 
195
196
  // Sanitize the content directly
@@ -202,12 +203,12 @@ export const sanitizeHTML = (html, variant = SANITIZER_VARIANTS.EMAIL, level = S
202
203
 
203
204
  // Add variant-specific warnings (check original HTML before sanitization)
204
205
  addVariantWarnings(html, variant, result, formatMessage);
205
-
206
206
  } catch (error) {
207
207
  result.warnings.push({
208
208
  type: 'error',
209
209
  message: formatMessage('sanitizer.sanitizationFailed', { error: error.message }),
210
- source: 'sanitizer'
210
+ source: 'sanitizer',
211
+ rule: 'sanitizer.sanitizationFailed', // Rule Group #1 – blocking error for UI gating
211
212
  });
212
213
  result.sanitized = ''; // Return empty content if sanitization fails
213
214
  result.isClean = false;
@@ -229,30 +230,30 @@ const addVariantWarnings = (html, variant, result, formatMessage = defaultMessag
229
230
  if (variant === SANITIZER_VARIANTS.EMAIL) {
230
231
  // Check for potentially problematic email elements
231
232
  const emailProblematicElements = ['<video', '<audio', '<canvas'];
232
- if (emailProblematicElements.some(element => html.includes(element))) {
233
+ if (emailProblematicElements.some((element) => html.includes(element))) {
233
234
  result.warnings.push({
234
235
  type: 'warning',
235
236
  message: formatMessage('sanitizer.emailMultimediaNotSupported'),
236
- source: 'email-compatibility'
237
+ source: 'email-compatibility',
237
238
  });
238
239
  }
239
240
 
240
241
  const problematicStyles = ['position: fixed', 'position: sticky', 'position:fixed', 'position:sticky'];
241
- if (problematicStyles.some(style => html.includes(style))) {
242
+ if (problematicStyles.some((style) => html.includes(style))) {
242
243
  result.warnings.push({
243
244
  type: 'warning',
244
245
  message: formatMessage('sanitizer.emailPositioningNotSupported'),
245
- source: 'email-compatibility'
246
+ source: 'email-compatibility',
246
247
  });
247
248
  }
248
249
 
249
250
  // Check for CSS Grid/Flexbox which may have limited email support
250
251
  const modernCssFeatures = ['display: grid', 'display: flex', 'display:grid', 'display:flex'];
251
- if (modernCssFeatures.some(feature => html.includes(feature))) {
252
+ if (modernCssFeatures.some((feature) => html.includes(feature))) {
252
253
  result.warnings.push({
253
254
  type: 'info',
254
255
  message: formatMessage('sanitizer.emailModernCssLimited'),
255
- source: 'email-compatibility'
256
+ source: 'email-compatibility',
256
257
  });
257
258
  }
258
259
  } else if (variant === SANITIZER_VARIANTS.INAPP) {
@@ -261,7 +262,7 @@ const addVariantWarnings = (html, variant, result, formatMessage = defaultMessag
261
262
  result.warnings.push({
262
263
  type: 'info',
263
264
  message: formatMessage('sanitizer.mobileTablesNotFriendly'),
264
- source: 'mobile-optimization'
265
+ source: 'mobile-optimization',
265
266
  });
266
267
  }
267
268
 
@@ -270,7 +271,7 @@ const addVariantWarnings = (html, variant, result, formatMessage = defaultMessag
270
271
  result.warnings.push({
271
272
  type: 'info',
272
273
  message: formatMessage('sanitizer.mobileRelativeFontSizes'),
273
- source: 'mobile-optimization'
274
+ source: 'mobile-optimization',
274
275
  });
275
276
  }
276
277
 
@@ -279,7 +280,7 @@ const addVariantWarnings = (html, variant, result, formatMessage = defaultMessag
279
280
  result.warnings.push({
280
281
  type: 'info',
281
282
  message: formatMessage('sanitizer.mobileFixedWidthsProblematic'),
282
- source: 'mobile-optimization'
283
+ source: 'mobile-optimization',
283
284
  });
284
285
  }
285
286
  }
@@ -302,9 +303,9 @@ export const prepareForProduction = (html, variant = SANITIZER_VARIANTS.EMAIL, f
302
303
  warnings: [{
303
304
  type: 'error',
304
305
  message: formatMessage('sanitizer.invalidInputNonEmpty'),
305
- source: 'production-validator'
306
+ source: 'production-validator',
306
307
  }],
307
- recommendations: [formatMessage('sanitizer.productionValidHtml')]
308
+ recommendations: [formatMessage('sanitizer.productionValidHtml')],
308
309
  };
309
310
  }
310
311
 
@@ -316,7 +317,7 @@ export const prepareForProduction = (html, variant = SANITIZER_VARIANTS.EMAIL, f
316
317
  isProductionReady: sanitizeResult.isClean,
317
318
  securityIssues: sanitizeResult.removedElements,
318
319
  warnings: sanitizeResult.warnings,
319
- recommendations: []
320
+ recommendations: [],
320
321
  };
321
322
 
322
323
  // Add production readiness recommendations
@@ -355,9 +356,7 @@ export const isContentSafe = (html) => {
355
356
  if (!html || typeof html !== 'string') return true;
356
357
 
357
358
  // Create dynamic patterns from constants
358
- const protocolPatterns = DANGEROUS_PROTOCOLS.map(protocol =>
359
- new RegExp(protocol.replace(':', '\\:'), 'gi')
360
- );
359
+ const protocolPatterns = DANGEROUS_PROTOCOLS.map((protocol) => new RegExp(protocol.replace(':', '\\:'), 'gi'));
361
360
 
362
361
  const eventHandlerPattern = new RegExp(EVENT_HANDLERS.join('|'), 'gi');
363
362
 
@@ -369,10 +368,10 @@ export const isContentSafe = (html) => {
369
368
  /<object/gi,
370
369
  /<embed/gi,
371
370
  /<applet/gi,
372
- /<form/gi
371
+ /<form/gi,
373
372
  ];
374
373
 
375
- return !dangerousPatterns.some(pattern => pattern.test(html));
374
+ return !dangerousPatterns.some((pattern) => pattern.test(html));
376
375
  };
377
376
 
378
377
  /**
@@ -395,7 +394,7 @@ export const findUnsafeContent = (html) => {
395
394
  'Iframe': /<iframe[^>]*>/gi,
396
395
  'Object/Embed': /<(object|embed)[^>]*>/gi,
397
396
  'Applet': /<applet[^>]*>/gi,
398
- 'Form': /<form[^>]*>/gi
397
+ 'Form': /<form[^>]*>/gi,
399
398
  };
400
399
 
401
400
  Object.entries(patterns).forEach(([name, pattern]) => {
@@ -406,7 +405,7 @@ export const findUnsafeContent = (html) => {
406
405
  type: name,
407
406
  content: match[0],
408
407
  position: match.index,
409
- length: match[0].length
408
+ length: match[0].length,
410
409
  });
411
410
 
412
411
  // Prevent infinite loop for global patterns
@@ -429,5 +428,5 @@ export default {
429
428
  // Include constants in default export for convenience
430
429
  VARIANTS: SANITIZER_VARIANTS,
431
430
  SECURITY_LEVELS,
432
- CONTENT_LIMITS
431
+ CONTENT_LIMITS,
433
432
  };
@@ -20,7 +20,7 @@ const defaultMessageFormatter = (messageKey, values = {}) => {
20
20
  'validator.largeImageDetected': 'Large image dimensions detected - consider mobile optimization',
21
21
  'validator.unclosedCssRule': 'Unclosed CSS rule detected',
22
22
  'validator.emptyCssRule': 'Empty CSS rule detected',
23
- 'validator.cssValidationFailed': `CSS validation failed: ${values.error || 'Unknown error'}`
23
+ 'validator.cssValidationFailed': `CSS validation failed: ${values.error || 'Unknown error'}`,
24
24
  };
25
25
 
26
26
  return fallbackMessages[messageKey] || messageKey;
@@ -49,7 +49,7 @@ const HTML_RULES = {
49
49
  'space-tab-mixed-disabled': 'space',
50
50
  'id-class-ad-disabled': false,
51
51
  'href-abs-or-rel': false,
52
- 'attr-unsafe-chars': true
52
+ 'attr-unsafe-chars': true,
53
53
  };
54
54
 
55
55
  // Additional custom validation rules
@@ -66,7 +66,7 @@ const CUSTOM_VALIDATIONS = {
66
66
 
67
67
  // InApp-specific validations
68
68
  MOBILE_INCOMPATIBLE: /<(object|embed|applet)\b/gi,
69
- LARGE_IMAGES: /width\s*:\s*[5-9]\d{2,}px|height\s*:\s*[5-9]\d{2,}px/gi
69
+ LARGE_IMAGES: /width\s*:\s*[5-9]\d{2,}px|height\s*:\s*[5-9]\d{2,}px/gi,
70
70
  };
71
71
 
72
72
  /**
@@ -82,7 +82,7 @@ export const validateHTML = (html, variant = 'email', formatMessage = defaultMes
82
82
  isValid: true,
83
83
  errors: [],
84
84
  warnings: [],
85
- info: []
85
+ info: [],
86
86
  };
87
87
  }
88
88
 
@@ -90,7 +90,7 @@ export const validateHTML = (html, variant = 'email', formatMessage = defaultMes
90
90
  isValid: true,
91
91
  errors: [],
92
92
  warnings: [],
93
- info: []
93
+ info: [],
94
94
  };
95
95
 
96
96
  try {
@@ -98,7 +98,7 @@ export const validateHTML = (html, variant = 'email', formatMessage = defaultMes
98
98
  const htmlHintResults = HTMLHint.verify(html, HTML_RULES);
99
99
 
100
100
  // Process HTMLHint results
101
- htmlHintResults.forEach(issue => {
101
+ htmlHintResults.forEach((issue) => {
102
102
  const error = {
103
103
  type: issue.type,
104
104
  message: issue.message,
@@ -106,66 +106,64 @@ export const validateHTML = (html, variant = 'email', formatMessage = defaultMes
106
106
  column: issue.col,
107
107
  rule: issue.rule.id,
108
108
  severity: getSeverityLevel(issue.type, issue.rule.id),
109
- source: 'htmlhint'
109
+ source: 'htmlhint',
110
110
  };
111
111
 
112
- if (error.severity === 'error') {
113
- results.errors.push(error);
114
- results.isValid = false;
115
- } else if (error.severity === 'warning') {
112
+ if (error.severity === 'warning') {
116
113
  results.warnings.push(error);
117
- } else {
114
+ } else if (error.severity === 'info') {
118
115
  results.info.push(error);
116
+ } else {
117
+ results.warnings.push(error);
119
118
  }
120
119
  });
121
-
122
- // Run custom validations
123
- runCustomValidations(html, variant, results, formatMessage);
124
-
125
- // Run Liquid template validation
126
- runLiquidValidation(html, variant, results, formatMessage);
127
-
128
120
  } catch (error) {
129
- results.errors.push({
130
- type: 'error',
121
+ // HTMLHint failed, but we still want to run custom validations and Liquid validation
122
+ results.warnings.push({
123
+ type: 'warning',
131
124
  message: formatMessage('validator.validationFailed', { error: error.message }),
132
125
  line: 1,
133
126
  column: 1,
134
127
  rule: 'validation-error',
135
- severity: 'error',
136
- source: 'validator'
128
+ severity: 'warning',
129
+ source: 'validator',
137
130
  });
138
- results.isValid = false;
139
131
  }
140
132
 
133
+ // Always run custom validations and Liquid validation, even if HTMLHint failed
134
+ // This ensures unsafe protocol detection and other critical validations still run
135
+ runCustomValidations(html, variant, results, formatMessage);
136
+ runLiquidValidation(html, variant, results, formatMessage);
137
+
141
138
  return results;
142
139
  };
143
140
 
144
141
  /**
145
- * Determines severity level based on error type and rule
142
+ * Determines severity level based on error type and rule.
143
+ * ONLY Rule Group #1 (Input & Sanitization) is blocking; that is handled in
144
+ * contentSanitizer/useValidation. All HTML/CSS/Liquid/security rules here are
145
+ * WARNING only for backward compatibility with CKEditor legacy templates.
146
146
  */
147
147
  const getSeverityLevel = (type, ruleId) => {
148
- const errorRules = [
148
+ const warningRules = [
149
149
  'tag-pair',
150
150
  'attr-no-duplication',
151
151
  'id-unique',
152
- 'spec-char-escape'
153
- ];
154
-
155
- const warningRules = [
152
+ 'spec-char-escape',
156
153
  'tagname-lowercase',
157
154
  'attr-lowercase',
158
155
  'attr-value-double-quotes',
159
- 'alt-require'
156
+ 'alt-require',
160
157
  ];
161
158
 
162
- if (type === 'error' || errorRules.includes(ruleId)) {
163
- return 'error';
164
- } else if (warningRules.includes(ruleId)) {
159
+ if (warningRules.includes(ruleId)) {
160
+ return 'warning';
161
+ }
162
+ // Downgrade HTMLHint "error" type to warning (Rule Group #1 is sanitizer-only)
163
+ if (type === 'error') {
165
164
  return 'warning';
166
- } else {
167
- return 'info';
168
165
  }
166
+ return 'info';
169
167
  };
170
168
 
171
169
  /**
@@ -177,6 +175,7 @@ const getSeverityLevel = (type, ruleId) => {
177
175
  */
178
176
  const runCustomValidations = (html, variant, results, formatMessage = defaultMessageFormatter) => {
179
177
  // Check for unsafe protocols using RegExp.exec loop
178
+ // These are BLOCKING ERRORS (Rule Group #1: sanitizer.dangerousProtocolDetected)
180
179
  const unsafeProtocolsRegex = new RegExp(CUSTOM_VALIDATIONS.UNSAFE_PROTOCOLS.source, CUSTOM_VALIDATIONS.UNSAFE_PROTOCOLS.flags);
181
180
  unsafeProtocolsRegex.lastIndex = 0; // Reset lastIndex before running
182
181
  let match;
@@ -186,9 +185,9 @@ const runCustomValidations = (html, variant, results, formatMessage = defaultMes
186
185
  message: formatMessage('validator.unsafeProtocolDetected', { protocol: match[0] }),
187
186
  line: getLineNumber(html, match.index),
188
187
  column: 1,
189
- rule: 'unsafe-protocol',
188
+ rule: 'sanitizer.dangerousProtocolDetected',
190
189
  severity: 'error',
191
- source: 'custom'
190
+ source: 'custom',
192
191
  });
193
192
  results.isValid = false;
194
193
 
@@ -209,7 +208,7 @@ const runCustomValidations = (html, variant, results, formatMessage = defaultMes
209
208
  column: 1,
210
209
  rule: 'script-tag-warning',
211
210
  severity: 'warning',
212
- source: 'custom'
211
+ source: 'custom',
213
212
  });
214
213
 
215
214
  // Guard against zero-length matches to avoid infinite loops
@@ -245,7 +244,7 @@ const validateEmailSpecific = (html, results, formatMessage = defaultMessageForm
245
244
  column: 1,
246
245
  rule: 'outlook-compatibility',
247
246
  severity: 'warning',
248
- source: 'email-specific'
247
+ source: 'email-specific',
249
248
  });
250
249
 
251
250
  // Guard against zero-length matches to avoid infinite loops
@@ -265,7 +264,7 @@ const validateEmailSpecific = (html, results, formatMessage = defaultMessageForm
265
264
  column: 1,
266
265
  rule: 'email-css-compatibility',
267
266
  severity: 'warning',
268
- source: 'email-specific'
267
+ source: 'email-specific',
269
268
  });
270
269
 
271
270
  // Guard against zero-length matches to avoid infinite loops
@@ -294,7 +293,7 @@ const validateInAppSpecific = (html, results, formatMessage = defaultMessageForm
294
293
  column: 1,
295
294
  rule: 'mobile-compatibility',
296
295
  severity: 'warning',
297
- source: 'inapp-specific'
296
+ source: 'inapp-specific',
298
297
  });
299
298
 
300
299
  // Guard against zero-length matches to avoid infinite loops
@@ -314,7 +313,7 @@ const validateInAppSpecific = (html, results, formatMessage = defaultMessageForm
314
313
  column: 1,
315
314
  rule: 'mobile-image-size',
316
315
  severity: 'info',
317
- source: 'inapp-specific'
316
+ source: 'inapp-specific',
318
317
  });
319
318
 
320
319
  // Guard against zero-length matches to avoid infinite loops
@@ -336,8 +335,9 @@ const runLiquidValidation = (html, variant, results, formatMessage = defaultMess
336
335
  const liquidResults = validateLiquidHTML(html, variant);
337
336
 
338
337
  // Merge Liquid validation results
338
+ // Client-side Liquid validation errors are blocking (genuine syntax errors)
339
339
  if (liquidResults.errors) {
340
- results.errors.push(...liquidResults.errors);
340
+ results.errors.push(...liquidResults.errors.map((e) => ({ ...e, severity: 'error' })));
341
341
  if (liquidResults.errors.length > 0) {
342
342
  results.isValid = false;
343
343
  }
@@ -375,7 +375,7 @@ export const validateCSS = (css, formatMessage = defaultMessageFormatter) => {
375
375
  isValid: true,
376
376
  errors: [],
377
377
  warnings: [],
378
- info: []
378
+ info: [],
379
379
  };
380
380
 
381
381
  if (!css || typeof css !== 'string') {
@@ -388,7 +388,7 @@ export const validateCSS = (css, formatMessage = defaultMessageFormatter) => {
388
388
  unclosedBraces: /\{[^{}]*$/gm,
389
389
  invalidProperty: /[^;{}]+:\s*[^;{}]*[^;}]/g,
390
390
  missingColon: /[^;{}]+\s+[^;{}:]+;/g,
391
- emptyRule: /[^{}]+\{\s*\}/g
391
+ emptyRule: /[^{}]+\{\s*\}/g,
392
392
  };
393
393
 
394
394
  // Check for unclosed braces using RegExp.exec loop
@@ -396,16 +396,15 @@ export const validateCSS = (css, formatMessage = defaultMessageFormatter) => {
396
396
  unclosedBracesRegex.lastIndex = 0; // Reset lastIndex before running
397
397
  let match;
398
398
  while ((match = unclosedBracesRegex.exec(css)) !== null) {
399
- results.errors.push({
400
- type: 'error',
399
+ results.warnings.push({
400
+ type: 'warning',
401
401
  message: formatMessage('validator.unclosedCssRule'),
402
402
  line: getLineNumber(css, match.index),
403
403
  column: 1,
404
404
  rule: 'unclosed-brace',
405
- severity: 'error',
406
- source: 'css-validator'
405
+ severity: 'warning',
406
+ source: 'css-validator',
407
407
  });
408
- results.isValid = false;
409
408
 
410
409
  // Guard against zero-length matches to avoid infinite loops
411
410
  if (match[0].length === 0) {
@@ -417,34 +416,31 @@ export const validateCSS = (css, formatMessage = defaultMessageFormatter) => {
417
416
  const emptyRulesRegex = new RegExp(validationPatterns.emptyRule.source, validationPatterns.emptyRule.flags);
418
417
  emptyRulesRegex.lastIndex = 0; // Reset lastIndex before running
419
418
  while ((match = emptyRulesRegex.exec(css)) !== null) {
420
- results.errors.push({
421
- type: 'error',
419
+ results.warnings.push({
420
+ type: 'warning',
422
421
  message: formatMessage('validator.emptyCssRule'),
423
422
  line: getLineNumber(css, match.index),
424
423
  column: 1,
425
424
  rule: 'empty-rule',
426
- severity: 'error',
427
- source: 'css-validator'
425
+ severity: 'warning',
426
+ source: 'css-validator',
428
427
  });
429
- results.isValid = false;
430
428
 
431
429
  // Guard against zero-length matches to avoid infinite loops
432
430
  if (match[0].length === 0) {
433
431
  emptyRulesRegex.lastIndex++;
434
432
  }
435
433
  }
436
-
437
434
  } catch (error) {
438
- results.errors.push({
439
- type: 'error',
435
+ results.warnings.push({
436
+ type: 'warning',
440
437
  message: formatMessage('validator.cssValidationFailed', { error: error.message }),
441
438
  line: 1,
442
439
  column: 1,
443
440
  rule: 'css-validation-error',
444
- severity: 'error',
445
- source: 'css-validator'
441
+ severity: 'warning',
442
+ source: 'css-validator',
446
443
  });
447
- results.isValid = false;
448
444
  }
449
445
 
450
446
  return results;
@@ -457,16 +453,20 @@ export const validateCSS = (css, formatMessage = defaultMessageFormatter) => {
457
453
  * @returns {Object} Combined validation results from all CSS blocks
458
454
  */
459
455
  export const extractAndValidateCSS = (html, formatMessage = defaultMessageFormatter) => {
460
- if (!html) return { isValid: true, errors: [], warnings: [], info: [] };
456
+ if (!html) {
457
+ return {
458
+ isValid: true, errors: [], warnings: [], info: [],
459
+ };
460
+ }
461
461
 
462
462
  // Extract CSS from style tags
463
463
  const styleTagPattern = /<style[^>]*>([\s\S]*?)<\/style>/gi;
464
464
  let match;
465
- let allResults = {
465
+ const allResults = {
466
466
  isValid: true,
467
467
  errors: [],
468
468
  warnings: [],
469
- info: []
469
+ info: [],
470
470
  };
471
471
 
472
472
  while ((match = styleTagPattern.exec(html)) !== null) {
@@ -483,19 +483,18 @@ export const extractAndValidateCSS = (html, formatMessage = defaultMessageFormat
483
483
  }
484
484
  }
485
485
 
486
- // Check for unclosed style tags
486
+ // Check for unclosed style tags (warning only for CKEditor legacy compatibility)
487
487
  const unclosedStylePattern = /<style[^>]*>(?![\s\S]*?<\/style>)/gi;
488
488
  if (unclosedStylePattern.test(html)) {
489
- allResults.errors.push({
490
- type: 'error',
489
+ allResults.warnings.push({
490
+ type: 'warning',
491
491
  message: formatMessage('validator.unclosedCssRule'),
492
492
  line: 1,
493
493
  column: 1,
494
494
  rule: 'unclosed-style-tag',
495
- severity: 'error',
496
- source: 'css-validator'
495
+ severity: 'warning',
496
+ source: 'css-validator',
497
497
  });
498
- allResults.isValid = false;
499
498
  }
500
499
 
501
500
  return allResults;
@@ -504,5 +503,5 @@ export const extractAndValidateCSS = (html, formatMessage = defaultMessageFormat
504
503
  export default {
505
504
  validateHTML,
506
505
  validateCSS,
507
- extractAndValidateCSS
506
+ extractAndValidateCSS,
508
507
  };