astro-tractstack 2.0.11 → 2.0.13
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/dist/index.js +8 -14
- package/package.json +1 -1
- package/templates/css/storykeep.css +1 -92872
- package/templates/src/components/compositor/Compositor.tsx +14 -6
- package/templates/src/components/compositor/Node.tsx +42 -2
- package/templates/src/components/compositor/nodes/Pane.tsx +0 -5
- package/templates/src/components/compositor/nodes/tagElements/NodeBasicTag.tsx +2 -1
- package/templates/src/components/edit/pane/AddPanePanel.tsx +5 -1
- package/templates/src/components/edit/pane/AddPanePanel_new.tsx +140 -116
- package/templates/src/components/edit/pane/AiPaneGenerator.tsx +575 -0
- package/templates/src/components/edit/pane/AiPanePreview.tsx +107 -0
- package/templates/src/components/edit/pane/PageGenSelector.tsx +10 -3
- package/templates/src/constants/prompts.json +35 -40
- package/templates/src/stores/nodes.ts +18 -15
- package/templates/src/types/compositorTypes.ts +27 -13
- package/templates/src/utils/compositor/aiPaneParser.ts +613 -0
- package/templates/src/utils/compositor/allowInsert.ts +1 -3
- package/templates/src/utils/compositor/nodesHelper.ts +61 -42
- package/templates/src/utils/compositor/tailwindClasses.ts +200 -70
- package/utils/inject-files.ts +8 -14
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy.tsx +0 -107
- package/templates/src/components/edit/pane/AddPanePanel_newAICopy_modal.tsx +0 -217
- package/templates/src/components/edit/pane/AddPanePanel_newCopyMode.tsx +0 -109
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { ulid } from 'ulid';
|
|
2
|
+
import type {
|
|
3
|
+
TemplatePane,
|
|
4
|
+
TemplateNode,
|
|
5
|
+
TemplateMarkdown,
|
|
6
|
+
ParentClassesPayload,
|
|
7
|
+
DefaultClasses,
|
|
8
|
+
ResponsiveClasses,
|
|
9
|
+
ButtonPayload,
|
|
10
|
+
} from '@/types/compositorTypes';
|
|
11
|
+
import { tailwindClasses } from '@/utils/compositor/tailwindClasses';
|
|
12
|
+
import { isDeepEqual } from '@/utils/helpers';
|
|
13
|
+
|
|
14
|
+
type LLMShellLayer = {
|
|
15
|
+
mobile?: Record<string, string>;
|
|
16
|
+
tablet?: Record<string, string>;
|
|
17
|
+
desktop?: Record<string, string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type LLMDefaultClasses = {
|
|
21
|
+
[tagName: string]: {
|
|
22
|
+
mobile?: string;
|
|
23
|
+
tablet?: string;
|
|
24
|
+
desktop?: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type ShellJson = {
|
|
29
|
+
bgColour: string;
|
|
30
|
+
parentClasses: LLMShellLayer[];
|
|
31
|
+
defaultClasses: LLMDefaultClasses;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type ParsedNode = {
|
|
35
|
+
flatNode: TemplateNode;
|
|
36
|
+
responsiveClasses: ResponsiveClasses;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ParentClassLayer = {
|
|
40
|
+
mobile: Record<string, string>;
|
|
41
|
+
tablet: Record<string, string>;
|
|
42
|
+
desktop: Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type DefaultClassValue = {
|
|
46
|
+
mobile: Record<string, string>;
|
|
47
|
+
tablet: Record<string, string>;
|
|
48
|
+
desktop: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type ClassLookupValue = {
|
|
52
|
+
key: string;
|
|
53
|
+
value: string;
|
|
54
|
+
viewport: 'mobile' | 'tablet' | 'desktop';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let KEY_NORMALIZATION_LOOKUP: Map<string, string> | null = null;
|
|
58
|
+
let RESPONSIVE_CLASS_LOOKUP: Map<string, ClassLookupValue> | null = null;
|
|
59
|
+
let BUTTON_CLASS_LOOKUP: Map<string, { key: string; value: string }> | null =
|
|
60
|
+
null;
|
|
61
|
+
|
|
62
|
+
const ALLOWED_TAGS = new Set([
|
|
63
|
+
'h2',
|
|
64
|
+
'h3',
|
|
65
|
+
'h4',
|
|
66
|
+
'h5',
|
|
67
|
+
'p',
|
|
68
|
+
'span',
|
|
69
|
+
'em',
|
|
70
|
+
'strong',
|
|
71
|
+
'button',
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
function buildKeyNormalizationLookup(): Map<string, string> {
|
|
75
|
+
if (KEY_NORMALIZATION_LOOKUP) {
|
|
76
|
+
return KEY_NORMALIZATION_LOOKUP;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const keyMap = new Map<string, string>();
|
|
80
|
+
for (const key in tailwindClasses) {
|
|
81
|
+
// Store lowercase key -> correctly cased key
|
|
82
|
+
keyMap.set(key.toLowerCase(), key);
|
|
83
|
+
}
|
|
84
|
+
KEY_NORMALIZATION_LOOKUP = keyMap;
|
|
85
|
+
return keyMap;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeKeys(
|
|
89
|
+
styleObj: Record<string, string> | undefined
|
|
90
|
+
): Record<string, string> {
|
|
91
|
+
if (!styleObj) return {};
|
|
92
|
+
|
|
93
|
+
const keyMap = buildKeyNormalizationLookup();
|
|
94
|
+
const normalized: Record<string, string> = {};
|
|
95
|
+
|
|
96
|
+
for (const key in styleObj) {
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(styleObj, key)) {
|
|
98
|
+
const lowerKey = key.toLowerCase();
|
|
99
|
+
const correctKey = keyMap.get(lowerKey);
|
|
100
|
+
// Use the correctly cased key if found, otherwise keep original (handles potential non-Tailwind keys)
|
|
101
|
+
normalized[correctKey || key] = styleObj[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildResponsiveClassLookup(): Map<string, ClassLookupValue> {
|
|
108
|
+
if (RESPONSIVE_CLASS_LOOKUP) {
|
|
109
|
+
return RESPONSIVE_CLASS_LOOKUP;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const classMap = new Map<string, ClassLookupValue>();
|
|
113
|
+
const viewports: Array<{
|
|
114
|
+
prefix: string;
|
|
115
|
+
key: 'mobile' | 'tablet' | 'desktop';
|
|
116
|
+
}> = [
|
|
117
|
+
{ prefix: '', key: 'mobile' },
|
|
118
|
+
{ prefix: 'md:', key: 'tablet' },
|
|
119
|
+
{ prefix: 'xl:', key: 'desktop' },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
for (const tailwindKey in tailwindClasses) {
|
|
123
|
+
const def = tailwindClasses[tailwindKey];
|
|
124
|
+
const classKey = tailwindKey;
|
|
125
|
+
|
|
126
|
+
def.values.forEach((value) => {
|
|
127
|
+
const className = def.useKeyAsClass ? value : `${def.prefix}${value}`;
|
|
128
|
+
viewports.forEach((vp) => {
|
|
129
|
+
const fullClassName = `${vp.prefix}${className}`;
|
|
130
|
+
classMap.set(fullClassName, {
|
|
131
|
+
key: classKey,
|
|
132
|
+
value: value,
|
|
133
|
+
viewport: vp.key,
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (def.allowNegative) {
|
|
139
|
+
def.values.forEach((value) => {
|
|
140
|
+
if (value === '0') return;
|
|
141
|
+
const className = def.useKeyAsClass ? value : `${def.prefix}${value}`;
|
|
142
|
+
viewports.forEach((vp) => {
|
|
143
|
+
const fullClassName = `${vp.prefix}-${className}`;
|
|
144
|
+
classMap.set(fullClassName, {
|
|
145
|
+
key: classKey,
|
|
146
|
+
value: `-${value}`,
|
|
147
|
+
viewport: vp.key,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
RESPONSIVE_CLASS_LOOKUP = classMap;
|
|
154
|
+
return classMap;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildButtonClassLookup(): Map<string, { key: string; value: string }> {
|
|
158
|
+
if (BUTTON_CLASS_LOOKUP) {
|
|
159
|
+
return BUTTON_CLASS_LOOKUP;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const classMap = new Map<string, { key: string; value: string }>();
|
|
163
|
+
|
|
164
|
+
for (const tailwindKey in tailwindClasses) {
|
|
165
|
+
const def = tailwindClasses[tailwindKey];
|
|
166
|
+
const classKey = tailwindKey;
|
|
167
|
+
|
|
168
|
+
def.values.forEach((value) => {
|
|
169
|
+
const className = def.useKeyAsClass ? value : `${def.prefix}${value}`;
|
|
170
|
+
classMap.set(className, {
|
|
171
|
+
key: classKey,
|
|
172
|
+
value: value,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (def.allowNegative) {
|
|
177
|
+
def.values.forEach((value) => {
|
|
178
|
+
if (value === '0') return;
|
|
179
|
+
const className = def.useKeyAsClass ? value : `${def.prefix}${value}`;
|
|
180
|
+
classMap.set(`-${className}`, {
|
|
181
|
+
key: classKey,
|
|
182
|
+
value: `-${value}`,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
BUTTON_CLASS_LOOKUP = classMap;
|
|
188
|
+
return classMap;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sanitizeResponsiveClasses(
|
|
192
|
+
classString: string | null | undefined
|
|
193
|
+
): ResponsiveClasses {
|
|
194
|
+
const responsive: ResponsiveClasses = {};
|
|
195
|
+
|
|
196
|
+
if (!classString) {
|
|
197
|
+
return responsive;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const classMap = buildResponsiveClassLookup();
|
|
201
|
+
const classes = classString.split(/\s+/).filter(Boolean);
|
|
202
|
+
|
|
203
|
+
classes.forEach((className) => {
|
|
204
|
+
const lookup = classMap.get(className);
|
|
205
|
+
if (lookup) {
|
|
206
|
+
if (!responsive[lookup.viewport]) {
|
|
207
|
+
responsive[lookup.viewport] = {};
|
|
208
|
+
}
|
|
209
|
+
responsive[lookup.viewport]![lookup.key] = lookup.value;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return responsive;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sanitizeButtonClasses(
|
|
217
|
+
classString: string | null | undefined
|
|
218
|
+
): ButtonPayload {
|
|
219
|
+
const buttonPayload: ButtonPayload = {
|
|
220
|
+
buttonClasses: {},
|
|
221
|
+
buttonHoverClasses: {},
|
|
222
|
+
callbackPayload: '',
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (!classString) {
|
|
226
|
+
return buttonPayload;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const classMap = buildButtonClassLookup();
|
|
230
|
+
const classes = classString.split(/\s+/).filter(Boolean);
|
|
231
|
+
|
|
232
|
+
classes.forEach((className) => {
|
|
233
|
+
let targetClasses = buttonPayload.buttonClasses;
|
|
234
|
+
let cleanClassName = className;
|
|
235
|
+
|
|
236
|
+
if (className.startsWith('hover:')) {
|
|
237
|
+
targetClasses = buttonPayload.buttonHoverClasses;
|
|
238
|
+
cleanClassName = className.substring(6);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const lookup = classMap.get(cleanClassName);
|
|
242
|
+
if (lookup) {
|
|
243
|
+
if (!targetClasses[lookup.key]) {
|
|
244
|
+
targetClasses[lookup.key] = [];
|
|
245
|
+
}
|
|
246
|
+
targetClasses[lookup.key]!.push(lookup.value);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return buttonPayload;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function walkDom(
|
|
254
|
+
domNode: Node,
|
|
255
|
+
parentId: string,
|
|
256
|
+
parsedNodes: ParsedNode[],
|
|
257
|
+
markdownId: string
|
|
258
|
+
) {
|
|
259
|
+
if (domNode.nodeType === Node.TEXT_NODE) {
|
|
260
|
+
const copy = domNode.textContent || '';
|
|
261
|
+
// Preserve leading/trailing spaces unless the *entire* content is just whitespace.
|
|
262
|
+
// Trim internal excessive whitespace as a basic sanitation step.
|
|
263
|
+
const trimmedCopy = copy.replace(/\s+/g, ' ').trim();
|
|
264
|
+
|
|
265
|
+
if (trimmedCopy.length > 0) {
|
|
266
|
+
// Use the original copy to preserve meaningful spaces, but cleaned up.
|
|
267
|
+
let finalCopy = copy.replace(/\s+/g, ' ');
|
|
268
|
+
// Preserve single leading space if original had one AND previous sibling exists
|
|
269
|
+
if (copy.startsWith(' ') && domNode.previousSibling) {
|
|
270
|
+
finalCopy = ' ' + finalCopy.trimStart();
|
|
271
|
+
}
|
|
272
|
+
// Preserve single trailing space if original had one AND next sibling exists
|
|
273
|
+
if (copy.endsWith(' ') && domNode.nextSibling) {
|
|
274
|
+
finalCopy = finalCopy.trimEnd() + ' ';
|
|
275
|
+
}
|
|
276
|
+
// Special case: if it was ONLY space, respect if it was intended between elements
|
|
277
|
+
if (
|
|
278
|
+
trimmedCopy.length === 0 &&
|
|
279
|
+
copy.length > 0 &&
|
|
280
|
+
domNode.previousSibling &&
|
|
281
|
+
domNode.nextSibling
|
|
282
|
+
) {
|
|
283
|
+
finalCopy = ' ';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Only create node if there's actual content or a meaningful space
|
|
287
|
+
if (finalCopy.trim().length > 0 || finalCopy === ' ') {
|
|
288
|
+
const textNode: TemplateNode = {
|
|
289
|
+
id: ulid(),
|
|
290
|
+
nodeType: 'TagElement',
|
|
291
|
+
parentId: parentId,
|
|
292
|
+
tagName: 'text',
|
|
293
|
+
copy: finalCopy, // Use the carefully preserved copy
|
|
294
|
+
overrideClasses: {},
|
|
295
|
+
};
|
|
296
|
+
parsedNodes.push({
|
|
297
|
+
flatNode: textNode,
|
|
298
|
+
responsiveClasses: {},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (domNode.nodeType !== Node.ELEMENT_NODE) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const el = domNode as Element;
|
|
310
|
+
const tagName = el.tagName.toLowerCase();
|
|
311
|
+
|
|
312
|
+
if (!ALLOWED_TAGS.has(tagName)) {
|
|
313
|
+
el.childNodes.forEach((child) =>
|
|
314
|
+
walkDom(child, parentId, parsedNodes, markdownId)
|
|
315
|
+
);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (tagName === 'button') {
|
|
320
|
+
let finalParentId = parentId;
|
|
321
|
+
|
|
322
|
+
if (parentId === markdownId) {
|
|
323
|
+
const pNodeId = ulid();
|
|
324
|
+
const pNode: TemplateNode = {
|
|
325
|
+
id: pNodeId,
|
|
326
|
+
nodeType: 'TagElement',
|
|
327
|
+
parentId: parentId,
|
|
328
|
+
tagName: 'p',
|
|
329
|
+
overrideClasses: {},
|
|
330
|
+
};
|
|
331
|
+
parsedNodes.push({
|
|
332
|
+
flatNode: pNode,
|
|
333
|
+
responsiveClasses: {},
|
|
334
|
+
});
|
|
335
|
+
finalParentId = pNodeId;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const buttonPayload = sanitizeButtonClasses(el.getAttribute('class'));
|
|
339
|
+
|
|
340
|
+
const flatNode: TemplateNode = {
|
|
341
|
+
id: ulid(),
|
|
342
|
+
nodeType: 'TagElement',
|
|
343
|
+
parentId: finalParentId,
|
|
344
|
+
tagName: 'a',
|
|
345
|
+
overrideClasses: {},
|
|
346
|
+
href: '#',
|
|
347
|
+
buttonPayload: {
|
|
348
|
+
...buttonPayload,
|
|
349
|
+
callbackPayload: '',
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
parsedNodes.push({
|
|
354
|
+
flatNode,
|
|
355
|
+
responsiveClasses: {},
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
el.childNodes.forEach((child) =>
|
|
359
|
+
walkDom(child, flatNode.id, parsedNodes, markdownId)
|
|
360
|
+
);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const responsive = sanitizeResponsiveClasses(el.getAttribute('class'));
|
|
365
|
+
|
|
366
|
+
const flatNode: TemplateNode = {
|
|
367
|
+
id: ulid(),
|
|
368
|
+
nodeType: 'TagElement',
|
|
369
|
+
parentId: parentId,
|
|
370
|
+
tagName: tagName,
|
|
371
|
+
overrideClasses: {},
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (tagName === 'span') {
|
|
375
|
+
flatNode.overrideClasses = responsive;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
parsedNodes.push({
|
|
379
|
+
flatNode,
|
|
380
|
+
responsiveClasses: responsive,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
el.childNodes.forEach((child) =>
|
|
384
|
+
walkDom(child, flatNode.id, parsedNodes, markdownId)
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function findMostCommonClasses(nodes: ParsedNode[]): ResponsiveClasses {
|
|
389
|
+
if (nodes.length === 0) return {};
|
|
390
|
+
const classCounts = new Map<string, number>();
|
|
391
|
+
const classMap = new Map<string, ResponsiveClasses>();
|
|
392
|
+
|
|
393
|
+
nodes.forEach((node) => {
|
|
394
|
+
const key = JSON.stringify(
|
|
395
|
+
node.responsiveClasses,
|
|
396
|
+
Object.keys(node.responsiveClasses).sort()
|
|
397
|
+
);
|
|
398
|
+
classCounts.set(key, (classCounts.get(key) || 0) + 1);
|
|
399
|
+
if (!classMap.has(key)) {
|
|
400
|
+
classMap.set(key, node.responsiveClasses);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
let mostCommonKey = '';
|
|
405
|
+
let maxCount = 0;
|
|
406
|
+
classCounts.forEach((count, key) => {
|
|
407
|
+
if (count > maxCount) {
|
|
408
|
+
maxCount = count;
|
|
409
|
+
mostCommonKey = key;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
return classMap.get(mostCommonKey) || {};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function ensureRequiredViewports(
|
|
417
|
+
responsive: ResponsiveClasses | undefined
|
|
418
|
+
): DefaultClassValue {
|
|
419
|
+
const base = responsive || {};
|
|
420
|
+
return {
|
|
421
|
+
mobile: base.mobile || {},
|
|
422
|
+
tablet: base.tablet || {},
|
|
423
|
+
desktop: base.desktop || {},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function mergeResponsive(
|
|
428
|
+
base: ResponsiveClasses | DefaultClassValue | undefined,
|
|
429
|
+
override: ResponsiveClasses | DefaultClassValue | undefined
|
|
430
|
+
): ResponsiveClasses {
|
|
431
|
+
const merged = base ? JSON.parse(JSON.stringify(base)) : {};
|
|
432
|
+
if (!override) return merged;
|
|
433
|
+
|
|
434
|
+
(['mobile', 'tablet', 'desktop'] as Array<keyof ResponsiveClasses>).forEach(
|
|
435
|
+
(vp) => {
|
|
436
|
+
if (override[vp]) {
|
|
437
|
+
if (!merged[vp]) {
|
|
438
|
+
merged[vp] = {};
|
|
439
|
+
}
|
|
440
|
+
merged[vp] = { ...merged[vp], ...override[vp] };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const result: ResponsiveClasses = {};
|
|
446
|
+
if (merged.mobile && Object.keys(merged.mobile).length > 0)
|
|
447
|
+
result.mobile = merged.mobile;
|
|
448
|
+
if (merged.tablet && Object.keys(merged.tablet).length > 0)
|
|
449
|
+
result.tablet = merged.tablet;
|
|
450
|
+
if (merged.desktop && Object.keys(merged.desktop).length > 0)
|
|
451
|
+
result.desktop = merged.desktop;
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function parseDefaultClassesFromShell(
|
|
456
|
+
llmDefaults: LLMDefaultClasses | undefined
|
|
457
|
+
): DefaultClasses {
|
|
458
|
+
const sanitizedDefaults: DefaultClasses = {};
|
|
459
|
+
if (!llmDefaults) return sanitizedDefaults;
|
|
460
|
+
|
|
461
|
+
for (const tagName in llmDefaults) {
|
|
462
|
+
if (Object.prototype.hasOwnProperty.call(llmDefaults, tagName)) {
|
|
463
|
+
const tagClasses = llmDefaults[tagName];
|
|
464
|
+
let responsiveForTag: ResponsiveClasses = {};
|
|
465
|
+
|
|
466
|
+
if (tagClasses.mobile) {
|
|
467
|
+
responsiveForTag = mergeResponsive(
|
|
468
|
+
responsiveForTag,
|
|
469
|
+
sanitizeResponsiveClasses(tagClasses.mobile)
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
if (tagClasses.tablet) {
|
|
473
|
+
const tabletClasses = sanitizeResponsiveClasses(tagClasses.tablet);
|
|
474
|
+
if (tabletClasses.mobile) {
|
|
475
|
+
responsiveForTag.tablet = {
|
|
476
|
+
...responsiveForTag.tablet,
|
|
477
|
+
...tabletClasses.mobile,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (tagClasses.desktop) {
|
|
482
|
+
const desktopClasses = sanitizeResponsiveClasses(tagClasses.desktop);
|
|
483
|
+
if (desktopClasses.mobile) {
|
|
484
|
+
responsiveForTag.desktop = {
|
|
485
|
+
...responsiveForTag.desktop,
|
|
486
|
+
...desktopClasses.mobile,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
sanitizedDefaults[tagName] = ensureRequiredViewports(responsiveForTag);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return sanitizedDefaults;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export const parseAiPane = (
|
|
497
|
+
shellJson: string,
|
|
498
|
+
copyHtml: string,
|
|
499
|
+
layout: string
|
|
500
|
+
): TemplatePane => {
|
|
501
|
+
const shell: ShellJson = JSON.parse(shellJson);
|
|
502
|
+
const parser = new DOMParser();
|
|
503
|
+
const doc = parser.parseFromString(copyHtml, 'text/html');
|
|
504
|
+
|
|
505
|
+
const paneId = ulid();
|
|
506
|
+
const markdownId = ulid();
|
|
507
|
+
|
|
508
|
+
const transformedParentClasses: ParentClassesPayload = (
|
|
509
|
+
shell.parentClasses || []
|
|
510
|
+
).map(
|
|
511
|
+
(layer): ParentClassLayer => ({
|
|
512
|
+
mobile: normalizeKeys(layer.mobile),
|
|
513
|
+
tablet: normalizeKeys(layer.tablet),
|
|
514
|
+
desktop: normalizeKeys(layer.desktop),
|
|
515
|
+
})
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const shellDefaults = parseDefaultClassesFromShell(shell.defaultClasses);
|
|
519
|
+
|
|
520
|
+
const markdownNode: TemplateMarkdown = {
|
|
521
|
+
id: markdownId,
|
|
522
|
+
nodeType: 'Markdown',
|
|
523
|
+
parentId: paneId,
|
|
524
|
+
type: 'markdown',
|
|
525
|
+
markdownId: ulid(),
|
|
526
|
+
parentClasses: transformedParentClasses,
|
|
527
|
+
defaultClasses: shellDefaults,
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const allParsedNodes: ParsedNode[] = [];
|
|
531
|
+
walkDom(doc.body, markdownId, allParsedNodes, markdownId);
|
|
532
|
+
|
|
533
|
+
const templateNodes: TemplateNode[] = [];
|
|
534
|
+
const nodesByTag = new Map<string, ParsedNode[]>();
|
|
535
|
+
|
|
536
|
+
allParsedNodes.forEach((parsedNode) => {
|
|
537
|
+
templateNodes.push(parsedNode.flatNode);
|
|
538
|
+
const tagName = parsedNode.flatNode.tagName;
|
|
539
|
+
|
|
540
|
+
if (
|
|
541
|
+
tagName &&
|
|
542
|
+
tagName !== 'span' &&
|
|
543
|
+
tagName !== 'text' &&
|
|
544
|
+
tagName !== 'em' &&
|
|
545
|
+
tagName !== 'strong' &&
|
|
546
|
+
tagName !== 'a'
|
|
547
|
+
) {
|
|
548
|
+
if (!nodesByTag.has(tagName)) {
|
|
549
|
+
nodesByTag.set(tagName, []);
|
|
550
|
+
}
|
|
551
|
+
nodesByTag.get(tagName)!.push(parsedNode);
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
nodesByTag.forEach((nodes, tagName) => {
|
|
556
|
+
const commonResponsiveFromCopy = findMostCommonClasses(nodes);
|
|
557
|
+
const requiredCommonFromCopy = ensureRequiredViewports(
|
|
558
|
+
commonResponsiveFromCopy
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const existingShellDefault = markdownNode.defaultClasses![tagName];
|
|
562
|
+
const mergedDefault = ensureRequiredViewports(
|
|
563
|
+
mergeResponsive(existingShellDefault, commonResponsiveFromCopy)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
markdownNode.defaultClasses![tagName] = mergedDefault;
|
|
567
|
+
|
|
568
|
+
nodes.forEach((parsedNode) => {
|
|
569
|
+
const requiredNodeResponsive = ensureRequiredViewports(
|
|
570
|
+
parsedNode.responsiveClasses
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
if (!isDeepEqual(requiredNodeResponsive, requiredCommonFromCopy)) {
|
|
574
|
+
if (!parsedNode.flatNode.overrideClasses) {
|
|
575
|
+
parsedNode.flatNode.overrideClasses = {};
|
|
576
|
+
}
|
|
577
|
+
parsedNode.flatNode.overrideClasses = parsedNode.responsiveClasses;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
if (layout.includes('Image')) {
|
|
583
|
+
const imgNode: TemplateNode = {
|
|
584
|
+
id: ulid(),
|
|
585
|
+
nodeType: 'TagElement',
|
|
586
|
+
parentId: markdownId,
|
|
587
|
+
tagName: 'img',
|
|
588
|
+
src: '/static.jpg',
|
|
589
|
+
overrideClasses: {},
|
|
590
|
+
};
|
|
591
|
+
if (layout === 'Text + Image Left') {
|
|
592
|
+
templateNodes.unshift(imgNode);
|
|
593
|
+
} else {
|
|
594
|
+
templateNodes.push(imgNode);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const templatePane: TemplatePane = {
|
|
599
|
+
id: paneId,
|
|
600
|
+
nodeType: 'Pane',
|
|
601
|
+
parentId: '',
|
|
602
|
+
title: 'AI Pane',
|
|
603
|
+
slug: `ai-${paneId.slice(-4)}`,
|
|
604
|
+
bgColour: shell.bgColour,
|
|
605
|
+
isDecorative: false,
|
|
606
|
+
markdown: {
|
|
607
|
+
...markdownNode,
|
|
608
|
+
nodes: templateNodes,
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
return templatePane;
|
|
613
|
+
};
|