cf-pagetree-parser 1.0.0
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/README.md +79 -0
- package/dist/cf-pagetree-parser.js +5044 -0
- package/package.json +33 -0
- package/src/index.js +708 -0
- package/src/parsers/button.js +379 -0
- package/src/parsers/form.js +658 -0
- package/src/parsers/index.js +65 -0
- package/src/parsers/interactive.js +484 -0
- package/src/parsers/layout.js +788 -0
- package/src/parsers/list.js +359 -0
- package/src/parsers/media.js +376 -0
- package/src/parsers/placeholders.js +196 -0
- package/src/parsers/popup.js +133 -0
- package/src/parsers/text.js +278 -0
- package/src/styles.js +444 -0
- package/src/utils.js +402 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* PAGETREE PARSER - Utilities
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* ID generation, fractional indexing, and common helpers.
|
|
7
|
+
*
|
|
8
|
+
* ============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Use DOMPurify in browser, simple passthrough on server
|
|
12
|
+
// Server-side parsing is from trusted FunnelWind source
|
|
13
|
+
let DOMPurify = null;
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
import('dompurify').then(mod => { DOMPurify = mod.default; });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sanitize HTML to prevent XSS attacks
|
|
20
|
+
* Allows only safe formatting tags for text content
|
|
21
|
+
*/
|
|
22
|
+
export function sanitizeHtml(html) {
|
|
23
|
+
if (!html) return '';
|
|
24
|
+
// On server or before DOMPurify loads, return HTML as-is
|
|
25
|
+
// (server-side parsing is from trusted FunnelWind source)
|
|
26
|
+
if (!DOMPurify) return html;
|
|
27
|
+
return DOMPurify.sanitize(html, {
|
|
28
|
+
ALLOWED_TAGS: ['b', 'strong', 'i', 'em', 'u', 's', 'strike', 'a', 'br', 'span', 'li'],
|
|
29
|
+
ALLOWED_ATTR: ['href', 'target', 'rel', 'style', 'class'],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Generate a unique ClickFunnels-compatible ID
|
|
35
|
+
* Format: 6Z-xxxxx-0 (5 random alphanumeric chars)
|
|
36
|
+
*/
|
|
37
|
+
export function generateId() {
|
|
38
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
39
|
+
let id = '';
|
|
40
|
+
for (let i = 0; i < 5; i++) {
|
|
41
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
42
|
+
}
|
|
43
|
+
return `6Z-${id}-0`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generate fractional index for ordering elements
|
|
48
|
+
* Sequence: a0, a1, a2... a9, aA, aB... aZ, b0, b1...
|
|
49
|
+
*/
|
|
50
|
+
export function generateFractionalIndex(index) {
|
|
51
|
+
const BASE_62 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
52
|
+
|
|
53
|
+
if (index < 36) {
|
|
54
|
+
// a0 through aZ (0-35)
|
|
55
|
+
return 'a' + BASE_62[index];
|
|
56
|
+
} else if (index < 72) {
|
|
57
|
+
// b0 through bZ (36-71)
|
|
58
|
+
return 'b' + BASE_62[index - 36];
|
|
59
|
+
} else if (index < 108) {
|
|
60
|
+
// c0 through cZ (72-107)
|
|
61
|
+
return 'c' + BASE_62[index - 72];
|
|
62
|
+
} else {
|
|
63
|
+
// For very long lists, use multi-character
|
|
64
|
+
const prefix = String.fromCharCode(97 + Math.floor(index / 36)); // a, b, c...
|
|
65
|
+
const suffix = BASE_62[index % 36];
|
|
66
|
+
return prefix + suffix;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a CSS value with unit (e.g., "48px" -> { value: 48, unit: "px" })
|
|
72
|
+
*/
|
|
73
|
+
export function parseValueWithUnit(str, defaultUnit = 'px') {
|
|
74
|
+
if (str === null || str === undefined) return null;
|
|
75
|
+
|
|
76
|
+
const strVal = String(str).trim();
|
|
77
|
+
|
|
78
|
+
// Handle percentage
|
|
79
|
+
if (strVal.endsWith('%')) {
|
|
80
|
+
return {
|
|
81
|
+
value: parseFloat(strVal),
|
|
82
|
+
unit: '%'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle rem
|
|
87
|
+
if (strVal.endsWith('rem')) {
|
|
88
|
+
return {
|
|
89
|
+
value: parseFloat(strVal),
|
|
90
|
+
unit: 'rem'
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle em
|
|
95
|
+
if (strVal.endsWith('em')) {
|
|
96
|
+
return {
|
|
97
|
+
value: parseFloat(strVal),
|
|
98
|
+
unit: 'em'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle px (explicit or implicit)
|
|
103
|
+
if (strVal.endsWith('px')) {
|
|
104
|
+
return {
|
|
105
|
+
value: parseFloat(strVal),
|
|
106
|
+
unit: 'px'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Pure number
|
|
111
|
+
const num = parseFloat(strVal);
|
|
112
|
+
if (!isNaN(num)) {
|
|
113
|
+
return {
|
|
114
|
+
value: num,
|
|
115
|
+
unit: defaultUnit
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Convert color to rgb/rgba format for ClickFunnels
|
|
124
|
+
*/
|
|
125
|
+
export function normalizeColor(color) {
|
|
126
|
+
if (!color) return null;
|
|
127
|
+
|
|
128
|
+
// Already in rgb/rgba format
|
|
129
|
+
if (color.startsWith('rgb')) {
|
|
130
|
+
return color;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Hex color
|
|
134
|
+
if (color.startsWith('#')) {
|
|
135
|
+
return hexToRgb(color);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Named colors or other formats - return as-is
|
|
139
|
+
return color;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Convert hex color to rgb format
|
|
144
|
+
*/
|
|
145
|
+
export function hexToRgb(hex) {
|
|
146
|
+
// Remove # if present
|
|
147
|
+
hex = hex.replace('#', '');
|
|
148
|
+
|
|
149
|
+
// Handle shorthand (e.g., #fff)
|
|
150
|
+
if (hex.length === 3) {
|
|
151
|
+
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Handle 8-character hex with alpha
|
|
155
|
+
if (hex.length === 8) {
|
|
156
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
157
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
158
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
159
|
+
const a = parseInt(hex.substring(6, 8), 16) / 255;
|
|
160
|
+
return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Standard 6-character hex
|
|
164
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
165
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
166
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
167
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Parse inline style string to object
|
|
172
|
+
*/
|
|
173
|
+
export function parseInlineStyle(styleStr) {
|
|
174
|
+
if (!styleStr) return {};
|
|
175
|
+
|
|
176
|
+
const styles = {};
|
|
177
|
+
const parts = styleStr.split(';');
|
|
178
|
+
|
|
179
|
+
for (const part of parts) {
|
|
180
|
+
const colonIndex = part.indexOf(':');
|
|
181
|
+
if (colonIndex > -1) {
|
|
182
|
+
const prop = part.substring(0, colonIndex).trim();
|
|
183
|
+
const val = part.substring(colonIndex + 1).trim();
|
|
184
|
+
if (prop && val) {
|
|
185
|
+
styles[prop] = val;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return styles;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get data-type attribute from element
|
|
195
|
+
*/
|
|
196
|
+
export function getDataType(element) {
|
|
197
|
+
return element.getAttribute('data-type') || '';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Parse animation data attributes from element
|
|
202
|
+
* Returns { attrs, params } object with animation settings or empty objects if no animation
|
|
203
|
+
*/
|
|
204
|
+
export function parseAnimationAttrs(element) {
|
|
205
|
+
const skipAnimation = element.getAttribute('data-skip-animation-settings');
|
|
206
|
+
if (skipAnimation !== 'false') {
|
|
207
|
+
return { attrs: {}, params: {} };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const attrs = {
|
|
211
|
+
'data-skip-animation-settings': 'false',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const params = {};
|
|
215
|
+
|
|
216
|
+
const animationType = element.getAttribute('data-animation-type');
|
|
217
|
+
if (animationType) attrs['data-animation-type'] = animationType;
|
|
218
|
+
|
|
219
|
+
const animationTime = element.getAttribute('data-animation-time');
|
|
220
|
+
if (animationTime) {
|
|
221
|
+
attrs['data-animation-time'] = parseInt(animationTime, 10);
|
|
222
|
+
params['data-animation-time--unit'] = 'ms';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const animationDelay = element.getAttribute('data-animation-delay');
|
|
226
|
+
if (animationDelay) {
|
|
227
|
+
attrs['data-animation-delay'] = parseInt(animationDelay, 10);
|
|
228
|
+
params['data-animation-delay--unit'] = 'ms';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const animationTrigger = element.getAttribute('data-animation-trigger');
|
|
232
|
+
if (animationTrigger) attrs['data-animation-trigger'] = animationTrigger;
|
|
233
|
+
|
|
234
|
+
const animationTiming = element.getAttribute('data-animation-timing-function');
|
|
235
|
+
if (animationTiming) attrs['data-animation-timing-function'] = animationTiming;
|
|
236
|
+
|
|
237
|
+
const animationDirection = element.getAttribute('data-animation-direction');
|
|
238
|
+
if (animationDirection) attrs['data-animation-direction'] = animationDirection;
|
|
239
|
+
|
|
240
|
+
const animationOnce = element.getAttribute('data-animation-once');
|
|
241
|
+
if (animationOnce) attrs['data-animation-once'] = animationOnce === 'true';
|
|
242
|
+
|
|
243
|
+
const animationLoop = element.getAttribute('data-animation-loop');
|
|
244
|
+
if (animationLoop) attrs['data-animation-loop'] = animationLoop === 'true';
|
|
245
|
+
|
|
246
|
+
return { attrs, params };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Deep merge two objects
|
|
251
|
+
*/
|
|
252
|
+
export function deepMerge(target, source) {
|
|
253
|
+
const result = { ...target };
|
|
254
|
+
|
|
255
|
+
for (const key of Object.keys(source)) {
|
|
256
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
257
|
+
result[key] = deepMerge(result[key] || {}, source[key]);
|
|
258
|
+
} else {
|
|
259
|
+
result[key] = source[key];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Extract text content from element (handles nested spans, etc.)
|
|
268
|
+
*/
|
|
269
|
+
export function extractTextContent(element) {
|
|
270
|
+
return element.textContent || element.innerText || '';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Parse HTML content to ContentEditableNode children
|
|
275
|
+
* @param {string} html - The HTML content to parse
|
|
276
|
+
* @param {string|null} defaultLinkColor - Default color for links (from styleguide or data attribute)
|
|
277
|
+
*/
|
|
278
|
+
export function parseHtmlToTextNodes(html, defaultLinkColor = null) {
|
|
279
|
+
const temp = document.createElement('div');
|
|
280
|
+
temp.innerHTML = sanitizeHtml(html);
|
|
281
|
+
|
|
282
|
+
const children = [];
|
|
283
|
+
|
|
284
|
+
function processNode(node) {
|
|
285
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
286
|
+
const text = node.textContent;
|
|
287
|
+
if (text) {
|
|
288
|
+
return { type: 'text', innerText: text };
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
294
|
+
const tagName = node.tagName.toLowerCase();
|
|
295
|
+
|
|
296
|
+
// Handle inline formatting
|
|
297
|
+
if (['b', 'strong'].includes(tagName)) {
|
|
298
|
+
return {
|
|
299
|
+
type: 'b',
|
|
300
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (['i', 'em'].includes(tagName)) {
|
|
305
|
+
return {
|
|
306
|
+
type: 'i',
|
|
307
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (tagName === 'u') {
|
|
312
|
+
return {
|
|
313
|
+
type: 'u',
|
|
314
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (tagName === 'strike' || tagName === 's') {
|
|
319
|
+
return {
|
|
320
|
+
type: 'strike',
|
|
321
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (tagName === 'a') {
|
|
326
|
+
// Parse anchor's inline style for color
|
|
327
|
+
const anchorStyle = node.getAttribute('style') || '';
|
|
328
|
+
const styleObj = parseInlineStyle(anchorStyle);
|
|
329
|
+
|
|
330
|
+
// Determine link color:
|
|
331
|
+
// - If defaultLinkColor is set (from paint theme data-link-color), use it
|
|
332
|
+
// because paint themes use CSS !important to override inline styles
|
|
333
|
+
// - Otherwise fall back to anchor's inline style
|
|
334
|
+
let linkColor = defaultLinkColor;
|
|
335
|
+
if (!linkColor && styleObj.color) {
|
|
336
|
+
linkColor = normalizeColor(styleObj.color);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Build attrs object
|
|
340
|
+
const attrs = {
|
|
341
|
+
href: node.getAttribute('href') || '#',
|
|
342
|
+
id: `link-${generateId().slice(0, 5)}`,
|
|
343
|
+
target: node.getAttribute('target') || '_self',
|
|
344
|
+
className: 'elTypographyLink',
|
|
345
|
+
rel: node.getAttribute('rel') || 'noopener',
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Add color as inline style if present
|
|
349
|
+
if (linkColor) {
|
|
350
|
+
attrs.style = { color: linkColor };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
type: 'a',
|
|
355
|
+
attrs,
|
|
356
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (tagName === 'br') {
|
|
361
|
+
return { type: 'br' };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (tagName === 'span') {
|
|
365
|
+
return {
|
|
366
|
+
type: 'span',
|
|
367
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (tagName === 'li') {
|
|
372
|
+
return {
|
|
373
|
+
type: 'li',
|
|
374
|
+
children: Array.from(node.childNodes).map(processNode).filter(Boolean)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// For other elements, just process children
|
|
379
|
+
const results = [];
|
|
380
|
+
for (const child of node.childNodes) {
|
|
381
|
+
const processed = processNode(child);
|
|
382
|
+
if (processed) results.push(processed);
|
|
383
|
+
}
|
|
384
|
+
return results.length === 1 ? results[0] : results;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
for (const child of temp.childNodes) {
|
|
391
|
+
const processed = processNode(child);
|
|
392
|
+
if (processed) {
|
|
393
|
+
if (Array.isArray(processed)) {
|
|
394
|
+
children.push(...processed);
|
|
395
|
+
} else {
|
|
396
|
+
children.push(processed);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return children;
|
|
402
|
+
}
|