@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
@@ -1,11 +1,17 @@
1
1
  /**
2
2
  * Validation Hook for HTML Editor
3
- * Manages real-time validation and error display
3
+ * Manages real-time validation and error display.
4
+ * UI gating: only Rule Group #1 (Input & Sanitization) blocks Save/Update/Preview/Test.
5
+ * All other rules are warnings for backward compatibility with CKEditor legacy templates.
4
6
  */
5
7
 
6
- import { useState, useEffect, useCallback, useRef } from 'react';
8
+ import {
9
+ useState, useEffect, useCallback, useRef,
10
+ } from 'react';
7
11
  import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
8
12
  import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
13
+ import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../constants';
14
+ import { ISSUE_SOURCES } from '../utils/validationConstants';
9
15
 
10
16
  /**
11
17
  * Custom hook for managing HTML/CSS validation
@@ -16,12 +22,61 @@ import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/content
16
22
  * @param {Function} formatValidatorMessage - Message formatter function for validator internationalization
17
23
  * @returns {Object} Validation state and methods
18
24
  */
25
+ /**
26
+ * Get line number for a character position in text
27
+ */
28
+ const getLineNumberFromPosition = (text, position) => {
29
+ if (position === undefined || position < 0) return 1;
30
+ return text.substring(0, position).split('\n').length;
31
+ };
32
+
33
+ /**
34
+ * Get 1-based line and column from a character position in text
35
+ */
36
+ const getLineAndColumnFromPosition = (text, position) => {
37
+ if (!text || position === undefined || position < 0) {
38
+ return { line: 1, column: 1 };
39
+ }
40
+ const before = text.substring(0, position);
41
+ const lines = before.split('\n');
42
+ const line = lines.length;
43
+ const lastLine = lines[lines.length - 1] || '';
44
+ const column = lastLine.length + 1;
45
+ return { line, column };
46
+ };
47
+
48
+ /**
49
+ * Find line number for a tag or pattern in content
50
+ * Used for API errors to locate where the error occurs
51
+ */
52
+ const findLineNumberForTag = (content, tagName) => {
53
+ if (!content || !tagName) return null;
54
+
55
+ // Try to find the tag in the content
56
+ // Look for patterns like {{ tagName }}, {{tagName}}, etc.
57
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
+ const patterns = [
59
+ new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g'),
60
+ new RegExp(`\\{%[^%]*${escapedTagName}[^%]*%\\}`, 'g'),
61
+ ];
62
+
63
+ for (const pattern of patterns) {
64
+ const match = pattern.exec(content);
65
+ if (match) {
66
+ return getLineNumberFromPosition(content, match.index);
67
+ }
68
+ }
69
+
70
+ return null;
71
+ };
72
+
19
73
  export const useValidation = (content, variant = 'email', options = {}, formatSanitizerMessage = null, formatValidatorMessage = null) => {
20
74
  const {
21
75
  enableRealTime = true,
22
76
  debounceMs = 500,
23
77
  enableSanitization = true,
24
- securityLevel = 'standard'
78
+ securityLevel = 'standard',
79
+ apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent
25
80
  } = options;
26
81
 
27
82
  // Validation state
@@ -42,8 +97,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
42
97
  totalErrors: 0,
43
98
  totalWarnings: 0,
44
99
  totalInfo: 0,
45
- hasSecurityIssues: false
46
- }
100
+ hasSecurityIssues: false,
101
+ },
47
102
  });
48
103
 
49
104
  // Refs for debouncing
@@ -55,7 +110,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
55
110
  */
