@rettangoli/fe 0.0.3

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/src/parser.js ADDED
@@ -0,0 +1,399 @@
1
+ import { render as jemplRender } from 'jempl';
2
+
3
+ import { flattenArrays } from './common.js';
4
+
5
+ const lodashGet = (obj, path) => {
6
+ if (!path) return obj;
7
+
8
+ // Parse path to handle both dot notation and bracket notation
9
+ const parts = [];
10
+ let current = '';
11
+ let inBrackets = false;
12
+ let quoteChar = null;
13
+
14
+ for (let i = 0; i < path.length; i++) {
15
+ const char = path[i];
16
+
17
+ if (!inBrackets && char === '.') {
18
+ if (current) {
19
+ parts.push(current);
20
+ current = '';
21
+ }
22
+ } else if (!inBrackets && char === '[') {
23
+ if (current) {
24
+ parts.push(current);
25
+ current = '';
26
+ }
27
+ inBrackets = true;
28
+ } else if (inBrackets && char === ']') {
29
+ if (current) {
30
+ // Remove quotes if present and add the key
31
+ if ((current.startsWith('"') && current.endsWith('"')) ||
32
+ (current.startsWith("'") && current.endsWith("'"))) {
33
+ parts.push(current.slice(1, -1));
34
+ } else {
35
+ // Numeric index or unquoted string
36
+ const numValue = Number(current);
37
+ parts.push(isNaN(numValue) ? current : numValue);
38
+ }
39
+ current = '';
40
+ }
41
+ inBrackets = false;
42
+ quoteChar = null;
43
+ } else if (inBrackets && (char === '"' || char === "'")) {
44
+ if (!quoteChar) {
45
+ quoteChar = char;
46
+ } else if (char === quoteChar) {
47
+ quoteChar = null;
48
+ }
49
+ current += char;
50
+ } else {
51
+ current += char;
52
+ }
53
+ }
54
+
55
+ if (current) {
56
+ parts.push(current);
57
+ }
58
+
59
+ return parts.reduce((acc, part) => acc && acc[part], obj);
60
+ };
61
+
62
+ export const parseView = ({ h, template, viewData, refs, handlers }) => {
63
+ // const startTime = performance.now();
64
+ const result = jemplRender({
65
+ ast: template,
66
+ data: viewData,
67
+ });
68
+ // const endTime = performance.now();
69
+ // const executionTime = endTime - startTime;
70
+ // console.log(`jemplRender execution time: ${executionTime.toFixed(2)}ms`);
71
+
72
+ // Flatten the array carefully to maintain structure
73
+ const flattenedResult = flattenArrays(result);
74
+
75
+ const childNodes = createVirtualDom({
76
+ h,
77
+ items: flattenedResult,
78
+ refs,
79
+ handlers,
80
+ viewData,
81
+ });
82
+
83
+ const vdom = h("div", { style: { display: "contents" } }, childNodes);
84
+ return vdom;
85
+ };
86
+
87
+ /**
88
+ *
89
+ * @param {Object} params
90
+ * @param {Array} params.items
91
+ * @param {Object} params.refs
92
+ * @param {Object} params.handlers
93
+ * @param {Object} params.viewData
94
+ * @returns
95
+ */
96
+ export const createVirtualDom = ({
97
+ h,
98
+ items,
99
+ refs = {},
100
+ handlers = {},
101
+ viewData = {},
102
+ }) => {
103
+ if (!Array.isArray(items)) {
104
+ console.error("Input to createVirtualDom must be an array.");
105
+ return [h("div", {}, [])];
106
+ }
107
+
108
+ function processItems(currentItems, parentPath = "") {
109
+ return currentItems
110
+ .map((item, index) => {
111
+ // Handle text nodes
112
+ if (typeof item === "string" || typeof item === "number") {
113
+ return String(item);
114
+ }
115
+
116
+ if (typeof item !== "object" || item === null) {
117
+ console.warn("Skipping invalid item in DOM structure:", item);
118
+ return null;
119
+ }
120
+
121
+ const entries = Object.entries(item);
122
+ if (entries.length === 0) {
123
+ console.warn("Skipping empty object item:", item);
124
+ return null;
125
+ }
126
+
127
+ const [keyString, value] = entries[0];
128
+
129
+ // Skip numeric keys that might come from array indices
130
+ if (!isNaN(Number(keyString))) {
131
+ if (Array.isArray(value)) {
132
+ return processItems(value, `${parentPath}.${keyString}`);
133
+ } else if (typeof value === "object" && value !== null) {
134
+ const nestedEntries = Object.entries(value);
135
+ if (nestedEntries.length > 0) {
136
+ return processItems([value], `${parentPath}.${keyString}`);
137
+ }
138
+ }
139
+ return String(value);
140
+ }
141
+
142
+ if (entries.length > 1) {
143
+ console.warn(
144
+ "Item has multiple keys, processing only the first:",
145
+ keyString,
146
+ );
147
+ }
148
+
149
+ // Parse keyString into selector and attributes
150
+ let selector;
151
+ let attrsString;
152
+ const firstSpaceIndex = keyString.indexOf(" ");
153
+
154
+ if (firstSpaceIndex === -1) {
155
+ selector = keyString;
156
+ attrsString = "";
157
+ } else {
158
+ selector = keyString.substring(0, firstSpaceIndex);
159
+ attrsString = keyString.substring(firstSpaceIndex + 1).trim();
160
+ }
161
+
162
+ // Handle web components (tags with hyphens)
163
+ const tagName = selector.split(/[.#]/)[0];
164
+ const isWebComponent = tagName.includes("-");
165
+
166
+ // 1. Parse attributes from attrsString
167
+ const attrs = {}; // Ensure attrs is always an object
168
+ const props = {};
169
+ if (attrsString) {
170
+ const attrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|(\S+))/g;
171
+ let match;
172
+ while ((match = attrRegex.exec(attrsString)) !== null) {
173
+ if (match[1].startsWith(".")) {
174
+ const propName = match[1].substring(1);
175
+ const valuePathName = match[4];
176
+ props[propName] = lodashGet(viewData, valuePathName);
177
+ } else {
178
+ attrs[match[1]] = match[2] || match[3] || match[4];
179
+ }
180
+ }
181
+ }
182
+
183
+ // 2. Handle ID from selector string (e.g., tag#id)
184
+ // If an 'id' was already parsed from attrsString (e.g. id=value), it takes precedence.
185
+ // Otherwise, use the id from the selector string.
186
+ const idMatchInSelector = selector.match(/#([^.#\s]+)/);
187
+ if (
188
+ idMatchInSelector &&
189
+ !Object.prototype.hasOwnProperty.call(attrs, "id")
190
+ ) {
191
+ attrs.id = idMatchInSelector[1];
192
+ }
193
+
194
+ // 3. Determine elementIdForRefs (this ID will be used for matching refs keys)
195
+ // This should be the actual ID that will be on the DOM element.
196
+ let elementIdForRefs = null;
197
+ if (attrs.id) {
198
+ // Check the definitive id from attrs object
199
+ elementIdForRefs = attrs.id;
200
+ } else if (isWebComponent) {
201
+ // Fallback for web components that don't end up with an 'id' attribute
202
+ elementIdForRefs = tagName;
203
+ }
204
+
205
+ // Extract classes and ID from selector (if not a web component)
206
+ const classObj = Object.create(null); // Using Object.create(null) to avoid prototype issues
207
+ let elementId = null;
208
+
209
+ if (!isWebComponent) {
210
+ const classMatches = selector.match(/\.([^.#]+)/g);
211
+ if (classMatches) {
212
+ classMatches.forEach((classMatch) => {
213
+ const className = classMatch.substring(1);
214
+ classObj[className] = true;
215
+ });
216
+ }
217
+
218
+ const idMatch = selector.match(/#([^.#\s]+)/);
219
+ if (idMatch) {
220
+ elementId = idMatch[1];
221
+ }
222
+ } else {
223
+ // For web components, use the tag name as the element ID for event binding
224
+ elementId = tagName;
225
+ }
226
+
227
+ // Determine children or text content
228
+ let childrenOrText;
229
+ if (typeof value === "string" || typeof value === "number") {
230
+ childrenOrText = String(value);
231
+ } else if (Array.isArray(value)) {
232
+ childrenOrText = processItems(value, `${parentPath}.${keyString}`);
233
+ } else {
234
+ childrenOrText = [];
235
+ }
236
+
237
+ // Add id to attributes if it exists and it's not a web component
238
+ if (elementId && !isWebComponent) {
239
+ attrs.id = elementId;
240
+ }
241
+
242
+ // Apply event listeners
243
+ const eventHandlers = Object.create(null);
244
+
245
+ if (elementIdForRefs && refs) {
246
+ const matchingRefKeys = [];
247
+ Object.keys(refs).forEach((refKey) => {
248
+ if (refKey.includes("*")) {
249
+ const pattern =
250
+ "^" +
251
+ refKey
252
+ .replace(/[.*+?^${}()|[\\\]\\]/g, "\\$&")
253
+ .replace(/\\\*/g, ".*") +
254
+ "$";
255
+ try {
256
+ const regex = new RegExp(pattern);
257
+ if (regex.test(elementIdForRefs)) {
258
+ matchingRefKeys.push(refKey);
259
+ }
260
+ } catch (e) {
261
+ // Keep this warning for invalid regex patterns
262
+ console.warn(
263
+ `[Parser] Invalid regex pattern created from refKey '${refKey}': ${pattern}`,
264
+ e,
265
+ );
266
+ }
267
+ } else {
268
+ if (elementIdForRefs === refKey) {
269
+ matchingRefKeys.push(refKey);
270
+ }
271
+ }
272
+ });
273
+
274
+ if (matchingRefKeys.length > 0) {
275
+ matchingRefKeys.sort((a, b) => {
276
+ const aIsExact = !a.includes("*");
277
+ const bIsExact = !b.includes("*");
278
+ if (aIsExact && !bIsExact) return -1;
279
+ if (!aIsExact && bIsExact) return 1;
280
+ return b.length - a.length;
281
+ });
282
+
283
+ const bestMatchRefKey = matchingRefKeys[0];
284
+
285
+ if (refs[bestMatchRefKey] && refs[bestMatchRefKey].eventListeners) {
286
+ const eventListeners = refs[bestMatchRefKey].eventListeners;
287
+ Object.entries(eventListeners).forEach(
288
+ ([eventType, eventConfig]) => {
289
+ if (eventConfig.handler && handlers[eventConfig.handler]) {
290
+ eventHandlers[eventType] = (event) => {
291
+ handlers[eventConfig.handler](event);
292
+ };
293
+ } else if (eventConfig.handler) {
294
+ // Keep this warning for missing handlers
295
+ console.warn(
296
+ `[Parser] Handler '${eventConfig.handler}' for refKey '${bestMatchRefKey}' (matching elementId '${elementIdForRefs}') is referenced but not found in available handlers.`,
297
+ );
298
+ }
299
+ },
300
+ );
301
+ }
302
+ }
303
+ }
304
+
305
+ // Create proper Snabbdom data object
306
+ const snabbdomData = {};
307
+
308
+ // Add key for better virtual DOM diffing
309
+ if (elementIdForRefs) {
310
+ snabbdomData.key = elementIdForRefs;
311
+ } else if (selector) {
312
+ // Generate a key based on selector, parent path, and index for list items
313
+ const itemPath = parentPath
314
+ ? `${parentPath}.${index}`
315
+ : String(index);
316
+ snabbdomData.key = `${selector}-${itemPath}`;
317
+
318
+ // Include props in key if they exist for better change detection
319
+ if (Object.keys(props).length > 0) {
320
+ const propsHash = JSON.stringify(props).substring(0, 50); // Limit length
321
+ snabbdomData.key += `-${propsHash}`;
322
+ }
323
+ }
324
+
325
+ if (Object.keys(attrs).length > 0) {
326
+ // This `attrs` object now correctly contains the intended 'id'
327
+ snabbdomData.attrs = attrs;
328
+ }
329
+ if (Object.keys(classObj).length > 0) {
330
+ // Ensure classObj is defined earlier
331
+ snabbdomData.class = classObj;
332
+ }
333
+ if (Object.keys(eventHandlers).length > 0) {
334
+ snabbdomData.on = eventHandlers;
335
+ }
336
+ if (Object.keys(props).length > 0) {
337
+ snabbdomData.props = props;
338
+
339
+ // For web components, add a hook to detect prop changes and set isDirty
340
+ if (isWebComponent) {
341
+ snabbdomData.hook = {
342
+ update: (oldVnode, vnode) => {
343
+ const oldProps = oldVnode.data?.props || {};
344
+ const newProps = vnode.data?.props || {};
345
+ const oldAttrs = oldVnode.data?.attrs || {};
346
+ const newAttrs = vnode.data?.attrs || {};
347
+
348
+ // Check if props have changed
349
+ const propsChanged =
350
+ JSON.stringify(oldProps) !== JSON.stringify(newProps);
351
+
352
+ // Check if attrs have changed
353
+ const attrsChanged =
354
+ JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs);
355
+
356
+ if (propsChanged || attrsChanged) {
357
+ // Set isDirty attribute and trigger re-render
358
+ const element = vnode.elm;
359
+ if (
360
+ element &&
361
+ element.render &&
362
+ typeof element.render === "function"
363
+ ) {
364
+ element.setAttribute("isDirty", "true");
365
+ requestAnimationFrame(() => {
366
+ element.render();
367
+ element.removeAttribute("isDirty");
368
+ });
369
+ }
370
+ }
371
+ },
372
+ };
373
+ }
374
+ }
375
+
376
+ try {
377
+ // For web components, use only the tag name without any selectors
378
+ if (isWebComponent) {
379
+ // For web components, we need to use just the tag name
380
+ return h(tagName, snabbdomData, childrenOrText);
381
+ } else {
382
+ // For regular elements, we can use the original selector or just the tag
383
+ return h(tagName, snabbdomData, childrenOrText);
384
+ }
385
+ } catch (error) {
386
+ console.error("Error creating virtual node:", error, {
387
+ tagName,
388
+ snabbdomData,
389
+ childrenOrText,
390
+ });
391
+ // Fallback to a simple div
392
+ return h("div", {}, ["Error creating element"]);
393
+ }
394
+ })
395
+ .filter(Boolean);
396
+ }
397
+
398
+ return processItems(items);
399
+ };