@dinoreic/fez 0.4.0 → 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.
@@ -14,69 +14,136 @@ export default (Fez) => {
14
14
  // Fez.head({ css: 'https://example.com/styles.css', media: 'print' }, () => { console.log('CSS loaded') })
15
15
  // Inline script evaluation
16
16
  // Fez.head({ script: 'console.log("Hello world")' })
17
+ // Fez component loading
18
+ // Fez.head({ fez: 'path/to/component.fez' })
17
19
  // Extract from nodes
18
20
  // Fez.head(domNode)
19
21
  Fez.head = (config, callback) => {
20
22
  if (config.nodeName) {
21
- if (config.nodeName == 'SCRIPT') {
22
- Fez.head({script: config.innerText})
23
- config.remove()
23
+ if (config.nodeName == "SCRIPT") {
24
+ Fez.head({ script: config.innerText });
25
+ config.remove();
24
26
  } else {
25
- config.querySelectorAll('script').forEach((n) => Fez.head(n) )
26
- config.querySelectorAll('template[fez], xmp[fez], script[fez]').forEach((n) => Fez.compile(n) )
27
+ config.querySelectorAll("script").forEach((n) => Fez.head(n));
28
+ config
29
+ .querySelectorAll("template[fez], xmp[fez], script[fez]")
30
+ .forEach((n) => Fez.compile(n));
27
31
  }
28
32
 
29
- return
33
+ return;
30
34
  }
31
35
 
32
- if (typeof config !== 'object' || config === null) {
33
- throw new Error('head requires an object parameter');
36
+ if (typeof config !== "object" || config === null) {
37
+ throw new Error("head requires an object parameter");
34
38
  }
35
39
 
36
- let src, attributes = {}, elementType;
40
+ let src,
41
+ attributes = {},
42
+ elementType;
43
+
44
+ // Load Fez component(s) from URL
45
+ // Supports:
46
+ // - Single component: { fez: 'path/to/component.fez' }
47
+ // - Component list: { fez: 'path/to/components.txt' }
48
+ // txt file contains one component path per line (relative to txt location or absolute if starts with /)
49
+ if (config.fez) {
50
+ const fezPath = config.fez;
51
+
52
+ // If it's a txt file, load it as a component list
53
+ if (fezPath.endsWith(".txt")) {
54
+ Fez.fetch(fezPath).then((content) => {
55
+ // Get base path from txt file location
56
+ const basePath = fezPath.substring(0, fezPath.lastIndexOf("/") + 1);
57
+
58
+ // Parse lines, filter empty lines and comments
59
+ const lines = content
60
+ .split("\n")
61
+ .map((line) => line.trim())
62
+ .filter((line) => line && !line.startsWith("#"));
63
+
64
+ // Load each component
65
+ let loaded = 0;
66
+ const total = lines.length;
67
+
68
+ lines.forEach((line) => {
69
+ // Determine full path
70
+ // - If starts with /, it's absolute from root
71
+ // - Otherwise, relative to txt file location
72
+ let componentPath;
73
+ if (line.startsWith("/")) {
74
+ componentPath = line;
75
+ } else {
76
+ // Add .fez extension if not present
77
+ const path = line.endsWith(".fez") ? line : line + ".fez";
78
+ componentPath = basePath + path;
79
+ }
80
+
81
+ // Extract component name from path
82
+ const name = componentPath.split("/").pop().split(".")[0];
83
+
84
+ Fez.fetch(componentPath).then((componentContent) => {
85
+ Fez.compile(name, componentContent);
86
+ loaded++;
87
+ if (loaded === total && callback) callback();
88
+ });
89
+ });
90
+ });
91
+ return;
92
+ }
93
+
94
+ // Single .fez component
95
+ Fez.fetch(fezPath).then((content) => {
96
+ const name = fezPath.split("/").pop().split(".")[0];
97
+ Fez.compile(name, content);
98
+ if (callback) callback();
99
+ });
100
+ return;
101
+ }
37
102
 
38
103
  if (config.script) {
39
- if (config.script.includes('import ')) {
104
+ if (config.script.includes("import ")) {
40
105
  if (callback) {
41
- Fez.error('Fez.head callback is not supported when script with import is passed (module context).')
106
+ Fez.consoleError(
107
+ "Fez.head callback is not supported when script with import is passed (module context).",
108
+ );
42
109
  }
43
110
 
44
111
  // Evaluate inline script in context in the module
45
- const script = document.createElement('script');
46
- script.type = 'module';
112
+ const script = document.createElement("script");
113
+ script.type = "module";
47
114
  script.textContent = config.script;
48
115
  document.head.appendChild(script);
49
- requestAnimationFrame(()=>script.remove())
116
+ requestAnimationFrame(() => script.remove());
50
117
  } else {
51
118
  try {
52
119
  new Function(config.script)();
53
120
  if (callback) callback();
54
121
  } catch (error) {
55
- Fez.error('Error executing script:', error);
122
+ Fez.consoleError("Error executing script:", error);
56
123
  console.log(config.script);
57
124
  }
58
125
  }
59
126
  return;
60
127
  } else if (config.js) {
61
128
  src = config.js;
62
- elementType = 'script';
129
+ elementType = "script";
63
130
  // Copy all properties except 'js' as attributes
64
131
  for (const [key, value] of Object.entries(config)) {
65
- if (key !== 'js' && key !== 'module') {
132
+ if (key !== "js" && key !== "module") {
66
133
  attributes[key] = value;
67
134
  }
68
135
  }
69
136
  // Handle module loading
70
137
  if (config.module) {
71
- attributes.type = 'module';
138
+ attributes.type = "module";
72
139
  }
73
140
  } else if (config.css) {
74
141
  src = config.css;
75
- elementType = 'link';
76
- attributes.rel = 'stylesheet';
142
+ elementType = "link";
143
+ attributes.rel = "stylesheet";
77
144
  // Copy all properties except 'css' as attributes
78
145
  for (const [key, value] of Object.entries(config)) {
79
- if (key !== 'css') {
146
+ if (key !== "css") {
80
147
  attributes[key] = value;
81
148
  }
82
149
  }
@@ -84,7 +151,9 @@ export default (Fez) => {
84
151
  throw new Error('head requires either "script", "js" or "css" property');
85
152
  }
86
153
 
87
- const existingNode = document.querySelector(`${elementType}[src="${src}"], ${elementType}[href="${src}"]`);
154
+ const existingNode = document.querySelector(
155
+ `${elementType}[src="${src}"], ${elementType}[href="${src}"]`,
156
+ );
88
157
  if (existingNode) {
89
158
  if (callback) callback();
90
159
  return existingNode;
@@ -92,7 +161,7 @@ export default (Fez) => {
92
161
 
93
162
  const element = document.createElement(elementType);
94
163
 
95
- if (elementType === 'link') {
164
+ if (elementType === "link") {
96
165
  element.href = src;
97
166
  } else {
98
167
  element.src = src;
@@ -105,12 +174,15 @@ export default (Fez) => {
105
174
  if (callback || config.module) {
106
175
  element.onload = () => {
107
176
  // If module name is provided, import it and assign to window
108
- if (config.module && elementType === 'script') {
109
- import(src).then(module => {
110
- window[config.module] = module.default || module[config.module] || module;
111
- }).catch(error => {
112
- console.error(`Error importing module ${config.module}:`, error);
113
- });
177
+ if (config.module && elementType === "script") {
178
+ import(src)
179
+ .then((module) => {
180
+ window[config.module] =
181
+ module.default || module[config.module] || module;
182
+ })
183
+ .catch((error) => {
184
+ console.error(`Error importing module ${config.module}:`, error);
185
+ });
114
186
  }
115
187
  if (callback) callback();
116
188
  };
@@ -119,7 +191,11 @@ export default (Fez) => {
119
191
  document.head.appendChild(element);
120
192
 
121
193
  return element;
122
- }
194
+ };
195
+
196
+ // Cache configuration
197
+ const FETCH_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
198
+ const FETCH_CACHE_MAX_SIZE = 100;
123
199
 
124
200
  // Fetch wrapper with automatic caching and data handling
125
201
  // Usage:
@@ -132,16 +208,16 @@ export default (Fez) => {
132
208
  // Data object is automatically converted:
133
209
  // - GET: appended as URL query parameters
134
210
  // - POST: sent as FormData (multipart/form-data) without custom headers
135
- Fez.fetch = function(...args) {
211
+ Fez.fetch = function (...args) {
136
212
  // Initialize cache if not exists
137
- Fez._fetchCache ||= {};
213
+ Fez._fetchCache ||= new Map();
138
214
 
139
- let method = 'GET';
215
+ let method = "GET";
140
216
  let url;
141
217
  let callback;
142
218
 
143
219
  // Check if first arg is HTTP method (uppercase letters)
144
- if (typeof args[0] === 'string' && /^[A-Z]+$/.test(args[0])) {
220
+ if (typeof args[0] === "string" && /^[A-Z]+$/.test(args[0])) {
145
221
  method = args.shift();
146
222
  }
147
223
 
@@ -151,22 +227,22 @@ export default (Fez) => {
151
227
  // Check for data/options object
152
228
  let opts = {};
153
229
  let data = null;
154
- if (typeof args[0] === 'object') {
230
+ if (typeof args[0] === "object") {
155
231
  data = args.shift();
156
232
  }
157
233
 
158
234
  // Check for callback function
159
- if (typeof args[0] === 'function') {
235
+ if (typeof args[0] === "function") {
160
236
  callback = args.shift();
161
237
  }
162
238
 
163
239
  // Handle data based on method
164
240
  if (data) {
165
- if (method === 'GET') {
241
+ if (method === "GET") {
166
242
  // For GET, append data as query parameters
167
243
  const params = new URLSearchParams(data);
168
- url += (url.includes('?') ? '&' : '?') + params.toString();
169
- } else if (method === 'POST') {
244
+ url += (url.includes("?") ? "&" : "?") + params.toString();
245
+ } else if (method === "POST") {
170
246
  // For POST, convert to FormData
171
247
  const formData = new FormData();
172
248
  for (const [key, value] of Object.entries(data)) {
@@ -182,127 +258,197 @@ export default (Fez) => {
182
258
  // Create cache key from method, url, and stringified opts
183
259
  const cacheKey = `${method}:${url}:${JSON.stringify(opts)}`;
184
260
 
185
- // Check cache first
186
- if (Fez._fetchCache[cacheKey]) {
187
- const cachedData = Fez._fetchCache[cacheKey];
188
- Fez.log(`fetch cache hit: ${method} ${url}`);
261
+ // Check cache first (with TTL validation)
262
+ const cached = Fez._fetchCache.get(cacheKey);
263
+ if (cached && Date.now() - cached.timestamp < FETCH_CACHE_TTL) {
264
+ Fez.consoleLog(`fetch cache hit: ${method} ${url}`);
189
265
  if (callback) {
190
- callback(cachedData);
266
+ callback(cached.data);
191
267
  return;
192
268
  }
193
- return Promise.resolve(cachedData);
269
+ return Promise.resolve(cached.data);
194
270
  }
195
271
 
196
272
  // Log live fetch
197
- Fez.log(`fetch live: ${method} ${url}`);
273
+ Fez.consoleLog(`fetch live: ${method} ${url}`);
198
274
 
199
275
  // Helper to process and cache response
200
276
  const processResponse = (response) => {
201
- if (response.headers.get('content-type')?.includes('application/json')) {
277
+ if (response.headers.get("content-type")?.includes("application/json")) {
202
278
  return response.json();
203
279
  }
204
280
  return response.text();
205
281
  };
206
282
 
283
+ // Helper to store in cache with size limit
284
+ const storeInCache = (key, data) => {
285
+ // Enforce max cache size by removing oldest entries
286
+ if (Fez._fetchCache.size >= FETCH_CACHE_MAX_SIZE) {
287
+ const oldestKey = Fez._fetchCache.keys().next().value;
288
+ Fez._fetchCache.delete(oldestKey);
289
+ }
290
+ Fez._fetchCache.set(key, { data, timestamp: Date.now() });
291
+ };
292
+
207
293
  // If callback provided, execute and handle
208
294
  if (callback) {
209
295
  fetch(url, opts)
210
296
  .then(processResponse)
211
- .then(data => {
212
- Fez._fetchCache[cacheKey] = data;
297
+ .then((data) => {
298
+ storeInCache(cacheKey, data);
213
299
  callback(data);
214
300
  })
215
- .catch(error => Fez.onError('fetch', error));
301
+ .catch((error) => Fez.onError("fetch", error));
216
302
  return;
217
303
  }
218
304
 
219
305
  // Return promise with automatic JSON parsing
220
306
  return fetch(url, opts)
221
307
  .then(processResponse)
222
- .then(data => {
223
- Fez._fetchCache[cacheKey] = data;
308
+ .then((data) => {
309
+ storeInCache(cacheKey, data);
224
310
  return data;
225
311
  });
226
- }
312
+ };
313
+
314
+ // Clear fetch cache (useful for testing or manual cache invalidation)
315
+ Fez.clearFetchCache = () => {
316
+ Fez._fetchCache?.clear();
317
+ };
227
318
 
228
319
  Fez.darkenColor = (color, percent = 20) => {
229
320
  // Convert hex to RGB
230
- const num = parseInt(color.replace("#", ""), 16)
231
- const amt = Math.round(2.55 * percent)
232
- const R = (num >> 16) - amt
233
- const G = (num >> 8 & 0x00FF) - amt
234
- const B = (num & 0x0000FF) - amt
235
- return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)
236
- }
321
+ const num = parseInt(color.replace("#", ""), 16);
322
+ const amt = Math.round(2.55 * percent);
323
+ const R = (num >> 16) - amt;
324
+ const G = ((num >> 8) & 0x00ff) - amt;
325
+ const B = (num & 0x0000ff) - amt;
326
+ return (
327
+ "#" +
328
+ (
329
+ 0x1000000 +
330
+ (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
331
+ (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
332
+ (B < 255 ? (B < 1 ? 0 : B) : 255)
333
+ )
334
+ .toString(16)
335
+ .slice(1)
336
+ );
337
+ };
237
338
 
238
339
  Fez.lightenColor = (color, percent = 20) => {
239
340
  // Convert hex to RGB
240
- const num = parseInt(color.replace("#", ""), 16)
241
- const amt = Math.round(2.55 * percent)
242
- const R = (num >> 16) + amt
243
- const G = (num >> 8 & 0x00FF) + amt
244
- const B = (num & 0x0000FF) + amt
245
- return "#" + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + (B < 255 ? B < 1 ? 0 : B : 255)).toString(16).slice(1)
246
- }
247
-
341
+ const num = parseInt(color.replace("#", ""), 16);
342
+ const amt = Math.round(2.55 * percent);
343
+ const R = (num >> 16) + amt;
344
+ const G = ((num >> 8) & 0x00ff) + amt;
345
+ const B = (num & 0x0000ff) + amt;
346
+ return (
347
+ "#" +
348
+ (
349
+ 0x1000000 +
350
+ (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 +
351
+ (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 +
352
+ (B < 255 ? (B < 1 ? 0 : B) : 255)
353
+ )
354
+ .toString(16)
355
+ .slice(1)
356
+ );
357
+ };
358
+
359
+ /**
360
+ * Escapes HTML special characters in a string
361
+ * Also strips font-family styles (common source of XSS via CSS)
362
+ */
248
363
  Fez.htmlEscape = (text) => {
249
- if (typeof text == 'string') {
250
- text = text
251
- // .replaceAll('&', "&amp;")
252
- .replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, '')
253
- .replaceAll("&", '&amp;')
254
- .replaceAll("'", '&apos;')
255
- .replaceAll('"', '&quot;')
256
- .replaceAll('<', '&lt;')
257
- .replaceAll('>', '&gt;')
258
- // .replaceAll('@', '&#64;') // needed for template escaping
259
-
364
+ if (typeof text === "string") {
260
365
  return text
261
- } else {
262
- return text === undefined ? '' : text
366
+ .replace(/font-family\s*:\s*(?:&[^;]+;|[^;])*?;/gi, "") // Strip font-family (CSS safety)
367
+ .replaceAll("&", "&amp;")
368
+ .replaceAll("'", "&apos;")
369
+ .replaceAll('"', "&quot;")
370
+ .replaceAll("<", "&lt;")
371
+ .replaceAll(">", "&gt;");
263
372
  }
264
- }
373
+ return text === undefined ? "" : text;
374
+ };
265
375
 
266
376
  // create dom root and return it
267
- Fez.domRoot = (data, name = 'div') => {
377
+ Fez.domRoot = (data, name = "div") => {
268
378
  if (data instanceof Node) {
269
- return data
379
+ return data;
270
380
  } else {
271
- const root = document.createElement(name)
272
- root.innerHTML = data
273
- return root
381
+ const root = document.createElement(name);
382
+ root.innerHTML = data;
383
+ return root;
274
384
  }
275
- }
385
+ };
276
386
 
277
387
  // add class by name to node and remove it from siblings
278
- Fez.activateNode = (node, klass = 'active') => {
279
- Array.from(node.parentElement.children).forEach(child => {
280
- child.classList.remove(klass)
281
- })
282
- node.classList.add(klass)
283
- }
388
+ Fez.activateNode = (node, klass = "active") => {
389
+ if (!node || !node.parentElement) return;
390
+ Array.from(node.parentElement.children).forEach((child) => {
391
+ child.classList.remove(klass);
392
+ });
393
+ node.classList.add(klass);
394
+ };
284
395
 
285
396
  Fez.isTrue = (val) => {
286
- return ['1', 'true', 'on'].includes(String(val).toLowerCase())
287
- }
397
+ return ["1", "true", "on"].includes(String(val).toLowerCase());
398
+ };
399
+
400
+ // get document unique ID
401
+ Fez.UID = 111;
402
+ Fez.uid = () => {
403
+ return "fez_uid_" + (++Fez.UID).toString(32);
404
+ };
405
+
406
+ // get global function pointer, used to pass functions to nested or inline elements
407
+ // <some-node :callback="${Fez.pointer(opts.callback)}" ...>
408
+ // Pointers are automatically cleaned up after first use (one-time use by default)
409
+ // Use Fez.pointer(func, { persist: true }) to keep the pointer
410
+ Fez.POINTER_SEQ = 0;
411
+ Fez.POINTER = {};
412
+ Fez.pointer = (func, opts = {}) => {
413
+ if (typeof func == "function") {
414
+ const uid = ++Fez.POINTER_SEQ;
415
+
416
+ if (opts.persist) {
417
+ // Persistent pointer - stays until manually removed
418
+ Fez.POINTER[uid] = func;
419
+ } else {
420
+ // One-time use pointer - auto-cleanup after first call
421
+ Fez.POINTER[uid] = (...args) => {
422
+ const result = func(...args);
423
+ delete Fez.POINTER[uid];
424
+ return result;
425
+ };
426
+ }
427
+
428
+ return `Fez.POINTER[${uid}]`;
429
+ }
430
+ };
431
+
432
+ // Manually clear all pointers (useful for testing or cleanup)
433
+ Fez.clearPointers = () => {
434
+ Fez.POINTER = {};
435
+ };
288
436
 
289
437
  // Resolve a function from a string or function reference
290
438
  Fez.getFunction = (pointer) => {
291
439
  if (!pointer) {
292
- return ()=>{}
293
- }
294
- else if (typeof pointer === 'function') {
440
+ return () => {};
441
+ } else if (typeof pointer === "function") {
295
442
  return pointer;
296
- }
297
- else if (typeof pointer === 'string') {
443
+ } else if (typeof pointer === "string") {
298
444
  // Check if it's a function expression (arrow function or function keyword)
299
445
  // Arrow function: (args) => or args =>
300
446
  const arrowFuncPattern = /^\s*\(?\s*\w+(\s*,\s*\w+)*\s*\)?\s*=>/;
301
447
  const functionPattern = /^\s*function\s*\(/;
302
448
 
303
449
  if (arrowFuncPattern.test(pointer) || functionPattern.test(pointer)) {
304
- return new Function('return ' + pointer)();
305
- } else if (pointer.includes('.') && !pointer.includes('(')) {
450
+ return new Function("return " + pointer)();
451
+ } else if (pointer.includes(".") && !pointer.includes("(")) {
306
452
  // It's a property access like "this.focus" - return a function that calls it
307
453
  return new Function(`return function() { return ${pointer}(); }`);
308
454
  } else {
@@ -310,57 +456,63 @@ export default (Fez) => {
310
456
  return new Function(pointer);
311
457
  }
312
458
  }
313
- }
459
+ };
314
460
 
315
461
  // Execute a function when DOM is ready or immediately if already loaded
316
462
  Fez.onReady = (callback) => {
317
- if (document.readyState === 'loading') {
318
- document.addEventListener('DOMContentLoaded', ()=>{
319
- callback()
320
- }, { once: true })
463
+ if (document.readyState === "loading") {
464
+ document.addEventListener(
465
+ "DOMContentLoaded",
466
+ () => {
467
+ callback();
468
+ },
469
+ { once: true },
470
+ );
321
471
  } else {
322
- callback()
472
+ callback();
323
473
  }
324
- }
474
+ };
325
475
 
326
476
  // get unique id from string
327
477
  Fez.fnv1 = (str) => {
328
- var FNV_OFFSET_BASIS, FNV_PRIME, hash, i, j, ref;
329
- FNV_OFFSET_BASIS = 2166136261;
330
- FNV_PRIME = 16777619;
331
- hash = FNV_OFFSET_BASIS;
332
- for (i = j = 0, ref = str.length - 1; (0 <= ref ? j <= ref : j >= ref); i = 0 <= ref ? ++j : --j) {
478
+ let FNV_OFFSET_BASIS = 2166136261;
479
+ let FNV_PRIME = 16777619;
480
+ let hash = FNV_OFFSET_BASIS;
481
+ for (let i = 0; i < str.length; i++) {
333
482
  hash ^= str.charCodeAt(i);
334
483
  hash *= FNV_PRIME;
335
484
  }
336
- return hash.toString(36).replaceAll('-', '');
337
- }
485
+ return hash.toString(36).replaceAll("-", "");
486
+ };
338
487
 
339
- Fez.tag = (tag, opts = {}, html = '') => {
340
- const json = encodeURIComponent(JSON.stringify(opts))
341
- return `<${tag} data-props="${json}">${html}</${tag}>`
488
+ Fez.tag = (tag, opts = {}, html = "") => {
489
+ const json = encodeURIComponent(JSON.stringify(opts));
490
+ return `<${tag} data-props="${json}">${html}</${tag}>`;
342
491
  // const json = JSON.stringify(opts, null, 2)
343
492
  // const data = `<script type="text/template">${json}</script><${tag} data-json-template="true">${html}</${tag}>`
344
493
  // return data
345
- }
494
+ };
346
495
 
347
496
  // execute function until it returns true
348
497
  Fez.untilTrue = (func, pingRate) => {
349
- pingRate ||= 200
498
+ pingRate ||= 200;
350
499
 
351
500
  if (!func()) {
352
- setTimeout(()=>{
353
- Fez.untilTrue(func, pingRate)
354
- } ,pingRate)
501
+ setTimeout(() => {
502
+ Fez.untilTrue(func, pingRate);
503
+ }, pingRate);
355
504
  }
356
- }
505
+ };
506
+
507
+ // Default throttle delay in ms
508
+ const DEFAULT_THROTTLE_DELAY = 200;
357
509
 
358
510
  // throttle function calls
359
- Fez.throttle = (func, delay = 200) => {
511
+ Fez.throttle = (func, delay = DEFAULT_THROTTLE_DELAY) => {
360
512
  let lastRun = 0;
361
513
  let timeout;
362
514
 
363
- return function(...args) {
515
+ return function (...args) {
364
516
  const now = Date.now();
365
517
 
366
518
  if (now - lastRun >= delay) {
@@ -368,26 +520,44 @@ export default (Fez) => {
368
520
  lastRun = now;
369
521
  } else {
370
522
  clearTimeout(timeout);
371
- timeout = setTimeout(() => {
372
- func.apply(this, args);
373
- lastRun = Date.now();
374
- }, delay - (now - lastRun));
523
+ timeout = setTimeout(
524
+ () => {
525
+ func.apply(this, args);
526
+ lastRun = Date.now();
527
+ },
528
+ delay - (now - lastRun),
529
+ );
375
530
  }
376
531
  };
377
- }
378
-
379
- // const firstTimeHash = new Map()
380
- // Fez.firstTime = (key, func) => {
381
- // if ( !firstTimeHash.get(key) ) {
382
- // firstTimeHash.set(key, true)
383
-
384
- // if (func) {
385
- // func()
386
- // }
387
-
388
- // return true
389
- // }
390
-
391
- // return false
392
- // }
393
- }
532
+ };
533
+
534
+ // Enhanced truthiness check for template conditionals (#if, #unless)
535
+ // Empty arrays and empty objects are falsy, everything else uses standard JS truthiness
536
+ Fez.isTruthy = (v) => {
537
+ if (Array.isArray(v)) return v.length > 0;
538
+ if (v && typeof v === "object") return Object.keys(v).length > 0;
539
+ return !!v;
540
+ };
541
+
542
+ // Convert any collection to pairs for loop destructuring
543
+ // Array: ['a', 'b'] → [['a', 0], ['b', 1]] (value, index)
544
+ // Object: {x: 1} → [['x', 1]] (key, value)
545
+ Fez.toPairs = (c) => {
546
+ if (Array.isArray(c)) return c.map((v, i) => [v, i]);
547
+ if (c && typeof c === "object") return Object.entries(c);
548
+ return [];
549
+ };
550
+
551
+ // Returns short type identifier for data:
552
+ // 'o' - object, 'f' - function, 's' - string, 'a' - array, 'i' - integer, 'n' - float/number, 'u' - undefined/null
553
+ Fez.typeof = (data) => {
554
+ if (data === null || data === undefined) return "u";
555
+ if (Array.isArray(data)) return "a";
556
+ const t = typeof data;
557
+ if (t === "function") return "f";
558
+ if (t === "string") return "s";
559
+ if (t === "number") return Number.isInteger(data) ? "i" : "n";
560
+ if (t === "object") return "o";
561
+ return t[0];
562
+ };
563
+ };