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,359 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - List Element Parser
4
+ * ============================================================================
5
+ *
6
+ * BulletList/V1
7
+ *
8
+ * ============================================================================
9
+ */
10
+
11
+ import {
12
+ generateId,
13
+ generateFractionalIndex,
14
+ parseInlineStyle,
15
+ parseValueWithUnit,
16
+ normalizeColor,
17
+ sanitizeHtml,
18
+ } from '../utils.js';
19
+
20
+ import {
21
+ parseSpacing,
22
+ spacingToAttrsAndParams,
23
+ } from '../styles.js';
24
+
25
+ /**
26
+ * Parse BulletList/V1
27
+ */
28
+ export function parseBulletList(element, parentId, index) {
29
+ const id = generateId();
30
+ const contentEditableId = generateId();
31
+
32
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
33
+ const spacing = parseSpacing(wrapperStyles);
34
+
35
+ // Find the ul element
36
+ const ul = element.querySelector('ul');
37
+ const ulStyles = ul ? parseInlineStyle(ul.getAttribute('style') || '') : {};
38
+
39
+ // Get item gap from ul's gap style
40
+ const itemGap = parseValueWithUnit(ulStyles.gap || '8px');
41
+
42
+ // Read data attributes first (most reliable)
43
+ const sizeResolvedAttr = element.getAttribute('data-size-resolved'); // Pre-resolved pixel value
44
+ const textSizeAttr = element.getAttribute('data-text-size'); // Legacy: direct pixel value
45
+ const iconSizeAttr = element.getAttribute('data-icon-size');
46
+ const textColorAttr = element.getAttribute('data-text-color');
47
+ const iconColorAttr = element.getAttribute('data-icon-color');
48
+ const iconAttr = element.getAttribute('data-icon');
49
+ const gapAttr = element.getAttribute('data-gap');
50
+
51
+ // Find list items
52
+ const items = ul ? ul.querySelectorAll('li') : [];
53
+
54
+ // Initialize with defaults, will be overridden by data attrs or inline styles
55
+ let iconClass = 'fas fa-check fa_icon';
56
+ let iconColor = '#10b981';
57
+ let iconMarginRight = 12;
58
+ let iconSize = null;
59
+ let textColor = '#334155';
60
+ let textSize = null;
61
+ let justifyContent = 'flex-start';
62
+
63
+ // Parse from inline styles as fallback
64
+ if (items.length > 0) {
65
+ const firstItem = items[0];
66
+ const icon = firstItem.querySelector('i');
67
+ const textSpan = firstItem.querySelector('span');
68
+
69
+ // Get alignment from li styles
70
+ const liStyles = parseInlineStyle(firstItem.getAttribute('style') || '');
71
+ if (liStyles['justify-content']) {
72
+ justifyContent = liStyles['justify-content'];
73
+ }
74
+
75
+ if (icon) {
76
+ const rawClass = icon.getAttribute('class') || 'fas fa-check';
77
+ // Ensure fa_icon is in the class (only if not from data attr)
78
+ if (!iconAttr) {
79
+ iconClass = rawClass.includes('fa_icon') ? rawClass : `${rawClass} fa_icon`;
80
+ }
81
+ const iconStyles = parseInlineStyle(icon.getAttribute('style') || '');
82
+ // Only use inline style if no data attr
83
+ if (!iconColorAttr) {
84
+ iconColor = normalizeColor(iconStyles.color) || iconColor;
85
+ }
86
+ const marginRight = parseValueWithUnit(iconStyles['margin-right']);
87
+ if (marginRight) iconMarginRight = marginRight.value;
88
+ // Get icon font size from inline style if not from data attr
89
+ if (!iconSizeAttr) {
90
+ const iconFontSize = parseValueWithUnit(iconStyles['font-size']);
91
+ if (iconFontSize) iconSize = iconFontSize;
92
+ }
93
+ }
94
+
95
+ if (textSpan) {
96
+ const textStyles = parseInlineStyle(textSpan.getAttribute('style') || '');
97
+ // Only use inline style if no data attr
98
+ if (!textColorAttr) {
99
+ textColor = normalizeColor(textStyles.color) || textColor;
100
+ }
101
+ // Get text font size from inline style if not from data attr
102
+ if (!textSizeAttr) {
103
+ const textFontSize = parseValueWithUnit(textStyles['font-size']);
104
+ if (textFontSize) textSize = textFontSize;
105
+ }
106
+ }
107
+ }
108
+
109
+ // Apply data attributes (override inline styles)
110
+ if (iconAttr) {
111
+ iconClass = iconAttr.includes('fa_icon') ? iconAttr : `${iconAttr} fa_icon`;
112
+ }
113
+ if (iconColorAttr) {
114
+ iconColor = normalizeColor(iconColorAttr);
115
+ }
116
+ if (textColorAttr) {
117
+ textColor = normalizeColor(textColorAttr);
118
+ }
119
+ // Priority: data-size-resolved > data-size (as px) > data-text-size (legacy)
120
+ if (sizeResolvedAttr) {
121
+ const parsed = parseValueWithUnit(sizeResolvedAttr);
122
+ if (parsed) textSize = parsed;
123
+ } else if (textSizeAttr) {
124
+ const parsed = parseValueWithUnit(textSizeAttr);
125
+ if (parsed) textSize = parsed;
126
+ }
127
+ if (iconSizeAttr) {
128
+ const parsed = parseValueWithUnit(iconSizeAttr);
129
+ if (parsed) iconSize = parsed;
130
+ }
131
+ if (gapAttr) {
132
+ const parsed = parseValueWithUnit(gapAttr);
133
+ if (parsed) iconMarginRight = parsed.value;
134
+ }
135
+
136
+ // Get link color - prefer data attribute, fallback to parsing anchor elements
137
+ const linkColorAttr = element.getAttribute('data-link-color');
138
+ let linkColor = null;
139
+ if (linkColorAttr) {
140
+ linkColor = normalizeColor(linkColorAttr);
141
+ } else if (ul) {
142
+ // Try to find an anchor in any list item
143
+ const anchor = ul.querySelector('a');
144
+ if (anchor) {
145
+ const anchorStyles = parseInlineStyle(anchor.getAttribute('style') || '');
146
+ if (anchorStyles.color) {
147
+ linkColor = normalizeColor(anchorStyles.color);
148
+ }
149
+ }
150
+ }
151
+
152
+ const node = {
153
+ type: 'BulletList/V1',
154
+ id,
155
+ version: 0,
156
+ parentId,
157
+ fractionalIndex: generateFractionalIndex(index),
158
+ attrs: {
159
+ style: {},
160
+ },
161
+ params: {
162
+ '--style-padding-horizontal--unit': 'px',
163
+ '--style-padding-horizontal': 0,
164
+ 'margin-top--unit': 'px',
165
+ },
166
+ selectors: {
167
+ '.elBulletList': {
168
+ attrs: {
169
+ 'data-style-guide-content': 'm',
170
+ 'data-skip-text-shadow-settings': 'true',
171
+ style: {
172
+ color: textColor,
173
+ ...(textSize ? { 'font-size': textSize.value } : {}),
174
+ },
175
+ },
176
+ params: {
177
+ ...(textSize ? { 'font-size--unit': textSize.unit } : {}),
178
+ },
179
+ },
180
+ '.elBulletList li:not(:first-child)': {
181
+ attrs: {
182
+ style: {
183
+ 'margin-top': itemGap ? itemGap.value : 15,
184
+ },
185
+ },
186
+ params: {
187
+ 'margin-top--unit': itemGap ? itemGap.unit : 'px',
188
+ },
189
+ },
190
+ '.elBulletList .fa_icon': {
191
+ attrs: {
192
+ style: {
193
+ 'margin-right': iconMarginRight,
194
+ ...(iconSize ? { 'font-size': iconSize.value } : {}),
195
+ },
196
+ },
197
+ params: {
198
+ 'margin-right--unit': 'px',
199
+ ...(iconSize ? { 'font-size--unit': iconSize.unit } : {}),
200
+ },
201
+ },
202
+ '.elBulletList .fa,\n.elBulletList .fas,\n.elBulletList .fa-fw': {
203
+ attrs: {
204
+ style: {
205
+ color: iconColor,
206
+ },
207
+ },
208
+ },
209
+ '.elBulletList li': {
210
+ attrs: {
211
+ style: {
212
+ 'justify-content': justifyContent,
213
+ },
214
+ },
215
+ },
216
+ },
217
+ children: [
218
+ {
219
+ type: 'ContentEditableNode',
220
+ attrs: {
221
+ 'data-align-selector': '.elBulletList li',
222
+ },
223
+ id: contentEditableId,
224
+ version: 0,
225
+ parentId: id,
226
+ fractionalIndex: 'a0',
227
+ children: [],
228
+ },
229
+ ],
230
+ };
231
+
232
+ // Apply spacing
233
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
234
+ Object.assign(node.attrs.style, spacingAttrs.style);
235
+ Object.assign(node.params, spacingParams);
236
+
237
+ // Apply link color if present
238
+ if (linkColor) {
239
+ node.selectors['.elBulletList .elTypographyLink'] = {
240
+ attrs: {
241
+ style: {
242
+ color: linkColor,
243
+ },
244
+ },
245
+ };
246
+ }
247
+
248
+ // Parse list items into proper structure
249
+ let itemIndex = 0;
250
+ items.forEach((item) => {
251
+ const liId = generateId();
252
+ const iconNodeId = generateId();
253
+ const spanWrapperId = generateId();
254
+
255
+ const textSpan = item.querySelector('span');
256
+ const text = textSpan ? sanitizeHtml(textSpan.innerHTML) : item.textContent;
257
+
258
+ // Build the li children
259
+ const liChildren = [];
260
+
261
+ // IconNode
262
+ liChildren.push({
263
+ type: 'IconNode',
264
+ attrs: {
265
+ className: iconClass,
266
+ contenteditable: 'false',
267
+ },
268
+ id: iconNodeId,
269
+ version: 0,
270
+ parentId: liId,
271
+ fractionalIndex: 'a0',
272
+ });
273
+
274
+ // Text span wrapper
275
+ const spanChildren = [];
276
+
277
+ // Parse the text content - handle bold tags
278
+ if (textSpan) {
279
+ const boldEl = textSpan.querySelector('b, strong');
280
+ if (boldEl) {
281
+ const boldId = generateId();
282
+ const boldTextId = generateId();
283
+ spanChildren.push({
284
+ type: 'b',
285
+ id: boldId,
286
+ version: 0,
287
+ parentId: spanWrapperId,
288
+ fractionalIndex: `a${spanChildren.length}`,
289
+ children: [
290
+ {
291
+ innerText: boldEl.textContent,
292
+ type: 'text',
293
+ id: boldTextId,
294
+ version: 0,
295
+ parentId: boldId,
296
+ fractionalIndex: 'a0',
297
+ },
298
+ ],
299
+ });
300
+ // Get text after bold
301
+ const afterBold = sanitizeHtml(textSpan.innerHTML.replace(boldEl.outerHTML, '')).trim();
302
+ if (afterBold) {
303
+ spanChildren.push({
304
+ innerText: afterBold,
305
+ type: 'text',
306
+ id: generateId(),
307
+ version: 0,
308
+ parentId: spanWrapperId,
309
+ fractionalIndex: `a${spanChildren.length}`,
310
+ });
311
+ }
312
+ } else {
313
+ // Plain text
314
+ spanChildren.push({
315
+ innerText: text,
316
+ type: 'text',
317
+ id: generateId(),
318
+ version: 0,
319
+ parentId: spanWrapperId,
320
+ fractionalIndex: 'a0',
321
+ });
322
+ }
323
+ } else {
324
+ spanChildren.push({
325
+ innerText: text,
326
+ type: 'text',
327
+ id: generateId(),
328
+ version: 0,
329
+ parentId: spanWrapperId,
330
+ fractionalIndex: 'a0',
331
+ });
332
+ }
333
+
334
+ liChildren.push({
335
+ type: 'span',
336
+ attrs: {
337
+ className: 'elBulletListTextWrapper',
338
+ },
339
+ id: spanWrapperId,
340
+ version: 0,
341
+ parentId: liId,
342
+ fractionalIndex: 'a1',
343
+ children: spanChildren,
344
+ });
345
+
346
+ node.children[0].children.push({
347
+ type: 'li',
348
+ id: liId,
349
+ version: 0,
350
+ parentId: contentEditableId,
351
+ fractionalIndex: `a${itemIndex}`,
352
+ children: liChildren,
353
+ });
354
+
355
+ itemIndex++;
356
+ });
357
+
358
+ return node;
359
+ }