@repobit/dex-store-elements 1.7.3 → 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.
- package/CHANGELOG.md +5 -913
- package/README.md +9 -7
- package/dist/src/actions/action.adapter.d.ts +36 -0
- package/dist/src/actions/action.adapter.js +183 -0
- package/dist/src/actions/action.adapter.js.map +1 -0
- package/dist/src/actions/action.attributes.d.ts +15 -0
- package/dist/src/actions/action.attributes.js +137 -0
- package/dist/src/actions/action.attributes.js.map +1 -0
- package/dist/src/actions/action.registry.d.ts +4 -0
- package/dist/src/actions/action.registry.js +119 -0
- package/dist/src/actions/action.registry.js.map +1 -0
- package/dist/src/actions/index.d.ts +3 -2
- package/dist/src/actions/index.js +3 -80
- package/dist/src/actions/index.js.map +1 -1
- package/dist/src/compute/compute.expand.d.ts +70 -0
- package/dist/src/compute/compute.expand.js +319 -0
- package/dist/src/compute/compute.expand.js.map +1 -0
- package/dist/src/compute/compute.scan.d.ts +54 -0
- package/dist/src/compute/compute.scan.js +160 -0
- package/dist/src/compute/compute.scan.js.map +1 -0
- package/dist/src/compute/option-resolver.d.ts +31 -0
- package/dist/src/compute/option-resolver.js +147 -0
- package/dist/src/compute/option-resolver.js.map +1 -0
- package/dist/src/contexts/context.compute.d.ts +12 -0
- package/dist/src/contexts/context.compute.js +4 -0
- package/dist/src/contexts/context.compute.js.map +1 -0
- package/dist/src/contexts/context.scope.d.ts +139 -0
- package/dist/src/contexts/context.scope.js +48 -0
- package/dist/src/contexts/context.scope.js.map +1 -0
- package/dist/src/controllers/controller.action-resolve.d.ts +110 -0
- package/dist/src/controllers/controller.action-resolve.js +203 -0
- package/dist/src/controllers/controller.action-resolve.js.map +1 -0
- package/dist/src/controllers/controller.action-topology.d.ts +59 -0
- package/dist/src/controllers/controller.action-topology.js +130 -0
- package/dist/src/controllers/controller.action-topology.js.map +1 -0
- package/dist/src/controllers/controller.compute.d.ts +72 -0
- package/dist/src/controllers/controller.compute.js +110 -0
- package/dist/src/controllers/controller.compute.js.map +1 -0
- package/dist/src/controllers/controller.resolve.d.ts +78 -0
- package/dist/src/controllers/controller.resolve.js +205 -0
- package/dist/src/controllers/controller.resolve.js.map +1 -0
- package/dist/src/core/action-topology.d.ts +62 -0
- package/dist/src/core/action-topology.js +67 -0
- package/dist/src/core/action-topology.js.map +1 -0
- package/dist/src/core/actions.d.ts +64 -0
- package/dist/src/core/actions.js +47 -0
- package/dist/src/core/actions.js.map +1 -0
- package/dist/src/core/node.element.d.ts +179 -0
- package/dist/src/core/node.element.js +573 -0
- package/dist/src/core/node.element.js.map +1 -0
- package/dist/src/core/node.scope.d.ts +374 -0
- package/dist/src/core/node.scope.js +558 -0
- package/dist/src/core/node.scope.js.map +1 -0
- package/dist/src/core/option-update.d.ts +11 -0
- package/dist/src/core/option-update.js +59 -0
- package/dist/src/core/option-update.js.map +1 -0
- package/dist/src/dsl/compilers/array/compiler.js +13 -14
- package/dist/src/dsl/compilers/array/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/boolean/compiler.d.ts +1 -1
- package/dist/src/dsl/compilers/boolean/compiler.js +14 -27
- package/dist/src/dsl/compilers/boolean/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/enum/compiler.js +7 -10
- package/dist/src/dsl/compilers/enum/compiler.js.map +1 -1
- package/dist/src/dsl/compilers/index.d.ts +1 -1
- package/dist/src/dsl/compilers/index.js +5 -5
- package/dist/src/dsl/compilers/index.js.map +1 -1
- package/dist/src/dsl/utilty.d.ts +0 -4
- package/dist/src/dsl/utilty.js +3 -30
- package/dist/src/dsl/utilty.js.map +1 -1
- package/dist/src/eta/batch.d.ts +123 -0
- package/dist/src/eta/batch.js +162 -0
- package/dist/src/eta/batch.js.map +1 -0
- package/dist/src/eta/batcher.d.ts +184 -0
- package/dist/src/eta/batcher.js +335 -0
- package/dist/src/eta/batcher.js.map +1 -0
- package/dist/src/eta/cache.d.ts +36 -0
- package/dist/src/eta/cache.js +56 -0
- package/dist/src/eta/cache.js.map +1 -0
- package/dist/src/eta/controller.d.ts +608 -0
- package/dist/src/eta/controller.js +1036 -0
- package/dist/src/eta/controller.js.map +1 -0
- package/dist/src/eta/factory.d.ts +74 -0
- package/dist/src/eta/factory.js +121 -0
- package/dist/src/eta/factory.js.map +1 -0
- package/dist/src/eta/idle.d.ts +39 -0
- package/dist/src/eta/idle.js +52 -0
- package/dist/src/eta/idle.js.map +1 -0
- package/dist/src/eta/index.d.ts +52 -0
- package/dist/src/eta/index.js +13 -0
- package/dist/src/eta/index.js.map +1 -0
- package/dist/src/eta/observer.d.ts +263 -0
- package/dist/src/eta/observer.js +263 -0
- package/dist/src/eta/observer.js.map +1 -0
- package/dist/src/eta/operations.d.ts +41 -0
- package/dist/src/eta/operations.js +59 -0
- package/dist/src/eta/operations.js.map +1 -0
- package/dist/src/eta/source-registry.d.ts +198 -0
- package/dist/src/eta/source-registry.js +267 -0
- package/dist/src/eta/source-registry.js.map +1 -0
- package/dist/src/eta/template-index.d.ts +285 -0
- package/dist/src/eta/template-index.js +642 -0
- package/dist/src/eta/template-index.js.map +1 -0
- package/dist/src/eta/types.d.ts +529 -0
- package/dist/src/eta/types.js +2 -0
- package/dist/src/eta/types.js.map +1 -0
- package/dist/src/index.d.ts +21 -9
- package/dist/src/index.js +22 -11
- package/dist/src/index.js.map +1 -1
- package/dist/src/nodes/node.context.d.ts +5 -3
- package/dist/src/nodes/node.context.js +48 -3
- package/dist/src/nodes/node.context.js.map +1 -1
- package/dist/src/nodes/node.option.d.ts +11 -31
- package/dist/src/nodes/node.option.js +158 -222
- package/dist/src/nodes/node.option.js.map +1 -1
- package/dist/src/nodes/node.product.d.ts +7 -11
- package/dist/src/nodes/node.product.js +85 -64
- package/dist/src/nodes/node.product.js.map +1 -1
- package/dist/src/renders/attributes/buyLink.d.ts +2 -2
- package/dist/src/renders/attributes/buyLink.js +14 -11
- package/dist/src/renders/attributes/buyLink.js.map +1 -1
- package/dist/src/renders/attributes/devices.d.ts +2 -2
- package/dist/src/renders/attributes/devices.js +9 -19
- package/dist/src/renders/attributes/devices.js.map +1 -1
- package/dist/src/renders/attributes/discount.d.ts +2 -2
- package/dist/src/renders/attributes/discount.js +56 -44
- package/dist/src/renders/attributes/discount.js.map +1 -1
- package/dist/src/renders/attributes/hide.d.ts +2 -2
- package/dist/src/renders/attributes/hide.js +49 -14
- package/dist/src/renders/attributes/hide.js.map +1 -1
- package/dist/src/renders/attributes/index.d.ts +3 -3
- package/dist/src/renders/attributes/index.js +2 -4
- package/dist/src/renders/attributes/index.js.map +1 -1
- package/dist/src/renders/attributes/price.d.ts +2 -2
- package/dist/src/renders/attributes/price.js +30 -44
- package/dist/src/renders/attributes/price.js.map +1 -1
- package/dist/src/renders/attributes/subscription.d.ts +2 -2
- package/dist/src/renders/attributes/subscription.js +14 -20
- package/dist/src/renders/attributes/subscription.js.map +1 -1
- package/dist/src/renders/attributes/utilty.d.ts +2 -0
- package/dist/src/renders/attributes/utilty.js +17 -4
- package/dist/src/renders/attributes/utilty.js.map +1 -1
- package/dist/src/renders/context.d.ts +122 -10
- package/dist/src/renders/context.js +140 -80
- package/dist/src/renders/context.js.map +1 -1
- package/dist/src/renders/format.js +2 -2
- package/dist/src/renders/format.js.map +1 -1
- package/dist/src/renders/index.d.ts +6 -2
- package/dist/src/renders/index.js +67 -29
- package/dist/src/renders/index.js.map +1 -1
- package/dist/src/renders/observe.d.ts +1 -4
- package/dist/src/renders/observe.js +10 -24
- package/dist/src/renders/observe.js.map +1 -1
- package/dist/src/renders/render.wrapper.d.ts +37 -0
- package/dist/src/renders/render.wrapper.js +147 -0
- package/dist/src/renders/render.wrapper.js.map +1 -0
- package/dist/src/templating/eta.js +1 -4
- package/dist/src/templating/eta.js.map +1 -1
- package/package.json +4 -5
- package/dist/src/actions/action.button.d.ts +0 -2
- package/dist/src/actions/action.button.js +0 -8
- package/dist/src/actions/action.button.js.map +0 -1
- package/dist/src/actions/action.input.d.ts +0 -2
- package/dist/src/actions/action.input.js +0 -43
- package/dist/src/actions/action.input.js.map +0 -1
- package/dist/src/actions/action.select.d.ts +0 -2
- package/dist/src/actions/action.select.js +0 -14
- package/dist/src/actions/action.select.js.map +0 -1
- package/dist/src/actions/utilty.d.ts +0 -16
- package/dist/src/actions/utilty.js +0 -149
- package/dist/src/actions/utilty.js.map +0 -1
- package/dist/src/contexts/context.datalayer.d.ts +0 -9
- package/dist/src/contexts/context.datalayer.js +0 -3
- package/dist/src/contexts/context.datalayer.js.map +0 -1
- package/dist/src/contexts/context.derived.d.ts +0 -12
- package/dist/src/contexts/context.derived.js +0 -3
- package/dist/src/contexts/context.derived.js.map +0 -1
- package/dist/src/contexts/context.option.d.ts +0 -5
- package/dist/src/contexts/context.option.js +0 -3
- package/dist/src/contexts/context.option.js.map +0 -1
- package/dist/src/contexts/context.product.d.ts +0 -5
- package/dist/src/contexts/context.product.js +0 -3
- package/dist/src/contexts/context.product.js.map +0 -1
- package/dist/src/contexts/context.state.d.ts +0 -28
- package/dist/src/contexts/context.state.js +0 -3
- package/dist/src/contexts/context.state.js.map +0 -1
- package/dist/src/contexts/context.store.d.ts +0 -5
- package/dist/src/contexts/context.store.js +0 -3
- package/dist/src/contexts/context.store.js.map +0 -1
- package/dist/src/controllers/collect-controller.d.ts +0 -39
- package/dist/src/controllers/collect-controller.js +0 -161
- package/dist/src/controllers/collect-controller.js.map +0 -1
- package/dist/src/controllers/compute-controller.d.ts +0 -55
- package/dist/src/controllers/compute-controller.js +0 -350
- package/dist/src/controllers/compute-controller.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.d.ts +0 -25
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.js +0 -192
- package/dist/src/controllers/eta-renderer/eta-dom-renderer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.d.ts +0 -28
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.js +0 -206
- package/dist/src/controllers/eta-renderer/eta-mutation-observer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-renderer.d.ts +0 -21
- package/dist/src/controllers/eta-renderer/eta-renderer.js +0 -77
- package/dist/src/controllers/eta-renderer/eta-renderer.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-template-cache.d.ts +0 -24
- package/dist/src/controllers/eta-renderer/eta-template-cache.js +0 -61
- package/dist/src/controllers/eta-renderer/eta-template-cache.js.map +0 -1
- package/dist/src/controllers/eta-renderer/eta-types.d.ts +0 -11
- package/dist/src/controllers/eta-renderer/eta-types.js +0 -1
- package/dist/src/controllers/eta-renderer/eta-types.js.map +0 -1
- package/dist/src/controllers/eta-renderer/index.d.ts +0 -2
- package/dist/src/controllers/eta-renderer/index.js +0 -2
- package/dist/src/controllers/eta-renderer/index.js.map +0 -1
- package/dist/src/controllers/event-pipeline-controller.d.ts +0 -28
- package/dist/src/controllers/event-pipeline-controller.js +0 -72
- package/dist/src/controllers/event-pipeline-controller.js.map +0 -1
- package/dist/src/events/events.d.ts +0 -75
- package/dist/src/events/events.js +0 -78
- package/dist/src/events/events.js.map +0 -1
- package/dist/src/nodes/node.root.d.ts +0 -16
- package/dist/src/nodes/node.root.js +0 -49
- package/dist/src/nodes/node.root.js.map +0 -1
- package/dist/src/nodes/node.state.d.ts +0 -88
- package/dist/src/nodes/node.state.js +0 -330
- package/dist/src/nodes/node.state.js.map +0 -1
- package/dist/src/renders/utility.d.ts +0 -32
- package/dist/src/renders/utility.js +0 -191
- 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
|