@rettangoli/fe 0.0.14 → 1.0.0-rc3

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 CHANGED
@@ -1,65 +1,25 @@
1
- import { render as jemplRender } from 'jempl';
1
+ import { parseAndRender as jemplParseAndRender, render as jemplRender } from "jempl";
2
2
 
3
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 }) => {
4
+ import { parseNodeBindings } from './core/view/bindings.js';
5
+ import {
6
+ createRefMatchers,
7
+ resolveBestRefMatcher,
8
+ validateElementIdForRefs,
9
+ } from './core/view/refs.js';
10
+ import {
11
+ createConfiguredEventListener,
12
+ getEventRateLimitState,
13
+ } from "./core/runtime/events.js";
14
+
15
+ export const parseView = ({
16
+ h,
17
+ template,
18
+ viewData,
19
+ refs,
20
+ handlers,
21
+ createComponentUpdateHook,
22
+ }) => {
63
23
  const result = jemplRender(template, viewData, {});
64
24
 
65
25
  // Flatten the array carefully to maintain structure
@@ -71,6 +31,7 @@ export const parseView = ({ h, template, viewData, refs, handlers }) => {
71
31
  refs,
72
32
  handlers,
73
33
  viewData,
34
+ createComponentUpdateHook,
74
35
  });
75
36
 
76
37
  const vdom = h("div", { style: { display: "contents" } }, childNodes);
@@ -91,13 +52,16 @@ export const createVirtualDom = ({
91
52
  items,
92
53
  refs = {},
93
54
  handlers = {},
94
- viewData = {}
55
+ viewData = {},
56
+ createComponentUpdateHook,
95
57
  }) => {
96
58
  if (!Array.isArray(items)) {
97
- console.error("Input to createVirtualDom must be an array.");
98
- return [h("div", {}, [])];
59
+ throw new Error("[Parser] Input to createVirtualDom must be an array, got " + typeof items);
99
60
  }
100
61
 
62
+ const refMatchers = createRefMatchers(refs);
63
+ const hasIdRefMatchers = refMatchers.some((refMatcher) => refMatcher.targetType === "id");
64
+
101
65
  function processItems(currentItems, parentPath = "") {
102
66
  return currentItems
103
67
  .map((item, index) => {
@@ -156,69 +120,20 @@ export const createVirtualDom = ({
156
120
  const tagName = selector.split(/[.#]/)[0];
157
121
  const isWebComponent = tagName.includes("-");
158
122
 
159
- // 1. Parse attributes from attrsString
160
- const attrs = {}; // Ensure attrs is always an object
161
- const props = {};
162
- if (attrsString) {
163
- // First, handle attributes with values
164
- const attrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
165
- let match;
166
- const processedAttrs = new Set();
167
-
168
- while ((match = attrRegex.exec(attrsString)) !== null) {
169
- processedAttrs.add(match[1]);
170
- if (match[1].startsWith(".")) {
171
- const propName = match[1].substring(1);
172
- const valuePathName = match[4];
173
- props[propName] = lodashGet(viewData, valuePathName);
174
- } else if (match[1].startsWith("?")) {
175
- // Handle conditional boolean attributes
176
- const attrName = match[1].substring(1);
177
- const attrValue = match[2] || match[3] || match[4];
178
-
179
- // Convert string values to boolean
180
- let evalValue;
181
- if (attrValue === "true") {
182
- evalValue = true;
183
- } else if (attrValue === "false") {
184
- evalValue = false;
185
- } else {
186
- // Try to get from viewData if it's not a literal boolean
187
- evalValue = lodashGet(viewData, attrValue);
188
- }
189
-
190
- // Only add attribute if value is truthy
191
- if (evalValue) {
192
- attrs[attrName] = "";
193
- }
194
- } else {
195
- attrs[match[1]] = match[2] || match[3] || match[4];
196
- }
197
- }
198
-
199
- // Then, handle boolean attributes without values
200
- // Remove all processed attribute-value pairs from the string first
201
- let remainingAttrsString = attrsString;
202
- const processedMatches = [];
203
- let tempMatch;
204
- const tempAttrRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]+))/g;
205
- while ((tempMatch = tempAttrRegex.exec(attrsString)) !== null) {
206
- processedMatches.push(tempMatch[0]);
207
- }
208
- // Remove all matched attribute=value pairs
209
- processedMatches.forEach(match => {
210
- remainingAttrsString = remainingAttrsString.replace(match, ' ');
211
- });
212
-
213
- const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
214
- let boolMatch;
215
- while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
216
- const attrName = boolMatch[1];
217
- // Skip if already processed or starts with . (prop) or contains =
218
- if (!processedAttrs.has(attrName) && !attrName.startsWith(".") && !attrName.includes("=")) {
219
- attrs[attrName] = "";
220
- }
221
- }
123
+ // 1. Parse selector bindings into attrs/props.
124
+ let attrs;
125
+ let props;
126
+ try {
127
+ ({ attrs, props } = parseNodeBindings({
128
+ attrsString,
129
+ viewData,
130
+ tagName,
131
+ isWebComponent,
132
+ }));
133
+ } catch (error) {
134
+ throw new Error(
135
+ `[Parser] Failed to parse bindings for selector '${selector}' with attrs '${attrsString}': ${error.message}`,
136
+ );
222
137
  }
223
138
 
224
139
  // 2. Handle ID from selector string (e.g., tag#id)
@@ -233,36 +148,33 @@ export const createVirtualDom = ({
233
148
  }
234
149
 
235
150
  // 3. Determine elementIdForRefs (this ID will be used for matching refs keys)
236
- // This should be the actual ID that will be on the DOM element.
151
+ // Only explicit IDs participate in id-based ref matching.
237
152
  let elementIdForRefs = null;
238
153
  if (attrs.id) {
239
- // Check the definitive id from attrs object
154
+ // Check the definitive id from attrs object.
240
155
  elementIdForRefs = attrs.id;
241
- } else if (isWebComponent) {
242
- // Fallback for web components that don't end up with an 'id' attribute
243
- elementIdForRefs = tagName;
244
156
  }
245
157
 
158
+ const selectorClassMatches = selector.match(/\.([^.#]+)/g) || [];
159
+ const selectorClassNames = selectorClassMatches.map((classMatch) => classMatch.substring(1));
160
+ const attributeClassNames = typeof attrs.class === "string"
161
+ ? attrs.class.split(/\s+/).filter(Boolean)
162
+ : [];
163
+ const classNamesForRefs = [...new Set([...selectorClassNames, ...attributeClassNames])];
164
+
246
165
  // Extract classes and ID from selector (if not a web component)
247
166
  const classObj = Object.create(null); // Using Object.create(null) to avoid prototype issues
248
167
  let elementId = null;
249
168
 
250
169
  if (!isWebComponent) {
251
- const classMatches = selector.match(/\.([^.#]+)/g);
252
- if (classMatches) {
253
- classMatches.forEach((classMatch) => {
254
- const className = classMatch.substring(1);
255
- classObj[className] = true;
256
- });
257
- }
170
+ selectorClassNames.forEach((className) => {
171
+ classObj[className] = true;
172
+ });
258
173
 
259
174
  const idMatch = selector.match(/#([^.#\s]+)/);
260
175
  if (idMatch) {
261
176
  elementId = idMatch[1];
262
177
  }
263
- } else {
264
- // For web components, use the tag name as the element ID for event binding
265
- elementId = tagName;
266
178
  }
267
179
 
268
180
  // Determine children or text content
@@ -283,78 +195,44 @@ export const createVirtualDom = ({
283
195
  // Apply event listeners
284
196
  const eventHandlers = Object.create(null);
285
197
 
286
- if (elementIdForRefs && refs) {
287
- const matchingRefKeys = [];
288
- Object.keys(refs).forEach((refKey) => {
289
- if (refKey.includes("*")) {
290
- const pattern =
291
- "^" +
292
- refKey
293
- .replace(/[.*+?^${}()|[\\\]\\]/g, "\\$&")
294
- .replace(/\\\*/g, ".*") +
295
- "$";
296
- try {
297
- const regex = new RegExp(pattern);
298
- if (regex.test(elementIdForRefs)) {
299
- matchingRefKeys.push(refKey);
300
- }
301
- } catch (e) {
302
- // Keep this warning for invalid regex patterns
303
- console.warn(
304
- `[Parser] Invalid regex pattern created from refKey '${refKey}': ${pattern}`,
305
- e,
306
- );
307
- }
308
- } else {
309
- if (elementIdForRefs === refKey) {
310
- matchingRefKeys.push(refKey);
311
- }
312
- }
198
+ if (refMatchers.length > 0) {
199
+ if (hasIdRefMatchers && elementIdForRefs) {
200
+ validateElementIdForRefs(elementIdForRefs);
201
+ }
202
+ const bestMatchRef = resolveBestRefMatcher({
203
+ elementIdForRefs,
204
+ classNames: classNamesForRefs,
205
+ refMatchers,
313
206
  });
314
207
 
315
- if (matchingRefKeys.length > 0) {
316
- matchingRefKeys.sort((a, b) => {
317
- const aIsExact = !a.includes("*");
318
- const bIsExact = !b.includes("*");
319
- if (aIsExact && !bIsExact) return -1;
320
- if (!aIsExact && bIsExact) return 1;
321
- return b.length - a.length;
322
- });
323
-
324
- const bestMatchRefKey = matchingRefKeys[0];
208
+ if (bestMatchRef) {
209
+ const bestMatchRefKey = bestMatchRef.refKey;
210
+ const matchIdentity = bestMatchRef.matchedValue || elementIdForRefs || bestMatchRefKey;
325
211
 
326
- if (refs[bestMatchRefKey] && refs[bestMatchRefKey].eventListeners) {
327
- const eventListeners = refs[bestMatchRefKey].eventListeners;
212
+ if (bestMatchRef.refConfig && bestMatchRef.refConfig.eventListeners) {
213
+ const eventListeners = bestMatchRef.refConfig.eventListeners;
214
+ const eventRateLimitState = getEventRateLimitState(handlers);
328
215
  Object.entries(eventListeners).forEach(
329
216
  ([eventType, eventConfig]) => {
330
- if (eventConfig.handler && eventConfig.action) {
331
- throw new Error('Each listener can have hanlder or action but not both')
332
- }
333
-
334
- if (eventConfig.action) {
335
- eventHandlers[eventType] = (event) => {
336
- handlers.handleCallStoreAction({
337
- ...eventConfig.payload,
338
- _event: event,
339
- _action: eventConfig.action,
340
- })
341
- }
217
+ const stateKey = `${bestMatchRefKey}:${matchIdentity}:${eventType}`;
218
+ const listener = createConfiguredEventListener({
219
+ eventType,
220
+ eventConfig,
221
+ refKey: bestMatchRefKey,
222
+ handlers,
223
+ eventRateLimitState,
224
+ stateKey,
225
+ parseAndRenderFn: jemplParseAndRender,
226
+ onMissingHandler: (missingHandlerName) => {
227
+ console.warn(
228
+ `[Parser] Handler '${missingHandlerName}' for refKey '${bestMatchRefKey}' (matching '${matchIdentity}') is referenced but not found in available handlers.`,
229
+ );
230
+ },
231
+ });
232
+ if (!listener) {
342
233
  return;
343
234
  }
344
-
345
- if (eventConfig.handler && handlers[eventConfig.handler]) {
346
- eventHandlers[eventType] = (event) => {
347
- handlers[eventConfig.handler]({
348
- ...eventConfig.payload,
349
- _event: event,
350
- });
351
- };
352
- } else if (eventConfig.handler) {
353
- // Keep this warning for missing handlers
354
- console.warn(
355
- `[Parser] Handler '${eventConfig.handler}' for refKey '${bestMatchRefKey}' (matching elementId '${elementIdForRefs}') is referenced but not found in available handlers.`,
356
- );
357
- }
235
+ eventHandlers[eventType] = listener;
358
236
  },
359
237
  );
360
238
  }
@@ -374,10 +252,10 @@ export const createVirtualDom = ({
374
252
  : String(index);
375
253
  snabbdomData.key = `${selector}-${itemPath}`;
376
254
 
377
- // Include props in key if they exist for better change detection
378
- if (Object.keys(props).length > 0) {
379
- const propsHash = JSON.stringify(props).substring(0, 50); // Limit length
380
- snabbdomData.key += `-${propsHash}`;
255
+ // Include prop keys in key for better change detection
256
+ const propKeys = Object.keys(props);
257
+ if (propKeys.length > 0) {
258
+ snabbdomData.key += `-p:${propKeys.join(",")}`;
381
259
  }
382
260
  }
383
261
 
@@ -396,77 +274,23 @@ export const createVirtualDom = ({
396
274
  snabbdomData.props = props;
397
275
  }
398
276
 
399
- // For web components, add a hook to detect prop and attr changes
400
- if (isWebComponent) {
401
- snabbdomData.hook = {
402
- update: (oldVnode, vnode) => {
403
- const oldProps = oldVnode.data?.props || {};
404
- const newProps = vnode.data?.props || {};
405
- const oldAttrs = oldVnode.data?.attrs || {};
406
- const newAttrs = vnode.data?.attrs || {};
407
-
408
- // Check if props have changed
409
- const propsChanged =
410
- JSON.stringify(oldProps) !== JSON.stringify(newProps);
411
-
412
- // Check if attrs have changed
413
- const attrsChanged =
414
- JSON.stringify(oldAttrs) !== JSON.stringify(newAttrs);
415
-
416
- if (propsChanged || attrsChanged) {
417
- // Set isDirty attribute and trigger re-render
418
- const element = vnode.elm;
419
- if (
420
- element &&
421
- element.render &&
422
- typeof element.render === "function"
423
- ) {
424
- element.setAttribute("isDirty", "true");
425
- requestAnimationFrame(() => {
426
- element.render();
427
- element.removeAttribute("isDirty");
428
- // Call the specific component's handleOnUpdate instead of the parent's onUpdate
429
- if (element.handlers && element.handlers.handleOnUpdate) {
430
- const deps = {
431
- ...(element.deps),
432
- store: element.store,
433
- render: element.render.bind(element),
434
- handlers: element.handlers,
435
- dispatchEvent: element.dispatchEvent.bind(element),
436
- refIds: element.refIds || {},
437
- getRefIds: () => element.refIds || {},
438
- };
439
- element.handlers.handleOnUpdate(deps, {
440
- oldProps,
441
- newProps,
442
- oldAttrs,
443
- newAttrs,
444
- });
445
- }
446
- });
447
- }
448
- }
449
- },
450
- };
277
+ // Hook behavior is injected so parser core stays environment-agnostic.
278
+ if (isWebComponent && typeof createComponentUpdateHook === "function") {
279
+ const componentHook = createComponentUpdateHook({
280
+ selector,
281
+ tagName,
282
+ });
283
+ if (componentHook) {
284
+ snabbdomData.hook = componentHook;
285
+ }
451
286
  }
452
287
 
453
288
  try {
454
- // For web components, use only the tag name without any selectors
455
- if (isWebComponent) {
456
- // For web components, we need to use just the tag name
457
- return h(tagName, snabbdomData, childrenOrText);
458
- } else {
459
- // For regular elements, we can use the original selector or just the tag
460
- return h(tagName, snabbdomData, childrenOrText);
461
- }
289
+ return h(tagName, snabbdomData, childrenOrText);
462
290
  } catch (error) {
463
- console.error("Error creating virtual node:", error, {
464
- tagName,
465
- snabbdomData,
466
- childrenOrText,
467
- });
468
- // Fallback to a simple div
469
- return h("div", {}, ["Error creating element"]);
291
+ throw new Error(
292
+ `[Parser] Error creating virtual node for '${tagName}': ${error.message}`,
293
+ );
470
294
  }
471
295
  })
472
296
  .filter(Boolean);
@@ -0,0 +1,49 @@
1
+ const COMMON_LINK_STYLE_TEXT = `
2
+ a, a:link, a:visited, a:hover, a:active {
3
+ display: contents;
4
+ color: inherit;
5
+ text-decoration: none;
6
+ background: none;
7
+ border: none;
8
+ padding: 0;
9
+ margin: 0;
10
+ font: inherit;
11
+ cursor: pointer;
12
+ }
13
+ `;
14
+
15
+ export const initializeComponentDom = ({
16
+ host,
17
+ cssText,
18
+ createStyleSheet = () => new CSSStyleSheet(),
19
+ createElement = (tagName) => document.createElement(tagName),
20
+ }) => {
21
+ const shadow = host.attachShadow({ mode: "open" });
22
+
23
+ const commonStyleSheet = createStyleSheet();
24
+ commonStyleSheet.replaceSync(COMMON_LINK_STYLE_TEXT);
25
+
26
+ const adoptedStyleSheets = [commonStyleSheet];
27
+
28
+ if (cssText) {
29
+ const styleSheet = createStyleSheet();
30
+ styleSheet.replaceSync(cssText);
31
+ adoptedStyleSheets.push(styleSheet);
32
+ }
33
+
34
+ shadow.adoptedStyleSheets = adoptedStyleSheets;
35
+
36
+ const renderTarget = createElement("div");
37
+ renderTarget.style.cssText = "display: contents;";
38
+ shadow.appendChild(renderTarget);
39
+ if (!renderTarget.parentNode) {
40
+ host.appendChild(renderTarget);
41
+ }
42
+ host.style.display = "contents";
43
+
44
+ return {
45
+ shadow,
46
+ renderTarget,
47
+ adoptedStyleSheets,
48
+ };
49
+ };
@@ -0,0 +1,43 @@
1
+ import { scheduleFrame } from "./scheduler.js";
2
+
3
+ export const createWebComponentUpdateHook = ({
4
+ scheduleFrameFn = scheduleFrame,
5
+ } = {}) => {
6
+ return {
7
+ update: (oldVnode, vnode) => {
8
+ const oldProps = oldVnode.data?.props || {};
9
+ const newProps = vnode.data?.props || {};
10
+
11
+ const propsChanged = JSON.stringify(oldProps) !== JSON.stringify(newProps);
12
+ if (!propsChanged) {
13
+ return;
14
+ }
15
+
16
+ const element = vnode.elm;
17
+ if (!element || typeof element.render !== "function") {
18
+ return;
19
+ }
20
+
21
+ element.setAttribute("isDirty", "true");
22
+ scheduleFrameFn(() => {
23
+ element.render();
24
+ element.removeAttribute("isDirty");
25
+
26
+ if (element.handlers && element.handlers.handleOnUpdate) {
27
+ const deps = {
28
+ ...(element.deps),
29
+ store: element.store,
30
+ render: element.render.bind(element),
31
+ handlers: element.handlers,
32
+ dispatchEvent: element.dispatchEvent.bind(element),
33
+ refs: element.refIds || {},
34
+ };
35
+ element.handlers.handleOnUpdate(deps, {
36
+ oldProps,
37
+ newProps,
38
+ });
39
+ }
40
+ });
41
+ },
42
+ };
43
+ };