@capillarytech/creatives-library 8.0.207 → 8.0.209

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 (77) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Templates/constants.js +8 -0
  73. package/v2Containers/Templates/index.js +56 -28
  74. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  75. package/v2Containers/Whatsapp/constants.js +26 -2
  76. package/v2Containers/Whatsapp/index.js +4 -1
  77. package/v2Containers/Whatsapp/tests/index.test.js +460 -18
@@ -0,0 +1,508 @@
1
+ /**
2
+ * HTML Validation Utility
3
+ * Uses HTMLHint for comprehensive HTML validation
4
+ */
5
+
6
+ import { HTMLHint } from 'htmlhint';
7
+ import { validateLiquidHTML } from './liquidTemplateSupport';
8
+
9
+ // Default message formatter for when intl is not available
10
+ const defaultMessageFormatter = (messageKey, values = {}) => {
11
+ // Fallback messages for when intl is not available
12
+ const fallbackMessages = {
13
+ 'validator.validationFailed': `Validation failed: ${values.error || 'Unknown error'}`,
14
+ 'validator.liquidValidationFailed': 'Liquid validation failed',
15
+ 'validator.unsafeProtocolDetected': `Potentially unsafe protocol detected: ${values.protocol || 'unknown'}`,
16
+ 'validator.scriptTagsDetected': 'Script tags detected - may be filtered in production',
17
+ 'validator.outlookIncompatible': `Element may not be supported in Outlook: ${values.element || 'unknown'}`,
18
+ 'validator.emailCssUnsupported': 'CSS feature may not be supported in email clients',
19
+ 'validator.mobileIncompatible': `Element may not be supported on mobile: ${values.element || 'unknown'}`,
20
+ 'validator.largeImageDetected': 'Large image dimensions detected - consider mobile optimization',
21
+ 'validator.unclosedCssRule': 'Unclosed CSS rule detected',
22
+ 'validator.emptyCssRule': 'Empty CSS rule detected',
23
+ 'validator.cssValidationFailed': `CSS validation failed: ${values.error || 'Unknown error'}`
24
+ };
25
+
26
+ return fallbackMessages[messageKey] || messageKey;
27
+ };
28
+
29
+ // Custom HTMLHint rules configuration
30
+ const HTML_RULES = {
31
+ 'tagname-lowercase': true,
32
+ 'attr-lowercase': true,
33
+ 'attr-value-double-quotes': true,
34
+ 'attr-value-not-empty': false,
35
+ 'attr-no-duplication': true,
36
+ 'doctype-first': false, // Allow HTML fragments
37
+ 'tag-pair': true,
38
+ 'tag-self-close': false,
39
+ 'spec-char-escape': false, // Allow > in HTML content - it's valid
40
+ 'id-unique': true,
41
+ 'src-not-empty': true,
42
+ 'title-require': false, // Allow fragments without title
43
+ 'alt-require': true,
44
+ 'doctype-html5': false, // Allow HTML fragments
45
+ 'id-class-value': 'dash',
46
+ 'style-disabled': false, // Allow inline styles
47
+ 'inline-style-disabled': false, // Allow inline styles
48
+ 'inline-script-disabled': false, // Allow inline scripts
49
+ 'space-tab-mixed-disabled': 'space',
50
+ 'id-class-ad-disabled': false,
51
+ 'href-abs-or-rel': false,
52
+ 'attr-unsafe-chars': true
53
+ };
54
+
55
+ // Additional custom validation rules
56
+ const CUSTOM_VALIDATIONS = {
57
+ // Check for potentially problematic patterns
58
+ UNSAFE_PROTOCOLS: /javascript:|data:|vbscript:/gi,
59
+ UNCLOSED_TAGS: /<(?!\/|!)(\w+)(?:[^>]*[^\/])?>(?!.*<\/\1>)/gi,
60
+ MALFORMED_ATTRIBUTES: /\s(\w+)(?!=)/g,
61
+ SCRIPT_TAGS: /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
62
+
63
+ // Email-specific validations
64
+ OUTLOOK_INCOMPATIBLE: /<(canvas|video|audio|svg)\b/gi,
65
+ UNSUPPORTED_CSS: /@(media|keyframes|supports)\s*\{/gi,
66
+
67
+ // InApp-specific validations
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
70
+ };
71
+
72
+ /**
73
+ * Validates HTML content and returns detailed error information
74
+ * @param {string} html - HTML content to validate
75
+ * @param {string} variant - Editor variant ('email' or 'inapp')
76
+ * @param {Function} formatMessage - Message formatter function for internationalization
77
+ * @returns {Object} Validation result with errors and warnings
78
+ */
79
+ export const validateHTML = (html, variant = 'email', formatMessage = defaultMessageFormatter) => {
80
+ if (!html || typeof html !== 'string') {
81
+ return {
82
+ isValid: true,
83
+ errors: [],
84
+ warnings: [],
85
+ info: []
86
+ };
87
+ }
88
+
89
+ const results = {
90
+ isValid: true,
91
+ errors: [],
92
+ warnings: [],
93
+ info: []
94
+ };
95
+
96
+ try {
97
+ // Run HTMLHint validation
98
+ const htmlHintResults = HTMLHint.verify(html, HTML_RULES);
99
+
100
+ // Process HTMLHint results
101
+ htmlHintResults.forEach(issue => {
102
+ const error = {
103
+ type: issue.type,
104
+ message: issue.message,
105
+ line: issue.line,
106
+ column: issue.col,
107
+ rule: issue.rule.id,
108
+ severity: getSeverityLevel(issue.type, issue.rule.id),
109
+ source: 'htmlhint'
110
+ };
111
+
112
+ if (error.severity === 'error') {
113
+ results.errors.push(error);
114
+ results.isValid = false;
115
+ } else if (error.severity === 'warning') {
116
+ results.warnings.push(error);
117
+ } else {
118
+ results.info.push(error);
119
+ }
120
+ });
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
+ } catch (error) {
129
+ results.errors.push({
130
+ type: 'error',
131
+ message: formatMessage('validator.validationFailed', { error: error.message }),
132
+ line: 1,
133
+ column: 1,
134
+ rule: 'validation-error',
135
+ severity: 'error',
136
+ source: 'validator'
137
+ });
138
+ results.isValid = false;
139
+ }
140
+
141
+ return results;
142
+ };
143
+
144
+ /**
145
+ * Determines severity level based on error type and rule
146
+ */
147
+ const getSeverityLevel = (type, ruleId) => {
148
+ const errorRules = [
149
+ 'tag-pair',
150
+ 'attr-no-duplication',
151
+ 'id-unique',
152
+ 'spec-char-escape'
153
+ ];
154
+
155
+ const warningRules = [
156
+ 'tagname-lowercase',
157
+ 'attr-lowercase',
158
+ 'attr-value-double-quotes',
159
+ 'alt-require'
160
+ ];
161
+
162
+ if (type === 'error' || errorRules.includes(ruleId)) {
163
+ return 'error';
164
+ } else if (warningRules.includes(ruleId)) {
165
+ return 'warning';
166
+ } else {
167
+ return 'info';
168
+ }
169
+ };
170
+
171
+ /**
172
+ * Runs custom validation rules
173
+ * @param {string} html - HTML content to validate
174
+ * @param {string} variant - Editor variant
175
+ * @param {Object} results - Results object to add validation issues to
176
+ * @param {Function} formatMessage - Message formatter function
177
+ */
178
+ const runCustomValidations = (html, variant, results, formatMessage = defaultMessageFormatter) => {
179
+ // Check for unsafe protocols using RegExp.exec loop
180
+ const unsafeProtocolsRegex = new RegExp(CUSTOM_VALIDATIONS.UNSAFE_PROTOCOLS.source, CUSTOM_VALIDATIONS.UNSAFE_PROTOCOLS.flags);
181
+ unsafeProtocolsRegex.lastIndex = 0; // Reset lastIndex before running
182
+ let match;
183
+ while ((match = unsafeProtocolsRegex.exec(html)) !== null) {
184
+ results.errors.push({
185
+ type: 'error',
186
+ message: formatMessage('validator.unsafeProtocolDetected', { protocol: match[0] }),
187
+ line: getLineNumber(html, match.index),
188
+ column: 1,
189
+ rule: 'unsafe-protocol',
190
+ severity: 'error',
191
+ source: 'custom'
192
+ });
193
+ results.isValid = false;
194
+
195
+ // Guard against zero-length matches to avoid infinite loops
196
+ if (match[0].length === 0) {
197
+ unsafeProtocolsRegex.lastIndex++;
198
+ }
199
+ }
200
+
201
+ // Check for script tags (security concern) using RegExp.exec loop
202
+ const scriptTagsRegex = new RegExp(CUSTOM_VALIDATIONS.SCRIPT_TAGS.source, CUSTOM_VALIDATIONS.SCRIPT_TAGS.flags);
203
+ scriptTagsRegex.lastIndex = 0; // Reset lastIndex before running
204
+ while ((match = scriptTagsRegex.exec(html)) !== null) {
205
+ results.warnings.push({
206
+ type: 'warning',
207
+ message: formatMessage('validator.scriptTagsDetected'),
208
+ line: getLineNumber(html, match.index),
209
+ column: 1,
210
+ rule: 'script-tag-warning',
211
+ severity: 'warning',
212
+ source: 'custom'
213
+ });
214
+
215
+ // Guard against zero-length matches to avoid infinite loops
216
+ if (match[0].length === 0) {
217
+ scriptTagsRegex.lastIndex++;
218
+ }
219
+ }
220
+
221
+ // Variant-specific validations
222
+ if (variant === 'email') {
223
+ validateEmailSpecific(html, results, formatMessage);
224
+ } else if (variant === 'inapp') {
225
+ validateInAppSpecific(html, results, formatMessage);
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Email-specific validations
231
+ * @param {string} html - HTML content to validate
232
+ * @param {Object} results - Results object to add validation issues to
233
+ * @param {Function} formatMessage - Message formatter function
234
+ */
235
+ const validateEmailSpecific = (html, results, formatMessage = defaultMessageFormatter) => {
236
+ // Check for Outlook incompatible elements using RegExp.exec loop
237
+ const outlookIncompatibleRegex = new RegExp(CUSTOM_VALIDATIONS.OUTLOOK_INCOMPATIBLE.source, CUSTOM_VALIDATIONS.OUTLOOK_INCOMPATIBLE.flags);
238
+ outlookIncompatibleRegex.lastIndex = 0; // Reset lastIndex before running
239
+ let match;
240
+ while ((match = outlookIncompatibleRegex.exec(html)) !== null) {
241
+ results.warnings.push({
242
+ type: 'warning',
243
+ message: formatMessage('validator.outlookIncompatible', { element: match[0] }),
244
+ line: getLineNumber(html, match.index),
245
+ column: 1,
246
+ rule: 'outlook-compatibility',
247
+ severity: 'warning',
248
+ source: 'email-specific'
249
+ });
250
+
251
+ // Guard against zero-length matches to avoid infinite loops
252
+ if (match[0].length === 0) {
253
+ outlookIncompatibleRegex.lastIndex++;
254
+ }
255
+ }
256
+
257
+ // Check for unsupported CSS using RegExp.exec loop
258
+ const unsupportedCSSRegex = new RegExp(CUSTOM_VALIDATIONS.UNSUPPORTED_CSS.source, CUSTOM_VALIDATIONS.UNSUPPORTED_CSS.flags);
259
+ unsupportedCSSRegex.lastIndex = 0; // Reset lastIndex before running
260
+ while ((match = unsupportedCSSRegex.exec(html)) !== null) {
261
+ results.warnings.push({
262
+ type: 'warning',
263
+ message: formatMessage('validator.emailCssUnsupported'),
264
+ line: getLineNumber(html, match.index),
265
+ column: 1,
266
+ rule: 'email-css-compatibility',
267
+ severity: 'warning',
268
+ source: 'email-specific'
269
+ });
270
+
271
+ // Guard against zero-length matches to avoid infinite loops
272
+ if (match[0].length === 0) {
273
+ unsupportedCSSRegex.lastIndex++;
274
+ }
275
+ }
276
+ };
277
+
278
+ /**
279
+ * InApp-specific validations
280
+ * @param {string} html - HTML content to validate
281
+ * @param {Object} results - Results object to add validation issues to
282
+ * @param {Function} formatMessage - Message formatter function
283
+ */
284
+ const validateInAppSpecific = (html, results, formatMessage = defaultMessageFormatter) => {
285
+ // Check for mobile incompatible elements using RegExp.exec loop
286
+ const mobileIncompatibleRegex = new RegExp(CUSTOM_VALIDATIONS.MOBILE_INCOMPATIBLE.source, CUSTOM_VALIDATIONS.MOBILE_INCOMPATIBLE.flags);
287
+ mobileIncompatibleRegex.lastIndex = 0; // Reset lastIndex before running
288
+ let match;
289
+ while ((match = mobileIncompatibleRegex.exec(html)) !== null) {
290
+ results.warnings.push({
291
+ type: 'warning',
292
+ message: formatMessage('validator.mobileIncompatible', { element: match[0] }),
293
+ line: getLineNumber(html, match.index),
294
+ column: 1,
295
+ rule: 'mobile-compatibility',
296
+ severity: 'warning',
297
+ source: 'inapp-specific'
298
+ });
299
+
300
+ // Guard against zero-length matches to avoid infinite loops
301
+ if (match[0].length === 0) {
302
+ mobileIncompatibleRegex.lastIndex++;
303
+ }
304
+ }
305
+
306
+ // Check for large images that might not fit mobile screens using RegExp.exec loop
307
+ const largeImagesRegex = new RegExp(CUSTOM_VALIDATIONS.LARGE_IMAGES.source, CUSTOM_VALIDATIONS.LARGE_IMAGES.flags);
308
+ largeImagesRegex.lastIndex = 0; // Reset lastIndex before running
309
+ while ((match = largeImagesRegex.exec(html)) !== null) {
310
+ results.info.push({
311
+ type: 'info',
312
+ message: formatMessage('validator.largeImageDetected'),
313
+ line: getLineNumber(html, match.index),
314
+ column: 1,
315
+ rule: 'mobile-image-size',
316
+ severity: 'info',
317
+ source: 'inapp-specific'
318
+ });
319
+
320
+ // Guard against zero-length matches to avoid infinite loops
321
+ if (match[0].length === 0) {
322
+ largeImagesRegex.lastIndex++;
323
+ }
324
+ }
325
+ };
326
+
327
+ /**
328
+ * Runs Liquid template validation
329
+ * @param {string} html - HTML content to validate
330
+ * @param {string} variant - Editor variant
331
+ * @param {Object} results - Results object to add validation issues to
332
+ * @param {Function} formatMessage - Message formatter function
333
+ */
334
+ const runLiquidValidation = (html, variant, results, formatMessage = defaultMessageFormatter) => {
335
+ try {
336
+ const liquidResults = validateLiquidHTML(html, variant);
337
+
338
+ // Merge Liquid validation results
339
+ if (liquidResults.errors) {
340
+ results.errors.push(...liquidResults.errors);
341
+ if (liquidResults.errors.length > 0) {
342
+ results.isValid = false;
343
+ }
344
+ }
345
+
346
+ if (liquidResults.warnings) {
347
+ results.warnings.push(...liquidResults.warnings);
348
+ }
349
+
350
+ if (liquidResults.info) {
351
+ results.info.push(...liquidResults.info);
352
+ }
353
+ } catch (error) {
354
+ console.warn(formatMessage('validator.liquidValidationFailed'), error);
355
+ // Don't fail the entire validation if Liquid validation has issues
356
+ }
357
+ };
358
+
359
+ /**
360
+ * Gets line number for a character position in text
361
+ */
362
+ const getLineNumber = (text, position) => {
363
+ if (position === undefined || position < 0) return 1;
364
+ return text.substring(0, position).split('\n').length;
365
+ };
366
+
367
+ /**
368
+ * Validates CSS content separately
369
+ * @param {string} css - CSS content to validate
370
+ * @param {Function} formatMessage - Message formatter function for internationalization
371
+ * @returns {Object} Validation result with errors and warnings
372
+ */
373
+ export const validateCSS = (css, formatMessage = defaultMessageFormatter) => {
374
+ const results = {
375
+ isValid: true,
376
+ errors: [],
377
+ warnings: [],
378
+ info: []
379
+ };
380
+
381
+ if (!css || typeof css !== 'string') {
382
+ return results;
383
+ }
384
+
385
+ try {
386
+ // Basic CSS validation patterns
387
+ const validationPatterns = {
388
+ unclosedBraces: /\{[^{}]*$/gm,
389
+ invalidProperty: /[^;{}]+:\s*[^;{}]*[^;}]/g,
390
+ missingColon: /[^;{}]+\s+[^;{}:]+;/g,
391
+ emptyRule: /[^{}]+\{\s*\}/g
392
+ };
393
+
394
+ // Check for unclosed braces using RegExp.exec loop
395
+ const unclosedBracesRegex = new RegExp(validationPatterns.unclosedBraces.source, validationPatterns.unclosedBraces.flags);
396
+ unclosedBracesRegex.lastIndex = 0; // Reset lastIndex before running
397
+ let match;
398
+ while ((match = unclosedBracesRegex.exec(css)) !== null) {
399
+ results.errors.push({
400
+ type: 'error',
401
+ message: formatMessage('validator.unclosedCssRule'),
402
+ line: getLineNumber(css, match.index),
403
+ column: 1,
404
+ rule: 'unclosed-brace',
405
+ severity: 'error',
406
+ source: 'css-validator'
407
+ });
408
+ results.isValid = false;
409
+
410
+ // Guard against zero-length matches to avoid infinite loops
411
+ if (match[0].length === 0) {
412
+ unclosedBracesRegex.lastIndex++;
413
+ }
414
+ }
415
+
416
+ // Check for empty rules using RegExp.exec loop
417
+ const emptyRulesRegex = new RegExp(validationPatterns.emptyRule.source, validationPatterns.emptyRule.flags);
418
+ emptyRulesRegex.lastIndex = 0; // Reset lastIndex before running
419
+ while ((match = emptyRulesRegex.exec(css)) !== null) {
420
+ results.errors.push({
421
+ type: 'error',
422
+ message: formatMessage('validator.emptyCssRule'),
423
+ line: getLineNumber(css, match.index),
424
+ column: 1,
425
+ rule: 'empty-rule',
426
+ severity: 'error',
427
+ source: 'css-validator'
428
+ });
429
+ results.isValid = false;
430
+
431
+ // Guard against zero-length matches to avoid infinite loops
432
+ if (match[0].length === 0) {
433
+ emptyRulesRegex.lastIndex++;
434
+ }
435
+ }
436
+
437
+ } catch (error) {
438
+ results.errors.push({
439
+ type: 'error',
440
+ message: formatMessage('validator.cssValidationFailed', { error: error.message }),
441
+ line: 1,
442
+ column: 1,
443
+ rule: 'css-validation-error',
444
+ severity: 'error',
445
+ source: 'css-validator'
446
+ });
447
+ results.isValid = false;
448
+ }
449
+
450
+ return results;
451
+ };
452
+
453
+ /**
454
+ * Extracts and validates CSS from HTML content
455
+ * @param {string} html - HTML content containing CSS
456
+ * @param {Function} formatMessage - Message formatter function for internationalization
457
+ * @returns {Object} Combined validation results from all CSS blocks
458
+ */
459
+ export const extractAndValidateCSS = (html, formatMessage = defaultMessageFormatter) => {
460
+ if (!html) return { isValid: true, errors: [], warnings: [], info: [] };
461
+
462
+ // Extract CSS from style tags
463
+ const styleTagPattern = /<style[^>]*>([\s\S]*?)<\/style>/gi;
464
+ let match;
465
+ let allResults = {
466
+ isValid: true,
467
+ errors: [],
468
+ warnings: [],
469
+ info: []
470
+ };
471
+
472
+ while ((match = styleTagPattern.exec(html)) !== null) {
473
+ const cssContent = match[1];
474
+ const cssResults = validateCSS(cssContent, formatMessage);
475
+
476
+ // Merge results
477
+ allResults.errors.push(...cssResults.errors);
478
+ allResults.warnings.push(...cssResults.warnings);
479
+ allResults.info.push(...cssResults.info);
480
+
481
+ if (!cssResults.isValid) {
482
+ allResults.isValid = false;
483
+ }
484
+ }
485
+
486
+ // Check for unclosed style tags
487
+ const unclosedStylePattern = /<style[^>]*>(?![\s\S]*?<\/style>)/gi;
488
+ if (unclosedStylePattern.test(html)) {
489
+ allResults.errors.push({
490
+ type: 'error',
491
+ message: formatMessage('validator.unclosedCssRule'),
492
+ line: 1,
493
+ column: 1,
494
+ rule: 'unclosed-style-tag',
495
+ severity: 'error',
496
+ source: 'css-validator'
497
+ });
498
+ allResults.isValid = false;
499
+ }
500
+
501
+ return allResults;
502
+ };
503
+
504
+ export default {
505
+ validateHTML,
506
+ validateCSS,
507
+ extractAndValidateCSS
508
+ };