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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - Parser Registry
4
+ * ============================================================================
5
+ *
6
+ * Exports all parsers from individual modules.
7
+ *
8
+ * ============================================================================
9
+ */
10
+
11
+ // Layout parsers
12
+ export {
13
+ parseContentNode,
14
+ parseSectionContainer,
15
+ parseRowContainer,
16
+ parseColContainer,
17
+ parseColInner,
18
+ parseFlexContainer,
19
+ } from './layout.js';
20
+
21
+ // Text parsers
22
+ export {
23
+ parseHeadline,
24
+ parseSubHeadline,
25
+ parseParagraph,
26
+ } from './text.js';
27
+
28
+ // Button parser
29
+ export { parseButton } from './button.js';
30
+
31
+ // Media parsers
32
+ export {
33
+ parseImage,
34
+ parseIcon,
35
+ parseVideo,
36
+ parseDivider,
37
+ } from './media.js';
38
+
39
+ // Form parsers
40
+ export {
41
+ parseInput,
42
+ parseTextArea,
43
+ parseSelectBox,
44
+ parseCheckbox,
45
+ } from './form.js';
46
+
47
+ // List parser
48
+ export { parseBulletList } from './list.js';
49
+
50
+ // Interactive parsers
51
+ export {
52
+ parseProgressBar,
53
+ parseVideoPopup,
54
+ parseCountdown,
55
+ } from './interactive.js';
56
+
57
+ // Placeholder parsers
58
+ export {
59
+ parseCheckoutPlaceholder,
60
+ parseOrderSummaryPlaceholder,
61
+ parseConfirmationPlaceholder,
62
+ } from './placeholders.js';
63
+
64
+ // Popup parser
65
+ export { parseModalContainer } from './popup.js';
@@ -0,0 +1,484 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - Interactive Element Parsers
4
+ * ============================================================================
5
+ *
6
+ * ProgressBar/V1, VideoPopup/V1, Countdown/V1
7
+ *
8
+ * ============================================================================
9
+ */
10
+
11
+ import {
12
+ generateId,
13
+ generateFractionalIndex,
14
+ parseInlineStyle,
15
+ parseValueWithUnit,
16
+ normalizeColor,
17
+ } from '../utils.js';
18
+
19
+ import {
20
+ parseSpacing,
21
+ spacingToAttrsAndParams,
22
+ parseBorderRadius,
23
+ parseBorder,
24
+ borderToParams,
25
+ parseShadow,
26
+ shadowToParams,
27
+ parseTextAlign,
28
+ } from '../styles.js';
29
+
30
+ /**
31
+ * Parse ProgressBar/V1
32
+ */
33
+ export function parseProgressBar(element, parentId, index) {
34
+ const id = generateId();
35
+
36
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
37
+ const spacing = parseSpacing(wrapperStyles);
38
+
39
+ // Read from data attributes (most reliable)
40
+ const progress = parseInt(element.getAttribute('data-progress') || '50', 10);
41
+ const progressText = element.getAttribute('data-text') || '';
42
+ const showTextOutside = element.getAttribute('data-text-outside') === 'true';
43
+ const align = element.getAttribute('data-align') || wrapperStyles['text-align'] || 'center';
44
+
45
+ // Find the progress track
46
+ const track = element.querySelector('.progress');
47
+ const trackStyles = track ? parseInlineStyle(track.getAttribute('style') || '') : {};
48
+
49
+ // Find the progress bar (fill)
50
+ const bar = element.querySelector('.progress-bar');
51
+ const barStyles = bar ? parseInlineStyle(bar.getAttribute('style') || '') : {};
52
+
53
+ // Find the label
54
+ const label = element.querySelector('.progress-label');
55
+ const labelStyles = label ? parseInlineStyle(label.getAttribute('style') || '') : {};
56
+
57
+ // Width comes from data-width attribute or wrapper styles
58
+ const widthAttr = element.getAttribute('data-width');
59
+ const width = parseValueWithUnit(widthAttr || wrapperStyles.width || '100%', '%');
60
+ const height = parseValueWithUnit(trackStyles.height || element.getAttribute('data-height') || '24px', 'px');
61
+ const borderRadius = parseBorderRadius(trackStyles);
62
+ const border = parseBorder(trackStyles);
63
+ const shadow = parseShadow(trackStyles['box-shadow']);
64
+
65
+ const bgColor = normalizeColor(trackStyles['background-color'] || element.getAttribute('data-bg') || '#e2e8f0');
66
+ const fillColor = normalizeColor(barStyles['background-color'] || element.getAttribute('data-fill') || '#3b82f6');
67
+ const labelColor = normalizeColor(labelStyles.color || '#ffffff');
68
+
69
+ // Map alignment to text-align for label
70
+ const textAlign = align || 'center';
71
+
72
+ const node = {
73
+ type: 'ProgressBar/V1',
74
+ id,
75
+ version: 0,
76
+ parentId,
77
+ fractionalIndex: generateFractionalIndex(index),
78
+ attrs: {
79
+ style: {},
80
+ },
81
+ params: {
82
+ progress,
83
+ 'progress-text': progressText,
84
+ 'show_text_outside': showTextOutside ? 'true' : 'false',
85
+ },
86
+ selectors: {
87
+ '.progress': {
88
+ attrs: {
89
+ style: {
90
+ width: width ? width.value : 100,
91
+ 'border-radius': borderRadius ? borderRadius.value : 9999,
92
+ },
93
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
94
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
95
+ },
96
+ params: {
97
+ '--style-background-color': bgColor,
98
+ 'border-radius--unit': borderRadius ? borderRadius.unit : 'px',
99
+ 'width--unit': width ? width.unit : '%',
100
+ },
101
+ },
102
+ '.progress-bar': {
103
+ attrs: {
104
+ style: {
105
+ height: height ? height.value : 24,
106
+ },
107
+ },
108
+ params: {
109
+ '--style-background-color': fillColor,
110
+ 'height--unit': height ? height.unit : 'px',
111
+ },
112
+ },
113
+ '.progress-label': {
114
+ attrs: {
115
+ style: {
116
+ 'font-size': 16,
117
+ 'text-align': textAlign,
118
+ },
119
+ },
120
+ params: {
121
+ 'font-size--unit': 'px',
122
+ },
123
+ },
124
+ '& > .progress-label': {
125
+ attrs: {
126
+ style: {
127
+ color: labelColor,
128
+ },
129
+ },
130
+ },
131
+ },
132
+ };
133
+
134
+ // Apply spacing
135
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
136
+ Object.assign(node.attrs.style, spacingAttrs.style);
137
+ Object.assign(node.params, spacingParams);
138
+
139
+ // Apply border
140
+ if (border.width || border.style || border.color) {
141
+ Object.assign(node.selectors['.progress'].params, borderToParams(border));
142
+ }
143
+
144
+ // Apply shadow
145
+ if (shadow) {
146
+ Object.assign(node.selectors['.progress'].params, shadowToParams(shadow));
147
+ }
148
+
149
+ return node;
150
+ }
151
+
152
+ /**
153
+ * Parse VideoPopup/V1
154
+ */
155
+ export function parseVideoPopup(element, parentId, index) {
156
+ const id = generateId();
157
+
158
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
159
+ const spacing = parseSpacing(wrapperStyles);
160
+ const textAlign = parseTextAlign(wrapperStyles['text-align']);
161
+
162
+ // Read from data attributes
163
+ const videoUrl = element.getAttribute('data-video-url') || '';
164
+ const videoType = element.getAttribute('data-video-type') || 'youtube';
165
+ const thumbnail = element.getAttribute('data-thumbnail') || '';
166
+ const overlayBg = element.getAttribute('data-overlay-bg') || 'rgba(0,0,0,0.8)';
167
+
168
+ // Find the image element
169
+ const img = element.querySelector('.elImage');
170
+ const imgStyles = img ? parseInlineStyle(img.getAttribute('style') || '') : {};
171
+ const alt = img ? img.getAttribute('alt') : '';
172
+
173
+ // Find image wrapper for alignment and width
174
+ const imageWrapper = element.querySelector('.elImageWrapper');
175
+ const imageWrapperStyles = imageWrapper ? parseInlineStyle(imageWrapper.getAttribute('style') || '') : {};
176
+
177
+ // Width comes from data-width attribute or imageWrapper styles
178
+ const widthAttr = element.getAttribute('data-width');
179
+ const width = parseValueWithUnit(widthAttr || imageWrapperStyles.width || '100%', '%');
180
+ const borderRadius = parseBorderRadius(imgStyles);
181
+ const border = parseBorder(imgStyles);
182
+ const shadow = parseShadow(imgStyles['box-shadow']);
183
+
184
+ // Extract video ID for thumbnail URL
185
+ const videoIdMatch = videoUrl.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\s?]+)/);
186
+ const videoId = videoIdMatch ? videoIdMatch[1] : '';
187
+ const thumbnailUrl = thumbnail || (videoId ? `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg` : '');
188
+
189
+ const node = {
190
+ type: 'VideoPopup/V1',
191
+ id,
192
+ version: 0,
193
+ parentId,
194
+ fractionalIndex: generateFractionalIndex(index),
195
+ attrs: {
196
+ alt: alt || 'Video thumbnail',
197
+ },
198
+ params: {
199
+ imageUrl: [{ type: 'text', innerText: thumbnailUrl }],
200
+ },
201
+ selectors: {
202
+ '.elImage': {
203
+ attrs: {
204
+ alt,
205
+ style: {
206
+ width: width ? width.value : 100,
207
+ 'border-radius': borderRadius ? borderRadius.value : 16,
208
+ },
209
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
210
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
211
+ },
212
+ params: {
213
+ 'width--unit': width ? width.unit : '%',
214
+ 'border-radius--unit': borderRadius ? borderRadius.unit : 'px',
215
+ },
216
+ },
217
+ '.elImageWrapper': {
218
+ attrs: {
219
+ style: {
220
+ 'text-align': textAlign || 'center',
221
+ },
222
+ },
223
+ },
224
+ '.elVideoWrapper': {
225
+ attrs: {
226
+ 'data-video-type': videoType,
227
+ 'data-video-title': alt,
228
+ },
229
+ params: {
230
+ video_url: videoUrl,
231
+ },
232
+ },
233
+ '.elVideoWrapper .elVideoplaceholder_inner': {
234
+ attrs: {
235
+ className: 'bgCoverCenter',
236
+ },
237
+ params: {
238
+ '--style-background-image-url': thumbnailUrl,
239
+ },
240
+ },
241
+ '.elModal': {
242
+ params: {
243
+ '--style-background-color': overlayBg,
244
+ },
245
+ },
246
+ },
247
+ };
248
+
249
+ // Apply spacing
250
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
251
+ Object.assign(node.attrs.style || (node.attrs.style = {}), spacingAttrs.style);
252
+ Object.assign(node.params, spacingParams);
253
+
254
+ // Apply border to image
255
+ if (border.width || border.style || border.color) {
256
+ Object.assign(node.selectors['.elImage'].params, borderToParams(border));
257
+ }
258
+
259
+ // Apply shadow to image
260
+ if (shadow) {
261
+ Object.assign(node.selectors['.elImage'].params, shadowToParams(shadow));
262
+ }
263
+
264
+ return node;
265
+ }
266
+
267
+ /**
268
+ * Parse Countdown/V1
269
+ */
270
+ export function parseCountdown(element, parentId, index) {
271
+ const id = generateId();
272
+
273
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
274
+ const spacing = parseSpacing(wrapperStyles);
275
+
276
+ // Read from data attributes
277
+ const endDate = element.getAttribute('data-end-date') || '';
278
+ const endTime = element.getAttribute('data-end-time') || '00:00:00';
279
+ const timezone = element.getAttribute('data-timezone') || 'America/New_York';
280
+ const showDays = element.getAttribute('data-show-days') !== 'false';
281
+ const showHours = element.getAttribute('data-show-hours') !== 'false';
282
+ const showMinutes = element.getAttribute('data-show-minutes') !== 'false';
283
+ const showSeconds = element.getAttribute('data-show-seconds') !== 'false';
284
+ const redirect = element.getAttribute('data-redirect') || '';
285
+ const numberBg = normalizeColor(element.getAttribute('data-number-bg') || '#1C65E1');
286
+ const numberColor = normalizeColor(element.getAttribute('data-number-color') || '#ffffff');
287
+ const labelColor = normalizeColor(element.getAttribute('data-label-color') || '#164EAD');
288
+
289
+ // Find countdown row for gap
290
+ const countdownRow = element.querySelector('.elCountdownRow');
291
+ const rowStyles = countdownRow ? parseInlineStyle(countdownRow.getAttribute('style') || '') : {};
292
+ const gap = parseValueWithUnit(rowStyles.gap || '0.65em', 'em');
293
+
294
+ // Find amount container for styling
295
+ const amountContainer = element.querySelector('.elCountdownAmountContainer');
296
+ const containerStyles = amountContainer ? parseInlineStyle(amountContainer.getAttribute('style') || '') : {};
297
+ const borderRadius = parseBorderRadius(containerStyles);
298
+
299
+ // Parse shadow from inline styles or data attribute
300
+ let shadow = parseShadow(containerStyles['box-shadow']);
301
+ if (!shadow) {
302
+ const shadowAttr = element.getAttribute('data-shadow');
303
+ if (shadowAttr) {
304
+ // Resolve preset names to actual shadow values
305
+ const SHADOW_VALUES = {
306
+ 'sm': '0 1px 2px rgba(0,0,0,0.05)',
307
+ 'md': '0 4px 6px rgba(0,0,0,0.1)',
308
+ 'lg': '0 10px 15px rgba(0,0,0,0.1)',
309
+ 'xl': '0 20px 25px rgba(0,0,0,0.1)',
310
+ '2xl': '0 25px 50px rgba(0,0,0,0.25)',
311
+ };
312
+ shadow = parseShadow(SHADOW_VALUES[shadowAttr] || shadowAttr);
313
+ }
314
+ }
315
+
316
+ // Parse border from inline styles or data attributes
317
+ let border = parseBorder(containerStyles);
318
+ if (!border.width && !border.style && !border.color) {
319
+ const borderAttr = element.getAttribute('data-border');
320
+ const borderColorAttr = element.getAttribute('data-border-color');
321
+ if (borderAttr) {
322
+ border = {
323
+ width: parseValueWithUnit(borderAttr.includes('px') ? borderAttr : `${borderAttr}px`),
324
+ style: 'solid',
325
+ color: normalizeColor(borderColorAttr || numberBg),
326
+ };
327
+ }
328
+ }
329
+
330
+ // Find amount text for font styling
331
+ const amountText = element.querySelector('.elCountdownAmount');
332
+ const amountStyles = amountText ? parseInlineStyle(amountText.getAttribute('style') || '') : {};
333
+ const numberSize = parseValueWithUnit(amountStyles['font-size'] || '28px', 'px');
334
+
335
+ // Find period text for font styling
336
+ const periodText = element.querySelector('.elCountdownPeriod');
337
+ const periodStyles = periodText ? parseInlineStyle(periodText.getAttribute('style') || '') : {};
338
+ const labelSize = parseValueWithUnit(periodStyles['font-size'] || '11px', 'px');
339
+
340
+ const node = {
341
+ type: 'Countdown/V1',
342
+ id,
343
+ version: 0,
344
+ parentId,
345
+ fractionalIndex: generateFractionalIndex(index),
346
+ params: {
347
+ type: 'countdown',
348
+ countdown_opts: {
349
+ show_years: false,
350
+ show_months: false,
351
+ show_weeks: false,
352
+ show_days: showDays,
353
+ show_hours: showHours,
354
+ show_minutes: showMinutes,
355
+ show_seconds: showSeconds,
356
+ },
357
+ show_colons: false,
358
+ timezone,
359
+ timer_action: redirect ? 'redirect_to' : 'none',
360
+ cookie_policy: 'none',
361
+ expire_days: 0,
362
+ countdownTexts: {
363
+ years: 'Years',
364
+ months: 'Months',
365
+ weeks: 'Weeks',
366
+ days: 'Days',
367
+ hours: 'Hours',
368
+ minutes: 'Minutes',
369
+ seconds: 'Seconds',
370
+ },
371
+ end_date: endDate,
372
+ countdown_id: id,
373
+ end_time: endTime,
374
+ redirect_to: redirect,
375
+ },
376
+ selectors: {
377
+ '.elCountdownAmount': {
378
+ attrs: {
379
+ style: {
380
+ color: numberColor,
381
+ 'font-size': numberSize ? `${numberSize.value}px` : '28px',
382
+ 'font-weight': '700',
383
+ 'line-height': '100%',
384
+ },
385
+ },
386
+ },
387
+ '.elCountdownPeriod': {
388
+ attrs: {
389
+ style: {
390
+ 'text-transform': 'uppercase',
391
+ color: labelColor,
392
+ 'text-align': 'center',
393
+ 'font-size': labelSize ? `${labelSize.value}px` : '11px',
394
+ 'font-weight': '600',
395
+ },
396
+ },
397
+ },
398
+ '.elCountdownRow': {
399
+ attrs: {
400
+ style: {
401
+ gap: gap ? gap.value : 0.65,
402
+ 'flex-direction': 'row',
403
+ },
404
+ },
405
+ params: {
406
+ 'gap--unit': gap ? gap.unit : 'em',
407
+ },
408
+ },
409
+ '.elCountdownGroupDate': {
410
+ attrs: {
411
+ style: {
412
+ gap: 0.78,
413
+ },
414
+ },
415
+ params: {
416
+ 'gap--unit': 'em',
417
+ },
418
+ },
419
+ '.elCountdownGroupTime': {
420
+ attrs: {
421
+ style: {
422
+ gap: '1.1em',
423
+ },
424
+ },
425
+ params: {
426
+ 'gap--unit': 'em',
427
+ },
428
+ },
429
+ '.elCountdownColumn': {
430
+ attrs: {
431
+ style: {
432
+ gap: '0.5em',
433
+ 'flex-direction': 'column',
434
+ 'border-style': 'none',
435
+ 'padding-top': 0,
436
+ 'padding-bottom': 0,
437
+ },
438
+ 'data-skip-shadow-settings': 'true',
439
+ },
440
+ params: {
441
+ 'gap--unit': 'em',
442
+ '--style-padding-horizontal': 0,
443
+ },
444
+ },
445
+ '.elCountdownAmountContainer': {
446
+ attrs: {
447
+ style: {
448
+ 'line-height': '100%',
449
+ 'padding-top': 14,
450
+ 'padding-bottom': 14,
451
+ 'border-radius': borderRadius ? borderRadius.value : 16,
452
+ },
453
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
454
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
455
+ },
456
+ params: {
457
+ '--style-background-color': numberBg,
458
+ '--style-padding-horizontal': 14,
459
+ '--style-padding-horizontal--unit': 'px',
460
+ 'padding-top--unit': 'px',
461
+ 'padding-bottom--unit': 'px',
462
+ 'border-radius--unit': borderRadius ? borderRadius.unit : 'px',
463
+ },
464
+ },
465
+ },
466
+ };
467
+
468
+ // Apply spacing
469
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
470
+ Object.assign(node.attrs || (node.attrs = {}), { style: spacingAttrs.style });
471
+ Object.assign(node.params, spacingParams);
472
+
473
+ // Apply border to amount container
474
+ if (border.width || border.style || border.color) {
475
+ Object.assign(node.selectors['.elCountdownAmountContainer'].params, borderToParams(border));
476
+ }
477
+
478
+ // Apply shadow to amount container
479
+ if (shadow) {
480
+ Object.assign(node.selectors['.elCountdownAmountContainer'].params, shadowToParams(shadow));
481
+ }
482
+
483
+ return node;
484
+ }