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/dist/zquery.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery (zeroQuery) v0.9.1
2
+ * zQuery (zeroQuery) v0.9.6
3
3
  * Lightweight Frontend Library
4
4
  * https://github.com/tonywied17/zero-query
5
5
  * (c) 2026 Anthony Wiedman - MIT License
@@ -1218,6 +1218,8 @@ query.children = (parentId) => {
1218
1218
  const p = document.getElementById(parentId);
1219
1219
  return new ZQueryCollection(p ? Array.from(p.children) : []);
1220
1220
  };
1221
+ query.qs = (sel, ctx = document) => ctx.querySelector(sel);
1222
+ query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
1221
1223
 
1222
1224
  // Create element shorthand — returns ZQueryCollection for chaining
1223
1225
  query.create = (tag, attrs = {}, ...children) => {
@@ -1225,7 +1227,7 @@ query.create = (tag, attrs = {}, ...children) => {
1225
1227
  for (const [k, v] of Object.entries(attrs)) {
1226
1228
  if (k === 'class') el.className = v;
1227
1229
  else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
1228
- else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2), v);
1230
+ else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
1229
1231
  else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
1230
1232
  else el.setAttribute(k, v);
1231
1233
  }
@@ -1445,6 +1447,11 @@ function tokenize(expr) {
1445
1447
  tokens.push({ t: T.OP, v: ch });
1446
1448
  i++; continue;
1447
1449
  }
