@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/README.md +324 -0
- package/package.json +37 -0
- package/src/common.js +203 -0
- package/src/commonBuild.js +22 -0
- package/src/createComponent.js +486 -0
- package/src/createWebPatch.js +18 -0
- package/src/index.js +7 -0
- package/src/parser.js +399 -0
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
|
+
};
|