zero-query 0.9.1 → 0.9.6
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 +11 -8
- package/cli/commands/dev/devtools/js/elements.js +5 -0
- package/cli/scaffold/app/app.js +1 -1
- package/cli/scaffold/global.css +1 -2
- package/cli/scaffold/index.html +2 -1
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +429 -35
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +46 -2
- package/index.js +32 -4
- package/package.json +1 -1
- package/src/component.js +98 -8
- package/src/core.js +3 -1
- package/src/expression.js +103 -16
- package/src/http.js +6 -2
- package/src/router.js +6 -1
- package/src/utils.js +191 -5
- package/tests/audit.test.js +4030 -0
- package/tests/component.test.js +1185 -0
- package/tests/core.test.js +41 -0
- package/tests/expression.test.js +10 -10
- package/tests/utils.test.js +543 -1
- package/types/utils.d.ts +103 -0
package/src/utils.js
CHANGED
|
@@ -79,6 +79,10 @@ export function escapeHtml(str) {
|
|
|
79
79
|
return String(str).replace(/[&<>"']/g, c => map[c]);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export function stripHtml(str) {
|
|
83
|
+
return String(str).replace(/<[^>]*>/g, '');
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
/**
|
|
83
87
|
* Template tag for auto-escaping interpolated values
|
|
84
88
|
* Usage: $.html`<div>${userInput}</div>`
|
|
@@ -94,7 +98,7 @@ export function html(strings, ...values) {
|
|
|
94
98
|
/**
|
|
95
99
|
* Mark HTML as trusted (skip escaping in $.html template)
|
|
96
100
|
*/
|
|
97
|
-
class TrustedHTML {
|
|
101
|
+
export class TrustedHTML {
|
|
98
102
|
constructor(html) { this._html = html; }
|
|
99
103
|
toString() { return this._html; }
|
|
100
104
|
}
|
|
@@ -124,7 +128,10 @@ export function camelCase(str) {
|
|
|
124
128
|
* CamelCase to kebab-case
|
|
125
129
|
*/
|
|
126
130
|
export function kebabCase(str) {
|
|
127
|
-
return str
|
|
131
|
+
return str
|
|
132
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
133
|
+
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
|
|
134
|
+
.toLowerCase();
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
|
|
@@ -165,15 +172,19 @@ export function deepMerge(target, ...sources) {
|
|
|
165
172
|
/**
|
|
166
173
|
* Simple object equality check
|
|
167
174
|
*/
|
|
168
|
-
export function isEqual(a, b) {
|
|
175
|
+
export function isEqual(a, b, _seen) {
|
|
169
176
|
if (a === b) return true;
|
|
170
177
|
if (typeof a !== typeof b) return false;
|
|
171
178
|
if (typeof a !== 'object' || a === null || b === null) return false;
|
|
172
179
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
180
|
+
// Guard against circular references
|
|
181
|
+
if (!_seen) _seen = new Set();
|
|
182
|
+
if (_seen.has(a)) return true;
|
|
183
|
+
_seen.add(a);
|
|
173
184
|
const keysA = Object.keys(a);
|
|
174
185
|
const keysB = Object.keys(b);
|
|
175
186
|
if (keysA.length !== keysB.length) return false;
|
|
176
|
-
return keysA.every(k => isEqual(a[k], b[k]));
|
|
187
|
+
return keysA.every(k => isEqual(a[k], b[k], _seen));
|
|
177
188
|
}
|
|
178
189
|
|
|
179
190
|
|
|
@@ -249,7 +260,7 @@ export const session = {
|
|
|
249
260
|
// ---------------------------------------------------------------------------
|
|
250
261
|
// Event bus (pub/sub)
|
|
251
262
|
// ---------------------------------------------------------------------------
|
|
252
|
-
class EventBus {
|
|
263
|
+
export class EventBus {
|
|
253
264
|
constructor() { this._handlers = new Map(); }
|
|
254
265
|
|
|
255
266
|
on(event, fn) {
|
|
@@ -275,3 +286,178 @@ class EventBus {
|
|
|
275
286
|
}
|
|
276
287
|
|
|
277
288
|
export const bus = new EventBus();
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Array utilities
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
export function range(startOrEnd, end, step) {
|
|
296
|
+
let s, e, st;
|
|
297
|
+
if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
|
|
298
|
+
else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
|
|
299
|
+
if (st === 0) return [];
|
|
300
|
+
const result = [];
|
|
301
|
+
if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
|
|
302
|
+
else { for (let i = s; i > e; i += st) result.push(i); }
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function unique(arr, keyFn) {
|
|
307
|
+
if (!keyFn) return [...new Set(arr)];
|
|
308
|
+
const seen = new Set();
|
|
309
|
+
return arr.filter(item => {
|
|
310
|
+
const k = keyFn(item);
|
|
311
|
+
if (seen.has(k)) return false;
|
|
312
|
+
seen.add(k);
|
|
313
|
+
return true;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function chunk(arr, size) {
|
|
318
|
+
const result = [];
|
|
319
|
+
for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function groupBy(arr, keyFn) {
|
|
324
|
+
const result = {};
|
|
325
|
+
for (const item of arr) {
|
|
326
|
+
const k = keyFn(item);
|
|
327
|
+
(result[k] ??= []).push(item);
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Object utilities
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
export function pick(obj, keys) {
|
|
338
|
+
const result = {};
|
|
339
|
+
for (const k of keys) { if (k in obj) result[k] = obj[k]; }
|
|
340
|
+
return result;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function omit(obj, keys) {
|
|
344
|
+
const exclude = new Set(keys);
|
|
345
|
+
const result = {};
|
|
346
|
+
for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function getPath(obj, path, fallback) {
|
|
351
|
+
const keys = path.split('.');
|
|
352
|
+
let cur = obj;
|
|
353
|
+
for (const k of keys) {
|
|
354
|
+
if (cur == null || typeof cur !== 'object') return fallback;
|
|
355
|
+
cur = cur[k];
|
|
356
|
+
}
|
|
357
|
+
return cur === undefined ? fallback : cur;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function setPath(obj, path, value) {
|
|
361
|
+
const keys = path.split('.');
|
|
362
|
+
let cur = obj;
|
|
363
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
364
|
+
const k = keys[i];
|
|
365
|
+
if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
|
|
366
|
+
cur = cur[k];
|
|
367
|
+
}
|
|
368
|
+
cur[keys[keys.length - 1]] = value;
|
|
369
|
+
return obj;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function isEmpty(val) {
|
|
373
|
+
if (val == null) return true;
|
|
374
|
+
if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
|
|
375
|
+
if (val instanceof Map || val instanceof Set) return val.size === 0;
|
|
376
|
+
if (typeof val === 'object') return Object.keys(val).length === 0;
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// String utilities
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
export function capitalize(str) {
|
|
386
|
+
if (!str) return '';
|
|
387
|
+
return str[0].toUpperCase() + str.slice(1).toLowerCase();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function truncate(str, maxLen, suffix = '…') {
|
|
391
|
+
if (str.length <= maxLen) return str;
|
|
392
|
+
const end = Math.max(0, maxLen - suffix.length);
|
|
393
|
+
return str.slice(0, end) + suffix;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Number utilities
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
export function clamp(val, min, max) {
|
|
402
|
+
return val < min ? min : val > max ? max : val;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Function utilities
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
export function memoize(fn, keyFnOrOpts) {
|
|
411
|
+
let keyFn, maxSize = 0;
|
|
412
|
+
if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
|
|
413
|
+
else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
|
|
414
|
+
|
|
415
|
+
const cache = new Map();
|
|
416
|
+
|
|
417
|
+
const memoized = (...args) => {
|
|
418
|
+
const key = keyFn ? keyFn(...args) : args[0];
|
|
419
|
+
if (cache.has(key)) return cache.get(key);
|
|
420
|
+
const result = fn(...args);
|
|
421
|
+
cache.set(key, result);
|
|
422
|
+
if (maxSize > 0 && cache.size > maxSize) {
|
|
423
|
+
cache.delete(cache.keys().next().value);
|
|
424
|
+
}
|
|
425
|
+
return result;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
memoized.clear = () => cache.clear();
|
|
429
|
+
return memoized;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Async utilities
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
export function retry(fn, opts = {}) {
|
|
438
|
+
const { attempts = 3, delay = 1000, backoff = 1 } = opts;
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
let attempt = 0, currentDelay = delay;
|
|
441
|
+
const tryOnce = () => {
|
|
442
|
+
attempt++;
|
|
443
|
+
fn(attempt).then(resolve, (err) => {
|
|
444
|
+
if (attempt >= attempts) return reject(err);
|
|
445
|
+
const d = currentDelay;
|
|
446
|
+
currentDelay *= backoff;
|
|
447
|
+
setTimeout(tryOnce, d);
|
|
448
|
+
});
|
|
449
|
+
};
|
|
450
|
+
tryOnce();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function timeout(promise, ms, message) {
|
|
455
|
+
let timer;
|
|
456
|
+
const race = Promise.race([
|
|
457
|
+
promise,
|
|
458
|
+
new Promise((_, reject) => {
|
|
459
|
+
timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
|
|
460
|
+
})
|
|
461
|
+
]);
|
|
462
|
+
return race.finally(() => clearTimeout(timer));
|
|
463
|
+
}
|