56
111
  const performValidation = useCallback(async (htmlContent) => {
57
112
  if (!htmlContent) {
58
- setValidationState(prev => ({
113
+ setValidationState((prev) => ({
59
114
  ...prev,
60
115
  isValidating: false,
61
116
  htmlErrors: [],
@@ -72,13 +127,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
72
127
  totalErrors: 0,
73
128
  totalWarnings: 0,
74
129
  totalInfo: 0,
75
- hasSecurityIssues: false
76
- }
130
+ hasSecurityIssues: false,
131
+ },
77
132
  }));
78
133
  return;
79
134
  }
80
135
 
81
- setValidationState(prev => ({ ...prev, isValidating: true }));
136
+ setValidationState((prev) => ({ ...prev, isValidating: true }));
82
137
 
83
138
  try {
84
139
  // 1. HTML Validation
@@ -91,15 +146,21 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
91
146
  const isSecure = isContentSafe(htmlContent);
92
147
  const securityIssues = isSecure ? [] : findUnsafeContent(htmlContent);
93
148
 
94
- // 4. Sanitization (if enabled)
149
+ // 4. Sanitization (if enabled) – Rule Group #1 issues come from here
95
150
  let sanitizationResult = null;
96
151
  if (enableSanitization) {
97
152
  sanitizationResult = sanitizeHTML(htmlContent, variant, securityLevel, formatSanitizerMessage);
98
153
  }
99
154
 
100
- // Calculate summary
101
- const totalErrors = htmlValidation.errors.length + cssValidation.errors.length;
102
- const totalWarnings = htmlValidation.warnings.length + cssValidation.warnings.length;
155
+ const sanitizationWarnings = sanitizationResult?.warnings || [];
156
+ const blockingSanitizerCount = sanitizationWarnings.filter((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)).length;
157
+ const protocolSecurityCount = (securityIssues || []).filter((s) => ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'].includes(s?.type)).length;
158
+
159
+ // Summary: totalErrors/totalWarnings are for display; blocking count is for gating
160
+ const totalErrors = htmlValidation.errors.length + cssValidation.errors.length + blockingSanitizerCount + protocolSecurityCount;
161
+ const totalWarnings = htmlValidation.warnings.length + cssValidation.warnings.length
162
+ + (sanitizationWarnings.length - blockingSanitizerCount)
163
+ + (securityIssues.length - protocolSecurityCount);
103
164
  const totalInfo = htmlValidation.info.length + cssValidation.info.length;
104
165
  const hasSecurityIssues = securityIssues.length > 0;
105
166
 
@@ -114,20 +175,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
114
175
  cssWarnings: cssValidation.warnings,
115
176
  cssInfo: cssValidation.info,
116
177
  securityIssues,
117
- sanitizationWarnings: sanitizationResult?.warnings || [],
178
+ sanitizationWarnings,
118
179
  isValid: htmlValidation.isValid && cssValidation.isValid,
119
180
  isSecure,
120
181
  summary: {
121
182
  totalErrors,
122
183
  totalWarnings,
123
184
  totalInfo,
124
- hasSecurityIssues
125
- }
185
+ hasSecurityIssues,
186
+ },
126
187
  });
127
-
128
188
  } catch (error) {
129
189
  console.error('Validation error:', error);
130
- setValidationState(prev => ({
190
+ setValidationState((prev) => ({
131
191
  ...prev,
132
192
  isValidating: false,
133
193
  htmlErrors: [{
@@ -137,13 +197,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
137
197
  column: 1,
138
198
  rule: 'validation-error',
139
199
  severity: 'error',
140
- source: 'validator'
200
+ source: 'validator',
141
201
  }],
142
202
  isValid: false,
143
203
  summary: {
144
204
  ...prev.summary,
145
- totalErrors: 1
146
- }
205
+ totalErrors: 1,
206
+ },
147
207
  }));
148
208
  }
149
209
  }, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage]);
@@ -208,8 +268,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
208
268
  totalErrors: 0,
209
269
  totalWarnings: 0,
210
270
  totalInfo: 0,
211
- hasSecurityIssues: false
212
- }
271
+ hasSecurityIssues: false,
272
+ },
213
273
  });
214
274
  }, []);
215
275
 
@@ -223,10 +283,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
223
283
  ...validationState.htmlInfo,
224
284
  ...validationState.cssErrors,
225
285
  ...validationState.cssWarnings,
226
- ...validationState.cssInfo
286
+ ...validationState.cssInfo,
227
287
  ];
228
288
 
229
- return allErrors.filter(error => error.severity === severity);
289
+ return allErrors.filter((error) => error.severity === severity);
230
290
  }, [validationState]);
231
291
 
