@repobit/dex-store-elements 1.7.2 → 2.0.0

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 (227) hide show
  1. package/CHANGELOG.md +5 -905
  2. package/README.md +9 -7
  3. package/dist/src/actions/action.adapter.d.ts +36 -0
  4. package/dist/src/actions/action.adapter.js +183 -0
  5. package/dist/src/actions/action.adapter.js.map +1 -0
  6. package/dist/src/actions/action.attributes.d.ts +15 -0
  7. package/dist/src/actions/action.attributes.js +137 -0
  8. package/dist/src/actions/action.attributes.js.map +1 -0
  9. package/dist/src/actions/action.registry.d.ts +4 -0
  10. package/dist/src/actions/action.registry.js +119 -0
  11. package/dist/src/actions/action.registry.js.map +1 -0
  12. package/dist/src/actions/index.d.ts +3 -2
  13. package/dist/src/actions/index.js +3 -80
  14. package/dist/src/actions/index.js.map +1 -1
  15. package/dist/src/compute/compute.expand.d.ts +70 -0
  16. package/dist/src/compute/compute.expand.js +319 -0
  17. package/dist/src/compute/compute.expand.js.map +1 -0
  18. package/dist/src/compute/compute.scan.d.ts +54 -0
  19. package/dist/src/compute/compute.scan.js +160 -0
  20. package/dist/src/compute/compute.scan.js.map +1 -0
  21. package/dist/src/compute/option-resolver.d.ts +31 -0
  22. package/dist/src/compute/option-resolver.js +147 -0
  23. package/dist/src/compute/option-resolver.js.map +1 -0
  24. package/dist/src/contexts/context.compute.d.ts +12 -0
  25. package/dist/src/contexts/context.compute.js +4 -0
  26. package/dist/src/contexts/context.compute.js.map +1 -0
  27. package/dist/src/contexts/context.scope.d.ts +139 -0
  28. package/dist/src/contexts/context.scope.js +48 -0
  29. package/dist/src/contexts/context.scope.js.map +1 -0
  30. package/dist/src/controllers/controller.action-resolve.d.ts +110 -0
  31. package/dist/src/controllers/controller.action-resolve.js +203 -0
  32. package/dist/src/controllers/controller.action-resolve.js.map +1 -0
  33. package/dist/src/controllers/controller.action-topology.d.ts +59 -0
  34. package/dist/src/controllers/controller.action-topology.js +130 -0
  35. package/dist/src/controllers/controller.action-topology.js.map +1 -0
  36. package/dist/src/controllers/controller.compute.d.ts +72 -0
  37. package/dist/src/controllers/controller.compute.js +110 -0
  38. package/dist/src/controllers/controller.compute.js.map +1 -0
  39. package/dist/src/controllers/controller.resolve.d.ts +78 -0
  40. package/dist/src/controllers/controller.resolve.js +205 -0
  41. package/dist/src/controllers/controller.resolve.js.map +1 -0
  42. package/dist/src/core/action-topology.d.ts +62 -0
  43. package/dist/src/core/action-topology.js +67 -0
  44. package/dist/src/core/action-topology.js.map +1 -0
  45. package/dist/src/core/actions.d.ts +64 -0
  46. package/dist/src/core/actions.js +47 -0
  47. package/dist/src/core/actions.js.map +1 -0
  48. package/dist/src/core/node.element.d.ts +179 -0
  49. package/dist/src/core/node.element.js +573 -0
  50. package/dist/src/core/node.element.js.map +1 -0
  51. package/dist/src/core/node.scope.d.ts +374 -0
  52. package/dist/src/core/node.scope.js +558 -0
  53. package/dist/src/core/node.scope.js.map +1 -0
  54. package/dist/src/core/option-update.d.ts +11 -0
  55. package/dist/src/core/option-update.js +59 -0
  56. package/dist/src/core/option-update.js.map +1 -0
  57. package/dist/src/dsl/compilers/array/compiler.js +13 -14
  58. package/dist/src/dsl/compilers/array/compiler.js.map +1 -1
  59. package/dist/src/dsl/compilers/boolean/compiler.d.ts +1 -1
  60. package/dist/src/dsl/compilers/boolean/compiler.js +14 -27
  61. package/dist/src/dsl/compilers/boolean/compiler.js.map +1 -1
  62. package/dist/src/dsl/compilers/enum/compiler.js +7 -10
  63. package/dist/src/dsl/compilers/enum/compiler.js.map +1 -1
  64. package/dist/src/dsl/compilers/index.d.ts +1 -1
  65. package/dist/src/dsl/compilers/index.js +5 -5
  66. package/dist/src/dsl/compilers/index.js.map +1 -1
  67. package/dist/src/dsl/utilty.d.ts +0 -4
  68. package/dist/src/dsl/utilty.js +3 -30
  69. package/dist/src/dsl/utilty.js.map +1 -1
  70. package/dist/src/eta/batch.d.ts +123 -0
  71. package/dist/src/eta/batch.js +162 -0
  72. package/dist/src/eta/batch.js.map +1 -0
  73. package/dist/src/eta/batcher.d.ts +184 -0
  74. package/dist/src/eta/batcher.js +335 -0
  75. package/dist/src/eta/batcher.js.map +1 -0
  76. package/dist/src/eta/cache.d.ts +36 -0
  77. package/dist/src/eta/cache.js +56 -0
  78. package/dist/src/eta/cache.js.map +1 -0
  79. package/dist/src/eta/controller.d.ts +608 -0
  80. package/dist/src/eta/controller.js +1036 -0
  81. package/dist/src/eta/controller.js.map +1 -0
  82. package/dist/src/eta/factory.d.ts +74 -0
  83. package/dist/src/eta/factory.js +121 -0
  84. package/dist/src/eta/factory.js.map +1 -0
  85. package/dist/src/eta/idle.d.ts +39 -0
  86. package/dist/src/eta/idle.js +52 -0
  87. package/dist/src/eta/idle.js.map +1 -0
  88. package/dist/src/eta/index.d.ts +52 -0
  89. package/dist/src/eta/index.js +13 -0
  90. package/dist/src/eta/index.js.map +1 -0
  91. package/dist/src/eta/observer.d.ts +263 -0
  92. package/dist/src/eta/observer.js +263 -0
  93. package/dist/src/eta/observer.js.map +1 -0
  94. package/dist/src/eta/operations.d.ts +41 -0
  95. package/dist/src/eta/operations.js +59 -0
  96. package/dist/src/eta/operations.js.map +1 -0
  97. package/dist/src/eta/source-registry.d.ts +198 -0
  98. package/dist/src/eta/source-registry.js +267 -0
  99. package/dist/src/eta/source-registry.js.map +1 -0
  100. package/dist/src/eta/template-index.d.ts +285 -0
  101. package/dist/src/eta/template-index.js +642 -0
  102. package/dist/src/eta/template-index.js.map +1 -0
  103. package/dist/src/eta/types.d.ts +529 -0
  104. package/dist/src/eta/types.js +2 -0
  105. package/dist/src/eta/types.js.map +1 -0
  106. package/dist/src/index.d.ts +21 -9
  107. package/dist/src/index.js +22 -11
  108. package/dist/src/index.js.map +1 -1
  109. package/dist/src/nodes/node.context.d.ts +5 -3
  110. package/dist/src/nodes/node.context.js +48 -3
  111. package/dist/src/nodes/node.context.js.map +1 -1
  112. package/dist/src/nodes/node.option.d.ts +11 -31
  113. package/dist/src/nodes/node.option.js +158 -222
  114. package/dist/src/nodes/node.option.js.map +1 -1
  115. package/dist/src/nodes/node.product.d.ts +7 -11
  116. package/dist/src/nodes/node.product.js +85 -64
  117. package/dist/src/nodes/node.product.js.map +1 -1
  118. package/dist/src/renders/attributes/buyLink.d.ts +2 -2
  119. package/dist/src/renders/attributes/buyLink.js +14 -11
  120. package/dist/src/renders/attributes/buyLink.js.map +1 -1
  121. package/dist/src/renders/attributes/devices.d.ts +2 -2
  122. package/dist/src/renders/attributes/devices.js +9 -19
  123. package/dist/src/renders/attributes/devices.js.map +1 -1
  124. package/dist/src/renders/attributes/discount.d.ts +2 -2
  125. package/dist/src/renders/attributes/discount.js +56 -44
  126. package/dist/src/renders/attributes/discount.js.map +1 -1
  127. package/dist/src/renders/attributes/hide.d.ts +2 -2
  128. package/dist/src/renders/attributes/hide.js +49 -14
  129. package/dist/src/renders/attributes/hide.js.map +1 -1
  130. package/dist/src/renders/attributes/index.d.ts +3 -3
  131. package/dist/src/renders/attributes/index.js +2 -4
  132. package/dist/src/renders/attributes/index.js.map +1 -1
  133. package/dist/src/renders/attributes/price.d.ts +2 -2
  134. package/dist/src/renders/attributes/price.js +30 -44
  135. package/dist/src/renders/attributes/price.js.map +1 -1
  136. package/dist/src/renders/attributes/subscription.d.ts +2 -2
  137. package/dist/src/renders/attributes/subscription.js +14 -20
  138. package/dist/src/renders/attributes/subscription.js.map +1 -1
  139. package/dist/src/renders/attributes/utilty.d.ts +2 -0
  140. package/dist/src/renders/attributes/utilty.js +17 -4
  141. package/dist/src/renders/attributes/utilty.js.map +1 -1
  142. package/dist/src/renders/context.d.ts +122 -10
  143. package/dist/src/renders/context.js +140 -80
  144. package/dist/src/renders/context.js.map +1 -1
  145. package/dist/src/renders/format.js +2 -2
  146. package/dist/src/renders/format.js.map +1 -1
  147. package/dist/src/renders/index.d.ts +6 -2
  148. package/dist/src/renders/index.js +67 -29
  149. package/dist/src/renders/index.js.map +1 -1
  150. package/dist/src/renders/observe.d.ts +1 -4
  151. package/dist/src/renders/observe.js +10 -24
  152. package/dist/src/renders/observe.js.map +1 -1
  153. package/dist/src/renders/render.wrapper.d.ts +37 -0
  154. package/dist/src/renders/render.wrapper.js +147 -0
  155. package/dist/src/renders/render.wrapper.js.map +1 -0
  156. package/dist/src/templating/eta.js +1 -4
  157. package/dist/src/templating/eta.js.map +1 -1
  158. package/package.json +4 -5
  159. package/dist/src/actions/action.button.d.ts +0 -2
  160. package/dist/src/actions/action.button.js +0 -8
  161. package/dist/src/actions/action.button.js.map +0 -1
  162. package/dist/src/actions/action.input.d.ts +0 -2
  163. package/dist/src/actions/action.input.js +0 -43
  164. package/dist/src/actions/action.input.js.map +0 -1
  165. package/dist/src/actions/action.select.d.ts +0 -2
  166. package/dist/src/actions/action.select.js +0 -14
  167. package/dist/src/actions/action.select.js.map +0 -1
  168. package/dist/src/actions/utilty.d.ts +0 -16
  169. package/dist/src/actions/utilty.js +0 -149
  170. package/dist/src/actions/utilty.js.map +0 -1
  171. package/dist/src/contexts/context.datalayer.d.ts +0 -9
  172. package/dist/src/contexts/context.datalayer.js +0 -3
  173. package/dist/src/contexts/context.datalayer.js.map +0 -1
  174. package/dist/src/contexts/context.derived.d.ts +0 -12
  175. package/dist/src/contexts/context.derived.js +0 -3
  176. package/dist/src/contexts/context.derived.js.map +0 -1
  177. package/dist/src/contexts/context.option.d.ts +0 -5
  178. package/dist/src/contexts/context.option.js +0 -3
  179. package/dist/src/contexts/context.option.js.map +0 -1
  180. package/dist/src/contexts/context.product.d.ts +0 -5
  181. package/dist/src/contexts/context.product.js +0 -3
  182. package/dist/src/contexts/context.product.js.map +0 -1
  183. package/dist/src/contexts/context.state.d.ts +0 -28
  184. package/dist/src/contexts/context.state.js +0 -3
  185. package/dist/src/contexts/context.state.js.map +0 -1
  186. package/dist/src/contexts/context.store.d.ts +0 -5
  187. package/dist/src/contexts/context.store.js +0 -3
  188. package/dist/src/contexts/context.store.js.map +0 -1
  189. package/dist/src/controllers/collect-controller.d.ts +0 -39
  190. package/dist/src/controllers/collect-controller.js +0 -161
  191. package/dist/src/controllers/collect-controller.js.map +0 -1
  192. package/dist/src/controllers/compute-controller.d.ts +0 -55
  193. package/dist/src/controllers/compute-controller.js +0 -350
  194. package/dist/src/controllers/compute-controller.js.map +0 -1
  195. package/dist/src/controllers/eta-renderer/eta-dom-renderer.d.ts +0 -25
  196. package/dist/src/controllers/eta-renderer/eta-dom-renderer.js +0 -192
  197. package/dist/src/controllers/eta-renderer/eta-dom-renderer.js.map +0 -1
  198. package/dist/src/controllers/eta-renderer/eta-mutation-observer.d.ts +0 -28
  199. package/dist/src/controllers/eta-renderer/eta-mutation-observer.js +0 -206
  200. package/dist/src/controllers/eta-renderer/eta-mutation-observer.js.map +0 -1
  201. package/dist/src/controllers/eta-renderer/eta-renderer.d.ts +0 -21
  202. package/dist/src/controllers/eta-renderer/eta-renderer.js +0 -77
  203. package/dist/src/controllers/eta-renderer/eta-renderer.js.map +0 -1
  204. package/dist/src/controllers/eta-renderer/eta-template-cache.d.ts +0 -24
  205. package/dist/src/controllers/eta-renderer/eta-template-cache.js +0 -61
  206. package/dist/src/controllers/eta-renderer/eta-template-cache.js.map +0 -1
  207. package/dist/src/controllers/eta-renderer/eta-types.d.ts +0 -11
  208. package/dist/src/controllers/eta-renderer/eta-types.js +0 -1
  209. package/dist/src/controllers/eta-renderer/eta-types.js.map +0 -1
  210. package/dist/src/controllers/eta-renderer/index.d.ts +0 -2
  211. package/dist/src/controllers/eta-renderer/index.js +0 -2
  212. package/dist/src/controllers/eta-renderer/index.js.map +0 -1
  213. package/dist/src/controllers/event-pipeline-controller.d.ts +0 -28
  214. package/dist/src/controllers/event-pipeline-controller.js +0 -72
  215. package/dist/src/controllers/event-pipeline-controller.js.map +0 -1
  216. package/dist/src/events/events.d.ts +0 -75
  217. package/dist/src/events/events.js +0 -78
  218. package/dist/src/events/events.js.map +0 -1
  219. package/dist/src/nodes/node.root.d.ts +0 -16
  220. package/dist/src/nodes/node.root.js +0 -49
  221. package/dist/src/nodes/node.root.js.map +0 -1
  222. package/dist/src/nodes/node.state.d.ts +0 -88
  223. package/dist/src/nodes/node.state.js +0 -330
  224. package/dist/src/nodes/node.state.js.map +0 -1
  225. package/dist/src/renders/utility.d.ts +0 -32
  226. package/dist/src/renders/utility.js +0 -191
  227. package/dist/src/renders/utility.js.map +0 -1
