tova 0.2.9 → 0.3.1
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/bin/tova.js +1404 -114
- package/package.json +3 -1
- package/src/analyzer/analyzer.js +882 -695
- package/src/analyzer/client-analyzer.js +191 -0
- package/src/analyzer/server-analyzer.js +467 -0
- package/src/analyzer/types.js +20 -4
- package/src/codegen/base-codegen.js +473 -111
- package/src/codegen/client-codegen.js +109 -46
- package/src/codegen/codegen.js +65 -5
- package/src/codegen/server-codegen.js +297 -38
- package/src/diagnostics/error-codes.js +255 -0
- package/src/diagnostics/formatter.js +150 -28
- package/src/docs/generator.js +390 -0
- package/src/lexer/lexer.js +306 -64
- package/src/lexer/tokens.js +19 -0
- package/src/lsp/server.js +935 -53
- package/src/parser/ast.js +81 -368
- package/src/parser/client-ast.js +138 -0
- package/src/parser/client-parser.js +504 -0
- package/src/parser/parser.js +492 -1056
- package/src/parser/server-ast.js +240 -0
- package/src/parser/server-parser.js +602 -0
- package/src/runtime/array-proto.js +32 -0
- package/src/runtime/embedded.js +1 -1
- package/src/runtime/reactivity.js +239 -42
- package/src/stdlib/advanced-collections.js +81 -0
- package/src/stdlib/inline.js +556 -13
- package/src/version.js +1 -1
|
@@ -363,9 +363,9 @@ export function watch(getter, callback, options = {}) {
|
|
|
363
363
|
const effect = createEffect(() => {
|
|
364
364
|
const newValue = getter();
|
|
365
365
|
if (initialized) {
|
|
366
|
-
callback(newValue, oldValue);
|
|
366
|
+
untrack(() => callback(newValue, oldValue));
|
|
367
367
|
} else if (options.immediate) {
|
|
368
|
-
callback(newValue, undefined);
|
|
368
|
+
untrack(() => callback(newValue, undefined));
|
|
369
369
|
}
|
|
370
370
|
oldValue = newValue;
|
|
371
371
|
initialized = true;
|
|
@@ -447,29 +447,41 @@ export function createErrorBoundary(options = {}) {
|
|
|
447
447
|
return { error, run, reset };
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
let __errorBoundaryIdCounter = 0;
|
|
451
|
+
|
|
452
|
+
export function ErrorBoundary({ fallback, children, onError, onReset, onErrorCleared, retry = 0 }) {
|
|
451
453
|
const [error, setError] = createSignal(null);
|
|
452
454
|
const [retryCount, setRetryCount] = createSignal(0);
|
|
455
|
+
const boundaryId = ++__errorBoundaryIdCounter;
|
|
456
|
+
let lastErrorId = 0;
|
|
453
457
|
|
|
454
458
|
function handleError(e) {
|
|
455
459
|
const stack = buildComponentStack();
|
|
456
|
-
|
|
460
|
+
const errorId = `EB${boundaryId}-${++lastErrorId}`;
|
|
461
|
+
|
|
462
|
+
if (e && typeof e === 'object') {
|
|
463
|
+
e.__tovaComponentStack = stack;
|
|
464
|
+
e.__tovaErrorId = errorId;
|
|
465
|
+
}
|
|
466
|
+
|
|
457
467
|
if (retryCount() < retry) {
|
|
458
468
|
setRetryCount(c => c + 1);
|
|
459
469
|
setError(null); // clear to re-trigger render
|
|
460
470
|
return;
|
|
461
471
|
}
|
|
462
472
|
setError(e);
|
|
463
|
-
if (onError) onError({ error: e, componentStack: stack });
|
|
473
|
+
if (onError) onError({ error: e, componentStack: stack, errorId, retryCount: retryCount() });
|
|
464
474
|
}
|
|
465
475
|
|
|
466
|
-
|
|
476
|
+
function resetBoundary() {
|
|
477
|
+
setRetryCount(0);
|
|
478
|
+
setError(null);
|
|
479
|
+
if (onReset) onReset();
|
|
480
|
+
}
|
|
467
481
|
|
|
468
482
|
// Return a reactive wrapper that switches between children and fallback
|
|
469
483
|
const childContent = children && children.length === 1 ? children[0] : tova_fragment(children || []);
|
|
470
484
|
|
|
471
|
-
popErrorHandler();
|
|
472
|
-
|
|
473
485
|
const vnode = {
|
|
474
486
|
__tova: true,
|
|
475
487
|
tag: '__dynamic',
|
|
@@ -477,19 +489,20 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
|
|
|
477
489
|
children: [],
|
|
478
490
|
_fallback: fallback,
|
|
479
491
|
_componentName: 'ErrorBoundary',
|
|
492
|
+
_errorHandler: handleError, // Active during __dynamic effect render cycle
|
|
480
493
|
compute: () => {
|
|
481
494
|
const err = error();
|
|
482
495
|
if (err) {
|
|
483
496
|
// Render fallback — if fallback itself throws, propagate to parent boundary
|
|
484
497
|
try {
|
|
498
|
+
const errorId = err && typeof err === 'object' ? err.__tovaErrorId : null;
|
|
485
499
|
return typeof fallback === 'function'
|
|
486
500
|
? fallback({
|
|
487
501
|
error: err,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
},
|
|
502
|
+
errorId,
|
|
503
|
+
retryCount: retryCount(),
|
|
504
|
+
componentStack: err && typeof err === 'object' ? err.__tovaComponentStack : [],
|
|
505
|
+
reset: resetBoundary,
|
|
493
506
|
})
|
|
494
507
|
: fallback;
|
|
495
508
|
} catch (fallbackError) {
|
|
@@ -500,6 +513,10 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
|
|
|
500
513
|
return null;
|
|
501
514
|
}
|
|
502
515
|
}
|
|
516
|
+
// Children rendered successfully — fire onErrorCleared if we recovered from an error
|
|
517
|
+
if (onErrorCleared && lastErrorId > 0 && retryCount() === 0) {
|
|
518
|
+
queueMicrotask(() => onErrorCleared());
|
|
519
|
+
}
|
|
503
520
|
return childContent;
|
|
504
521
|
},
|
|
505
522
|
};
|
|
@@ -507,6 +524,58 @@ export function ErrorBoundary({ fallback, children, onError, onReset, retry = 0
|
|
|
507
524
|
return vnode;
|
|
508
525
|
}
|
|
509
526
|
|
|
527
|
+
// Built-in ErrorInfo component — renders a formatted error display
|
|
528
|
+
// Usage: <ErrorBoundary fallback={fn(props) ErrorInfo(props)} />
|
|
529
|
+
export function ErrorInfo({ error, errorId, componentStack, reset, retryCount }) {
|
|
530
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
531
|
+
const stackTrace = error instanceof Error && error.stack ? error.stack : '';
|
|
532
|
+
const compStack = (componentStack || []).join(' > ');
|
|
533
|
+
|
|
534
|
+
const children = [
|
|
535
|
+
tova_el('h3', { style: { margin: '0 0 8px 0', color: '#e53e3e' } }, ['Something went wrong']),
|
|
536
|
+
tova_el('p', { style: { margin: '4px 0', fontFamily: 'monospace', fontSize: '14px' } }, [message]),
|
|
537
|
+
];
|
|
538
|
+
|
|
539
|
+
if (compStack) {
|
|
540
|
+
children.push(
|
|
541
|
+
tova_el('p', { style: { margin: '4px 0', fontSize: '12px', color: '#718096' } }, [
|
|
542
|
+
'Component: ', compStack
|
|
543
|
+
])
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (errorId) {
|
|
548
|
+
children.push(
|
|
549
|
+
tova_el('p', { style: { margin: '4px 0', fontSize: '11px', color: '#a0aec0' } }, [
|
|
550
|
+
'Error ID: ', errorId
|
|
551
|
+
])
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if (stackTrace) {
|
|
556
|
+
children.push(
|
|
557
|
+
tova_el('details', { style: { marginTop: '8px', fontSize: '12px' } }, [
|
|
558
|
+
tova_el('summary', { style: { cursor: 'pointer', color: '#4a5568' } }, ['Stack trace']),
|
|
559
|
+
tova_el('pre', { style: { margin: '4px 0', padding: '8px', background: '#1a202c', color: '#e2e8f0', borderRadius: '4px', overflow: 'auto', fontSize: '11px', maxHeight: '200px' } }, [stackTrace]),
|
|
560
|
+
])
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (reset) {
|
|
565
|
+
children.push(
|
|
566
|
+
tova_el('button', {
|
|
567
|
+
style: { marginTop: '8px', padding: '6px 16px', background: '#3182ce', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '13px' },
|
|
568
|
+
onClick: reset,
|
|
569
|
+
}, [retryCount > 0 ? 'Retry again' : 'Try again'])
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return tova_el('div', {
|
|
574
|
+
style: { padding: '16px', border: '1px solid #fed7d7', borderRadius: '8px', background: '#fff5f5', color: '#2d3748', fontFamily: 'system-ui, -apple-system, sans-serif' },
|
|
575
|
+
role: 'alert',
|
|
576
|
+
}, children);
|
|
577
|
+
}
|
|
578
|
+
|
|
510
579
|
// ─── Dynamic Component ──────────────────────────────────
|
|
511
580
|
// Renders a component dynamically based on a reactive signal.
|
|
512
581
|
// Usage: Dynamic({ component: mySignal, ...props })
|
|
@@ -547,6 +616,7 @@ export function Portal({ target, children }) {
|
|
|
547
616
|
|
|
548
617
|
export function lazy(loader) {
|
|
549
618
|
let resolved = null;
|
|
619
|
+
let loadError = null;
|
|
550
620
|
let promise = null;
|
|
551
621
|
|
|
552
622
|
return function LazyWrapper(props) {
|
|
@@ -554,28 +624,28 @@ export function lazy(loader) {
|
|
|
554
624
|
return resolved(props);
|
|
555
625
|
}
|
|
556
626
|
|
|
557
|
-
const [comp, setComp] = createSignal(null);
|
|
558
|
-
const [err, setErr] = createSignal(null);
|
|
559
|
-
|
|
560
627
|
if (!promise) {
|
|
561
628
|
promise = loader()
|
|
562
629
|
.then(mod => {
|
|
563
630
|
resolved = mod.default || mod;
|
|
564
|
-
setComp(() => resolved);
|
|
565
631
|
})
|
|
566
|
-
.catch(e =>
|
|
632
|
+
.catch(e => { loadError = e; });
|
|
567
633
|
}
|
|
568
634
|
|
|
635
|
+
const [tick, setTick] = createSignal(0);
|
|
636
|
+
|
|
637
|
+
// Trigger re-render when promise settles
|
|
638
|
+
promise.then(() => setTick(1)).catch(() => setTick(1));
|
|
639
|
+
|
|
569
640
|
return {
|
|
570
641
|
__tova: true,
|
|
571
642
|
tag: '__dynamic',
|
|
572
643
|
props: {},
|
|
573
644
|
children: [],
|
|
574
645
|
compute: () => {
|
|
575
|
-
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
if (c) return c(props);
|
|
646
|
+
tick(); // Track for reactivity
|
|
647
|
+
if (loadError) return tova_el('span', { className: 'tova-error' }, [String(loadError)]);
|
|
648
|
+
if (resolved) return resolved(props);
|
|
579
649
|
// Fallback while loading
|
|
580
650
|
return props && props.fallback ? props.fallback : null;
|
|
581
651
|
},
|
|
@@ -631,6 +701,100 @@ export function tova_fragment(children) {
|
|
|
631
701
|
return { __tova: true, tag: '__fragment', props: {}, children };
|
|
632
702
|
}
|
|
633
703
|
|
|
704
|
+
// ─── Transitions ──────────────────────────────────────────
|
|
705
|
+
// CSS transition directives for mount/unmount animations.
|
|
706
|
+
// Usage: tova_transition(vnode, "fade", { duration: 300 })
|
|
707
|
+
|
|
708
|
+
const TRANSITION_DEFAULTS = {
|
|
709
|
+
fade: { duration: 200, easing: 'ease' },
|
|
710
|
+
slide: { duration: 300, easing: 'ease-out', axis: 'y' },
|
|
711
|
+
scale: { duration: 200, easing: 'ease' },
|
|
712
|
+
fly: { duration: 300, easing: 'ease-out', x: 0, y: -20 },
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
function getTransitionCSS(name, config, phase) {
|
|
716
|
+
const opts = { ...TRANSITION_DEFAULTS[name], ...config };
|
|
717
|
+
const dur = opts.duration + 'ms';
|
|
718
|
+
const ease = opts.easing;
|
|
719
|
+
|
|
720
|
+
switch (name) {
|
|
721
|
+
case 'fade':
|
|
722
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
723
|
+
return { opacity: '0', transition: `opacity ${dur} ${ease}` };
|
|
724
|
+
}
|
|
725
|
+
return { opacity: '1', transition: `opacity ${dur} ${ease}` };
|
|
726
|
+
|
|
727
|
+
case 'slide': {
|
|
728
|
+
const axis = opts.axis || 'y';
|
|
729
|
+
const prop = axis === 'x' ? 'translateX' : 'translateY';
|
|
730
|
+
const dist = (opts.distance || 20) + 'px';
|
|
731
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
732
|
+
return { transform: `${prop}(${dist})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
733
|
+
}
|
|
734
|
+
return { transform: `${prop}(0)`, opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
case 'scale':
|
|
738
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
739
|
+
return { transform: 'scale(0)', opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
740
|
+
}
|
|
741
|
+
return { transform: 'scale(1)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
742
|
+
|
|
743
|
+
case 'fly': {
|
|
744
|
+
const x = (opts.x || 0) + 'px';
|
|
745
|
+
const y = (opts.y || -20) + 'px';
|
|
746
|
+
if (phase === 'enter-from' || phase === 'leave-to') {
|
|
747
|
+
return { transform: `translate(${x}, ${y})`, opacity: '0', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
748
|
+
}
|
|
749
|
+
return { transform: 'translate(0, 0)', opacity: '1', transition: `transform ${dur} ${ease}, opacity ${dur} ${ease}` };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
default:
|
|
753
|
+
return {};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function tova_transition(vnode, name, config = {}) {
|
|
758
|
+
if (!vnode || !vnode.__tova) return vnode;
|
|
759
|
+
vnode._transition = { name, config };
|
|
760
|
+
return vnode;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Apply enter transition to a DOM element after render
|
|
764
|
+
function applyEnterTransition(el, trans) {
|
|
765
|
+
if (!trans) return;
|
|
766
|
+
const fromStyles = getTransitionCSS(trans.name, trans.config, 'enter-from');
|
|
767
|
+
const toStyles = getTransitionCSS(trans.name, trans.config, 'enter-to');
|
|
768
|
+
|
|
769
|
+
// Set initial state
|
|
770
|
+
Object.assign(el.style, fromStyles);
|
|
771
|
+
|
|
772
|
+
// Force reflow, then apply target state
|
|
773
|
+
requestAnimationFrame(() => {
|
|
774
|
+
requestAnimationFrame(() => {
|
|
775
|
+
Object.assign(el.style, toStyles);
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Apply leave transition and return a Promise that resolves when done
|
|
781
|
+
function applyLeaveTransition(el, trans) {
|
|
782
|
+
if (!trans) return Promise.resolve();
|
|
783
|
+
const duration = (trans.config && trans.config.duration) || TRANSITION_DEFAULTS[trans.name]?.duration || 200;
|
|
784
|
+
const toStyles = getTransitionCSS(trans.name, trans.config, 'leave-to');
|
|
785
|
+
Object.assign(el.style, toStyles);
|
|
786
|
+
|
|
787
|
+
return new Promise(resolve => {
|
|
788
|
+
const handler = () => {
|
|
789
|
+
el.removeEventListener('transitionend', handler);
|
|
790
|
+
resolve();
|
|
791
|
+
};
|
|
792
|
+
el.addEventListener('transitionend', handler);
|
|
793
|
+
// Fallback timeout in case transitionend doesn't fire
|
|
794
|
+
setTimeout(resolve, duration + 50);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
634
798
|
// Inject a key prop into a vnode for keyed reconciliation
|
|
635
799
|
export function tova_keyed(key, vnode) {
|
|
636
800
|
if (vnode && vnode.__tova) {
|
|
@@ -741,8 +905,17 @@ function insertRendered(parent, rendered, ref, owner) {
|
|
|
741
905
|
// Clear a marker's content from the DOM and reset __tovaNodes
|
|
742
906
|
function clearMarkerContent(marker) {
|
|
743
907
|
for (const node of marker.__tovaNodes) {
|
|
744
|
-
|
|
745
|
-
if (node.
|
|
908
|
+
// If element has a leave transition, animate out before removing
|
|
909
|
+
if (node.__tovaTransition && node.nodeType === 1) {
|
|
910
|
+
const el = node;
|
|
911
|
+
applyLeaveTransition(el, el.__tovaTransition).then(() => {
|
|
912
|
+
disposeNode(el);
|
|
913
|
+
if (el.parentNode) el.parentNode.removeChild(el);
|
|
914
|
+
});
|
|
915
|
+
} else {
|
|
916
|
+
disposeNode(node);
|
|
917
|
+
if (node.parentNode) node.parentNode.removeChild(node);
|
|
918
|
+
}
|
|
746
919
|
}
|
|
747
920
|
marker.__tovaNodes = [];
|
|
748
921
|
}
|
|
@@ -861,22 +1034,36 @@ export function render(vnode) {
|
|
|
861
1034
|
frag.appendChild(marker);
|
|
862
1035
|
|
|
863
1036
|
let prevDispose = null;
|
|
1037
|
+
const errHandler = vnode._errorHandler || null;
|
|
864
1038
|
createEffect(() => {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1039
|
+
if (errHandler) pushErrorHandler(errHandler);
|
|
1040
|
+
try {
|
|
1041
|
+
const inner = vnode.compute();
|
|
1042
|
+
const parent = marker.parentNode;
|
|
1043
|
+
const ref = nextSiblingAfterMarker(marker);
|
|
1044
|
+
|
|
1045
|
+
if (prevDispose) {
|
|
1046
|
+
prevDispose();
|
|
1047
|
+
prevDispose = null;
|
|
1048
|
+
}
|
|
1049
|
+
clearMarkerContent(marker);
|
|
868
1050
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1051
|
+
createRoot((dispose) => {
|
|
1052
|
+
prevDispose = dispose;
|
|
1053
|
+
const rendered = render(inner);
|
|
1054
|
+
marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);
|
|
1055
|
+
});
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
if (errHandler) {
|
|
1058
|
+
errHandler(e);
|
|
1059
|
+
} else if (currentErrorHandler) {
|
|
1060
|
+
currentErrorHandler(e);
|
|
1061
|
+
} else {
|
|
1062
|
+
console.error('Uncaught error during render:', e);
|
|
1063
|
+
}
|
|
1064
|
+
} finally {
|
|
1065
|
+
if (errHandler) popErrorHandler();
|
|
872
1066
|
}
|
|
873
|
-
clearMarkerContent(marker);
|
|
874
|
-
|
|
875
|
-
createRoot((dispose) => {
|
|
876
|
-
prevDispose = dispose;
|
|
877
|
-
const rendered = render(inner);
|
|
878
|
-
marker.__tovaNodes = insertRendered(parent, rendered, ref, marker);
|
|
879
|
-
});
|
|
880
1067
|
});
|
|
881
1068
|
|
|
882
1069
|
return frag;
|
|
@@ -919,6 +1106,12 @@ export function render(vnode) {
|
|
|
919
1106
|
// Store vnode reference for patching
|
|
920
1107
|
el.__vnode = vnode;
|
|
921
1108
|
|
|
1109
|
+
// Apply enter transition if present
|
|
1110
|
+
if (vnode._transition) {
|
|
1111
|
+
el.__tovaTransition = vnode._transition;
|
|
1112
|
+
applyEnterTransition(el, vnode._transition);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
922
1115
|
return el;
|
|
923
1116
|
}
|
|
924
1117
|
|
|
@@ -965,6 +1158,14 @@ function applyPropValue(el, key, val) {
|
|
|
965
1158
|
} else if (key === 'disabled' || key === 'readOnly' || key === 'hidden') {
|
|
966
1159
|
el[key] = !!val;
|
|
967
1160
|
} else if (key === 'style' && typeof val === 'object') {
|
|
1161
|
+
// Clear old properties not present in new style object
|
|
1162
|
+
for (let i = el.style.length - 1; i >= 0; i--) {
|
|
1163
|
+
const prop = el.style[i];
|
|
1164
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1165
|
+
if (!(prop in val) && !(camel in val)) {
|
|
1166
|
+
el.style.removeProperty(prop);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
968
1169
|
Object.assign(el.style, val);
|
|
969
1170
|
} else {
|
|
970
1171
|
const s = val == null ? '' : String(val);
|
|
@@ -1150,11 +1351,7 @@ function patchPositionalInMarker(marker, newChildren) {
|
|
|
1150
1351
|
if (oldNodes[i].parentNode === parent) parent.removeChild(oldNodes[i]);
|
|
1151
1352
|
}
|
|
1152
1353
|
|
|
1153
|
-
marker.__tovaNodes = oldNodes.slice(0,
|
|
1154
|
-
// Simplify: rebuild __tovaNodes from what should remain
|
|
1155
|
-
if (newCount <= oldCount) {
|
|
1156
|
-
marker.__tovaNodes = oldNodes.slice(0, newCount);
|
|
1157
|
-
}
|
|
1354
|
+
marker.__tovaNodes = oldNodes.slice(0, newCount);
|
|
1158
1355
|
}
|
|
1159
1356
|
|
|
1160
1357
|
// Keyed reconciliation for children of an element (not marker-based)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Advanced collection data structures for Tova
|
|
2
|
+
// These are the real JS implementations used for testing and reference.
|
|
3
|
+
|
|
4
|
+
export class OrderedDict {
|
|
5
|
+
constructor(entries) {
|
|
6
|
+
this._map = new Map(entries || []);
|
|
7
|
+
}
|
|
8
|
+
get(key) { return this._map.has(key) ? this._map.get(key) : null; }
|
|
9
|
+
set(key, value) { const m = new Map(this._map); m.set(key, value); return new OrderedDict([...m]); }
|
|
10
|
+
delete(key) { const m = new Map(this._map); m.delete(key); return new OrderedDict([...m]); }
|
|
11
|
+
has(key) { return this._map.has(key); }
|
|
12
|
+
keys() { return [...this._map.keys()]; }
|
|
13
|
+
values() { return [...this._map.values()]; }
|
|
14
|
+
entries() { return [...this._map.entries()]; }
|
|
15
|
+
get length() { return this._map.size; }
|
|
16
|
+
[Symbol.iterator]() { return this._map[Symbol.iterator](); }
|
|
17
|
+
toString() { return 'OrderedDict(' + this._map.size + ' entries)'; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class DefaultDict {
|
|
21
|
+
constructor(defaultFn) {
|
|
22
|
+
this._map = new Map();
|
|
23
|
+
this._default = defaultFn;
|
|
24
|
+
}
|
|
25
|
+
get(key) {
|
|
26
|
+
if (!this._map.has(key)) {
|
|
27
|
+
this._map.set(key, this._default());
|
|
28
|
+
}
|
|
29
|
+
return this._map.get(key);
|
|
30
|
+
}
|
|
31
|
+
set(key, value) { this._map.set(key, value); return this; }
|
|
32
|
+
has(key) { return this._map.has(key); }
|
|
33
|
+
delete(key) { this._map.delete(key); return this; }
|
|
34
|
+
keys() { return [...this._map.keys()]; }
|
|
35
|
+
values() { return [...this._map.values()]; }
|
|
36
|
+
entries() { return [...this._map.entries()]; }
|
|
37
|
+
get length() { return this._map.size; }
|
|
38
|
+
[Symbol.iterator]() { return this._map[Symbol.iterator](); }
|
|
39
|
+
toString() { return 'DefaultDict(' + this._map.size + ' entries)'; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class Counter {
|
|
43
|
+
constructor(items) {
|
|
44
|
+
this._counts = new Map();
|
|
45
|
+
if (items) {
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
const k = item;
|
|
48
|
+
this._counts.set(k, (this._counts.get(k) || 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
count(item) { return this._counts.get(item) || 0; }
|
|
53
|
+
total() { let s = 0; for (const v of this._counts.values()) s += v; return s; }
|
|
54
|
+
most_common(n) {
|
|
55
|
+
const sorted = [...this._counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
56
|
+
return n !== undefined ? sorted.slice(0, n) : sorted;
|
|
57
|
+
}
|
|
58
|
+
keys() { return [...this._counts.keys()]; }
|
|
59
|
+
values() { return [...this._counts.values()]; }
|
|
60
|
+
entries() { return [...this._counts.entries()]; }
|
|
61
|
+
has(item) { return this._counts.has(item); }
|
|
62
|
+
get length() { return this._counts.size; }
|
|
63
|
+
[Symbol.iterator]() { return this._counts[Symbol.iterator](); }
|
|
64
|
+
toString() { return 'Counter(' + this._counts.size + ' items)'; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class Deque {
|
|
68
|
+
constructor(items) {
|
|
69
|
+
this._items = items ? [...items] : [];
|
|
70
|
+
}
|
|
71
|
+
push_back(val) { return new Deque([...this._items, val]); }
|
|
72
|
+
push_front(val) { return new Deque([val, ...this._items]); }
|
|
73
|
+
pop_back() { if (this._items.length === 0) return [null, this]; const items = this._items.slice(0, -1); return [this._items[this._items.length - 1], new Deque(items)]; }
|
|
74
|
+
pop_front() { if (this._items.length === 0) return [null, this]; return [this._items[0], new Deque(this._items.slice(1))]; }
|
|
75
|
+
peek_front() { return this._items.length > 0 ? this._items[0] : null; }
|
|
76
|
+
peek_back() { return this._items.length > 0 ? this._items[this._items.length - 1] : null; }
|
|
77
|
+
get length() { return this._items.length; }
|
|
78
|
+
toArray() { return [...this._items]; }
|
|
79
|
+
[Symbol.iterator]() { return this._items[Symbol.iterator](); }
|
|
80
|
+
toString() { return 'Deque(' + this._items.length + ' items)'; }
|
|
81
|
+
}
|