232
292
  /**
@@ -239,32 +299,116 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
239
299
  ...validationState.htmlInfo,
240
300
  ...validationState.cssErrors,
241
301
  ...validationState.cssWarnings,
242
- ...validationState.cssInfo
302
+ ...validationState.cssInfo,
243
303
  ];
244
304
 
245
- return allErrors.filter(error => error.source === source);
305
+ return allErrors.filter((error) => error.source === source);
246
306
  }, [validationState]);
247
307
 
308
+ /**
309
+ * Extract line number from error message if present
310
+ * API errors might contain line numbers in messages like "Error at line 5" or "Line 10: error"
311
+ * Also tries to find line number by searching for the problematic tag in content
312
+ */
313
+ const extractLineNumberFromMessage = useCallback((message) => {
314
+ if (!message || typeof message !== 'string') {
315
+ return null;
316
+ }
317
+ // Try to match patterns like "line 5", "Line 10", "at line 15", "line: 20", etc.
318
+ const lineMatch = message.match(/\b(?:line|Line|LINE)\s*:?\s*(\d+)\b/i);
319
+ if (lineMatch && lineMatch[1]) {
320
+ return parseInt(lineMatch[1], 10);
321
+ }
322
+
323
+ // Try to extract tag name from error message (e.g., "Unsupported tags: test" -> "test")
324
+ const tagMatch = message.match(/(?:tag|tags|Tag|Tags)[\s:]+([a-zA-Z_][a-zA-Z0-9_.]*)/i);
325
+ if (tagMatch && tagMatch[1] && content) {
326
+ const tagName = tagMatch[1];
327
+ const lineNumber = findLineNumberForTag(content, tagName);
328
+ if (lineNumber) {
329
+ return lineNumber;
330
+ }
331
+ }
332
+
333
+ return null;
334
+ }, [content]);
335
+
248
336
  /**
249
337
  * Get all errors and warnings combined
338
+ * Includes both client-side validation errors and API validation errors
250
339
  */
251
340
  const getAllIssues = useCallback(() => {
252
- return [
253
- ...validationState.htmlErrors,
254
- ...validationState.htmlWarnings,
255
- ...validationState.htmlInfo,
256
- ...validationState.cssErrors,
257
- ...validationState.cssWarnings,
258
- ...validationState.cssInfo,
259
- ...validationState.securityIssues.map(issue => ({
260
- type: 'error',
341
+ // API errors (liquid + standard) are blocking – they block Save/Update/Preview/Test
342
+ const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
343
+ const extractedLine = extractLineNumberFromMessage(errorMessage);
344
+ return {
345
+ type: VALIDATION_SEVERITY.ERROR,
346
+ message: errorMessage,
347
+ line: extractedLine,
348
+ column: null,
349
+ rule: 'liquid-api-validation',
350
+ severity: VALIDATION_SEVERITY.ERROR,
351
+ source: ISSUE_SOURCES.LIQUID,
352
+ };
353
+ });
354
+
355
+ const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
356
+ const extractedLine = extractLineNumberFromMessage(errorMessage);
357
+ return {
358
+ type: VALIDATION_SEVERITY.ERROR,
359
+ message: errorMessage,
360
+ line: extractedLine,
361
+ column: null,
362
+ rule: 'standard-api-validation',
363
+ severity: VALIDATION_SEVERITY.ERROR,
364
+ source: 'api-validator',
365
+ };
366
+ });
367
+
368
+ // Security: protocol types are Rule Group #1 (blocking); others are warnings
369
+ // Use issue.position (from findUnsafeContent) to show real line/column when available
370
+ const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
371
+ const contentStr = typeof content === 'string' ? content : '';
372
+ const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
373
+ const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
374
+ const { line, column } = (issue?.position !== undefined && contentStr)
375
+ ? getLineAndColumnFromPosition(contentStr, issue.position)
376
+ : { line: 1, column: 1 };
377
+ return {
378
+ type: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
261
379
  message: `Security issue: ${issue.type}`,
262
- line: 1,
263
- column: 1,
264
- rule: 'security-violation',
265
- severity: 'error',
266
- source: 'security'
267
- }))
380
+ line,
381
+ column,
382
+ rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
383
+ severity: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
384
+ source: 'security',
385
+ };
386
+ });
387
+
388
+ // Sanitization warnings (Rule Group #1 entries have rule set by contentSanitizer)
389
+ const sanitizationAsIssues = (validationState.sanitizationWarnings || []).map((w) => {
390
+ const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING;
391
+ return {
392
+ ...w,
393
+ severity: sev,
394
+ rule: w.rule || 'sanitizer.unknown',
395
+ line: w.line ?? 1,
396
+ column: w.column ?? 1,
397
+ source: w.source || 'sanitizer',
398
+ };
399
+ });
400
+
401
+ const allIssues = [
402
+ ...(validationState.htmlErrors || []),
403
+ ...(validationState.htmlWarnings || []),
404
+ ...(validationState.htmlInfo || []),
405
+ ...(validationState.cssErrors || []),
406
+ ...(validationState.cssWarnings || []),
407
+ ...(validationState.cssInfo || []),
408
+ ...securityAsIssues,
409
+ ...sanitizationAsIssues,
410
+ ...apiLiquidErrors,
411
+ ...apiStandardErrors,
268
412
  ].sort((a, b) => {
269
413
  // Sort by severity (error > warning > info) then by line number
270
414
  const severityOrder = { error: 0, warning: 1, info: 2 };
@@ -273,16 +417,22 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
273
417
  }
274
418
  return (a.line || 0) - (b.line || 0);
275
419
  });
