@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,198 @@
1
+ /**
2
+ * Sidecar registry that preserves original template sources across renders.
3
+ *
4
+ * The Eta template subsystem overwrites DOM values during render:
5
+ *
6
+ * <span>{{=it.name}}</span>
7
+ * becomes
8
+ * <span>iPhone</span>
9
+ *
10
+ * Once a render has happened, the original `{{=it.name}}` source is gone
11
+ * from the DOM. If the index is later cleared and re-initialized — or if a
12
+ * mutation observer forwards an external edit — the factory can only see
13
+ * the rendered string, not the template source.
14
+ *
15
+ * The registry stores the original sources keyed by node identity. The
16
+ * factory consults the registry first and only falls back to the DOM
17
+ * value when the registry has no entry.
18
+ *
19
+ * The registry is intentionally generic: it does not know about Eta, scope,
20
+ * products, options, or pricing. It is a small, GC-friendly cache.
21
+ *
22
+ * Lifetime:
23
+ *
24
+ * - Uses `WeakMap` / `Map` keyed by DOM nodes so disconnected nodes can be
25
+ * reclaimed by the GC.
26
+ * - `clear()` resets the registry to empty.
27
+ *
28
+ * **Cross-scope limitation.** A source registry is owned by the
29
+ * index that created it (one registry per `EtaTemplateIndex`
30
+ * instance by default). Moving a rendered template node from one
31
+ * scope to another loses the original template source — the
32
+ * destination index's registry has no record of it, so the
33
+ * factory falls back to the live DOM value, which now holds the
34
+ * rendered string rather than the template literal. The
35
+ * destination index cannot recreate the operation without
36
+ * discovering the new template content externally.
37
+ *
38
+ * If an application reparents rendered template nodes across
39
+ * scopes, the safe options are:
40
+ *
41
+ * 1. Share a single `EtaTemplateSourceRegistry` across the
42
+ * affected indices and have each index pass that shared
43
+ * registry through the `TemplateOperationContext` on every
44
+ * factory call — the factory itself is stateless at the call
45
+ * site, so a single registry can back multiple indices.
46
+ * 2. Accept that moved nodes must be re-discovered by the
47
+ * destination index — for example, by re-emitting the original
48
+ * template literal into the moved node, or by relying on the
49
+ * index's normal `MutationObserver` flow to pick up the new
50
+ * template content.
51
+ *
52
+ * Today the controller does not expose a `sourceRegistry`
53
+ * cross-index option; sharing requires reaching into the index
54
+ * constructor. The v4 review recommends keeping this limitation
55
+ * documented unless an application actually needs cross-scope
56
+ * DOM moves — at which point the index/controller can grow a
57
+ * `sourceRegistry` option that overrides the per-index
58
+ * `createEtaTemplateSourceRegistry()` default.
59
+ */
60
+ /**
61
+ * Default predicate used by `EtaTemplateSourceRegistry` to recognise
62
+ * template text.
63
+ *
64
+ * Requires BOTH an opening `{{` AND a closing `}}` so the registry
65
+ * can safely inspect arbitrary DOM values without false positives on
66
+ * stray `{{` markers in plain string literals.
67
+ *
68
+ /**
69
+ * Default predicate used to decide whether a string contains an
70
+ * Eta template token.
71
+ *
72
+ * Requires BOTH an opening `{{` AND a closing `}}` so the
73
+ * predicate does not pay the cost of compiling partial template
74
+ * fragments (e.g. `Hello {{ user` with a typo) or arbitrary
75
+ * DOM values that happen to contain `{{` inside a plain string
76
+ * literal. The empty-string guard short-circuits the
77
+ * `indexOf` calls on falsy input.
78
+ *
79
+ * **Single source of truth.** The factory (`factory.ts`) re-exports
80
+ * this predicate as `defaultIsTemplateText` for backward
81
+ * compatibility; the two names point at the same function so the
82
+ * registry and the factory can never drift apart.
83
+ */
84
+ export declare function defaultContainsEtaTemplate(text: string): boolean;
85
+ /**
86
+ * Predicate used to decide whether a string contains a template token.
87
+ *
88
+ * The default (`defaultContainsEtaTemplate`) requires BOTH an opening
89
+ * `{{` AND a closing `}}` so the registry can safely inspect arbitrary
90
+ * DOM values. Callers can plug in a stricter predicate when they want
91
+ * to be even more conservative.
92
+ */
93
+ export type EtaTemplatePredicate = (text: string) => boolean;
94
+ /**
95
+ * Options accepted by `EtaTemplateSourceRegistry`.
96
+ */
97
+ export interface EtaTemplateSourceRegistryOptions {
98
+ /**
99
+ * Predicate used to recognise template text. Defaults to
100
+ * `defaultContainsEtaTemplate`, which requires both `{{` AND `}}`.
101
+ */
102
+ readonly isTemplateText?: EtaTemplatePredicate;
103
+ }
104
+ /**
105
+ * Sidecar storage for original template sources.
106
+ *
107
+ * Reads are tolerant: if no source is preserved for a node, the registry
108
+ * consults the DOM value, and if that contains a template it persists the
109
+ * value so the next read returns the preserved source rather than the
110
+ * (possibly already-rendered) DOM value.
111
+ *
112
+ * **Destructive operations out of scope.** The registry only preserves
113
+ * sources; it does not enumerate attribute names that have been removed
114
+ * from the DOM. Removing an attribute at runtime (e.g. via
115
+ * `element.removeAttribute`) is a destructive operation: the registry
116
+ * has no way to discover a removed attribute name from a live element
117
+ * alone. Once `removeWhenEmpty` was dropped from the renderer, the
118
+ * registry only ever needs to handle attributes that are still present
119
+ * in the DOM, so this limitation is no longer a concern in practice.
120
+ */
121
+ export declare class EtaTemplateSourceRegistry {
122
+ private textSources;
123
+ private attrSources;
124
+ private readonly isTemplateText;
125
+ constructor(options?: EtaTemplateSourceRegistryOptions);
126
+ /**
127
+ * Returns the preserved source for a text node, falling back to the
128
+ * current DOM value.
129
+ *
130
+ * If the DOM value contains a template and no preserved source exists,
131
+ * the value is persisted so future reads stay stable even after the
132
+ * DOM value changes.
133
+ */
134
+ readTextSource(node: Text): string | undefined;
135
+ /**
136
+ * Sets or clears the preserved source for a text node.
137
+ *
138
+ * Pass `undefined` to remove any preserved source; the next call to
139
+ * `readTextSource` will then fall back to the current DOM value.
140
+ */
141
+ replaceTextSource(node: Text, source: string | undefined): void;
142
+ /**
143
+ * Returns the preserved source for a single attribute on an element.
144
+ *
145
+ * If no preserved source exists, the registry consults the current
146
+ * DOM value; when the value contains a template, the registry persists
147
+ * it so future reads stay stable.
148
+ */
149
+ readAttributeSource(element: Element, name: string): string | undefined;
150
+ /**
151
+ * Stores a preserved source for an attribute. Used both internally
152
+ * (after reading a fresh DOM value) and externally (after parsing a
153
+ * template literal that the DOM no longer exposes).
154
+ */
155
+ setAttributeSource(element: Element, name: string, source: string): void;
156
+ /**
157
+ * Removes a preserved attribute source. No-op if no source exists.
158
+ */
159
+ removeAttributeSource(element: Element, name: string): void;
160
+ /**
161
+ * Reads the current DOM value for a text node and updates the
162
+ * registry accordingly.
163
+ *
164
+ * Persists the value when it contains a template; clears any
165
+ * previously preserved source otherwise. This is the right call for
166
+ * external mutations where the DOM has been edited and the registry
167
+ * should reflect the new state.
168
+ */
169
+ syncTextSource(node: Text): void;
170
+ /**
171
+ * Reads the current DOM value for an attribute and updates the
172
+ * registry accordingly.
173
+ *
174
+ * Persists the value when it contains a template; clears any
175
+ * previously preserved source otherwise. The right call for external
176
+ * attribute mutations.
177
+ */
178
+ syncAttributeSource(element: Element, name: string): void;
179
+ /**
180
+ * Wipes every preserved source.
181
+ *
182
+ * Mostly useful for tests. Production code should let the GC reclaim
183
+ * entries as nodes become unreachable.
184
+ */
185
+ clear(): void;
186
+ /**
187
+ * Returns `true` when `text` looks like an Eta template.
188
+ *
189
+ * Delegates to the configured predicate; the default
190
+ * (`defaultContainsEtaTemplate`) requires both an opening `{{` AND a
191
+ * closing `}}`.
192
+ */
193
+ private containsTemplate;
194
+ }
195
+ /**
196
+ * Convenience constructor used by the index and the default factory.
197
+ */
198
+ export declare function createEtaTemplateSourceRegistry(options?: EtaTemplateSourceRegistryOptions): EtaTemplateSourceRegistry;
@@ -0,0 +1,267 @@
1
+ // src/eta/source-registry.ts
2
+ /**
3
+ * Sidecar registry that preserves original template sources across renders.
4
+ *
5
+ * The Eta template subsystem overwrites DOM values during render:
6
+ *
7
+ * <span>{{=it.name}}</span>
8
+ * becomes
9
+ * <span>iPhone</span>
10
+ *
11
+ * Once a render has happened, the original `{{=it.name}}` source is gone
12
+ * from the DOM. If the index is later cleared and re-initialized — or if a
13
+ * mutation observer forwards an external edit — the factory can only see
14
+ * the rendered string, not the template source.
15
+ *
16
+ * The registry stores the original sources keyed by node identity. The
17
+ * factory consults the registry first and only falls back to the DOM
18
+ * value when the registry has no entry.
19
+ *
20
+ * The registry is intentionally generic: it does not know about Eta, scope,
21
+ * products, options, or pricing. It is a small, GC-friendly cache.
22
+ *
23
+ * Lifetime:
24
+ *
25
+ * - Uses `WeakMap` / `Map` keyed by DOM nodes so disconnected nodes can be
26
+ * reclaimed by the GC.
27
+ * - `clear()` resets the registry to empty.
28
+ *
29
+ * **Cross-scope limitation.** A source registry is owned by the
30
+ * index that created it (one registry per `EtaTemplateIndex`
31
+ * instance by default). Moving a rendered template node from one
32
+ * scope to another loses the original template source — the
33
+ * destination index's registry has no record of it, so the
34
+ * factory falls back to the live DOM value, which now holds the
35
+ * rendered string rather than the template literal. The
36
+ * destination index cannot recreate the operation without
37
+ * discovering the new template content externally.
38
+ *
39
+ * If an application reparents rendered template nodes across
40
+ * scopes, the safe options are:
41
+ *
42
+ * 1. Share a single `EtaTemplateSourceRegistry` across the
43
+ * affected indices and have each index pass that shared
44
+ * registry through the `TemplateOperationContext` on every
45
+ * factory call — the factory itself is stateless at the call
46
+ * site, so a single registry can back multiple indices.
47
+ * 2. Accept that moved nodes must be re-discovered by the
48
+ * destination index — for example, by re-emitting the original
49
+ * template literal into the moved node, or by relying on the
50
+ * index's normal `MutationObserver` flow to pick up the new
51
+ * template content.
52
+ *
53
+ * Today the controller does not expose a `sourceRegistry`
54
+ * cross-index option; sharing requires reaching into the index
55
+ * constructor. The v4 review recommends keeping this limitation
56
+ * documented unless an application actually needs cross-scope
57
+ * DOM moves — at which point the index/controller can grow a
58
+ * `sourceRegistry` option that overrides the per-index
59
+ * `createEtaTemplateSourceRegistry()` default.
60
+ */
61
+ /**
62
+ * Default predicate used by `EtaTemplateSourceRegistry` to recognise
63
+ * template text.
64
+ *
65
+ * Requires BOTH an opening `{{` AND a closing `}}` so the registry
66
+ * can safely inspect arbitrary DOM values without false positives on
67
+ * stray `{{` markers in plain string literals.
68
+ *
69
+ /**
70
+ * Default predicate used to decide whether a string contains an
71
+ * Eta template token.
72
+ *
73
+ * Requires BOTH an opening `{{` AND a closing `}}` so the
74
+ * predicate does not pay the cost of compiling partial template
75
+ * fragments (e.g. `Hello {{ user` with a typo) or arbitrary
76
+ * DOM values that happen to contain `{{` inside a plain string
77
+ * literal. The empty-string guard short-circuits the
78
+ * `indexOf` calls on falsy input.
79
+ *
80
+ * **Single source of truth.** The factory (`factory.ts`) re-exports
81
+ * this predicate as `defaultIsTemplateText` for backward
82
+ * compatibility; the two names point at the same function so the
83
+ * registry and the factory can never drift apart.
84
+ */
85
+ export function defaultContainsEtaTemplate(text) {
86
+ if (!text) {
87
+ return false;
88
+ }
89
+ return (text.indexOf('{{') !== -1 &&
90
+ text.indexOf('}}') !== -1);
91
+ }
92
+ /**
93
+ * Sidecar storage for original template sources.
94
+ *
95
+ * Reads are tolerant: if no source is preserved for a node, the registry
96
+ * consults the DOM value, and if that contains a template it persists the
97
+ * value so the next read returns the preserved source rather than the
98
+ * (possibly already-rendered) DOM value.
99
+ *
100
+ * **Destructive operations out of scope.** The registry only preserves
101
+ * sources; it does not enumerate attribute names that have been removed
102
+ * from the DOM. Removing an attribute at runtime (e.g. via
103
+ * `element.removeAttribute`) is a destructive operation: the registry
104
+ * has no way to discover a removed attribute name from a live element
105
+ * alone. Once `removeWhenEmpty` was dropped from the renderer, the
106
+ * registry only ever needs to handle attributes that are still present
107
+ * in the DOM, so this limitation is no longer a concern in practice.
108
+ */
109
+ export class EtaTemplateSourceRegistry {
110
+ constructor(options = {}) {
111
+ this.textSources = new WeakMap();
112
+ this.attrSources = new WeakMap();
113
+ this.isTemplateText =
114
+ options.isTemplateText ??
115
+ defaultContainsEtaTemplate;
116
+ }
117
+ /**
118
+ * Returns the preserved source for a text node, falling back to the
119
+ * current DOM value.
120
+ *
121
+ * If the DOM value contains a template and no preserved source exists,
122
+ * the value is persisted so future reads stay stable even after the
123
+ * DOM value changes.
124
+ */
125
+ readTextSource(node) {
126
+ const existing = this.textSources.get(node);
127
+ if (existing !== undefined) {
128
+ return existing;
129
+ }
130
+ const current = node.nodeValue ?? '';
131
+ if (!this.containsTemplate(current)) {
132
+ return undefined;
133
+ }
134
+ this.textSources.set(node, current);
135
+ return current;
136
+ }
137
+ /**
138
+ * Sets or clears the preserved source for a text node.
139
+ *
140
+ * Pass `undefined` to remove any preserved source; the next call to
141
+ * `readTextSource` will then fall back to the current DOM value.
142
+ */
143
+ replaceTextSource(node, source) {
144
+ if (source === undefined) {
145
+ this.textSources.delete(node);
146
+ return;
147
+ }
148
+ this.textSources.set(node, source);
149
+ }
150
+ /**
151
+ * Returns the preserved source for a single attribute on an element.
152
+ *
153
+ * If no preserved source exists, the registry consults the current
154
+ * DOM value; when the value contains a template, the registry persists
155
+ * it so future reads stay stable.
156
+ */
157
+ readAttributeSource(element, name) {
158
+ const existing = this.attrSources.get(element)?.get(name);
159
+ if (existing !== undefined) {
160
+ return existing;
161
+ }
162
+ const current = element.getAttribute(name);
163
+ if (current === null ||
164
+ current === undefined ||
165
+ !this.containsTemplate(current)) {
166
+ return undefined;
167
+ }
168
+ this.setAttributeSource(element, name, current);
169
+ return current;
170
+ }
171
+ /**
172
+ * Stores a preserved source for an attribute. Used both internally
173
+ * (after reading a fresh DOM value) and externally (after parsing a
174
+ * template literal that the DOM no longer exposes).
175
+ */
176
+ setAttributeSource(element, name, source) {
177
+ let byName = this.attrSources.get(element);
178
+ if (!byName) {
179
+ byName = new Map();
180
+ this.attrSources.set(element, byName);
181
+ }
182
+ byName.set(name, source);
183
+ }
184
+ /**
185
+ * Removes a preserved attribute source. No-op if no source exists.
186
+ */
187
+ removeAttributeSource(element, name) {
188
+ const byName = this.attrSources.get(element);
189
+ if (!byName) {
190
+ return;
191
+ }
192
+ byName.delete(name);
193
+ if (byName.size === 0) {
194
+ this.attrSources.delete(element);
195
+ }
196
+ }
197
+ /**
198
+ * Reads the current DOM value for a text node and updates the
199
+ * registry accordingly.
200
+ *
201
+ * Persists the value when it contains a template; clears any
202
+ * previously preserved source otherwise. This is the right call for
203
+ * external mutations where the DOM has been edited and the registry
204
+ * should reflect the new state.
205
+ */
206
+ syncTextSource(node) {
207
+ const current = node.nodeValue ?? '';
208
+ if (this.containsTemplate(current)) {
209
+ this.textSources.set(node, current);
210
+ }
211
+ else {
212
+ this.textSources.delete(node);
213
+ }
214
+ }
215
+ /**
216
+ * Reads the current DOM value for an attribute and updates the
217
+ * registry accordingly.
218
+ *
219
+ * Persists the value when it contains a template; clears any
220
+ * previously preserved source otherwise. The right call for external
221
+ * attribute mutations.
222
+ */
223
+ syncAttributeSource(element, name) {
224
+ const current = element.getAttribute(name);
225
+ if (current === null ||
226
+ current === undefined) {
227
+ this.removeAttributeSource(element, name);
228
+ return;
229
+ }
230
+ if (this.containsTemplate(current)) {
231
+ this.setAttributeSource(element, name, current);
232
+ }
233
+ else {
234
+ this.removeAttributeSource(element, name);
235
+ }
236
+ }
237
+ /**
238
+ * Wipes every preserved source.
239
+ *
240
+ * Mostly useful for tests. Production code should let the GC reclaim
241
+ * entries as nodes become unreachable.
242
+ */
243
+ clear() {
244
+ // WeakMap has no `clear` in older targets, so we replace the
245
+ // internal references with fresh maps. The old maps become
246
+ // unreachable as soon as no caller holds them.
247
+ this.textSources = new WeakMap();
248
+ this.attrSources = new WeakMap();
249
+ }
250
+ /**
251
+ * Returns `true` when `text` looks like an Eta template.
252
+ *
253
+ * Delegates to the configured predicate; the default
254
+ * (`defaultContainsEtaTemplate`) requires both an opening `{{` AND a
255
+ * closing `}}`.
256
+ */
257
+ containsTemplate(text) {
258
+ return this.isTemplateText(text);
259
+ }
260
+ }
261
+ /**
262
+ * Convenience constructor used by the index and the default factory.
263
+ */
264
+ export function createEtaTemplateSourceRegistry(options) {
265
+ return new EtaTemplateSourceRegistry(options);
266
+ }
267
+ //# sourceMappingURL=source-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-registry.js","sourceRoot":"","sources":["../../../src/eta/source-registry.ts"],"names":[],"mappings":"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","sourcesContent":["// src/eta/source-registry.ts\n\n/**\n * Sidecar registry that preserves original template sources across renders.\n *\n * The Eta template subsystem overwrites DOM values during render:\n *\n * <span>{{=it.name}}</span>\n * becomes\n * <span>iPhone</span>\n *\n * Once a render has happened, the original `{{=it.name}}` source is gone\n * from the DOM. If the index is later cleared and re-initialized — or if a\n * mutation observer forwards an external edit — the factory can only see\n * the rendered string, not the template source.\n *\n * The registry stores the original sources keyed by node identity. The\n * factory consults the registry first and only falls back to the DOM\n * value when the registry has no entry.\n *\n * The registry is intentionally generic: it does not know about Eta, scope,\n * products, options, or pricing. It is a small, GC-friendly cache.\n *\n * Lifetime:\n *\n * - Uses `WeakMap` / `Map` keyed by DOM nodes so disconnected nodes can be\n * reclaimed by the GC.\n * - `clear()` resets the registry to empty.\n *\n * **Cross-scope limitation.** A source registry is owned by the\n * index that created it (one registry per `EtaTemplateIndex`\n * instance by default). Moving a rendered template node from one\n * scope to another loses the original template source — the\n * destination index's registry has no record of it, so the\n * factory falls back to the live DOM value, which now holds the\n * rendered string rather than the template literal. The\n * destination index cannot recreate the operation without\n * discovering the new template content externally.\n *\n * If an application reparents rendered template nodes across\n * scopes, the safe options are:\n *\n * 1. Share a single `EtaTemplateSourceRegistry` across the\n * affected indices and have each index pass that shared\n * registry through the `TemplateOperationContext` on every\n * factory call — the factory itself is stateless at the call\n * site, so a single registry can back multiple indices.\n * 2. Accept that moved nodes must be re-discovered by the\n * destination index — for example, by re-emitting the original\n * template literal into the moved node, or by relying on the\n * index's normal `MutationObserver` flow to pick up the new\n * template content.\n *\n * Today the controller does not expose a `sourceRegistry`\n * cross-index option; sharing requires reaching into the index\n * constructor. The v4 review recommends keeping this limitation\n * documented unless an application actually needs cross-scope\n * DOM moves — at which point the index/controller can grow a\n * `sourceRegistry` option that overrides the per-index\n * `createEtaTemplateSourceRegistry()` default.\n */\n\n/**\n * Default predicate used by `EtaTemplateSourceRegistry` to recognise\n * template text.\n *\n * Requires BOTH an opening `{{` AND a closing `}}` so the registry\n * can safely inspect arbitrary DOM values without false positives on\n * stray `{{` markers in plain string literals.\n *\n/**\n * Default predicate used to decide whether a string contains an\n * Eta template token.\n *\n * Requires BOTH an opening `{{` AND a closing `}}` so the\n * predicate does not pay the cost of compiling partial template\n * fragments (e.g. `Hello {{ user` with a typo) or arbitrary\n * DOM values that happen to contain `{{` inside a plain string\n * literal. The empty-string guard short-circuits the\n * `indexOf` calls on falsy input.\n *\n * **Single source of truth.** The factory (`factory.ts`) re-exports\n * this predicate as `defaultIsTemplateText` for backward\n * compatibility; the two names point at the same function so the\n * registry and the factory can never drift apart.\n */\nexport function defaultContainsEtaTemplate(\n text: string\n): boolean {\n if (!text) {\n return false;\n }\n\n return (\n text.indexOf('{{') !== -1 &&\n text.indexOf('}}') !== -1\n );\n}\n\n/**\n * Predicate used to decide whether a string contains a template token.\n *\n * The default (`defaultContainsEtaTemplate`) requires BOTH an opening\n * `{{` AND a closing `}}` so the registry can safely inspect arbitrary\n * DOM values. Callers can plug in a stricter predicate when they want\n * to be even more conservative.\n */\nexport type EtaTemplatePredicate = (text: string) => boolean;\n\n/**\n * Options accepted by `EtaTemplateSourceRegistry`.\n */\nexport interface EtaTemplateSourceRegistryOptions {\n /**\n * Predicate used to recognise template text. Defaults to\n * `defaultContainsEtaTemplate`, which requires both `{{` AND `}}`.\n */\n readonly isTemplateText?: EtaTemplatePredicate;\n}\n\n/**\n * Sidecar storage for original template sources.\n *\n * Reads are tolerant: if no source is preserved for a node, the registry\n * consults the DOM value, and if that contains a template it persists the\n * value so the next read returns the preserved source rather than the\n * (possibly already-rendered) DOM value.\n *\n * **Destructive operations out of scope.** The registry only preserves\n * sources; it does not enumerate attribute names that have been removed\n * from the DOM. Removing an attribute at runtime (e.g. via\n * `element.removeAttribute`) is a destructive operation: the registry\n * has no way to discover a removed attribute name from a live element\n * alone. Once `removeWhenEmpty` was dropped from the renderer, the\n * registry only ever needs to handle attributes that are still present\n * in the DOM, so this limitation is no longer a concern in practice.\n */\nexport class EtaTemplateSourceRegistry {\n private textSources: WeakMap<Text, string> =\n new WeakMap();\n\n private attrSources: WeakMap<\n Element,\n Map<string, string>\n > = new WeakMap();\n\n private readonly isTemplateText: EtaTemplatePredicate;\n\n constructor(\n options: EtaTemplateSourceRegistryOptions = {}\n ) {\n this.isTemplateText =\n options.isTemplateText ??\n defaultContainsEtaTemplate;\n }\n\n /**\n * Returns the preserved source for a text node, falling back to the\n * current DOM value.\n *\n * If the DOM value contains a template and no preserved source exists,\n * the value is persisted so future reads stay stable even after the\n * DOM value changes.\n */\n readTextSource(node: Text): string | undefined {\n const existing = this.textSources.get(node);\n\n if (existing !== undefined) {\n return existing;\n }\n\n const current = node.nodeValue ?? '';\n\n if (!this.containsTemplate(current)) {\n return undefined;\n }\n\n this.textSources.set(node, current);\n\n return current;\n }\n\n /**\n * Sets or clears the preserved source for a text node.\n *\n * Pass `undefined` to remove any preserved source; the next call to\n * `readTextSource` will then fall back to the current DOM value.\n */\n replaceTextSource(\n node: Text,\n source: string | undefined\n ): void {\n if (source === undefined) {\n this.textSources.delete(node);\n return;\n }\n\n this.textSources.set(node, source);\n }\n\n /**\n * Returns the preserved source for a single attribute on an element.\n *\n * If no preserved source exists, the registry consults the current\n * DOM value; when the value contains a template, the registry persists\n * it so future reads stay stable.\n */\n readAttributeSource(\n element: Element,\n name: string\n ): string | undefined {\n const existing =\n this.attrSources.get(element)?.get(name);\n\n if (existing !== undefined) {\n return existing;\n }\n\n const current = element.getAttribute(name);\n\n if (\n current === null ||\n current === undefined ||\n !this.containsTemplate(current)\n ) {\n return undefined;\n }\n\n this.setAttributeSource(element, name, current);\n\n return current;\n }\n\n /**\n * Stores a preserved source for an attribute. Used both internally\n * (after reading a fresh DOM value) and externally (after parsing a\n * template literal that the DOM no longer exposes).\n */\n setAttributeSource(\n element: Element,\n name: string,\n source: string\n ): void {\n let byName = this.attrSources.get(element);\n\n if (!byName) {\n byName = new Map<string, string>();\n this.attrSources.set(element, byName);\n }\n\n byName.set(name, source);\n }\n\n /**\n * Removes a preserved attribute source. No-op if no source exists.\n */\n removeAttributeSource(\n element: Element,\n name: string\n ): void {\n const byName = this.attrSources.get(element);\n\n if (!byName) {\n return;\n }\n\n byName.delete(name);\n\n if (byName.size === 0) {\n this.attrSources.delete(element);\n }\n }\n\n /**\n * Reads the current DOM value for a text node and updates the\n * registry accordingly.\n *\n * Persists the value when it contains a template; clears any\n * previously preserved source otherwise. This is the right call for\n * external mutations where the DOM has been edited and the registry\n * should reflect the new state.\n */\n syncTextSource(node: Text): void {\n const current = node.nodeValue ?? '';\n\n if (this.containsTemplate(current)) {\n this.textSources.set(node, current);\n } else {\n this.textSources.delete(node);\n }\n }\n\n /**\n * Reads the current DOM value for an attribute and updates the\n * registry accordingly.\n *\n * Persists the value when it contains a template; clears any\n * previously preserved source otherwise. The right call for external\n * attribute mutations.\n */\n syncAttributeSource(\n element: Element,\n name: string\n ): void {\n const current = element.getAttribute(name);\n\n if (\n current === null ||\n current === undefined\n ) {\n this.removeAttributeSource(element, name);\n return;\n }\n\n if (this.containsTemplate(current)) {\n this.setAttributeSource(element, name, current);\n } else {\n this.removeAttributeSource(element, name);\n }\n }\n\n /**\n * Wipes every preserved source.\n *\n * Mostly useful for tests. Production code should let the GC reclaim\n * entries as nodes become unreachable.\n */\n clear(): void {\n // WeakMap has no `clear` in older targets, so we replace the\n // internal references with fresh maps. The old maps become\n // unreachable as soon as no caller holds them.\n this.textSources = new WeakMap();\n this.attrSources = new WeakMap();\n }\n\n /**\n * Returns `true` when `text` looks like an Eta template.\n *\n * Delegates to the configured predicate; the default\n * (`defaultContainsEtaTemplate`) requires both an opening `{{` AND a\n * closing `}}`.\n */\n private containsTemplate(text: string): boolean {\n return this.isTemplateText(text);\n }\n}\n\n/**\n * Convenience constructor used by the index and the default factory.\n */\nexport function createEtaTemplateSourceRegistry(\n options?: EtaTemplateSourceRegistryOptions\n): EtaTemplateSourceRegistry {\n return new EtaTemplateSourceRegistry(options);\n}\n"]}