zero-query 0.9.6 → 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 +36 -8
- 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 +1949 -1894
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +10 -1
- package/index.js +5 -3
- package/package.json +1 -1
- package/src/component.js +6 -3
- package/src/diff.js +15 -2
- package/src/http.js +37 -0
- package/tests/cli.test.js +304 -0
- package/tests/http.test.js +200 -0
- package/types/http.d.ts +15 -4
- 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,972 +353,1471 @@ 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
|
-
|
|
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
|
+
}
|
|
1348
|
+
|
|
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
|
+
}
|
|
1354
|
+
|
|
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
|
+
}
|
|
1360
|
+
|
|
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
|
+
}
|
|
1372
|
+
|
|
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
|
+
}
|
|
1382
|
+
|
|
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;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
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
|
+
}
|
|
1399
|
+
|
|
1400
|
+
detach() {
|
|
1401
|
+
return this.each((_, el) => el.remove());
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// --- Visibility ----------------------------------------------------------
|
|
1405
|
+
|
|
1406
|
+
show(display = '') {
|
|
1407
|
+
return this.each((_, el) => { el.style.display = display; });
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
hide() {
|
|
1411
|
+
return this.each((_, el) => { el.style.display = 'none'; });
|
|
1412
|
+
}
|
|
1413
|
+
|
|
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
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// --- Events --------------------------------------------------------------
|
|
1423
|
+
|
|
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
|
+
}
|
|
1448
|
+
|
|
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
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
one(event, handler) {
|
|
1470
|
+
return this.each((_, el) => {
|
|
1471
|
+
el.addEventListener(event, handler, { once: true });
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
trigger(event, detail) {
|
|
1476
|
+
return this.each((_, el) => {
|
|
1477
|
+
el.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, cancelable: true }));
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
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
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// --- Animation -----------------------------------------------------------
|
|
1492
|
+
|
|
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 = '';
|
|
1524
|
+
}
|
|
1525
|
+
resolve(this);
|
|
1526
|
+
}
|
|
1527
|
+
}, duration + 50);
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
fadeIn(duration = 300) {
|
|
1532
|
+
return this.css({ opacity: '0', display: '' }).animate({ opacity: '1' }, duration);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
fadeOut(duration = 300) {
|
|
1536
|
+
return this.animate({ opacity: '0' }, duration).then(col => col.hide());
|
|
1537
|
+
}
|
|
1538
|
+
|
|
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);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
fadeTo(duration, opacity) {
|
|
1549
|
+
return this.animate({ opacity: String(opacity) }, duration);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
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
|
+
}
|
|
1563
|
+
|
|
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
|
+
}
|
|
1573
|
+
|
|
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);
|
|
1584
|
+
} else {
|
|
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);
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// --- Form helpers --------------------------------------------------------
|
|
1595
|
+
|
|
1596
|
+
serialize() {
|
|
1597
|
+
const form = this.first();
|
|
1598
|
+
if (!form || form.tagName !== 'FORM') return '';
|
|
1599
|
+
return new URLSearchParams(new FormData(form)).toString();
|
|
1600
|
+
}
|
|
1601
|
+
|
|
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;
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
return obj;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
|
|
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
|
+
}
|
|
1627
|
+
|
|
1628
|
+
|
|
1629
|
+
// ---------------------------------------------------------------------------
|
|
1630
|
+
// $() — main selector / creator (returns ZQueryCollection, like jQuery)
|
|
1631
|
+
// ---------------------------------------------------------------------------
|
|
1632
|
+
function query(selector, context) {
|
|
1633
|
+
// null / undefined
|
|
1634
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1635
|
+
|
|
1636
|
+
// Already a collection — return as-is
|
|
1637
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1638
|
+
|
|
1639
|
+
// DOM element or Window — wrap in collection
|
|
1640
|
+
if (selector instanceof Node || selector === window) {
|
|
1641
|
+
return new ZQueryCollection([selector]);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
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
|
+
}
|
|
1648
|
+
|
|
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
|
+
}
|
|
1654
|
+
|
|
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)]);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
return new ZQueryCollection([]);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
|
|
1667
|
+
// ---------------------------------------------------------------------------
|
|
1668
|
+
// $.all() — collection selector (returns ZQueryCollection for CSS selectors)
|
|
1669
|
+
// ---------------------------------------------------------------------------
|
|
1670
|
+
function queryAll(selector, context) {
|
|
1671
|
+
// null / undefined
|
|
1672
|
+
if (!selector) return new ZQueryCollection([]);
|
|
1673
|
+
|
|
1674
|
+
// Already a collection
|
|
1675
|
+
if (selector instanceof ZQueryCollection) return selector;
|
|
1676
|
+
|
|
1677
|
+
// DOM element or Window
|
|
1678
|
+
if (selector instanceof Node || selector === window) {
|
|
1679
|
+
return new ZQueryCollection([selector]);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// NodeList / HTMLCollection / Array
|
|
1683
|
+
if (selector instanceof NodeList || selector instanceof HTMLCollection || Array.isArray(selector)) {
|
|
1684
|
+
return new ZQueryCollection(Array.from(selector));
|
|
1685
|
+
}
|
|
1686
|
+
|
|
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
|
+
}
|
|
1692
|
+
|
|
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
|
+
}
|
|
1700
|
+
|
|
1701
|
+
return new ZQueryCollection([]);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
|
|
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));
|
|
1722
|
+
|
|
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
|
+
};
|
|
1739
|
+
|
|
1740
|
+
// DOM ready
|
|
1741
|
+
query.ready = (fn) => {
|
|
1742
|
+
if (document.readyState !== 'loading') fn();
|
|
1743
|
+
else document.addEventListener('DOMContentLoaded', fn);
|
|
1744
|
+
};
|
|
1745
|
+
|
|
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
|
+
};
|
|
1768
|
+
|
|
1769
|
+
// Remove a direct global listener
|
|
1770
|
+
query.off = (event, handler) => {
|
|
1771
|
+
document.removeEventListener(event, handler);
|
|
1772
|
+
};
|
|
1773
|
+
|
|
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
|
+
*/
|
|
1802
|
+
|
|
1803
|
+
// Token types
|
|
1804
|
+
const T = {
|
|
1805
|
+
NUM: 1, STR: 2, IDENT: 3, OP: 4, PUNC: 5, TMPL: 6, EOF: 7
|
|
1806
|
+
};
|
|
1807
|
+
|
|
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
|
+
};
|
|
1818
|
+
|
|
1819
|
+
const KEYWORDS = new Set([
|
|
1820
|
+
'true', 'false', 'null', 'undefined', 'typeof', 'instanceof', 'in',
|
|
1322
1821
|
'new', 'void'
|
|
1323
1822
|
]);
|
|
1324
1823
|
|
|
@@ -1407,1284 +1906,798 @@ function tokenize(expr) {
|
|
|
1407
1906
|
parts.push(str);
|
|
1408
1907
|
tokens.push({ t: T.TMPL, v: parts });
|
|
1409
1908
|
continue;
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// Identifiers & keywords
|
|
1413
|
-
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '$') {
|
|
1414
|
-
let ident = '';
|
|
1415
|
-
while (i < len && /[\w$]/.test(expr[i])) ident += expr[i++];
|
|
1416
|
-
tokens.push({ t: T.IDENT, v: ident });
|
|
1417
|
-
continue;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
// Multi-char operators
|
|
1421
|
-
const two = expr.slice(i, i + 3);
|
|
1422
|
-
if (two === '===' || two === '!==' || two === '?.') {
|
|
1423
|
-
if (two === '?.') {
|
|
1424
|
-
tokens.push({ t: T.OP, v: '?.' });
|
|
1425
|
-
i += 2;
|
|
1426
|
-
} else {
|
|
1427
|
-
tokens.push({ t: T.OP, v: two });
|
|
1428
|
-
i += 3;
|
|
1429
|
-
}
|
|
1430
|
-
continue;
|
|
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
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// Single char operators and punctuation
|
|
1442
|
-
if ('+-*/%'.includes(ch)) {
|
|
1443
|
-
tokens.push({ t: T.OP, v: ch });
|
|
1444
|
-
i++; continue;
|
|
1445
|
-
}
|
|
1446
|
-
if ('<>=!'.includes(ch)) {
|
|
1447
|
-
tokens.push({ t: T.OP, v: ch });
|
|
1448
|
-
i++; continue;
|
|
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
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Unknown — skip
|
|
1461
|
-
i++;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
tokens.push({ t: T.EOF, v: null });
|
|
1465
|
-
return tokens;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// ---------------------------------------------------------------------------
|
|
1469
|
-
// Parser — Pratt (precedence climbing)
|
|
1470
|
-
// ---------------------------------------------------------------------------
|
|
1471
|
-
class Parser {
|
|
1472
|
-
constructor(tokens, scope) {
|
|
1473
|
-
this.tokens = tokens;
|
|
1474
|
-
this.pos = 0;
|
|
1475
|
-
this.scope = scope;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
peek() { return this.tokens[this.pos]; }
|
|
1479
|
-
next() { return this.tokens[this.pos++]; }
|
|
1480
|
-
|
|
1481
|
-
expect(type, val) {
|
|
1482
|
-
const t = this.next();
|
|
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;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
match(type, val) {
|
|
1490
|
-
const t = this.peek();
|
|
1491
|
-
if (t.t === type && (val === undefined || t.v === val)) {
|
|
1492
|
-
return this.next();
|
|
1493
|
-
}
|
|
1494
|
-
return null;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// Main entry
|
|
1498
|
-
parse() {
|
|
1499
|
-
const result = this.parseExpression(0);
|
|
1500
|
-
return result;
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// Precedence climbing
|
|
1504
|
-
parseExpression(minPrec) {
|
|
1505
|
-
let left = this.parseUnary();
|
|
1506
|
-
|
|
1507
|
-
while (true) {
|
|
1508
|
-
const tok = this.peek();
|
|
1509
|
-
|
|
1510
|
-
// Ternary
|
|
1511
|
-
if (tok.t === T.PUNC && tok.v === '?') {
|
|
1512
|
-
// Distinguish ternary ? from optional chaining ?.
|
|
1513
|
-
if (this.tokens[this.pos + 1]?.v !== '.') {
|
|
1514
|
-
if (1 <= minPrec) break; // ternary has very low precedence
|
|
1515
|
-
this.next(); // consume ?
|
|
1516
|
-
const truthy = this.parseExpression(0);
|
|
1517
|
-
this.expect(T.PUNC, ':');
|
|
1518
|
-
const falsy = this.parseExpression(0);
|
|
1519
|
-
left = { type: 'ternary', cond: left, truthy, falsy };
|
|
1520
|
-
continue;
|
|
1521
|
-
}
|
|
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;
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
parseUnary() {
|
|
1550
|
-
const tok = this.peek();
|
|
1551
|
-
|
|
1552
|
-
// typeof
|
|
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();
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
parsePostfix() {
|
|
1584
|
-
let left = this.parsePrimary();
|
|
1585
|
-
|
|
1586
|
-
while (true) {
|
|
1587
|
-
const tok = this.peek();
|
|
1588
|
-
|
|
1589
|
-
// Property access: a.b
|
|
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
|
-
}
|
|
1600
|
-
|
|
1601
|
-
// Optional chaining: a?.b, a?.[b], a?.()
|
|
1602
|
-
if (tok.t === T.OP && tok.v === '?.') {
|
|
1603
|
-
this.next();
|
|
1604
|
-
const next = this.peek();
|
|
1605
|
-
if (next.t === T.PUNC && next.v === '[') {
|
|
1606
|
-
// a?.[expr]
|
|
1607
|
-
this.next();
|
|
1608
|
-
const prop = this.parseExpression(0);
|
|
1609
|
-
this.expect(T.PUNC, ']');
|
|
1610
|
-
left = { type: 'optional_member', obj: left, prop, computed: true };
|
|
1611
|
-
} else if (next.t === T.PUNC && next.v === '(') {
|
|
1612
|
-
// a?.()
|
|
1613
|
-
left = { type: 'optional_call', callee: left, args: this._parseArgs() };
|
|
1614
|
-
} else {
|
|
1615
|
-
// a?.b
|
|
1616
|
-
const prop = this.next();
|
|
1617
|
-
left = { type: 'optional_member', obj: left, prop: prop.v, computed: false };
|
|
1618
|
-
if (this.peek().t === T.PUNC && this.peek().v === '(') {
|
|
1619
|
-
left = this._parseCall(left);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
continue;
|
|
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
|
-
}
|
|
1643
|
-
|
|
1644
|
-
break;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
return left;
|
|
1648
|
-
}
|
|
1909
|
+
}
|
|
1649
1910
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
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;
|
|
1917
|
+
}
|
|
1654
1918
|
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
args.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
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;
|
|
1662
1925
|
} else {
|
|
1663
|
-
|
|
1926
|
+
tokens.push({ t: T.OP, v: two });
|
|
1927
|
+
i += 3;
|
|
1664
1928
|
}
|
|
1665
|
-
|
|
1929
|
+
continue;
|
|
1666
1930
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
// Number literal
|
|
1675
|
-
if (tok.t === T.NUM) {
|
|
1676
|
-
this.next();
|
|
1677
|
-
return { type: 'literal', value: tok.v };
|
|
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;
|
|
1678
1938
|
}
|
|
1679
1939
|
|
|
1680
|
-
//
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1940
|
+
// Single char operators and punctuation
|
|
1941
|
+
if ('+-*/%'.includes(ch)) {
|
|
1942
|
+
tokens.push({ t: T.OP, v: ch });
|
|
1943
|
+
i++; continue;
|
|
1684
1944
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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;
|
|
1690
1957
|
}
|
|
1691
1958
|
|
|
1692
|
-
//
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
const savedPos = this.pos;
|
|
1696
|
-
this.next(); // consume (
|
|
1697
|
-
const params = [];
|
|
1698
|
-
let couldBeArrow = true;
|
|
1959
|
+
// Unknown — skip
|
|
1960
|
+
i++;
|
|
1961
|
+
}
|
|
1699
1962
|
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
while (couldBeArrow) {
|
|
1704
|
-
const p = this.peek();
|
|
1705
|
-
if (p.t === T.IDENT && !KEYWORDS.has(p.v)) {
|
|
1706
|
-
params.push(this.next().v);
|
|
1707
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') {
|
|
1708
|
-
this.next();
|
|
1709
|
-
} else {
|
|
1710
|
-
break;
|
|
1711
|
-
}
|
|
1712
|
-
} else {
|
|
1713
|
-
couldBeArrow = false;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1963
|
+
tokens.push({ t: T.EOF, v: null });
|
|
1964
|
+
return tokens;
|
|
1965
|
+
}
|
|
1717
1966
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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;
|
|
1975
|
+
}
|
|
1726
1976
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
this.next(); // consume (
|
|
1730
|
-
const expr = this.parseExpression(0);
|
|
1731
|
-
this.expect(T.PUNC, ')');
|
|
1732
|
-
return expr;
|
|
1733
|
-
}
|
|
1977
|
+
peek() { return this.tokens[this.pos]; }
|
|
1978
|
+
next() { return this.tokens[this.pos++]; }
|
|
1734
1979
|
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
while (!(this.peek().t === T.PUNC && this.peek().v === ']') && this.peek().t !== T.EOF) {
|
|
1740
|
-
if (this.peek().t === T.OP && this.peek().v === '...') {
|
|
1741
|
-
this.next();
|
|
1742
|
-
elements.push({ type: 'spread', arg: this.parseExpression(0) });
|
|
1743
|
-
} else {
|
|
1744
|
-
elements.push(this.parseExpression(0));
|
|
1745
|
-
}
|
|
1746
|
-
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1747
|
-
}
|
|
1748
|
-
this.expect(T.PUNC, ']');
|
|
1749
|
-
return { type: 'array', elements };
|
|
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}`);
|
|
1750
1984
|
}
|
|
1985
|
+
return t;
|
|
1986
|
+
}
|
|
1751
1987
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
1765
|
-
const keyTok = this.next();
|
|
1766
|
-
let key;
|
|
1767
|
-
if (keyTok.t === T.IDENT || keyTok.t === T.STR) key = keyTok.v;
|
|
1768
|
-
else if (keyTok.t === T.NUM) key = String(keyTok.v);
|
|
1769
|
-
else throw new Error('Invalid object key: ' + keyTok.v);
|
|
1770
|
-
|
|
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 };
|
|
1988
|
+
match(type, val) {
|
|
1989
|
+
const t = this.peek();
|
|
1990
|
+
if (t.t === type && (val === undefined || t.v === val)) {
|
|
1991
|
+
return this.next();
|
|
1782
1992
|
}
|
|
1993
|
+
return null;
|
|
1994
|
+
}
|
|
1783
1995
|
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1996
|
+
// Main entry
|
|
1997
|
+
parse() {
|
|
1998
|
+
const result = this.parseExpression(0);
|
|
1999
|
+
return result;
|
|
2000
|
+
}
|
|
1787
2001
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
if (tok.v === 'null') return { type: 'literal', value: null };
|
|
1792
|
-
if (tok.v === 'undefined') return { type: 'literal', value: undefined };
|
|
2002
|
+
// Precedence climbing
|
|
2003
|
+
parseExpression(minPrec) {
|
|
2004
|
+
let left = this.parseUnary();
|
|
1793
2005
|
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2006
|
+
while (true) {
|
|
2007
|
+
const tok = this.peek();
|
|
2008
|
+
|
|
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;
|
|
1806
2020
|
}
|
|
1807
|
-
return { type: 'new', callee: classExpr, args };
|
|
1808
2021
|
}
|
|
1809
2022
|
|
|
1810
|
-
//
|
|
1811
|
-
if (
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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;
|
|
1815
2031
|
}
|
|
1816
2032
|
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}
|
|
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;
|
|
2040
|
+
}
|
|
1825
2041
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
// ---------------------------------------------------------------------------
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
1829
2044
|
|
|
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
|
-
]);
|
|
2045
|
+
return left;
|
|
2046
|
+
}
|
|
1837
2047
|
|
|
1838
|
-
|
|
1839
|
-
|
|
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
|
-
]);
|
|
2048
|
+
parseUnary() {
|
|
2049
|
+
const tok = this.peek();
|
|
1845
2050
|
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
+
}
|
|
1849
2057
|
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
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
|
+
}
|
|
1853
2064
|
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
2065
|
+
// !expr
|
|
2066
|
+
if (tok.t === T.OP && tok.v === '!') {
|
|
2067
|
+
this.next();
|
|
2068
|
+
const arg = this.parseUnary();
|
|
2069
|
+
return { type: 'not', arg };
|
|
2070
|
+
}
|
|
1859
2071
|
|
|
1860
|
-
|
|
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
|
+
}
|
|
1861
2078
|
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
*/
|
|
1865
|
-
function _isSafeAccess(obj, prop) {
|
|
1866
|
-
// Never allow access to dangerous properties
|
|
1867
|
-
const BLOCKED = new Set([
|
|
1868
|
-
'constructor', '__proto__', 'prototype', '__defineGetter__',
|
|
1869
|
-
'__defineSetter__', '__lookupGetter__', '__lookupSetter__',
|
|
1870
|
-
'call', 'apply', 'bind',
|
|
1871
|
-
]);
|
|
1872
|
-
if (typeof prop === 'string' && BLOCKED.has(prop)) return false;
|
|
2079
|
+
return this.parsePostfix();
|
|
2080
|
+
}
|
|
1873
2081
|
|
|
1874
|
-
|
|
1875
|
-
|
|
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;
|
|
1879
|
-
}
|
|
2082
|
+
parsePostfix() {
|
|
2083
|
+
let left = this.parsePrimary();
|
|
1880
2084
|
|
|
1881
|
-
|
|
1882
|
-
|
|
2085
|
+
while (true) {
|
|
2086
|
+
const tok = this.peek();
|
|
1883
2087
|
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
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
|
+
}
|
|
1887
2099
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
if (
|
|
1893
|
-
|
|
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
|
+
}
|
|
1894
2120
|
}
|
|
2121
|
+
continue;
|
|
1895
2122
|
}
|
|
1896
|
-
// Built-in globals (safe ones only)
|
|
1897
|
-
if (name === 'Math') return Math;
|
|
1898
|
-
if (name === 'JSON') return JSON;
|
|
1899
|
-
if (name === 'Date') return Date;
|
|
1900
|
-
if (name === 'Array') return Array;
|
|
1901
|
-
if (name === 'Object') return Object;
|
|
1902
|
-
if (name === 'String') return String;
|
|
1903
|
-
if (name === 'Number') return Number;
|
|
1904
|
-
if (name === 'Boolean') return Boolean;
|
|
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
|
-
}
|
|
1922
2123
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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);
|
|
1931
2133
|
}
|
|
2134
|
+
continue;
|
|
1932
2135
|
}
|
|
1933
|
-
return result;
|
|
1934
|
-
}
|
|
1935
2136
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
return obj[prop];
|
|
1942
|
-
}
|
|
2137
|
+
// Function call: fn()
|
|
2138
|
+
if (tok.t === T.PUNC && tok.v === '(') {
|
|
2139
|
+
left = this._parseCall(left);
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
1943
2142
|
|
|
1944
|
-
|
|
1945
|
-
const obj = evaluate(node.obj, scope);
|
|
1946
|
-
if (obj == null) return undefined;
|
|
1947
|
-
const prop = node.computed ? evaluate(node.prop, scope) : node.prop;
|
|
1948
|
-
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
1949
|
-
return obj[prop];
|
|
2143
|
+
break;
|
|
1950
2144
|
}
|
|
1951
2145
|
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
return result;
|
|
1955
|
-
}
|
|
2146
|
+
return left;
|
|
2147
|
+
}
|
|
1956
2148
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
if (calleeNode.type === 'member' || calleeNode.type === 'optional_member') {
|
|
1962
|
-
const obj = evaluate(calleeNode.obj, scope);
|
|
1963
|
-
if (obj == null) return undefined;
|
|
1964
|
-
const prop = calleeNode.computed ? evaluate(calleeNode.prop, scope) : calleeNode.prop;
|
|
1965
|
-
if (!_isSafeAccess(obj, prop)) return undefined;
|
|
1966
|
-
const fn = obj[prop];
|
|
1967
|
-
if (typeof fn !== 'function') return undefined;
|
|
1968
|
-
return fn.apply(obj, args);
|
|
1969
|
-
}
|
|
1970
|
-
const callee = evaluate(calleeNode, scope);
|
|
1971
|
-
if (callee == null) return undefined;
|
|
1972
|
-
if (typeof callee !== 'function') return undefined;
|
|
1973
|
-
return callee(...args);
|
|
1974
|
-
}
|
|
2149
|
+
_parseCall(callee) {
|
|
2150
|
+
const args = this._parseArgs();
|
|
2151
|
+
return { type: 'call', callee, args };
|
|
2152
|
+
}
|
|
1975
2153
|
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
if (
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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));
|
|
1984
2163
|
}
|
|
1985
|
-
|
|
2164
|
+
if (this.peek().t === T.PUNC && this.peek().v === ',') this.next();
|
|
1986
2165
|
}
|
|
2166
|
+
this.expect(T.PUNC, ')');
|
|
2167
|
+
return args;
|
|
2168
|
+
}
|
|
1987
2169
|
|
|
1988
|
-
|
|
1989
|
-
|
|
2170
|
+
parsePrimary() {
|
|
2171
|
+
const tok = this.peek();
|
|
1990
2172
|
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
2173
|
+
// Number literal
|
|
2174
|
+
if (tok.t === T.NUM) {
|
|
2175
|
+
this.next();
|
|
2176
|
+
return { type: 'literal', value: tok.v };
|
|
1994
2177
|
}
|
|
1995
2178
|
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
try {
|
|
2001
|
-
return typeof evaluate(node.arg, scope);
|
|
2002
|
-
} catch {
|
|
2003
|
-
return 'undefined';
|
|
2004
|
-
}
|
|
2179
|
+
// String literal
|
|
2180
|
+
if (tok.t === T.STR) {
|
|
2181
|
+
this.next();
|
|
2182
|
+
return { type: 'literal', value: tok.v };
|
|
2005
2183
|
}
|
|
2006
2184
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2185
|
+
// Template literal
|
|
2186
|
+
if (tok.t === T.TMPL) {
|
|
2187
|
+
this.next();
|
|
2188
|
+
return { type: 'template', parts: tok.v };
|
|
2010
2189
|
}
|
|
2011
2190
|
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
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;
|
|
2198
|
+
|
|
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;
|
|
2019
2213
|
}
|
|
2020
|
-
} else {
|
|
2021
|
-
arr.push(evaluate(e, scope));
|
|
2022
2214
|
}
|
|
2023
2215
|
}
|
|
2024
|
-
return arr;
|
|
2025
|
-
}
|
|
2026
2216
|
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
Object.assign(obj, source);
|
|
2034
|
-
}
|
|
2035
|
-
} else {
|
|
2036
|
-
obj[prop.key] = evaluate(prop.value, scope);
|
|
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 };
|
|
2037
2223
|
}
|
|
2038
2224
|
}
|
|
2039
|
-
return obj;
|
|
2040
|
-
}
|
|
2041
2225
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
const
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
paramNames.forEach((name, i) => { arrowScope[name] = args[i]; });
|
|
2049
|
-
return evaluate(bodyNode, [arrowScope, ...closedScope]);
|
|
2050
|
-
};
|
|
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;
|
|
2051
2232
|
}
|
|
2052
2233
|
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
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);
|
|
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();
|
|
2068
2246
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2247
|
+
this.expect(T.PUNC, ']');
|
|
2248
|
+
return { type: 'array', elements };
|
|
2071
2249
|
}
|
|
2072
|
-
}
|
|
2073
|
-
return result;
|
|
2074
|
-
}
|
|
2075
|
-
|
|
2076
|
-
/**
|
|
2077
|
-
* Resolve and execute a function call safely.
|
|
2078
|
-
*/
|
|
2079
|
-
function _resolveCall(node, scope) {
|
|
2080
|
-
const callee = node.callee;
|
|
2081
|
-
const args = _evalArgs(node.args, scope);
|
|
2082
|
-
|
|
2083
|
-
// Method call: obj.method() — bind `this` to obj
|
|
2084
|
-
if (callee.type === 'member' || callee.type === 'optional_member') {
|
|
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);
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// Direct call: fn(args)
|
|
2095
|
-
const fn = evaluate(callee, scope);
|
|
2096
|
-
if (typeof fn !== 'function') return undefined;
|
|
2097
|
-
return fn(...args);
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
/**
|
|
2101
|
-
* Evaluate binary expression.
|
|
2102
|
-
*/
|
|
2103
|
-
function _evalBinary(node, scope) {
|
|
2104
|
-
// Short-circuit for logical ops
|
|
2105
|
-
if (node.op === '&&') {
|
|
2106
|
-
const left = evaluate(node.left, scope);
|
|
2107
|
-
return left ? evaluate(node.right, scope) : left;
|
|
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);
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
const left = evaluate(node.left, scope);
|
|
2119
|
-
const right = evaluate(node.right, scope);
|
|
2120
|
-
|
|
2121
|
-
switch (node.op) {
|
|
2122
|
-
case '+': return left + right;
|
|
2123
|
-
case '-': return left - right;
|
|
2124
|
-
case '*': return left * right;
|
|
2125
|
-
case '/': return left / right;
|
|
2126
|
-
case '%': return left % right;
|
|
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;
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
2250
|
|
|
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
|
+
}
|
|
2141
2263
|
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
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);
|
|
2145
2269
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
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 };
|
|
2281
|
+
}
|
|
2154
2282
|
|
|
2155
|
-
//
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const _astCache = new Map();
|
|
2159
|
-
const _AST_CACHE_MAX = 512;
|
|
2283
|
+
// Identifiers & keywords
|
|
2284
|
+
if (tok.t === T.IDENT) {
|
|
2285
|
+
this.next();
|
|
2160
2286
|
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
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 };
|
|
2165
2292
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
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();
|
|
2172
2305
|
}
|
|
2306
|
+
return { type: 'new', callee: classExpr, args };
|
|
2173
2307
|
}
|
|
2174
|
-
// Fall through to full parser for built-in globals (Math, JSON, etc.)
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
// Check AST cache (LRU: move to end on hit)
|
|
2178
|
-
let ast = _astCache.get(trimmed);
|
|
2179
|
-
if (ast) {
|
|
2180
|
-
_astCache.delete(trimmed);
|
|
2181
|
-
_astCache.set(trimmed, ast);
|
|
2182
|
-
} else {
|
|
2183
|
-
const tokens = tokenize(trimmed);
|
|
2184
|
-
const parser = new Parser(tokens, scope);
|
|
2185
|
-
ast = parser.parse();
|
|
2186
2308
|
|
|
2187
|
-
//
|
|
2188
|
-
if (
|
|
2189
|
-
|
|
2190
|
-
|
|
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 };
|
|
2191
2314
|
}
|
|
2192
|
-
_astCache.set(trimmed, ast);
|
|
2193
|
-
}
|
|
2194
2315
|
|
|
2195
|
-
|
|
2196
|
-
} catch (err) {
|
|
2197
|
-
if (typeof console !== 'undefined' && console.debug) {
|
|
2198
|
-
console.debug(`[zQuery EXPR_EVAL] Failed to evaluate: "${expr}"`, err.message);
|
|
2316
|
+
return { type: 'ident', name: tok.v };
|
|
2199
2317
|
}
|
|
2200
|
-
return undefined;
|
|
2201
|
-
}
|
|
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
|
-
|
|
2225
|
-
// ---------------------------------------------------------------------------
|
|
2226
|
-
// Reusable template element — avoids per-call allocation
|
|
2227
|
-
// ---------------------------------------------------------------------------
|
|
2228
|
-
let _tpl = null;
|
|
2229
2318
|
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2319
|
+
// Fallback — return undefined for unparseable
|
|
2320
|
+
this.next();
|
|
2321
|
+
return { type: 'literal', value: undefined };
|
|
2322
|
+
}
|
|
2233
2323
|
}
|
|
2234
2324
|
|
|
2235
2325
|
// ---------------------------------------------------------------------------
|
|
2236
|
-
//
|
|
2326
|
+
// Evaluator — walks the AST, resolves against scope
|
|
2237
2327
|
// ---------------------------------------------------------------------------
|
|
2238
2328
|
|
|
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;
|
|
2251
|
-
|
|
2252
|
-
// Move children into a wrapper for consistent handling.
|
|
2253
|
-
// We move (not clone) from the template — cheaper than cloning.
|
|
2254
|
-
const tempDiv = document.createElement('div');
|
|
2255
|
-
while (newRoot.firstChild) tempDiv.appendChild(newRoot.firstChild);
|
|
2256
|
-
|
|
2257
|
-
_morphChildren(rootEl, tempDiv);
|
|
2258
|
-
|
|
2259
|
-
if (start) window.__zqMorphHook(rootEl, performance.now() - start);
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
/**
|
|
2263
|
-
* Morph a single element in place — diffs attributes and children
|
|
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;
|
|
2280
|
-
|
|
2281
|
-
// Same tag — morph in place (preserves identity, event listeners, refs)
|
|
2282
|
-
if (oldEl.nodeName === newEl.nodeName) {
|
|
2283
|
-
_morphAttributes(oldEl, newEl);
|
|
2284
|
-
_morphChildren(oldEl, newEl);
|
|
2285
|
-
if (start) window.__zqMorphHook(oldEl, performance.now() - start);
|
|
2286
|
-
return oldEl;
|
|
2287
|
-
}
|
|
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
|
+
]);
|
|
2288
2336
|
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
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
|
+
]);
|
|
2295
2344
|
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
* @param {Element} oldParent — live DOM parent
|
|
2300
|
-
* @param {Element} newParent — desired state parent
|
|
2301
|
-
*/
|
|
2302
|
-
function _morphChildren(oldParent, newParent) {
|
|
2303
|
-
// Snapshot live NodeLists into arrays — childNodes is live and
|
|
2304
|
-
// mutates during insertBefore/removeChild. Using a for loop to push
|
|
2305
|
-
// avoids spread operator overhead for large child lists.
|
|
2306
|
-
const oldCN = oldParent.childNodes;
|
|
2307
|
-
const newCN = newParent.childNodes;
|
|
2308
|
-
const oldLen = oldCN.length;
|
|
2309
|
-
const newLen = newCN.length;
|
|
2310
|
-
const oldChildren = new Array(oldLen);
|
|
2311
|
-
const newChildren = new Array(newLen);
|
|
2312
|
-
for (let i = 0; i < oldLen; i++) oldChildren[i] = oldCN[i];
|
|
2313
|
-
for (let i = 0; i < newLen; i++) newChildren[i] = newCN[i];
|
|
2345
|
+
const SAFE_NUMBER_METHODS = new Set([
|
|
2346
|
+
'toFixed', 'toPrecision', 'toString', 'valueOf',
|
|
2347
|
+
]);
|
|
2314
2348
|
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2349
|
+
const SAFE_OBJECT_METHODS = new Set([
|
|
2350
|
+
'hasOwnProperty', 'toString', 'valueOf',
|
|
2351
|
+
]);
|
|
2318
2352
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
if (_getKey(newChildren[i]) != null) { hasKeys = true; break; }
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
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
|
+
]);
|
|
2327
2358
|
|
|
2328
|
-
|
|
2329
|
-
oldKeyMap = new Map();
|
|
2330
|
-
newKeyMap = new Map();
|
|
2331
|
-
for (let i = 0; i < oldLen; i++) {
|
|
2332
|
-
const key = _getKey(oldChildren[i]);
|
|
2333
|
-
if (key != null) oldKeyMap.set(key, i);
|
|
2334
|
-
}
|
|
2335
|
-
for (let i = 0; i < newLen; i++) {
|
|
2336
|
-
const key = _getKey(newChildren[i]);
|
|
2337
|
-
if (key != null) newKeyMap.set(key, i);
|
|
2338
|
-
}
|
|
2339
|
-
_morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap);
|
|
2340
|
-
} else {
|
|
2341
|
-
_morphChildrenUnkeyed(oldParent, oldChildren, newChildren);
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2359
|
+
const SAFE_JSON_PROPS = new Set(['parse', 'stringify']);
|
|
2344
2360
|
|
|
2345
2361
|
/**
|
|
2346
|
-
*
|
|
2362
|
+
* Check if property access is safe
|
|
2347
2363
|
*/
|
|
2348
|
-
function
|
|
2349
|
-
|
|
2350
|
-
const
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
// Append new nodes
|
|
2359
|
-
if (newLen > oldLen) {
|
|
2360
|
-
for (let i = oldLen; i < newLen; i++) {
|
|
2361
|
-
oldParent.appendChild(newChildren[i].cloneNode(true));
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
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;
|
|
2364
2372
|
|
|
2365
|
-
//
|
|
2366
|
-
if (
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
}
|
|
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;
|
|
2371
2378
|
}
|
|
2372
2379
|
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
* using Longest Increasing Subsequence (LIS) to find the maximum set
|
|
2376
|
-
* of nodes that are already in the correct relative order, then only
|
|
2377
|
-
* move the remaining nodes.
|
|
2378
|
-
*/
|
|
2379
|
-
function _morphChildrenKeyed(oldParent, oldChildren, newChildren, oldKeyMap, newKeyMap) {
|
|
2380
|
-
const consumed = new Set();
|
|
2381
|
-
const newLen = newChildren.length;
|
|
2382
|
-
const matched = new Array(newLen);
|
|
2380
|
+
function evaluate(node, scope) {
|
|
2381
|
+
if (!node) return undefined;
|
|
2383
2382
|
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
if (key != null && oldKeyMap.has(key)) {
|
|
2388
|
-
const oldIdx = oldKeyMap.get(key);
|
|
2389
|
-
matched[i] = oldChildren[oldIdx];
|
|
2390
|
-
consumed.add(oldIdx);
|
|
2391
|
-
} else {
|
|
2392
|
-
matched[i] = null;
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2383
|
+
switch (node.type) {
|
|
2384
|
+
case 'literal':
|
|
2385
|
+
return node.value;
|
|
2395
2386
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
const
|
|
2400
|
-
|
|
2401
|
-
|
|
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
|
+
}
|
|
2402
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;
|
|
2403
2420
|
}
|
|
2404
|
-
}
|
|
2405
2421
|
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
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;
|
|
2416
2433
|
}
|
|
2417
|
-
}
|
|
2418
|
-
|
|
2419
|
-
const lisSet = _lis(oldIndices);
|
|
2420
2434
|
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
unkeyedOld.push(oldChildren[i]);
|
|
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];
|
|
2428
2441
|
}
|
|
2429
|
-
}
|
|
2430
|
-
let unkeyedIdx = 0;
|
|
2431
|
-
|
|
2432
|
-
for (let i = 0; i < newLen; i++) {
|
|
2433
|
-
const newNode = newChildren[i];
|
|
2434
|
-
const newKey = _getKey(newNode);
|
|
2435
|
-
let oldNode = matched[i];
|
|
2436
2442
|
|
|
2437
|
-
|
|
2438
|
-
|
|
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];
|
|
2439
2449
|
}
|
|
2440
2450
|
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
oldParent.insertBefore(oldNode, cursor);
|
|
2445
|
-
}
|
|
2446
|
-
// Capture next sibling BEFORE _morphNode — if _morphNode calls
|
|
2447
|
-
// replaceChild, oldNode is removed and nextSibling becomes stale.
|
|
2448
|
-
const nextSib = oldNode.nextSibling;
|
|
2449
|
-
_morphNode(oldParent, oldNode, newNode);
|
|
2450
|
-
cursor = nextSib;
|
|
2451
|
-
} else {
|
|
2452
|
-
// Insert new node
|
|
2453
|
-
const clone = newNode.cloneNode(true);
|
|
2454
|
-
if (cursor) {
|
|
2455
|
-
oldParent.insertBefore(clone, cursor);
|
|
2456
|
-
} else {
|
|
2457
|
-
oldParent.appendChild(clone);
|
|
2458
|
-
}
|
|
2451
|
+
case 'call': {
|
|
2452
|
+
const result = _resolveCall(node, scope, false);
|
|
2453
|
+
return result;
|
|
2459
2454
|
}
|
|
2460
|
-
}
|
|
2461
2455
|
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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);
|
|
2468
|
+
}
|
|
2469
|
+
const callee = evaluate(calleeNode, scope);
|
|
2470
|
+
if (callee == null) return undefined;
|
|
2471
|
+
if (typeof callee !== 'function') return undefined;
|
|
2472
|
+
return callee(...args);
|
|
2467
2473
|
}
|
|
2468
|
-
}
|
|
2469
2474
|
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
if (
|
|
2475
|
-
|
|
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);
|
|
2476
2483
|
}
|
|
2484
|
+
return undefined;
|
|
2477
2485
|
}
|
|
2478
|
-
}
|
|
2479
|
-
}
|
|
2480
|
-
|
|
2481
|
-
/**
|
|
2482
|
-
* Compute the Longest Increasing Subsequence of an index array.
|
|
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
|
|
2490
|
-
*/
|
|
2491
|
-
function _lis(arr) {
|
|
2492
|
-
const len = arr.length;
|
|
2493
|
-
const result = new Set();
|
|
2494
|
-
if (len === 0) return result;
|
|
2495
|
-
|
|
2496
|
-
// tails[i] = index in arr of the smallest tail element for LIS of length i+1
|
|
2497
|
-
const tails = [];
|
|
2498
|
-
// prev[i] = predecessor index in arr for the LIS ending at arr[i]
|
|
2499
|
-
const prev = new Array(len).fill(-1);
|
|
2500
|
-
const tailIndices = []; // parallel to tails: actual positions
|
|
2501
2486
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
const val = arr[i];
|
|
2487
|
+
case 'binary':
|
|
2488
|
+
return _evalBinary(node, scope);
|
|
2505
2489
|
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
const mid = (lo + hi) >> 1;
|
|
2510
|
-
if (tails[mid] < val) lo = mid + 1;
|
|
2511
|
-
else hi = mid;
|
|
2490
|
+
case 'unary': {
|
|
2491
|
+
const val = evaluate(node.arg, scope);
|
|
2492
|
+
return node.op === '-' ? -val : +val;
|
|
2512
2493
|
}
|
|
2513
2494
|
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
prev[i] = lo > 0 ? tailIndices[lo - 1] : -1;
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
// Reconstruct: walk backwards from the last element of LIS
|
|
2520
|
-
let k = tailIndices[tails.length - 1];
|
|
2521
|
-
for (let i = tails.length - 1; i >= 0; i--) {
|
|
2522
|
-
result.add(k);
|
|
2523
|
-
k = prev[k];
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
return result;
|
|
2527
|
-
}
|
|
2495
|
+
case 'not':
|
|
2496
|
+
return !evaluate(node.arg, scope);
|
|
2528
2497
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
if (oldNode.nodeType === 3 || oldNode.nodeType === 8) {
|
|
2535
|
-
if (newNode.nodeType === oldNode.nodeType) {
|
|
2536
|
-
if (oldNode.nodeValue !== newNode.nodeValue) {
|
|
2537
|
-
oldNode.nodeValue = newNode.nodeValue;
|
|
2498
|
+
case 'typeof': {
|
|
2499
|
+
try {
|
|
2500
|
+
return typeof evaluate(node.arg, scope);
|
|
2501
|
+
} catch {
|
|
2502
|
+
return 'undefined';
|
|
2538
2503
|
}
|
|
2539
|
-
return;
|
|
2540
2504
|
}
|
|
2541
|
-
// Different node types — replace
|
|
2542
|
-
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
2543
|
-
return;
|
|
2544
|
-
}
|
|
2545
2505
|
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
parent.replaceChild(newNode.cloneNode(true), oldNode);
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
// Both are elements — diff attributes then recurse children
|
|
2554
|
-
if (oldNode.nodeType === 1) {
|
|
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;
|
|
2558
|
-
|
|
2559
|
-
// Fast bail-out: if the elements are identical, skip everything.
|
|
2560
|
-
// isEqualNode() is a native C++ comparison — much faster than walking
|
|
2561
|
-
// attributes + children in JS when trees haven't changed.
|
|
2562
|
-
if (oldNode.isEqualNode(newNode)) return;
|
|
2563
|
-
|
|
2564
|
-
_morphAttributes(oldNode, newNode);
|
|
2565
|
-
|
|
2566
|
-
// Special elements: don't recurse into their children
|
|
2567
|
-
const tag = oldNode.nodeName;
|
|
2568
|
-
if (tag === 'INPUT') {
|
|
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);
|
|
@@ -5127,6 +5143,7 @@ const http = {
|
|
|
5127
5143
|
put: (url, data, opts) => request('PUT', url, data, opts),
|
|
5128
5144
|
patch: (url, data, opts) => request('PATCH', url, data, opts),
|
|
5129
5145
|
delete: (url, data, opts) => request('DELETE', url, data, opts),
|
|
5146
|
+
head: (url, opts) => request('HEAD', url, undefined, opts),
|
|
5130
5147
|
|
|
5131
5148
|
/**
|
|
5132
5149
|
* Configure defaults
|
|
@@ -5137,20 +5154,56 @@ const http = {
|
|
|
5137
5154
|
if (opts.timeout !== undefined) _config.timeout = opts.timeout;
|
|
5138
5155
|
},
|
|
5139
5156
|
|
|
5157
|
+
/**
|
|
5158
|
+
* Read-only snapshot of current configuration
|
|
5159
|
+
*/
|
|
5160
|
+
getConfig() {
|
|
5161
|
+
return {
|
|
5162
|
+
baseURL: _config.baseURL,
|
|
5163
|
+
headers: { ..._config.headers },
|
|
5164
|
+
timeout: _config.timeout,
|
|
5165
|
+
};
|
|
5166
|
+
},
|
|
5167
|
+
|
|
5140
5168
|
/**
|
|
5141
5169
|
* Add request interceptor
|
|
5142
5170
|
* @param {Function} fn — (fetchOpts, url) → void | false | { url, options }
|
|
5171
|
+
* @returns {Function} unsubscribe function
|
|
5143
5172
|
*/
|
|
5144
5173
|
onRequest(fn) {
|
|
5145
5174
|
_interceptors.request.push(fn);
|
|
5175
|
+
return () => {
|
|
5176
|
+
const idx = _interceptors.request.indexOf(fn);
|
|
5177
|
+
if (idx !== -1) _interceptors.request.splice(idx, 1);
|
|
5178
|
+
};
|
|
5146
5179
|
},
|
|
5147
5180
|
|
|
5148
5181
|
/**
|
|
5149
5182
|
* Add response interceptor
|
|
5150
5183
|
* @param {Function} fn — (result) → void
|
|
5184
|
+
* @returns {Function} unsubscribe function
|
|
5151
5185
|
*/
|
|
5152
5186
|
onResponse(fn) {
|
|
5153
5187
|
_interceptors.response.push(fn);
|
|
5188
|
+
return () => {
|
|
5189
|
+
const idx = _interceptors.response.indexOf(fn);
|
|
5190
|
+
if (idx !== -1) _interceptors.response.splice(idx, 1);
|
|
5191
|
+
};
|
|
5192
|
+
},
|
|
5193
|
+
|
|
5194
|
+
/**
|
|
5195
|
+
* Clear interceptors — all, or just 'request' / 'response'
|
|
5196
|
+
*/
|
|
5197
|
+
clearInterceptors(type) {
|
|
5198
|
+
if (!type || type === 'request') _interceptors.request.length = 0;
|
|
5199
|
+
if (!type || type === 'response') _interceptors.response.length = 0;
|
|
5200
|
+
},
|
|
5201
|
+
|
|
5202
|
+
/**
|
|
5203
|
+
* Run multiple requests in parallel
|
|
5204
|
+
*/
|
|
5205
|
+
all(requests) {
|
|
5206
|
+
return Promise.all(requests);
|
|
5154
5207
|
},
|
|
5155
5208
|
|
|
5156
5209
|
/**
|
|
@@ -5750,6 +5803,7 @@ $.post = http.post;
|
|
|
5750
5803
|
$.put = http.put;
|
|
5751
5804
|
$.patch = http.patch;
|
|
5752
5805
|
$.delete = http.delete;
|
|
5806
|
+
$.head = http.head;
|
|
5753
5807
|
|
|
5754
5808
|
// --- Utilities -------------------------------------------------------------
|
|
5755
5809
|
$.debounce = debounce;
|
|
@@ -5798,9 +5852,10 @@ $.guardCallback = guardCallback;
|
|
|
5798
5852
|
$.validate = validate;
|
|
5799
5853
|
|
|
5800
5854
|
// --- Meta ------------------------------------------------------------------
|
|
5801
|
-
$.version
|
|
5802
|
-
$.libSize
|
|
5803
|
-
$.
|
|
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
|
|
5804
5859
|
|
|
5805
5860
|
$.noConflict = () => {
|
|
5806
5861
|
if (typeof window !== 'undefined' && window.$ === $) {
|