@dinoreic/fez 0.4.1 → 0.5.2

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,339 @@
1
+ // Template utility functions for svelte-template parser
2
+ // Extracted to keep main parser file smaller
3
+
4
+ /**
5
+ * Parse loop binding to get params and detect object iteration
6
+ */
7
+ export function parseLoopBinding(binding) {
8
+ const isDestructured = binding.startsWith("[");
9
+
10
+ if (isDestructured) {
11
+ const match = binding.match(/^\[([^\]]+)\](?:\s*,\s*(\w+))?$/);
12
+ if (match) {
13
+ return {
14
+ params: match[1].split(",").map((s) => s.trim()),
15
+ indexParam: match[2] || null,
16
+ isDestructured: true,
17
+ };
18
+ }
19
+ }
20
+
21
+ const parts = binding.split(",").map((s) => s.trim());
22
+
23
+ // 2 params without brackets = destructuring
24
+ // Runtime auto-converts: Array.isArray(c) ? c : Object.entries(c)
25
+ if (parts.length === 2) {
26
+ return { params: parts, indexParam: null, isDestructured: true };
27
+ }
28
+
29
+ return { params: parts, indexParam: null, isDestructured: false };
30
+ }
31
+
32
+ /**
33
+ * Get loop variable names from binding
34
+ */
35
+ export function getLoopVarNames(binding) {
36
+ const parsed = parseLoopBinding(binding);
37
+ const names = [...parsed.params];
38
+ if (parsed.indexParam) names.push(parsed.indexParam);
39
+ // Add implicit i for single-param
40
+ if (parsed.params.length === 1 && !names.includes("i")) names.push("i");
41
+ return names;
42
+ }
43
+
44
+ /**
45
+ * Get loop item variables (non-index) from binding
46
+ * These are variables that could be objects/arrays (not primitives like indices)
47
+ */
48
+ export function getLoopItemVars(binding) {
49
+ const parsed = parseLoopBinding(binding);
50
+ // For 2-param destructuring: [value, index] - only first is item var
51
+ if (parsed.isDestructured && parsed.params.length === 2) {
52
+ return [parsed.params[0]];
53
+ }
54
+ // For other destructured bindings, all params are item vars
55
+ if (parsed.isDestructured) {
56
+ return parsed.params;
57
+ }
58
+ // For {#each items as item, index}, only 'item' is the item var
59
+ // For {#each obj as key, value, index}, 'key' and 'value' are item vars
60
+ if (parsed.params.length >= 3) {
61
+ // Last param is index, rest are item vars
62
+ return parsed.params.slice(0, -1);
63
+ }
64
+ if (parsed.params.length === 2) {
65
+ // Could be "item, index" - first is item, second is index
66
+ return [parsed.params[0]];
67
+ }
68
+ // Single param is the item var
69
+ return parsed.params;
70
+ }
71
+
72
+ /**
73
+ * Build collection expression for iteration
74
+ */
75
+ export function buildCollectionExpr(collection, binding) {
76
+ const parsed = parseLoopBinding(binding);
77
+
78
+ // 2-param destructuring uses Fez.toPairs for unified array/object handling
79
+ // Array: ['a', 'b'] → [['a', 0], ['b', 1]] (value, index)
80
+ // Object: {x: 1} → [['x', 1]] (key, value)
81
+ if (parsed.isDestructured && parsed.params.length === 2) {
82
+ return `Fez.toPairs(${collection})`;
83
+ }
84
+
85
+ // 3+ params: object iteration with explicit index
86
+ if (parsed.isDestructured || parsed.params.length >= 3) {
87
+ return `((_c)=>Array.isArray(_c)?_c:(_c&&typeof _c==="object")?Object.entries(_c):[])(${collection})`;
88
+ }
89
+
90
+ return `(${collection}||[])`;
91
+ }
92
+
93
+ /**
94
+ * Build loop callback params
95
+ */
96
+ export function buildLoopParams(binding) {
97
+ const parsed = parseLoopBinding(binding);
98
+
99
+ if (parsed.isDestructured) {
100
+ const destructure = "[" + parsed.params.join(", ") + "]";
101
+ const indexName =
102
+ parsed.indexParam || (parsed.params.includes("i") ? "_i" : "i");
103
+ return destructure + ", " + indexName;
104
+ }
105
+
106
+ if (parsed.params.length >= 3) {
107
+ const params = [...parsed.params];
108
+ const index = params.pop();
109
+ return "[" + params.join(", ") + "], " + index;
110
+ }
111
+
112
+ if (parsed.params.length === 2) {
113
+ return parsed.params.join(", ");
114
+ }
115
+
116
+ // If loop var is 'i', use '_i' for index to avoid collision
117
+ const indexName = parsed.params[0] === "i" ? "_i" : "i";
118
+ return parsed.params[0] + ", " + indexName;
119
+ }
120
+
121
+ /**
122
+ * Check if expression is an arrow function
123
+ */
124
+ export function isArrowFunction(expr) {
125
+ // Match: () => ..., (e) => ..., (e, foo) => ..., e => ...
126
+ return /^\s*(\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/.test(expr);
127
+ }
128
+
129
+ /**
130
+ * Transform arrow function to onclick-compatible string
131
+ * Input: "(e) => removeTask(index)" with loopVars = ['item', 'index', 'i']
132
+ *
133
+ * For loop variables that are item references (not indices), we store the handler
134
+ * in fezGlobals to capture the value at render time. For index-only references,
135
+ * we use simple interpolation since indices are primitives.
136
+ *
137
+ * Output for index-only: "fez.removeTask(${index})"
138
+ * Output for item refs: "${'Fez(' + UID + ').fezGlobals.delete(' + fez.fezGlobals.set(() => fez.removeTask(item)) + ')()'}"
139
+ */
140
+ export function transformArrowToHandler(
141
+ expr,
142
+ loopVars = [],
143
+ loopItemVars = [],
144
+ ) {
145
+ // Extract the arrow function body
146
+ const arrowMatch = expr.match(
147
+ /^\s*(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>\s*(.+)$/s,
148
+ );
149
+ if (!arrowMatch) return expr;
150
+
151
+ let body = arrowMatch[1].trim();
152
+
153
+ // Check if arrow has event param: (e) => or (event) => or e =>
154
+ const paramMatch = expr.match(
155
+ /^\s*\(?\s*([a-zA-Z_$][a-zA-Z0-9_$]*)?\s*(?:,\s*[^)]+)?\)?\s*=>/,
156
+ );
157
+ const eventParam = paramMatch?.[1];
158
+ const hasEventParam = eventParam && ["e", "event", "ev"].includes(eventParam);
159
+
160
+ // Check if body references loop item variables (non-index vars that could be objects)
161
+ const usedItemVars = loopItemVars.filter((varName) => {
162
+ const varRegex = new RegExp(`\\b${varName}\\b`);
163
+ return varRegex.test(body);
164
+ });
165
+
166
+ // If arrow function uses item variables (not just indices), store the function in fezGlobals
167
+ // This ensures object references are captured at render time
168
+ if (usedItemVars.length > 0) {
169
+ // Replace event param with 'event' in the body if needed
170
+ if (hasEventParam && eventParam !== "event") {
171
+ const eventRegex = new RegExp(`\\b${eventParam}\\b`, "g");
172
+ body = body.replace(eventRegex, "event");
173
+ }
174
+
175
+ // Prefix bare function calls with fez.
176
+ body = body.replace(
177
+ /(?<![.\w])([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
178
+ (match, funcName) => {
179
+ const globals = [
180
+ "console",
181
+ "window",
182
+ "document",
183
+ "Math",
184
+ "JSON",
185
+ "Date",
186
+ "Array",
187
+ "Object",
188
+ "String",
189
+ "Number",
190
+ "Boolean",
191
+ "parseInt",
192
+ "parseFloat",
193
+ "setTimeout",
194
+ "setInterval",
195
+ "clearTimeout",
196
+ "clearInterval",
197
+ "alert",
198
+ "confirm",
199
+ "prompt",
200
+ "fetch",
201
+ "event",
202
+ ];
203
+ if (globals.includes(funcName)) {
204
+ return match;
205
+ }
206
+ return `fez.${funcName}(`;
207
+ },
208
+ );
209
+
210
+ // Store the function with captured loop vars, retrieve and call at click time
211
+ // Uses IIFE to build the string at render time with UID and set() evaluated
212
+ return `\${'Fez(' + UID + ').fezGlobals.delete(' + fez.fezGlobals.set(() => ${body}) + ')()'}`;
213
+ }
214
+
215
+ // No item variables - use simple interpolation for indices (original behavior)
216
+ // Replace event param with 'event' (the actual DOM event)
217
+ if (hasEventParam && eventParam !== "event") {
218
+ const eventRegex = new RegExp(`\\b${eventParam}\\b`, "g");
219
+ body = body.replace(eventRegex, "event");
220
+ }
221
+
222
+ // Interpolate loop variables - they need to be evaluated at render time
223
+ // e.g., removeTask(index) becomes removeTask(${index})
224
+ for (const varName of loopVars) {
225
+ // Match the variable as a standalone identifier (not part of another word)
226
+ // and not already inside a template literal
227
+ const varRegex = new RegExp(`(?<!\\$\\{)\\b${varName}\\b(?![^{]*\\})`, "g");
228
+ body = body.replace(varRegex, `\${${varName}}`);
229
+ }
230
+
231
+ // Prefix bare function calls with fez.
232
+ body = body.replace(
233
+ /(?<![.\w])([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
234
+ (match, funcName) => {
235
+ const globals = [
236
+ "console",
237
+ "window",
238
+ "document",
239
+ "Math",
240
+ "JSON",
241
+ "Date",
242
+ "Array",
243
+ "Object",
244
+ "String",
245
+ "Number",
246
+ "Boolean",
247
+ "parseInt",
248
+ "parseFloat",
249
+ "setTimeout",
250
+ "setInterval",
251
+ "clearTimeout",
252
+ "clearInterval",
253
+ "alert",
254
+ "confirm",
255
+ "prompt",
256
+ "fetch",
257
+ "event",
258
+ ];
259
+ if (globals.includes(funcName)) {
260
+ return match;
261
+ }
262
+ return `fez.${funcName}(`;
263
+ },
264
+ );
265
+
266
+ return body;
267
+ }
268
+
269
+ /**
270
+ * Extract a braced expression with proper depth counting
271
+ */
272
+ export function extractBracedExpression(text, startIndex) {
273
+ let depth = 0;
274
+ let i = startIndex;
275
+
276
+ while (i < text.length) {
277
+ const char = text[i];
278
+ if (char === "{") {
279
+ depth++;
280
+ } else if (char === "}") {
281
+ depth--;
282
+ if (depth === 0) {
283
+ return { expression: text.slice(startIndex + 1, i), endIndex: i };
284
+ }
285
+ } else if (char === '"' || char === "'" || char === "`") {
286
+ // Skip string literals
287
+ const quote = char;
288
+ i++;
289
+ while (i < text.length && text[i] !== quote) {
290
+ if (text[i] === "\\") i++;
291
+ i++;
292
+ }
293
+ }
294
+ i++;
295
+ }
296
+ throw new Error(`Unmatched brace at ${startIndex}`);
297
+ }
298
+
299
+ /**
300
+ * Check if position is inside an attribute (attr={...})
301
+ * Returns the attribute name if inside one, null otherwise
302
+ */
303
+ export function getAttributeContext(text, pos) {
304
+ // Look backwards for pattern like: attr={
305
+ // We need to find the last '=' before pos that's preceded by an attribute name
306
+ let j = pos - 1;
307
+ // Skip whitespace and opening brace
308
+ while (j >= 0 && (text[j] === "{" || text[j] === " " || text[j] === "\t"))
309
+ j--;
310
+ if (j >= 0 && text[j] === "=") {
311
+ // Found '=', now look for attribute name
312
+ j--;
313
+ while (j >= 0 && (text[j] === " " || text[j] === "\t")) j--;
314
+ // Extract attribute name
315
+ let attrEnd = j + 1;
316
+ while (j >= 0 && /[a-zA-Z0-9_:-]/.test(text[j])) j--;
317
+ const attrName = text.slice(j + 1, attrEnd);
318
+ if (
319
+ attrName &&
320
+ /^[a-zA-Z]/.test(attrName) &&
321
+ (j < 0 || /\s/.test(text[j]))
322
+ ) {
323
+ return attrName.toLowerCase();
324
+ }
325
+ }
326
+ return null;
327
+ }
328
+
329
+ /**
330
+ * Check if position is inside an event attribute (onclick=, onchange=, etc.)
331
+ * Returns the attribute name if inside one, null otherwise
332
+ */
333
+ export function getEventAttributeContext(text, pos) {
334
+ const attr = getAttributeContext(text, pos);
335
+ if (attr && /^on[a-z]+$/.test(attr)) {
336
+ return attr;
337
+ }
338
+ return null;
339
+ }