cf-pagetree-parser 1.0.6 → 1.0.8

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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Parse FunnelWind HTML to ClickFunnels PageTree JSON.
4
4
 
5
+ **Requires [cf-elements](https://www.npmjs.com/package/cf-elements)** - This parser works on the rendered HTML output from cf-elements. The cf-elements package renders FunnelWind custom elements (like `<cf-section>`, `<cf-headline>`, `<cf-button>`) into the DOM structure that this parser reads.
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
@@ -420,6 +420,34 @@ function parseHtmlToTextNodes(html, defaultLinkColor = null) {
420
420
  * ============================================================================
421
421
  */
422
422
 
423
+ /**
424
+ * Shadow preset names to CSS values (matches cf-elements SHADOWS)
425
+ */
426
+ const SHADOW_NAMES = {
427
+ none: 'none',
428
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
429
+ default: '0 1px 3px rgba(0,0,0,0.1)',
430
+ md: '0 4px 6px rgba(0,0,0,0.1)',
431
+ lg: '0 10px 15px rgba(0,0,0,0.1)',
432
+ xl: '0 20px 25px rgba(0,0,0,0.1)',
433
+ '2xl': '0 25px 50px rgba(0,0,0,0.25)',
434
+ };
435
+
436
+ /**
437
+ * Radius preset names to CSS values (matches cf-elements RADIUS)
438
+ */
439
+ const RADIUS_NAMES = {
440
+ none: '0',
441
+ sm: '4px',
442
+ default: '8px',
443
+ md: '12px',
444
+ lg: '16px',
445
+ xl: '20px',
446
+ '2xl': '24px',
447
+ '3xl': '32px',
448
+ full: '9999px',
449
+ };
450
+
423
451
  /**
424
452
  * Shadow presets mapping (from inline shadow to CF params)
425
453
  */
@@ -447,11 +475,20 @@ const SHADOW_PRESETS = {
447
475
 
448
476
  /**
449
477
  * Parse box-shadow value to CF params
478
+ * Handles both preset names (sm, md, lg, xl) and CSS shadow strings
450
479
  */
451
480
  function parseShadow(shadowValue) {
452
481
  if (!shadowValue || shadowValue === 'none') return null;
453
482
 
454
- // Check if it matches a preset
483
+ // Check if it's a preset name first (e.g., "sm", "lg", "xl")
484
+ if (SHADOW_NAMES[shadowValue]) {
485
+ const resolvedShadow = SHADOW_NAMES[shadowValue];
486
+ if (resolvedShadow === 'none') return null;
487
+ // Now parse the resolved CSS value
488
+ return parseShadow(resolvedShadow);
489
+ }
490
+
491
+ // Check if it matches a preset CSS value
455
492
  const normalized = shadowValue.replace(/\s+/g, ' ').trim();
456
493
  if (SHADOW_PRESETS[normalized]) {
457
494
  return SHADOW_PRESETS[normalized];
@@ -694,6 +731,23 @@ function parseBorderRadius(styles) {
694
731
  return null;
695
732
  }
696
733
 
734
+ /**
735
+ * Resolve radius value - handles both preset names (sm, md, lg) and CSS values
736
+ * Returns { value, unit } or null
737
+ */
738
+ function resolveRadius(radiusValue) {
739
+ if (!radiusValue || radiusValue === 'none') return { value: 0, unit: 'px' };
740
+
741
+ // Check if it's a preset name first (e.g., "sm", "lg", "xl")
742
+ if (RADIUS_NAMES[radiusValue]) {
743
+ const resolvedRadius = RADIUS_NAMES[radiusValue];
744
+ return parseValueWithUnit(resolvedRadius);
745
+ }
746
+
747
+ // Otherwise parse as a CSS value
748
+ return parseValueWithUnit(radiusValue);
749
+ }
750
+
697
751
  /**
698
752
  * Convert font-weight string to numeric value
699
753
  */
@@ -1653,7 +1707,12 @@ function parseTextElement(
1653
1707
  ? parseFontFamily(fontAttr)
1654
1708
  : parseFontFamily(textStyles["font-family"]);
1655
1709
 
1656
- // Get color: element data-color > inline style > page color > default
1710
+ // Get color with proper inheritance chain:
1711
+ // 1. Element's own data-color attribute
1712
+ // 2. Inline style color
1713
+ // 3. Walk up parent containers checking for data-color (cf-flex, cf-col, cf-row, cf-section)
1714
+ // 4. cf-page text-color attribute
1715
+ // 5. Default fallback
1657
1716
  const colorAttr = element.getAttribute("data-color");
1658
1717
  let color;
1659
1718
  if (colorAttr) {
@@ -1661,10 +1720,34 @@ function parseTextElement(
1661
1720
  } else if (textStyles.color) {
1662
1721
  color = normalizeColor(textStyles.color);
1663
1722
  } else {
1664
- // Fall back to page-level color from ContentNode
1665
- const contentNode = element.closest('[data-type="ContentNode"]');
1666
- const pageColor = contentNode?.getAttribute("data-color") || contentNode?.getAttribute("data-text-color");
1667
- color = pageColor ? normalizeColor(pageColor) : "#000000";
1723
+ // Walk up the DOM tree checking for data-color on parent containers
1724
+ // Check cf-flex, cf-col, cf-row, cf-section in order (closest first)
1725
+ const containerSelectors = [
1726
+ '[data-type="FlexContainer/V1"]',
1727
+ '[data-type="FlexContainer/V2"]',
1728
+ '[data-type="ColContainer/V1"]',
1729
+ '[data-type="RowContainer/V1"]',
1730
+ '[data-type="SectionContainer/V1"]',
1731
+ ];
1732
+
1733
+ let inheritedColor = null;
1734
+ for (const selector of containerSelectors) {
1735
+ const parent = element.closest(selector);
1736
+ const parentColor = parent?.getAttribute("data-color");
1737
+ if (parentColor) {
1738
+ inheritedColor = normalizeColor(parentColor);
1739
+ break;
1740
+ }
1741
+ }
1742
+
1743
+ if (inheritedColor) {
1744
+ color = inheritedColor;
1745
+ } else {
1746
+ // Fall back to page-level color from ContentNode
1747
+ const contentNode = element.closest('[data-type="ContentNode"]');
1748
+ const pageColor = contentNode?.getAttribute("data-color") || contentNode?.getAttribute("data-text-color");
1749
+ color = pageColor ? normalizeColor(pageColor) : "#000000";
1750
+ }
1668
1751
  }
1669
1752
 
1670
1753
  const alignAttr = element.getAttribute("data-align");
@@ -1928,9 +2011,10 @@ function parseButton(element, parentId, index) {
1928
2011
  const paddingVertical = pyAttr ? parseValueWithUnit(pyAttr) : parseValueWithUnit(anchorStyles['padding-top'] || '16px');
1929
2012
 
1930
2013
  // Border and corners - data attributes first, then inline styles
2014
+ // Use resolveRadius to handle preset names like "lg", "xl", etc.
1931
2015
  const roundedAttr = element.getAttribute('data-rounded');
1932
2016
  const borderRadius = roundedAttr
1933
- ? parseValueWithUnit(roundedAttr)
2017
+ ? resolveRadius(roundedAttr)
1934
2018
  : parseBorderRadius(anchorStyles);
1935
2019
 
1936
2020
  const borderColorAttr = element.getAttribute('data-border-color');
@@ -1980,9 +2064,12 @@ function parseButton(element, parentId, index) {
1980
2064
  const fullWidth = element.getAttribute('data-full-width') === 'true';
1981
2065
 
1982
2066
  // Build button selector - always include padding params
2067
+ const hasBorder = borderWidth && borderWidth.value > 0;
1983
2068
  const buttonSelector = {
1984
2069
  attrs: {
1985
2070
  style: {},
2071
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
2072
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
1986
2073
  },
1987
2074
  params: {
1988
2075
  '--style-padding-horizontal': paddingHorizontal ? paddingHorizontal.value : 32,
@@ -1993,6 +2080,7 @@ function parseButton(element, parentId, index) {
1993
2080
  '--style-border-color': borderColor || 'transparent',
1994
2081
  '--style-border-width': borderWidth ? borderWidth.value : 0,
1995
2082
  '--style-border-width--unit': borderWidth ? borderWidth.unit : 'px',
2083
+ '--style-border-style': hasBorder ? 'solid' : 'none',
1996
2084
  },
1997
2085
  };
1998
2086
 
@@ -2097,9 +2185,9 @@ function parseButton(element, parentId, index) {
2097
2185
  node.selectors['.elButton'].params['border-radius--unit'] = borderRadius.unit;
2098
2186
  }
2099
2187
 
2100
- // Apply shadow (button doesn't support shadow in CF, but keep for reference)
2188
+ // Apply shadow
2101
2189
  if (shadow) {
2102
- // CF buttons don't have native shadow support
2190
+ Object.assign(node.selectors['.elButton'].params, shadowToParams(shadow));
2103
2191
  }
2104
2192
 
2105
2193
  // Add subtext if present
@@ -3849,19 +3937,12 @@ function parseCountdown(element, parentId, index) {
3849
3937
  const borderRadius = parseBorderRadius(containerStyles);
3850
3938
 
3851
3939
  // Parse shadow from inline styles or data attribute
3940
+ // parseShadow() now handles preset names like "sm", "lg", "xl" automatically
3852
3941
  let shadow = parseShadow(containerStyles['box-shadow']);
3853
3942
  if (!shadow) {
3854
3943
  const shadowAttr = element.getAttribute('data-shadow');
3855
3944
  if (shadowAttr) {
3856
- // Resolve preset names to actual shadow values
3857
- const SHADOW_VALUES = {
3858
- 'sm': '0 1px 2px rgba(0,0,0,0.05)',
3859
- 'md': '0 4px 6px rgba(0,0,0,0.1)',
3860
- 'lg': '0 10px 15px rgba(0,0,0,0.1)',
3861
- 'xl': '0 20px 25px rgba(0,0,0,0.1)',
3862
- '2xl': '0 25px 50px rgba(0,0,0,0.25)',
3863
- };
3864
- shadow = parseShadow(SHADOW_VALUES[shadowAttr] || shadowAttr);
3945
+ shadow = parseShadow(shadowAttr);
3865
3946
  }
3866
3947
  }
3867
3948
 
@@ -4385,6 +4466,7 @@ const PARSER_MAP = {
4385
4466
  "ColContainer/V1": parseColContainer,
4386
4467
  "ColInner/V1": parseColInner,
4387
4468
  "FlexContainer/V1": parseFlexContainer,
4469
+ "FlexContainer/V2": parseFlexContainer,
4388
4470
  "Headline/V1": parseHeadline,
4389
4471
  "SubHeadline/V1": parseSubHeadline,
4390
4472
  "Paragraph/V1": parseParagraph,
@@ -4435,6 +4517,7 @@ function createParseElement(elementIdMap) {
4435
4517
  "RowContainer/V1",
4436
4518
  "ColContainer/V1",
4437
4519
  "FlexContainer/V1",
4520
+ "FlexContainer/V2",
4438
4521
  ];
4439
4522
 
4440
4523
  let node;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-pagetree-parser",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Parse FunnelWind HTML to ClickFunnels PageTree JSON",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -28,6 +28,6 @@
28
28
  "license": "MIT",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "https://github.com/WynterJones/cf-pagetree-parser"
31
+ "url": "https://github.com/PrimeMoverHQ/BarnumPT-Builder.git"
32
32
  }
33
33
  }
package/src/index.js CHANGED
@@ -63,6 +63,7 @@ const PARSER_MAP = {
63
63
  "ColContainer/V1": parseColContainer,
64
64
  "ColInner/V1": parseColInner,
65
65
  "FlexContainer/V1": parseFlexContainer,
66
+ "FlexContainer/V2": parseFlexContainer,
66
67
  "Headline/V1": parseHeadline,
67
68
  "SubHeadline/V1": parseSubHeadline,
68
69
  "Paragraph/V1": parseParagraph,
@@ -113,6 +114,7 @@ function createParseElement(elementIdMap) {
113
114
  "RowContainer/V1",
114
115
  "ColContainer/V1",
115
116
  "FlexContainer/V1",
117
+ "FlexContainer/V2",
116
118
  ];
117
119
 
118
120
  let node;
@@ -23,6 +23,8 @@ import {
23
23
  spacingToAttrsAndParams,
24
24
  parseBorderRadius,
25
25
  parseShadow,
26
+ shadowToParams,
27
+ resolveRadius,
26
28
  normalizeFontWeight,
27
29
  parseTextAlign,
28
30
  } from '../styles.js';
@@ -87,9 +89,10 @@ export function parseButton(element, parentId, index) {
87
89
  const paddingVertical = pyAttr ? parseValueWithUnit(pyAttr) : parseValueWithUnit(anchorStyles['padding-top'] || '16px');
88
90
 
89
91
  // Border and corners - data attributes first, then inline styles
92
+ // Use resolveRadius to handle preset names like "lg", "xl", etc.
90
93
  const roundedAttr = element.getAttribute('data-rounded');
91
94
  const borderRadius = roundedAttr
92
- ? parseValueWithUnit(roundedAttr)
95
+ ? resolveRadius(roundedAttr)
93
96
  : parseBorderRadius(anchorStyles);
94
97
 
95
98
  const borderColorAttr = element.getAttribute('data-border-color');
@@ -139,9 +142,12 @@ export function parseButton(element, parentId, index) {
139
142
  const fullWidth = element.getAttribute('data-full-width') === 'true';
140
143
 
141
144
  // Build button selector - always include padding params
145
+ const hasBorder = borderWidth && borderWidth.value > 0;
142
146
  const buttonSelector = {
143
147
  attrs: {
144
148
  style: {},
149
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
150
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
145
151
  },
146
152
  params: {
147
153
  '--style-padding-horizontal': paddingHorizontal ? paddingHorizontal.value : 32,
@@ -152,6 +158,7 @@ export function parseButton(element, parentId, index) {
152
158
  '--style-border-color': borderColor || 'transparent',
153
159
  '--style-border-width': borderWidth ? borderWidth.value : 0,
154
160
  '--style-border-width--unit': borderWidth ? borderWidth.unit : 'px',
161
+ '--style-border-style': hasBorder ? 'solid' : 'none',
155
162
  },
156
163
  };
157
164
 
@@ -256,9 +263,9 @@ export function parseButton(element, parentId, index) {
256
263
  node.selectors['.elButton'].params['border-radius--unit'] = borderRadius.unit;
257
264
  }
258
265
 
259
- // Apply shadow (button doesn't support shadow in CF, but keep for reference)
266
+ // Apply shadow
260
267
  if (shadow) {
261
- // CF buttons don't have native shadow support
268
+ Object.assign(node.selectors['.elButton'].params, shadowToParams(shadow));
262
269
  }
263
270
 
264
271
  // Add subtext if present
@@ -308,19 +308,12 @@ export function parseCountdown(element, parentId, index) {
308
308
  const borderRadius = parseBorderRadius(containerStyles);
309
309
 
310
310
  // Parse shadow from inline styles or data attribute
311
+ // parseShadow() now handles preset names like "sm", "lg", "xl" automatically
311
312
  let shadow = parseShadow(containerStyles['box-shadow']);
312
313
  if (!shadow) {
313
314
  const shadowAttr = element.getAttribute('data-shadow');
314
315
  if (shadowAttr) {
315
- // Resolve preset names to actual shadow values
316
- const SHADOW_VALUES = {
317
- 'sm': '0 1px 2px rgba(0,0,0,0.05)',
318
- 'md': '0 4px 6px rgba(0,0,0,0.1)',
319
- 'lg': '0 10px 15px rgba(0,0,0,0.1)',
320
- 'xl': '0 20px 25px rgba(0,0,0,0.1)',
321
- '2xl': '0 25px 50px rgba(0,0,0,0.25)',
322
- };
323
- shadow = parseShadow(SHADOW_VALUES[shadowAttr] || shadowAttr);
316
+ shadow = parseShadow(shadowAttr);
324
317
  }
325
318
  }
326
319
 
@@ -73,7 +73,12 @@ function parseTextElement(
73
73
  ? parseFontFamily(fontAttr)
74
74
  : parseFontFamily(textStyles["font-family"]);
75
75
 
76
- // Get color: element data-color > inline style > page color > default
76
+ // Get color with proper inheritance chain:
77
+ // 1. Element's own data-color attribute
78
+ // 2. Inline style color
79
+ // 3. Walk up parent containers checking for data-color (cf-flex, cf-col, cf-row, cf-section)
80
+ // 4. cf-page text-color attribute
81
+ // 5. Default fallback
77
82
  const colorAttr = element.getAttribute("data-color");
78
83
  let color;
79
84
  if (colorAttr) {
@@ -81,10 +86,34 @@ function parseTextElement(
81
86
  } else if (textStyles.color) {
82
87
  color = normalizeColor(textStyles.color);
83
88
  } else {
84
- // Fall back to page-level color from ContentNode
85
- const contentNode = element.closest('[data-type="ContentNode"]');
86
- const pageColor = contentNode?.getAttribute("data-color") || contentNode?.getAttribute("data-text-color");
87
- color = pageColor ? normalizeColor(pageColor) : "#000000";
89
+ // Walk up the DOM tree checking for data-color on parent containers
90
+ // Check cf-flex, cf-col, cf-row, cf-section in order (closest first)
91
+ const containerSelectors = [
92
+ '[data-type="FlexContainer/V1"]',
93
+ '[data-type="FlexContainer/V2"]',
94
+ '[data-type="ColContainer/V1"]',
95
+ '[data-type="RowContainer/V1"]',
96
+ '[data-type="SectionContainer/V1"]',
97
+ ];
98
+
99
+ let inheritedColor = null;
100
+ for (const selector of containerSelectors) {
101
+ const parent = element.closest(selector);
102
+ const parentColor = parent?.getAttribute("data-color");
103
+ if (parentColor) {
104
+ inheritedColor = normalizeColor(parentColor);
105
+ break;
106
+ }
107
+ }
108
+
109
+ if (inheritedColor) {
110
+ color = inheritedColor;
111
+ } else {
112
+ // Fall back to page-level color from ContentNode
113
+ const contentNode = element.closest('[data-type="ContentNode"]');
114
+ const pageColor = contentNode?.getAttribute("data-color") || contentNode?.getAttribute("data-text-color");
115
+ color = pageColor ? normalizeColor(pageColor) : "#000000";
116
+ }
88
117
  }
89
118
 
90
119
  const alignAttr = element.getAttribute("data-align");
package/src/styles.js CHANGED
@@ -10,6 +10,34 @@
10
10
 
11
11
  import { parseValueWithUnit, normalizeColor } from './utils.js';
12
12
 
13
+ /**
14
+ * Shadow preset names to CSS values (matches cf-elements SHADOWS)
15
+ */
16
+ export const SHADOW_NAMES = {
17
+ none: 'none',
18
+ sm: '0 1px 2px rgba(0,0,0,0.05)',
19
+ default: '0 1px 3px rgba(0,0,0,0.1)',
20
+ md: '0 4px 6px rgba(0,0,0,0.1)',
21
+ lg: '0 10px 15px rgba(0,0,0,0.1)',
22
+ xl: '0 20px 25px rgba(0,0,0,0.1)',
23
+ '2xl': '0 25px 50px rgba(0,0,0,0.25)',
24
+ };
25
+
26
+ /**
27
+ * Radius preset names to CSS values (matches cf-elements RADIUS)
28
+ */
29
+ export const RADIUS_NAMES = {
30
+ none: '0',
31
+ sm: '4px',
32
+ default: '8px',
33
+ md: '12px',
34
+ lg: '16px',
35
+ xl: '20px',
36
+ '2xl': '24px',
37
+ '3xl': '32px',
38
+ full: '9999px',
39
+ };
40
+
13
41
  /**
14
42
  * Shadow presets mapping (from inline shadow to CF params)
15
43
  */
@@ -37,11 +65,20 @@ export const SHADOW_PRESETS = {
37
65
 
38
66
  /**
39
67
  * Parse box-shadow value to CF params
68
+ * Handles both preset names (sm, md, lg, xl) and CSS shadow strings
40
69
  */
41
70
  export function parseShadow(shadowValue) {
42
71
  if (!shadowValue || shadowValue === 'none') return null;
43
72
 
44
- // Check if it matches a preset
73
+ // Check if it's a preset name first (e.g., "sm", "lg", "xl")
74
+ if (SHADOW_NAMES[shadowValue]) {
75
+ const resolvedShadow = SHADOW_NAMES[shadowValue];
76
+ if (resolvedShadow === 'none') return null;
77
+ // Now parse the resolved CSS value
78
+ return parseShadow(resolvedShadow);
79
+ }
80
+
81
+ // Check if it matches a preset CSS value
45
82
  const normalized = shadowValue.replace(/\s+/g, ' ').trim();
46
83
  if (SHADOW_PRESETS[normalized]) {
47
84
  return SHADOW_PRESETS[normalized];
@@ -284,6 +321,23 @@ export function parseBorderRadius(styles) {
284
321
  return null;
285
322
  }
286
323
 
324
+ /**
325
+ * Resolve radius value - handles both preset names (sm, md, lg) and CSS values
326
+ * Returns { value, unit } or null
327
+ */
328
+ export function resolveRadius(radiusValue) {
329
+ if (!radiusValue || radiusValue === 'none') return { value: 0, unit: 'px' };
330
+
331
+ // Check if it's a preset name first (e.g., "sm", "lg", "xl")
332
+ if (RADIUS_NAMES[radiusValue]) {
333
+ const resolvedRadius = RADIUS_NAMES[radiusValue];
334
+ return parseValueWithUnit(resolvedRadius);
335
+ }
336
+
337
+ // Otherwise parse as a CSS value
338
+ return parseValueWithUnit(radiusValue);
339
+ }
340
+
287
341
  /**
288
342
  * Convert font-weight string to numeric value
289
343
  */