276
- }, [validationState]);
420
+
421
+ // Ensure we always return an array
422
+ return Array.isArray(allIssues) ? allIssues : [];
423
+ }, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
277
424
 
278
425
  /**
279
426
  * Check if validation is clean (no errors or warnings)
427
+ * Includes API validation errors in the check
280
428
  */
281
429
  const isClean = useCallback(() => {
282
- return validationState.summary.totalErrors === 0 &&
283
- validationState.summary.totalWarnings === 0 &&
284
- !validationState.summary.hasSecurityIssues;
285
- }, [validationState.summary]);
430
+ const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
431
+ return validationState.summary.totalErrors === 0
432
+ && validationState.summary.totalWarnings === 0
433
+ && !validationState.summary.hasSecurityIssues
434
+ && !hasApiErrors;
435
+ }, [validationState.summary, apiValidationErrors]);
286
436
 
287
437
  // Effect to validate content when it changes
288
438
  useEffect(() => {
@@ -292,14 +442,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
292
442
  }, [content, validateContent, enableRealTime]);
293
443
 
294
444
  // Cleanup on unmount
295
- useEffect(() => {
296
- return () => {
297
- if (debounceRef.current) {
298
- clearTimeout(debounceRef.current);
299
- }
300
- };
445
+ useEffect(() => () => {
446
+ if (debounceRef.current) {
447
+ clearTimeout(debounceRef.current);
448
+ }
301
449
  }, []);
302
450
 
451
+ const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
452
+
453
+ const protocolTypes = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
454
+ // Client-side Liquid validation errors are blocking (genuine syntax errors)
455
+ const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === ISSUE_SOURCES.LIQUID && e.severity === VALIDATION_SEVERITY.ERROR);
456
+ const hasBlockingErrors = (validationState.sanitizationWarnings || []).some((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)) || (validationState.securityIssues || []).some((s) => protocolTypes.includes(s?.type)) || hasApiErrors || hasClientSideLiquidErrors;
457
+
303
458
  return {
304
459
  // Validation state
305
460
  ...validationState,
@@ -316,10 +471,12 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
316
471
  isClean,
317
472
 
318
473
  // Computed properties
319
- hasErrors: validationState.summary.totalErrors > 0,
474
+ hasErrors: validationState.summary.totalErrors > 0 || hasApiErrors,
320
475
  hasWarnings: validationState.summary.totalWarnings > 0,
321
- hasSecurityIssues: validationState.summary.hasSecurityIssues
476
+ hasSecurityIssues: validationState.summary.hasSecurityIssues,
477
+ /** True only when Rule Group #1 (Input & Sanitization) issues exist. Use for UI gating. */
478
+ hasBlockingErrors,
322
479
  };
323
480
  };
324
481
 
325
- export default useValidation;
482
+ export default useValidation;
@@ -26,4 +26,4 @@ export { HTML_EDITOR_VARIANTS, DEVICE_TYPES } from './constants';
26
26
  * - Default export (lazy): ~0KB initial bundle (loads ~400KB when needed)
27
27
  * - HTMLEditorSync: ~400KB added to initial bundle
28
28
  * - Constants: ~1KB added to initial bundle
29
- */
29
+ */