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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* PAGETREE PARSER - Text Element Parsers
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Headline/V1, SubHeadline/V1, Paragraph/V1
|
|
7
|
+
*
|
|
8
|
+
* ============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
generateId,
|
|
13
|
+
generateFractionalIndex,
|
|
14
|
+
parseInlineStyle,
|
|
15
|
+
parseValueWithUnit,
|
|
16
|
+
normalizeColor,
|
|
17
|
+
parseHtmlToTextNodes,
|
|
18
|
+
parseAnimationAttrs,
|
|
19
|
+
} from "../utils.js";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
parseSpacing,
|
|
23
|
+
spacingToAttrsAndParams,
|
|
24
|
+
normalizeFontWeight,
|
|
25
|
+
parseLineHeight,
|
|
26
|
+
parseTextAlign,
|
|
27
|
+
parseFontFamily,
|
|
28
|
+
} from "../styles.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse text element (Headline, SubHeadline, or Paragraph)
|
|
32
|
+
*
|
|
33
|
+
* Reads from data attributes first (most reliable), falls back to inline styles
|
|
34
|
+
*/
|
|
35
|
+
function parseTextElement(
|
|
36
|
+
element,
|
|
37
|
+
parentId,
|
|
38
|
+
index,
|
|
39
|
+
type,
|
|
40
|
+
selector,
|
|
41
|
+
_styleGuideAttr
|
|
42
|
+
) {
|
|
43
|
+
const id = generateId();
|
|
44
|
+
const contentEditableId = generateId();
|
|
45
|
+
|
|
46
|
+
const wrapperStyles = parseInlineStyle(element.getAttribute("style") || "");
|
|
47
|
+
const spacing = parseSpacing(wrapperStyles);
|
|
48
|
+
|
|
49
|
+
// Find the text element (h1, h2, p, span)
|
|
50
|
+
const textEl =
|
|
51
|
+
element.querySelector("h1, h2, h3, h4, h5, h6, p, span") || element;
|
|
52
|
+
const textStyles = parseInlineStyle(textEl.getAttribute("style") || "");
|
|
53
|
+
|
|
54
|
+
// Read from data attributes first (most reliable), fallback to inline styles
|
|
55
|
+
// data-size-resolved is set by applyStyleguideDataAttributes() when size presets are used
|
|
56
|
+
// This converts "xl", "l", "m", "s" presets to actual pixel values from the typescale
|
|
57
|
+
const sizeResolved = element.getAttribute("data-size-resolved");
|
|
58
|
+
const sizeAttr = element.getAttribute("data-size");
|
|
59
|
+
const parsedSizeResolved = sizeResolved ? parseValueWithUnit(sizeResolved) : null;
|
|
60
|
+
const parsedSizeAttr = sizeAttr ? parseValueWithUnit(sizeAttr) : null;
|
|
61
|
+
const fontSize = parsedSizeResolved || parsedSizeAttr || parseValueWithUnit(textStyles["font-size"] || "48px");
|
|
62
|
+
|
|
63
|
+
const weightAttr = element.getAttribute("data-weight");
|
|
64
|
+
const fontWeight = weightAttr
|
|
65
|
+
? normalizeFontWeight(weightAttr)
|
|
66
|
+
: normalizeFontWeight(textStyles["font-weight"] || "normal");
|
|
67
|
+
|
|
68
|
+
const fontAttr = element.getAttribute("data-font");
|
|
69
|
+
// Normalize font-family to ClickFunnels format (e.g., "\"Poppins\", sans-serif")
|
|
70
|
+
const fontFamily = fontAttr
|
|
71
|
+
? parseFontFamily(fontAttr)
|
|
72
|
+
: parseFontFamily(textStyles["font-family"]);
|
|
73
|
+
|
|
74
|
+
const colorAttr = element.getAttribute("data-color");
|
|
75
|
+
const color = colorAttr
|
|
76
|
+
? normalizeColor(colorAttr)
|
|
77
|
+
: normalizeColor(textStyles.color || "#000000");
|
|
78
|
+
|
|
79
|
+
const alignAttr = element.getAttribute("data-align");
|
|
80
|
+
const textAlign = alignAttr || parseTextAlign(textStyles["text-align"]);
|
|
81
|
+
|
|
82
|
+
const leadingAttr = element.getAttribute("data-leading");
|
|
83
|
+
const lineHeight = leadingAttr
|
|
84
|
+
? parseLineHeight(leadingAttr)
|
|
85
|
+
: parseLineHeight(textStyles["line-height"]);
|
|
86
|
+
|
|
87
|
+
const trackingAttr = element.getAttribute("data-tracking");
|
|
88
|
+
const letterSpacing = trackingAttr
|
|
89
|
+
? parseValueWithUnit(trackingAttr, "rem")
|
|
90
|
+
: parseValueWithUnit(textStyles["letter-spacing"], "rem");
|
|
91
|
+
|
|
92
|
+
const transformAttr = element.getAttribute("data-transform");
|
|
93
|
+
const textTransform = transformAttr || textStyles["text-transform"];
|
|
94
|
+
|
|
95
|
+
// Get icon attributes
|
|
96
|
+
const iconAttr = element.getAttribute("data-icon");
|
|
97
|
+
const iconAlignAttr = element.getAttribute("data-icon-align");
|
|
98
|
+
|
|
99
|
+
// Get link color - prefer data attribute, fallback to parsing anchor elements
|
|
100
|
+
const linkColorAttr = element.getAttribute("data-link-color");
|
|
101
|
+
let linkColor = null;
|
|
102
|
+
if (linkColorAttr) {
|
|
103
|
+
linkColor = normalizeColor(linkColorAttr);
|
|
104
|
+
} else {
|
|
105
|
+
// Try to find an anchor in the content and get its color
|
|
106
|
+
const anchor = textEl.querySelector("a");
|
|
107
|
+
if (anchor) {
|
|
108
|
+
const anchorStyles = parseInlineStyle(anchor.getAttribute("style") || "");
|
|
109
|
+
if (anchorStyles.color) {
|
|
110
|
+
linkColor = normalizeColor(anchorStyles.color);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Build the style object for the selector
|
|
116
|
+
const selectorStyle = {
|
|
117
|
+
"font-size": fontSize ? fontSize.value : 48,
|
|
118
|
+
"font-weight": fontWeight,
|
|
119
|
+
color: color,
|
|
120
|
+
"text-align": textAlign,
|
|
121
|
+
"line-height": lineHeight ? lineHeight.value : 140,
|
|
122
|
+
"letter-spacing": letterSpacing ? letterSpacing.value : 0,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Add font-family if present
|
|
126
|
+
if (fontFamily) {
|
|
127
|
+
selectorStyle["font-family"] = fontFamily;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Determine the style-guide-override param name based on type
|
|
131
|
+
const styleGuideOverrideMap = {
|
|
132
|
+
"Headline/V1": "style-guide-override-headline",
|
|
133
|
+
"SubHeadline/V1": "style-guide-override-subheadline",
|
|
134
|
+
"Paragraph/V1": "style-guide-override-content",
|
|
135
|
+
};
|
|
136
|
+
const styleGuideOverrideParam = styleGuideOverrideMap[type];
|
|
137
|
+
|
|
138
|
+
// Parse animation attributes
|
|
139
|
+
const { attrs: animationAttrs, params: animationParams } = parseAnimationAttrs(element);
|
|
140
|
+
|
|
141
|
+
const node = {
|
|
142
|
+
type,
|
|
143
|
+
id,
|
|
144
|
+
version: 0,
|
|
145
|
+
parentId,
|
|
146
|
+
fractionalIndex: generateFractionalIndex(index),
|
|
147
|
+
attrs: {
|
|
148
|
+
style: {},
|
|
149
|
+
...animationAttrs,
|
|
150
|
+
},
|
|
151
|
+
params: {
|
|
152
|
+
"padding-top--unit": "px",
|
|
153
|
+
"padding-bottom--unit": "px",
|
|
154
|
+
"--style-padding-horizontal--unit": "px",
|
|
155
|
+
"--style-padding-horizontal": 0,
|
|
156
|
+
...animationParams,
|
|
157
|
+
},
|
|
158
|
+
selectors: {
|
|
159
|
+
[selector]: {
|
|
160
|
+
attrs: {
|
|
161
|
+
style: selectorStyle,
|
|
162
|
+
},
|
|
163
|
+
params: {
|
|
164
|
+
[styleGuideOverrideParam]: true,
|
|
165
|
+
"font-size--unit": fontSize ? fontSize.unit : "px",
|
|
166
|
+
"line-height--unit": "%",
|
|
167
|
+
"letter-spacing--unit": "rem",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
children: [
|
|
172
|
+
{
|
|
173
|
+
type: "ContentEditableNode",
|
|
174
|
+
attrs: {
|
|
175
|
+
"data-align-selector": selector,
|
|
176
|
+
},
|
|
177
|
+
id: contentEditableId,
|
|
178
|
+
version: 0,
|
|
179
|
+
parentId: id,
|
|
180
|
+
fractionalIndex: "a0",
|
|
181
|
+
children: [],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Apply spacing to wrapper
|
|
187
|
+
const spacingWithDefaults = {
|
|
188
|
+
paddingTop: spacing.paddingTop || parseValueWithUnit("0px"),
|
|
189
|
+
paddingBottom: spacing.paddingBottom || parseValueWithUnit("0px"),
|
|
190
|
+
paddingHorizontal: spacing.paddingHorizontal || parseValueWithUnit("0px"),
|
|
191
|
+
marginTop: spacing.marginTop || parseValueWithUnit("0px"),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const { attrs: spacingAttrs, params: spacingParams } =
|
|
195
|
+
spacingToAttrsAndParams(spacingWithDefaults, true);
|
|
196
|
+
Object.assign(node.attrs.style, spacingAttrs.style);
|
|
197
|
+
Object.assign(node.params, spacingParams);
|
|
198
|
+
|
|
199
|
+
// Apply text-transform if present
|
|
200
|
+
if (textTransform) {
|
|
201
|
+
node.selectors[selector].attrs.style["text-transform"] = textTransform;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Apply link color to selector (for CSS cascade)
|
|
205
|
+
if (linkColor) {
|
|
206
|
+
node.selectors[`${selector} .elTypographyLink`] = {
|
|
207
|
+
attrs: {
|
|
208
|
+
style: {
|
|
209
|
+
color: linkColor,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Add icon attributes to params if present
|
|
216
|
+
if (iconAttr) {
|
|
217
|
+
node.params["icon"] = iconAttr;
|
|
218
|
+
}
|
|
219
|
+
if (iconAlignAttr) {
|
|
220
|
+
node.params["icon-align"] = iconAlignAttr;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Parse text content - pass linkColor for inline anchor styles
|
|
224
|
+
const textContent = textEl.innerHTML;
|
|
225
|
+
node.children[0].children = parseHtmlToTextNodes(textContent, linkColor);
|
|
226
|
+
|
|
227
|
+
// If no children were parsed, add simple text
|
|
228
|
+
if (node.children[0].children.length === 0) {
|
|
229
|
+
node.children[0].children.push({
|
|
230
|
+
type: "text",
|
|
231
|
+
innerText: textEl.textContent || "",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return node;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Parse Headline/V1
|
|
240
|
+
*/
|
|
241
|
+
export function parseHeadline(element, parentId, index) {
|
|
242
|
+
return parseTextElement(
|
|
243
|
+
element,
|
|
244
|
+
parentId,
|
|
245
|
+
index,
|
|
246
|
+
"Headline/V1",
|
|
247
|
+
".elHeadline",
|
|
248
|
+
"data-style-guide-headline"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Parse SubHeadline/V1
|
|
254
|
+
*/
|
|
255
|
+
export function parseSubHeadline(element, parentId, index) {
|
|
256
|
+
return parseTextElement(
|
|
257
|
+
element,
|
|
258
|
+
parentId,
|
|
259
|
+
index,
|
|
260
|
+
"SubHeadline/V1",
|
|
261
|
+
".elSubheadline",
|
|
262
|
+
"data-style-guide-subheadline"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse Paragraph/V1
|
|
268
|
+
*/
|
|
269
|
+
export function parseParagraph(element, parentId, index) {
|
|
270
|
+
return parseTextElement(
|
|
271
|
+
element,
|
|
272
|
+
parentId,
|
|
273
|
+
index,
|
|
274
|
+
"Paragraph/V1",
|
|
275
|
+
".elParagraph",
|
|
276
|
+
"data-style-guide-content"
|
|
277
|
+
);
|
|
278
|
+
}
|
package/src/styles.js
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* PAGETREE PARSER - Style Parsing Helpers
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Parse inline styles and convert to ClickFunnels pagetree format.
|
|
7
|
+
*
|
|
8
|
+
* ============================================================================
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { parseValueWithUnit, normalizeColor } from './utils.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Shadow presets mapping (from inline shadow to CF params)
|
|
15
|
+
*/
|
|
16
|
+
export const SHADOW_PRESETS = {
|
|
17
|
+
'none': null,
|
|
18
|
+
'0 1px 2px rgba(0,0,0,0.05)': {
|
|
19
|
+
x: 0, y: 1, blur: 2, spread: 0, color: 'rgba(0, 0, 0, 0.05)'
|
|
20
|
+
},
|
|
21
|
+
'0 1px 3px rgba(0,0,0,0.1)': {
|
|
22
|
+
x: 0, y: 1, blur: 3, spread: 0, color: 'rgba(0, 0, 0, 0.1)'
|
|
23
|
+
},
|
|
24
|
+
'0 4px 6px rgba(0,0,0,0.1)': {
|
|
25
|
+
x: 0, y: 4, blur: 6, spread: 0, color: 'rgba(0, 0, 0, 0.1)'
|
|
26
|
+
},
|
|
27
|
+
'0 10px 15px rgba(0,0,0,0.1)': {
|
|
28
|
+
x: 0, y: 10, blur: 15, spread: 0, color: 'rgba(0, 0, 0, 0.1)'
|
|
29
|
+
},
|
|
30
|
+
'0 20px 25px rgba(0,0,0,0.1)': {
|
|
31
|
+
x: 0, y: 20, blur: 25, spread: 0, color: 'rgba(0, 0, 0, 0.1)'
|
|
32
|
+
},
|
|
33
|
+
'0 25px 50px rgba(0,0,0,0.25)': {
|
|
34
|
+
x: 0, y: 25, blur: 50, spread: 0, color: 'rgba(0, 0, 0, 0.25)'
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse box-shadow value to CF params
|
|
40
|
+
*/
|
|
41
|
+
export function parseShadow(shadowValue) {
|
|
42
|
+
if (!shadowValue || shadowValue === 'none') return null;
|
|
43
|
+
|
|
44
|
+
// Check if it matches a preset
|
|
45
|
+
const normalized = shadowValue.replace(/\s+/g, ' ').trim();
|
|
46
|
+
if (SHADOW_PRESETS[normalized]) {
|
|
47
|
+
return SHADOW_PRESETS[normalized];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for inset keyword
|
|
51
|
+
const isInset = shadowValue.toLowerCase().includes('inset');
|
|
52
|
+
const shadowWithoutInset = shadowValue.replace(/inset\s*/gi, '').trim();
|
|
53
|
+
|
|
54
|
+
// Parse custom shadow: "0 4px 6px rgba(0,0,0,0.1)"
|
|
55
|
+
const match = shadowWithoutInset.match(
|
|
56
|
+
/(-?\d+(?:\.\d+)?(?:px)?)\s+(-?\d+(?:\.\d+)?(?:px)?)\s+(-?\d+(?:\.\d+)?(?:px)?)\s*(-?\d+(?:\.\d+)?(?:px)?)?\s*(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8})?/
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (match) {
|
|
60
|
+
return {
|
|
61
|
+
x: parseFloat(match[1]) || 0,
|
|
62
|
+
y: parseFloat(match[2]) || 0,
|
|
63
|
+
blur: parseFloat(match[3]) || 0,
|
|
64
|
+
spread: parseFloat(match[4]) || 0,
|
|
65
|
+
color: match[5] || 'rgba(0, 0, 0, 0.1)',
|
|
66
|
+
inset: isInset,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convert shadow params to CF pagetree format
|
|
75
|
+
*/
|
|
76
|
+
export function shadowToParams(shadow) {
|
|
77
|
+
if (!shadow) return {};
|
|
78
|
+
|
|
79
|
+
const params = {
|
|
80
|
+
'--style-box-shadow-distance-x': shadow.x,
|
|
81
|
+
'--style-box-shadow-distance-y': shadow.y,
|
|
82
|
+
'--style-box-shadow-blur': shadow.blur,
|
|
83
|
+
'--style-box-shadow-spread': shadow.spread,
|
|
84
|
+
'--style-box-shadow-color': shadow.color,
|
|
85
|
+
'--style-box-shadow-distance-x--unit': 'px',
|
|
86
|
+
'--style-box-shadow-distance-y--unit': 'px',
|
|
87
|
+
'--style-box-shadow-blur--unit': 'px',
|
|
88
|
+
'--style-box-shadow-spread--unit': 'px',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (shadow.inset) {
|
|
92
|
+
params['--style-box-shadow-style-type'] = 'inset';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return params;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse border properties from inline styles
|
|
100
|
+
*/
|
|
101
|
+
export function parseBorder(styles) {
|
|
102
|
+
const result = {
|
|
103
|
+
width: null,
|
|
104
|
+
style: null,
|
|
105
|
+
color: null,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Check for border shorthand
|
|
109
|
+
if (styles.border) {
|
|
110
|
+
const parts = styles.border.split(/\s+/);
|
|
111
|
+
for (const part of parts) {
|
|
112
|
+
if (part.match(/^\d/)) {
|
|
113
|
+
result.width = parseValueWithUnit(part);
|
|
114
|
+
} else if (['solid', 'dashed', 'dotted', 'none'].includes(part)) {
|
|
115
|
+
result.style = part;
|
|
116
|
+
} else {
|
|
117
|
+
result.color = normalizeColor(part);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check individual properties (override shorthand)
|
|
123
|
+
if (styles['border-width']) {
|
|
124
|
+
result.width = parseValueWithUnit(styles['border-width']);
|
|
125
|
+
}
|
|
126
|
+
if (styles['border-style']) {
|
|
127
|
+
result.style = styles['border-style'];
|
|
128
|
+
}
|
|
129
|
+
if (styles['border-color']) {
|
|
130
|
+
result.color = normalizeColor(styles['border-color']);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Convert border to CF params format
|
|
138
|
+
*/
|
|
139
|
+
export function borderToParams(border) {
|
|
140
|
+
if (!border.width && !border.style && !border.color) return {};
|
|
141
|
+
|
|
142
|
+
const params = {};
|
|
143
|
+
|
|
144
|
+
if (border.width) {
|
|
145
|
+
params['--style-border-width'] = border.width.value;
|
|
146
|
+
params['--style-border-width--unit'] = border.width.unit;
|
|
147
|
+
}
|
|
148
|
+
if (border.style) {
|
|
149
|
+
params['--style-border-style'] = border.style;
|
|
150
|
+
}
|
|
151
|
+
if (border.color) {
|
|
152
|
+
params['--style-border-color'] = border.color;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return params;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Parse spacing (padding/margin) from inline styles
|
|
160
|
+
*/
|
|
161
|
+
export function parseSpacing(styles) {
|
|
162
|
+
const result = {};
|
|
163
|
+
|
|
164
|
+
if (styles['padding-top']) {
|
|
165
|
+
result.paddingTop = parseValueWithUnit(styles['padding-top']);
|
|
166
|
+
}
|
|
167
|
+
if (styles['padding-bottom']) {
|
|
168
|
+
result.paddingBottom = parseValueWithUnit(styles['padding-bottom']);
|
|
169
|
+
}
|
|
170
|
+
if (styles['padding-left'] || styles['padding-right']) {
|
|
171
|
+
result.paddingHorizontal = parseValueWithUnit(
|
|
172
|
+
styles['padding-left'] || styles['padding-right']
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (styles['margin-top']) {
|
|
176
|
+
result.marginTop = parseValueWithUnit(styles['margin-top']);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Convert spacing to CF attrs/params format
|
|
184
|
+
*/
|
|
185
|
+
export function spacingToAttrsAndParams(spacing, forceDefaults = false) {
|
|
186
|
+
const attrs = { style: {} };
|
|
187
|
+
const params = {};
|
|
188
|
+
|
|
189
|
+
const paddingTop = spacing.paddingTop || (forceDefaults ? parseValueWithUnit('0px') : null);
|
|
190
|
+
const paddingBottom = spacing.paddingBottom || (forceDefaults ? parseValueWithUnit('0px') : null);
|
|
191
|
+
const paddingHorizontal = spacing.paddingHorizontal || (forceDefaults ? parseValueWithUnit('0px') : null);
|
|
192
|
+
const marginTop = spacing.marginTop || (forceDefaults ? parseValueWithUnit('0px') : null);
|
|
193
|
+
|
|
194
|
+
if (paddingTop) {
|
|
195
|
+
attrs.style['padding-top'] = paddingTop.value;
|
|
196
|
+
params['padding-top--unit'] = paddingTop.unit;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (paddingBottom) {
|
|
200
|
+
attrs.style['padding-bottom'] = paddingBottom.value;
|
|
201
|
+
params['padding-bottom--unit'] = paddingBottom.unit;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (paddingHorizontal) {
|
|
205
|
+
params['--style-padding-horizontal'] = paddingHorizontal.value;
|
|
206
|
+
params['--style-padding-horizontal--unit'] = paddingHorizontal.unit;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (marginTop) {
|
|
210
|
+
attrs.style['margin-top'] = marginTop.value;
|
|
211
|
+
params['margin-top--unit'] = marginTop.unit;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return { attrs, params };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Parse background from inline styles
|
|
219
|
+
*/
|
|
220
|
+
export function parseBackground(styles) {
|
|
221
|
+
const result = {
|
|
222
|
+
color: null,
|
|
223
|
+
imageUrl: null,
|
|
224
|
+
gradient: null,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
if (styles['background-color']) {
|
|
228
|
+
result.color = normalizeColor(styles['background-color']);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (styles.background) {
|
|
232
|
+
// Check if it's a gradient
|
|
233
|
+
if (styles.background.includes('gradient')) {
|
|
234
|
+
result.gradient = styles.background;
|
|
235
|
+
} else {
|
|
236
|
+
// Could be color
|
|
237
|
+
result.color = normalizeColor(styles.background);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (styles['background-image']) {
|
|
242
|
+
const urlMatch = styles['background-image'].match(/url\(['"]?([^'"]+)['"]?\)/);
|
|
243
|
+
if (urlMatch) {
|
|
244
|
+
result.imageUrl = urlMatch[1];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Convert background to CF params format
|
|
253
|
+
*
|
|
254
|
+
* NOTE: In ClickFunnels, gradients use --style-background-color (not --style-background-image)
|
|
255
|
+
* The gradient value like "linear-gradient(180deg, #6366f1 0%, #a855f7 100%)"
|
|
256
|
+
* goes directly into --style-background-color
|
|
257
|
+
*
|
|
258
|
+
* IMPORTANT: --style-background-image-url is always included (even as empty string)
|
|
259
|
+
* This is required for ClickFunnels to recognize background settings
|
|
260
|
+
*/
|
|
261
|
+
export function backgroundToParams(background) {
|
|
262
|
+
const params = {};
|
|
263
|
+
|
|
264
|
+
// Gradients take priority and use --style-background-color
|
|
265
|
+
if (background.gradient) {
|
|
266
|
+
params['--style-background-color'] = background.gradient;
|
|
267
|
+
} else if (background.color) {
|
|
268
|
+
params['--style-background-color'] = background.color;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Always include background-image-url (empty string if no image)
|
|
272
|
+
params['--style-background-image-url'] = background.imageUrl || '';
|
|
273
|
+
|
|
274
|
+
return params;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Parse border-radius from inline styles
|
|
279
|
+
*/
|
|
280
|
+
export function parseBorderRadius(styles) {
|
|
281
|
+
if (styles['border-radius']) {
|
|
282
|
+
return parseValueWithUnit(styles['border-radius']);
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Convert font-weight string to numeric value
|
|
289
|
+
*/
|
|
290
|
+
export function normalizeFontWeight(weight) {
|
|
291
|
+
const weightMap = {
|
|
292
|
+
thin: '100',
|
|
293
|
+
extralight: '200',
|
|
294
|
+
light: '300',
|
|
295
|
+
normal: '400',
|
|
296
|
+
medium: '500',
|
|
297
|
+
semibold: '600',
|
|
298
|
+
bold: '700',
|
|
299
|
+
extrabold: '800',
|
|
300
|
+
black: '900',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
if (weightMap[weight]) {
|
|
304
|
+
return weightMap[weight];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return weight;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Parse line-height and convert to percentage
|
|
312
|
+
*/
|
|
313
|
+
export function parseLineHeight(value) {
|
|
314
|
+
if (!value) return null;
|
|
315
|
+
|
|
316
|
+
// Already percentage
|
|
317
|
+
if (String(value).endsWith('%')) {
|
|
318
|
+
return {
|
|
319
|
+
value: parseFloat(value),
|
|
320
|
+
unit: '%'
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Decimal multiplier (e.g., 1.5 = 150%)
|
|
325
|
+
const num = parseFloat(value);
|
|
326
|
+
if (!isNaN(num) && num > 0 && num < 5) {
|
|
327
|
+
return {
|
|
328
|
+
value: Math.round(num * 100),
|
|
329
|
+
unit: '%'
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Named values
|
|
334
|
+
const lineHeightMap = {
|
|
335
|
+
none: 100,
|
|
336
|
+
tight: 110,
|
|
337
|
+
snug: 120,
|
|
338
|
+
normal: 140,
|
|
339
|
+
relaxed: 160,
|
|
340
|
+
loose: 180,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
if (lineHeightMap[value]) {
|
|
344
|
+
return {
|
|
345
|
+
value: lineHeightMap[value],
|
|
346
|
+
unit: '%'
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
value: parseFloat(value) || 140,
|
|
352
|
+
unit: '%'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Parse text-align value
|
|
358
|
+
*/
|
|
359
|
+
export function parseTextAlign(value) {
|
|
360
|
+
if (!value) return 'center'; // CF default
|
|
361
|
+
const valid = ['left', 'center', 'right'];
|
|
362
|
+
return valid.includes(value) ? value : 'center';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Parse flex direction
|
|
367
|
+
*/
|
|
368
|
+
export function parseFlexDirection(value) {
|
|
369
|
+
const map = {
|
|
370
|
+
row: 'row',
|
|
371
|
+
column: 'column',
|
|
372
|
+
'row-reverse': 'row-reverse',
|
|
373
|
+
'column-reverse': 'column-reverse',
|
|
374
|
+
};
|
|
375
|
+
return map[value] || 'row';
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Parse justify-content
|
|
380
|
+
*/
|
|
381
|
+
export function parseJustifyContent(value) {
|
|
382
|
+
const valid = [
|
|
383
|
+
'flex-start', 'center', 'flex-end',
|
|
384
|
+
'space-between', 'space-around', 'space-evenly'
|
|
385
|
+
];
|
|
386
|
+
return valid.includes(value) ? value : 'center';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Parse align-items
|
|
391
|
+
*/
|
|
392
|
+
export function parseAlignItems(value) {
|
|
393
|
+
const valid = ['flex-start', 'center', 'flex-end', 'stretch', 'baseline'];
|
|
394
|
+
return valid.includes(value) ? value : 'center';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Normalize font-family to ClickFunnels format
|
|
399
|
+
* ClickFunnels expects: "\"FontName\", sans-serif" (with escaped quotes around font name)
|
|
400
|
+
*
|
|
401
|
+
* Input formats handled:
|
|
402
|
+
* - "Poppins" -> "\"Poppins\", sans-serif"
|
|
403
|
+
* - "Noto Sans JP" -> "\"Noto Sans JP\", sans-serif"
|
|
404
|
+
* - "'Poppins', sans-serif" -> "\"Poppins\", sans-serif"
|
|
405
|
+
* - "\"Poppins\", sans-serif" -> "\"Poppins\", sans-serif" (already correct)
|
|
406
|
+
* - "Poppins, sans-serif" -> "\"Poppins\", sans-serif"
|
|
407
|
+
*/
|
|
408
|
+
export function normalizeFontFamily(fontFamily) {
|
|
409
|
+
if (!fontFamily) return null;
|
|
410
|
+
|
|
411
|
+
// Trim whitespace
|
|
412
|
+
let font = fontFamily.trim();
|
|
413
|
+
|
|
414
|
+
// If already in ClickFunnels format (starts with escaped quote or actual quote char for the font name)
|
|
415
|
+
if (font.startsWith('"') && font.includes(',')) {
|
|
416
|
+
// Already has quotes, return as-is
|
|
417
|
+
return font;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Remove surrounding single or double quotes if present
|
|
421
|
+
font = font.replace(/^['"]|['"]$/g, '');
|
|
422
|
+
|
|
423
|
+
// Check if it has a fallback already (contains comma)
|
|
424
|
+
if (font.includes(',')) {
|
|
425
|
+
// Extract the primary font name
|
|
426
|
+
const parts = font.split(',');
|
|
427
|
+
const primaryFont = parts[0].trim().replace(/^['"]|['"]$/g, '');
|
|
428
|
+
const fallback = parts.slice(1).map(p => p.trim()).join(', ') || 'sans-serif';
|
|
429
|
+
|
|
430
|
+
return `"${primaryFont}", ${fallback}`;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Just a font name, add quotes and fallback
|
|
434
|
+
return `"${font}", sans-serif`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Parse font-family from inline style value
|
|
439
|
+
* Returns the normalized ClickFunnels format
|
|
440
|
+
*/
|
|
441
|
+
export function parseFontFamily(value) {
|
|
442
|
+
if (!value) return null;
|
|
443
|
+
return normalizeFontFamily(value);
|
|
444
|
+
}
|