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/index.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FUNNELWIND PAGETREE PARSER
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Converts rendered FunnelWind web components to ClickFunnels pagetree JSON.
|
|
7
|
+
*
|
|
8
|
+
* USAGE:
|
|
9
|
+
* // After FunnelWind renders the page
|
|
10
|
+
* import { parsePageTree } from './pagetree-parser/index.js';
|
|
11
|
+
*
|
|
12
|
+
* // Parse the entire page
|
|
13
|
+
* const pageTree = parsePageTree();
|
|
14
|
+
*
|
|
15
|
+
* // Or parse from a specific container
|
|
16
|
+
* const pageTree = parsePageTree(document.querySelector('[data-type="ContentNode"]'));
|
|
17
|
+
*
|
|
18
|
+
* // Export as JSON
|
|
19
|
+
* const json = JSON.stringify(pageTree, null, 2);
|
|
20
|
+
*
|
|
21
|
+
* ============================================================================
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { getDataType, generateId, normalizeColor } from "./utils.js";
|
|
25
|
+
import { normalizeFontFamily } from "./styles.js";
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
parseContentNode,
|
|
29
|
+
parseSectionContainer,
|
|
30
|
+
parseRowContainer,
|
|
31
|
+
parseColContainer,
|
|
32
|
+
parseColInner,
|
|
33
|
+
parseFlexContainer,
|
|
34
|
+
parseHeadline,
|
|
35
|
+
parseSubHeadline,
|
|
36
|
+
parseParagraph,
|
|
37
|
+
parseButton,
|
|
38
|
+
parseImage,
|
|
39
|
+
parseIcon,
|
|
40
|
+
parseVideo,
|
|
41
|
+
parseDivider,
|
|
42
|
+
parseInput,
|
|
43
|
+
parseTextArea,
|
|
44
|
+
parseSelectBox,
|
|
45
|
+
parseCheckbox,
|
|
46
|
+
parseBulletList,
|
|
47
|
+
parseProgressBar,
|
|
48
|
+
parseVideoPopup,
|
|
49
|
+
parseCountdown,
|
|
50
|
+
parseCheckoutPlaceholder,
|
|
51
|
+
parseOrderSummaryPlaceholder,
|
|
52
|
+
parseConfirmationPlaceholder,
|
|
53
|
+
parseModalContainer,
|
|
54
|
+
} from "./parsers/index.js";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get typescale sizes calculated from baseSize and scaleRatio.
|
|
58
|
+
* Matches StyleguideEditor.tsx and cf-elements.js getTypescale() for consistency.
|
|
59
|
+
* Returns element-specific scales because headlines, subheadlines, and paragraphs
|
|
60
|
+
* have different sizes at the same preset (e.g., "s" for headline = 20px, "s" for paragraph = 13px).
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} typography - Typography settings with baseSize and scaleRatio
|
|
63
|
+
* @returns {Object|null} - Map of element types to their size preset maps
|
|
64
|
+
*/
|
|
65
|
+
function getTypescale(typography) {
|
|
66
|
+
if (!typography) return null;
|
|
67
|
+
|
|
68
|
+
const { baseSize = 16, scaleRatio = 1.25 } = typography;
|
|
69
|
+
const r = scaleRatio;
|
|
70
|
+
const b = baseSize;
|
|
71
|
+
|
|
72
|
+
// Build the base scale points (negative = smaller, positive = larger)
|
|
73
|
+
const scale = {
|
|
74
|
+
n3: Math.round(b / Math.pow(r, 3)), // ~8
|
|
75
|
+
n2: Math.round(b / Math.pow(r, 2)), // ~10
|
|
76
|
+
n1: Math.round(b / r), // ~13
|
|
77
|
+
base: b, // 16
|
|
78
|
+
p1: Math.round(b * r), // ~20
|
|
79
|
+
p2: Math.round(b * Math.pow(r, 2)), // ~25
|
|
80
|
+
p3: Math.round(b * Math.pow(r, 3)), // ~31
|
|
81
|
+
p4: Math.round(b * Math.pow(r, 4)), // ~39
|
|
82
|
+
p5: Math.round(b * Math.pow(r, 5)), // ~49
|
|
83
|
+
p6: Math.round(b * Math.pow(r, 6)), // ~61
|
|
84
|
+
p7: Math.round(b * Math.pow(r, 7)), // ~76
|
|
85
|
+
p8: Math.round(b * Math.pow(r, 8)), // ~95
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Return element-specific scales (each element type maps presets to different scale points)
|
|
89
|
+
return {
|
|
90
|
+
headline: {
|
|
91
|
+
"5xl": scale.p8,
|
|
92
|
+
"4xl": scale.p7,
|
|
93
|
+
"3xl": scale.p6,
|
|
94
|
+
"2xl": scale.p5,
|
|
95
|
+
xl: scale.p4,
|
|
96
|
+
l: scale.p3,
|
|
97
|
+
lg: scale.p3,
|
|
98
|
+
m: scale.p2,
|
|
99
|
+
md: scale.p2,
|
|
100
|
+
s: scale.p1,
|
|
101
|
+
sm: scale.p1,
|
|
102
|
+
xs: scale.base,
|
|
103
|
+
},
|
|
104
|
+
subheadline: {
|
|
105
|
+
"5xl": scale.p7,
|
|
106
|
+
"4xl": scale.p6,
|
|
107
|
+
"3xl": scale.p5,
|
|
108
|
+
"2xl": scale.p4,
|
|
109
|
+
xl: scale.p3,
|
|
110
|
+
l: scale.p2,
|
|
111
|
+
lg: scale.p2,
|
|
112
|
+
m: scale.p1,
|
|
113
|
+
md: scale.p1,
|
|
114
|
+
s: scale.base,
|
|
115
|
+
sm: scale.base,
|
|
116
|
+
xs: scale.n1,
|
|
117
|
+
},
|
|
118
|
+
paragraph: {
|
|
119
|
+
"5xl": scale.p6,
|
|
120
|
+
"4xl": scale.p5,
|
|
121
|
+
"3xl": scale.p4,
|
|
122
|
+
"2xl": scale.p3,
|
|
123
|
+
xl: scale.p2,
|
|
124
|
+
l: scale.p1,
|
|
125
|
+
lg: scale.p1,
|
|
126
|
+
m: scale.base,
|
|
127
|
+
md: scale.base,
|
|
128
|
+
s: scale.n1,
|
|
129
|
+
sm: scale.n1,
|
|
130
|
+
xs: scale.n2,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Apply styleguide fonts and colors as data attributes before parsing.
|
|
137
|
+
* This ensures the parser captures styleguide-applied values.
|
|
138
|
+
*
|
|
139
|
+
* @param {Document|HTMLElement} root - The root element or document to process
|
|
140
|
+
* @param {Object} styleguideData - Optional styleguide data (will try to read from embedded JSON if not provided)
|
|
141
|
+
*/
|
|
142
|
+
function applyStyleguideDataAttributes(root, styleguideData = null) {
|
|
143
|
+
// Try to get styleguide from embedded JSON if not provided
|
|
144
|
+
if (!styleguideData) {
|
|
145
|
+
const scriptEl = root.querySelector
|
|
146
|
+
? root.querySelector("#cf-styleguide-data")
|
|
147
|
+
: root.getElementById
|
|
148
|
+
? root.getElementById("cf-styleguide-data")
|
|
149
|
+
: null;
|
|
150
|
+
if (scriptEl) {
|
|
151
|
+
try {
|
|
152
|
+
styleguideData = JSON.parse(scriptEl.textContent);
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.warn("PageTree Parser: Failed to parse styleguide data:", e);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!styleguideData) return;
|
|
161
|
+
|
|
162
|
+
const { typography, paintThemes, colors } = styleguideData;
|
|
163
|
+
|
|
164
|
+
// Helper to get color hex by ID
|
|
165
|
+
const getColorHex = (colorId) => {
|
|
166
|
+
if (!colors) return "#000000";
|
|
167
|
+
const color = colors.find((c) => c.id === colorId);
|
|
168
|
+
return color ? color.hex : "#000000";
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Apply typography fonts to elements without explicit fonts
|
|
172
|
+
if (typography) {
|
|
173
|
+
const { headlineFont, subheadlineFont, contentFont } = typography;
|
|
174
|
+
|
|
175
|
+
if (headlineFont) {
|
|
176
|
+
root
|
|
177
|
+
.querySelectorAll('[data-type="Headline/V1"]:not([data-font])')
|
|
178
|
+
.forEach((el) => {
|
|
179
|
+
el.setAttribute("data-font", headlineFont);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (subheadlineFont) {
|
|
184
|
+
root
|
|
185
|
+
.querySelectorAll('[data-type="SubHeadline/V1"]:not([data-font])')
|
|
186
|
+
.forEach((el) => {
|
|
187
|
+
el.setAttribute("data-font", subheadlineFont);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (contentFont) {
|
|
192
|
+
root
|
|
193
|
+
.querySelectorAll('[data-type="Paragraph/V1"]:not([data-font])')
|
|
194
|
+
.forEach((el) => {
|
|
195
|
+
el.setAttribute("data-font", contentFont);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Helper to check if element's closest paint-themed ancestor is the given container
|
|
201
|
+
// This prevents applying colors to elements inside nested non-paint containers
|
|
202
|
+
const isDirectPaintDescendant = (el, paintContainer) => {
|
|
203
|
+
// Find the closest ancestor with data-paint-colors attribute
|
|
204
|
+
const closestPaint = el.closest("[data-paint-colors]");
|
|
205
|
+
// Element should only get colors if its closest paint ancestor is this container
|
|
206
|
+
return closestPaint === paintContainer;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Apply paint theme colors - OVERRIDE existing (paint themes take precedence)
|
|
210
|
+
// Only apply to elements that are direct descendants (no intervening non-paint containers)
|
|
211
|
+
if (paintThemes?.length) {
|
|
212
|
+
paintThemes.forEach((theme) => {
|
|
213
|
+
const containers = root.querySelectorAll(
|
|
214
|
+
`[data-paint-colors="${theme.id}"]`
|
|
215
|
+
);
|
|
216
|
+
containers.forEach((container) => {
|
|
217
|
+
const headlineColor = getColorHex(theme.headlineColorId);
|
|
218
|
+
const subheadlineColor = getColorHex(theme.subheadlineColorId);
|
|
219
|
+
const contentColor = getColorHex(theme.contentColorId);
|
|
220
|
+
const iconColor = getColorHex(theme.iconColorId);
|
|
221
|
+
const linkColor = theme.linkColorId
|
|
222
|
+
? getColorHex(theme.linkColorId)
|
|
223
|
+
: null;
|
|
224
|
+
|
|
225
|
+
// Apply headline color (only to direct paint descendants)
|
|
226
|
+
container
|
|
227
|
+
.querySelectorAll('[data-type="Headline/V1"]')
|
|
228
|
+
.forEach((el) => {
|
|
229
|
+
if (isDirectPaintDescendant(el, container)) {
|
|
230
|
+
if (!el.hasAttribute("data-color-explicit")) {
|
|
231
|
+
el.setAttribute("data-color", headlineColor);
|
|
232
|
+
}
|
|
233
|
+
if (linkColor) el.setAttribute("data-link-color", linkColor);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Apply subheadline color (only to direct paint descendants)
|
|
238
|
+
container
|
|
239
|
+
.querySelectorAll('[data-type="SubHeadline/V1"]')
|
|
240
|
+
.forEach((el) => {
|
|
241
|
+
if (isDirectPaintDescendant(el, container)) {
|
|
242
|
+
if (!el.hasAttribute("data-color-explicit")) {
|
|
243
|
+
el.setAttribute("data-color", subheadlineColor);
|
|
244
|
+
}
|
|
245
|
+
if (linkColor) el.setAttribute("data-link-color", linkColor);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Apply content/paragraph color (only to direct paint descendants)
|
|
250
|
+
container
|
|
251
|
+
.querySelectorAll('[data-type="Paragraph/V1"]')
|
|
252
|
+
.forEach((el) => {
|
|
253
|
+
if (isDirectPaintDescendant(el, container)) {
|
|
254
|
+
if (!el.hasAttribute("data-color-explicit")) {
|
|
255
|
+
el.setAttribute("data-color", contentColor);
|
|
256
|
+
}
|
|
257
|
+
if (linkColor) el.setAttribute("data-link-color", linkColor);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Apply icon color (only to direct paint descendants)
|
|
262
|
+
container.querySelectorAll('[data-type="Icon/V1"]').forEach((el) => {
|
|
263
|
+
if (isDirectPaintDescendant(el, container)) {
|
|
264
|
+
if (!el.hasAttribute("data-color-explicit")) {
|
|
265
|
+
el.setAttribute("data-color", iconColor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Apply colors to bullet lists (only to direct paint descendants)
|
|
271
|
+
container
|
|
272
|
+
.querySelectorAll('[data-type="BulletList/V1"]')
|
|
273
|
+
.forEach((el) => {
|
|
274
|
+
if (isDirectPaintDescendant(el, container)) {
|
|
275
|
+
if (!el.hasAttribute("data-text-color-explicit")) {
|
|
276
|
+
el.setAttribute("data-text-color", contentColor);
|
|
277
|
+
}
|
|
278
|
+
if (!el.hasAttribute("data-icon-color-explicit")) {
|
|
279
|
+
el.setAttribute("data-icon-color", iconColor);
|
|
280
|
+
}
|
|
281
|
+
if (linkColor) el.setAttribute("data-link-color", linkColor);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Resolve size presets (xl, l, m, s) to pixel values for text elements
|
|
289
|
+
// This allows the parser to capture correct font sizes even when using presets
|
|
290
|
+
if (typography) {
|
|
291
|
+
const typescale = getTypescale(typography);
|
|
292
|
+
|
|
293
|
+
if (typescale) {
|
|
294
|
+
// Map data-type to element scale key
|
|
295
|
+
const elementTypeMap = {
|
|
296
|
+
"Headline/V1": "headline",
|
|
297
|
+
"SubHeadline/V1": "subheadline",
|
|
298
|
+
"Paragraph/V1": "paragraph",
|
|
299
|
+
"BulletList/V1": "paragraph", // Bullet lists use paragraph scale
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Find all text elements with a size attribute
|
|
303
|
+
const textElements = root.querySelectorAll(
|
|
304
|
+
'[data-type="Headline/V1"][data-size], ' +
|
|
305
|
+
'[data-type="SubHeadline/V1"][data-size], ' +
|
|
306
|
+
'[data-type="Paragraph/V1"][data-size], ' +
|
|
307
|
+
'[data-type="BulletList/V1"][data-size]'
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
textElements.forEach((el) => {
|
|
311
|
+
const sizeAttr = el.getAttribute("data-size");
|
|
312
|
+
const dataType = el.getAttribute("data-type");
|
|
313
|
+
const elementKey = elementTypeMap[dataType] || "headline";
|
|
314
|
+
const elementScale = typescale[elementKey];
|
|
315
|
+
|
|
316
|
+
// Check if it's a preset (not already a px value)
|
|
317
|
+
if (sizeAttr && elementScale && elementScale[sizeAttr] !== undefined) {
|
|
318
|
+
// Set the resolved pixel value as a separate attribute
|
|
319
|
+
el.setAttribute("data-size-resolved", `${elementScale[sizeAttr]}px`);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Map of data-type to parser function
|
|
328
|
+
*/
|
|
329
|
+
const PARSER_MAP = {
|
|
330
|
+
ContentNode: parseContentNode,
|
|
331
|
+
"SectionContainer/V1": parseSectionContainer,
|
|
332
|
+
"RowContainer/V1": parseRowContainer,
|
|
333
|
+
"ColContainer/V1": parseColContainer,
|
|
334
|
+
"ColInner/V1": parseColInner,
|
|
335
|
+
"FlexContainer/V1": parseFlexContainer,
|
|
336
|
+
"Headline/V1": parseHeadline,
|
|
337
|
+
"SubHeadline/V1": parseSubHeadline,
|
|
338
|
+
"Paragraph/V1": parseParagraph,
|
|
339
|
+
"Button/V1": parseButton,
|
|
340
|
+
"Image/V2": parseImage,
|
|
341
|
+
"Icon/V1": parseIcon,
|
|
342
|
+
"Video/V1": parseVideo,
|
|
343
|
+
"Divider/V1": parseDivider,
|
|
344
|
+
"Input/V1": parseInput,
|
|
345
|
+
"TextArea/V1": parseTextArea,
|
|
346
|
+
"SelectBox/V1": parseSelectBox,
|
|
347
|
+
"Checkbox/V1": parseCheckbox,
|
|
348
|
+
"BulletList/V1": parseBulletList,
|
|
349
|
+
"ProgressBar/V1": parseProgressBar,
|
|
350
|
+
"VideoPopup/V1": parseVideoPopup,
|
|
351
|
+
"Countdown/V1": parseCountdown,
|
|
352
|
+
// Placeholders (output actual ClickFunnels element types)
|
|
353
|
+
"CheckoutPlaceholder": parseCheckoutPlaceholder,
|
|
354
|
+
"OrderSummaryPlaceholder": parseOrderSummaryPlaceholder,
|
|
355
|
+
"ConfirmationPlaceholder": parseConfirmationPlaceholder,
|
|
356
|
+
// Popup
|
|
357
|
+
"ModalContainer/V1": parseModalContainer,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create parseElement function with styleguide data in closure
|
|
362
|
+
* Also tracks element-id to internal-id mappings for scroll/show-hide resolution
|
|
363
|
+
*/
|
|
364
|
+
function createParseElement(styleguideData, elementIdMap) {
|
|
365
|
+
function parseElement(element, parentId, index) {
|
|
366
|
+
const dataType = getDataType(element);
|
|
367
|
+
|
|
368
|
+
// Skip elements without data-type (like col-inner wrappers)
|
|
369
|
+
if (!dataType) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const parser = PARSER_MAP[dataType];
|
|
374
|
+
if (!parser) {
|
|
375
|
+
console.warn(`No parser found for data-type: ${dataType}`);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Container elements need the parseChildren callback
|
|
380
|
+
const containerTypes = [
|
|
381
|
+
"ContentNode",
|
|
382
|
+
"SectionContainer/V1",
|
|
383
|
+
"RowContainer/V1",
|
|
384
|
+
"ColContainer/V1",
|
|
385
|
+
"FlexContainer/V1",
|
|
386
|
+
];
|
|
387
|
+
|
|
388
|
+
let node;
|
|
389
|
+
if (containerTypes.includes(dataType)) {
|
|
390
|
+
node = parser(element, parentId, index, parseElement);
|
|
391
|
+
} else if (dataType === "Button/V1") {
|
|
392
|
+
// Button needs styleguide data to look up button styles
|
|
393
|
+
node = parser(element, parentId, index, styleguideData);
|
|
394
|
+
} else {
|
|
395
|
+
node = parser(element, parentId, index);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Track element-id to internal-id mapping for scroll/show-hide resolution
|
|
399
|
+
if (node && node.attrs && node.attrs.id && node.id) {
|
|
400
|
+
elementIdMap[node.attrs.id] = node.id;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return node;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return parseElement;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Resolve element-id references in button scroll-target and show-ids/hide-ids
|
|
411
|
+
* to use internal pagetree IDs with id- prefix
|
|
412
|
+
*/
|
|
413
|
+
function resolveButtonReferences(node, elementIdMap) {
|
|
414
|
+
if (!node) return;
|
|
415
|
+
|
|
416
|
+
// Process buttons
|
|
417
|
+
if (node.type === "Button/V1" && node.params) {
|
|
418
|
+
// Resolve scroll href: #scroll-elementId → #scroll-id-internalId
|
|
419
|
+
if (node.params.href && node.params.href.startsWith("#scroll-")) {
|
|
420
|
+
const elementId = node.params.href.replace("#scroll-", "");
|
|
421
|
+
const internalId = elementIdMap[elementId];
|
|
422
|
+
if (internalId) {
|
|
423
|
+
node.params.href = `#scroll-id-${internalId}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Resolve showIds: elementId1,elementId2 → id-internalId1,id-internalId2
|
|
428
|
+
if (node.params.showIds) {
|
|
429
|
+
const ids = node.params.showIds.split(",").map((id) => id.trim());
|
|
430
|
+
const resolvedIds = ids.map((elementId) => {
|
|
431
|
+
const internalId = elementIdMap[elementId];
|
|
432
|
+
return internalId ? `id-${internalId}` : elementId;
|
|
433
|
+
});
|
|
434
|
+
node.params.showIds = resolvedIds.join(",");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Resolve hideIds: elementId1,elementId2 → id-internalId1,id-internalId2
|
|
438
|
+
if (node.params.hideIds) {
|
|
439
|
+
const ids = node.params.hideIds.split(",").map((id) => id.trim());
|
|
440
|
+
const resolvedIds = ids.map((elementId) => {
|
|
441
|
+
const internalId = elementIdMap[elementId];
|
|
442
|
+
return internalId ? `id-${internalId}` : elementId;
|
|
443
|
+
});
|
|
444
|
+
node.params.hideIds = resolvedIds.join(",");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Recursively process children
|
|
449
|
+
if (node.children && Array.isArray(node.children)) {
|
|
450
|
+
node.children.forEach((child) => resolveButtonReferences(child, elementIdMap));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Parse the entire page tree starting from a root element
|
|
456
|
+
*
|
|
457
|
+
* @param {HTMLElement} rootElement - The root element to parse (default: find ContentNode)
|
|
458
|
+
* @param {Object} styleguideData - Optional styleguide data for applying fonts/colors
|
|
459
|
+
* @returns {Object} The pagetree JSON object
|
|
460
|
+
*/
|
|
461
|
+
export function parsePageTree(rootElement = null, styleguideData = null) {
|
|
462
|
+
// Find the root ContentNode if not provided
|
|
463
|
+
if (!rootElement) {
|
|
464
|
+
rootElement = document.querySelector('[data-type="ContentNode"]');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!rootElement) {
|
|
468
|
+
console.error("No ContentNode found in document");
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Apply styleguide fonts and colors as data attributes before parsing
|
|
473
|
+
// This ensures the parser captures styleguide-applied values (fonts, paint theme colors)
|
|
474
|
+
const docRoot = rootElement.ownerDocument || document;
|
|
475
|
+
applyStyleguideDataAttributes(docRoot, styleguideData);
|
|
476
|
+
|
|
477
|
+
// Create element ID map for scroll/show-hide reference resolution
|
|
478
|
+
const elementIdMap = {};
|
|
479
|
+
|
|
480
|
+
// Create parseElement with styleguide data in closure for button style lookup
|
|
481
|
+
const parseElement = createParseElement(styleguideData, elementIdMap);
|
|
482
|
+
|
|
483
|
+
// Parse the content node
|
|
484
|
+
const content = parseContentNode(rootElement, parseElement);
|
|
485
|
+
|
|
486
|
+
// Resolve button scroll-target and show-ids/hide-ids references
|
|
487
|
+
// This converts element-id references to internal pagetree IDs with id- prefix
|
|
488
|
+
resolveButtonReferences(content, elementIdMap);
|
|
489
|
+
|
|
490
|
+
// Parse popup if present (cf-popup renders to .cf-popup-wrapper with data-type="ModalContainer/V1")
|
|
491
|
+
let popupNode = null;
|
|
492
|
+
const popupElement = docRoot.querySelector('.cf-popup-wrapper[data-type="ModalContainer/V1"]');
|
|
493
|
+
if (popupElement) {
|
|
494
|
+
popupNode = parseModalContainer(popupElement, parseElement);
|
|
495
|
+
// Also resolve button references in popup
|
|
496
|
+
resolveButtonReferences(popupNode, elementIdMap);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Extract page settings from data attributes
|
|
500
|
+
const textColorRaw = rootElement.getAttribute("data-text-color") || "#334155";
|
|
501
|
+
const linkColorRaw = rootElement.getAttribute("data-link-color") || "#3b82f6";
|
|
502
|
+
const textColor = normalizeColor(textColorRaw);
|
|
503
|
+
const linkColor = normalizeColor(linkColorRaw);
|
|
504
|
+
const fontFamily = rootElement.getAttribute("data-font-family") || "";
|
|
505
|
+
const fontWeight = rootElement.getAttribute("data-font-weight") || "";
|
|
506
|
+
// Decode URL-encoded values
|
|
507
|
+
const headerCodeRaw = rootElement.getAttribute("data-header-code") || "";
|
|
508
|
+
const footerCodeRaw = rootElement.getAttribute("data-footer-code") || "";
|
|
509
|
+
const customCssRaw = rootElement.getAttribute("data-custom-css") || "";
|
|
510
|
+
const headerCode = headerCodeRaw ? decodeURIComponent(headerCodeRaw) : "";
|
|
511
|
+
const footerCode = footerCodeRaw ? decodeURIComponent(footerCodeRaw) : "";
|
|
512
|
+
const customCss = customCssRaw ? decodeURIComponent(customCssRaw) : "";
|
|
513
|
+
|
|
514
|
+
// Generate settings ID
|
|
515
|
+
const settingsId = generateId();
|
|
516
|
+
|
|
517
|
+
// Build settings children
|
|
518
|
+
const settingsChildren = [];
|
|
519
|
+
|
|
520
|
+
// page_style (CSS/fonts/colors)
|
|
521
|
+
const pageStyleAttrs = {
|
|
522
|
+
style: {
|
|
523
|
+
color: textColor,
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
// Normalize font-family to ClickFunnels format (e.g., "\"Poppins\", sans-serif")
|
|
527
|
+
const normalizedFontFamily = fontFamily
|
|
528
|
+
? normalizeFontFamily(fontFamily)
|
|
529
|
+
: null;
|
|
530
|
+
if (normalizedFontFamily)
|
|
531
|
+
pageStyleAttrs.style["font-family"] = normalizedFontFamily;
|
|
532
|
+
if (fontWeight) pageStyleAttrs.style["font-weight"] = fontWeight;
|
|
533
|
+
|
|
534
|
+
settingsChildren.push({
|
|
535
|
+
id: "page_style",
|
|
536
|
+
type: "css",
|
|
537
|
+
parentId: settingsId,
|
|
538
|
+
fractionalIndex: "a0",
|
|
539
|
+
attrs: pageStyleAttrs,
|
|
540
|
+
selectors: {
|
|
541
|
+
".elTypographyLink": {
|
|
542
|
+
attrs: {
|
|
543
|
+
style: {
|
|
544
|
+
color: linkColor,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
params: {},
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
params: {},
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// header-code
|
|
554
|
+
settingsChildren.push({
|
|
555
|
+
id: "header-code",
|
|
556
|
+
type: "raw",
|
|
557
|
+
parentId: settingsId,
|
|
558
|
+
fractionalIndex: "a1",
|
|
559
|
+
innerText: headerCode,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// footer-code
|
|
563
|
+
settingsChildren.push({
|
|
564
|
+
id: "footer-code",
|
|
565
|
+
type: "raw",
|
|
566
|
+
parentId: settingsId,
|
|
567
|
+
fractionalIndex: "a2",
|
|
568
|
+
innerText: footerCode,
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// css - default CSS plus any custom CSS
|
|
572
|
+
const defaultCss = "";
|
|
573
|
+
const combinedCss = customCss ? `${defaultCss}\n\n${customCss}` : defaultCss;
|
|
574
|
+
settingsChildren.push({
|
|
575
|
+
id: "css",
|
|
576
|
+
type: "raw",
|
|
577
|
+
parentId: settingsId,
|
|
578
|
+
fractionalIndex: "a3",
|
|
579
|
+
innerText: combinedCss,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Build the full pagetree structure
|
|
583
|
+
const pageTree = {
|
|
584
|
+
version: 157,
|
|
585
|
+
content,
|
|
586
|
+
settings: {
|
|
587
|
+
id: settingsId,
|
|
588
|
+
type: "settings",
|
|
589
|
+
version: 0,
|
|
590
|
+
children: settingsChildren,
|
|
591
|
+
},
|
|
592
|
+
popup: popupNode || {
|
|
593
|
+
id: "",
|
|
594
|
+
version: 0,
|
|
595
|
+
type: "ModalContainer/V1",
|
|
596
|
+
selectors: {
|
|
597
|
+
".containerModal": {
|
|
598
|
+
attrs: {
|
|
599
|
+
"data-skip-corners-settings": "false",
|
|
600
|
+
"data-style-guide-corner": "style1",
|
|
601
|
+
style: { "margin-bottom": 0 },
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
return pageTree;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Parse and export as JSON string
|
|
613
|
+
*
|
|
614
|
+
* @param {HTMLElement} rootElement - The root element to parse
|
|
615
|
+
* @param {boolean} pretty - Whether to pretty-print the JSON
|
|
616
|
+
* @param {Object} styleguideData - Optional styleguide data for applying fonts/colors
|
|
617
|
+
* @returns {string} The pagetree as JSON string
|
|
618
|
+
*/
|
|
619
|
+
export function exportPageTreeJSON(
|
|
620
|
+
rootElement = null,
|
|
621
|
+
pretty = true,
|
|
622
|
+
styleguideData = null
|
|
623
|
+
) {
|
|
624
|
+
const pageTree = parsePageTree(rootElement, styleguideData);
|
|
625
|
+
if (!pageTree) return null;
|
|
626
|
+
|
|
627
|
+
return pretty ? JSON.stringify(pageTree, null, 2) : JSON.stringify(pageTree);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Download the pagetree as a JSON file
|
|
632
|
+
*
|
|
633
|
+
* @param {string} filename - The filename (default: 'pagetree.json')
|
|
634
|
+
* @param {HTMLElement} rootElement - The root element to parse
|
|
635
|
+
* @param {Object} styleguideData - Optional styleguide data for applying fonts/colors
|
|
636
|
+
*/
|
|
637
|
+
export function downloadPageTree(
|
|
638
|
+
filename = "pagetree.json",
|
|
639
|
+
rootElement = null,
|
|
640
|
+
styleguideData = null
|
|
641
|
+
) {
|
|
642
|
+
const json = exportPageTreeJSON(rootElement, true, styleguideData);
|
|
643
|
+
if (!json) return;
|
|
644
|
+
|
|
645
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
646
|
+
const url = URL.createObjectURL(blob);
|
|
647
|
+
|
|
648
|
+
const a = document.createElement("a");
|
|
649
|
+
a.href = url;
|
|
650
|
+
a.download = filename;
|
|
651
|
+
document.body.appendChild(a);
|
|
652
|
+
a.click();
|
|
653
|
+
document.body.removeChild(a);
|
|
654
|
+
URL.revokeObjectURL(url);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Copy pagetree JSON to clipboard
|
|
659
|
+
*
|
|
660
|
+
* @param {HTMLElement} rootElement - The root element to parse
|
|
661
|
+
* @param {Object} styleguideData - Optional styleguide data for applying fonts/colors
|
|
662
|
+
* @returns {Promise<boolean>} Whether the copy was successful
|
|
663
|
+
*/
|
|
664
|
+
export async function copyPageTreeToClipboard(
|
|
665
|
+
rootElement = null,
|
|
666
|
+
styleguideData = null
|
|
667
|
+
) {
|
|
668
|
+
const json = exportPageTreeJSON(rootElement, true, styleguideData);
|
|
669
|
+
if (!json) return false;
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
await navigator.clipboard.writeText(json);
|
|
673
|
+
return true;
|
|
674
|
+
} catch (err) {
|
|
675
|
+
console.error("Failed to copy to clipboard:", err);
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Expose global API when running in the browser
|
|
681
|
+
if (typeof window !== "undefined") {
|
|
682
|
+
window.PageTreeParser = {
|
|
683
|
+
parse: parsePageTree,
|
|
684
|
+
toJSON: exportPageTreeJSON,
|
|
685
|
+
download: downloadPageTree,
|
|
686
|
+
copyToClipboard: copyPageTreeToClipboard,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Export all utilities for advanced usage
|
|
691
|
+
export {
|
|
692
|
+
generateId,
|
|
693
|
+
generateFractionalIndex,
|
|
694
|
+
parseValueWithUnit,
|
|
695
|
+
normalizeColor,
|
|
696
|
+
parseInlineStyle,
|
|
697
|
+
} from "./utils.js";
|
|
698
|
+
|
|
699
|
+
export {
|
|
700
|
+
parseShadow,
|
|
701
|
+
shadowToParams,
|
|
702
|
+
parseBorder,
|
|
703
|
+
borderToParams,
|
|
704
|
+
parseBackground,
|
|
705
|
+
backgroundToParams,
|
|
706
|
+
} from "./styles.js";
|
|
707
|
+
|
|
708
|
+
export { PARSER_MAP };
|