@capillarytech/creatives-library 8.0.208 → 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/config/app.js +1 -2
  4. package/package.json +16 -2
  5. package/services/api.js +0 -2
  6. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  7. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  8. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  9. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  10. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  11. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  12. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  14. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  15. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  19. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  20. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  21. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  29. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  30. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  31. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  32. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  35. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  36. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  38. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  39. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  42. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  43. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  44. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  45. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  46. package/v2Components/HtmlEditor/constants.js +241 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  50. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  51. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  52. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  53. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  54. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  55. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  56. package/v2Components/HtmlEditor/index.js +29 -0
  57. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  58. package/v2Components/HtmlEditor/messages.js +389 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  63. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  64. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  65. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  66. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  67. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  68. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  69. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  70. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
  72. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  73. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  74. package/v2Containers/EmailWrapper/index.js +8 -1
  75. package/v2Containers/Templates/constants.js +8 -0
  76. package/v2Containers/Templates/index.js +56 -28
  77. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
@@ -0,0 +1,524 @@
1
+ /**
2
+ * Liquid Template Language Support for CodeMirror 6
3
+ * Provides syntax highlighting and validation for Liquid templates in HTML
4
+ */
5
+
6
+ import { html } from '@codemirror/lang-html';
7
+ import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
8
+ import { tags } from '@lezer/highlight';
9
+ import { EditorView } from '@codemirror/view';
10
+
11
+ /**
12
+ * Liquid Template Syntax Patterns
13
+ */
14
+ export const LIQUID_PATTERNS = {
15
+ // Liquid output tags: {{ variable }}
16
+ OUTPUT_TAG: /\{\{\s*([^}]+)\s*\}\}/g,
17
+
18
+ // Liquid logic tags: {% if condition %}, {% for item in items %}, etc.
19
+ LOGIC_TAG: /\{%\s*([^%]+)\s*%\}/g,
20
+
21
+ // Liquid comments: {% comment %} ... {% endcomment %}
22
+ COMMENT_TAG: /\{%\s*comment\s*%\}[\s\S]*?\{%\s*endcomment\s*%\}/g,
23
+
24
+ // Liquid filters: {{ variable | filter }}
25
+ FILTER: /\|\s*([a-zA-Z_][a-zA-Z0-9_]*)/g,
26
+
27
+ // Liquid variables and properties
28
+ VARIABLE: /([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/g,
29
+
30
+ // Liquid keywords
31
+ KEYWORDS: /\b(if|unless|elsif|else|endif|for|endfor|case|when|endcase|assign|capture|endcapture|include|render|layout|break|continue|cycle|tablerow|endtablerow|raw|endraw|liquid)\b/g,
32
+
33
+ // Liquid operators
34
+ OPERATORS: /\b(and|or|not|contains|in|==|!=|<|>|<=|>=)\b/g,
35
+
36
+ // String literals in Liquid
37
+ STRING_LITERALS: /(["'])((?:\\.|(?!\1)[^\\])*?)\1/g
38
+ };
39
+
40
+ /**
41
+ * Enhanced VS Code Theme with Liquid Template Support
42
+ */
43
+ export const liquidVSCodeTheme = HighlightStyle.define([
44
+ // Base content
45
+ { tag: tags.content, color: '#d4d4d4' },
46
+ { tag: tags.name, color: '#d4d4d4' },
47
+
48
+ // HTML elements
49
+ { tag: tags.tagName, color: '#569cd6', fontWeight: 'bold' },
50
+ { tag: tags.attributeName, color: '#92c5f8' },
51
+ { tag: tags.attributeValue, color: '#ce9178' },
52
+ { tag: tags.angleBracket, color: '#808080' },
53
+ { tag: tags.quote, color: '#ce9178' },
54
+
55
+ // Standard syntax
56
+ { tag: tags.comment, color: '#6a9955', fontStyle: 'italic' },
57
+ { tag: tags.string, color: '#ce9178' },
58
+ { tag: tags.number, color: '#b5cea8' },
59
+ { tag: tags.keyword, color: '#569cd6', fontWeight: 'bold' },
60
+ { tag: tags.operator, color: '#d4d4d4' },
61
+ { tag: tags.variableName, color: '#9cdcfe' },
62
+ { tag: tags.function, color: '#dcdcaa' },
63
+
64
+ // Liquid-specific styling (using meta and special tags)
65
+ { tag: tags.meta, color: '#c586c0', fontWeight: 'bold' }, // Liquid tags {% %}
66
+ { tag: tags.special, color: '#4fc1ff' }, // Liquid output {{ }}
67
+ { tag: tags.processingInstruction, color: '#569cd6' }, // Liquid keywords
68
+ { tag: tags.atom, color: '#4ec9b0' }, // Liquid filters
69
+ { tag: tags.punctuation, color: '#d4d4d4' },
70
+ { tag: tags.bracket, color: '#ffd700' },
71
+ { tag: tags.brace, color: '#ffd700' }
72
+ ]);
73
+
74
+ /**
75
+ * Liquid Template Validator
76
+ */
77
+ export class LiquidValidator {
78
+ constructor() {
79
+ this.errors = [];
80
+ this.warnings = [];
81
+ this.info = [];
82
+ }
83
+
84
+ /**
85
+ * Validates Liquid template syntax in HTML content
86
+ * @param {string} html - HTML content with Liquid templates
87
+ * @returns {Object} Validation results
88
+ */
89
+ validate(html) {
90
+ this.errors = [];
91
+ this.warnings = [];
92
+ this.info = [];
93
+
94
+ if (!html || typeof html !== 'string') {
95
+ return this.getResults();
96
+ }
97
+
98
+ // Validate Liquid syntax
99
+ this.validateLiquidTags(html);
100
+ this.validateLiquidLogic(html);
101
+ this.validateLiquidFilters(html);
102
+ this.validateLiquidVariables(html);
103
+
104
+ return this.getResults();
105
+ }
106
+
107
+ /**
108
+ * Validates Liquid tag syntax
109
+ */
110
+ validateLiquidTags(html) {
111
+ // Only validate actual Liquid tags, not HTML content
112
+ // Check for unclosed output tags: {{ without matching }}
113
+ this.validateUnclosedOutputTags(html);
114
+
115
+ // Check for unclosed logic tags: {% without matching %}
116
+ this.validateUnclosedLogicTags(html);
117
+
118
+ // Check for nested braces within Liquid tags
119
+ this.validateNestedBraces(html);
120
+ }
121
+
122
+ /**
123
+ * Validates unclosed output tags using stack-based matching
124
+ */
125
+ validateUnclosedOutputTags(html) {
126
+ const stack = []; // Stack to track opening {{ positions
127
+
128
+ // Create a combined pattern to find all {{ and }} tokens in order
129
+ const combinedPattern = /(\{\{|\}\})/g;
130
+ let match;
131
+
132
+ // Perform single left-to-right scan
133
+ while ((match = combinedPattern.exec(html)) !== null) {
134
+ const token = match[1];
135
+ const position = match.index;
136
+
137
+ if (token === '{{') {
138
+ // Push opening brace position onto stack
139
+ stack.push(position);
140
+ } else if (token === '}}') {
141
+ // Found closing brace
142
+ if (stack.length > 0) {
143
+ // Pop matching opening brace from stack
144
+ stack.pop();
145
+ } else {
146
+ // Stray closing brace - no matching opening brace
147
+ this.errors.push({
148
+ type: 'error',
149
+ message: 'Stray closing }} without matching opening {{',
150
+ line: this.getLineNumber(html, position),
151
+ column: 1,
152
+ rule: 'liquid-stray-closing-output',
153
+ severity: 'error',
154
+ source: 'liquid-validator'
155
+ });
156
+ }
157
+ }
158
+ }
159
+
160
+ // After scan, any remaining entries on stack are unclosed opening braces
161
+ if (stack.length > 0) {
162
+ // Report each unclosed opening brace
163
+ stack.forEach(position => {
164
+ this.errors.push({
165
+ type: 'error',
166
+ message: 'unclosed Liquid output tag - missing }}',
167
+ line: this.getLineNumber(html, position),
168
+ column: 1,
169
+ rule: 'liquid-unclosed-output',
170
+ severity: 'error',
171
+ source: 'liquid-validator'
172
+ });
173
+ });
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Validates unclosed logic tags
179
+ */
180
+ validateUnclosedLogicTags(html) {
181
+ // Find all {% positions
182
+ const openTags = [];
183
+ let match;
184
+ const openPattern = /\{%/g;
185
+ while ((match = openPattern.exec(html)) !== null) {
186
+ openTags.push(match.index);
187
+ }
188
+
189
+ // Find all %} positions
190
+ const closeTags = [];
191
+ const closePattern = /%\}/g;
192
+ while ((match = closePattern.exec(html)) !== null) {
193
+ closeTags.push(match.index);
194
+ }
195
+
196
+ // Check if we have unmatched opening tags
197
+ if (openTags.length > closeTags.length) {
198
+ const unmatchedCount = openTags.length - closeTags.length;
199
+ this.errors.push({
200
+ type: 'error',
201
+ message: `${unmatchedCount} unclosed Liquid logic tag(s) - missing %}`,
202
+ line: this.getLineNumber(html, openTags[openTags.length - 1]),
203
+ column: 1,
204
+ rule: 'liquid-unclosed-logic',
205
+ severity: 'error',
206
+ source: 'liquid-validator'
207
+ });
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Validates nested braces within Liquid tags
213
+ */
214
+ validateNestedBraces(html) {
215
+ // Check for {{ inside {{ }} tags
216
+ const nestedOutputPattern = /\{\{[^}]*\{\{[^}]*\}\}/g;
217
+ const nestedOutput = html.match(nestedOutputPattern);
218
+ if (nestedOutput) {
219
+ nestedOutput.forEach(match => {
220
+ this.errors.push({
221
+ type: 'error',
222
+ message: `Nested braces in Liquid output tag: ${match}`,
223
+ line: this.getLineNumber(html, html.indexOf(match)),
224
+ column: 1,
225
+ rule: 'liquid-nested-braces',
226
+ severity: 'error',
227
+ source: 'liquid-validator'
228
+ });
229
+ });
230
+ }
231
+
232
+ // Check for {% inside {% %} tags
233
+ const nestedLogicPattern = /\{%[^%]*\{%[^%]*%\}/g;
234
+ const nestedLogic = html.match(nestedLogicPattern);
235
+ if (nestedLogic) {
236
+ nestedLogic.forEach(match => {
237
+ this.errors.push({
238
+ type: 'error',
239
+ message: `Nested braces in Liquid logic tag: ${match}`,
240
+ line: this.getLineNumber(html, html.indexOf(match)),
241
+ column: 1,
242
+ rule: 'liquid-nested-braces',
243
+ severity: 'error',
244
+ source: 'liquid-validator'
245
+ });
246
+ });
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Validates Liquid logic blocks
252
+ */
253
+ validateLiquidLogic(html) {
254
+ const logicTags = [];
255
+ let match;
256
+
257
+ // Extract all logic tags
258
+ const logicPattern = /\{%\s*([^%]+)\s*%\}/g;
259
+ while ((match = logicPattern.exec(html)) !== null) {
260
+ const content = match[1].trim();
261
+ const keyword = content.split(/\s+/)[0];
262
+
263
+ logicTags.push({
264
+ full: match[0],
265
+ content: content,
266
+ keyword: keyword,
267
+ position: match.index,
268
+ line: this.getLineNumber(html, match.index)
269
+ });
270
+ }
271
+
272
+ // Check for balanced tags
273
+ this.validateBalancedTags(logicTags);
274
+ }
275
+
276
+ /**
277
+ * Validates balanced Liquid tags (if/endif, for/endfor, etc.)
278
+ */
279
+ validateBalancedTags(logicTags) {
280
+ const stack = [];
281
+ const pairs = {
282
+ 'if': 'endif',
283
+ 'unless': 'endunless',
284
+ 'for': 'endfor',
285
+ 'case': 'endcase',
286
+ 'capture': 'endcapture',
287
+ 'comment': 'endcomment',
288
+ 'tablerow': 'endtablerow',
289
+ 'raw': 'endraw'
290
+ };
291
+
292
+ logicTags.forEach(tag => {
293
+ const keyword = tag.keyword;
294
+
295
+ if (pairs[keyword]) {
296
+ // Opening tag
297
+ stack.push({ keyword, tag });
298
+ } else if (Object.values(pairs).includes(keyword)) {
299
+ // Closing tag
300
+ const expectedOpening = Object.keys(pairs).find(key => pairs[key] === keyword);
301
+ const lastOpening = stack.pop();
302
+
303
+ if (!lastOpening) {
304
+ this.errors.push({
305
+ type: 'error',
306
+ message: `Unexpected closing tag: {% ${keyword} %}`,
307
+ line: tag.line,
308
+ column: 1,
309
+ rule: 'liquid-unexpected-closing',
310
+ severity: 'error',
311
+ source: 'liquid-validator'
312
+ });
313
+ } else if (lastOpening.keyword !== expectedOpening) {
314
+ this.errors.push({
315
+ type: 'error',
316
+ message: `Mismatched Liquid tags: {% ${lastOpening.keyword} %} ... {% ${keyword} %}`,
317
+ line: tag.line,
318
+ column: 1,
319
+ rule: 'liquid-mismatched-tags',
320
+ severity: 'error',
321
+ source: 'liquid-validator'
322
+ });
323
+ }
324
+ }
325
+ });
326
+
327
+ // Check for unclosed opening tags
328
+ stack.forEach(unclosed => {
329
+ this.errors.push({
330
+ type: 'error',
331
+ message: `Unclosed Liquid tag: {% ${unclosed.keyword} %}`,
332
+ line: unclosed.tag.line,
333
+ column: 1,
334
+ rule: 'liquid-unclosed-tag',
335
+ severity: 'error',
336
+ source: 'liquid-validator'
337
+ });
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Validates Liquid filters
343
+ */
344
+ validateLiquidFilters(html) {
345
+ // Check for malformed filters
346
+ const malformedFilters = html.match(/\|\s*\||\|\s*$/gm);
347
+ if (malformedFilters) {
348
+ malformedFilters.forEach(match => {
349
+ this.warnings.push({
350
+ type: 'warning',
351
+ message: `Malformed Liquid filter: ${match.trim()}`,
352
+ line: this.getLineNumber(html, html.indexOf(match)),
353
+ column: 1,
354
+ rule: 'liquid-malformed-filter',
355
+ severity: 'warning',
356
+ source: 'liquid-validator'
357
+ });
358
+ });
359
+ }
360
+
361
+ // Check for common filter usage
362
+ const commonFilters = ['date', 'capitalize', 'upcase', 'downcase', 'strip', 'truncate', 'default'];
363
+ const filterPattern = /\|\s*([a-zA-Z_][a-zA-Z0-9_]*)/g;
364
+ let filterMatch;
365
+
366
+ while ((filterMatch = filterPattern.exec(html)) !== null) {
367
+ const filterName = filterMatch[1];
368
+ if (!commonFilters.includes(filterName)) {
369
+ this.info.push({
370
+ type: 'info',
371
+ message: `Using filter: ${filterName}`,
372
+ line: this.getLineNumber(html, filterMatch.index),
373
+ column: 1,
374
+ rule: 'liquid-filter-usage',
375
+ severity: 'info',
376
+ source: 'liquid-validator'
377
+ });
378
+ }
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Validates Liquid variables
384
+ */
385
+ validateLiquidVariables(html) {
386
+ // Check for undefined variable patterns (basic check)
387
+ const suspiciousVariables = html.match(/\{\{\s*[^}]*undefined[^}]*\s*\}\}/g);
388
+ if (suspiciousVariables) {
389
+ suspiciousVariables.forEach(match => {
390
+ this.warnings.push({
391
+ type: 'warning',
392
+ message: `Potentially undefined variable: ${match}`,
393
+ line: this.getLineNumber(html, html.indexOf(match)),
394
+ column: 1,
395
+ rule: 'liquid-undefined-variable',
396
+ severity: 'warning',
397
+ source: 'liquid-validator'
398
+ });
399
+ });
400
+ }
401
+ }
402
+
403
+
404
+ /**
405
+ * Gets line number for a character position
406
+ */
407
+ getLineNumber(text, position) {
408
+ if (position === undefined || position < 0) return 1;
409
+ return text.substring(0, position).split('\n').length;
410
+ }
411
+
412
+ /**
413
+ * Returns validation results
414
+ */
415
+ getResults() {
416
+ return {
417
+ isValid: this.errors.length === 0,
418
+ errors: this.errors,
419
+ warnings: this.warnings,
420
+ info: this.info
421
+ };
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Enhanced CodeMirror extensions with Liquid support
427
+ */
428
+ export const createLiquidExtensions = () => {
429
+ return [
430
+ // HTML language support (base)
431
+ html(),
432
+
433
+ // Enhanced syntax highlighting with Liquid support
434
+ syntaxHighlighting(liquidVSCodeTheme),
435
+
436
+ // Editor theme
437
+ EditorView.theme({
438
+ "&": {
439
+ height: "500px",
440
+ backgroundColor: "#1e1e1e",
441
+ fontSize: "14px",
442
+ fontFamily: "'DM Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace"
443
+ },
444
+ ".cm-content": {
445
+ padding: "12px",
446
+ backgroundColor: "#1e1e1e",
447
+ fontSize: "14px",
448
+ lineHeight: "20px",
449
+ fontFamily: "'DM Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
450
+ color: "#d4d4d4"
451
+ },
452
+ ".cm-focused": {
453
+ outline: "none"
454
+ },
455
+ ".cm-lineNumbers": {
456
+ fontSize: "14px",
457
+ lineHeight: "20px",
458
+ color: "#858585",
459
+ backgroundColor: "#1e1e1e",
460
+ paddingRight: "8px",
461
+ paddingLeft: "8px",
462
+ borderRight: "1px solid #3e3e3e",
463
+ minWidth: "45px",
464
+ textAlign: "right"
465
+ },
466
+ ".cm-gutters": {
467
+ backgroundColor: "#1e1e1e",
468
+ borderRight: "1px solid #3e3e3e"
469
+ },
470
+ ".cm-activeLine": {
471
+ backgroundColor: "#2a2d2e"
472
+ },
473
+ ".cm-selection": {
474
+ backgroundColor: "#264f78"
475
+ },
476
+ ".cm-cursor": {
477
+ borderLeft: "2px solid #ffffff"
478
+ }
479
+ })
480
+ ];
481
+ };
482
+
483
+ /**
484
+ * Validates HTML content with Liquid template support
485
+ * @param {string} html - HTML content with Liquid templates
486
+ * @param {string} variant - Editor variant ('email' or 'inapp')
487
+ * @returns {Object} Validation results
488
+ */
489
+ export const validateLiquidHTML = (html, variant = 'email') => {
490
+ const liquidValidator = new LiquidValidator();
491
+ return liquidValidator.validate(html);
492
+ };
493
+
494
+ /**
495
+ * Common Liquid template examples for autocomplete/snippets
496
+ */
497
+ export const LIQUID_SNIPPETS = {
498
+ // Output tags
499
+ 'customer_name': '{{ customer.first_name }} {{ customer.last_name }}',
500
+ 'customer_email': '{{ customer.email }}',
501
+ 'current_date': '{{ "now" | date: "%B %d, %Y" }}',
502
+ 'organization_name': '{{ organization.name }}',
503
+
504
+ // Logic tags
505
+ 'if_statement': '{% if condition %}\n <!-- content -->\n{% endif %}',
506
+ 'for_loop': '{% for item in collection %}\n {{ item.name }}\n{% endfor %}',
507
+ 'unless_statement': '{% unless condition %}\n <!-- content -->\n{% endunless %}',
508
+ 'case_statement': '{% case variable %}\n {% when "value1" %}\n <!-- content -->\n {% when "value2" %}\n <!-- content -->\n {% else %}\n <!-- default content -->\n{% endcase %}',
509
+
510
+ // Common filters
511
+ 'date_filter': '{{ date_variable | date: "%B %d, %Y" }}',
512
+ 'capitalize_filter': '{{ text | capitalize }}',
513
+ 'truncate_filter': '{{ text | truncate: 50 }}',
514
+ 'default_filter': '{{ variable | default: "Default Value" }}'
515
+ };
516
+
517
+ export default {
518
+ LIQUID_PATTERNS,
519
+ liquidVSCodeTheme,
520
+ LiquidValidator,
521
+ createLiquidExtensions,
522
+ validateLiquidHTML,
523
+ LIQUID_SNIPPETS
524
+ };
@@ -0,0 +1,163 @@
1
+ /**
2
+ * ROBUST CodeMirror 6 Syntax Highlighting - Fixed Implementation
3
+ *
4
+ * This is the definitive fix for the white text issue and missing syntax highlighting.
5
+ * Only uses VERIFIED tags from @lezer/highlight to avoid undefined errors.
6
+ */
7
+
8
+ import { html } from '@codemirror/lang-html';
9
+ import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
10
+ import { tags } from '@lezer/highlight';
11
+ import { EditorView } from '@codemirror/view';
12
+
13
+ /**
14
+ * ENHANCED VS Code Dark Theme - Comprehensive token coverage
15
+ * Uses verified tags with extensive fallbacks for maximum compatibility
16
+ */
17
+ export const comprehensiveVSCodeTheme = HighlightStyle.define([
18
+ // === BASE CONTENT (CRITICAL - must be first) ===
19
+ { tag: tags.content, color: '#d4d4d4' },
20
+ { tag: tags.name, color: '#d4d4d4' },
21
+
22
+ // === COMMENTS ===
23
+ { tag: tags.comment, color: '#6a9955', fontStyle: 'italic' },
24
+ { tag: tags.lineComment, color: '#6a9955', fontStyle: 'italic' },
25
+ { tag: tags.blockComment, color: '#6a9955', fontStyle: 'italic' },
26
+
27
+ // === STRINGS AND LITERALS ===
28
+ { tag: tags.string, color: '#ce9178' },
29
+ { tag: tags.character, color: '#ce9178' },
30
+ { tag: tags.number, color: '#b5cea8' },
31
+ { tag: tags.bool, color: '#569cd6' },
32
+ { tag: tags.null, color: '#569cd6' },
33
+ { tag: tags.regexp, color: '#d16969' },
34
+ { tag: tags.escape, color: '#d7ba7d' },
35
+
36
+ // === KEYWORDS ===
37
+ { tag: tags.keyword, color: '#569cd6', fontWeight: 'bold' },
38
+ { tag: tags.controlKeyword, color: '#c586c0', fontWeight: 'bold' },
39
+ { tag: tags.operatorKeyword, color: '#569cd6' },
40
+ { tag: tags.moduleKeyword, color: '#c586c0' },
41
+ { tag: tags.definitionKeyword, color: '#569cd6' },
42
+
43
+ // === HTML ELEMENTS ===
44
+ { tag: tags.tagName, color: '#569cd6', fontWeight: 'bold' },
45
+ { tag: tags.attributeName, color: '#92c5f8' },
46
+ { tag: tags.attributeValue, color: '#ce9178' },
47
+ { tag: tags.angleBracket, color: '#808080' },
48
+ { tag: tags.quote, color: '#ce9178' },
49
+
50
+ // === CSS SPECIFIC ===
51
+ { tag: tags.propertyName, color: '#9cdcfe' },
52
+ { tag: tags.className, color: '#d7ba7d' },
53
+ { tag: tags.unit, color: '#b5cea8' },
54
+ { tag: tags.color, color: '#ce9178' },
55
+ { tag: tags.atom, color: '#569cd6' },
56
+
57
+ // === PROGRAMMING CONSTRUCTS ===
58
+ { tag: tags.variableName, color: '#9cdcfe' },
59
+ { tag: tags.function, color: '#dcdcaa' },
60
+ { tag: tags.definition, color: '#dcdcaa' },
61
+ { tag: tags.typeName, color: '#4ec9b0' },
62
+ { tag: tags.namespace, color: '#4ec9b0' },
63
+
64
+ // === OPERATORS AND PUNCTUATION ===
65
+ { tag: tags.operator, color: '#d4d4d4' },
66
+ { tag: tags.punctuation, color: '#d4d4d4' },
67
+ { tag: tags.paren, color: '#ffd700' },
68
+ { tag: tags.bracket, color: '#ffd700' },
69
+ { tag: tags.brace, color: '#ffd700' },
70
+ { tag: tags.squareBracket, color: '#ffd700' },
71
+
72
+ // === SPECIAL TOKENS ===
73
+ { tag: tags.meta, color: '#569cd6' },
74
+ { tag: tags.processingInstruction, color: '#569cd6' },
75
+ { tag: tags.invalid, color: '#f44747', textDecoration: 'underline' },
76
+ { tag: tags.link, color: '#4fc1ff', textDecoration: 'underline' },
77
+ { tag: tags.heading, color: '#9cdcfe', fontWeight: 'bold' },
78
+ { tag: tags.emphasis, fontStyle: 'italic' },
79
+ { tag: tags.strong, fontWeight: 'bold' }
80
+ ]);
81
+
82
+ /**
83
+ * CLEAN Editor Theme - NO color conflicts
84
+ * Only defines layout and structure, lets syntax highlighting handle colors
85
+ */
86
+ export const cleanEditorTheme = EditorView.theme({
87
+ "&": {
88
+ height: "500px",
89
+ backgroundColor: "#1e1e1e",
90
+ fontSize: "14px",
91
+ fontFamily: "'DM Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace"
92
+ },
93
+ ".cm-content": {
94
+ padding: "12px",
95
+ backgroundColor: "#1e1e1e",
96
+ fontSize: "14px",
97
+ lineHeight: "20px",
98
+ fontFamily: "'DM Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
99
+ color: "#d4d4d4" // Base text color for all content
100
+ },
101
+ ".cm-focused": {
102
+ outline: "none"
103
+ },
104
+ ".cm-editor": {
105
+ borderRadius: "0",
106
+ border: "none"
107
+ },
108
+ ".cm-scroller": {
109
+ fontFamily: "'DM Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace"
110
+ },
111
+ ".cm-lineNumbers": {
112
+ fontSize: "14px",
113
+ lineHeight: "20px",
114
+ color: "#858585",
115
+ backgroundColor: "#1e1e1e",
116
+ paddingRight: "8px",
117
+ paddingLeft: "8px",
118
+ borderRight: "1px solid #3e3e3e",
119
+ minWidth: "45px",
120
+ textAlign: "right"
121
+ },
122
+ ".cm-gutters": {
123
+ backgroundColor: "#1e1e1e",
124
+ borderRight: "1px solid #3e3e3e"
125
+ },
126
+ ".cm-activeLine": {
127
+ backgroundColor: "#2a2d2e"
128
+ },
129
+ ".cm-selection": {
130
+ backgroundColor: "#264f78"
131
+ },
132
+ ".cm-cursor": {
133
+ borderLeft: "2px solid #ffffff"
134
+ },
135
+ // Additional fallback for text nodes
136
+ ".cm-line": {
137
+ color: "#d4d4d4"
138
+ }
139
+ });
140
+
141
+ /**
142
+ * ROBUST Extension Creator - Single, clean implementation
143
+ * Uses only verified tags to avoid undefined errors
144
+ */
145
+ export const createRobustExtensions = () => {
146
+ return [
147
+ // 1. HTML language support with proper parsing
148
+ html(),
149
+
150
+ // 2. SAFE syntax highlighting (using only confirmed tags)
151
+ syntaxHighlighting(comprehensiveVSCodeTheme),
152
+
153
+ // 3. Clean theme (structure only, no color conflicts)
154
+ cleanEditorTheme
155
+ ];
156
+ };
157
+
158
+ // Export the main function
159
+ export default {
160
+ comprehensiveVSCodeTheme,
161
+ cleanEditorTheme,
162
+ createRobustExtensions
163
+ };