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.
Files changed (89) hide show
  1. package/dist/cli/routes-generator.js +1 -2
  2. package/dist/components/ui/dialog/index.d.ts +0 -8
  3. package/dist/components/ui/dialog/index.js +2 -41
  4. package/dist/components/ui/dialog/internal.d.ts +5 -0
  5. package/dist/{componentes/ui/dialog/index.js → components/ui/dialog/internal.js} +1 -23
  6. package/dist/components/ui/drawer/index.js +2 -2
  7. package/dist/components/ui/index.d.ts +1 -1
  8. package/dist/components/ui/index.js +1 -1
  9. package/dist/core/index.d.ts +1 -1
  10. package/dist/core/index.js +1 -1
  11. package/dist/core/query.js +9 -7
  12. package/dist/core/resource.d.ts +23 -171
  13. package/dist/core/resource.js +178 -15
  14. package/dist/form/index.d.ts +1 -1
  15. package/dist/form/index.js +1 -1
  16. package/dist/router/index.d.ts +1 -1
  17. package/dist/router/index.js +1 -1
  18. package/dist/runtime/bind.d.ts +10 -0
  19. package/dist/runtime/bind.js +382 -53
  20. package/package.json +1 -1
  21. package/dist/componentes/ui/accordion/index.d.ts +0 -2
  22. package/dist/componentes/ui/accordion/index.js +0 -114
  23. package/dist/componentes/ui/calendar/index.d.ts +0 -2
  24. package/dist/componentes/ui/calendar/index.js +0 -132
  25. package/dist/componentes/ui/combobox/index.d.ts +0 -2
  26. package/dist/componentes/ui/combobox/index.js +0 -161
  27. package/dist/componentes/ui/dialog/index.d.ts +0 -10
  28. package/dist/componentes/ui/drawer/index.d.ts +0 -2
  29. package/dist/componentes/ui/drawer/index.js +0 -41
  30. package/dist/componentes/ui/dropdown/index.d.ts +0 -2
  31. package/dist/componentes/ui/dropdown/index.js +0 -48
  32. package/dist/componentes/ui/dropzone/index.d.ts +0 -2
  33. package/dist/componentes/ui/dropzone/index.js +0 -92
  34. package/dist/componentes/ui/env.d.ts +0 -1
  35. package/dist/componentes/ui/env.js +0 -2
  36. package/dist/componentes/ui/index.d.ts +0 -13
  37. package/dist/componentes/ui/index.js +0 -12
  38. package/dist/componentes/ui/popover/index.d.ts +0 -2
  39. package/dist/componentes/ui/popover/index.js +0 -156
  40. package/dist/componentes/ui/runtime.d.ts +0 -20
  41. package/dist/componentes/ui/runtime.js +0 -421
  42. package/dist/componentes/ui/tabs/index.d.ts +0 -3
  43. package/dist/componentes/ui/tabs/index.js +0 -101
  44. package/dist/componentes/ui/toast/index.d.ts +0 -3
  45. package/dist/componentes/ui/toast/index.js +0 -115
  46. package/dist/componentes/ui/ui-types.d.ts +0 -175
  47. package/dist/componentes/ui/ui-types.js +0 -1
  48. package/dist/componentes/ui/validate.d.ts +0 -7
  49. package/dist/componentes/ui/validate.js +0 -71
  50. package/dist/core/store.d.ts +0 -130
  51. package/dist/core/store.js +0 -234
  52. package/dist/core/virtual.d.ts +0 -26
  53. package/dist/core/virtual.js +0 -277
  54. package/dist/core/watch-testing.d.ts +0 -13
  55. package/dist/core/watch-testing.js +0 -16
  56. package/dist/router/route.d.ts +0 -23
  57. package/dist/router/route.js +0 -48
  58. package/dist/simple.d.ts +0 -11
  59. package/dist/simple.js +0 -11
  60. package/dist/ui/accordion.d.ts +0 -2
  61. package/dist/ui/accordion.js +0 -114
  62. package/dist/ui/calendar.d.ts +0 -2
  63. package/dist/ui/calendar.js +0 -132
  64. package/dist/ui/combobox.d.ts +0 -2
  65. package/dist/ui/combobox.js +0 -161
  66. package/dist/ui/dialog.d.ts +0 -10
  67. package/dist/ui/dialog.js +0 -54
  68. package/dist/ui/drawer.d.ts +0 -2
  69. package/dist/ui/drawer.js +0 -41
  70. package/dist/ui/dropdown.d.ts +0 -2
  71. package/dist/ui/dropdown.js +0 -48
  72. package/dist/ui/dropzone.d.ts +0 -2
  73. package/dist/ui/dropzone.js +0 -92
  74. package/dist/ui/env.d.ts +0 -1
  75. package/dist/ui/env.js +0 -2
  76. package/dist/ui/index.d.ts +0 -13
  77. package/dist/ui/index.js +0 -12
  78. package/dist/ui/popover.d.ts +0 -2
  79. package/dist/ui/popover.js +0 -156
  80. package/dist/ui/runtime.d.ts +0 -20
  81. package/dist/ui/runtime.js +0 -421
  82. package/dist/ui/tabs.d.ts +0 -3
  83. package/dist/ui/tabs.js +0 -101
  84. package/dist/ui/toast.d.ts +0 -3
  85. package/dist/ui/toast.js +0 -115
  86. package/dist/ui/ui-types.d.ts +0 -175
  87. package/dist/ui/ui-types.js +0 -1
  88. package/dist/ui/validate.d.ts +0 -7
  89. package/dist/ui/validate.js +0 -71
@@ -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/index.js';
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 parseInterpolationExpression(expression) {
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
- * Process a text node and replace {tokens} with reactive bindings
735
+ * Build interpolation segments for one text node.
580
736
  */
581
- function bindTextNode(node, ctx, cleanups) {
582
- const text = node.data;
737
+ function buildTextInterpolationSegments(text) {
583
738
  const regex = /\{([^{}]+)\}/g;
584
- // Check if there are any tokens
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
- frag.appendChild(document.createTextNode(before));
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
- const binding = ctx[bindingName];
1134
+ let binding = ctx[bindingName];
784
1135
  if (binding === undefined) {
785
1136
  warn(`d-each: "${bindingName}" not found in context`);
786
- continue;
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
- const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,2 +0,0 @@
1
- import type { Accordion, AccordionOptions } from "../ui-types.js";
2
- export declare function createAccordion(options?: AccordionOptions): Accordion;
@@ -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
- }
@@ -1,2 +0,0 @@
1
- import type { Calendar, CalendarOptions } from "../ui-types.js";
2
- export declare function createCalendar(options?: CalendarOptions): Calendar;