@@ -0,0 +1,642 @@
1
+ // src/eta/template-index.ts
2
+ import { createEtaTemplateSourceRegistry } from './source-registry.js';
3
+ /**
4
+ * The EtaTemplateIndex maintains the set of renderable operations for a
5
+ * scoped boundary.
6
+ *
7
+ * It is Eta-agnostic. Discovery is delegated to a TemplateOperationFactory
8
+ * which returns either an operation or `undefined`. The index simply stores
9
+ * whatever the factory produces and looks it up by node for incremental
10
+ * updates.
11
+ *
12
+ * Three views of the data are kept in sync:
13
+ *
14
+ * - `operations` — a flat Set of operations iterated during render.
15
+ * - `nodeIndex` — a WeakMap from owning node (Element or Text) to the
16
+ * operations that own it, used for O(1) removal.
17
+ * - `attrOps` — a WeakMap from Element to a Map of `attributeName` to the
18
+ * single operation that targets that attribute. Needed because Attr
19
+ * identity is unstable across `setAttribute` calls — looking up by the
20
+ * current Attr reference would never find the previous operation.
21
+ *
22
+ * Boundary handling is configurable through an injected predicate
23
+ * (`EtaTemplateIndexOptions.isBoundaryElement`). The index itself does
24
+ * not own boundary semantics — it asks the host/controller for a
25
+ * decision. That makes boundary detection work consistently across
26
+ * controllers that share the same root but own different indices.
27
+ *
28
+ * Source preservation is delegated to an
29
+ * `EtaTemplateSourceRegistry` so the factory can re-read original
30
+ * template sources after a render has overwritten the DOM value.
31
+ *
32
+ * **Cross-scope source registry limitation.** Each `EtaTemplateIndex`
33
+ * owns the source registry it created in its constructor
34
+ * (`createEtaTemplateSourceRegistry()`). Moving a rendered
35
+ * template node from one scope to another loses the original
36
+ * template source — the destination index's registry has no
37
+ * record of it, so the factory falls back to the live DOM value,
38
+ * which now holds the rendered string rather than the template
39
+ * literal. The destination index cannot recreate the operation
40
+ * without discovering the new template content externally.
41
+ *
42
+ * Applications that need to reparent rendered template nodes
43
+ * across scopes should share a single
44
+ * `EtaTemplateSourceRegistry` between the affected indices.
45
+ * Because the factory is stateless at the call site (the
46
+ * registry is threaded through every call via
47
+ * `TemplateOperationContext`), two indices can share one factory
48
+ * safely — each just passes the shared registry as the
49
+ * `sources` field on every call. The v4 review recommends
50
+ * keeping this limitation documented unless cross-scope DOM
51
+ * moves become a real use case — at which point the
52
+ * index/controller can grow a `sourceRegistry` option that
53
+ * overrides the per-index `createEtaTemplateSourceRegistry()`
54
+ * default.
55
+ */
56
+ export class EtaTemplateIndex {
57
+ constructor(factory, options = {}) {
58
+ this._operations = new Set();
59
+ /**
60
+ * Per-node operation lookup.
61
+ *
62
+ * Reassignable so `clear()` can fully reset the index even though
63
+ * `WeakMap` cannot be cleared. The old map is reclaimed by the GC
64
+ * as soon as no caller holds it.
65
+ */
66
+ this._nodeIndex = new WeakMap();
67
+ /**
68
+ * Per-element, per-attribute-name operation lookup.
69
+ *
70
+ * `Attr` identity is not stable across DOM mutations (`setAttribute`
71
+ * removes the old Attr and inserts a new one), so we cannot key attribute
72
+ * operations by their Attr reference. Instead we key by the owning
73
+ * element + the attribute's qualified name. Each (element, name) pair
74
+ * owns at most one operation because the factory only ever produces one
75
+ * AttributeOperation per attribute.
76
+ */
77
+ this._attrOps = new WeakMap();
78
+ /**
79
+ * Tracks nodes that have at least one operation associated with them.
80
+ *
81
+ * `WeakSet` cannot be iterated and cannot be cleared, so it is
82
+ * reassigned by `clear()`. It only needs `add` / `has` / `delete`,
83
+ * which `WeakSet` supports.
84
+ */
85
+ this._indexedNodes = new WeakSet();
86
+ this.factory = factory;
87
+ this.options = options;
88
+ this.sources = createEtaTemplateSourceRegistry();
89
+ // The source registry is threaded through every factory call
90
+ // via the `TemplateOperationContext` parameter, NOT wired into
91
+ // the factory itself. That keeps the factory stateless at the
92
+ // call site and makes factory reuse across indices safe — the
93
+ // last constructor to call a registry setter no longer wins,
94
+ // because there is no setter to call. See `_addNode`,
95
+ // `_collectAttributes`, `_updateAttribute`, and `_updateText`
96
+ // for the call sites that pass `{ sources: this.sources }`.
97
+ }
98
+ /**
99
+ * Walks the subtree of `root` and indexes every node that contains a
100
+ * template.
101
+ *
102
+ * The walk is implemented by `_addNode`, which descends into
103
+ * descendants and respects the `isBoundaryElement` predicate. The
104
+ * root element is added first so a root element's own attribute
105
+ * templates are indexed even when it has no child nodes.
106
+ *
107
+ * Boundary semantics:
108
+ *
109
+ * - If `root` itself matches `isBoundaryElement`, it is still
110
+ * indexed as the local boundary. Its own attribute templates and
111
+ * its owned descendants are collected.
112
+ * - If a nested element matches `isBoundaryElement`, the walk
113
+ * neither collects the foreign boundary's own attributes nor
114
+ * descends into the foreign boundary's subtree. Outer
115
+ * controllers leave that subtree entirely to the boundary's
116
+ * own controller.
117
+ *
118
+ * ⚠️ **Stable-on-raw-DOM invariant for nested boundaries.**
119
+ * When `initialize` encounters a nested boundary element, the
120
+ * `isBoundaryElement` predicate MUST classify it as a foreign
121
+ * boundary **without** relying on prototype-side state such as
122
+ * `instanceof BdScopedElement` or a runtime brand. The parent
123
+ * index may scan a subtree before a nested custom element has
124
+ * upgraded; in that window the nested element has not yet been
125
+ * augmented with the brand, and a brand-only predicate would
126
+ * fail to classify it as a boundary. No DOM mutation fires when
127
+ * the upgrade completes, so the parent index would keep the
128
+ * wrong operations for the lifetime of the page. Prefer
129
+ * tag-name or marker-attribute checks so the classification is
130
+ * stable from the moment the element is parsed. See
131
+ * `EtaTemplateIndexOptions.isBoundaryElement` for full
132
+ * rationale and example patterns.
133
+ *
134
+ * ⚠️ **Source preservation trust boundary.** `initialize()`
135
+ * assumes external DOM edits have been observed while the
136
+ * index was online. During normal operation the
137
+ * `MutationObserver` keeps the `EtaTemplateSourceRegistry` in
138
+ * sync via `syncTextSource()` / `syncAttributeSource()`. If
139
+ * external code mutates already-rendered template nodes while
140
+ * the controller is disconnected or the index has been cleared
141
+ * (for example between two `clear()` / `initialize()` cycles),
142
+ * the registry can hand back a stale preserved source and the
143
+ * next render will re-emit the old template literal instead of
144
+ * the new DOM value. Callers in that situation should `reset()`
145
+ * (which also clears the source registry) before
146
+ * re-initializing, or otherwise clear the registry themselves.
147
+ */
148
+ initialize(root) {
149
+ this.clear();
150
+ this._addNode(root);
151
+ }
152
+ /**
153
+ * Applies a single mutation to the index.
154
+ *
155
+ * Returns `true` when the index actually changed as a result of
156
+ * the mutation (an operation was added, replaced, or removed);
157
+ * `false` when the mutation was a no-op for this index because
158
+ * the node was outside the root, inside a foreign boundary, or
159
+ * did not contain a template.
160
+ *
161
+ * Mutation semantics:
162
+ *
163
+ * - `add` walks the node and its descendants.
164
+ * - `remove` drops every operation whose owner is the node or one of its
165
+ * descendants.
166
+ * - `attribute` re-runs attribute discovery on a single attribute of an
167
+ * element that is already part of the index.
168
+ * - `text` re-runs text discovery on a text node that is already part
169
+ * of the index.
170
+ */
171
+ apply(mutation) {
172
+ switch (mutation.type) {
173
+ case 'add':
174
+ return this._addNode(mutation.node);
175
+ case 'remove':
176
+ return this._removeNode(mutation.node);
177
+ case 'attribute':
178
+ return this._updateAttribute(mutation.element, mutation.attribute);
179
+ case 'text':
180
+ return this._updateText(mutation.node);
181
+ // Exhaustive guard: if a new mutation variant is added,
182
+ // TypeScript flags this `_exhaustive` assignment as a
183
+ // type error until the new variant is handled here.
184
+ default: {
185
+ const _exhaustive = mutation;
186
+ return _exhaustive;
187
+ }
188
+ }
189
+ }
190
+ /**
191
+ * Drops every operation from the index and forgets every node.
192
+ *
193
+ * Every internal structure — including the `WeakMap` / `WeakSet`
194
+ * instances — is reassigned so the index returns to a fully empty
195
+ * state. The old `WeakMap` / `WeakSet` references become unreachable
196
+ * as soon as `clear()` returns and the GC reclaims them.
197
+ *
198
+ * **Source registry is intentionally preserved.** The
199
+ * `EtaTemplateSourceRegistry` keeps the original template source
200
+ * strings for the live DOM nodes so the factory can re-render the
201
+ * same nodes after a render has overwritten their text / attribute
202
+ * values. Clearing the registry here would make
203
+ * `clear()` + `initialize()` destructive on already-rendered
204
+ * DOM — the next render would see plain text instead of the
205
+ * original template literal. Callers that explicitly want a
206
+ * fully destructive reset should use `reset()` instead, which
207
+ * also clears the source registry.
208
+ */
209
+ clear() {
210
+ for (const op of this._operations) {
211
+ op.dispose();
212
+ }
213
+ this._operations.clear();
214
+ this._nodeIndex = new WeakMap();
215
+ this._attrOps = new WeakMap();
216
+ this._indexedNodes = new WeakSet();
217
+ }
218
+ /**
219
+ * Fully destructive reset — equivalent to `clear()` plus clearing
220
+ * the `EtaTemplateSourceRegistry`.
221
+ *
222
+ * Use this when the live DOM no longer reflects the original
223
+ * template sources (for example, because external code mutated
224
+ * already-rendered nodes while the index was offline, or the
225
+ * caller is switching to a completely different template set).
226
+ *
227
+ * After `reset()`, the next `initialize(root)` walks the subtree
228
+ * from scratch using whatever is in the DOM at that moment —
229
+ * the registry will not serve a stale preserved source.
230
+ *
231
+ * For normal `clear()` + `initialize()` cycles where the source
232
+ * strings are still valid, prefer `clear()` so the registry
233
+ * survives re-indexing.
234
+ */
235
+ reset() {
236
+ this.clear();
237
+ this.sources.clear();
238
+ }
239
+ [Symbol.iterator]() {
240
+ return this._operations.values();
241
+ }
242
+ /**
243
+ * Number of operations currently held by the index.
244
+ *
245
+ * Useful for tests and diagnostics. The renderer iterates operations
246
+ * directly and does not need this.
247
+ */
248
+ get size() {
249
+ return this._operations.size;
250
+ }
251
+ /**
252
+ * Whether the given node has any operation associated with it.
253
+ *
254
+ * Used by the controller to decide if a mutation is interesting.
255
+ *
256
+ * For text nodes this checks whether any operation is keyed on the node.
257
+ * For elements this returns true if the element owns any operation
258
+ * directly (e.g. its attributes) — `hasNode(element)` does NOT imply
259
+ * "this element has been fully indexed", only "this element owns at
260
+ * least one operation".
261
+ */
262
+ hasNode(node) {
263
+ if (this._indexedNodes.has(node)) {
264
+ return true;
265
+ }
266
+ if (node.nodeType === Node.ELEMENT_NODE) {
267
+ return this._attrOps.has(node);
268
+ }
269
+ return false;
270
+ }
271
+ /**
272
+ * Disposes every operation currently held by the index.
273
+ *
274
+ * The default operations are no-ops for dispose, but custom
275
+ * operations may need to release observers. Calling this from the
276
+ * controller's teardown keeps the lifecycle clean.
277
+ *
278
+ * Delegates to `clear()` so disposal and reset happen through one
279
+ * well-defined code path; this also avoids disposing each
280
+ * operation twice.
281
+ */
282
+ disposeAll() {
283
+ this.clear();
284
+ }
285
+ /**
286
+ * Returns `true` when `element` matches the configured
287
+ * `isBoundaryElement` predicate AND is a foreign boundary (i.e. is
288
+ * not the index's own root).
289
+ *
290
+ * The root itself is always walked; only nested boundaries stop the
291
+ * traversal.
292
+ */
293
+ _isForeignBoundary(element) {
294
+ if (this.options.root !== undefined &&
295
+ element === this.options.root) {
296
+ return false;
297
+ }
298
+ return (this.options.isBoundaryElement?.(element) === true);
299
+ }
300
+ /**
301
+ * Returns `true` when `node` belongs to this index's root.
302
+ *
303
+ * The walk climbs ancestors looking for either `root` or a foreign
304
+ * boundary. If the walk reaches `root` without hitting a foreign
305
+ * boundary, the node is owned. If the walk hits a foreign boundary
306
+ * first, the node is not owned.
307
+ *
308
+ * When `root` is not configured, the index accepts every mutation
309
+ * it receives — callers that wire a parent observer MUST set
310
+ * `root` to prevent nested-scope leakage.
311
+ *
312
+ * Foreign-boundary element itself: when the target `node` is a
313
+ * boundary element other than the index's own `root`, it is owned
314
+ * by another controller. The check is performed up-front so the
315
+ * ancestor loop only handles descendants and the `current !==
316
+ * node` guard it used to need can go away.
317
+ */
318
+ _ownsNode(node) {
319
+ const root = this.options.root;
320
+ if (root === undefined) {
321
+ return true;
322
+ }
323
+ // Shadow-DOM index: a node belongs to this index when the
324
+ // ancestor chain reaches the configured shadow root before
325
+ // climbing out at the shadow host.
326
+ //
327
+ // `getRootNode()` alone is not sufficient because a foreign
328
+ // boundary element nested deeper inside the shadow can sit
329
+ // between `node` and the shadow root — the foreign boundary
330
+ // belongs to a different controller, so this index must not
331
+ // claim it. Walking `parentNode` upward also catches the
332
+ // shadow-host case: the host's `parentNode` chain leaves the
333
+ // shadow tree entirely, so the walk terminates at `null`
334
+ // without ever reaching `root`.
335
+ //
336
+ // Foreign-boundary predicates are also consulted at each
337
+ // step so a nested element whose `getRootNode()` is still
338
+ // `root` but which `isBoundaryElement` classifies as a
339
+ // foreign boundary is correctly excluded — matching the
340
+ // light-DOM branch's behaviour below.
341
+ if (root instanceof ShadowRoot) {
342
+ let current = node;
343
+ while (current !== null) {
344
+ if (current === root) {
345
+ return true;
346
+ }
347
+ // Foreign boundary nested inside this shadow — owned by
348
+ // another controller even though `getRootNode()` still
349
+ // returns this shadow root. `current` is narrowed to
350
+ // `Element` here and `root` is a `ShadowRoot` (we are in
351
+ // the `root instanceof ShadowRoot` branch), so the two
352
+ // are disjoint types — the explicit `current !== root`
353
+ // check is unnecessary and would be flagged as
354
+ // unintentional.
355
+ if (current instanceof Element &&
356
+ this.options.isBoundaryElement?.(current) === true) {
357
+ return false;
358
+ }
359
+ current = current.parentNode;
360
+ }
361
+ return false;
362
+ }
363
+ if (node instanceof Element &&
364
+ node !== root &&
365
+ this.options.isBoundaryElement?.(node) === true) {
366
+ return false;
367
+ }
368
+ let current = node;
369
+ while (current) {
370
+ if (current === root) {
371
+ return true;
372
+ }
373
+ if (current instanceof Element &&
374
+ current !== root &&
375
+ this.options.isBoundaryElement?.(current) === true) {
376
+ return false;
377
+ }
378
+ current = current.parentNode;
379
+ }
380
+ return false;
381
+ }
382
+ _addNode(node) {
383
+ // Refuse to walk into nodes that are not part of the root's owned
384
+ // subtree. Observer feedback can deliver `add` mutations for
385
+ // nested scope elements or sibling trees that the parent index
386
+ // does not own — the index must ignore them.
387
+ if (!this._ownsNode(node)) {
388
+ return false;
389
+ }
390
+ if (this._isIndexed(node)) {
391
+ // Already indexed. Avoid double-processing the same node.
392
+ return false;
393
+ }
394
+ if (node.nodeType === Node.TEXT_NODE) {
395
+ const op = this.factory.createTextOperation(node, { sources: this.sources });
396
+ if (op) {
397
+ this._addTextOperation(node, op);
398
+ return true;
399
+ }
400
+ return false;
401
+ }
402
+ // `ShadowRoot` (and any `DocumentFragment`-shaped root passed
403
+ // to `initialize()`) is not itself a template target — it is
404
+ // a container. Descend into its children and index each one.
405
+ if (node instanceof DocumentFragment) {
406
+ for (const child of Array.from(node.childNodes)) {
407
+ this._addNode(child);
408
+ }
409
+ return false;
410
+ }
411
+ if (node.nodeType !== Node.ELEMENT_NODE) {
412
+ return false;
413
+ }
414
+ const element = node;
415
+ // Foreign-boundary check happens BEFORE attribute collection so a
416
+ // nested boundary element is fully skipped. The boundary's own
417
+ // controller owns both its attributes and its descendants — a
418
+ // parent index must not collect either.
419
+ if (this._isForeignBoundary(element)) {
420
+ return false;
421
+ }
422
+ const beforeSize = this._operations.size;
423
+ this._collectAttributes(element);
424
+ for (const child of Array.from(element.childNodes)) {
425
+ this._addNode(child);
426
+ }
427
+ return this._operations.size !== beforeSize;
428
+ }
429
+ _isIndexed(node) {
430
+ if (this._indexedNodes.has(node)) {
431
+ return true;
432
+ }
433
+ if (node.nodeType === Node.ELEMENT_NODE) {
434
+ return this._attrOps.has(node);
435
+ }
436
+ return false;
437
+ }
438
+ _collectAttributes(element) {
439
+ const context = { sources: this.sources };
440
+ for (const attr of Array.from(element.attributes)) {
441
+ const op = this.factory.createAttributeOperation(element, attr, context);
442
+ if (!op) {
443
+ continue;
444
+ }
445
+ this._addAttributeOperation(element, attr.name, op);
446
+ }
447
+ }
448
+ /**
449
+ * Removes every operation that targets `node` or one of its descendants.
450
+ *
451
+ * The walk mirrors `_addNode` but does not need to consult the factory.
452
+ * Returns `true` when at least one operation was disposed by the
453
+ * call.
454
+ */
455
+ _removeNode(node) {
456
+ let removed = false;
457
+ const owned = this._nodeIndex.get(node);
458
+ if (owned) {
459
+ for (const op of owned) {
460
+ if (this._deleteOperation(op)) {
461
+ removed = true;
462
+ }
463
+ }
464
+ this._nodeIndex.delete(node);
465
+ }
466
+ this._indexedNodes.delete(node);
467
+ if (node.nodeType === Node.ELEMENT_NODE) {
468
+ const element = node;
469
+ // Drop any attribute operations owned by this element. The
470
+ // per-attribute map is authoritative for attribute lookup, so we
471
+ // must clear it here.
472
+ const byName = this._attrOps.get(element);
473
+ if (byName) {
474
+ for (const op of byName.values()) {
475
+ if (this._deleteOperation(op)) {
476
+ removed = true;
477
+ }
478
+ }
479
+ this._attrOps.delete(element);
480
+ }
481
+ for (const child of Array.from(element.childNodes)) {
482
+ if (this._removeNode(child)) {
483
+ removed = true;
484
+ }
485
+ }
486
+ }
487
+ return removed;
488
+ }
489
+ _updateAttribute(element, attribute) {
490
+ if (!this._ownsNode(element)) {
491
+ return false;
492
+ }
493
+ // Sync the registry with the current DOM value before re-running
494
+ // discovery so an external mutation that turned a template into
495
+ // plain text is reflected (the registry must not serve a stale
496
+ // preserved source).
497
+ this.sources.syncAttributeSource(element, attribute);
498
+ // Drop the previous operation for this (element, name) pair. The
499
+ // Attr reference is unstable across mutations, so we key by element
500
+ // + name via `_attrOps` rather than by Attr. We capture the return
501
+ // value because the contract for `apply()` requires this method
502
+ // to report the index actually changed — even if no replacement
503
+ // attribute is found, removing the old operation IS a change.
504
+ const removed = this._removeAttributeOperation(element, attribute);
505
+ // Resolve the current Attr for the new value. If the attribute was
506
+ // removed entirely there is nothing to re-create.
507
+ const attr = element.attributes.getNamedItem(attribute);
508
+ if (!attr) {
509
+ return removed;
510
+ }
511
+ const op = this.factory.createAttributeOperation(element, attr, { sources: this.sources });
512
+ if (!op) {
513
+ // The new attribute value is not a template (e.g. external
514
+ // code replaced `{{ url }}` with plain text). The old
515
+ // operation, if any, was already removed above — return
516
+ // whether that removal actually happened so callers see an
517
+ // accurate dirty signal.
518
+ return removed;
519
+ }
520
+ this._addAttributeOperation(element, attribute, op);
521
+ return true;
522
+ }
523
+ _updateText(node) {
524
+ if (!this._ownsNode(node)) {
525
+ return false;
526
+ }
527
+ // Sync the registry with the current DOM value before re-running
528
+ // discovery. External mutations that turn a template into plain
529
+ // text must not leave the registry holding a stale preserved
530
+ // source.
531
+ this.sources.syncTextSource(node);
532
+ // Capture whether the previous text operation was actually
533
+ // removed. The contract for `apply()` requires this method to
534
+ // report the index actually changed — even if no replacement
535
+ // text operation is created, dropping the old one IS a change.
536
+ let removed = false;
537
+ const owned = this._nodeIndex.get(node);
538
+ if (owned) {
539
+ for (const op of owned) {
540
+ if (op.kind === 'text') {
541
+ if (this._deleteOperation(op)) {
542
+ removed = true;
543
+ }
544
+ }
545
+ }
546
+ const remaining = new Set();
547
+ for (const op of owned) {
548
+ if (this._operations.has(op)) {
549
+ remaining.add(op);
550
+ }
551
+ }
552
+ if (remaining.size === 0) {
553
+ this._nodeIndex.delete(node);
554
+ this._indexedNodes.delete(node);
555
+ }
556
+ else {
557
+ this._nodeIndex.set(node, remaining);
558
+ }
559
+ }
560
+ const op = this.factory.createTextOperation(node, { sources: this.sources });
561
+ if (!op) {
562
+ // The new value is not a template (e.g. external code replaced
563
+ // `{{ name }}` with plain text). The previous text operation,
564
+ // if any, was already removed above — return whether that
565
+ // removal actually happened so callers see an accurate dirty
566
+ // signal.
567
+ return removed;
568
+ }
569
+ this._addTextOperation(node, op);
570
+ return true;
571
+ }
572
+ _addTextOperation(node, operation) {
573
+ this._operations.add(operation);
574
+ const owned = this._nodeIndex.get(node) ??
575
+ new Set();
576
+ owned.add(operation);
577
+ this._nodeIndex.set(node, owned);
578
+ this._indexedNodes.add(node);
579
+ }
580
+ _addAttributeOperation(element, name, operation) {
581
+ this._operations.add(operation);
582
+ // Register the operation under both the element and the
583
+ // (element, name) pair. `_nodeIndex` lets `hasNode(element)` and
584
+ // `_removeNode(element)` find the operation via element identity;
585
+ // `_attrOps` lets `_updateAttribute` find the previous op by name
586
+ // even after the Attr reference changes.
587
+ const owned = this._nodeIndex.get(element) ??
588
+ new Set();
589
+ owned.add(operation);
590
+ this._nodeIndex.set(element, owned);
591
+ this._indexedNodes.add(element);
592
+ let byName = this._attrOps.get(element);
593
+ if (!byName) {
594
+ byName = new Map();
595
+ this._attrOps.set(element, byName);
596
+ }
597
+ byName.set(name, operation);
598
+ }
599
+ _removeAttributeOperation(element, name) {
600
+ const byName = this._attrOps.get(element);
601
+ if (!byName) {
602
+ return false;
603
+ }
604
+ const op = byName.get(name);
605
+ if (!op) {
606
+ return false;
607
+ }
608
+ this._deleteOperation(op);
609
+ byName.delete(name);
610
+ if (byName.size === 0) {
611
+ this._attrOps.delete(element);
612
+ }
613
+ const owned = this._nodeIndex.get(element);
614
+ if (owned) {
615
+ owned.delete(op);
616
+ if (owned.size === 0) {
617
+ this._nodeIndex.delete(element);
618
+ this._indexedNodes.delete(element);
619
+ }
620
+ }
621
+ return true;
622
+ }
623
+ /**
624
+ * Removes `operation` from the live `_operations` set and calls its
625
+ * `dispose()` hook.
626
+ *
627
+ * Returns `true` when the operation was actually present and
628
+ * removed, `false` when it had already been removed by a previous
629
+ * path (idempotent removal).
630
+ *
631
+ * Single entry point used by every removal path so the disposal
632
+ * contract is honoured uniformly.
633
+ */
634
+ _deleteOperation(operation) {
635
+ if (!this._operations.delete(operation)) {
636
+ return false;
637
+ }
638
+ operation.dispose();
639
+ return true;
640
+ }
641
+ }
642
+ //# sourceMappingURL=template-index.js.map