zero-query 0.9.7 → 0.9.8
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 +31 -3
- package/cli/commands/build.js +50 -3
- package/cli/commands/create.js +22 -9
- package/cli/help.js +2 -0
- package/cli/scaffold/default/app/app.js +211 -0
- package/cli/scaffold/default/app/components/about.js +201 -0
- package/cli/scaffold/default/app/components/api-demo.js +143 -0
- package/cli/scaffold/default/app/components/contact-card.js +231 -0
- package/cli/scaffold/default/app/components/contacts/contacts.css +706 -0
- package/cli/scaffold/default/app/components/contacts/contacts.html +200 -0
- package/cli/scaffold/default/app/components/contacts/contacts.js +196 -0
- package/cli/scaffold/default/app/components/counter.js +127 -0
- package/cli/scaffold/default/app/components/home.js +249 -0
- package/cli/scaffold/{app → default/app}/components/not-found.js +2 -2
- package/cli/scaffold/default/app/components/playground/playground.css +116 -0
- package/cli/scaffold/default/app/components/playground/playground.html +162 -0
- package/cli/scaffold/default/app/components/playground/playground.js +117 -0
- package/cli/scaffold/default/app/components/todos.js +225 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -0
- package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -0
- package/cli/scaffold/default/app/routes.js +15 -0
- package/cli/scaffold/{app → default/app}/store.js +15 -10
- package/cli/scaffold/{global.css → default/global.css} +238 -252
- package/cli/scaffold/{index.html → default/index.html} +35 -0
- package/cli/scaffold/{app → minimal/app}/app.js +37 -39
- package/cli/scaffold/minimal/app/components/about.js +68 -0
- package/cli/scaffold/minimal/app/components/counter.js +122 -0
- package/cli/scaffold/minimal/app/components/home.js +68 -0
- package/cli/scaffold/minimal/app/components/not-found.js +16 -0
- package/cli/scaffold/minimal/app/routes.js +9 -0
- package/cli/scaffold/minimal/app/store.js +36 -0
- package/cli/scaffold/minimal/assets/.gitkeep +0 -0
- package/cli/scaffold/minimal/global.css +291 -0
- package/cli/scaffold/minimal/index.html +44 -0
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +1942 -1925
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +10 -1
- package/index.js +4 -3
- package/package.json +1 -1
- package/src/component.js +6 -3
- package/src/diff.js +15 -2
- package/tests/cli.test.js +304 -0
- package/cli/scaffold/app/components/about.js +0 -131
- package/cli/scaffold/app/components/api-demo.js +0 -103
- package/cli/scaffold/app/components/contacts/contacts.css +0 -246
- package/cli/scaffold/app/components/contacts/contacts.html +0 -140
- package/cli/scaffold/app/components/contacts/contacts.js +0 -153
- package/cli/scaffold/app/components/counter.js +0 -85
- package/cli/scaffold/app/components/home.js +0 -137
- package/cli/scaffold/app/components/todos.js +0 -131
- package/cli/scaffold/app/routes.js +0 -13
- /package/cli/scaffold/{LICENSE → default/LICENSE} +0 -0
- /package/cli/scaffold/{assets → default/assets}/.gitkeep +0 -0
- /package/cli/scaffold/{favicon.ico → default/favicon.ico} +0 -0
package/dist/zquery.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* zQuery (zeroQuery) v0.9.
|
|
2
|
+
* zQuery (zeroQuery) v0.9.8
|
|
3
3
|
* Lightweight Frontend Library
|
|
4
4
|
* https://github.com/tonywied17/zero-query
|
|
5
5
|
* (c) 2026 Anthony Wiedman - MIT License
|
|
@@ -353,2338 +353,2351 @@ function effect(fn) {
|
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
// --- src/
|
|
356
|
+
// --- src/diff.js -------------------------------------------------
|
|
357
357
|
/**
|
|
358
|
-
* zQuery
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
358
|
+
* zQuery Diff — Lightweight DOM morphing engine
|
|
359
|
+
*
|
|
360
|
+
* Patches an existing DOM tree to match new HTML without destroying nodes
|
|
361
|
+
* that haven't changed. Preserves focus, scroll positions, third-party
|
|
362
|
+
* widget state, video playback, and other live DOM state.
|
|
363
|
+
*
|
|
364
|
+
* Approach: walk old and new trees in parallel, reconcile node by node.
|
|
365
|
+
* Keyed elements (via `z-key`) get matched across position changes.
|
|
366
|
+
*
|
|
367
|
+
* Performance advantages over virtual DOM (React/Angular):
|
|
368
|
+
* - No virtual tree allocation or diffing — works directly on real DOM
|
|
369
|
+
* - Skips unchanged subtrees via fast isEqualNode() check
|
|
370
|
+
* - z-skip attribute to opt out of diffing entire subtrees
|
|
371
|
+
* - Reuses a single template element for HTML parsing (zero GC pressure)
|
|
372
|
+
* - Keyed reconciliation uses LIS (Longest Increasing Subsequence) to
|
|
373
|
+
* minimize DOM moves — same algorithm as Vue 3 / ivi
|
|
374
|
+
* - Minimal attribute diffing with early bail-out
|
|
362
375
|
*/
|
|
363
376
|
|
|
364
|
-
|
|
365
377
|
// ---------------------------------------------------------------------------
|
|
366
|
-
//
|
|
378
|
+
// Reusable template element — avoids per-call allocation
|
|
367
379
|
// ---------------------------------------------------------------------------
|
|
368
|
-
|
|
369
|
-
constructor(elements) {
|
|
370
|
-
this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
|
|
371
|
-
this.length = this.elements.length;
|
|
372
|
-
this.elements.forEach((el, i) => { this[i] = el; });
|
|
373
|
-
}
|
|
380
|
+
let _tpl = null;
|
|
374
381
|
|
|
375
|
-
|
|
382
|
+
function _getTemplate() {
|
|
383
|
+
if (!_tpl) _tpl = document.createElement('template');
|
|
384
|
+
return _tpl;
|
|
385
|
+
}
|
|
376
386
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// morph(existingRoot, newHTML) — patch existing DOM to match newHTML
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
381
390
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
391
|
+
/**
|
|
392
|
+
* Morph an existing DOM element's children to match new HTML.
|
|
393
|
+
* Only touches nodes that actually differ.
|
|
394
|
+
*
|
|
395
|
+
* @param {Element} rootEl — The live DOM container to patch
|
|
396
|
+
* @param {string} newHTML — The desired HTML string
|
|
397
|
+
*/
|
|
398
|
+
function morph(rootEl, newHTML) {
|
|
399
|
+
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
400
|
+
const tpl = _getTemplate();
|
|
401
|
+
tpl.innerHTML = newHTML;
|
|
402
|
+
const newRoot = tpl.content;
|
|
385
403
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
404
|
+
// Move children into a wrapper for consistent handling.
|
|
405
|
+
// We move (not clone) from the template — cheaper than cloning.
|
|
406
|
+
const tempDiv = document.createElement('div');
|
|
407
|
+
while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
|
|
390
408
|
|
|
391
|
-
|
|
392
|
-
last() { return this.elements[this.length - 1] || null; }
|
|
393
|
-
eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
|
|
394
|
-
toArray(){ return [...this.elements]; }
|
|
409
|
+
_morphChildren(rootEl, tempDiv);
|
|
395
410
|
|
|
396
|
-
|
|
411
|
+
if (start) window.__zqMorphHook(rootEl, performance.now() - start);
|
|
412
|
+
}
|
|
397
413
|
|
|
398
|
-
|
|
414
|
+
/**
|
|
415
|
+
* Morph a single element in place — diffs attributes and children
|
|
416
|
+
* without replacing the node reference. Useful for replaceWith-style
|
|
417
|
+
* updates where you want to keep the element identity when the tag
|
|
418
|
+
* name matches.
|
|
419
|
+
*
|
|
420
|
+
* If the new HTML produces a different tag, falls back to native replace.
|
|
421
|
+
*
|
|
422
|
+
* @param {Element} oldEl — The live DOM element to patch
|
|
423
|
+
* @param {string} newHTML — HTML string for the replacement element
|
|
424
|
+
* @returns {Element} — The resulting element (same ref if morphed, new if replaced)
|
|
425
|
+
*/
|
|
426
|
+
function morphElement(oldEl, newHTML) {
|
|
427
|
+
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
428
|
+
const tpl = _getTemplate();
|
|
429
|
+
tpl.innerHTML = newHTML;
|
|
430
|
+
const newEl = tpl.content.firstElementChild;
|
|
431
|
+
if (!newEl) return oldEl;
|
|
399
432
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
433
|
+
// Same tag — morph in place (preserves identity, event listeners, refs)
|
|
434
|
+
if (oldEl.nodeName === newEl.nodeName) {
|
|
435
|
+
_morphAttributes(oldEl, newEl);
|
|
436
|
+
_morphChildren(oldEl, newEl);
|
|
437
|
+
if (start) window.__zqMorphHook(oldEl, performance.now() - start);
|
|
438
|
+
return oldEl;
|
|
404
439
|
}
|
|
405
440
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
441
|
+
// Different tag — must replace (can't morph <div> into <span>)
|
|
442
|
+
const clone = newEl.cloneNode(true);
|
|
443
|
+
oldEl.parentNode.replaceChild(clone, oldEl);
|
|
444
|
+
if (start) window.__zqMorphHook(clone, performance.now() - start);
|
|
445
|
+
return clone;
|
|
446
|
+
}
|
|
410
447
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
448
|
+
// Aliases for the concat build — core.js imports these as _morph / _morphElement,
|
|
449
|
+
// but the build strips `import … as` lines, so the aliases must exist at runtime.
|
|
450
|
+
const _morph = morph;
|
|
451
|
+
const _morphElement = morphElement;
|
|
416
452
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Reconcile children of `oldParent` to match `newParent`.
|
|
455
|
+
*
|
|
456
|
+
* @param {Element} oldParent — live DOM parent
|
|
457
|
+
* @param {Element} newParent — desired state parent
|
|
458
|
+
*/
|
|
459
|
+
function _morphChildren(oldParent, newParent) {
|
|
460
|
+
// Snapshot live NodeLists into arrays — childNodes is live and
|
|
461
|
+
// mutates during insertBefore/removeChild. Using a for loop to push
|
|
462
|
+
// avoids spread operator overhead for large child lists.
|
|
463
|
+
const oldCN = oldParent.childNodes;
|
|
464
|
+
const newCN = newParent.childNodes;
|
|
465
|
+
const oldLen = oldCN.length;
|
|
466
|
+
const newLen = newCN.length;
|
|
467
|
+
const oldChildren = new Array(oldLen);
|
|
468
|
+
const newChildren = new Array(newLen);
|
|
469
|
+
for (let i = 0; i < oldLen; i++) oldChildren[i] = oldCN[i];
|
|
470
|
+
for (let i = 0; i < newLen; i++) newChildren[i] = newCN[i];
|
|
426
471
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (!el.parentElement) return;
|
|
431
|
-
const all = [...el.parentElement.children].filter(c => c !== el);
|
|
432
|
-
sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
|
|
433
|
-
});
|
|
434
|
-
return new ZQueryCollection(sibs);
|
|
435
|
-
}
|
|
472
|
+
// Scan for keyed elements — only build maps if keys exist
|
|
473
|
+
let hasKeys = false;
|
|
474
|
+
let oldKeyMap, newKeyMap;
|
|
436
475
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
476
|
+
for (let i = 0; i < oldLen; i++) {
|
|
477
|
+
if (_getKey(oldChildren[i]) != null) { hasKeys = true; break; }
|
|
440
478
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
479
|
+
if (!hasKeys) {
|
|
480
|
+
for (let i = 0; i < newLen; i++) {
|
|
481
|
+
if (_getKey(newChildren[i]) != null) { hasKeys = true; break; }
|
|
482
|
+
}
|
|
445
483
|
}
|
|
446
484
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
485
|
+
if (hasKeys) {
|
|
486
|
+
oldKeyMap = new Map();
|
|
487
|
+
newKeyMap = new Map();
|
|
488
|
+
for (let i = 0; i < oldLen; i++) {
|
|
489
|
+
const key = _getKey(oldChildren[i]);
|
|
490
|
+
if (key != null) oldKeyMap.set(key, i);
|
|
491
|
+
}
|
|
492
|
+
for (let i = 0; i < newLen; i++) {
|
|
493
|
+
const key = _getKey(newChildren[i]);
|
|
494
|
+
if (key != null) newKeyMap.set(key, i);
|
|
495
|
+
}
|
|
496
|
+
_morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap);
|
|
497
|
+
} else {
|
|
498
|
+
_morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
|
|
457
499
|
}
|
|
500
|
+
}
|
|
458
501
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
502
|
+
/**
|
|
503
|
+
* Unkeyed reconciliation — positional matching.
|
|
504
|
+
*/
|
|
505
|
+
function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
506
|
+
const oldLen = oldChildren.length;
|
|
507
|
+
const newLen = newChildren.length;
|
|
508
|
+
const minLen = oldLen < newLen ? oldLen : newLen;
|
|
509
|
+
|
|
510
|
+
// Morph overlapping range
|
|
511
|
+
for (let i = 0; i < minLen; i++) {
|
|
512
|
+
_morphNode(oldParent, oldChildren[i], newChildren[i]);
|
|
470
513
|
}
|
|
471
514
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (!selector || sib.matches(selector)) result.push(sib);
|
|
478
|
-
sib = sib.previousElementSibling;
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
return new ZQueryCollection(result);
|
|
515
|
+
// Append new nodes
|
|
516
|
+
if (newLen > oldLen) {
|
|
517
|
+
for (let i = oldLen; i < newLen; i++) {
|
|
518
|
+
oldParent.appendChild(newChildren[i].cloneNode(true));
|
|
519
|
+
}
|
|
482
520
|
}
|
|
483
521
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (selector && sib.matches(selector)) break;
|
|
490
|
-
if (!filter || sib.matches(filter)) result.push(sib);
|
|
491
|
-
sib = sib.previousElementSibling;
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
return new ZQueryCollection(result);
|
|
522
|
+
// Remove excess old nodes (iterate backwards to avoid index shifting)
|
|
523
|
+
if (oldLen > newLen) {
|
|
524
|
+
for (let i = oldLen - 1; i >= newLen; i--) {
|
|
525
|
+
oldParent.removeChild(oldChildren[i]);
|
|
526
|
+
}
|
|
495
527
|
}
|
|
528
|
+
}
|
|
496
529
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
parentsUntil(selector, filter) {
|
|
510
|
-
const result = [];
|
|
511
|
-
this.elements.forEach(el => {
|
|
512
|
-
let parent = el.parentElement;
|
|
513
|
-
while (parent) {
|
|
514
|
-
if (selector && parent.matches(selector)) break;
|
|
515
|
-
if (!filter || parent.matches(filter)) result.push(parent);
|
|
516
|
-
parent = parent.parentElement;
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
return new ZQueryCollection([...new Set(result)]);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
contents() {
|
|
523
|
-
const result = [];
|
|
524
|
-
this.elements.forEach(el => result.push(...el.childNodes));
|
|
525
|
-
return new ZQueryCollection(result);
|
|
526
|
-
}
|
|
530
|
+
/**
|
|
531
|
+
* Keyed reconciliation — match by z-key, reorder with minimal moves
|
|
532
|
+
* using Longest Increasing Subsequence (LIS) to find the maximum set
|
|
533
|
+
* of nodes that are already in the correct relative order, then only
|
|
534
|
+
* move the remaining nodes.
|
|
535
|
+
*/
|
|
536
|
+
function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap) {
|
|
537
|
+
const consumed = new Set();
|
|
538
|
+
const newLen = newChildren.length;
|
|
539
|
+
const matched = new Array(newLen);
|
|
527
540
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
541
|
+
// Step 1: Match new children to old children by key
|
|
542
|
+
for (let i = 0; i < newLen; i++) {
|
|
543
|
+
const key = _getKey(newChildren[i]);
|
|
544
|
+
if (key != null && oldKeyMap.has(key)) {
|
|
545
|
+
const oldIdx = oldKeyMap.get(key);
|
|
546
|
+
matched[i] = oldChildren[oldIdx];
|
|
547
|
+
consumed.add(oldIdx);
|
|
548
|
+
} else {
|
|
549
|
+
matched[i] = null;
|
|
531
550
|
}
|
|
532
|
-
return new ZQueryCollection(this.elements.filter(el => el.matches(selector)));
|
|
533
551
|
}
|
|
534
552
|
|
|
535
|
-
not
|
|
536
|
-
|
|
537
|
-
|
|
553
|
+
// Step 2: Remove old keyed nodes not in the new tree
|
|
554
|
+
for (let i = oldChildren.length - 1; i >= 0; i--) {
|
|
555
|
+
if (!consumed.has(i)) {
|
|
556
|
+
const key = _getKey(oldChildren[i]);
|
|
557
|
+
if (key != null && !newKeyMap.has(key)) {
|
|
558
|
+
oldParent.removeChild(oldChildren[i]);
|
|
559
|
+
}
|
|
538
560
|
}
|
|
539
|
-
return new ZQueryCollection(this.elements.filter(el => !el.matches(selector)));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
has(selector) {
|
|
543
|
-
return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
|
|
544
561
|
}
|
|
545
562
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
563
|
+
// Step 3: Build index array for LIS of matched old indices.
|
|
564
|
+
// This finds the largest set of keyed nodes already in order,
|
|
565
|
+
// so we only need to move the rest — O(n log n) instead of O(n²).
|
|
566
|
+
const oldIndices = []; // Maps new-position → old-position (or -1)
|
|
567
|
+
for (let i = 0; i < newLen; i++) {
|
|
568
|
+
if (matched[i]) {
|
|
569
|
+
const key = _getKey(newChildren[i]);
|
|
570
|
+
oldIndices.push(oldKeyMap.get(key));
|
|
571
|
+
} else {
|
|
572
|
+
oldIndices.push(-1);
|
|
549
573
|
}
|
|
550
|
-
return this.elements.some(el => el.matches(selector));
|
|
551
574
|
}
|
|
552
575
|
|
|
553
|
-
|
|
554
|
-
return new ZQueryCollection(this.elements.slice(start, end));
|
|
555
|
-
}
|
|
576
|
+
const lisSet = _lis(oldIndices);
|
|
556
577
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
578
|
+
// Step 4: Insert / reorder / morph — walk new children forward,
|
|
579
|
+
// using LIS to decide which nodes stay in place.
|
|
580
|
+
let cursor = oldParent.firstChild;
|
|
581
|
+
const unkeyedOld = [];
|
|
582
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
583
|
+
if (!consumed.has(i) && _getKey(oldChildren[i]) == null) {
|
|
584
|
+
unkeyedOld.push(oldChildren[i]);
|
|
585
|
+
}
|
|
564
586
|
}
|
|
587
|
+
let unkeyedIdx = 0;
|
|
565
588
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
589
|
+
for (let i = 0; i < newLen; i++) {
|
|
590
|
+
const newNode = newChildren[i];
|
|
591
|
+
const newKey = _getKey(newNode);
|
|
592
|
+
let oldNode = matched[i];
|
|
570
593
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const el = this.first();
|
|
574
|
-
if (!el || !el.parentElement) return -1;
|
|
575
|
-
return Array.from(el.parentElement.children).indexOf(el);
|
|
594
|
+
if (!oldNode && newKey == null) {
|
|
595
|
+
oldNode = unkeyedOld[unkeyedIdx++] || null;
|
|
576
596
|
}
|
|
577
|
-
const target = (typeof selector === 'string')
|
|
578
|
-
? document.querySelector(selector)
|
|
579
|
-
: selector;
|
|
580
|
-
return this.elements.indexOf(target);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// --- Classes -------------------------------------------------------------
|
|
584
597
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
598
|
+
if (oldNode) {
|
|
599
|
+
// If this node is NOT part of the LIS, it needs to be moved
|
|
600
|
+
if (!lisSet.has(i)) {
|
|
601
|
+
oldParent.insertBefore(oldNode, cursor);
|
|
602
|
+
}
|
|
603
|
+
// Capture next sibling BEFORE _morphNode — if _morphNode calls
|
|
604
|
+
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
605
|
+
const nextSib = oldNode.nextSibling;
|
|
606
|
+
_morphNode(oldParent, oldNode, newNode);
|
|
607
|
+
cursor = nextSib;
|
|
608
|
+
} else {
|
|
609
|
+
// Insert new node
|
|
610
|
+
const clone = newNode.cloneNode(true);
|
|
611
|
+
if (cursor) {
|
|
612
|
+
oldParent.insertBefore(clone, cursor);
|
|
613
|
+
} else {
|
|
614
|
+
oldParent.appendChild(clone);
|
|
615
|
+
}
|
|
591
616
|
}
|
|
592
|
-
const classes = names.flatMap(n => n.split(/\s+/));
|
|
593
|
-
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(...classes);
|
|
594
|
-
return this;
|
|
595
617
|
}
|
|
596
618
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
619
|
+
// Remove remaining unkeyed old nodes
|
|
620
|
+
while (unkeyedIdx < unkeyedOld.length) {
|
|
621
|
+
const leftover = unkeyedOld[unkeyedIdx++];
|
|
622
|
+
if (leftover.parentNode === oldParent) {
|
|
623
|
+
oldParent.removeChild(leftover);
|
|
602
624
|
}
|
|
603
|
-
const classes = names.flatMap(n => n.split(/\s+/));
|
|
604
|
-
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(...classes);
|
|
605
|
-
return this;
|
|
606
625
|
}
|
|
607
626
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
force !== undefined ? this.elements[i].classList.toggle(c, force) : this.elements[i].classList.toggle(c);
|
|
615
|
-
}
|
|
616
|
-
return this;
|
|
617
|
-
}
|
|
618
|
-
const classes = args.flatMap(n => n.split(/\s+/));
|
|
619
|
-
for (let i = 0; i < this.elements.length; i++) {
|
|
620
|
-
const el = this.elements[i];
|
|
621
|
-
for (let j = 0; j < classes.length; j++) {
|
|
622
|
-
force !== undefined ? el.classList.toggle(classes[j], force) : el.classList.toggle(classes[j]);
|
|
627
|
+
// Remove any remaining keyed old nodes that weren't consumed
|
|
628
|
+
for (let i = 0; i < oldChildren.length; i++) {
|
|
629
|
+
if (!consumed.has(i)) {
|
|
630
|
+
const node = oldChildren[i];
|
|
631
|
+
if (node.parentNode === oldParent && _getKey(node) != null && !newKeyMap.has(_getKey(node))) {
|
|
632
|
+
oldParent.removeChild(node);
|
|
623
633
|
}
|
|
624
634
|
}
|
|
625
|
-
return this;
|
|
626
635
|
}
|
|
636
|
+
}
|
|
627
637
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
638
|
+
/**
|
|
639
|
+
* Compute the Longest Increasing Subsequence of an index array.
|
|
640
|
+
* Returns a Set of positions (in the input) that form the LIS.
|
|
641
|
+
* Entries with value -1 (unmatched) are excluded.
|
|
642
|
+
*
|
|
643
|
+
* O(n log n) — same algorithm used by Vue 3 and ivi.
|
|
644
|
+
*
|
|
645
|
+
* @param {number[]} arr — array of old-tree indices (-1 = unmatched)
|
|
646
|
+
* @returns {Set<number>} — positions in arr belonging to the LIS
|
|
647
|
+
*/
|
|
648
|
+
function _lis(arr) {
|
|
649
|
+
const len = arr.length;
|
|
650
|
+
const result = new Set();
|
|
651
|
+
if (len === 0) return result;
|
|
631
652
|
|
|
632
|
-
//
|
|
653
|
+
// tails[i] = index in arr of the smallest tail element for LIS of length i+1
|
|
654
|
+
const tails = [];
|
|
655
|
+
// prev[i] = predecessor index in arr for the LIS ending at arr[i]
|
|
656
|
+
const prev = new Array(len).fill(-1);
|
|
657
|
+
const tailIndices = []; // parallel to tails: actual positions
|
|
633
658
|
|
|
634
|
-
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
for (const [k, v] of Object.entries(name)) el.setAttribute(k, v);
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
if (value === undefined) return this.first()?.getAttribute(name);
|
|
641
|
-
return this.each((_, el) => el.setAttribute(name, value));
|
|
642
|
-
}
|
|
659
|
+
for (let i = 0; i < len; i++) {
|
|
660
|
+
if (arr[i] === -1) continue;
|
|
661
|
+
const val = arr[i];
|
|
643
662
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
663
|
+
// Binary search for insertion point in tails
|
|
664
|
+
let lo = 0, hi = tails.length;
|
|
665
|
+
while (lo < hi) {
|
|
666
|
+
const mid = (lo + hi) >> 1;
|
|
667
|
+
if (tails[mid] < val) lo = mid + 1;
|
|
668
|
+
else hi = mid;
|
|
669
|
+
}
|
|
647
670
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
671
|
+
tails[lo] = val;
|
|
672
|
+
tailIndices[lo] = i;
|
|
673
|
+
prev[i] = lo > 0 ? tailIndices[lo - 1] : -1;
|
|
651
674
|
}
|
|
652
675
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; });
|
|
676
|
+
// Reconstruct: walk backwards from the last element of LIS
|
|
677
|
+
let k = tailIndices[tails.length - 1];
|
|
678
|
+
for (let i = tails.length - 1; i >= 0; i--) {
|
|
679
|
+
result.add(k);
|
|
680
|
+
k = prev[k];
|
|
660
681
|
}
|
|
661
682
|
|
|
662
|
-
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
663
685
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
686
|
+
/**
|
|
687
|
+
* Morph a single node in place.
|
|
688
|
+
*/
|
|
689
|
+
function _morphNode(parent, oldNode, newNode) {
|
|
690
|
+
// Text / comment nodes — just update content
|
|
691
|
+
if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
|
|
692
|
+
if (newNode.nodeType === oldNode.nodeType) {
|
|
693
|
+
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
694
|
+
oldNode.nodeValue = newNode.nodeValue;
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
// Different node types — replace
|
|
699
|
+
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
700
|
+
return;
|
|
673
701
|
}
|
|
674
702
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null;
|
|
703
|
+
// Different node types or tag names — replace entirely
|
|
704
|
+
if (oldNode.nodeType !== newNode.nodeType ||
|
|
705
|
+
oldNode.nodeName !== newNode.nodeName) {
|
|
706
|
+
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
707
|
+
return;
|
|
681
708
|
}
|
|
682
709
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
710
|
+
// Both are elements — diff attributes then recurse children
|
|
711
|
+
if (oldNode.nodeType === 1) {
|
|
712
|
+
// z-skip: developer opt-out — skip diffing this subtree entirely.
|
|
713
|
+
// Useful for third-party widgets, canvas, video, or large static content.
|
|
714
|
+
if (oldNode.hasAttribute('z-skip')) return;
|
|
687
715
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
return this.each((_, el) => {
|
|
694
|
-
if (el === window) window.scrollTo(window.scrollX, value);
|
|
695
|
-
else el.scrollTop = value;
|
|
696
|
-
});
|
|
697
|
-
}
|
|
716
|
+
// Fast bail-out: if the elements are identical, skip everything.
|
|
717
|
+
// isEqualNode() is a native C++ comparison — much faster than walking
|
|
718
|
+
// attributes + children in JS when trees haven't changed.
|
|
719
|
+
if (oldNode.isEqualNode(newNode)) return;
|
|
698
720
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
721
|
+
_morphAttributes(oldNode, newNode);
|
|
722
|
+
|
|
723
|
+
// Special elements: don't recurse into their children
|
|
724
|
+
const tag = oldNode.nodeName;
|
|
725
|
+
if (tag === 'INPUT') {
|
|
726
|
+
_syncInputValue(oldNode, newNode);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (tag === 'TEXTAREA') {
|
|
730
|
+
if (oldNode.value !== newNode.textContent) {
|
|
731
|
+
oldNode.value = newNode.textContent || '';
|
|
732
|
+
}
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (tag === 'SELECT') {
|
|
736
|
+
_morphChildren(oldNode, newNode);
|
|
737
|
+
if (oldNode.value !== newNode.value) {
|
|
738
|
+
oldNode.value = newNode.value;
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
703
741
|
}
|
|
704
|
-
return this.each((_, el) => {
|
|
705
|
-
if (el === window) window.scrollTo(value, window.scrollY);
|
|
706
|
-
else el.scrollLeft = value;
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
742
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return el?.clientWidth;
|
|
743
|
+
// Generic element — recurse children
|
|
744
|
+
_morphChildren(oldNode, newNode);
|
|
713
745
|
}
|
|
746
|
+
}
|
|
714
747
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
748
|
+
/**
|
|
749
|
+
* Sync attributes from newEl onto oldEl.
|
|
750
|
+
* Uses a single pass: build a set of new attribute names, iterate
|
|
751
|
+
* old attrs for removals, then apply new attrs.
|
|
752
|
+
*/
|
|
753
|
+
function _morphAttributes(oldEl, newEl) {
|
|
754
|
+
const newAttrs = newEl.attributes;
|
|
755
|
+
const oldAttrs = oldEl.attributes;
|
|
756
|
+
const newLen = newAttrs.length;
|
|
757
|
+
const oldLen = oldAttrs.length;
|
|
758
|
+
|
|
759
|
+
// Fast path: if both have same number of attributes, check if they're identical
|
|
760
|
+
if (newLen === oldLen) {
|
|
761
|
+
let same = true;
|
|
762
|
+
for (let i = 0; i < newLen; i++) {
|
|
763
|
+
const na = newAttrs[i];
|
|
764
|
+
if (oldEl.getAttribute(na.name) !== na.value) { same = false; break; }
|
|
765
|
+
}
|
|
766
|
+
if (same) {
|
|
767
|
+
// Also verify no extra old attrs (names mismatch)
|
|
768
|
+
for (let i = 0; i < oldLen; i++) {
|
|
769
|
+
if (!newEl.hasAttribute(oldAttrs[i].name)) { same = false; break; }
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (same) return;
|
|
718
773
|
}
|
|
719
774
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
775
|
+
// Build set of new attr names for O(1) lookup during removal pass
|
|
776
|
+
const newNames = new Set();
|
|
777
|
+
for (let i = 0; i < newLen; i++) {
|
|
778
|
+
const attr = newAttrs[i];
|
|
779
|
+
newNames.add(attr.name);
|
|
780
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
781
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
727
782
|
}
|
|
728
|
-
return w;
|
|
729
783
|
}
|
|
730
784
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
785
|
+
// Remove stale attributes — snapshot names first because oldAttrs
|
|
786
|
+
// is a live NamedNodeMap that mutates on removeAttribute().
|
|
787
|
+
const oldNames = new Array(oldLen);
|
|
788
|
+
for (let i = 0; i < oldLen; i++) oldNames[i] = oldAttrs[i].name;
|
|
789
|
+
for (let i = oldNames.length - 1; i >= 0; i--) {
|
|
790
|
+
if (!newNames.has(oldNames[i])) {
|
|
791
|
+
oldEl.removeAttribute(oldNames[i]);
|
|
738
792
|
}
|
|
739
|
-
return h;
|
|
740
793
|
}
|
|
794
|
+
}
|
|
741
795
|
|
|
742
|
-
|
|
796
|
+
/**
|
|
797
|
+
* Sync input element value, checked, disabled states.
|
|
798
|
+
*
|
|
799
|
+
* Only updates the value when the new HTML explicitly carries a `value`
|
|
800
|
+
* attribute. Templates that use z-model manage values through reactive
|
|
801
|
+
* state + _bindModels — the morph engine should not interfere by wiping
|
|
802
|
+
* a live input's content to '' just because the template has no `value`
|
|
803
|
+
* attr. This prevents the wipe-then-restore cycle that resets cursor
|
|
804
|
+
* position on every keystroke.
|
|
805
|
+
*/
|
|
806
|
+
function _syncInputValue(oldEl, newEl) {
|
|
807
|
+
const type = (oldEl.type || '').toLowerCase();
|
|
743
808
|
|
|
744
|
-
|
|
745
|
-
if (
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (el.childNodes.length > 0) {
|
|
752
|
-
_morph(el, content);
|
|
753
|
-
} else {
|
|
754
|
-
el.innerHTML = content;
|
|
755
|
-
}
|
|
756
|
-
});
|
|
809
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
810
|
+
if (oldEl.checked !== newEl.checked) oldEl.checked = newEl.checked;
|
|
811
|
+
} else {
|
|
812
|
+
const newVal = newEl.getAttribute('value');
|
|
813
|
+
if (newVal !== null && oldEl.value !== newVal) {
|
|
814
|
+
oldEl.value = newVal;
|
|
815
|
+
}
|
|
757
816
|
}
|
|
758
817
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
818
|
+
// Sync disabled
|
|
819
|
+
if (oldEl.disabled !== newEl.disabled) oldEl.disabled = newEl.disabled;
|
|
820
|
+
}
|
|
762
821
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
822
|
+
/**
|
|
823
|
+
* Get the reconciliation key from a node.
|
|
824
|
+
*
|
|
825
|
+
* Priority: z-key attribute → id attribute → data-id / data-key.
|
|
826
|
+
* Auto-detected keys use a `\0` prefix to avoid collisions with
|
|
827
|
+
* explicit z-key values.
|
|
828
|
+
*
|
|
829
|
+
* This means the LIS-optimised keyed path activates automatically
|
|
830
|
+
* whenever elements carry `id` or `data-id` / `data-key` attributes
|
|
831
|
+
* — no extra markup required.
|
|
832
|
+
*
|
|
833
|
+
* @returns {string|null}
|
|
834
|
+
*/
|
|
835
|
+
function _getKey(node) {
|
|
836
|
+
if (node.nodeType !== 1) return null;
|
|
767
837
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
838
|
+
// Explicit z-key — highest priority
|
|
839
|
+
const zk = node.getAttribute('z-key');
|
|
840
|
+
if (zk) return zk;
|
|
772
841
|
|
|
773
|
-
//
|
|
842
|
+
// Auto-key: id attribute (unique by spec)
|
|
843
|
+
if (node.id) return '\0id:' + node.id;
|
|
774
844
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
});
|
|
845
|
+
// Auto-key: data-id or data-key attributes
|
|
846
|
+
const ds = node.dataset;
|
|
847
|
+
if (ds) {
|
|
848
|
+
if (ds.id) return '\0data-id:' + ds.id;
|
|
849
|
+
if (ds.key) return '\0data-key:' + ds.key;
|
|
781
850
|
}
|
|
782
851
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// --- src/core.js -------------------------------------------------
|
|
856
|
+
/**
|
|
857
|
+
* zQuery Core — Selector engine & chainable DOM collection
|
|
858
|
+
*
|
|
859
|
+
* Extends the quick-ref pattern (Id, Class, Classes, Children)
|
|
860
|
+
* into a full jQuery-like chainable wrapper with modern APIs.
|
|
861
|
+
*/
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
// ---------------------------------------------------------------------------
|
|
865
|
+
// ZQueryCollection — wraps an array of elements with chainable methods
|
|
866
|
+
// ---------------------------------------------------------------------------
|
|
867
|
+
class ZQueryCollection {
|
|
868
|
+
constructor(elements) {
|
|
869
|
+
this.elements = Array.isArray(elements) ? elements : (elements ? [elements] : []);
|
|
870
|
+
this.length = this.elements.length;
|
|
871
|
+
this.elements.forEach((el, i) => { this[i] = el; });
|
|
788
872
|
}
|
|
789
873
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
874
|
+
// --- Iteration -----------------------------------------------------------
|
|
875
|
+
|
|
876
|
+
each(fn) {
|
|
877
|
+
this.elements.forEach((el, i) => fn.call(el, i, el));
|
|
878
|
+
return this;
|
|
795
879
|
}
|
|
796
880
|
|
|
797
|
-
|
|
798
|
-
return this.
|
|
799
|
-
if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content);
|
|
800
|
-
else if (content instanceof Node) el.parentNode.insertBefore(content, el);
|
|
801
|
-
});
|
|
881
|
+
map(fn) {
|
|
882
|
+
return this.elements.map((el, i) => fn.call(el, i, el));
|
|
802
883
|
}
|
|
803
884
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
if (!w || !el.parentNode) return;
|
|
808
|
-
el.parentNode.insertBefore(w, el);
|
|
809
|
-
w.appendChild(el);
|
|
810
|
-
});
|
|
885
|
+
forEach(fn) {
|
|
886
|
+
this.elements.forEach((el, i) => fn(el, i, this.elements));
|
|
887
|
+
return this;
|
|
811
888
|
}
|
|
812
889
|
|
|
813
|
-
|
|
814
|
-
|
|
890
|
+
first() { return this.elements[0] || null; }
|
|
891
|
+
last() { return this.elements[this.length - 1] || null; }
|
|
892
|
+
eq(i) { return new ZQueryCollection(this.elements[i] ? [this.elements[i]] : []); }
|
|
893
|
+
toArray(){ return [...this.elements]; }
|
|
894
|
+
|
|
895
|
+
[Symbol.iterator]() { return this.elements[Symbol.iterator](); }
|
|
896
|
+
|
|
897
|
+
// --- Traversal -----------------------------------------------------------
|
|
898
|
+
|
|
899
|
+
find(selector) {
|
|
900
|
+
const found = [];
|
|
901
|
+
this.elements.forEach(el => found.push(...el.querySelectorAll(selector)));
|
|
902
|
+
return new ZQueryCollection(found);
|
|
815
903
|
}
|
|
816
904
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
return
|
|
905
|
+
parent() {
|
|
906
|
+
const parents = [...new Set(this.elements.map(el => el.parentElement).filter(Boolean))];
|
|
907
|
+
return new ZQueryCollection(parents);
|
|
820
908
|
}
|
|
821
909
|
|
|
822
|
-
|
|
823
|
-
return new ZQueryCollection(
|
|
910
|
+
closest(selector) {
|
|
911
|
+
return new ZQueryCollection(
|
|
912
|
+
this.elements.map(el => el.closest(selector)).filter(Boolean)
|
|
913
|
+
);
|
|
824
914
|
}
|
|
825
915
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
} else if (content instanceof Node) {
|
|
833
|
-
el.parentNode.replaceChild(content, el);
|
|
834
|
-
}
|
|
916
|
+
children(selector) {
|
|
917
|
+
const kids = [];
|
|
918
|
+
this.elements.forEach(el => {
|
|
919
|
+
kids.push(...(selector
|
|
920
|
+
? el.querySelectorAll(`:scope > ${selector}`)
|
|
921
|
+
: el.children));
|
|
835
922
|
});
|
|
923
|
+
return new ZQueryCollection([...kids]);
|
|
836
924
|
}
|
|
837
925
|
|
|
838
|
-
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
926
|
+
siblings(selector) {
|
|
927
|
+
const sibs = [];
|
|
928
|
+
this.elements.forEach(el => {
|
|
929
|
+
if (!el.parentElement) return;
|
|
930
|
+
const all = [...el.parentElement.children].filter(c => c !== el);
|
|
931
|
+
sibs.push(...(selector ? all.filter(c => c.matches(selector)) : all));
|
|
932
|
+
});
|
|
933
|
+
return new ZQueryCollection(sibs);
|
|
842
934
|
}
|
|
843
935
|
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
return this;
|
|
936
|
+
next(selector) {
|
|
937
|
+
const els = this.elements.map(el => el.nextElementSibling).filter(Boolean);
|
|
938
|
+
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
848
939
|
}
|
|
849
940
|
|
|
850
|
-
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
return this;
|
|
941
|
+
prev(selector) {
|
|
942
|
+
const els = this.elements.map(el => el.previousElementSibling).filter(Boolean);
|
|
943
|
+
return new ZQueryCollection(selector ? els.filter(el => el.matches(selector)) : els);
|
|
854
944
|
}
|
|
855
945
|
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
946
|
+
nextAll(selector) {
|
|
947
|
+
const result = [];
|
|
948
|
+
this.elements.forEach(el => {
|
|
949
|
+
let sib = el.nextElementSibling;
|
|
950
|
+
while (sib) {
|
|
951
|
+
if (!selector || sib.matches(selector)) result.push(sib);
|
|
952
|
+
sib = sib.nextElementSibling;
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
return new ZQueryCollection(result);
|
|
860
956
|
}
|
|
861
957
|
|
|
862
|
-
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
958
|
+
nextUntil(selector, filter) {
|
|
959
|
+
const result = [];
|
|
960
|
+
this.elements.forEach(el => {
|
|
961
|
+
let sib = el.nextElementSibling;
|
|
962
|
+
while (sib) {
|
|
963
|
+
if (selector && sib.matches(selector)) break;
|
|
964
|
+
if (!filter || sib.matches(filter)) result.push(sib);
|
|
965
|
+
sib = sib.nextElementSibling;
|
|
966
|
+
}
|
|
870
967
|
});
|
|
871
|
-
return
|
|
968
|
+
return new ZQueryCollection(result);
|
|
872
969
|
}
|
|
873
970
|
|
|
874
|
-
|
|
971
|
+
prevAll(selector) {
|
|
972
|
+
const result = [];
|
|
875
973
|
this.elements.forEach(el => {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
974
|
+
let sib = el.previousElementSibling;
|
|
975
|
+
while (sib) {
|
|
976
|
+
if (!selector || sib.matches(selector)) result.push(sib);
|
|
977
|
+
sib = sib.previousElementSibling;
|
|
978
|
+
}
|
|
880
979
|
});
|
|
881
|
-
return
|
|
980
|
+
return new ZQueryCollection(result);
|
|
882
981
|
}
|
|
883
982
|
|
|
884
|
-
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
983
|
+
prevUntil(selector, filter) {
|
|
984
|
+
const result = [];
|
|
985
|
+
this.elements.forEach(el => {
|
|
986
|
+
let sib = el.previousElementSibling;
|
|
987
|
+
while (sib) {
|
|
988
|
+
if (selector && sib.matches(selector)) break;
|
|
989
|
+
if (!filter || sib.matches(filter)) result.push(sib);
|
|
990
|
+
sib = sib.previousElementSibling;
|
|
991
|
+
}
|
|
992
|
+
});
|
|
993
|
+
return new ZQueryCollection(result);
|
|
891
994
|
}
|
|
892
995
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
996
|
+
parents(selector) {
|
|
997
|
+
const result = [];
|
|
998
|
+
this.elements.forEach(el => {
|
|
999
|
+
let parent = el.parentElement;
|
|
1000
|
+
while (parent) {
|
|
1001
|
+
if (!selector || parent.matches(selector)) result.push(parent);
|
|
1002
|
+
parent = parent.parentElement;
|
|
1003
|
+
}
|
|
898
1004
|
});
|
|
1005
|
+
return new ZQueryCollection([...new Set(result)]);
|
|
899
1006
|
}
|
|
900
1007
|
|
|
901
|
-
|
|
902
|
-
|
|
1008
|
+
parentsUntil(selector, filter) {
|
|
1009
|
+
const result = [];
|
|
1010
|
+
this.elements.forEach(el => {
|
|
1011
|
+
let parent = el.parentElement;
|
|
1012
|
+
while (parent) {
|
|
1013
|
+
if (selector && parent.matches(selector)) break;
|
|
1014
|
+
if (!filter || parent.matches(filter)) result.push(parent);
|
|
1015
|
+
parent = parent.parentElement;
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
return new ZQueryCollection([...new Set(result)]);
|
|
903
1019
|
}
|
|
904
1020
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
return
|
|
1021
|
+
contents() {
|
|
1022
|
+
const result = [];
|
|
1023
|
+
this.elements.forEach(el => result.push(...el.childNodes));
|
|
1024
|
+
return new ZQueryCollection(result);
|
|
909
1025
|
}
|
|
910
1026
|
|
|
911
|
-
|
|
912
|
-
|
|
1027
|
+
filter(selector) {
|
|
1028
|
+
if (typeof selector === 'function') {
|
|
1029
|
+
return new ZQueryCollection(this.elements.filter(selector));
|
|
1030
|
+
}
|
|
1031
|
+
return new ZQueryCollection(this.elements.filter(el => el.matches(selector)));
|
|
913
1032
|
}
|
|
914
1033
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
});
|
|
1034
|
+
not(selector) {
|
|
1035
|
+
if (typeof selector === 'function') {
|
|
1036
|
+
return new ZQueryCollection(this.elements.filter((el, i) => !selector.call(el, i, el)));
|
|
1037
|
+
}
|
|
1038
|
+
return new ZQueryCollection(this.elements.filter(el => !el.matches(selector)));
|
|
921
1039
|
}
|
|
922
1040
|
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
on(event, selectorOrHandler, handler) {
|
|
926
|
-
// Support multiple events: "click mouseenter"
|
|
927
|
-
const events = event.split(/\s+/);
|
|
928
|
-
return this.each((_, el) => {
|
|
929
|
-
events.forEach(evt => {
|
|
930
|
-
if (typeof selectorOrHandler === 'function') {
|
|
931
|
-
el.addEventListener(evt, selectorOrHandler);
|
|
932
|
-
} else if (typeof selectorOrHandler === 'string') {
|
|
933
|
-
// Delegated event — store wrapper so off() can remove it
|
|
934
|
-
const wrapper = (e) => {
|
|
935
|
-
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
936
|
-
const target = e.target.closest(selectorOrHandler);
|
|
937
|
-
if (target && el.contains(target)) handler.call(target, e);
|
|
938
|
-
};
|
|
939
|
-
wrapper._zqOriginal = handler;
|
|
940
|
-
wrapper._zqSelector = selectorOrHandler;
|
|
941
|
-
el.addEventListener(evt, wrapper);
|
|
942
|
-
// Track delegated handlers for removal
|
|
943
|
-
if (!el._zqDelegated) el._zqDelegated = [];
|
|
944
|
-
el._zqDelegated.push({ evt, wrapper });
|
|
945
|
-
}
|
|
946
|
-
});
|
|
947
|
-
});
|
|
1041
|
+
has(selector) {
|
|
1042
|
+
return new ZQueryCollection(this.elements.filter(el => el.querySelector(selector)));
|
|
948
1043
|
}
|
|
949
1044
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
el.removeEventListener(evt, handler);
|
|
956
|
-
// Also check delegated handlers
|
|
957
|
-
if (el._zqDelegated) {
|
|
958
|
-
el._zqDelegated = el._zqDelegated.filter(d => {
|
|
959
|
-
if (d.evt === evt && d.wrapper._zqOriginal === handler) {
|
|
960
|
-
el.removeEventListener(evt, d.wrapper);
|
|
961
|
-
return false;
|
|
962
|
-
}
|
|
963
|
-
return true;
|
|
964
|
-
});
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
});
|
|
1045
|
+
is(selector) {
|
|
1046
|
+
if (typeof selector === 'function') {
|
|
1047
|
+
return this.elements.some((el, i) => selector.call(el, i, el));
|
|
1048
|
+
}
|
|
1049
|
+
return this.elements.some(el => el.matches(selector));
|
|
968
1050
|
}
|
|
969
1051
|
|
|
970
|
-
|
|
971
|
-
return this.
|
|
972
|
-
el.addEventListener(event, handler, { once: true });
|
|
973
|
-
});
|
|
1052
|
+
slice(start, end) {
|
|
1053
|
+
return new ZQueryCollection(this.elements.slice(start, end));
|
|
974
1054
|
}
|
|
975
1055
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1056
|
+
add(selector, context) {
|
|
1057
|
+
const toAdd = (selector instanceof ZQueryCollection)
|
|
1058
|
+
? selector.elements
|
|
1059
|
+
: (selector instanceof Node)
|
|
1060
|
+
? [selector]
|
|
1061
|
+
: Array.from((context || document).querySelectorAll(selector));
|
|
1062
|
+
return new ZQueryCollection([...this.elements, ...toAdd]);
|
|
980
1063
|
}
|
|
981
1064
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
focus() { this.first()?.focus(); return this; }
|
|
986
|
-
blur() { this.first()?.blur(); return this; }
|
|
987
|
-
hover(enterFn, leaveFn) {
|
|
988
|
-
this.on('mouseenter', enterFn);
|
|
989
|
-
return this.on('mouseleave', leaveFn || enterFn);
|
|
1065
|
+
get(index) {
|
|
1066
|
+
if (index === undefined) return [...this.elements];
|
|
1067
|
+
return index < 0 ? this.elements[this.length + index] : this.elements[index];
|
|
990
1068
|
}
|
|
991
1069
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
el.style.transition = `all ${duration}ms ${easing}`;
|
|
1003
|
-
requestAnimationFrame(() => {
|
|
1004
|
-
Object.assign(el.style, props);
|
|
1005
|
-
const onEnd = () => {
|
|
1006
|
-
el.removeEventListener('transitionend', onEnd);
|
|
1007
|
-
el.style.transition = '';
|
|
1008
|
-
if (!resolved && ++count.done >= this.length) {
|
|
1009
|
-
resolved = true;
|
|
1010
|
-
resolve(this);
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
|
-
el.addEventListener('transitionend', onEnd);
|
|
1014
|
-
listeners.push({ el, onEnd });
|
|
1015
|
-
});
|
|
1016
|
-
});
|
|
1017
|
-
// Fallback in case transitionend doesn't fire
|
|
1018
|
-
setTimeout(() => {
|
|
1019
|
-
if (!resolved) {
|
|
1020
|
-
resolved = true;
|
|
1021
|
-
// Clean up any remaining transitionend listeners
|
|
1022
|
-
for (const { el, onEnd } of listeners) {
|
|
1023
|
-
el.removeEventListener('transitionend', onEnd);
|
|
1024
|
-
el.style.transition = '';
|
|
1025
|
-
}
|
|
1026
|
-
resolve(this);
|
|
1027
|
-
}
|
|
1028
|
-
}, duration + 50);
|
|
1029
|
-
});
|
|
1070
|
+
index(selector) {
|
|
1071
|
+
if (selector === undefined) {
|
|
1072
|
+
const el = this.first();
|
|
1073
|
+
if (!el || !el.parentElement) return -1;
|
|
1074
|
+
return Array.from(el.parentElement.children).indexOf(el);
|
|
1075
|
+
}
|
|
1076
|
+
const target = (typeof selector === 'string')
|
|
1077
|
+
? document.querySelector(selector)
|
|
1078
|
+
: selector;
|
|
1079
|
+
return this.elements.indexOf(target);
|
|
1030
1080
|
}
|
|
1031
1081
|
|
|
1032
|
-
|
|
1033
|
-
return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration);
|
|
1034
|
-
}
|
|
1082
|
+
// --- Classes -------------------------------------------------------------
|
|
1035
1083
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1084
|
+
addClass(...names) {
|
|
1085
|
+
// Fast path: single class, no spaces — avoids flatMap + regex split allocation
|
|
1086
|
+
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
1087
|
+
const c = names[0];
|
|
1088
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(c);
|
|
1089
|
+
return this;
|
|
1090
|
+
}
|
|
1091
|
+
const classes = names.flatMap(n => n.split(/\s+/));
|
|
1092
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.add(...classes);
|
|
1093
|
+
return this;
|
|
1038
1094
|
}
|
|
1039
1095
|
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1096
|
+
removeClass(...names) {
|
|
1097
|
+
if (names.length === 1 && names[0].indexOf(' ') === -1) {
|
|
1098
|
+
const c = names[0];
|
|
1099
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(c);
|
|
1100
|
+
return this;
|
|
1101
|
+
}
|
|
1102
|
+
const classes = names.flatMap(n => n.split(/\s+/));
|
|
1103
|
+
for (let i = 0; i < this.elements.length; i++) this.elements[i].classList.remove(...classes);
|
|
1104
|
+
return this;
|
|
1047
1105
|
}
|
|
1048
1106
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1107
|
+
toggleClass(...args) {
|
|
1108
|
+
const force = typeof args[args.length - 1] === 'boolean' ? args.pop() : undefined;
|
|
1109
|
+
// Fast path: single class, no spaces
|
|
1110
|
+
if (args.length === 1 && args[0].indexOf(' ') === -1) {
|
|
1111
|
+
const c = args[0];
|
|
1112
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
1113
|
+
force !== undefined ? this.elements[i].classList.toggle(c, force) : this.elements[i].classList.toggle(c);
|
|
1114
|
+
}
|
|
1115
|
+
return this;
|
|
1116
|
+
}
|
|
1117
|
+
const classes = args.flatMap(n => n.split(/\s+/));
|
|
1118
|
+
for (let i = 0; i < this.elements.length; i++) {
|
|
1119
|
+
const el = this.elements[i];
|
|
1120
|
+
for (let j = 0; j < classes.length; j++) {
|
|
1121
|
+
force !== undefined ? el.classList.toggle(classes[j], force) : el.classList.toggle(classes[j]);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return this;
|
|
1051
1125
|
}
|
|
1052
1126
|
|
|
1053
|
-
|
|
1054
|
-
return this.
|
|
1055
|
-
el.style.display = '';
|
|
1056
|
-
el.style.overflow = 'hidden';
|
|
1057
|
-
const h = el.scrollHeight + 'px';
|
|
1058
|
-
el.style.maxHeight = '0';
|
|
1059
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1060
|
-
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1061
|
-
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1062
|
-
});
|
|
1127
|
+
hasClass(name) {
|
|
1128
|
+
return this.first()?.classList.contains(name) || false;
|
|
1063
1129
|
}
|
|
1064
1130
|
|
|
1065
|
-
|
|
1066
|
-
return this.each((_, el) => {
|
|
1067
|
-
el.style.overflow = 'hidden';
|
|
1068
|
-
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1069
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1070
|
-
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1071
|
-
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1072
|
-
});
|
|
1073
|
-
}
|
|
1131
|
+
// --- Attributes ----------------------------------------------------------
|
|
1074
1132
|
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
el.
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1084
|
-
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1085
|
-
} else {
|
|
1086
|
-
el.style.overflow = 'hidden';
|
|
1087
|
-
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1088
|
-
el.style.transition = `max-height ${duration}ms ease`;
|
|
1089
|
-
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1090
|
-
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1091
|
-
}
|
|
1092
|
-
});
|
|
1133
|
+
attr(name, value) {
|
|
1134
|
+
if (typeof name === 'object' && name !== null) {
|
|
1135
|
+
return this.each((_, el) => {
|
|
1136
|
+
for (const [k, v] of Object.entries(name)) el.setAttribute(k, v);
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
if (value === undefined) return this.first()?.getAttribute(name);
|
|
1140
|
+
return this.each((_, el) => el.setAttribute(name, value));
|
|
1093
1141
|
}
|
|
1094
1142
|
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
serialize() {
|
|
1098
|
-
const form = this.first();
|
|
1099
|
-
if (!form || form.tagName !== 'FORM') return '';
|
|
1100
|
-
return new URLSearchParams(new FormData(form)).toString();
|
|
1143
|
+
removeAttr(name) {
|
|
1144
|
+
return this.each((_, el) => el.removeAttribute(name));
|
|
1101
1145
|
}
|
|
1102
1146
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const obj = {};
|
|
1107
|
-
new FormData(form).forEach((v, k) => {
|
|
1108
|
-
if (obj[k] !== undefined) {
|
|
1109
|
-
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
1110
|
-
obj[k].push(v);
|
|
1111
|
-
} else {
|
|
1112
|
-
obj[k] = v;
|
|
1113
|
-
}
|
|
1114
|
-
});
|
|
1115
|
-
return obj;
|
|
1147
|
+
prop(name, value) {
|
|
1148
|
+
if (value === undefined) return this.first()?.[name];
|
|
1149
|
+
return this.each((_, el) => { el[name] = value; });
|
|
1116
1150
|
}
|
|
1117
|
-
}
|
|
1118
1151
|
|
|
1152
|
+
data(key, value) {
|
|
1153
|
+
if (value === undefined) {
|
|
1154
|
+
if (key === undefined) return this.first()?.dataset;
|
|
1155
|
+
const raw = this.first()?.dataset[key];
|
|
1156
|
+
try { return JSON.parse(raw); } catch { return raw; }
|
|
1157
|
+
}
|
|
1158
|
+
return this.each((_, el) => { el.dataset[key] = typeof value === 'object' ? JSON.stringify(value) : value; });
|
|
1159
|
+
}
|
|
1119
1160
|
|
|
1120
|
-
//
|
|
1121
|
-
// Helper — create document fragment from HTML string
|
|
1122
|
-
// ---------------------------------------------------------------------------
|
|
1123
|
-
function createFragment(html) {
|
|
1124
|
-
const tpl = document.createElement('template');
|
|
1125
|
-
tpl.innerHTML = html.trim();
|
|
1126
|
-
return tpl.content;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1161
|
+
// --- CSS / Dimensions ----------------------------------------------------
|
|
1129
1162
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1163
|
+
css(props, value) {
|
|
1164
|
+
if (typeof props === 'string' && value !== undefined) {
|
|
1165
|
+
return this.each((_, el) => { el.style[props] = value; });
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof props === 'string') {
|
|
1168
|
+
const el = this.first();
|
|
1169
|
+
return el ? getComputedStyle(el)[props] : undefined;
|
|
1170
|
+
}
|
|
1171
|
+
return this.each((_, el) => Object.assign(el.style, props));
|
|
1172
|
+
}
|
|
1136
1173
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1174
|
+
width() { return this.first()?.getBoundingClientRect().width; }
|
|
1175
|
+
height() { return this.first()?.getBoundingClientRect().height; }
|
|
1139
1176
|
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
return
|
|
1177
|
+
offset() {
|
|
1178
|
+
const r = this.first()?.getBoundingClientRect();
|
|
1179
|
+
return r ? { top: r.top + window.scrollY, left: r.left + window.scrollX, width: r.width, height: r.height } : null;
|
|
1143
1180
|
}
|
|
1144
1181
|
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
return
|
|
1182
|
+
position() {
|
|
1183
|
+
const el = this.first();
|
|
1184
|
+
return el ? { top: el.offsetTop, left: el.offsetLeft } : null;
|
|
1148
1185
|
}
|
|
1149
1186
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1187
|
+
scrollTop(value) {
|
|
1188
|
+
if (value === undefined) {
|
|
1189
|
+
const el = this.first();
|
|
1190
|
+
return el === window ? window.scrollY : el?.scrollTop;
|
|
1191
|
+
}
|
|
1192
|
+
return this.each((_, el) => {
|
|
1193
|
+
if (el === window) window.scrollTo(window.scrollX, value);
|
|
1194
|
+
else el.scrollTop = value;
|
|
1195
|
+
});
|
|
1154
1196
|
}
|
|
1155
1197
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
return
|
|
1198
|
+
scrollLeft(value) {
|
|
1199
|
+
if (value === undefined) {
|
|
1200
|
+
const el = this.first();
|
|
1201
|
+
return el === window ? window.scrollX : el?.scrollLeft;
|
|
1202
|
+
}
|
|
1203
|
+
return this.each((_, el) => {
|
|
1204
|
+
if (el === window) window.scrollTo(value, window.scrollY);
|
|
1205
|
+
else el.scrollLeft = value;
|
|
1206
|
+
});
|
|
1162
1207
|
}
|
|
1163
1208
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1209
|
+
innerWidth() {
|
|
1210
|
+
const el = this.first();
|
|
1211
|
+
return el?.clientWidth;
|
|
1212
|
+
}
|
|
1166
1213
|
|
|
1214
|
+
innerHeight() {
|
|
1215
|
+
const el = this.first();
|
|
1216
|
+
return el?.clientHeight;
|
|
1217
|
+
}
|
|
1167
1218
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1219
|
+
outerWidth(includeMargin = false) {
|
|
1220
|
+
const el = this.first();
|
|
1221
|
+
if (!el) return undefined;
|
|
1222
|
+
let w = el.offsetWidth;
|
|
1223
|
+
if (includeMargin) {
|
|
1224
|
+
const style = getComputedStyle(el);
|
|
1225
|
+
w += parseFloat(style.marginLeft) + parseFloat(style.marginRight);
|
|
1226
|
+
}
|
|
1227
|
+
return w;
|
|
1228
|
+
}
|
|
1174
1229
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1230
|
+
outerHeight(includeMargin = false) {
|
|
1231
|
+
const el = this.first();
|
|
1232
|
+
if (!el) return undefined;
|
|
1233
|
+
let h = el.offsetHeight;
|
|
1234
|
+
if (includeMargin) {
|
|
1235
|
+
const style = getComputedStyle(el);
|
|
1236
|
+
h += parseFloat(style.marginTop) + parseFloat(style.marginBottom);
|
|
1237
|
+
}
|
|
1238
|
+
return h;
|
|
1239
|
+
}
|
|
1177
1240
|
|
|
1178
|
-
//
|
|
1179
|
-
|
|
1180
|
-
|
|
1241
|
+
// --- Content -------------------------------------------------------------
|
|
1242
|
+
|
|
1243
|
+
html(content) {
|
|
1244
|
+
if (content === undefined) return this.first()?.innerHTML;
|
|
1245
|
+
// Auto-morph: if the element already has children, use the diff engine
|
|
1246
|
+
// to patch the DOM (preserves focus, scroll, state, keyed reorder via LIS).
|
|
1247
|
+
// Empty elements get raw innerHTML for fast first-paint — same strategy
|
|
1248
|
+
// the component system uses (first render = innerHTML, updates = morph).
|
|
1249
|
+
return this.each((_, el) => {
|
|
1250
|
+
if (el.childNodes.length > 0) {
|
|
1251
|
+
_morph(el, content);
|
|
1252
|
+
} else {
|
|
1253
|
+
el.innerHTML = content;
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1181
1256
|
}
|
|
1182
1257
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
return new ZQueryCollection(Array.from(selector));
|
|
1258
|
+
morph(content) {
|
|
1259
|
+
return this.each((_, el) => { _morph(el, content); });
|
|
1186
1260
|
}
|
|
1187
1261
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1262
|
+
text(content) {
|
|
1263
|
+
if (content === undefined) return this.first()?.textContent;
|
|
1264
|
+
return this.each((_, el) => { el.textContent = content; });
|
|
1192
1265
|
}
|
|
1193
1266
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1198
|
-
: document;
|
|
1199
|
-
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1267
|
+
val(value) {
|
|
1268
|
+
if (value === undefined) return this.first()?.value;
|
|
1269
|
+
return this.each((_, el) => { el.value = value; });
|
|
1200
1270
|
}
|
|
1201
1271
|
|
|
1202
|
-
|
|
1203
|
-
}
|
|
1272
|
+
// --- DOM Manipulation ----------------------------------------------------
|
|
1204
1273
|
|
|
1274
|
+
append(content) {
|
|
1275
|
+
return this.each((_, el) => {
|
|
1276
|
+
if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content);
|
|
1277
|
+
else if (content instanceof ZQueryCollection) content.each((__, c) => el.appendChild(c));
|
|
1278
|
+
else if (content instanceof Node) el.appendChild(content);
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1205
1281
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
query.tag = (name) => new ZQueryCollection(Array.from(document.getElementsByTagName(name)));
|
|
1213
|
-
Object.defineProperty(query, 'name', {
|
|
1214
|
-
value: (name) => new ZQueryCollection(Array.from(document.getElementsByName(name))),
|
|
1215
|
-
writable: true, configurable: true
|
|
1216
|
-
});
|
|
1217
|
-
query.children = (parentId) => {
|
|
1218
|
-
const p = document.getElementById(parentId);
|
|
1219
|
-
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1220
|
-
};
|
|
1221
|
-
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
1222
|
-
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
1282
|
+
prepend(content) {
|
|
1283
|
+
return this.each((_, el) => {
|
|
1284
|
+
if (typeof content === 'string') el.insertAdjacentHTML('afterbegin', content);
|
|
1285
|
+
else if (content instanceof Node) el.insertBefore(content, el.firstChild);
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1223
1288
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1230
|
-
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
1231
|
-
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1232
|
-
else el.setAttribute(k, v);
|
|
1289
|
+
after(content) {
|
|
1290
|
+
return this.each((_, el) => {
|
|
1291
|
+
if (typeof content === 'string') el.insertAdjacentHTML('afterend', content);
|
|
1292
|
+
else if (content instanceof Node) el.parentNode.insertBefore(content, el.nextSibling);
|
|
1293
|
+
});
|
|
1233
1294
|
}
|
|
1234
|
-
children.flat().forEach(child => {
|
|
1235
|
-
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
1236
|
-
else if (child instanceof Node) el.appendChild(child);
|
|
1237
|
-
});
|
|
1238
|
-
return new ZQueryCollection(el);
|
|
1239
|
-
};
|
|
1240
1295
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
};
|
|
1296
|
+
before(content) {
|
|
1297
|
+
return this.each((_, el) => {
|
|
1298
|
+
if (typeof content === 'string') el.insertAdjacentHTML('beforebegin', content);
|
|
1299
|
+
else if (content instanceof Node) el.parentNode.insertBefore(content, el);
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1246
1302
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
document.addEventListener(event, selectorOrHandler);
|
|
1255
|
-
return;
|
|
1303
|
+
wrap(wrapper) {
|
|
1304
|
+
return this.each((_, el) => {
|
|
1305
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
1306
|
+
if (!w || !el.parentNode) return;
|
|
1307
|
+
el.parentNode.insertBefore(w, el);
|
|
1308
|
+
w.appendChild(el);
|
|
1309
|
+
});
|
|
1256
1310
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
return;
|
|
1311
|
+
|
|
1312
|
+
remove() {
|
|
1313
|
+
return this.each((_, el) => el.remove());
|
|
1261
1314
|
}
|
|
1262
|
-
// 3-arg string: delegated
|
|
1263
|
-
document.addEventListener(event, (e) => {
|
|
1264
|
-
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1265
|
-
const target = e.target.closest(selectorOrHandler);
|
|
1266
|
-
if (target) handler.call(target, e);
|
|
1267
|
-
});
|
|
1268
|
-
};
|
|
1269
1315
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
}
|
|
1316
|
+
empty() {
|
|
1317
|
+
// textContent = '' clears all children without invoking the HTML parser
|
|
1318
|
+
return this.each((_, el) => { el.textContent = ''; });
|
|
1319
|
+
}
|
|
1274
1320
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
// --- src/expression.js -------------------------------------------
|
|
1279
|
-
/**
|
|
1280
|
-
* zQuery Expression Parser — CSP-safe expression evaluator
|
|
1281
|
-
*
|
|
1282
|
-
* Replaces `new Function()` / `eval()` with a hand-written parser that
|
|
1283
|
-
* evaluates expressions safely without violating Content Security Policy.
|
|
1284
|
-
*
|
|
1285
|
-
* Supports:
|
|
1286
|
-
* - Property access: user.name, items[0], items[i]
|
|
1287
|
-
* - Method calls: items.length, str.toUpperCase()
|
|
1288
|
-
* - Arithmetic: a + b, count * 2, i % 2
|
|
1289
|
-
* - Comparison: a === b, count > 0, x != null
|
|
1290
|
-
* - Logical: a && b, a || b, !a
|
|
1291
|
-
* - Ternary: a ? b : c
|
|
1292
|
-
* - Typeof: typeof x
|
|
1293
|
-
* - Unary: -a, +a, !a
|
|
1294
|
-
* - Literals: 42, 'hello', "world", true, false, null, undefined
|
|
1295
|
-
* - Template literals: `Hello ${name}`
|
|
1296
|
-
* - Array literals: [1, 2, 3]
|
|
1297
|
-
* - Object literals: { foo: 'bar', baz: 1 }
|
|
1298
|
-
* - Grouping: (a + b) * c
|
|
1299
|
-
* - Nullish coalescing: a ?? b
|
|
1300
|
-
* - Optional chaining: a?.b, a?.[b], a?.()
|
|
1301
|
-
* - Arrow functions: x => x.id, (a, b) => a + b
|
|
1302
|
-
*/
|
|
1321
|
+
clone(deep = true) {
|
|
1322
|
+
return new ZQueryCollection(this.elements.map(el => el.cloneNode(deep)));
|
|
1323
|
+
}
|
|
1303
1324
|
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1325
|
+
replaceWith(content) {
|
|
1326
|
+
return this.each((_, el) => {
|
|
1327
|
+
if (typeof content === 'string') {
|
|
1328
|
+
// Auto-morph: diff attributes + children when the tag name matches
|
|
1329
|
+
// instead of destroying and re-creating the element.
|
|
1330
|
+
_morphElement(el, content);
|
|
1331
|
+
} else if (content instanceof Node) {
|
|
1332
|
+
el.parentNode.replaceChild(content, el);
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1308
1336
|
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
'==': 8, '!=': 8, '===': 8, '!==': 8,
|
|
1315
|
-
'<': 9, '>': 9, '<=': 9, '>=': 9, 'instanceof': 9, 'in': 9,
|
|
1316
|
-
'+': 11, '-': 11,
|
|
1317
|
-
'*': 12, '/': 12, '%': 12,
|
|
1318
|
-
};
|
|
1337
|
+
appendTo(target) {
|
|
1338
|
+
const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1339
|
+
if (dest) this.each((_, el) => dest.appendChild(el));
|
|
1340
|
+
return this;
|
|
1341
|
+
}
|
|
1319
1342
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
// ---------------------------------------------------------------------------
|
|
1326
|
-
// Tokenizer
|
|
1327
|
-
// ---------------------------------------------------------------------------
|
|
1328
|
-
function tokenize(expr) {
|
|
1329
|
-
const tokens = [];
|
|
1330
|
-
let i = 0;
|
|
1331
|
-
const len = expr.length;
|
|
1332
|
-
|
|
1333
|
-
while (i < len) {
|
|
1334
|
-
const ch = expr[i];
|
|
1335
|
-
|
|
1336
|
-
// Whitespace
|
|
1337
|
-
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; }
|
|
1338
|
-
|
|
1339
|
-
// Numbers
|
|
1340
|
-
if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < len && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {
|
|
1341
|
-
let num = '';
|
|
1342
|
-
if (ch === '0' && i + 1 < len && (expr[i + 1] === 'x' || expr[i + 1] === 'X')) {
|
|
1343
|
-
num = '0x'; i += 2;
|
|
1344
|
-
while (i < len && /[0-9a-fA-F]/.test(expr[i])) num += expr[i++];
|
|
1345
|
-
} else {
|
|
1346
|
-
while (i < len && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) num += expr[i++];
|
|
1347
|
-
if (i < len && (expr[i] === 'e' || expr[i] === 'E')) {
|
|
1348
|
-
num += expr[i++];
|
|
1349
|
-
if (i < len && (expr[i] === '+' || expr[i] === '-')) num += expr[i++];
|
|
1350
|
-
while (i < len && expr[i] >= '0' && expr[i] <= '9') num += expr[i++];
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
tokens.push({ t: T.NUM, v: Number(num) });
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
// Strings
|
|
1358
|
-
if (ch === "'" || ch === '"') {
|
|
1359
|
-
const quote = ch;
|
|
1360
|
-
let str = '';
|
|
1361
|
-
i++;
|
|
1362
|
-
while (i < len && expr[i] !== quote) {
|
|
1363
|
-
if (expr[i] === '\\' && i + 1 < len) {
|
|
1364
|
-
const esc = expr[++i];
|
|
1365
|
-
if (esc === 'n') str += '\n';
|
|
1366
|
-
else if (esc === 't') str += '\t';
|
|
1367
|
-
else if (esc === 'r') str += '\r';
|
|
1368
|
-
else if (esc === '\\') str += '\\';
|
|
1369
|
-
else if (esc === quote) str += quote;
|
|
1370
|
-
else str += esc;
|
|
1371
|
-
} else {
|
|
1372
|
-
str += expr[i];
|
|
1373
|
-
}
|
|
1374
|
-
i++;
|
|
1375
|
-
}
|
|
1376
|
-
i++; // closing quote
|
|
1377
|
-
tokens.push({ t: T.STR, v: str });
|
|
1378
|
-
continue;
|
|
1379
|
-
}
|
|
1343
|
+
prependTo(target) {
|
|
1344
|
+
const dest = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1345
|
+
if (dest) this.each((_, el) => dest.insertBefore(el, dest.firstChild));
|
|
1346
|
+
return this;
|
|
1347
|
+
}
|
|
1380
1348
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
while (i < len && expr[i] !== '`') {
|
|
1387
|
-
if (expr[i] === '$' && i + 1 < len && expr[i + 1] === '{') {
|
|
1388
|
-
parts.push(str);
|
|
1389
|
-
str = '';
|
|
1390
|
-
i += 2;
|
|
1391
|
-
let depth = 1;
|
|
1392
|
-
let inner = '';
|
|
1393
|
-
while (i < len && depth > 0) {
|
|
1394
|
-
if (expr[i] === '{') depth++;
|
|
1395
|
-
else if (expr[i] === '}') { depth--; if (depth === 0) break; }
|
|
1396
|
-
inner += expr[i++];
|
|
1397
|
-
}
|
|
1398
|
-
i++; // closing }
|
|
1399
|
-
parts.push({ expr: inner });
|
|
1400
|
-
} else {
|
|
1401
|
-
if (expr[i] === '\\' && i + 1 < len) { str += expr[++i]; }
|
|
1402
|
-
else str += expr[i];
|
|
1403
|
-
i++;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
i++; // closing backtick
|
|
1407
|
-
parts.push(str);
|
|
1408
|
-
tokens.push({ t: T.TMPL, v: parts });
|
|
1409
|
-
continue;
|
|
1410
|
-
}
|
|
1349
|
+
insertAfter(target) {
|
|
1350
|
+
const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1351
|
+
if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref.nextSibling));
|
|
1352
|
+
return this;
|
|
1353
|
+
}
|
|
1411
1354
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1355
|
+
insertBefore(target) {
|
|
1356
|
+
const ref = typeof target === 'string' ? document.querySelector(target) : target instanceof ZQueryCollection ? target.first() : target;
|
|
1357
|
+
if (ref && ref.parentNode) this.each((_, el) => ref.parentNode.insertBefore(el, ref));
|
|
1358
|
+
return this;
|
|
1359
|
+
}
|
|
1419
1360
|
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
}
|
|
1432
|
-
const pair = expr.slice(i, i + 2);
|
|
1433
|
-
if (pair === '==' || pair === '!=' || pair === '<=' || pair === '>=' ||
|
|
1434
|
-
pair === '&&' || pair === '||' || pair === '??' || pair === '?.' ||
|
|
1435
|
-
pair === '=>') {
|
|
1436
|
-
tokens.push({ t: T.OP, v: pair });
|
|
1437
|
-
i += 2;
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1361
|
+
replaceAll(target) {
|
|
1362
|
+
const targets = typeof target === 'string'
|
|
1363
|
+
? Array.from(document.querySelectorAll(target))
|
|
1364
|
+
: target instanceof ZQueryCollection ? target.elements : [target];
|
|
1365
|
+
targets.forEach((t, i) => {
|
|
1366
|
+
const nodes = i === 0 ? this.elements : this.elements.map(el => el.cloneNode(true));
|
|
1367
|
+
nodes.forEach(el => t.parentNode.insertBefore(el, t));
|
|
1368
|
+
t.remove();
|
|
1369
|
+
});
|
|
1370
|
+
return this;
|
|
1371
|
+
}
|
|
1440
1372
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
-
}
|
|
1455
|
-
if ('()[]{},.?:'.includes(ch)) {
|
|
1456
|
-
tokens.push({ t: T.PUNC, v: ch });
|
|
1457
|
-
i++; continue;
|
|
1458
|
-
}
|
|
1373
|
+
unwrap(selector) {
|
|
1374
|
+
this.elements.forEach(el => {
|
|
1375
|
+
const parent = el.parentElement;
|
|
1376
|
+
if (!parent || parent === document.body) return;
|
|
1377
|
+
if (selector && !parent.matches(selector)) return;
|
|
1378
|
+
parent.replaceWith(...parent.childNodes);
|
|
1379
|
+
});
|
|
1380
|
+
return this;
|
|
1381
|
+
}
|
|
1459
1382
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1383
|
+
wrapAll(wrapper) {
|
|
1384
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
1385
|
+
const first = this.first();
|
|
1386
|
+
if (!first) return this;
|
|
1387
|
+
first.parentNode.insertBefore(w, first);
|
|
1388
|
+
this.each((_, el) => w.appendChild(el));
|
|
1389
|
+
return this;
|
|
1462
1390
|
}
|
|
1463
1391
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1392
|
+
wrapInner(wrapper) {
|
|
1393
|
+
return this.each((_, el) => {
|
|
1394
|
+
const w = typeof wrapper === 'string' ? createFragment(wrapper).firstElementChild : wrapper.cloneNode(true);
|
|
1395
|
+
while (el.firstChild) w.appendChild(el.firstChild);
|
|
1396
|
+
el.appendChild(w);
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1467
1399
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
// ---------------------------------------------------------------------------
|
|
1471
|
-
class Parser {
|
|
1472
|
-
constructor(tokens, scope) {
|
|
1473
|
-
this.tokens = tokens;
|
|
1474
|
-
this.pos = 0;
|
|
1475
|
-
this.scope = scope;
|
|
1400
|
+
detach() {
|
|
1401
|
+
return this.each((_, el) => el.remove());
|
|
1476
1402
|
}
|
|
1477
1403
|
|
|
1478
|
-
|
|
1479
|
-
next() { return this.tokens[this.pos++]; }
|
|
1404
|
+
// --- Visibility ----------------------------------------------------------
|
|
1480
1405
|
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
if (t.t !== type || (val !== undefined && t.v !== val)) {
|
|
1484
|
-
throw new Error(`Expected ${val || type} but got ${t.v}`);
|
|
1485
|
-
}
|
|
1486
|
-
return t;
|
|
1406
|
+
show(display = '') {
|
|
1407
|
+
return this.each((_, el) => { el.style.display = display; });
|
|
1487
1408
|
}
|
|
1488
1409
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
if (t.t === type && (val === undefined || t.v === val)) {
|
|
1492
|
-
return this.next();
|
|
1493
|
-
}
|
|
1494
|
-
return null;
|
|
1410
|
+
hide() {
|
|
1411
|
+
return this.each((_, el) => { el.style.display = 'none'; });
|
|
1495
1412
|
}
|
|
1496
1413
|
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1414
|
+
toggle(display = '') {
|
|
1415
|
+
return this.each((_, el) => {
|
|
1416
|
+
// Check inline style first (cheap) before forcing layout via getComputedStyle
|
|
1417
|
+
const hidden = el.style.display === 'none' || (el.style.display !== '' ? false : getComputedStyle(el).display === 'none');
|
|
1418
|
+
el.style.display = hidden ? display : 'none';
|
|
1419
|
+
});
|
|
1501
1420
|
}
|
|
1502
1421
|
|
|
1503
|
-
//
|
|
1504
|
-
parseExpression(minPrec) {
|
|
1505
|
-
let left = this.parseUnary();
|
|
1422
|
+
// --- Events --------------------------------------------------------------
|
|
1506
1423
|
|
|
1507
|
-
|
|
1508
|
-
|
|
1424
|
+
on(event, selectorOrHandler, handler) {
|
|
1425
|
+
// Support multiple events: "click mouseenter"
|
|
1426
|
+
const events = event.split(/\s+/);
|
|
1427
|
+
return this.each((_, el) => {
|
|
1428
|
+
events.forEach(evt => {
|
|
1429
|
+
if (typeof selectorOrHandler === 'function') {
|
|
1430
|
+
el.addEventListener(evt, selectorOrHandler);
|
|
1431
|
+
} else if (typeof selectorOrHandler === 'string') {
|
|
1432
|
+
// Delegated event — store wrapper so off() can remove it
|
|
1433
|
+
const wrapper = (e) => {
|
|
1434
|
+
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1435
|
+
const target = e.target.closest(selectorOrHandler);
|
|
1436
|
+
if (target && el.contains(target)) handler.call(target, e);
|
|
1437
|
+
};
|
|
1438
|
+
wrapper._zqOriginal = handler;
|
|
1439
|
+
wrapper._zqSelector = selectorOrHandler;
|
|
1440
|
+
el.addEventListener(evt, wrapper);
|
|
1441
|
+
// Track delegated handlers for removal
|
|
1442
|
+
if (!el._zqDelegated) el._zqDelegated = [];
|
|
1443
|
+
el._zqDelegated.push({ evt, wrapper });
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1509
1448
|
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1449
|
+
off(event, handler) {
|
|
1450
|
+
const events = event.split(/\s+/);
|
|
1451
|
+
return this.each((_, el) => {
|
|
1452
|
+
events.forEach(evt => {
|
|
1453
|
+
// Try direct removal first
|
|
1454
|
+
el.removeEventListener(evt, handler);
|
|
1455
|
+
// Also check delegated handlers
|
|
1456
|
+
if (el._zqDelegated) {
|
|
1457
|
+
el._zqDelegated = el._zqDelegated.filter(d => {
|
|
1458
|
+
if (d.evt === evt && d.wrapper._zqOriginal === handler) {
|
|
1459
|
+
el.removeEventListener(evt, d.wrapper);
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
return true;
|
|
1463
|
+
});
|
|
1521
1464
|
}
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// Binary operators
|
|
1525
|
-
if (tok.t === T.OP && tok.v in PREC) {
|
|
1526
|
-
const prec = PREC[tok.v];
|
|
1527
|
-
if (prec <= minPrec) break;
|
|
1528
|
-
this.next();
|
|
1529
|
-
const right = this.parseExpression(prec);
|
|
1530
|
-
left = { type: 'binary', op: tok.v, left, right };
|
|
1531
|
-
continue;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
// instanceof and in as binary operators
|
|
1535
|
-
if (tok.t === T.IDENT && (tok.v === 'instanceof' || tok.v === 'in') && PREC[tok.v] > minPrec) {
|
|
1536
|
-
const prec = PREC[tok.v];
|
|
1537
|
-
this.next();
|
|
1538
|
-
const right = this.parseExpression(prec);
|
|
1539
|
-
left = { type: 'binary', op: tok.v, left, right };
|
|
1540
|
-
continue;
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
break;
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
return left;
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1547
1467
|
}
|
|
1548
1468
|
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
if (tok.t === T.IDENT && tok.v === 'typeof') {
|
|
1554
|
-
this.next();
|
|
1555
|
-
const arg = this.parseUnary();
|
|
1556
|
-
return { type: 'typeof', arg };
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
// void
|
|
1560
|
-
if (tok.t === T.IDENT && tok.v === 'void') {
|
|
1561
|
-
this.next();
|
|
1562
|
-
this.parseUnary(); // evaluate but discard
|
|
1563
|
-
return { type: 'literal', value: undefined };
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
// !expr
|
|
1567
|
-
if (tok.t === T.OP && tok.v === '!') {
|
|
1568
|
-
this.next();
|
|
1569
|
-
const arg = this.parseUnary();
|
|
1570
|
-
return { type: 'not', arg };
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
// -expr, +expr
|
|
1574
|
-
if (tok.t === T.OP && (tok.v === '-' || tok.v === '+')) {
|
|
1575
|
-
this.next();
|
|
1576
|
-
const arg = this.parseUnary();
|
|
1577
|
-
return { type: 'unary', op: tok.v, arg };
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
return this.parsePostfix();
|
|
1469
|
+
one(event, handler) {
|
|
1470
|
+
return this.each((_, el) => {
|
|
1471
|
+
el.addEventListener(event, handler, { once: true });
|
|
1472
|
+
});
|
|
1581
1473
|
}
|
|
1582
1474
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1475
|
+
trigger(event, detail) {
|
|
1476
|
+
return this.each((_, el) => {
|
|
1477
|
+
el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1585
1480
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1481
|
+
// Convenience event shorthands
|
|
1482
|
+
click(fn) { return fn ? this.on('click', fn) : this.trigger('click'); }
|
|
1483
|
+
submit(fn) { return fn ? this.on('submit', fn) : this.trigger('submit'); }
|
|
1484
|
+
focus() { this.first()?.focus(); return this; }
|
|
1485
|
+
blur() { this.first()?.blur(); return this; }
|
|
1486
|
+
hover(enterFn, leaveFn) {
|
|
1487
|
+
this.on('mouseenter', enterFn);
|
|
1488
|
+
return this.on('mouseleave', leaveFn || enterFn);
|
|
1489
|
+
}
|
|
1588
1490
|
|
|
1589
|
-
|
|
1590
|
-
if (tok.t === T.PUNC && tok.v === '.') {
|
|
1591
|
-
this.next();
|
|
1592
|
-
const prop = this.next();
|
|
1593
|
-
left = { type: 'member', obj: left, prop: prop.v, computed: false };
|
|
1594
|
-
// Check for method call: a.b()
|
|
1595
|
-
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1596
|
-
left = this._parseCall(left);
|
|
1597
|
-
}
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1491
|
+
// --- Animation -----------------------------------------------------------
|
|
1600
1492
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1493
|
+
animate(props, duration = 300, easing = 'ease') {
|
|
1494
|
+
// Empty collection — resolve immediately
|
|
1495
|
+
if (this.length === 0) return Promise.resolve(this);
|
|
1496
|
+
return new Promise(resolve => {
|
|
1497
|
+
let resolved = false;
|
|
1498
|
+
const count = { done: 0 };
|
|
1499
|
+
const listeners = [];
|
|
1500
|
+
this.each((_, el) => {
|
|
1501
|
+
el.style.transition = `all ${duration}ms ${easing}`;
|
|
1502
|
+
requestAnimationFrame(() => {
|
|
1503
|
+
Object.assign(el.style, props);
|
|
1504
|
+
const onEnd = () => {
|
|
1505
|
+
el.removeEventListener('transitionend', onEnd);
|
|
1506
|
+
el.style.transition = '';
|
|
1507
|
+
if (!resolved && ++count.done >= this.length) {
|
|
1508
|
+
resolved = true;
|
|
1509
|
+
resolve(this);
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
el.addEventListener('transitionend', onEnd);
|
|
1513
|
+
listeners.push({ el, onEnd });
|
|
1514
|
+
});
|
|
1515
|
+
});
|
|
1516
|
+
// Fallback in case transitionend doesn't fire
|
|
1517
|
+
setTimeout(() => {
|
|
1518
|
+
if (!resolved) {
|
|
1519
|
+
resolved = true;
|
|
1520
|
+
// Clean up any remaining transitionend listeners
|
|
1521
|
+
for (const { el, onEnd } of listeners) {
|
|
1522
|
+
el.removeEventListener('transitionend', onEnd);
|
|
1523
|
+
el.style.transition = '';
|
|
1620
1524
|
}
|
|
1525
|
+
resolve(this);
|
|
1621
1526
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
// Computed access: a[b]
|
|
1626
|
-
if (tok.t === T.PUNC && tok.v === '[') {
|
|
1627
|
-
this.next();
|
|
1628
|
-
const prop = this.parseExpression(0);
|
|
1629
|
-
this.expect(T.PUNC, ']');
|
|
1630
|
-
left = { type: 'member', obj: left, prop, computed: true };
|
|
1631
|
-
// Check for method call: a[b]()
|
|
1632
|
-
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1633
|
-
left = this._parseCall(left);
|
|
1634
|
-
}
|
|
1635
|
-
continue;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
// Function call: fn()
|
|
1639
|
-
if (tok.t === T.PUNC && tok.v === '(') {
|
|
1640
|
-
left = this._parseCall(left);
|
|
1641
|
-
continue;
|
|
1642
|
-
}
|
|
1527
|
+
}, duration + 50);
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1643
1530
|
|
|
1644
|
-
|
|
1645
|
-
}
|
|
1531
|
+
fadeIn(duration = 300) {
|
|
1532
|
+
return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration);
|
|
1533
|
+
}
|
|
1646
1534
|
|
|
1647
|
-
|
|
1535
|
+
fadeOut(duration = 300) {
|
|
1536
|
+
return this.animate({ opacity: '0' }, duration).then(col => col.hide());
|
|
1648
1537
|
}
|
|
1649
1538
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1539
|
+
fadeToggle(duration = 300) {
|
|
1540
|
+
return Promise.all(this.elements.map(el => {
|
|
1541
|
+
const cs = getComputedStyle(el);
|
|
1542
|
+
const visible = cs.opacity !== '0' && cs.display !== 'none';
|
|
1543
|
+
const col = new ZQueryCollection([el]);
|
|
1544
|
+
return visible ? col.fadeOut(duration) : col.fadeIn(duration);
|
|
1545
|
+
})).then(() => this);
|
|
1653
1546
|
}
|
|
1654
1547
|
|
|
1655
|
-
|
|
1656
|
-
this.
|
|
1657
|
-
const args = [];
|
|
1658
|
-
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
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
|
-
}
|
|
1665
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1666
|
-
}
|
|
1667
|
-
this.expect(T.PUNC, ')');
|
|
1668
|
-
return args;
|
|
1548
|
+
fadeTo(duration, opacity) {
|
|
1549
|
+
return this.animate({ opacity: String(opacity) }, duration);
|
|
1669
1550
|
}
|
|
1670
1551
|
|
|
1671
|
-
|
|
1672
|
-
|
|
1552
|
+
slideDown(duration = 300) {
|
|
1553
|
+
return this.each((_, el) => {
|
|
1554
|
+
el.style.display = '';
|
|
1555
|
+
el.style.overflow = 'hidden';
|
|
1556
|
+
const h = el.scrollHeight + 'px';
|
|
1557
|
+
el.style.maxHeight = '0';
|
|
1558
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1559
|
+
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1560
|
+
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1673
1563
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1564
|
+
slideUp(duration = 300) {
|
|
1565
|
+
return this.each((_, el) => {
|
|
1566
|
+
el.style.overflow = 'hidden';
|
|
1567
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1568
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1569
|
+
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1570
|
+
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1679
1573
|
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// Arrow function with parens: () =>, (a) =>, (a, b) =>
|
|
1693
|
-
// or regular grouping: (expr)
|
|
1694
|
-
if (tok.t === T.PUNC && tok.v === '(') {
|
|
1695
|
-
const savedPos = this.pos;
|
|
1696
|
-
this.next(); // consume (
|
|
1697
|
-
const params = [];
|
|
1698
|
-
let couldBeArrow = true;
|
|
1699
|
-
|
|
1700
|
-
if (this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
1701
|
-
// () => ... — no params
|
|
1574
|
+
slideToggle(duration = 300) {
|
|
1575
|
+
return this.each((_, el) => {
|
|
1576
|
+
if (el.style.display === 'none' || getComputedStyle(el).display === 'none') {
|
|
1577
|
+
el.style.display = '';
|
|
1578
|
+
el.style.overflow = 'hidden';
|
|
1579
|
+
const h = el.scrollHeight + 'px';
|
|
1580
|
+
el.style.maxHeight = '0';
|
|
1581
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1582
|
+
requestAnimationFrame(() => { el.style.maxHeight = h; });
|
|
1583
|
+
setTimeout(() => { el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1702
1584
|
} else {
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
this.next();
|
|
1709
|
-
} else {
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
} else {
|
|
1713
|
-
couldBeArrow = false;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1585
|
+
el.style.overflow = 'hidden';
|
|
1586
|
+
el.style.maxHeight = el.scrollHeight + 'px';
|
|
1587
|
+
el.style.transition = `max-height ${duration}ms ease`;
|
|
1588
|
+
requestAnimationFrame(() => { el.style.maxHeight = '0'; });
|
|
1589
|
+
setTimeout(() => { el.style.display = 'none'; el.style.maxHeight = ''; el.style.overflow = ''; el.style.transition = ''; }, duration);
|
|
1716
1590
|
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1717
1593
|
|
|
1718
|
-
|
|
1719
|
-
this.next(); // consume )
|
|
1720
|
-
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
1721
|
-
this.next(); // consume =>
|
|
1722
|
-
const body = this.parseExpression(0);
|
|
1723
|
-
return { type: 'arrow', params, body };
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1594
|
+
// --- Form helpers --------------------------------------------------------
|
|
1726
1595
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
return expr;
|
|
1733
|
-
}
|
|
1596
|
+
serialize() {
|
|
1597
|
+
const form = this.first();
|
|
1598
|
+
if (!form || form.tagName !== 'FORM') return '';
|
|
1599
|
+
return new URLSearchParams(new FormData(form)).toString();
|
|
1600
|
+
}
|
|
1734
1601
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
}
|
|
1746
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1602
|
+
serializeObject() {
|
|
1603
|
+
const form = this.first();
|
|
1604
|
+
if (!form || form.tagName !== 'FORM') return {};
|
|
1605
|
+
const obj = {};
|
|
1606
|
+
new FormData(form).forEach((v, k) => {
|
|
1607
|
+
if (obj[k] !== undefined) {
|
|
1608
|
+
if (!Array.isArray(obj[k])) obj[k] = [obj[k]];
|
|
1609
|
+
obj[k].push(v);
|
|
1610
|
+
} else {
|
|
1611
|
+
obj[k] = v;
|
|
1747
1612
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1613
|
+
});
|
|
1614
|
+
return obj;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1751
1617
|
|
|
1752
|
-
// Object literal
|
|
1753
|
-
if (tok.t === T.PUNC && tok.v === '{') {
|
|
1754
|
-
this.next();
|
|
1755
|
-
const properties = [];
|
|
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
1618
|
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1619
|
+
// ---------------------------------------------------------------------------
|
|
1620
|
+
// Helper — create document fragment from HTML string
|
|
1621
|
+
// ---------------------------------------------------------------------------
|
|
1622
|
+
function createFragment(html) {
|
|
1623
|
+
const tpl = document.createElement('template');
|
|
1624
|
+
tpl.innerHTML = html.trim();
|
|
1625
|
+
return tpl.content;
|
|
1626
|
+
}
|
|
1770
1627
|
|
|
1771
|
-
// Shorthand property: { foo } means { foo: foo }
|
|
1772
|
-
if (this.peek().t === T.PUNC && (this.peek().v === ',' || this.peek().v === '}')) {
|
|
1773
|
-
properties.push({ key, value: { type: 'ident', name: key } });
|
|
1774
|
-
} else {
|
|
1775
|
-
this.expect(T.PUNC, ':');
|
|
1776
|
-
properties.push({ key, value: this.parseExpression(0) });
|
|
1777
|
-
}
|
|
1778
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1779
|
-
}
|
|
1780
|
-
this.expect(T.PUNC, '}');
|
|
1781
|
-
return { type: 'object', properties };
|
|
1782
|
-
}
|
|
1783
1628
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1629
|
+
// ---------------------------------------------------------------------------
|
|
1630
|
+
// $() — main selector / creator (returns ZQueryCollection, like jQuery)
|
|
1631
|
+
// ---------------------------------------------------------------------------
|
|
1632
|
+
function query(selector, context) {
|
|
1633
|
+
// null / undefined
|
|
1634
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1787
1635
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
if (tok.v === 'false') return { type: 'literal', value: false };
|
|
1791
|
-
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
1792
|
-
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
1636
|
+
// Already a collection — return as-is
|
|
1637
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1793
1638
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
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
|
-
}
|
|
1803
|
-
let args = [];
|
|
1804
|
-
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1805
|
-
args = this._parseArgs();
|
|
1806
|
-
}
|
|
1807
|
-
return { type: 'new', callee: classExpr, args };
|
|
1808
|
-
}
|
|
1639
|
+
// DOM element or Window — wrap in collection
|
|
1640
|
+
if (selector instanceof Node || selector === window) {
|
|
1641
|
+
return new ZQueryCollection([selector]);
|
|
1642
|
+
}
|
|
1809
1643
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
return { type: 'arrow', params: [tok.v], body };
|
|
1815
|
-
}
|
|
1644
|
+
// NodeList / HTMLCollection / Array — wrap in collection
|
|
1645
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1646
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1647
|
+
}
|
|
1816
1648
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1649
|
+
// HTML string → create elements, wrap in collection
|
|
1650
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
1651
|
+
const fragment = createFragment(selector);
|
|
1652
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1653
|
+
}
|
|
1819
1654
|
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1655
|
+
// CSS selector string → querySelectorAll (collection)
|
|
1656
|
+
if (typeof selector === 'string') {
|
|
1657
|
+
const root = context
|
|
1658
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1659
|
+
: document;
|
|
1660
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1823
1661
|
}
|
|
1662
|
+
|
|
1663
|
+
return new ZQueryCollection([]);
|
|
1824
1664
|
}
|
|
1825
1665
|
|
|
1666
|
+
|
|
1826
1667
|
// ---------------------------------------------------------------------------
|
|
1827
|
-
//
|
|
1668
|
+
// $.all() — collection selector (returns ZQueryCollection for CSS selectors)
|
|
1828
1669
|
// ---------------------------------------------------------------------------
|
|
1670
|
+
function queryAll(selector, context) {
|
|
1671
|
+
// null / undefined
|
|
1672
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1829
1673
|
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
1833
|
-
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
1834
|
-
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
1835
|
-
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
1836
|
-
]);
|
|
1837
|
-
|
|
1838
|
-
const SAFE_STRING_METHODS = new Set([
|
|
1839
|
-
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
1840
|
-
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
1841
|
-
'toUpperCase', 'split', 'replace', 'replaceAll', 'match', 'search',
|
|
1842
|
-
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'at',
|
|
1843
|
-
'toString', 'valueOf',
|
|
1844
|
-
]);
|
|
1845
|
-
|
|
1846
|
-
const SAFE_NUMBER_METHODS = new Set([
|
|
1847
|
-
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
1848
|
-
]);
|
|
1674
|
+
// Already a collection
|
|
1675
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1849
1676
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
]);
|
|
1677
|
+
// DOM element or Window
|
|
1678
|
+
if (selector instanceof Node || selector === window) {
|
|
1679
|
+
return new ZQueryCollection([selector]);
|
|
1680
|
+
}
|
|
1853
1681
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
]);
|
|
1682
|
+
// NodeList / HTMLCollection / Array
|
|
1683
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1684
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1685
|
+
}
|
|
1859
1686
|
|
|
1860
|
-
|
|
1687
|
+
// HTML string → create elements
|
|
1688
|
+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
|
|
1689
|
+
const fragment = createFragment(selector);
|
|
1690
|
+
return new ZQueryCollection([...fragment.childNodes].filter(n => n.nodeType === 1));
|
|
1691
|
+
}
|
|
1861
1692
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
1870
|
-
'call', 'apply', 'bind',
|
|
1871
|
-
]);
|
|
1872
|
-
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
1693
|
+
// CSS selector string → querySelectorAll (collection)
|
|
1694
|
+
if (typeof selector === 'string') {
|
|
1695
|
+
const root = context
|
|
1696
|
+
? (typeof context === 'string' ? document.querySelector(context) : context)
|
|
1697
|
+
: document;
|
|
1698
|
+
return new ZQueryCollection([...root.querySelectorAll(selector)]);
|
|
1699
|
+
}
|
|
1873
1700
|
|
|
1874
|
-
|
|
1875
|
-
if (obj !== null && obj !== undefined && (typeof obj === 'object' || typeof obj === 'function')) return true;
|
|
1876
|
-
if (typeof obj === 'string') return SAFE_STRING_METHODS.has(prop);
|
|
1877
|
-
if (typeof obj === 'number') return SAFE_NUMBER_METHODS.has(prop);
|
|
1878
|
-
return false;
|
|
1701
|
+
return new ZQueryCollection([]);
|
|
1879
1702
|
}
|
|
1880
1703
|
|
|
1881
|
-
function evaluate(node, scope) {
|
|
1882
|
-
if (!node) return undefined;
|
|
1883
|
-
|
|
1884
|
-
switch (node.type) {
|
|
1885
|
-
case 'literal':
|
|
1886
|
-
return node.value;
|
|
1887
1704
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
if (name === 'parseInt') return parseInt;
|
|
1906
|
-
if (name === 'parseFloat') return parseFloat;
|
|
1907
|
-
if (name === 'isNaN') return isNaN;
|
|
1908
|
-
if (name === 'isFinite') return isFinite;
|
|
1909
|
-
if (name === 'Infinity') return Infinity;
|
|
1910
|
-
if (name === 'NaN') return NaN;
|
|
1911
|
-
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
1912
|
-
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
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;
|
|
1920
|
-
return undefined;
|
|
1921
|
-
}
|
|
1705
|
+
// ---------------------------------------------------------------------------
|
|
1706
|
+
// Quick-ref shortcuts, on $ namespace)
|
|
1707
|
+
// ---------------------------------------------------------------------------
|
|
1708
|
+
query.id = (id) => document.getElementById(id);
|
|
1709
|
+
query.class = (name) => document.querySelector(`.${name}`);
|
|
1710
|
+
query.classes = (name) => new ZQueryCollection(Array.from(document.getElementsByClassName(name)));
|
|
1711
|
+
query.tag = (name) => new ZQueryCollection(Array.from(document.getElementsByTagName(name)));
|
|
1712
|
+
Object.defineProperty(query, 'name', {
|
|
1713
|
+
value: (name) => new ZQueryCollection(Array.from(document.getElementsByName(name))),
|
|
1714
|
+
writable: true, configurable: true
|
|
1715
|
+
});
|
|
1716
|
+
query.children = (parentId) => {
|
|
1717
|
+
const p = document.getElementById(parentId);
|
|
1718
|
+
return new ZQueryCollection(p ? Array.from(p.children) : []);
|
|
1719
|
+
};
|
|
1720
|
+
query.qs = (sel, ctx = document) => ctx.querySelector(sel);
|
|
1721
|
+
query.qsa = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
|
|
1922
1722
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1723
|
+
// Create element shorthand — returns ZQueryCollection for chaining
|
|
1724
|
+
query.create = (tag, attrs = {}, ...children) => {
|
|
1725
|
+
const el = document.createElement(tag);
|
|
1726
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1727
|
+
if (k === 'class') el.className = v;
|
|
1728
|
+
else if (k === 'style' && typeof v === 'object') Object.assign(el.style, v);
|
|
1729
|
+
else if (k.startsWith('on') && typeof v === 'function') el.addEventListener(k.slice(2).toLowerCase(), v);
|
|
1730
|
+
else if (k === 'data' && typeof v === 'object') Object.entries(v).forEach(([dk, dv]) => { el.dataset[dk] = dv; });
|
|
1731
|
+
else el.setAttribute(k, v);
|
|
1732
|
+
}
|
|
1733
|
+
children.flat().forEach(child => {
|
|
1734
|
+
if (typeof child === 'string') el.appendChild(document.createTextNode(child));
|
|
1735
|
+
else if (child instanceof Node) el.appendChild(child);
|
|
1736
|
+
});
|
|
1737
|
+
return new ZQueryCollection(el);
|
|
1738
|
+
};
|
|
1935
1739
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
return obj[prop];
|
|
1942
|
-
}
|
|
1740
|
+
// DOM ready
|
|
1741
|
+
query.ready = (fn) => {
|
|
1742
|
+
if (document.readyState !== 'loading') fn();
|
|
1743
|
+
else document.addEventListener('DOMContentLoaded', fn);
|
|
1744
|
+
};
|
|
1943
1745
|
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1746
|
+
// Global event listeners — supports direct, delegated, and target-bound forms
|
|
1747
|
+
// $.on('keydown', handler) → direct listener on document
|
|
1748
|
+
// $.on('click', '.btn', handler) → delegated via closest()
|
|
1749
|
+
// $.on('scroll', window, handler) → direct listener on target
|
|
1750
|
+
query.on = (event, selectorOrHandler, handler) => {
|
|
1751
|
+
if (typeof selectorOrHandler === 'function') {
|
|
1752
|
+
// 2-arg: direct document listener (keydown, resize, etc.)
|
|
1753
|
+
document.addEventListener(event, selectorOrHandler);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
// EventTarget (window, element, etc.) — direct listener on target
|
|
1757
|
+
if (typeof selectorOrHandler === 'object' && typeof selectorOrHandler.addEventListener === 'function') {
|
|
1758
|
+
selectorOrHandler.addEventListener(event, handler);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
// 3-arg string: delegated
|
|
1762
|
+
document.addEventListener(event, (e) => {
|
|
1763
|
+
if (!e.target || typeof e.target.closest !== 'function') return;
|
|
1764
|
+
const target = e.target.closest(selectorOrHandler);
|
|
1765
|
+
if (target) handler.call(target, e);
|
|
1766
|
+
});
|
|
1767
|
+
};
|
|
1951
1768
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1769
|
+
// Remove a direct global listener
|
|
1770
|
+
query.off = (event, handler) => {
|
|
1771
|
+
document.removeEventListener(event, handler);
|
|
1772
|
+
};
|
|
1956
1773
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1774
|
+
// Extend collection prototype (like $.fn in jQuery)
|
|
1775
|
+
query.fn = ZQueryCollection.prototype;
|
|
1776
|
+
|
|
1777
|
+
// --- src/expression.js -------------------------------------------
|
|
1778
|
+
/**
|
|
1779
|
+
* zQuery Expression Parser — CSP-safe expression evaluator
|
|
1780
|
+
*
|
|
1781
|
+
* Replaces `new Function()` / `eval()` with a hand-written parser that
|
|
1782
|
+
* evaluates expressions safely without violating Content Security Policy.
|
|
1783
|
+
*
|
|
1784
|
+
* Supports:
|
|
1785
|
+
* - Property access: user.name, items[0], items[i]
|
|
1786
|
+
* - Method calls: items.length, str.toUpperCase()
|
|
1787
|
+
* - Arithmetic: a + b, count * 2, i % 2
|
|
1788
|
+
* - Comparison: a === b, count > 0, x != null
|
|
1789
|
+
* - Logical: a && b, a || b, !a
|
|
1790
|
+
* - Ternary: a ? b : c
|
|
1791
|
+
* - Typeof: typeof x
|
|
1792
|
+
* - Unary: -a, +a, !a
|
|
1793
|
+
* - Literals: 42, 'hello', "world", true, false, null, undefined
|
|
1794
|
+
* - Template literals: `Hello ${name}`
|
|
1795
|
+
* - Array literals: [1, 2, 3]
|
|
1796
|
+
* - Object literals: { foo: 'bar', baz: 1 }
|
|
1797
|
+
* - Grouping: (a + b) * c
|
|
1798
|
+
* - Nullish coalescing: a ?? b
|
|
1799
|
+
* - Optional chaining: a?.b, a?.[b], a?.()
|
|
1800
|
+
* - Arrow functions: x => x.id, (a, b) => a + b
|
|
1801
|
+
*/
|
|
1975
1802
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
1981
|
-
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
1982
|
-
const args = _evalArgs(node.args, scope);
|
|
1983
|
-
return new Ctor(...args);
|
|
1984
|
-
}
|
|
1985
|
-
return undefined;
|
|
1986
|
-
}
|
|
1803
|
+
// Token types
|
|
1804
|
+
const T = {
|
|
1805
|
+
NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
|
|
1806
|
+
};
|
|
1987
1807
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1808
|
+
// Operator precedence (higher = binds tighter)
|
|
1809
|
+
const PREC = {
|
|
1810
|
+
'??': 2,
|
|
1811
|
+
'||': 3,
|
|
1812
|
+
'&&': 4,
|
|
1813
|
+
'==': 8, '!=': 8, '===': 8, '!==': 8,
|
|
1814
|
+
'<': 9, '>': 9, '<=': 9, '>=': 9, 'instanceof': 9, 'in': 9,
|
|
1815
|
+
'+': 11, '-': 11,
|
|
1816
|
+
'*': 12, '/': 12, '%': 12,
|
|
1817
|
+
};
|
|
1990
1818
|
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1819
|
+
const KEYWORDS = new Set([
|
|
1820
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
1821
|
+
'new', 'void'
|
|
1822
|
+
]);
|
|
1995
1823
|
|
|
1996
|
-
|
|
1997
|
-
|
|
1824
|
+
// ---------------------------------------------------------------------------
|
|
1825
|
+
// Tokenizer
|
|
1826
|
+
// ---------------------------------------------------------------------------
|
|
1827
|
+
function tokenize(expr) {
|
|
1828
|
+
const tokens = [];
|
|
1829
|
+
let i = 0;
|
|
1830
|
+
const len = expr.length;
|
|
1998
1831
|
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
return typeof evaluate(node.arg, scope);
|
|
2002
|
-
} catch {
|
|
2003
|
-
return 'undefined';
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
1832
|
+
while (i < len) {
|
|
1833
|
+
const ch = expr[i];
|
|
2006
1834
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1835
|
+
// Whitespace
|
|
1836
|
+
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') { i++; continue; }
|
|
1837
|
+
|
|
1838
|
+
// Numbers
|
|
1839
|
+
if ((ch >= '0' && ch <= '9') || (ch === '.' && i + 1 < len && expr[i + 1] >= '0' && expr[i + 1] <= '9')) {
|
|
1840
|
+
let num = '';
|
|
1841
|
+
if (ch === '0' && i + 1 < len && (expr[i + 1] === 'x' || expr[i + 1] === 'X')) {
|
|
1842
|
+
num = '0x'; i += 2;
|
|
1843
|
+
while (i < len && /[0-9a-fA-F]/.test(expr[i])) num += expr[i++];
|
|
1844
|
+
} else {
|
|
1845
|
+
while (i < len && ((expr[i] >= '0' && expr[i] <= '9') || expr[i] === '.')) num += expr[i++];
|
|
1846
|
+
if (i < len && (expr[i] === 'e' || expr[i] === 'E')) {
|
|
1847
|
+
num += expr[i++];
|
|
1848
|
+
if (i < len && (expr[i] === '+' || expr[i] === '-')) num += expr[i++];
|
|
1849
|
+
while (i < len && expr[i] >= '0' && expr[i] <= '9') num += expr[i++];
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
tokens.push({ t: T.NUM, v: Number(num) });
|
|
1853
|
+
continue;
|
|
2010
1854
|
}
|
|
2011
1855
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
1856
|
+
// Strings
|
|
1857
|
+
if (ch === "'" || ch === '"') {
|
|
1858
|
+
const quote = ch;
|
|
1859
|
+
let str = '';
|
|
1860
|
+
i++;
|
|
1861
|
+
while (i < len && expr[i] !== quote) {
|
|
1862
|
+
if (expr[i] === '\\' && i + 1 < len) {
|
|
1863
|
+
const esc = expr[++i];
|
|
1864
|
+
if (esc === 'n') str += '\n';
|
|
1865
|
+
else if (esc === 't') str += '\t';
|
|
1866
|
+
else if (esc === 'r') str += '\r';
|
|
1867
|
+
else if (esc === '\\') str += '\\';
|
|
1868
|
+
else if (esc === quote) str += quote;
|
|
1869
|
+
else str += esc;
|
|
2020
1870
|
} else {
|
|
2021
|
-
|
|
1871
|
+
str += expr[i];
|
|
2022
1872
|
}
|
|
1873
|
+
i++;
|
|
2023
1874
|
}
|
|
2024
|
-
|
|
1875
|
+
i++; // closing quote
|
|
1876
|
+
tokens.push({ t: T.STR, v: str });
|
|
1877
|
+
continue;
|
|
2025
1878
|
}
|
|
2026
1879
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
1880
|
+
// Template literals
|
|
1881
|
+
if (ch === '`') {
|
|
1882
|
+
const parts = []; // alternating: string, expr, string, expr, ...
|
|
1883
|
+
let str = '';
|
|
1884
|
+
i++;
|
|
1885
|
+
while (i < len && expr[i] !== '`') {
|
|
1886
|
+
if (expr[i] === '$' && i + 1 < len && expr[i + 1] === '{') {
|
|
1887
|
+
parts.push(str);
|
|
1888
|
+
str = '';
|
|
1889
|
+
i += 2;
|
|
1890
|
+
let depth = 1;
|
|
1891
|
+
let inner = '';
|
|
1892
|
+
while (i < len && depth > 0) {
|
|
1893
|
+
if (expr[i] === '{') depth++;
|
|
1894
|
+
else if (expr[i] === '}') { depth--; if (depth === 0) break; }
|
|
1895
|
+
inner += expr[i++];
|
|
2034
1896
|
}
|
|
1897
|
+
i++; // closing }
|
|
1898
|
+
parts.push({ expr: inner });
|
|
2035
1899
|
} else {
|
|
2036
|
-
|
|
1900
|
+
if (expr[i] === '\\' && i + 1 < len) { str += expr[++i]; }
|
|
1901
|
+
else str += expr[i];
|
|
1902
|
+
i++;
|
|
2037
1903
|
}
|
|
2038
1904
|
}
|
|
2039
|
-
|
|
1905
|
+
i++; // closing backtick
|
|
1906
|
+
parts.push(str);
|
|
1907
|
+
tokens.push({ t: T.TMPL, v: parts });
|
|
1908
|
+
continue;
|
|
2040
1909
|
}
|
|
2041
1910
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
2049
|
-
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
2050
|
-
};
|
|
1911
|
+
// Identifiers & keywords
|
|
1912
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
1913
|
+
let ident = '';
|
|
1914
|
+
while (i < len && /[\w$]/.test(expr[i])) ident += expr[i++];
|
|
1915
|
+
tokens.push({ t: T.IDENT, v: ident });
|
|
1916
|
+
continue;
|
|
2051
1917
|
}
|
|
2052
1918
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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);
|
|
1919
|
+
// Multi-char operators
|
|
1920
|
+
const two = expr.slice(i, i + 3);
|
|
1921
|
+
if (two === '===' || two === '!==' || two === '?.') {
|
|
1922
|
+
if (two === '?.') {
|
|
1923
|
+
tokens.push({ t: T.OP, v: '?.' });
|
|
1924
|
+
i += 2;
|
|
1925
|
+
} else {
|
|
1926
|
+
tokens.push({ t: T.OP, v: two });
|
|
1927
|
+
i += 3;
|
|
2068
1928
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
1929
|
+
continue;
|
|
1930
|
+
}
|
|
1931
|
+
const pair = expr.slice(i, i + 2);
|
|
1932
|
+
if (pair === '==' || pair === '!=' || pair === '<=' || pair === '>=' ||
|
|
1933
|
+
pair === '&&' || pair === '||' || pair === '??' || pair === '?.' ||
|
|
1934
|
+
pair === '=>') {
|
|
1935
|
+
tokens.push({ t: T.OP, v: pair });
|
|
1936
|
+
i += 2;
|
|
1937
|
+
continue;
|
|
2071
1938
|
}
|
|
2072
|
-
}
|
|
2073
|
-
return result;
|
|
2074
|
-
}
|
|
2075
1939
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
1940
|
+
// Single char operators and punctuation
|
|
1941
|
+
if ('+-*/%'.includes(ch)) {
|
|
1942
|
+
tokens.push({ t: T.OP, v: ch });
|
|
1943
|
+
i++; continue;
|
|
1944
|
+
}
|
|
1945
|
+
if ('<>=!'.includes(ch)) {
|
|
1946
|
+
tokens.push({ t: T.OP, v: ch });
|
|
1947
|
+
i++; continue;
|
|
1948
|
+
}
|
|
1949
|
+
// Spread operator: ...
|
|
1950
|
+
if (ch === '.' && i + 2 < len && expr[i + 1] === '.' && expr[i + 2] === '.') {
|
|
1951
|
+
tokens.push({ t: T.OP, v: '...' });
|
|
1952
|
+
i += 3; continue;
|
|
1953
|
+
}
|
|
1954
|
+
if ('()[]{},.?:'.includes(ch)) {
|
|
1955
|
+
tokens.push({ t: T.PUNC, v: ch });
|
|
1956
|
+
i++; continue;
|
|
1957
|
+
}
|
|
2082
1958
|
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
const obj = evaluate(callee.obj, scope);
|
|
2086
|
-
if (obj == null) return undefined;
|
|
2087
|
-
const prop = callee.computed ? evaluate(callee.prop, scope) : callee.prop;
|
|
2088
|
-
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2089
|
-
const fn = obj[prop];
|
|
2090
|
-
if (typeof fn !== 'function') return undefined;
|
|
2091
|
-
return fn.apply(obj, args);
|
|
1959
|
+
// Unknown — skip
|
|
1960
|
+
i++;
|
|
2092
1961
|
}
|
|
2093
1962
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
if (typeof fn !== 'function') return undefined;
|
|
2097
|
-
return fn(...args);
|
|
1963
|
+
tokens.push({ t: T.EOF, v: null });
|
|
1964
|
+
return tokens;
|
|
2098
1965
|
}
|
|
2099
1966
|
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
}
|
|
2109
|
-
if (node.op === '||') {
|
|
2110
|
-
const left = evaluate(node.left, scope);
|
|
2111
|
-
return left ? left : evaluate(node.right, scope);
|
|
2112
|
-
}
|
|
2113
|
-
if (node.op === '??') {
|
|
2114
|
-
const left = evaluate(node.left, scope);
|
|
2115
|
-
return left != null ? left : evaluate(node.right, scope);
|
|
1967
|
+
// ---------------------------------------------------------------------------
|
|
1968
|
+
// Parser — Pratt (precedence climbing)
|
|
1969
|
+
// ---------------------------------------------------------------------------
|
|
1970
|
+
class Parser {
|
|
1971
|
+
constructor(tokens, scope) {
|
|
1972
|
+
this.tokens = tokens;
|
|
1973
|
+
this.pos = 0;
|
|
1974
|
+
this.scope = scope;
|
|
2116
1975
|
}
|
|
2117
1976
|
|
|
2118
|
-
|
|
2119
|
-
|
|
1977
|
+
peek() { return this.tokens[this.pos]; }
|
|
1978
|
+
next() { return this.tokens[this.pos++]; }
|
|
2120
1979
|
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
case '==': return left == right;
|
|
2128
|
-
case '!=': return left != right;
|
|
2129
|
-
case '===': return left === right;
|
|
2130
|
-
case '!==': return left !== right;
|
|
2131
|
-
case '<': return left < right;
|
|
2132
|
-
case '>': return left > right;
|
|
2133
|
-
case '<=': return left <= right;
|
|
2134
|
-
case '>=': return left >= right;
|
|
2135
|
-
case 'instanceof': return left instanceof right;
|
|
2136
|
-
case 'in': return left in right;
|
|
2137
|
-
default: return undefined;
|
|
1980
|
+
expect(type, val) {
|
|
1981
|
+
const t = this.next();
|
|
1982
|
+
if (t.t !== type || (val !== undefined && t.v !== val)) {
|
|
1983
|
+
throw new Error(`Expected ${val || type} but got ${t.v}`);
|
|
1984
|
+
}
|
|
1985
|
+
return t;
|
|
2138
1986
|
}
|
|
2139
|
-
}
|
|
2140
1987
|
|
|
1988
|
+
match(type, val) {
|
|
1989
|
+
const t = this.peek();
|
|
1990
|
+
if (t.t === type && (val === undefined || t.v === val)) {
|
|
1991
|
+
return this.next();
|
|
1992
|
+
}
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
2141
1995
|
|
|
2142
|
-
//
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
* Safely evaluate a JS expression string against scope layers.
|
|
2148
|
-
*
|
|
2149
|
-
* @param {string} expr — expression string
|
|
2150
|
-
* @param {object[]} scope — array of scope objects, checked in order
|
|
2151
|
-
* Typical: [loopVars, state, { props, refs, $ }]
|
|
2152
|
-
* @returns {*} — evaluation result, or undefined on error
|
|
2153
|
-
*/
|
|
1996
|
+
// Main entry
|
|
1997
|
+
parse() {
|
|
1998
|
+
const result = this.parseExpression(0);
|
|
1999
|
+
return result;
|
|
2000
|
+
}
|
|
2154
2001
|
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const _astCache = new Map();
|
|
2159
|
-
const _AST_CACHE_MAX = 512;
|
|
2002
|
+
// Precedence climbing
|
|
2003
|
+
parseExpression(minPrec) {
|
|
2004
|
+
let left = this.parseUnary();
|
|
2160
2005
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
const trimmed = expr.trim();
|
|
2164
|
-
if (!trimmed) return undefined;
|
|
2006
|
+
while (true) {
|
|
2007
|
+
const tok = this.peek();
|
|
2165
2008
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2009
|
+
// Ternary
|
|
2010
|
+
if (tok.t === T.PUNC && tok.v === '?') {
|
|
2011
|
+
// Distinguish ternary ? from optional chaining ?.
|
|
2012
|
+
if (this.tokens[this.pos + 1]?.v !== '.') {
|
|
2013
|
+
if (1 <= minPrec) break; // ternary has very low precedence
|
|
2014
|
+
this.next(); // consume ?
|
|
2015
|
+
const truthy = this.parseExpression(0);
|
|
2016
|
+
this.expect(T.PUNC, ':');
|
|
2017
|
+
const falsy = this.parseExpression(0);
|
|
2018
|
+
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
2019
|
+
continue;
|
|
2172
2020
|
}
|
|
2173
2021
|
}
|
|
2174
|
-
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2175
|
-
}
|
|
2176
2022
|
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2023
|
+
// Binary operators
|
|
2024
|
+
if (tok.t === T.OP && tok.v in PREC) {
|
|
2025
|
+
const prec = PREC[tok.v];
|
|
2026
|
+
if (prec <= minPrec) break;
|
|
2027
|
+
this.next();
|
|
2028
|
+
const right = this.parseExpression(prec);
|
|
2029
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2186
2032
|
|
|
2187
|
-
//
|
|
2188
|
-
if (
|
|
2189
|
-
const
|
|
2190
|
-
|
|
2033
|
+
// instanceof and in as binary operators
|
|
2034
|
+
if (tok.t === T.IDENT && (tok.v === 'instanceof' || tok.v === 'in') && PREC[tok.v] > minPrec) {
|
|
2035
|
+
const prec = PREC[tok.v];
|
|
2036
|
+
this.next();
|
|
2037
|
+
const right = this.parseExpression(prec);
|
|
2038
|
+
left = { type: 'binary', op: tok.v, left, right };
|
|
2039
|
+
continue;
|
|
2191
2040
|
}
|
|
2192
|
-
_astCache.set(trimmed, ast);
|
|
2193
|
-
}
|
|
2194
2041
|
|
|
2195
|
-
|
|
2196
|
-
} catch (err) {
|
|
2197
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
2198
|
-
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
2042
|
+
break;
|
|
2199
2043
|
}
|
|
2200
|
-
|
|
2044
|
+
|
|
2045
|
+
return left;
|
|
2201
2046
|
}
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
// --- src/diff.js -------------------------------------------------
|
|
2205
|
-
/**
|
|
2206
|
-
* zQuery Diff — Lightweight DOM morphing engine
|
|
2207
|
-
*
|
|
2208
|
-
* Patches an existing DOM tree to match new HTML without destroying nodes
|
|
2209
|
-
* that haven't changed. Preserves focus, scroll positions, third-party
|
|
2210
|
-
* widget state, video playback, and other live DOM state.
|
|
2211
|
-
*
|
|
2212
|
-
* Approach: walk old and new trees in parallel, reconcile node by node.
|
|
2213
|
-
* Keyed elements (via `z-key`) get matched across position changes.
|
|
2214
|
-
*
|
|
2215
|
-
* Performance advantages over virtual DOM (React/Angular):
|
|
2216
|
-
* - No virtual tree allocation or diffing — works directly on real DOM
|
|
2217
|
-
* - Skips unchanged subtrees via fast isEqualNode() check
|
|
2218
|
-
* - z-skip attribute to opt out of diffing entire subtrees
|
|
2219
|
-
* - Reuses a single template element for HTML parsing (zero GC pressure)
|
|
2220
|
-
* - Keyed reconciliation uses LIS (Longest Increasing Subsequence) to
|
|
2221
|
-
* minimize DOM moves — same algorithm as Vue 3 / ivi
|
|
2222
|
-
* - Minimal attribute diffing with early bail-out
|
|
2223
|
-
*/
|
|
2224
2047
|
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
// ---------------------------------------------------------------------------
|
|
2228
|
-
let _tpl = null;
|
|
2048
|
+
parseUnary() {
|
|
2049
|
+
const tok = this.peek();
|
|
2229
2050
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2051
|
+
// typeof
|
|
2052
|
+
if (tok.t === T.IDENT && tok.v === 'typeof') {
|
|
2053
|
+
this.next();
|
|
2054
|
+
const arg = this.parseUnary();
|
|
2055
|
+
return { type: 'typeof', arg };
|
|
2056
|
+
}
|
|
2234
2057
|
|
|
2235
|
-
//
|
|
2236
|
-
|
|
2237
|
-
|
|
2058
|
+
// void
|
|
2059
|
+
if (tok.t === T.IDENT && tok.v === 'void') {
|
|
2060
|
+
this.next();
|
|
2061
|
+
this.parseUnary(); // evaluate but discard
|
|
2062
|
+
return { type: 'literal', value: undefined };
|
|
2063
|
+
}
|
|
2238
2064
|
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
*/
|
|
2246
|
-
function morph(rootEl, newHTML) {
|
|
2247
|
-
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
2248
|
-
const tpl = _getTemplate();
|
|
2249
|
-
tpl.innerHTML = newHTML;
|
|
2250
|
-
const newRoot = tpl.content;
|
|
2065
|
+
// !expr
|
|
2066
|
+
if (tok.t === T.OP && tok.v === '!') {
|
|
2067
|
+
this.next();
|
|
2068
|
+
const arg = this.parseUnary();
|
|
2069
|
+
return { type: 'not', arg };
|
|
2070
|
+
}
|
|
2251
2071
|
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2072
|
+
// -expr, +expr
|
|
2073
|
+
if (tok.t === T.OP && (tok.v === '-' || tok.v === '+')) {
|
|
2074
|
+
this.next();
|
|
2075
|
+
const arg = this.parseUnary();
|
|
2076
|
+
return { type: 'unary', op: tok.v, arg };
|
|
2077
|
+
}
|
|
2256
2078
|
|
|
2257
|
-
|
|
2079
|
+
return this.parsePostfix();
|
|
2080
|
+
}
|
|
2258
2081
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2082
|
+
parsePostfix() {
|
|
2083
|
+
let left = this.parsePrimary();
|
|
2261
2084
|
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
* without replacing the node reference. Useful for replaceWith-style
|
|
2265
|
-
* updates where you want to keep the element identity when the tag
|
|
2266
|
-
* name matches.
|
|
2267
|
-
*
|
|
2268
|
-
* If the new HTML produces a different tag, falls back to native replace.
|
|
2269
|
-
*
|
|
2270
|
-
* @param {Element} oldEl — The live DOM element to patch
|
|
2271
|
-
* @param {string} newHTML — HTML string for the replacement element
|
|
2272
|
-
* @returns {Element} — The resulting element (same ref if morphed, new if replaced)
|
|
2273
|
-
*/
|
|
2274
|
-
function morphElement(oldEl, newHTML) {
|
|
2275
|
-
const start = typeof window !== 'undefined' && window.__zqMorphHook ? performance.now() : 0;
|
|
2276
|
-
const tpl = _getTemplate();
|
|
2277
|
-
tpl.innerHTML = newHTML;
|
|
2278
|
-
const newEl = tpl.content.firstElementChild;
|
|
2279
|
-
if (!newEl) return oldEl;
|
|
2085
|
+
while (true) {
|
|
2086
|
+
const tok = this.peek();
|
|
2280
2087
|
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2088
|
+
// Property access: a.b
|
|
2089
|
+
if (tok.t === T.PUNC && tok.v === '.') {
|
|
2090
|
+
this.next();
|
|
2091
|
+
const prop = this.next();
|
|
2092
|
+
left = { type: 'member', obj: left, prop: prop.v, computed: false };
|
|
2093
|
+
// Check for method call: a.b()
|
|
2094
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2095
|
+
left = this._parseCall(left);
|
|
2096
|
+
}
|
|
2097
|
+
continue;
|
|
2098
|
+
}
|
|
2288
2099
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2100
|
+
// Optional chaining: a?.b, a?.[b], a?.()
|
|
2101
|
+
if (tok.t === T.OP && tok.v === '?.') {
|
|
2102
|
+
this.next();
|
|
2103
|
+
const next = this.peek();
|
|
2104
|
+
if (next.t === T.PUNC && next.v === '[') {
|
|
2105
|
+
// a?.[expr]
|
|
2106
|
+
this.next();
|
|
2107
|
+
const prop = this.parseExpression(0);
|
|
2108
|
+
this.expect(T.PUNC, ']');
|
|
2109
|
+
left = { type: 'optional_member', obj: left, prop, computed: true };
|
|
2110
|
+
} else if (next.t === T.PUNC && next.v === '(') {
|
|
2111
|
+
// a?.()
|
|
2112
|
+
left = { type: 'optional_call', callee: left, args: this._parseArgs() };
|
|
2113
|
+
} else {
|
|
2114
|
+
// a?.b
|
|
2115
|
+
const prop = this.next();
|
|
2116
|
+
left = { type: 'optional_member', obj: left, prop: prop.v, computed: false };
|
|
2117
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2118
|
+
left = this._parseCall(left);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2295
2123
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2124
|
+
// Computed access: a[b]
|
|
2125
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
2126
|
+
this.next();
|
|
2127
|
+
const prop = this.parseExpression(0);
|
|
2128
|
+
this.expect(T.PUNC, ']');
|
|
2129
|
+
left = { type: 'member', obj: left, prop, computed: true };
|
|
2130
|
+
// Check for method call: a[b]()
|
|
2131
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2132
|
+
left = this._parseCall(left);
|
|
2133
|
+
}
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// Function call: fn()
|
|
2138
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
2139
|
+
left = this._parseCall(left);
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2314
2142
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
let oldKeyMap, newKeyMap;
|
|
2143
|
+
break;
|
|
2144
|
+
}
|
|
2318
2145
|
|
|
2319
|
-
|
|
2320
|
-
if (_getKey(oldChildren[i]) != null) { hasKeys = true; break; }
|
|
2146
|
+
return left;
|
|
2321
2147
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2148
|
+
|
|
2149
|
+
_parseCall(callee) {
|
|
2150
|
+
const args = this._parseArgs();
|
|
2151
|
+
return { type: 'call', callee, args };
|
|
2326
2152
|
}
|
|
2327
2153
|
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2154
|
+
_parseArgs() {
|
|
2155
|
+
this.expect(T.PUNC, '(');
|
|
2156
|
+
const args = [];
|
|
2157
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ')') && this.peek().t !== T.EOF) {
|
|
2158
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
2159
|
+
this.next();
|
|
2160
|
+
args.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
2161
|
+
} else {
|
|
2162
|
+
args.push(this.parseExpression(0));
|
|
2163
|
+
}
|
|
2164
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2338
2165
|
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
_morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
|
|
2166
|
+
this.expect(T.PUNC, ')');
|
|
2167
|
+
return args;
|
|
2342
2168
|
}
|
|
2343
|
-
}
|
|
2344
2169
|
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
*/
|
|
2348
|
-
function _morphChildrenUnkeyed(oldParent, oldChildren, newChildren) {
|
|
2349
|
-
const oldLen = oldChildren.length;
|
|
2350
|
-
const newLen = newChildren.length;
|
|
2351
|
-
const minLen = oldLen < newLen ? oldLen : newLen;
|
|
2170
|
+
parsePrimary() {
|
|
2171
|
+
const tok = this.peek();
|
|
2352
2172
|
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2173
|
+
// Number literal
|
|
2174
|
+
if (tok.t === T.NUM) {
|
|
2175
|
+
this.next();
|
|
2176
|
+
return { type: 'literal', value: tok.v };
|
|
2177
|
+
}
|
|
2357
2178
|
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2179
|
+
// String literal
|
|
2180
|
+
if (tok.t === T.STR) {
|
|
2181
|
+
this.next();
|
|
2182
|
+
return { type: 'literal', value: tok.v };
|
|
2362
2183
|
}
|
|
2363
|
-
}
|
|
2364
2184
|
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2185
|
+
// Template literal
|
|
2186
|
+
if (tok.t === T.TMPL) {
|
|
2187
|
+
this.next();
|
|
2188
|
+
return { type: 'template', parts: tok.v };
|
|
2369
2189
|
}
|
|
2370
|
-
}
|
|
2371
|
-
}
|
|
2372
2190
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
const consumed = new Set();
|
|
2381
|
-
const newLen = newChildren.length;
|
|
2382
|
-
const matched = new Array(newLen);
|
|
2191
|
+
// Arrow function with parens: () =>, (a) =>, (a, b) =>
|
|
2192
|
+
// or regular grouping: (expr)
|
|
2193
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
2194
|
+
const savedPos = this.pos;
|
|
2195
|
+
this.next(); // consume (
|
|
2196
|
+
const params = [];
|
|
2197
|
+
let couldBeArrow = true;
|
|
2383
2198
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2199
|
+
if (this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
2200
|
+
// () => ... — no params
|
|
2201
|
+
} else {
|
|
2202
|
+
while (couldBeArrow) {
|
|
2203
|
+
const p = this.peek();
|
|
2204
|
+
if (p.t === T.IDENT && !KEYWORDS.has(p.v)) {
|
|
2205
|
+
params.push(this.next().v);
|
|
2206
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') {
|
|
2207
|
+
this.next();
|
|
2208
|
+
} else {
|
|
2209
|
+
break;
|
|
2210
|
+
}
|
|
2211
|
+
} else {
|
|
2212
|
+
couldBeArrow = false;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2395
2216
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2217
|
+
if (couldBeArrow && this.peek().t === T.PUNC && this.peek().v === ')') {
|
|
2218
|
+
this.next(); // consume )
|
|
2219
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
2220
|
+
this.next(); // consume =>
|
|
2221
|
+
const body = this.parseExpression(0);
|
|
2222
|
+
return { type: 'arrow', params, body };
|
|
2223
|
+
}
|
|
2402
2224
|
}
|
|
2225
|
+
|
|
2226
|
+
// Not an arrow — restore and parse as grouping
|
|
2227
|
+
this.pos = savedPos;
|
|
2228
|
+
this.next(); // consume (
|
|
2229
|
+
const expr = this.parseExpression(0);
|
|
2230
|
+
this.expect(T.PUNC, ')');
|
|
2231
|
+
return expr;
|
|
2403
2232
|
}
|
|
2404
|
-
}
|
|
2405
2233
|
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2234
|
+
// Array literal
|
|
2235
|
+
if (tok.t === T.PUNC && tok.v === '[') {
|
|
2236
|
+
this.next();
|
|
2237
|
+
const elements = [];
|
|
2238
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
2239
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
2240
|
+
this.next();
|
|
2241
|
+
elements.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
2242
|
+
} else {
|
|
2243
|
+
elements.push(this.parseExpression(0));
|
|
2244
|
+
}
|
|
2245
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2246
|
+
}
|
|
2247
|
+
this.expect(T.PUNC, ']');
|
|
2248
|
+
return { type: 'array', elements };
|
|
2416
2249
|
}
|
|
2417
|
-
}
|
|
2418
2250
|
|
|
2419
|
-
|
|
2251
|
+
// Object literal
|
|
2252
|
+
if (tok.t === T.PUNC && tok.v === '{') {
|
|
2253
|
+
this.next();
|
|
2254
|
+
const properties = [];
|
|
2255
|
+
while (!(this.peek().t === T.PUNC && this.peek().v === '}') && this.peek().t !== T.EOF) {
|
|
2256
|
+
// Spread in object: { ...obj }
|
|
2257
|
+
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
2258
|
+
this.next();
|
|
2259
|
+
properties.push({ spread: true, value: this.parseExpression(0) });
|
|
2260
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2420
2263
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2264
|
+
const keyTok = this.next();
|
|
2265
|
+
let key;
|
|
2266
|
+
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
2267
|
+
else if (keyTok.t === T.NUM) key = String(keyTok.v);
|
|
2268
|
+
else throw new Error('Invalid object key: ' + keyTok.v);
|
|
2269
|
+
|
|
2270
|
+
// Shorthand property: { foo } means { foo: foo }
|
|
2271
|
+
if (this.peek().t === T.PUNC && (this.peek().v === ',' || this.peek().v === '}')) {
|
|
2272
|
+
properties.push({ key, value: { type: 'ident', name: key } });
|
|
2273
|
+
} else {
|
|
2274
|
+
this.expect(T.PUNC, ':');
|
|
2275
|
+
properties.push({ key, value: this.parseExpression(0) });
|
|
2276
|
+
}
|
|
2277
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
2278
|
+
}
|
|
2279
|
+
this.expect(T.PUNC, '}');
|
|
2280
|
+
return { type: 'object', properties };
|
|
2428
2281
|
}
|
|
2429
|
-
}
|
|
2430
|
-
let unkeyedIdx = 0;
|
|
2431
2282
|
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
let oldNode = matched[i];
|
|
2283
|
+
// Identifiers & keywords
|
|
2284
|
+
if (tok.t === T.IDENT) {
|
|
2285
|
+
this.next();
|
|
2436
2286
|
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2287
|
+
// Keywords
|
|
2288
|
+
if (tok.v === 'true') return { type: 'literal', value: true };
|
|
2289
|
+
if (tok.v === 'false') return { type: 'literal', value: false };
|
|
2290
|
+
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
2291
|
+
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
2440
2292
|
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
oldParent.insertBefore(clone, cursor);
|
|
2456
|
-
} else {
|
|
2457
|
-
oldParent.appendChild(clone);
|
|
2293
|
+
// new keyword
|
|
2294
|
+
if (tok.v === 'new') {
|
|
2295
|
+
let classExpr = this.parsePrimary();
|
|
2296
|
+
// Handle member access (e.g. ns.MyClass) without consuming call args
|
|
2297
|
+
while (this.peek().t === T.PUNC && this.peek().v === '.') {
|
|
2298
|
+
this.next();
|
|
2299
|
+
const prop = this.next();
|
|
2300
|
+
classExpr = { type: 'member', obj: classExpr, prop: prop.v, computed: false };
|
|
2301
|
+
}
|
|
2302
|
+
let args = [];
|
|
2303
|
+
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
2304
|
+
args = this._parseArgs();
|
|
2305
|
+
}
|
|
2306
|
+
return { type: 'new', callee: classExpr, args };
|
|
2458
2307
|
}
|
|
2459
|
-
}
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
// Remove remaining unkeyed old nodes
|
|
2463
|
-
while (unkeyedIdx < unkeyedOld.length) {
|
|
2464
|
-
const leftover = unkeyedOld[unkeyedIdx++];
|
|
2465
|
-
if (leftover.parentNode === oldParent) {
|
|
2466
|
-
oldParent.removeChild(leftover);
|
|
2467
|
-
}
|
|
2468
|
-
}
|
|
2469
2308
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
oldParent.removeChild(node);
|
|
2309
|
+
// Arrow function: x => expr
|
|
2310
|
+
if (this.peek().t === T.OP && this.peek().v === '=>') {
|
|
2311
|
+
this.next(); // consume =>
|
|
2312
|
+
const body = this.parseExpression(0);
|
|
2313
|
+
return { type: 'arrow', params: [tok.v], body };
|
|
2476
2314
|
}
|
|
2315
|
+
|
|
2316
|
+
return { type: 'ident', name: tok.v };
|
|
2477
2317
|
}
|
|
2318
|
+
|
|
2319
|
+
// Fallback — return undefined for unparseable
|
|
2320
|
+
this.next();
|
|
2321
|
+
return { type: 'literal', value: undefined };
|
|
2478
2322
|
}
|
|
2479
2323
|
}
|
|
2480
2324
|
|
|
2325
|
+
// ---------------------------------------------------------------------------
|
|
2326
|
+
// Evaluator — walks the AST, resolves against scope
|
|
2327
|
+
// ---------------------------------------------------------------------------
|
|
2328
|
+
|
|
2329
|
+
/** Safe property access whitelist for built-in prototypes */
|
|
2330
|
+
const SAFE_ARRAY_METHODS = new Set([
|
|
2331
|
+
'length', 'map', 'filter', 'find', 'findIndex', 'some', 'every',
|
|
2332
|
+
'reduce', 'reduceRight', 'forEach', 'includes', 'indexOf', 'lastIndexOf',
|
|
2333
|
+
'join', 'slice', 'concat', 'flat', 'flatMap', 'reverse', 'sort',
|
|
2334
|
+
'fill', 'keys', 'values', 'entries', 'at', 'toString',
|
|
2335
|
+
]);
|
|
2336
|
+
|
|
2337
|
+
const SAFE_STRING_METHODS = new Set([
|
|
2338
|
+
'length', 'charAt', 'charCodeAt', 'includes', 'indexOf', 'lastIndexOf',
|
|
2339
|
+
'slice', 'substring', 'trim', 'trimStart', 'trimEnd', 'toLowerCase',
|
|
2340
|
+
'toUpperCase', 'split', 'replace', 'replaceAll', 'match', 'search',
|
|
2341
|
+
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'at',
|
|
2342
|
+
'toString', 'valueOf',
|
|
2343
|
+
]);
|
|
2344
|
+
|
|
2345
|
+
const SAFE_NUMBER_METHODS = new Set([
|
|
2346
|
+
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
2347
|
+
]);
|
|
2348
|
+
|
|
2349
|
+
const SAFE_OBJECT_METHODS = new Set([
|
|
2350
|
+
'hasOwnProperty', 'toString', 'valueOf',
|
|
2351
|
+
]);
|
|
2352
|
+
|
|
2353
|
+
const SAFE_MATH_PROPS = new Set([
|
|
2354
|
+
'PI', 'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'SQRT2', 'SQRT1_2',
|
|
2355
|
+
'abs', 'ceil', 'floor', 'round', 'trunc', 'max', 'min', 'pow',
|
|
2356
|
+
'sqrt', 'sign', 'random', 'log', 'log2', 'log10',
|
|
2357
|
+
]);
|
|
2358
|
+
|
|
2359
|
+
const SAFE_JSON_PROPS = new Set(['parse', 'stringify']);
|
|
2360
|
+
|
|
2481
2361
|
/**
|
|
2482
|
-
*
|
|
2483
|
-
* Returns a Set of positions (in the input) that form the LIS.
|
|
2484
|
-
* Entries with value -1 (unmatched) are excluded.
|
|
2485
|
-
*
|
|
2486
|
-
* O(n log n) — same algorithm used by Vue 3 and ivi.
|
|
2487
|
-
*
|
|
2488
|
-
* @param {number[]} arr — array of old-tree indices (-1 = unmatched)
|
|
2489
|
-
* @returns {Set<number>} — positions in arr belonging to the LIS
|
|
2362
|
+
* Check if property access is safe
|
|
2490
2363
|
*/
|
|
2491
|
-
function
|
|
2492
|
-
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2364
|
+
function _isSafeAccess(obj, prop) {
|
|
2365
|
+
// Never allow access to dangerous properties
|
|
2366
|
+
const BLOCKED = new Set([
|
|
2367
|
+
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
2368
|
+
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
2369
|
+
'call', 'apply', 'bind',
|
|
2370
|
+
]);
|
|
2371
|
+
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
2495
2372
|
|
|
2496
|
-
//
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2373
|
+
// Always allow plain object/function property access and array index access
|
|
2374
|
+
if (obj !== null && obj !== undefined && (typeof obj === 'object' || typeof obj === 'function')) return true;
|
|
2375
|
+
if (typeof obj === 'string') return SAFE_STRING_METHODS.has(prop);
|
|
2376
|
+
if (typeof obj === 'number') return SAFE_NUMBER_METHODS.has(prop);
|
|
2377
|
+
return false;
|
|
2378
|
+
}
|
|
2501
2379
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const val = arr[i];
|
|
2380
|
+
function evaluate(node, scope) {
|
|
2381
|
+
if (!node) return undefined;
|
|
2505
2382
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2383
|
+
switch (node.type) {
|
|
2384
|
+
case 'literal':
|
|
2385
|
+
return node.value;
|
|
2386
|
+
|
|
2387
|
+
case 'ident': {
|
|
2388
|
+
const name = node.name;
|
|
2389
|
+
// Check scope layers in order
|
|
2390
|
+
for (const layer of scope) {
|
|
2391
|
+
if (layer && typeof layer === 'object' && name in layer) {
|
|
2392
|
+
return layer[name];
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
// Built-in globals (safe ones only)
|
|
2396
|
+
if (name === 'Math') return Math;
|
|
2397
|
+
if (name === 'JSON') return JSON;
|
|
2398
|
+
if (name === 'Date') return Date;
|
|
2399
|
+
if (name === 'Array') return Array;
|
|
2400
|
+
if (name === 'Object') return Object;
|
|
2401
|
+
if (name === 'String') return String;
|
|
2402
|
+
if (name === 'Number') return Number;
|
|
2403
|
+
if (name === 'Boolean') return Boolean;
|
|
2404
|
+
if (name === 'parseInt') return parseInt;
|
|
2405
|
+
if (name === 'parseFloat') return parseFloat;
|
|
2406
|
+
if (name === 'isNaN') return isNaN;
|
|
2407
|
+
if (name === 'isFinite') return isFinite;
|
|
2408
|
+
if (name === 'Infinity') return Infinity;
|
|
2409
|
+
if (name === 'NaN') return NaN;
|
|
2410
|
+
if (name === 'encodeURIComponent') return encodeURIComponent;
|
|
2411
|
+
if (name === 'decodeURIComponent') return decodeURIComponent;
|
|
2412
|
+
if (name === 'console') return console;
|
|
2413
|
+
if (name === 'Map') return Map;
|
|
2414
|
+
if (name === 'Set') return Set;
|
|
2415
|
+
if (name === 'RegExp') return RegExp;
|
|
2416
|
+
if (name === 'Error') return Error;
|
|
2417
|
+
if (name === 'URL') return URL;
|
|
2418
|
+
if (name === 'URLSearchParams') return URLSearchParams;
|
|
2419
|
+
return undefined;
|
|
2512
2420
|
}
|
|
2513
2421
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2422
|
+
case 'template': {
|
|
2423
|
+
// Template literal with interpolation
|
|
2424
|
+
let result = '';
|
|
2425
|
+
for (const part of node.parts) {
|
|
2426
|
+
if (typeof part === 'string') {
|
|
2427
|
+
result += part;
|
|
2428
|
+
} else if (part && part.expr) {
|
|
2429
|
+
result += String(safeEval(part.expr, scope) ?? '');
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
return result;
|
|
2433
|
+
}
|
|
2518
2434
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2435
|
+
case 'member': {
|
|
2436
|
+
const obj = evaluate(node.obj, scope);
|
|
2437
|
+
if (obj == null) return undefined;
|
|
2438
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
2439
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2440
|
+
return obj[prop];
|
|
2441
|
+
}
|
|
2525
2442
|
|
|
2526
|
-
|
|
2527
|
-
|
|
2443
|
+
case 'optional_member': {
|
|
2444
|
+
const obj = evaluate(node.obj, scope);
|
|
2445
|
+
if (obj == null) return undefined;
|
|
2446
|
+
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
2447
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2448
|
+
return obj[prop];
|
|
2449
|
+
}
|
|
2528
2450
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2451
|
+
case 'call': {
|
|
2452
|
+
const result = _resolveCall(node, scope, false);
|
|
2453
|
+
return result;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
case 'optional_call': {
|
|
2457
|
+
const calleeNode = node.callee;
|
|
2458
|
+
const args = _evalArgs(node.args, scope);
|
|
2459
|
+
// Method call: obj?.method() — bind `this` to obj
|
|
2460
|
+
if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
|
|
2461
|
+
const obj = evaluate(calleeNode.obj, scope);
|
|
2462
|
+
if (obj == null) return undefined;
|
|
2463
|
+
const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
|
|
2464
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2465
|
+
const fn = obj[prop];
|
|
2466
|
+
if (typeof fn !== 'function') return undefined;
|
|
2467
|
+
return fn.apply(obj, args);
|
|
2538
2468
|
}
|
|
2539
|
-
|
|
2469
|
+
const callee = evaluate(calleeNode, scope);
|
|
2470
|
+
if (callee == null) return undefined;
|
|
2471
|
+
if (typeof callee !== 'function') return undefined;
|
|
2472
|
+
return callee(...args);
|
|
2540
2473
|
}
|
|
2541
|
-
// Different node types — replace
|
|
2542
|
-
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
2474
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2475
|
+
case 'new': {
|
|
2476
|
+
const Ctor = evaluate(node.callee, scope);
|
|
2477
|
+
if (typeof Ctor !== 'function') return undefined;
|
|
2478
|
+
// Only allow safe constructors
|
|
2479
|
+
if (Ctor === Date || Ctor === Array || Ctor === Map || Ctor === Set ||
|
|
2480
|
+
Ctor === RegExp || Ctor === Error || Ctor === URL || Ctor === URLSearchParams) {
|
|
2481
|
+
const args = _evalArgs(node.args, scope);
|
|
2482
|
+
return new Ctor(...args);
|
|
2483
|
+
}
|
|
2484
|
+
return undefined;
|
|
2485
|
+
}
|
|
2552
2486
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
// z-skip: developer opt-out — skip diffing this subtree entirely.
|
|
2556
|
-
// Useful for third-party widgets, canvas, video, or large static content.
|
|
2557
|
-
if (oldNode.hasAttribute('z-skip')) return;
|
|
2487
|
+
case 'binary':
|
|
2488
|
+
return _evalBinary(node, scope);
|
|
2558
2489
|
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2490
|
+
case 'unary': {
|
|
2491
|
+
const val = evaluate(node.arg, scope);
|
|
2492
|
+
return node.op === '-' ? -val : +val;
|
|
2493
|
+
}
|
|
2563
2494
|
|
|
2564
|
-
|
|
2495
|
+
case 'not':
|
|
2496
|
+
return !evaluate(node.arg, scope);
|
|
2497
|
+
|
|
2498
|
+
case 'typeof': {
|
|
2499
|
+
try {
|
|
2500
|
+
return typeof evaluate(node.arg, scope);
|
|
2501
|
+
} catch {
|
|
2502
|
+
return 'undefined';
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2565
2505
|
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
_syncInputValue(oldNode, newNode);
|
|
2570
|
-
return;
|
|
2506
|
+
case 'ternary': {
|
|
2507
|
+
const cond = evaluate(node.cond, scope);
|
|
2508
|
+
return cond ? evaluate(node.truthy, scope) : evaluate(node.falsy, scope);
|
|
2571
2509
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2510
|
+
|
|
2511
|
+
case 'array': {
|
|
2512
|
+
const arr = [];
|
|
2513
|
+
for (const e of node.elements) {
|
|
2514
|
+
if (e.type === 'spread') {
|
|
2515
|
+
const iterable = evaluate(e.arg, scope);
|
|
2516
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2517
|
+
for (const v of iterable) arr.push(v);
|
|
2518
|
+
}
|
|
2519
|
+
} else {
|
|
2520
|
+
arr.push(evaluate(e, scope));
|
|
2521
|
+
}
|
|
2575
2522
|
}
|
|
2576
|
-
return;
|
|
2523
|
+
return arr;
|
|
2577
2524
|
}
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2525
|
+
|
|
2526
|
+
case 'object': {
|
|
2527
|
+
const obj = {};
|
|
2528
|
+
for (const prop of node.properties) {
|
|
2529
|
+
if (prop.spread) {
|
|
2530
|
+
const source = evaluate(prop.value, scope);
|
|
2531
|
+
if (source != null && typeof source === 'object') {
|
|
2532
|
+
Object.assign(obj, source);
|
|
2533
|
+
}
|
|
2534
|
+
} else {
|
|
2535
|
+
obj[prop.key] = evaluate(prop.value, scope);
|
|
2536
|
+
}
|
|
2582
2537
|
}
|
|
2583
|
-
return;
|
|
2538
|
+
return obj;
|
|
2584
2539
|
}
|
|
2585
2540
|
|
|
2586
|
-
|
|
2587
|
-
|
|
2541
|
+
case 'arrow': {
|
|
2542
|
+
const paramNames = node.params;
|
|
2543
|
+
const bodyNode = node.body;
|
|
2544
|
+
const closedScope = scope;
|
|
2545
|
+
return function(...args) {
|
|
2546
|
+
const arrowScope = {};
|
|
2547
|
+
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
2548
|
+
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
default:
|
|
2553
|
+
return undefined;
|
|
2588
2554
|
}
|
|
2589
2555
|
}
|
|
2590
2556
|
|
|
2591
2557
|
/**
|
|
2592
|
-
*
|
|
2593
|
-
* Uses a single pass: build a set of new attribute names, iterate
|
|
2594
|
-
* old attrs for removals, then apply new attrs.
|
|
2558
|
+
* Evaluate a list of argument AST nodes, flattening any spread elements.
|
|
2595
2559
|
*/
|
|
2596
|
-
function
|
|
2597
|
-
const
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
if (newLen === oldLen) {
|
|
2604
|
-
let same = true;
|
|
2605
|
-
for (let i = 0; i < newLen; i++) {
|
|
2606
|
-
const na = newAttrs[i];
|
|
2607
|
-
if (oldEl.getAttribute(na.name) !== na.value) { same = false; break; }
|
|
2608
|
-
}
|
|
2609
|
-
if (same) {
|
|
2610
|
-
// Also verify no extra old attrs (names mismatch)
|
|
2611
|
-
for (let i = 0; i < oldLen; i++) {
|
|
2612
|
-
if (!newEl.hasAttribute(oldAttrs[i].name)) { same = false; break; }
|
|
2560
|
+
function _evalArgs(argNodes, scope) {
|
|
2561
|
+
const result = [];
|
|
2562
|
+
for (const a of argNodes) {
|
|
2563
|
+
if (a.type === 'spread') {
|
|
2564
|
+
const iterable = evaluate(a.arg, scope);
|
|
2565
|
+
if (iterable != null && typeof iterable[Symbol.iterator] === 'function') {
|
|
2566
|
+
for (const v of iterable) result.push(v);
|
|
2613
2567
|
}
|
|
2568
|
+
} else {
|
|
2569
|
+
result.push(evaluate(a, scope));
|
|
2614
2570
|
}
|
|
2615
|
-
if (same) return;
|
|
2616
2571
|
}
|
|
2572
|
+
return result;
|
|
2573
|
+
}
|
|
2617
2574
|
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
oldEl.setAttribute(attr.name, attr.value);
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2575
|
+
/**
|
|
2576
|
+
* Resolve and execute a function call safely.
|
|
2577
|
+
*/
|
|
2578
|
+
function _resolveCall(node, scope) {
|
|
2579
|
+
const callee = node.callee;
|
|
2580
|
+
const args = _evalArgs(node.args, scope);
|
|
2627
2581
|
|
|
2628
|
-
//
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (!
|
|
2634
|
-
|
|
2635
|
-
|
|
2582
|
+
// Method call: obj.method() — bind `this` to obj
|
|
2583
|
+
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
2584
|
+
const obj = evaluate(callee.obj, scope);
|
|
2585
|
+
if (obj == null) return undefined;
|
|
2586
|
+
const prop = callee.computed ? evaluate(callee.prop, scope) : callee.prop;
|
|
2587
|
+
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
2588
|
+
const fn = obj[prop];
|
|
2589
|
+
if (typeof fn !== 'function') return undefined;
|
|
2590
|
+
return fn.apply(obj, args);
|
|
2636
2591
|
}
|
|
2592
|
+
|
|
2593
|
+
// Direct call: fn(args)
|
|
2594
|
+
const fn = evaluate(callee, scope);
|
|
2595
|
+
if (typeof fn !== 'function') return undefined;
|
|
2596
|
+
return fn(...args);
|
|
2637
2597
|
}
|
|
2638
2598
|
|
|
2639
2599
|
/**
|
|
2640
|
-
*
|
|
2600
|
+
* Evaluate binary expression.
|
|
2641
2601
|
*/
|
|
2642
|
-
function
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2602
|
+
function _evalBinary(node, scope) {
|
|
2603
|
+
// Short-circuit for logical ops
|
|
2604
|
+
if (node.op === '&&') {
|
|
2605
|
+
const left = evaluate(node.left, scope);
|
|
2606
|
+
return left ? evaluate(node.right, scope) : left;
|
|
2607
|
+
}
|
|
2608
|
+
if (node.op === '||') {
|
|
2609
|
+
const left = evaluate(node.left, scope);
|
|
2610
|
+
return left ? left : evaluate(node.right, scope);
|
|
2611
|
+
}
|
|
2612
|
+
if (node.op === '??') {
|
|
2613
|
+
const left = evaluate(node.left, scope);
|
|
2614
|
+
return left != null ? left : evaluate(node.right, scope);
|
|
2651
2615
|
}
|
|
2652
2616
|
|
|
2653
|
-
|
|
2654
|
-
|
|
2617
|
+
const left = evaluate(node.left, scope);
|
|
2618
|
+
const right = evaluate(node.right, scope);
|
|
2619
|
+
|
|
2620
|
+
switch (node.op) {
|
|
2621
|
+
case '+': return left + right;
|
|
2622
|
+
case '-': return left - right;
|
|
2623
|
+
case '*': return left * right;
|
|
2624
|
+
case '/': return left / right;
|
|
2625
|
+
case '%': return left % right;
|
|
2626
|
+
case '==': return left == right;
|
|
2627
|
+
case '!=': return left != right;
|
|
2628
|
+
case '===': return left === right;
|
|
2629
|
+
case '!==': return left !== right;
|
|
2630
|
+
case '<': return left < right;
|
|
2631
|
+
case '>': return left > right;
|
|
2632
|
+
case '<=': return left <= right;
|
|
2633
|
+
case '>=': return left >= right;
|
|
2634
|
+
case 'instanceof': return left instanceof right;
|
|
2635
|
+
case 'in': return left in right;
|
|
2636
|
+
default: return undefined;
|
|
2637
|
+
}
|
|
2655
2638
|
}
|
|
2656
2639
|
|
|
2640
|
+
|
|
2641
|
+
// ---------------------------------------------------------------------------
|
|
2642
|
+
// Public API
|
|
2643
|
+
// ---------------------------------------------------------------------------
|
|
2644
|
+
|
|
2657
2645
|
/**
|
|
2658
|
-
*
|
|
2659
|
-
*
|
|
2660
|
-
* Priority: z-key attribute → id attribute → data-id / data-key.
|
|
2661
|
-
* Auto-detected keys use a `\0` prefix to avoid collisions with
|
|
2662
|
-
* explicit z-key values.
|
|
2663
|
-
*
|
|
2664
|
-
* This means the LIS-optimised keyed path activates automatically
|
|
2665
|
-
* whenever elements carry `id` or `data-id` / `data-key` attributes
|
|
2666
|
-
* — no extra markup required.
|
|
2646
|
+
* Safely evaluate a JS expression string against scope layers.
|
|
2667
2647
|
*
|
|
2668
|
-
* @
|
|
2648
|
+
* @param {string} expr — expression string
|
|
2649
|
+
* @param {object[]} scope — array of scope objects, checked in order
|
|
2650
|
+
* Typical: [loopVars, state, { props, refs, $ }]
|
|
2651
|
+
* @returns {*} — evaluation result, or undefined on error
|
|
2669
2652
|
*/
|
|
2670
|
-
function _getKey(node) {
|
|
2671
|
-
if (node.nodeType !== 1) return null;
|
|
2672
2653
|
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2654
|
+
// AST cache (LRU) — avoids re-tokenizing and re-parsing the same expression.
|
|
2655
|
+
// Uses Map insertion-order: on hit, delete + re-set moves entry to the end.
|
|
2656
|
+
// Eviction removes the least-recently-used (first) entry when at capacity.
|
|
2657
|
+
const _astCache = new Map();
|
|
2658
|
+
const _AST_CACHE_MAX = 512;
|
|
2676
2659
|
|
|
2677
|
-
|
|
2678
|
-
|
|
2660
|
+
function safeEval(expr, scope) {
|
|
2661
|
+
try {
|
|
2662
|
+
const trimmed = expr.trim();
|
|
2663
|
+
if (!trimmed) return undefined;
|
|
2679
2664
|
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2665
|
+
// Fast path for simple identifiers: "count", "name", "visible"
|
|
2666
|
+
// Avoids full tokenize→parse→evaluate overhead for the most common case.
|
|
2667
|
+
if (/^[a-zA-Z_$][\w$]*$/.test(trimmed)) {
|
|
2668
|
+
for (const layer of scope) {
|
|
2669
|
+
if (layer && typeof layer === 'object' && trimmed in layer) {
|
|
2670
|
+
return layer[trimmed];
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2674
|
+
}
|
|
2686
2675
|
|
|
2687
|
-
|
|
2676
|
+
// Check AST cache (LRU: move to end on hit)
|
|
2677
|
+
let ast = _astCache.get(trimmed);
|
|
2678
|
+
if (ast) {
|
|
2679
|
+
_astCache.delete(trimmed);
|
|
2680
|
+
_astCache.set(trimmed, ast);
|
|
2681
|
+
} else {
|
|
2682
|
+
const tokens = tokenize(trimmed);
|
|
2683
|
+
const parser = new Parser(tokens, scope);
|
|
2684
|
+
ast = parser.parse();
|
|
2685
|
+
|
|
2686
|
+
// Evict oldest entries when cache is full
|
|
2687
|
+
if (_astCache.size >= _AST_CACHE_MAX) {
|
|
2688
|
+
const first = _astCache.keys().next().value;
|
|
2689
|
+
_astCache.delete(first);
|
|
2690
|
+
}
|
|
2691
|
+
_astCache.set(trimmed, ast);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
return evaluate(ast, scope);
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
if (typeof console !== 'undefined' && console.debug) {
|
|
2697
|
+
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
2698
|
+
}
|
|
2699
|
+
return undefined;
|
|
2700
|
+
}
|
|
2688
2701
|
}
|
|
2689
2702
|
|
|
2690
2703
|
// --- src/component.js --------------------------------------------
|
|
@@ -3191,11 +3204,14 @@ class Component {
|
|
|
3191
3204
|
this._bindRefs();
|
|
3192
3205
|
this._bindModels();
|
|
3193
3206
|
|
|
3194
|
-
// Restore focus if the morph replaced the focused element
|
|
3207
|
+
// Restore focus if the morph replaced the focused element.
|
|
3208
|
+
// Always restore selectionRange — even when the element is still
|
|
3209
|
+
// the activeElement — because _bindModels or morph attribute syncing
|
|
3210
|
+
// can alter the value and move the cursor.
|
|
3195
3211
|
if (_focusInfo) {
|
|
3196
3212
|
const el = this._el.querySelector(_focusInfo.selector);
|
|
3197
|
-
if (el
|
|
3198
|
-
el.focus();
|
|
3213
|
+
if (el) {
|
|
3214
|
+
if (el !== document.activeElement) el.focus();
|
|
3199
3215
|
try {
|
|
3200
3216
|
if (_focusInfo.start !== null && _focusInfo.start !== undefined) {
|
|
3201
3217
|
el.setSelectionRange(_focusInfo.start, _focusInfo.end, _focusInfo.dir);
|
|
@@ -5836,9 +5852,10 @@ $.guardCallback = guardCallback;
|
|
|
5836
5852
|
$.validate = validate;
|
|
5837
5853
|
|
|
5838
5854
|
// --- Meta ------------------------------------------------------------------
|
|
5839
|
-
$.version
|
|
5840
|
-
$.libSize
|
|
5841
|
-
$.
|
|
5855
|
+
$.version = '0.9.8';
|
|
5856
|
+
$.libSize = '~101 KB';
|
|
5857
|
+
$.unitTests = {"passed":1733,"failed":0,"total":1733,"suites":489,"duration":2847,"ok":true};
|
|
5858
|
+
$.meta = {}; // populated at build time by CLI bundler
|
|
5842
5859
|
|
|
5843
5860
|
$.noConflict = () => {
|
|
5844
5861
|
if (typeof window !== 'undefined' && window.$ === $) {
|