@rettangoli/fe 1.0.0-rc4 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rettangoli/fe",
3
- "version": "1.0.0-rc4",
3
+ "version": "1.0.0",
4
4
  "description": "Frontend framework for building reactive web components",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -1,6 +1,9 @@
1
1
  const PROP_PREFIX = ":";
2
2
 
3
3
  const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
4
+ const ATTRIBUTE_NAME_REGEX = /^[A-Za-z_][A-Za-z0-9_.:-]*$/;
5
+
6
+ const isValidAttributeName = (value) => ATTRIBUTE_NAME_REGEX.test(value);
4
7
 
5
8
  const lodashGet = (obj, path) => {
6
9
  if (!path) return obj;
@@ -73,7 +76,7 @@ export const collectBindingNames = (attrsString = "") => {
73
76
  }
74
77
 
75
78
  const attrAssignmentRegex = /(\S+?)=(?:\"([^\"]*)\"|\'([^\']*)\'|([^\s]*))/g;
76
- const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
79
+ const booleanAttrRegex = /(\S+?)(?=\s|$)/g;
77
80
  const processedAttrs = new Set();
78
81
  const bindingNames = [];
79
82
  let match;
@@ -97,14 +100,20 @@ export const collectBindingNames = (attrsString = "") => {
97
100
 
98
101
  let boolMatch;
99
102
  while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
100
- const attrName = boolMatch[1];
101
- if (attrName.startsWith(".")) {
103
+ const rawToken = boolMatch[1];
104
+ if (rawToken.startsWith(".")) {
105
+ continue;
106
+ }
107
+ const attrName = rawToken.startsWith("?")
108
+ ? rawToken.substring(1)
109
+ : rawToken;
110
+ if (!attrName || !isValidAttributeName(attrName)) {
102
111
  continue;
103
112
  }
104
113
  if (
105
- !processedAttrs.has(attrName)
106
- && !attrName.startsWith(PROP_PREFIX)
107
- && !attrName.includes("=")
114
+ !processedAttrs.has(rawToken)
115
+ && !rawToken.startsWith(PROP_PREFIX)
116
+ && !rawToken.includes("=")
108
117
  ) {
109
118
  bindingNames.push(attrName);
110
119
  }
@@ -146,6 +155,14 @@ export const parseNodeBindings = ({
146
155
  props[normalizedPropName] = propValue;
147
156
  };
148
157
 
158
+ const assertValidAttributeName = (attrName, sourceLabel) => {
159
+ if (!isValidAttributeName(attrName)) {
160
+ throw new Error(
161
+ `[Parser] Invalid ${sourceLabel} attribute name '${attrName}' on '${tagName}'.`,
162
+ );
163
+ }
164
+ };
165
+
149
166
  if (!attrsString) {
150
167
  return { attrs, props };
151
168
  }
@@ -180,6 +197,7 @@ export const parseNodeBindings = ({
180
197
 
181
198
  if (rawBindingName.startsWith("?")) {
182
199
  const attrName = rawBindingName.substring(1);
200
+ assertValidAttributeName(attrName, "boolean toggle");
183
201
  const attrValue = rawValue;
184
202
  assertSupportedBooleanToggleAttr(attrName);
185
203
 
@@ -203,6 +221,7 @@ export const parseNodeBindings = ({
203
221
  continue;
204
222
  }
205
223
 
224
+ assertValidAttributeName(rawBindingName, "binding");
206
225
  attrs[rawBindingName] = rawValue;
207
226
  if (isWebComponent && rawBindingName !== "id") {
208
227
  setComponentProp(rawBindingName, rawValue, "attribute-form");
@@ -221,21 +240,42 @@ export const parseNodeBindings = ({
221
240
  remainingAttrsString = remainingAttrsString.replace(processedMatch, " ");
222
241
  });
223
242
 
224
- const booleanAttrRegex = /\b(\S+?)(?=\s|$)/g;
243
+ const booleanAttrRegex = /(\S+?)(?=\s|$)/g;
225
244
  let boolMatch;
226
245
  while ((boolMatch = booleanAttrRegex.exec(remainingAttrsString)) !== null) {
227
- const attrName = boolMatch[1];
228
- if (attrName.startsWith(".")) {
246
+ const rawToken = boolMatch[1];
247
+ if (rawToken.startsWith(".")) {
248
+ continue;
249
+ }
250
+ if (processedAttrs.has(rawToken) || rawToken.startsWith(PROP_PREFIX) || rawToken.includes("=")) {
229
251
  continue;
230
252
  }
253
+
254
+ if (rawToken.startsWith("?")) {
255
+ const toggleAttrName = rawToken.substring(1);
256
+ assertValidAttributeName(toggleAttrName, "boolean toggle");
257
+ assertSupportedBooleanToggleAttr(toggleAttrName);
258
+ attrs[toggleAttrName] = "";
259
+ if (isWebComponent && toggleAttrName !== "id") {
260
+ setComponentProp(toggleAttrName, true, "boolean attribute-form");
261
+ }
262
+ continue;
263
+ }
264
+
265
+ if (!isValidAttributeName(rawToken)) {
266
+ throw new Error(
267
+ `[Parser] Invalid attribute token '${rawToken}' on '${tagName}'.`,
268
+ );
269
+ }
270
+
231
271
  if (
232
- !processedAttrs.has(attrName)
233
- && !attrName.startsWith(PROP_PREFIX)
234
- && !attrName.includes("=")
272
+ !processedAttrs.has(rawToken)
273
+ && !rawToken.startsWith(PROP_PREFIX)
274
+ && !rawToken.includes("=")
235
275
  ) {
236
- attrs[attrName] = "";
237
- if (isWebComponent && attrName !== "id") {
238
- setComponentProp(attrName, true, "boolean attribute-form");
276
+ attrs[rawToken] = "";
277
+ if (isWebComponent && rawToken !== "id") {
278
+ setComponentProp(rawToken, true, "boolean attribute-form");
239
279
  }
240
280
  }
241
281
  }
package/src/parser.js CHANGED
@@ -65,6 +65,8 @@ export const createVirtualDom = ({
65
65
  function processItems(currentItems, parentPath = "") {
66
66
  return currentItems
67
67
  .map((item, index) => {
68
+ const nodePath = parentPath ? `${parentPath}.${index}` : String(index);
69
+
68
70
  // Handle text nodes
69
71
  if (typeof item === "string" || typeof item === "number") {
70
72
  return String(item);
@@ -86,11 +88,11 @@ export const createVirtualDom = ({
86
88
  // Skip numeric keys that might come from array indices
87
89
  if (!isNaN(Number(keyString))) {
88
90
  if (Array.isArray(value)) {
89
- return processItems(value, `${parentPath}.${keyString}`);
91
+ return processItems(value, nodePath);
90
92
  } else if (typeof value === "object" && value !== null) {
91
93
  const nestedEntries = Object.entries(value);
92
94
  if (nestedEntries.length > 0) {
93
- return processItems([value], `${parentPath}.${keyString}`);
95
+ return processItems([value], nodePath);
94
96
  }
95
97
  }
96
98
  return String(value);
@@ -182,7 +184,7 @@ export const createVirtualDom = ({
182
184
  if (typeof value === "string" || typeof value === "number") {
183
185
  childrenOrText = String(value);
184
186
  } else if (Array.isArray(value)) {
185
- childrenOrText = processItems(value, `${parentPath}.${keyString}`);
187
+ childrenOrText = processItems(value, nodePath);
186
188
  } else {
187
189
  childrenOrText = [];
188
190
  }
@@ -246,11 +248,8 @@ export const createVirtualDom = ({
246
248
  if (elementIdForRefs) {
247
249
  snabbdomData.key = elementIdForRefs;
248
250
  } else if (selector) {
249
- // Generate a key based on selector, parent path, and index for list items
250
- const itemPath = parentPath
251
- ? `${parentPath}.${index}`
252
- : String(index);
253
- snabbdomData.key = `${selector}-${itemPath}`;
251
+ // Generate a key from selector and stable structural path.
252
+ snabbdomData.key = `${selector}-${nodePath}`;
254
253
  }
255
254
 
256
255
  if (Object.keys(attrs).length > 0) {