@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.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/package.json +16 -2
- package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
- package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
- package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
- package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
- package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
- package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
- package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
- package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
- package/v2Components/HtmlEditor/constants.js +241 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
- package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
- package/v2Components/HtmlEditor/index.js +29 -0
- package/v2Components/HtmlEditor/index.lazy.js +114 -0
- package/v2Components/HtmlEditor/messages.js +389 -0
- package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
- package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
- package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
- package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
- package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
- package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
- package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
- package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
- package/v2Containers/EmailWrapper/index.js +8 -1
- package/v2Containers/Templates/constants.js +8 -0
- package/v2Containers/Templates/index.js +56 -28
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
- package/v2Containers/Whatsapp/constants.js +26 -2
- package/v2Containers/Whatsapp/index.js +4 -1
- 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
|
+
};
|