1450
+ // Spread operator: ...
1451
+ if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
1452
+ tokens.push({ t: T.OP, v: '...' });
1453
+ i += 3; continue;
1454
+ }
1448
1455
  if ('()[]{},.?:'.includes(ch)) {
1449
1456
  tokens.push({ t: T.PUNC, v: ch });
1450
1457
  i++; continue;
@@ -1508,7 +1515,7 @@ class Parser {
1508
1515
  this.next(); // consume ?
1509
1516
  const truthy = this.parseExpression(0);
1510
1517
  this.expect(T.PUNC, ':');
1511
- const falsy = this.parseExpression(1);
1518
+ const falsy = this.parseExpression(0);
1512
1519
  left = { type: 'ternary', cond: left, truthy, falsy };
1513
1520
  continue;
1514
1521
  }
@@ -1649,7 +1656,12 @@ class Parser {
1649
1656
  this.expect(T.PUNC, '(');
1650
1657
  const args = [];
1651
1658
  while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
1652
- args.push(this.parseExpression(0));
1659
+ if (this.peek().t === T.OP && this.peek().v === '...') {
1660
+ this.next();
1661
+ args.push({ type: 'spread', arg: this.parseExpression(0) });
1662
+ } else {
1663
+ args.push(this.parseExpression(0));
1664
+ }
1653
1665
  if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
1654
1666
  }
1655
1667
  this.expect(T.PUNC, ')');
@@ -1725,7 +1737,12 @@ class Parser {
1725
1737
  this.next();
1726
1738
  const elements = [];
1727
1739
  while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
1728
- elements.push(this.parseExpression(0));
1740
+ if (this.peek().t === T.OP && this.peek().v === '...') {
1741
+ this.next();
1742
+ elements.push({ type: 'spread', arg: this.parseExpression(0) });
1743
+ } else {
1744
+ elements.push(this.parseExpression(0));
1745
+ }
1729
1746
  if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
1730
1747
  }
1731
1748
  this.expect(T.PUNC, ']');
@@ -1737,6 +1754,14 @@ class Parser {
1737
1754
  this.next();
1738
1755
  const properties = [];
1739
1756
  while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
1757
+ // Spread in object: { ...obj }
1758
+ if (this.peek().t === T.OP && this.peek().v === '...') {
1759
+ this.next();
1760
+ properties.push({ spread: true, value: this.parseExpression(0) });
1761
+ if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
1762
+ continue;
1763
+ }
1764
+
1740
1765
  const keyTok = this.next();
1741
1766
  let key;
1742
1767
  if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
@@ -1768,7 +1793,13 @@ class Parser {
1768
1793
 
1769
1794
  // new keyword
1770
1795
  if (tok.v === 'new') {
1771
- const classExpr = this.parsePostfix();
1796
+ let classExpr = this.parsePrimary();
1797
+ // Handle member access (e.g. ns.MyClass) without consuming call args
1798
+ while (this.peek().t === T.PUNC && this.peek().v === '.') {
1799
+ this.next();
1800
+ const prop = this.next();
1801
+ classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
1802
+ }
1772
1803
  let args = [];
1773
1804
  if (this.peek().t === T.PUNC && this.peek().v === '(') {
1774
1805
  args = this._parseArgs();
@@ -1880,6 +1911,12 @@ function evaluate(node, scope) {
1880
1911
  if (name === 'encodeURIComponent') return encodeURIComponent;
1881
1912
  if (name === 'decodeURIComponent') return decodeURIComponent;
1882
1913
  if (name === 'console') return console;
1914
+ if (name === 'Map') return Map;
1915
+ if (name === 'Set') return Set;
1916
+ if (name === 'RegExp') return RegExp;
1917
+ if (name === 'Error') return Error;
1918
+ if (name === 'URL') return URL;
1919
+ if (name === 'URLSearchParams') return URLSearchParams;
1883
1920
  return undefined;
1884
1921
  }
1885
1922
 
@@ -1918,10 +1955,21 @@ function evaluate(node, scope) {
1918
1955
  }
1919
1956
 
1920
1957
  case 'optional_call': {
1921
- const callee = evaluate(node.callee, scope);
1958
+ const calleeNode = node.callee;
1959
+ const args = _evalArgs(node.args, scope);
1960
+ // Method call: obj?.method() — bind `this` to obj
1961
+ if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
1962
+ const obj = evaluate(calleeNode.obj, scope);
1963
+ if (obj == null) return undefined;
1964
+ const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
1965
+ if (!_isSafeAccess(obj, prop)) return undefined;
1966
+ const fn = obj[prop];
1967
+ if (typeof fn !== 'function') return undefined;
1968
+ return fn.apply(obj, args);
1969
+ }
1970
+ const callee = evaluate(calleeNode, scope);
1922
1971
  if (callee == null) return undefined;
1923
1972
  if (typeof callee !== 'function') return undefined;
1924
- const args = node.args.map(a => evaluate(a, scope));
1925
1973
  return callee(...args);
1926
1974
  }
1927
1975
 
@@ -1931,7 +1979,7 @@ function evaluate(node, scope) {
1931
1979
  // Only allow safe constructors
1932
1980
  if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
1933
1981
  Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
1934
- const args = node.args.map(a => evaluate(a, scope));
1982
+ const args = _evalArgs(node.args, scope);
1935
1983
  return new Ctor(...args);
1936
1984
  }
1937
1985
  return undefined;
@@ -1961,13 +2009,32 @@ function evaluate(node, scope) {
1961
2009
  return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
1962
2010
  }
1963
2011
 
1964
- case 'array':
1965
- return node.elements.map(e => evaluate(e, scope));
2012
+ case 'array': {
2013
+ const arr = [];
2014
+ for (const e of node.elements) {
2015
+ if (e.type === 'spread') {
2016
+ const iterable = evaluate(e.arg, scope);
2017
+ if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
2018
+ for (const v of iterable) arr.push(v);
2019
+ }
2020
+ } else {
2021
+ arr.push(evaluate(e, scope));
2022
+ }
2023
+ }
2024
+ return arr;
2025
+ }
1966
2026
 
1967
2027
  case 'object': {
1968
2028
  const obj = {};
1969
- for (const { key, value } of node.properties) {
1970
- obj[key] = evaluate(value, scope);
2029
+ for (const prop of node.properties) {
2030
+ if (prop.spread) {
2031
+ const source = evaluate(prop.value, scope);
2032
+ if (source != null && typeof source === 'object') {
2033
+ Object.assign(obj, source);
2034
+ }
2035
+ } else {
2036
+ obj[prop.key] = evaluate(prop.value, scope);
2037
+ }
1971
2038
  }
1972
2039
  return obj;
1973
2040
  }
@@ -1988,12 +2055,30 @@ function evaluate(node, scope) {
1988
2055
  }
1989
2056
  }
1990
2057
 
2058
+ /**
2059
+ * Evaluate a list of argument AST nodes, flattening any spread elements.
2060
+ */
2061
+ function _evalArgs(argNodes, scope) {
2062
+ const result = [];
2063
+ for (const a of argNodes) {
2064
+ if (a.type === 'spread') {
2065
+ const iterable = evaluate(a.arg, scope);
2066
+ if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
2067
+ for (const v of iterable) result.push(v);
2068
+ }
2069
+ } else {
2070
+ result.push(evaluate(a, scope));
2071
+ }
2072
+ }
2073
+ return result;
2074
+ }
2075
+
1991
2076
  /**
1992
2077
  * Resolve and execute a function call safely.
1993
2078
  */
1994
2079
  function _resolveCall(node, scope) {
1995
2080
  const callee = node.callee;
1996
- const args = node.args.map(a => evaluate(a, scope));
2081
+ const args = _evalArgs(node.args, scope);
1997
2082
 
1998
2083
  // Method call: obj.method() — bind `this` to obj
1999
2084
  if (callee.type === 'member' || callee.type === 'optional_member') {
@@ -2067,8 +2152,9 @@ function _evalBinary(node, scope) {
2067
2152
  * @returns {*} — evaluation result, or undefined on error
2068
2153
  */
2069
2154
 
2070
- // AST cache — avoids re-tokenizing and re-parsing the same expression string.
2071
- // Bounded to prevent unbounded memory growth in long-lived apps.
2155
+ // AST cache (LRU) — avoids re-tokenizing and re-parsing the same expression.
2156
+ // Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
2157
+ // Eviction removes the least-recently-used (first) entry when at capacity.
2072
2158
  const _astCache = new Map();
2073
2159
  const _AST_CACHE_MAX = 512;
2074
2160
 
@@ -2088,9 +2174,12 @@ function safeEval(expr, scope) {
2088
2174
  // Fall through to full parser for built-in globals (Math, JSON, etc.)
2089
2175
  }
2090
2176
 
2091
- // Check AST cache
2177
+ // Check AST cache (LRU: move to end on hit)
2092
2178
  let ast = _astCache.get(trimmed);
2093
- if (!ast) {
2179
+ if (ast) {
2180
+ _astCache.delete(trimmed);
2181
+ _astCache.set(trimmed, ast);
2182
+ } else {
2094
2183
  const tokens = tokenize(trimmed);
2095
2184
  const parser = new Parser(tokens, scope);
2096
2185
  ast = parser.parse();
@@ -2798,7 +2887,7 @@ class Component {
2798
2887
  const defaultSlotNodes = [];
2799
2888
  [...el.childNodes].forEach(node => {
2800
2889
  if (node.nodeType === 1 && node.hasAttribute('slot')) {
2801
- const slotName = node.getAttribute('slot');
2890
+ const slotName = node.getAttribute('slot') || 'default';
2802
2891
  if (!this._slotContent[slotName]) this._slotContent[slotName] = '';
2803
2892
  this._slotContent[slotName] += node.outerHTML;
2804
2893
  } else if (node.nodeType === 1 || (node.nodeType === 3 && node.textContent.trim())) {
@@ -3207,6 +3296,24 @@ class Component {
3207
3296
  for (const [event, bindings] of eventMap) {
3208
3297
  this._attachDelegatedEvent(event, bindings);
3209
3298
  }
3299
+
3300
+ // .outside — attach a document-level listener for bindings that need
3301
+ // to detect clicks/events outside their element.
3302
+ this._outsideListeners = this._outsideListeners || [];
3303
+ for (const [event, bindings] of eventMap) {
3304
+ for (const binding of bindings) {
3305
+ if (!binding.modifiers.includes('outside')) continue;
3306
+ const outsideHandler = (e) => {
3307
+ if (binding.el.contains(e.target)) return;
3308
+ const match = binding.methodExpr.match(/^(\w+)(?:\(([^)]*)\))?$/);
3309
+ if (!match) return;
3310
+ const fn = this[match[1]];
3311
+ if (typeof fn === 'function') fn.call(this, e);
3312
+ };
3313
+ document.addEventListener(event, outsideHandler, true);
3314
+ this._outsideListeners.push({ event, handler: outsideHandler });
3315
+ }
3316
+ }
3210
3317
  }
3211
3318
 
3212
3319
  // Attach a single delegated listener for an event type
@@ -3220,15 +3327,66 @@ class Component {
3220
3327
  const handler = (e) => {
3221
3328
  // Read bindings from live map — always up to date after re-renders
3222
3329
  const currentBindings = this._eventBindings?.get(event) || [];
3223
- for (const { selector, methodExpr, modifiers, el } of currentBindings) {
3224
- if (!e.target.closest(selector)) continue;
3330
+
3331
+ // Collect matching bindings with their matched elements, then sort
3332
+ // deepest-first so .stop correctly prevents ancestor handlers
3333
+ // (mimics real DOM bubbling order within delegated events).
3334
+ const hits = [];
3335
+ for (const binding of currentBindings) {
3336
+ const matched = e.target.closest(binding.selector);
3337
+ if (!matched) continue;
3338
+ hits.push({ ...binding, matched });
3339
+ }
3340
+ hits.sort((a, b) => {
3341
+ if (a.matched === b.matched) return 0;
3342
+ return a.matched.contains(b.matched) ? 1 : -1;
3343
+ });
3344
+
3345
+ let stoppedAt = null; // Track elements that called .stop
3346
+ for (const { selector, methodExpr, modifiers, el, matched } of hits) {
3347
+
3348
+ // In delegated events, .stop should prevent ancestor bindings from
3349
+ // firing — stopPropagation alone only stops real DOM bubbling.
3350
+ if (stoppedAt) {
3351
+ let blocked = false;
3352
+ for (const stopped of stoppedAt) {
3353
+ if (matched.contains(stopped) && matched !== stopped) { blocked = true; break; }
3354
+ }
3355
+ if (blocked) continue;
3356
+ }
3225
3357
 
3226
3358
  // .self — only fire if target is the element itself
3227
3359
  if (modifiers.includes('self') && e.target !== el) continue;
3228
3360
 
3361
+ // .outside — only fire if event target is OUTSIDE the element
3362
+ if (modifiers.includes('outside')) {
3363
+ if (el.contains(e.target)) continue;
3364
+ }
3365
+
3366
+ // Key modifiers — filter keyboard events by key
3367
+ const _keyMap = { enter: 'Enter', escape: 'Escape', tab: 'Tab', space: ' ', delete: 'Delete|Backspace', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight' };
3368
+ let keyFiltered = false;
3369
+ for (const mod of modifiers) {
3370
+ if (_keyMap[mod]) {
3371
+ const keys = _keyMap[mod].split('|');
3372
+ if (!e.key || !keys.includes(e.key)) { keyFiltered = true; break; }
3373
+ }
3374
+ }
3375
+ if (keyFiltered) continue;
3376
+
3377
+ // System key modifiers — require modifier keys to be held
3378
+ if (modifiers.includes('ctrl') && !e.ctrlKey) continue;
3379
+ if (modifiers.includes('shift') && !e.shiftKey) continue;
3380
+ if (modifiers.includes('alt') && !e.altKey) continue;
3381
+ if (modifiers.includes('meta') && !e.metaKey) continue;
3382
+
3229
3383
  // Handle modifiers
3230
3384
  if (modifiers.includes('prevent')) e.preventDefault();
3231
- if (modifiers.includes('stop')) e.stopPropagation();
3385
+ if (modifiers.includes('stop')) {
3386
+ e.stopPropagation();
3387
+ if (!stoppedAt) stoppedAt = [];
3388
+ stoppedAt.push(matched);
3389
+ }
3232
3390
 
3233
3391
  // Build the invocation function
3234
3392
  const invoke = (evt) => {
@@ -3307,9 +3465,12 @@ class Component {
3307
3465
  // textarea, select (single & multiple), contenteditable
3308
3466
  // Nested state keys: z-model="user.name" → this.state.user.name
3309
3467
  // Modifiers (boolean attributes on the same element):
3310
- // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
3311
- // z-trim — trim whitespace before writing to state
3312
- // z-number — force Number() conversion regardless of input type
3468
+ // z-lazy — listen on 'change' instead of 'input' (update on blur / commit)
3469
+ // z-trim — trim whitespace before writing to state
3470
+ // z-number — force Number() conversion regardless of input type
3471
+ // z-debounce — debounce state writes (default 250ms, or z-debounce="300")
3472
+ // z-uppercase — convert string to uppercase before writing to state
3473
+ // z-lowercase — convert string to lowercase before writing to state
3313
3474
  //
3314
3475
  // Writes to reactive state so the rest of the UI stays in sync.
3315
3476
  // Focus and cursor position are preserved in _render() via focusInfo.
@@ -3325,6 +3486,10 @@ class Component {
3325
3486
  const isLazy = el.hasAttribute('z-lazy');
3326
3487
  const isTrim = el.hasAttribute('z-trim');
3327
3488
  const isNum = el.hasAttribute('z-number');
3489
+ const isUpper = el.hasAttribute('z-uppercase');
3490
+ const isLower = el.hasAttribute('z-lowercase');
3491
+ const hasDebounce = el.hasAttribute('z-debounce');
3492
+ const debounceMs = hasDebounce ? (parseInt(el.getAttribute('z-debounce'), 10) || 250) : 0;
3328
3493
 
3329
3494
  // Read current state value (supports dot-path keys)
3330
3495
  const currentVal = _getPath(this.state, key);
@@ -3365,6 +3530,8 @@ class Component {
3365
3530
 
3366
3531
  // Apply modifiers
3367
3532
  if (isTrim && typeof val === 'string') val = val.trim();
3533
+ if (isUpper && typeof val === 'string') val = val.toUpperCase();
3534
+ if (isLower && typeof val === 'string') val = val.toLowerCase();
3368
3535
  if (isNum || type === 'number' || type === 'range') val = Number(val);
3369
3536
 
3370
3537
  // Write through the reactive proxy (triggers re-render).
@@ -3372,7 +3539,15 @@ class Component {
3372
3539
  _setPath(this.state, key, val);
3373
3540
  };
3374
3541
 
3375
- el.addEventListener(event, handler);
3542
+ if (hasDebounce) {
3543
+ let timer = null;
3544
+ el.addEventListener(event, () => {
3545
+ clearTimeout(timer);
3546
+ timer = setTimeout(handler, debounceMs);
3547
+ });
3548
+ } else {
3549
+ el.addEventListener(event, handler);
3550
+ }
3376
3551
  });
3377
3552
  }
3378
3553
 
@@ -3658,6 +3833,10 @@ class Component {
3658
3833
  }
3659
3834
  this._listeners.forEach(({ event, handler }) => this._el.removeEventListener(event, handler));
3660
3835
  this._listeners = [];
3836
+ if (this._outsideListeners) {
3837
+ this._outsideListeners.forEach(({ event, handler }) => document.removeEventListener(event, handler, true));
3838
+ this._outsideListeners = [];
3839
+ }
3661
3840
  this._delegatedEvents = null;
3662
3841
  this._eventBindings = null;
3663
3842
  // Clear any pending debounce/throttle timers to prevent stale closures.
@@ -4529,7 +4708,12 @@ class Router {
4529
4708
  if (typeof matched.component === 'string') {
4530
4709
  const container = document.createElement(matched.component);
4531
4710
  this._el.appendChild(container);
4532
- this._instance = mount(container, matched.component, props);
4711
+ try {
4712
+ this._instance = mount(container, matched.component, props);
4713
+ } catch (err) {
4714
+ reportError(ErrorCode.COMP_NOT_FOUND, `Failed to mount component for route "${matched.path}"`, { component: matched.component, path: matched.path }, err);
4715
+ return;
4716
+ }
4533
4717
  if (_routeStart) window.__zqRenderHook(this._el, performance.now() - _routeStart, 'route', matched.component);
4534
4718
  }
4535
4719
  // If component is a render function
@@ -4862,8 +5046,9 @@ async function request(method, url, data, options = {}) {
4862
5046
  } else {
4863
5047
  fetchOpts.signal = controller.signal;
4864
5048
  }
5049
+ let _timedOut = false;
4865
5050
  if (timeout > 0) {
4866
- timer = setTimeout(() => controller.abort(), timeout);
5051
+ timer = setTimeout(() => { _timedOut = true; controller.abort(); }, timeout);
4867
5052
  }
4868
5053
 
4869
5054
  // Run request interceptors
@@ -4923,7 +5108,10 @@ async function request(method, url, data, options = {}) {
4923
5108
  } catch (err) {
4924
5109
  if (timer) clearTimeout(timer);
4925
5110
  if (err.name === 'AbortError') {
4926
- throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
5111
+ if (_timedOut) {
5112
+ throw new Error(`Request timeout after ${timeout}ms: ${method} ${fullURL}`);
5113
+ }
5114
+ throw new Error(`Request aborted: ${method} ${fullURL}`);
4927
5115
  }
4928
5116
  throw err;
4929
5117
  }
@@ -5060,6 +5248,10 @@ function escapeHtml(str) {
5060
5248
  return String(str).replace(/[&<>"']/g, c => map[c]);
5061
5249
  }
5062
5250
 
5251
+ function stripHtml(str) {
5252
+ return String(str).replace(/<[^>]*>/g, '');
5253
+ }
5254
+
5063
5255
  /**
5064
5256
  * Template tag for auto-escaping interpolated values
5065
5257
  * Usage: $.html`<div>${userInput}</div>`
@@ -5105,7 +5297,10 @@ function camelCase(str) {
5105
5297
  * CamelCase to kebab-case
5106
5298
  */
5107
5299
  function kebabCase(str) {
5108
- return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
5300
+ return str
5301
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
5302
+ .replace(/([a-z\d])([A-Z])/g, '$1-$2')
5303
+ .toLowerCase();
5109
5304
  }
5110
5305
 
5111
5306
 
@@ -5146,15 +5341,19 @@ function deepMerge(target, ...sources) {
5146
5341
  /**
5147
5342
  * Simple object equality check
5148
5343
  */
5149
- function isEqual(a, b) {
5344
+ function isEqual(a, b, _seen) {
5150
5345
  if (a === b) return true;
5151
5346
  if (typeof a !== typeof b) return false;
5152
5347
  if (typeof a !== 'object' || a === null || b === null) return false;
5153
5348
  if (Array.isArray(a) !== Array.isArray(b)) return false;
5349
+ // Guard against circular references
5350
+ if (!_seen) _seen = new Set();
5351
+ if (_seen.has(a)) return true;
5352
+ _seen.add(a);
5154
5353
  const keysA = Object.keys(a);
5155
5354
  const keysB = Object.keys(b);
5156
5355
  if (keysA.length !== keysB.length) return false;
5157
- return keysA.every(k => isEqual(a[k], b[k]));
5356
+ return keysA.every(k => isEqual(a[k], b[k], _seen));
5158
5357
  }
5159
5358
 
5160
5359
 
@@ -5255,7 +5454,182 @@ class EventBus {
5255
5454
  clear() { this._handlers.clear(); }
5256
5455
  }
5257
5456
 
5258
- const bus = new EventBus();
5457
+ const bus = new EventBus();
5458
+
5459
+
5460
+ // ---------------------------------------------------------------------------
5461
+ // Array utilities
5462
+ // ---------------------------------------------------------------------------
5463
+
5464
+ function range(startOrEnd, end, step) {
5465
+ let s, e, st;
5466
+ if (end === undefined) { s = 0; e = startOrEnd; st = 1; }
5467
+ else { s = startOrEnd; e = end; st = step !== undefined ? step : 1; }
5468
+ if (st === 0) return [];
5469
+ const result = [];
5470
+ if (st > 0) { for (let i = s; i < e; i += st) result.push(i); }
5471
+ else { for (let i = s; i > e; i += st) result.push(i); }
5472
+ return result;
5473
+ }
5474
+
5475
+ function unique(arr, keyFn) {
5476
+ if (!keyFn) return [...new Set(arr)];
5477
+ const seen = new Set();
5478
+ return arr.filter(item => {
5479
+ const k = keyFn(item);
5480
+ if (seen.has(k)) return false;
5481
+ seen.add(k);
5482
+ return true;
5483
+ });
5484
+ }
5485
+
5486
+ function chunk(arr, size) {
5487
+ const result = [];
5488
+ for (let i = 0; i < arr.length; i += size) result.push(arr.slice(i, i + size));
5489
+ return result;
5490
+ }
5491
+
5492
+ function groupBy(arr, keyFn) {
5493
+ const result = {};
5494
+ for (const item of arr) {
5495
+ const k = keyFn(item);
5496
+ (result[k] ??= []).push(item);
5497
+ }
5498
+ return result;
5499
+ }
5500
+
5501
+
5502
+ // ---------------------------------------------------------------------------
5503
+ // Object utilities
5504
+ // ---------------------------------------------------------------------------
5505
+
5506
+ function pick(obj, keys) {
5507
+ const result = {};
5508
+ for (const k of keys) { if (k in obj) result[k] = obj[k]; }
5509
+ return result;
5510
+ }
5511
+
5512
+ function omit(obj, keys) {
5513
+ const exclude = new Set(keys);
5514
+ const result = {};
5515
+ for (const k of Object.keys(obj)) { if (!exclude.has(k)) result[k] = obj[k]; }
5516
+ return result;
5517
+ }
5518
+
5519
+ function getPath(obj, path, fallback) {
5520
+ const keys = path.split('.');
5521
+ let cur = obj;
5522
+ for (const k of keys) {
5523
+ if (cur == null || typeof cur !== 'object') return fallback;
5524
+ cur = cur[k];
5525
+ }
5526
+ return cur === undefined ? fallback : cur;
5527
+ }
5528
+
5529
+ function setPath(obj, path, value) {
5530
+ const keys = path.split('.');
5531
+ let cur = obj;
5532
+ for (let i = 0; i < keys.length - 1; i++) {
5533
+ const k = keys[i];
5534
+ if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = {};
5535
+ cur = cur[k];
5536
+ }
5537
+ cur[keys[keys.length - 1]] = value;
5538
+ return obj;
5539
+ }
5540
+
5541
+ function isEmpty(val) {
5542
+ if (val == null) return true;
5543
+ if (typeof val === 'string' || Array.isArray(val)) return val.length === 0;
5544
+ if (val instanceof Map || val instanceof Set) return val.size === 0;
5545
+ if (typeof val === 'object') return Object.keys(val).length === 0;
5546
+ return false;
5547
+ }
5548
+
5549
+
5550
+ // ---------------------------------------------------------------------------
5551
+ // String utilities
5552
+ // ---------------------------------------------------------------------------
5553
+
5554
+ function capitalize(str) {
5555
+ if (!str) return '';
5556
+ return str[0].toUpperCase() + str.slice(1).toLowerCase();
5557
+ }
5558
+
5559
+ function truncate(str, maxLen, suffix = '…') {
5560
+ if (str.length <= maxLen) return str;
5561
+ const end = Math.max(0, maxLen - suffix.length);
5562
+ return str.slice(0, end) + suffix;
5563
+ }
5564
+
5565
+
5566
+ // ---------------------------------------------------------------------------
5567
+ // Number utilities
5568
+ // ---------------------------------------------------------------------------
5569
+
5570
+ function clamp(val, min, max) {
5571
+ return val < min ? min : val > max ? max : val;
5572
+ }
5573
+
5574
+
5575
+ // ---------------------------------------------------------------------------
5576
+ // Function utilities
5577
+ // ---------------------------------------------------------------------------
5578
+
5579
+ function memoize(fn, keyFnOrOpts) {
5580
+ let keyFn, maxSize = 0;
5581
+ if (typeof keyFnOrOpts === 'function') keyFn = keyFnOrOpts;
5582
+ else if (keyFnOrOpts && typeof keyFnOrOpts === 'object') maxSize = keyFnOrOpts.maxSize || 0;
5583
+
5584
+ const cache = new Map();
5585
+
5586
+ const memoized = (...args) => {
5587
+ const key = keyFn ? keyFn(...args) : args[0];
5588
+ if (cache.has(key)) return cache.get(key);
5589
+ const result = fn(...args);
5590
+ cache.set(key, result);
5591
+ if (maxSize > 0 && cache.size > maxSize) {
5592
+ cache.delete(cache.keys().next().value);
5593
+ }
5594
+ return result;
5595
+ };
5596
+
5597
+ memoized.clear = () => cache.clear();
5598
+ return memoized;
5599
+ }
5600
+
5601
+
5602
+ // ---------------------------------------------------------------------------
5603
+ // Async utilities
5604
+ // ---------------------------------------------------------------------------
5605
+
5606
+ function retry(fn, opts = {}) {
5607
+ const { attempts = 3, delay = 1000, backoff = 1 } = opts;
5608
+ return new Promise((resolve, reject) => {
5609
+ let attempt = 0, currentDelay = delay;
5610
+ const tryOnce = () => {
5611
+ attempt++;
5612
+ fn(attempt).then(resolve, (err) => {
5613
+ if (attempt >= attempts) return reject(err);
5614
+ const d = currentDelay;
5615
+ currentDelay *= backoff;
5616
+ setTimeout(tryOnce, d);
5617
+ });
5618
+ };
5619
+ tryOnce();
5620
+ });
5621
+ }
5622
+
5623
+ function timeout(promise, ms, message) {
5624
+ let timer;
5625
+ const race = Promise.race([
5626
+ promise,
5627
+ new Promise((_, reject) => {
5628
+ timer = setTimeout(() => reject(new Error(message || `Timed out after ${ms}ms`)), ms);
5629
+ })
5630
+ ]);
5631
+ return race.finally(() => clearTimeout(timer));
5632
+ }
5259
5633
 
5260
5634
  // --- index.js (assembly) ------------------------------------------
5261
5635
  /**
@@ -5314,6 +5688,8 @@ Object.defineProperty($, 'name', {
5314
5688
  value: query.name, writable: true, configurable: true
5315
5689
  });
5316
5690
  $.children = query.children;
5691
+ $.qs = query.qs;
5692
+ $.qsa = query.qsa;
5317
5693
 
5318
5694
  // --- Collection selector ---------------------------------------------------
5319
5695
  /**
@@ -5382,8 +5758,10 @@ $.pipe = pipe;
5382
5758
  $.once = once;
5383
5759
  $.sleep = sleep;
5384
5760
  $.escapeHtml = escapeHtml;
5761
+ $.stripHtml = stripHtml;
5385
5762
  $.html = html;
5386
5763
  $.trust = trust;
5764
+ $.TrustedHTML = TrustedHTML;
5387
5765
  $.uuid = uuid;
5388
5766
  $.camelCase = camelCase;
5389
5767
  $.kebabCase = kebabCase;
@@ -5394,7 +5772,23 @@ $.param = param;
5394
5772
  $.parseQuery = parseQuery;
5395
5773
  $.storage = storage;
5396
5774
  $.session = session;
5775
+ $.EventBus = EventBus;
5397
5776
  $.bus = bus;
5777
+ $.range = range;
5778
+ $.unique = unique;
5779
+ $.chunk = chunk;
5780
+ $.groupBy = groupBy;
5781
+ $.pick = pick;
5782
+ $.omit = omit;
5783
+ $.getPath = getPath;
5784
+ $.setPath = setPath;
5785
+ $.isEmpty = isEmpty;
5786
+ $.capitalize = capitalize;
5787
+ $.truncate = truncate;
5788
+ $.clamp = clamp;
5789
+ $.memoize = memoize;
5790
+ $.retry = retry;
5791
+ $.timeout = timeout;
5398
5792
 
5399
5793
  // --- Error handling --------------------------------------------------------
5400
5794
  $.onError = onError;
@@ -5404,8 +5798,8 @@ $.guardCallback = guardCallback;
5404
5798
  $.validate = validate;
5405
5799
 
5406
5800
  // --- Meta ------------------------------------------------------------------
5407
- $.version = '0.9.1';
5408
- $.libSize = '~92 KB';
5801
+ $.version = '0.9.6';
5802
+ $.libSize = '~100 KB';
5409
5803
  $.meta = {}; // populated at build time by CLI bundler
5410
5804
 
5411
5805
  $.noConflict = () => {