dalila 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/routes-generator.js +1 -2
- package/dist/components/ui/dialog/index.d.ts +0 -8
- package/dist/components/ui/dialog/index.js +2 -41
- package/dist/components/ui/dialog/internal.d.ts +5 -0
- package/dist/{componentes/ui/dialog/index.js → components/ui/dialog/internal.js} +1 -23
- package/dist/components/ui/drawer/index.js +2 -2
- package/dist/components/ui/index.d.ts +1 -1
- package/dist/components/ui/index.js +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/query.js +9 -7
- package/dist/core/resource.d.ts +23 -171
- package/dist/core/resource.js +178 -15
- package/dist/form/index.d.ts +1 -1
- package/dist/form/index.js +1 -1
- package/dist/router/index.d.ts +1 -1
- package/dist/router/index.js +1 -1
- package/dist/runtime/bind.d.ts +10 -0
- package/dist/runtime/bind.js +382 -53
- package/package.json +1 -1
- package/dist/componentes/ui/accordion/index.d.ts +0 -2
- package/dist/componentes/ui/accordion/index.js +0 -114
- package/dist/componentes/ui/calendar/index.d.ts +0 -2
- package/dist/componentes/ui/calendar/index.js +0 -132
- package/dist/componentes/ui/combobox/index.d.ts +0 -2
- package/dist/componentes/ui/combobox/index.js +0 -161
- package/dist/componentes/ui/dialog/index.d.ts +0 -10
- package/dist/componentes/ui/drawer/index.d.ts +0 -2
- package/dist/componentes/ui/drawer/index.js +0 -41
- package/dist/componentes/ui/dropdown/index.d.ts +0 -2
- package/dist/componentes/ui/dropdown/index.js +0 -48
- package/dist/componentes/ui/dropzone/index.d.ts +0 -2
- package/dist/componentes/ui/dropzone/index.js +0 -92
- package/dist/componentes/ui/env.d.ts +0 -1
- package/dist/componentes/ui/env.js +0 -2
- package/dist/componentes/ui/index.d.ts +0 -13
- package/dist/componentes/ui/index.js +0 -12
- package/dist/componentes/ui/popover/index.d.ts +0 -2
- package/dist/componentes/ui/popover/index.js +0 -156
- package/dist/componentes/ui/runtime.d.ts +0 -20
- package/dist/componentes/ui/runtime.js +0 -421
- package/dist/componentes/ui/tabs/index.d.ts +0 -3
- package/dist/componentes/ui/tabs/index.js +0 -101
- package/dist/componentes/ui/toast/index.d.ts +0 -3
- package/dist/componentes/ui/toast/index.js +0 -115
- package/dist/componentes/ui/ui-types.d.ts +0 -175
- package/dist/componentes/ui/ui-types.js +0 -1
- package/dist/componentes/ui/validate.d.ts +0 -7
- package/dist/componentes/ui/validate.js +0 -71
- package/dist/core/store.d.ts +0 -130
- package/dist/core/store.js +0 -234
- package/dist/core/virtual.d.ts +0 -26
- package/dist/core/virtual.js +0 -277
- package/dist/core/watch-testing.d.ts +0 -13
- package/dist/core/watch-testing.js +0 -16
- package/dist/router/route.d.ts +0 -23
- package/dist/router/route.js +0 -48
- package/dist/simple.d.ts +0 -11
- package/dist/simple.js +0 -11
- package/dist/ui/accordion.d.ts +0 -2
- package/dist/ui/accordion.js +0 -114
- package/dist/ui/calendar.d.ts +0 -2
- package/dist/ui/calendar.js +0 -132
- package/dist/ui/combobox.d.ts +0 -2
- package/dist/ui/combobox.js +0 -161
- package/dist/ui/dialog.d.ts +0 -10
- package/dist/ui/dialog.js +0 -54
- package/dist/ui/drawer.d.ts +0 -2
- package/dist/ui/drawer.js +0 -41
- package/dist/ui/dropdown.d.ts +0 -2
- package/dist/ui/dropdown.js +0 -48
- package/dist/ui/dropzone.d.ts +0 -2
- package/dist/ui/dropzone.js +0 -92
- package/dist/ui/env.d.ts +0 -1
- package/dist/ui/env.js +0 -2
- package/dist/ui/index.d.ts +0 -13
- package/dist/ui/index.js +0 -12
- package/dist/ui/popover.d.ts +0 -2
- package/dist/ui/popover.js +0 -156
- package/dist/ui/runtime.d.ts +0 -20
- package/dist/ui/runtime.js +0 -421
- package/dist/ui/tabs.d.ts +0 -3
- package/dist/ui/tabs.js +0 -101
- package/dist/ui/toast.d.ts +0 -3
- package/dist/ui/toast.js +0 -115
- package/dist/ui/ui-types.d.ts +0 -175
- package/dist/ui/ui-types.js +0 -1
- package/dist/ui/validate.d.ts +0 -7
- package/dist/ui/validate.js +0 -71
package/dist/runtime/bind.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module dalila/runtime
|
|
8
8
|
*/
|
|
9
9
|
import { effect, createScope, withScope, isInDevMode, signal } from '../core/index.js';
|
|
10
|
-
import { WRAPPED_HANDLER } from '../form/
|
|
10
|
+
import { WRAPPED_HANDLER } from '../form/form.js';
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// Utilities
|
|
13
13
|
// ============================================================================
|
|
@@ -82,6 +82,148 @@ function warn(message) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
const expressionCache = new Map();
|
|
85
|
+
const templateInterpolationPlanCache = new Map();
|
|
86
|
+
const TEMPLATE_PLAN_CACHE_MAX_ENTRIES = 250;
|
|
87
|
+
const TEMPLATE_PLAN_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
88
|
+
const TEMPLATE_PLAN_CACHE_CONFIG_KEY = '__dalila_bind_template_cache';
|
|
89
|
+
const BENCH_FLAG = '__dalila_bind_bench';
|
|
90
|
+
const BENCH_STATS_KEY = '__dalila_bind_bench_stats';
|
|
91
|
+
function nowMs() {
|
|
92
|
+
return typeof performance !== 'undefined' && typeof performance.now === 'function'
|
|
93
|
+
? performance.now()
|
|
94
|
+
: Date.now();
|
|
95
|
+
}
|
|
96
|
+
function coerceCacheSetting(value, fallback) {
|
|
97
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
98
|
+
return fallback;
|
|
99
|
+
return Math.max(0, Math.floor(value));
|
|
100
|
+
}
|
|
101
|
+
function resolveTemplatePlanCacheConfig(options) {
|
|
102
|
+
const globalRaw = globalThis[TEMPLATE_PLAN_CACHE_CONFIG_KEY];
|
|
103
|
+
const fromOptions = options.templatePlanCache;
|
|
104
|
+
const maxEntries = coerceCacheSetting(fromOptions?.maxEntries ?? globalRaw?.maxEntries, TEMPLATE_PLAN_CACHE_MAX_ENTRIES);
|
|
105
|
+
const ttlMs = coerceCacheSetting(fromOptions?.ttlMs ?? globalRaw?.ttlMs, TEMPLATE_PLAN_CACHE_TTL_MS);
|
|
106
|
+
return { maxEntries, ttlMs };
|
|
107
|
+
}
|
|
108
|
+
function createBindBenchSession() {
|
|
109
|
+
const enabled = isInDevMode() && globalThis[BENCH_FLAG] === true;
|
|
110
|
+
return {
|
|
111
|
+
enabled,
|
|
112
|
+
scanMs: 0,
|
|
113
|
+
parseMs: 0,
|
|
114
|
+
totalExpressions: 0,
|
|
115
|
+
fastPathExpressions: 0,
|
|
116
|
+
planCacheHit: false,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function flushBindBenchSession(session) {
|
|
120
|
+
if (!session.enabled)
|
|
121
|
+
return;
|
|
122
|
+
const stats = {
|
|
123
|
+
scanMs: Number(session.scanMs.toFixed(3)),
|
|
124
|
+
parseMs: Number(session.parseMs.toFixed(3)),
|
|
125
|
+
totalExpressions: session.totalExpressions,
|
|
126
|
+
fastPathExpressions: session.fastPathExpressions,
|
|
127
|
+
fastPathHitPercent: session.totalExpressions === 0
|
|
128
|
+
? 0
|
|
129
|
+
: Number(((session.fastPathExpressions / session.totalExpressions) * 100).toFixed(2)),
|
|
130
|
+
planCacheHit: session.planCacheHit,
|
|
131
|
+
};
|
|
132
|
+
const globalObj = globalThis;
|
|
133
|
+
const bucket = globalObj[BENCH_STATS_KEY] ?? {};
|
|
134
|
+
const runs = Array.isArray(bucket.runs) ? bucket.runs : [];
|
|
135
|
+
runs.push(stats);
|
|
136
|
+
if (runs.length > 200)
|
|
137
|
+
runs.shift();
|
|
138
|
+
bucket.runs = runs;
|
|
139
|
+
bucket.last = stats;
|
|
140
|
+
globalObj[BENCH_STATS_KEY] = bucket;
|
|
141
|
+
}
|
|
142
|
+
function compileFastPathExpression(expression) {
|
|
143
|
+
let i = 0;
|
|
144
|
+
const literalKeywords = {
|
|
145
|
+
true: true,
|
|
146
|
+
false: false,
|
|
147
|
+
null: null,
|
|
148
|
+
undefined: undefined,
|
|
149
|
+
};
|
|
150
|
+
const skipSpaces = () => {
|
|
151
|
+
while (i < expression.length && /\s/.test(expression[i]))
|
|
152
|
+
i++;
|
|
153
|
+
};
|
|
154
|
+
const readIdentifier = () => {
|
|
155
|
+
if (i >= expression.length || !isIdentStart(expression[i]))
|
|
156
|
+
return null;
|
|
157
|
+
const start = i;
|
|
158
|
+
i++;
|
|
159
|
+
while (i < expression.length && isIdentPart(expression[i]))
|
|
160
|
+
i++;
|
|
161
|
+
return expression.slice(start, i);
|
|
162
|
+
};
|
|
163
|
+
const readNumericIndex = () => {
|
|
164
|
+
if (i >= expression.length || !/[0-9]/.test(expression[i]))
|
|
165
|
+
return null;
|
|
166
|
+
const start = i;
|
|
167
|
+
i++;
|
|
168
|
+
while (i < expression.length && /[0-9]/.test(expression[i]))
|
|
169
|
+
i++;
|
|
170
|
+
return Number(expression.slice(start, i));
|
|
171
|
+
};
|
|
172
|
+
skipSpaces();
|
|
173
|
+
const root = readIdentifier();
|
|
174
|
+
if (!root)
|
|
175
|
+
return null;
|
|
176
|
+
if (Object.prototype.hasOwnProperty.call(literalKeywords, root)) {
|
|
177
|
+
skipSpaces();
|
|
178
|
+
if (i === expression.length) {
|
|
179
|
+
return { type: 'literal', value: literalKeywords[root] };
|
|
180
|
+
}
|
|
181
|
+
// Keep parser behavior for keyword literals followed by extra syntax.
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
let node = { type: 'identifier', name: root };
|
|
185
|
+
skipSpaces();
|
|
186
|
+
while (i < expression.length) {
|
|
187
|
+
if (expression[i] === '.') {
|
|
188
|
+
i++;
|
|
189
|
+
skipSpaces();
|
|
190
|
+
const prop = readIdentifier();
|
|
191
|
+
if (!prop)
|
|
192
|
+
return null;
|
|
193
|
+
node = {
|
|
194
|
+
type: 'member',
|
|
195
|
+
object: node,
|
|
196
|
+
property: { type: 'literal', value: prop },
|
|
197
|
+
computed: false,
|
|
198
|
+
optional: false,
|
|
199
|
+
};
|
|
200
|
+
skipSpaces();
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (expression[i] === '[') {
|
|
204
|
+
i++;
|
|
205
|
+
skipSpaces();
|
|
206
|
+
const index = readNumericIndex();
|
|
207
|
+
if (index === null)
|
|
208
|
+
return null;
|
|
209
|
+
skipSpaces();
|
|
210
|
+
if (expression[i] !== ']')
|
|
211
|
+
return null;
|
|
212
|
+
i++;
|
|
213
|
+
node = {
|
|
214
|
+
type: 'member',
|
|
215
|
+
object: node,
|
|
216
|
+
property: { type: 'literal', value: index },
|
|
217
|
+
computed: true,
|
|
218
|
+
optional: false,
|
|
219
|
+
};
|
|
220
|
+
skipSpaces();
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return node;
|
|
226
|
+
}
|
|
85
227
|
function isIdentStart(ch) {
|
|
86
228
|
return /[a-zA-Z_$]/.test(ch);
|
|
87
229
|
}
|
|
@@ -481,15 +623,29 @@ function evalExpressionAst(node, ctx) {
|
|
|
481
623
|
};
|
|
482
624
|
return evalNode(node);
|
|
483
625
|
}
|
|
484
|
-
function
|
|
626
|
+
function compileInterpolationExpression(expression) {
|
|
627
|
+
const fastPathAst = compileFastPathExpression(expression);
|
|
628
|
+
if (fastPathAst) {
|
|
629
|
+
return { kind: 'fast_path', ast: fastPathAst };
|
|
630
|
+
}
|
|
631
|
+
return { kind: 'parser', expression };
|
|
632
|
+
}
|
|
633
|
+
function parseInterpolationExpression(expression, benchSession) {
|
|
485
634
|
let ast = expressionCache.get(expression);
|
|
486
635
|
if (ast === undefined) {
|
|
636
|
+
const parseStart = benchSession?.enabled ? nowMs() : 0;
|
|
487
637
|
try {
|
|
488
638
|
ast = parseExpression(expression);
|
|
489
639
|
expressionCache.set(expression, ast);
|
|
640
|
+
if (benchSession?.enabled) {
|
|
641
|
+
benchSession.parseMs += nowMs() - parseStart;
|
|
642
|
+
}
|
|
490
643
|
}
|
|
491
644
|
catch (err) {
|
|
492
645
|
expressionCache.set(expression, null);
|
|
646
|
+
if (benchSession?.enabled) {
|
|
647
|
+
benchSession.parseMs += nowMs() - parseStart;
|
|
648
|
+
}
|
|
493
649
|
return {
|
|
494
650
|
ok: false,
|
|
495
651
|
message: `Text interpolation parse error in "{${expression}}": ${err.message}`,
|
|
@@ -576,31 +732,203 @@ const DEFAULT_RAW_TEXT_SELECTORS = 'pre, code';
|
|
|
576
732
|
// Text Interpolation
|
|
577
733
|
// ============================================================================
|
|
578
734
|
/**
|
|
579
|
-
*
|
|
735
|
+
* Build interpolation segments for one text node.
|
|
580
736
|
*/
|
|
581
|
-
function
|
|
582
|
-
const text = node.data;
|
|
737
|
+
function buildTextInterpolationSegments(text) {
|
|
583
738
|
const regex = /\{([^{}]+)\}/g;
|
|
584
|
-
|
|
585
|
-
if (!regex.test(text))
|
|
586
|
-
return;
|
|
587
|
-
// Reset regex
|
|
588
|
-
regex.lastIndex = 0;
|
|
589
|
-
const frag = document.createDocumentFragment();
|
|
739
|
+
const segments = [];
|
|
590
740
|
let cursor = 0;
|
|
591
741
|
let match;
|
|
592
742
|
while ((match = regex.exec(text)) !== null) {
|
|
593
|
-
// Add text before the token
|
|
594
743
|
const before = text.slice(cursor, match.index);
|
|
595
744
|
if (before) {
|
|
596
|
-
|
|
745
|
+
segments.push({ type: 'text', value: before });
|
|
597
746
|
}
|
|
598
747
|
const rawToken = match[0];
|
|
599
748
|
const expression = match[1].trim();
|
|
749
|
+
segments.push({
|
|
750
|
+
type: 'expr',
|
|
751
|
+
rawToken,
|
|
752
|
+
expression,
|
|
753
|
+
compiled: compileInterpolationExpression(expression),
|
|
754
|
+
});
|
|
755
|
+
cursor = match.index + match[0].length;
|
|
756
|
+
}
|
|
757
|
+
const after = text.slice(cursor);
|
|
758
|
+
if (after) {
|
|
759
|
+
segments.push({ type: 'text', value: after });
|
|
760
|
+
}
|
|
761
|
+
return segments;
|
|
762
|
+
}
|
|
763
|
+
function getNodePath(root, node) {
|
|
764
|
+
const path = [];
|
|
765
|
+
let current = node;
|
|
766
|
+
while (current && current !== root) {
|
|
767
|
+
const parentNode = current.parentNode;
|
|
768
|
+
if (!parentNode)
|
|
769
|
+
return null;
|
|
770
|
+
path.push(Array.prototype.indexOf.call(parentNode.childNodes, current));
|
|
771
|
+
current = parentNode;
|
|
772
|
+
}
|
|
773
|
+
if (current !== root)
|
|
774
|
+
return null;
|
|
775
|
+
path.reverse();
|
|
776
|
+
return path;
|
|
777
|
+
}
|
|
778
|
+
function getNodeAtPath(root, path) {
|
|
779
|
+
let current = root;
|
|
780
|
+
for (const index of path) {
|
|
781
|
+
const child = current.childNodes[index];
|
|
782
|
+
if (!child)
|
|
783
|
+
return null;
|
|
784
|
+
current = child;
|
|
785
|
+
}
|
|
786
|
+
return current;
|
|
787
|
+
}
|
|
788
|
+
function fnv1aStep(hash, value) {
|
|
789
|
+
let h = hash ^ value;
|
|
790
|
+
h = Math.imul(h, 0x01000193);
|
|
791
|
+
return h >>> 0;
|
|
792
|
+
}
|
|
793
|
+
function fnv1aString(hash, value) {
|
|
794
|
+
let h = hash;
|
|
795
|
+
for (let i = 0; i < value.length; i++) {
|
|
796
|
+
h = fnv1aStep(h, value.charCodeAt(i));
|
|
797
|
+
}
|
|
798
|
+
return h;
|
|
799
|
+
}
|
|
800
|
+
function hashNodeStructure(hash, node) {
|
|
801
|
+
let h = fnv1aStep(hash, node.nodeType);
|
|
802
|
+
if (node.nodeType === 1) {
|
|
803
|
+
const el = node;
|
|
804
|
+
h = fnv1aString(h, el.tagName);
|
|
805
|
+
h = fnv1aStep(h, el.attributes.length);
|
|
806
|
+
const attrs = Array.from(el.attributes)
|
|
807
|
+
.map((attr) => `${attr.name}=${attr.value}`)
|
|
808
|
+
.sort();
|
|
809
|
+
for (const attr of attrs) {
|
|
810
|
+
h = fnv1aString(h, attr);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
else if (node.nodeType === 3) {
|
|
814
|
+
h = fnv1aString(h, node.data);
|
|
815
|
+
}
|
|
816
|
+
else if (node.nodeType === 8) {
|
|
817
|
+
h = fnv1aString(h, node.data);
|
|
818
|
+
}
|
|
819
|
+
h = fnv1aStep(h, node.childNodes.length);
|
|
820
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
821
|
+
h = hashNodeStructure(h, node.childNodes[i]);
|
|
822
|
+
}
|
|
823
|
+
return h;
|
|
824
|
+
}
|
|
825
|
+
function createInterpolationTemplateSignature(root, rawTextSelectors) {
|
|
826
|
+
let hash = 0x811c9dc5;
|
|
827
|
+
hash = fnv1aString(hash, rawTextSelectors);
|
|
828
|
+
hash = fnv1aString(hash, root.tagName);
|
|
829
|
+
hash = hashNodeStructure(hash, root);
|
|
830
|
+
return `${root.tagName}:${hash.toString(16)}`;
|
|
831
|
+
}
|
|
832
|
+
function createInterpolationTemplatePlan(root, rawTextSelectors) {
|
|
833
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
834
|
+
const bindings = [];
|
|
835
|
+
let totalExpressions = 0;
|
|
836
|
+
let fastPathExpressions = 0;
|
|
837
|
+
const textBoundary = root.closest('[data-dalila-internal-bound]');
|
|
838
|
+
while (walker.nextNode()) {
|
|
839
|
+
const node = walker.currentNode;
|
|
840
|
+
const parent = node.parentElement;
|
|
841
|
+
if (parent && parent.closest(rawTextSelectors))
|
|
842
|
+
continue;
|
|
843
|
+
if (parent) {
|
|
844
|
+
const bound = parent.closest('[data-dalila-internal-bound]');
|
|
845
|
+
if (bound !== textBoundary)
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
if (!node.data.includes('{'))
|
|
849
|
+
continue;
|
|
850
|
+
const segments = buildTextInterpolationSegments(node.data);
|
|
851
|
+
const hasExpression = segments.some((segment) => segment.type === 'expr');
|
|
852
|
+
if (!hasExpression)
|
|
853
|
+
continue;
|
|
854
|
+
const path = getNodePath(root, node);
|
|
855
|
+
if (!path)
|
|
856
|
+
continue;
|
|
857
|
+
for (const segment of segments) {
|
|
858
|
+
if (segment.type !== 'expr')
|
|
859
|
+
continue;
|
|
860
|
+
totalExpressions++;
|
|
861
|
+
if (segment.compiled.kind === 'fast_path')
|
|
862
|
+
fastPathExpressions++;
|
|
863
|
+
}
|
|
864
|
+
bindings.push({ path, segments });
|
|
865
|
+
}
|
|
866
|
+
return {
|
|
867
|
+
bindings,
|
|
868
|
+
totalExpressions,
|
|
869
|
+
fastPathExpressions,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
function resolveCompiledExpression(compiled, benchSession) {
|
|
873
|
+
if (compiled.kind === 'fast_path') {
|
|
874
|
+
return { ok: true, ast: compiled.ast };
|
|
875
|
+
}
|
|
876
|
+
return parseInterpolationExpression(compiled.expression, benchSession);
|
|
877
|
+
}
|
|
878
|
+
function pruneTemplatePlanCache(now, config) {
|
|
879
|
+
// 1) Remove expired plans first.
|
|
880
|
+
for (const [key, entry] of templateInterpolationPlanCache) {
|
|
881
|
+
if (entry.expiresAt <= now) {
|
|
882
|
+
templateInterpolationPlanCache.delete(key);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// 2) Enforce LRU cap.
|
|
886
|
+
while (templateInterpolationPlanCache.size > config.maxEntries) {
|
|
887
|
+
const oldestKey = templateInterpolationPlanCache.keys().next().value;
|
|
888
|
+
if (!oldestKey)
|
|
889
|
+
break;
|
|
890
|
+
templateInterpolationPlanCache.delete(oldestKey);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
function getCachedTemplatePlan(signature, now, config) {
|
|
894
|
+
if (config.maxEntries === 0 || config.ttlMs === 0)
|
|
895
|
+
return null;
|
|
896
|
+
const entry = templateInterpolationPlanCache.get(signature);
|
|
897
|
+
if (!entry)
|
|
898
|
+
return null;
|
|
899
|
+
if (entry.expiresAt <= now) {
|
|
900
|
+
templateInterpolationPlanCache.delete(signature);
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
// Refresh recency and TTL window.
|
|
904
|
+
templateInterpolationPlanCache.delete(signature);
|
|
905
|
+
templateInterpolationPlanCache.set(signature, {
|
|
906
|
+
plan: entry.plan,
|
|
907
|
+
lastUsedAt: now,
|
|
908
|
+
expiresAt: now + config.ttlMs,
|
|
909
|
+
});
|
|
910
|
+
return entry.plan;
|
|
911
|
+
}
|
|
912
|
+
function setCachedTemplatePlan(signature, plan, now, config) {
|
|
913
|
+
if (config.maxEntries === 0 || config.ttlMs === 0)
|
|
914
|
+
return;
|
|
915
|
+
templateInterpolationPlanCache.set(signature, {
|
|
916
|
+
plan,
|
|
917
|
+
lastUsedAt: now,
|
|
918
|
+
expiresAt: now + config.ttlMs,
|
|
919
|
+
});
|
|
920
|
+
pruneTemplatePlanCache(now, config);
|
|
921
|
+
}
|
|
922
|
+
function bindTextNodeFromPlan(node, plan, ctx, benchSession) {
|
|
923
|
+
const frag = document.createDocumentFragment();
|
|
924
|
+
for (const segment of plan.segments) {
|
|
925
|
+
if (segment.type === 'text') {
|
|
926
|
+
frag.appendChild(document.createTextNode(segment.value));
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
600
929
|
const textNode = document.createTextNode('');
|
|
601
930
|
let warnedParse = false;
|
|
602
931
|
let warnedMissingIdentifier = false;
|
|
603
|
-
const parsed = parseInterpolationExpression(expression);
|
|
604
932
|
const applyResult = (result) => {
|
|
605
933
|
if (!result.ok) {
|
|
606
934
|
if (result.reason === 'parse') {
|
|
@@ -615,9 +943,9 @@ function bindTextNode(node, ctx, cleanups) {
|
|
|
615
943
|
}
|
|
616
944
|
// Backward compatibility for "{identifier}" missing from context:
|
|
617
945
|
// preserve the literal token exactly as before.
|
|
618
|
-
const simpleIdent = expression.match(/^[a-zA-Z_$][\w$]*$/);
|
|
946
|
+
const simpleIdent = segment.expression.match(/^[a-zA-Z_$][\w$]*$/);
|
|
619
947
|
if (result.reason === 'missing_identifier' && simpleIdent && result.identifier === simpleIdent[0]) {
|
|
620
|
-
textNode.data = rawToken;
|
|
948
|
+
textNode.data = segment.rawToken;
|
|
621
949
|
}
|
|
622
950
|
else {
|
|
623
951
|
textNode.data = '';
|
|
@@ -626,10 +954,10 @@ function bindTextNode(node, ctx, cleanups) {
|
|
|
626
954
|
}
|
|
627
955
|
textNode.data = result.value == null ? '' : String(result.value);
|
|
628
956
|
};
|
|
957
|
+
const parsed = resolveCompiledExpression(segment.compiled, benchSession);
|
|
629
958
|
if (!parsed.ok) {
|
|
630
959
|
applyResult({ ok: false, reason: 'parse', message: parsed.message });
|
|
631
960
|
frag.appendChild(textNode);
|
|
632
|
-
cursor = match.index + match[0].length;
|
|
633
961
|
continue;
|
|
634
962
|
}
|
|
635
963
|
// First render is synchronous to avoid empty text until microtask flush.
|
|
@@ -641,18 +969,41 @@ function bindTextNode(node, ctx, cleanups) {
|
|
|
641
969
|
});
|
|
642
970
|
}
|
|
643
971
|
frag.appendChild(textNode);
|
|
644
|
-
cursor = match.index + match[0].length;
|
|
645
|
-
}
|
|
646
|
-
// Add remaining text
|
|
647
|
-
const after = text.slice(cursor);
|
|
648
|
-
if (after) {
|
|
649
|
-
frag.appendChild(document.createTextNode(after));
|
|
650
972
|
}
|
|
651
|
-
// Replace original node
|
|
652
973
|
if (node.parentNode) {
|
|
653
974
|
node.parentNode.replaceChild(frag, node);
|
|
654
975
|
}
|
|
655
976
|
}
|
|
977
|
+
function bindTextInterpolation(root, ctx, rawTextSelectors, cacheConfig, benchSession) {
|
|
978
|
+
const signature = createInterpolationTemplateSignature(root, rawTextSelectors);
|
|
979
|
+
const now = nowMs();
|
|
980
|
+
let plan = getCachedTemplatePlan(signature, now, cacheConfig);
|
|
981
|
+
const scanStart = benchSession?.enabled ? nowMs() : 0;
|
|
982
|
+
if (!plan) {
|
|
983
|
+
plan = createInterpolationTemplatePlan(root, rawTextSelectors);
|
|
984
|
+
setCachedTemplatePlan(signature, plan, now, cacheConfig);
|
|
985
|
+
}
|
|
986
|
+
else if (benchSession) {
|
|
987
|
+
benchSession.planCacheHit = true;
|
|
988
|
+
}
|
|
989
|
+
if (benchSession?.enabled) {
|
|
990
|
+
benchSession.scanMs += nowMs() - scanStart;
|
|
991
|
+
benchSession.totalExpressions = plan.totalExpressions;
|
|
992
|
+
benchSession.fastPathExpressions = plan.fastPathExpressions;
|
|
993
|
+
}
|
|
994
|
+
if (plan.bindings.length === 0)
|
|
995
|
+
return;
|
|
996
|
+
const nodesToBind = [];
|
|
997
|
+
for (const binding of plan.bindings) {
|
|
998
|
+
const target = getNodeAtPath(root, binding.path);
|
|
999
|
+
if (target && target.nodeType === 3) {
|
|
1000
|
+
nodesToBind.push({ node: target, binding });
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
for (const item of nodesToBind) {
|
|
1004
|
+
bindTextNodeFromPlan(item.node, item.binding, ctx, benchSession);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
656
1007
|
// ============================================================================
|
|
657
1008
|
// Event Binding
|
|
658
1009
|
// ============================================================================
|
|
@@ -780,10 +1131,10 @@ function bindEach(root, ctx, cleanups) {
|
|
|
780
1131
|
const bindingName = normalizeBinding(el.getAttribute('d-each'));
|
|
781
1132
|
if (!bindingName)
|
|
782
1133
|
continue;
|
|
783
|
-
|
|
1134
|
+
let binding = ctx[bindingName];
|
|
784
1135
|
if (binding === undefined) {
|
|
785
1136
|
warn(`d-each: "${bindingName}" not found in context`);
|
|
786
|
-
|
|
1137
|
+
binding = [];
|
|
787
1138
|
}
|
|
788
1139
|
const comment = document.createComment('d-each');
|
|
789
1140
|
el.parentNode?.replaceChild(comment, el);
|
|
@@ -1718,6 +2069,8 @@ function bindArrayOperations(container, fieldArray, cleanups) {
|
|
|
1718
2069
|
export function bind(root, ctx, options = {}) {
|
|
1719
2070
|
const events = options.events ?? DEFAULT_EVENTS;
|
|
1720
2071
|
const rawTextSelectors = options.rawTextSelectors ?? DEFAULT_RAW_TEXT_SELECTORS;
|
|
2072
|
+
const templatePlanCacheConfig = resolveTemplatePlanCacheConfig(options);
|
|
2073
|
+
const benchSession = createBindBenchSession();
|
|
1721
2074
|
const htmlRoot = root;
|
|
1722
2075
|
// HMR support: Register binding context globally in dev mode.
|
|
1723
2076
|
// Skip for internal (d-each clone) bindings — only the top-level bind owns HMR.
|
|
@@ -1735,33 +2088,8 @@ export function bind(root, ctx, options = {}) {
|
|
|
1735
2088
|
bindArray(root, ctx, cleanups);
|
|
1736
2089
|
// 3. d-each — must run early: removes templates before TreeWalker visits them
|
|
1737
2090
|
bindEach(root, ctx, cleanups);
|
|
1738
|
-
// 4. Text interpolation
|
|
1739
|
-
|
|
1740
|
-
const textNodes = [];
|
|
1741
|
-
// Same boundary logic as qsaIncludingRoot: only visit text nodes that
|
|
1742
|
-
// belong to this bind scope, not to nested already-bound subtrees.
|
|
1743
|
-
const textBoundary = root.closest('[data-dalila-internal-bound]');
|
|
1744
|
-
while (walker.nextNode()) {
|
|
1745
|
-
const node = walker.currentNode;
|
|
1746
|
-
const parent = node.parentElement;
|
|
1747
|
-
// Skip nodes inside raw text containers
|
|
1748
|
-
if (parent && parent.closest(rawTextSelectors)) {
|
|
1749
|
-
continue;
|
|
1750
|
-
}
|
|
1751
|
-
// Skip nodes inside already-bound subtrees (d-each clones)
|
|
1752
|
-
if (parent) {
|
|
1753
|
-
const bound = parent.closest('[data-dalila-internal-bound]');
|
|
1754
|
-
if (bound !== textBoundary)
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
if (node.data.includes('{')) {
|
|
1758
|
-
textNodes.push(node);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
// Process text nodes (collect first, then process to avoid walker issues)
|
|
1762
|
-
for (const node of textNodes) {
|
|
1763
|
-
bindTextNode(node, ctx, cleanups);
|
|
1764
|
-
}
|
|
2091
|
+
// 4. Text interpolation (template plan cache + lazy parser fallback)
|
|
2092
|
+
bindTextInterpolation(root, ctx, rawTextSelectors, templatePlanCacheConfig, benchSession);
|
|
1765
2093
|
// 5. d-attr bindings
|
|
1766
2094
|
bindAttrs(root, ctx, cleanups);
|
|
1767
2095
|
// 6. d-html bindings
|
|
@@ -1788,6 +2116,7 @@ export function bind(root, ctx, options = {}) {
|
|
|
1788
2116
|
htmlRoot.setAttribute('d-ready', '');
|
|
1789
2117
|
});
|
|
1790
2118
|
}
|
|
2119
|
+
flushBindBenchSession(benchSession);
|
|
1791
2120
|
// Return dispose function
|
|
1792
2121
|
return () => {
|
|
1793
2122
|
// Run manual cleanups (event listeners)
|
package/package.json
CHANGED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { signal, computed } from "../../../core/signal.js";
|
|
2
|
-
import { getCurrentScope } from "../../../core/scope.js";
|
|
3
|
-
export function createAccordion(options = {}) {
|
|
4
|
-
const { single = false, initial = [] } = options;
|
|
5
|
-
const hasInitial = Object.prototype.hasOwnProperty.call(options, "initial");
|
|
6
|
-
const seededInitial = single ? initial.slice(0, 1) : initial;
|
|
7
|
-
const openItems = signal(new Set(seededInitial));
|
|
8
|
-
const toggle = (itemId) => {
|
|
9
|
-
openItems.update((current) => {
|
|
10
|
-
const next = new Set(current);
|
|
11
|
-
if (next.has(itemId)) {
|
|
12
|
-
next.delete(itemId);
|
|
13
|
-
}
|
|
14
|
-
else {
|
|
15
|
-
if (single)
|
|
16
|
-
next.clear();
|
|
17
|
-
next.add(itemId);
|
|
18
|
-
}
|
|
19
|
-
return next;
|
|
20
|
-
});
|
|
21
|
-
};
|
|
22
|
-
const open = (itemId) => {
|
|
23
|
-
openItems.update((current) => {
|
|
24
|
-
const next = new Set(current);
|
|
25
|
-
if (single)
|
|
26
|
-
next.clear();
|
|
27
|
-
next.add(itemId);
|
|
28
|
-
return next;
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
const close = (itemId) => {
|
|
32
|
-
openItems.update((current) => {
|
|
33
|
-
const next = new Set(current);
|
|
34
|
-
next.delete(itemId);
|
|
35
|
-
return next;
|
|
36
|
-
});
|
|
37
|
-
};
|
|
38
|
-
const _isOpenCache = new Map();
|
|
39
|
-
const isOpen = (itemId) => {
|
|
40
|
-
let sig = _isOpenCache.get(itemId);
|
|
41
|
-
if (!sig) {
|
|
42
|
-
sig = computed(() => openItems().has(itemId));
|
|
43
|
-
_isOpenCache.set(itemId, sig);
|
|
44
|
-
}
|
|
45
|
-
return sig;
|
|
46
|
-
};
|
|
47
|
-
const _attachTo = (el) => {
|
|
48
|
-
const scope = getCurrentScope();
|
|
49
|
-
let syncing = false;
|
|
50
|
-
const allDetails = () => Array.from(el.querySelectorAll("details[data-accordion]"));
|
|
51
|
-
const syncDOMFromSignal = (set) => {
|
|
52
|
-
syncing = true;
|
|
53
|
-
for (const details of allDetails()) {
|
|
54
|
-
const itemId = details.dataset.accordion;
|
|
55
|
-
if (!itemId)
|
|
56
|
-
continue;
|
|
57
|
-
details.open = set.has(itemId);
|
|
58
|
-
}
|
|
59
|
-
syncing = false;
|
|
60
|
-
};
|
|
61
|
-
const syncSignalFromDOM = () => {
|
|
62
|
-
const next = new Set();
|
|
63
|
-
for (const details of allDetails()) {
|
|
64
|
-
if (!details.open)
|
|
65
|
-
continue;
|
|
66
|
-
const itemId = details.dataset.accordion;
|
|
67
|
-
if (!itemId)
|
|
68
|
-
continue;
|
|
69
|
-
if (single) {
|
|
70
|
-
next.clear();
|
|
71
|
-
}
|
|
72
|
-
next.add(itemId);
|
|
73
|
-
}
|
|
74
|
-
openItems.set(next);
|
|
75
|
-
};
|
|
76
|
-
const onToggle = (ev) => {
|
|
77
|
-
const details = ev.target;
|
|
78
|
-
if (syncing)
|
|
79
|
-
return;
|
|
80
|
-
const itemId = details.dataset.accordion;
|
|
81
|
-
if (!itemId)
|
|
82
|
-
return;
|
|
83
|
-
openItems.update((current) => {
|
|
84
|
-
const next = new Set(current);
|
|
85
|
-
if (details.open) {
|
|
86
|
-
if (single)
|
|
87
|
-
next.clear();
|
|
88
|
-
next.add(itemId);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
next.delete(itemId);
|
|
92
|
-
}
|
|
93
|
-
return next;
|
|
94
|
-
});
|
|
95
|
-
};
|
|
96
|
-
const unsub = openItems.on((set) => {
|
|
97
|
-
if (!syncing)
|
|
98
|
-
syncDOMFromSignal(set);
|
|
99
|
-
});
|
|
100
|
-
// If initial was not provided, respect current DOM open state.
|
|
101
|
-
if (!hasInitial)
|
|
102
|
-
syncSignalFromDOM();
|
|
103
|
-
else
|
|
104
|
-
syncDOMFromSignal(openItems());
|
|
105
|
-
el.addEventListener("toggle", onToggle, true);
|
|
106
|
-
if (scope) {
|
|
107
|
-
scope.onCleanup(() => {
|
|
108
|
-
unsub();
|
|
109
|
-
el.removeEventListener("toggle", onToggle, true);
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
return { openItems, toggle, open, close, isOpen, _attachTo };
|
|
114
|
-
}
|