@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.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/config/app.js +1 -2
- package/package.json +16 -2
- package/services/api.js +0 -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/CreativesContainer/SlideBoxContent.js +0 -2
- 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
|
@@ -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
|
+
};
|