@adaas/are-html 0.0.21 → 0.0.23
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/.conf/tsconfig.base.json +1 -0
- package/.conf/tsconfig.browser.json +1 -0
- package/.conf/tsconfig.node.json +1 -0
- package/dist/browser/index.d.mts +214 -3
- package/dist/browser/index.mjs +787 -201
- package/dist/browser/index.mjs.map +1 -1
- package/dist/node/{AreBinding.attribute-doUvtOjc.d.mts → AreBinding.attribute-BWzEIw6H.d.mts} +45 -0
- package/dist/node/{AreBinding.attribute-Bm5LlOyE.d.ts → AreBinding.attribute-GpT-5Qmf.d.ts} +45 -0
- package/dist/node/attributes/AreBinding.attribute.d.mts +1 -1
- package/dist/node/attributes/AreBinding.attribute.d.ts +1 -1
- package/dist/node/attributes/AreDirective.attribute.d.mts +1 -1
- package/dist/node/attributes/AreDirective.attribute.d.ts +1 -1
- package/dist/node/attributes/AreEvent.attribute.d.mts +1 -1
- package/dist/node/attributes/AreEvent.attribute.d.ts +1 -1
- package/dist/node/attributes/AreStatic.attribute.d.mts +1 -1
- package/dist/node/attributes/AreStatic.attribute.d.ts +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.mts +55 -2
- package/dist/node/directives/AreDirectiveFor.directive.d.ts +55 -2
- package/dist/node/directives/AreDirectiveFor.directive.js +141 -12
- package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.mjs +141 -12
- package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.mts +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.ts +1 -1
- package/dist/node/directives/AreDirectiveShow.directive.d.mts +1 -1
- package/dist/node/directives/AreDirectiveShow.directive.d.ts +1 -1
- package/dist/node/engine/AreHTML.compiler.d.mts +1 -1
- package/dist/node/engine/AreHTML.compiler.d.ts +1 -1
- package/dist/node/engine/AreHTML.compiler.js +4 -0
- package/dist/node/engine/AreHTML.compiler.js.map +1 -1
- package/dist/node/engine/AreHTML.compiler.mjs +4 -0
- package/dist/node/engine/AreHTML.compiler.mjs.map +1 -1
- package/dist/node/engine/AreHTML.constants.d.mts +33 -1
- package/dist/node/engine/AreHTML.constants.d.ts +33 -1
- package/dist/node/engine/AreHTML.constants.js +166 -0
- package/dist/node/engine/AreHTML.constants.js.map +1 -1
- package/dist/node/engine/AreHTML.constants.mjs +165 -1
- package/dist/node/engine/AreHTML.constants.mjs.map +1 -1
- package/dist/node/engine/AreHTML.context.d.mts +66 -0
- package/dist/node/engine/AreHTML.context.d.ts +66 -0
- package/dist/node/engine/AreHTML.context.js +98 -0
- package/dist/node/engine/AreHTML.context.js.map +1 -1
- package/dist/node/engine/AreHTML.context.mjs +98 -0
- package/dist/node/engine/AreHTML.context.mjs.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.d.mts +3 -0
- package/dist/node/engine/AreHTML.interpreter.d.ts +3 -0
- package/dist/node/engine/AreHTML.interpreter.js +66 -10
- package/dist/node/engine/AreHTML.interpreter.js.map +1 -1
- package/dist/node/engine/AreHTML.interpreter.mjs +66 -10
- package/dist/node/engine/AreHTML.interpreter.mjs.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.d.mts +2 -2
- package/dist/node/engine/AreHTML.lifecycle.d.ts +2 -2
- package/dist/node/engine/AreHTML.lifecycle.js +32 -4
- package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.mjs +32 -4
- package/dist/node/engine/AreHTML.lifecycle.mjs.map +1 -1
- package/dist/node/engine/AreHTML.tokenizer.d.mts +1 -1
- package/dist/node/engine/AreHTML.tokenizer.d.ts +1 -1
- package/dist/node/engine/AreHTML.tokenizer.js +7 -1
- package/dist/node/engine/AreHTML.tokenizer.js.map +1 -1
- package/dist/node/engine/AreHTML.tokenizer.mjs +7 -1
- package/dist/node/engine/AreHTML.tokenizer.mjs.map +1 -1
- package/dist/node/engine/AreHTML.transformer.d.mts +1 -1
- package/dist/node/engine/AreHTML.transformer.d.ts +1 -1
- package/dist/node/helpers/AreScheduler.helper.d.mts +39 -0
- package/dist/node/helpers/AreScheduler.helper.d.ts +39 -0
- package/dist/node/helpers/AreScheduler.helper.js +40 -0
- package/dist/node/helpers/AreScheduler.helper.js.map +1 -0
- package/dist/node/helpers/AreScheduler.helper.mjs +40 -0
- package/dist/node/helpers/AreScheduler.helper.mjs.map +1 -0
- package/dist/node/index.d.mts +4 -3
- package/dist/node/index.d.ts +4 -3
- package/dist/node/index.js +7 -0
- package/dist/node/index.mjs +1 -0
- package/dist/node/instructions/AddStaticHTML.instruction.d.mts +8 -0
- package/dist/node/instructions/AddStaticHTML.instruction.d.ts +8 -0
- package/dist/node/instructions/AddStaticHTML.instruction.js +31 -0
- package/dist/node/instructions/AddStaticHTML.instruction.js.map +1 -0
- package/dist/node/instructions/AddStaticHTML.instruction.mjs +24 -0
- package/dist/node/instructions/AddStaticHTML.instruction.mjs.map +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.d.mts +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.d.ts +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.js +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.js.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.constants.mjs +1 -0
- package/dist/node/instructions/AreHTML.instructions.constants.mjs.map +1 -1
- package/dist/node/instructions/AreHTML.instructions.types.d.mts +9 -1
- package/dist/node/instructions/AreHTML.instructions.types.d.ts +9 -1
- package/dist/node/lib/AreDirective/AreDirective.component.d.mts +1 -1
- package/dist/node/lib/AreDirective/AreDirective.component.d.ts +1 -1
- package/dist/node/lib/AreDirective/AreDirective.types.d.mts +1 -1
- package/dist/node/lib/AreDirective/AreDirective.types.d.ts +1 -1
- package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.mts +1 -1
- package/dist/node/lib/AreHTML/AreHTML.tokenizer.d.ts +1 -1
- package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.mts +1 -1
- package/dist/node/lib/AreHTMLAttribute/AreHTML.attribute.d.ts +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.mts +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.d.ts +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.js +51 -0
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.js.map +1 -1
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs +51 -0
- package/dist/node/lib/AreHTMLNode/AreHTMLNode.mjs.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.js +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.js.map +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs +1 -1
- package/dist/node/lib/AreRoot/AreRoot.component.mjs.map +1 -1
- package/dist/node/nodes/AreComment.d.mts +1 -1
- package/dist/node/nodes/AreComment.d.ts +1 -1
- package/dist/node/nodes/AreComponent.d.mts +1 -1
- package/dist/node/nodes/AreComponent.d.ts +1 -1
- package/dist/node/nodes/AreInterpolation.d.mts +1 -1
- package/dist/node/nodes/AreInterpolation.d.ts +1 -1
- package/dist/node/nodes/AreRoot.d.mts +1 -1
- package/dist/node/nodes/AreRoot.d.ts +1 -1
- package/dist/node/nodes/AreText.d.mts +1 -1
- package/dist/node/nodes/AreText.d.ts +1 -1
- package/examples/dashboard/concept.ts +1 -1
- package/examples/dashboard/dist/index.html +1 -1
- package/examples/dashboard/dist/{mq19zxz4-mnlgmd.js → mqiw5sqa-ypckmj.js} +2275 -1323
- package/examples/dashboard/src/concept.ts +3 -2
- package/examples/for-perf/concept.ts +45 -0
- package/examples/for-perf/containers/UI.container.ts +161 -0
- package/examples/for-perf/dist/index.html +270 -0
- package/examples/for-perf/dist/mqj1mpf2-z4aokv.js +15664 -0
- package/examples/for-perf/dist/mqj1mpff-4fr7mw.js +15664 -0
- package/examples/for-perf/public/index.html +270 -0
- package/examples/for-perf/src/components/PerfApp.component.ts +37 -0
- package/examples/for-perf/src/components/PerfControls.component.ts +34 -0
- package/examples/for-perf/src/components/PerfGrid.component.ts +225 -0
- package/examples/for-perf/src/components/PerfHeader.component.ts +34 -0
- package/examples/for-perf/src/components/PerfStats.component.ts +43 -0
- package/examples/for-perf/src/concept.ts +94 -0
- package/examples/jumpstart/dist/index.html +1 -1
- package/examples/jumpstart/dist/{mq7hqrxy-4kus50.js → mq7mgf58-vbf07e.js} +269 -91
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mq7k53th-qiwy4x.js → mqiwo23h-bhcolu.js} +2090 -1430
- package/jest.config.ts +1 -0
- package/package.json +10 -9
- package/src/directives/AreDirectiveFor.directive.ts +233 -19
- package/src/engine/AreHTML.compiler.ts +13 -0
- package/src/engine/AreHTML.constants.ts +142 -0
- package/src/engine/AreHTML.context.ts +112 -0
- package/src/engine/AreHTML.interpreter.ts +114 -13
- package/src/engine/AreHTML.lifecycle.ts +91 -7
- package/src/engine/AreHTML.tokenizer.ts +30 -1
- package/src/helpers/AreScheduler.helper.ts +61 -0
- package/src/index.ts +1 -0
- package/src/instructions/AddStaticHTML.instruction.ts +23 -0
- package/src/instructions/AreHTML.instructions.constants.ts +1 -0
- package/src/instructions/AreHTML.instructions.types.ts +9 -0
- package/src/lib/AreHTMLNode/AreHTMLNode.ts +74 -0
- package/src/lib/AreRoot/AreRoot.component.ts +4 -1
- package/tests/StaticIsland.test.ts +115 -0
- package/tsconfig.json +1 -0
|
@@ -54,6 +54,38 @@ export class AreHTMLEngineContext extends AreContext {
|
|
|
54
54
|
*/
|
|
55
55
|
protected _container: Document;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Parsed-fragment cache for static islands (see AddStaticHTMLInstruction).
|
|
59
|
+
*
|
|
60
|
+
* Keyed by `hostTag\u0000markup`, each entry holds a `DocumentFragment` whose
|
|
61
|
+
* children were parsed by the browser exactly once — in the *correct element
|
|
62
|
+
* context* (the host tag), so table fragments (`<tr>`, `<td>`, …) and other
|
|
63
|
+
* context-sensitive content parse correctly. Repeated static islands with
|
|
64
|
+
* identical markup (e.g. list rows, reused components) clone the pre-parsed
|
|
65
|
+
* fragment instead of re-parsing the HTML string on every mount — turning an
|
|
66
|
+
* O(parse) operation into an O(clone) one.
|
|
67
|
+
*/
|
|
68
|
+
protected _staticFragmentCache = new Map<string, DocumentFragment>();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Live-DOM attachments deferred while a mount pass is batching.
|
|
72
|
+
*
|
|
73
|
+
* A freshly-mounted subtree is built inside a *detached* root element, so
|
|
74
|
+
* every descendant `appendChild`/`insertBefore` happens off-document and
|
|
75
|
+
* triggers zero layout/paint invalidation. The single mutation that actually
|
|
76
|
+
* connects the built subtree to the live document is deferred and collected
|
|
77
|
+
* here, then flushed once when the batch closes — collapsing O(nodes) reflows
|
|
78
|
+
* into O(1) per mount root.
|
|
79
|
+
*/
|
|
80
|
+
protected _pendingAttachments: Array<() => void> = [];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Depth of the currently open batching scopes. Re-entrant so that nested
|
|
84
|
+
* `beginBatch`/`endBatch` pairs flush exactly once, when the outermost scope
|
|
85
|
+
* closes.
|
|
86
|
+
*/
|
|
87
|
+
protected _batchDepth = 0;
|
|
88
|
+
|
|
57
89
|
|
|
58
90
|
constructor(props: Partial<AreHTMLContextConstructor>) {
|
|
59
91
|
super(props.container?.body.innerHTML || props.source || '');
|
|
@@ -64,6 +96,86 @@ export class AreHTMLEngineContext extends AreContext {
|
|
|
64
96
|
return this._container;
|
|
65
97
|
}
|
|
66
98
|
|
|
99
|
+
/**
|
|
100
|
+
* `true` while a synchronous mount pass is batching live-DOM attachments.
|
|
101
|
+
* Interpreter handlers consult this to decide whether to attach an element
|
|
102
|
+
* immediately or hand the attachment to {@link deferAttach}.
|
|
103
|
+
*/
|
|
104
|
+
get isBatching(): boolean {
|
|
105
|
+
return this._batchDepth > 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Opens a batching scope. Re-entrant: only the outermost matching
|
|
110
|
+
* {@link endBatch} flushes the deferred attachments, so a single mount pass
|
|
111
|
+
* connects its built subtree to the live DOM exactly once.
|
|
112
|
+
*/
|
|
113
|
+
beginBatch(): void {
|
|
114
|
+
this._batchDepth++;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Registers a live-DOM attachment to run when the current batch flushes. If
|
|
119
|
+
* no batch is active the attachment runs immediately, preserving the original
|
|
120
|
+
* synchronous behaviour for updates that mount outside a batch.
|
|
121
|
+
*
|
|
122
|
+
* @param attach the DOM mutation that connects a built subtree to the document
|
|
123
|
+
*/
|
|
124
|
+
deferAttach(attach: () => void): void {
|
|
125
|
+
if (this._batchDepth > 0) {
|
|
126
|
+
this._pendingAttachments.push(attach);
|
|
127
|
+
} else {
|
|
128
|
+
attach();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Closes a batching scope. When the outermost scope closes, every deferred
|
|
134
|
+
* attachment runs in registration (document) order, connecting the built
|
|
135
|
+
* subtrees to the live DOM in a single pass.
|
|
136
|
+
*/
|
|
137
|
+
endBatch(): void {
|
|
138
|
+
if (this._batchDepth === 0) return;
|
|
139
|
+
this._batchDepth--;
|
|
140
|
+
if (this._batchDepth > 0) return;
|
|
141
|
+
|
|
142
|
+
const pending = this._pendingAttachments;
|
|
143
|
+
this._pendingAttachments = [];
|
|
144
|
+
for (let i = 0; i < pending.length; i++) {
|
|
145
|
+
pending[i]();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns a `DocumentFragment` containing the parsed form of `html`, parsed
|
|
151
|
+
* once in the context of `hostTag` (so context-sensitive content such as
|
|
152
|
+
* table rows/cells parses correctly) and cached thereafter. Callers should
|
|
153
|
+
* `cloneNode(true)` the returned fragment rather than mutating it, so the
|
|
154
|
+
* cache stays reusable.
|
|
155
|
+
*
|
|
156
|
+
* @param hostTag the tag name of the element the markup will be injected into
|
|
157
|
+
* @param html verbatim static-island inner markup
|
|
158
|
+
*/
|
|
159
|
+
getStaticFragment(hostTag: string, html: string): DocumentFragment {
|
|
160
|
+
const key = `${hostTag}\u0000${html}`;
|
|
161
|
+
let fragment = this._staticFragmentCache.get(key);
|
|
162
|
+
if (!fragment) {
|
|
163
|
+
// Parse in the correct element context: the fragment-parsing
|
|
164
|
+
// algorithm uses the container element's tag to choose the right
|
|
165
|
+
// insertion mode (e.g. `<tbody>` legitimately allows `<tr>`).
|
|
166
|
+
const container = this._container.createElement(hostTag);
|
|
167
|
+
container.innerHTML = html;
|
|
168
|
+
|
|
169
|
+
fragment = this._container.createDocumentFragment();
|
|
170
|
+
while (container.firstChild) {
|
|
171
|
+
fragment.appendChild(container.firstChild);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this._staticFragmentCache.set(key, fragment);
|
|
175
|
+
}
|
|
176
|
+
return fragment;
|
|
177
|
+
}
|
|
178
|
+
|
|
67
179
|
|
|
68
180
|
/**
|
|
69
181
|
* Retrieves the DOM element associated with a given AreNode. This method looks up the node's ASEID in the nodeToHostElements map to find the corresponding DOM element. If the node is not found, it returns undefined. This allows the engine to efficiently access and manipulate the DOM elements that correspond to specific nodes in the AreNode tree, enabling dynamic updates and interactions based on the application state.
|
|
@@ -15,6 +15,7 @@ import { AddElementInstruction } from "@adaas/are-html/instructions/AddElement.i
|
|
|
15
15
|
import { AddListenerInstruction } from "@adaas/are-html/instructions/AddListener.instruction";
|
|
16
16
|
import { AddTextInstruction } from "@adaas/are-html/instructions/AddText.instruction";
|
|
17
17
|
import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
|
|
18
|
+
import { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
|
|
18
19
|
import { HideElementInstruction } from "@adaas/are-html/instructions/HideElement.instruction";
|
|
19
20
|
import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
|
|
20
21
|
import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
|
|
@@ -84,16 +85,25 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
84
85
|
? context.container.createElementNS(SVG_NAMESPACE, tag)
|
|
85
86
|
: context.container.createElement(tag);
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
// Index the element synchronously so descendants can resolve this
|
|
89
|
+
// node as their mount point during the same (still off-document) pass.
|
|
90
|
+
context.setInstructionElement(declaration, element);
|
|
91
|
+
|
|
92
|
+
const attach = mountPoint.nodeType === Node.ELEMENT_NODE
|
|
88
93
|
// parent is a real element — just append
|
|
89
|
-
mountPoint.appendChild(element)
|
|
90
|
-
} else {
|
|
94
|
+
? () => mountPoint.appendChild(element)
|
|
91
95
|
// parent is an anchor (comment/text node) — insert before it
|
|
92
96
|
// so content always appears before the anchor marker
|
|
93
|
-
mountPoint.parentNode?.insertBefore(element, mountPoint);
|
|
94
|
-
}
|
|
97
|
+
: () => { mountPoint.parentNode?.insertBefore(element, mountPoint); };
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
// When batching, only the mutation that touches the *live* document
|
|
100
|
+
// needs deferring; appends into an already-detached subtree are free
|
|
101
|
+
// and run immediately so the offline tree keeps taking shape.
|
|
102
|
+
if (context.isBatching && mountPoint.isConnected) {
|
|
103
|
+
context.deferAttach(attach);
|
|
104
|
+
} else {
|
|
105
|
+
attach();
|
|
106
|
+
}
|
|
97
107
|
|
|
98
108
|
} else {
|
|
99
109
|
const mountPoint = context.container.getElementById(node.id);
|
|
@@ -108,9 +118,17 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
108
118
|
? context.container.createElementNS(SVG_NAMESPACE, tag)
|
|
109
119
|
: context.container.createElement(tag);
|
|
110
120
|
|
|
111
|
-
mountPoint.parentNode?.replaceChild(element, mountPoint);
|
|
112
|
-
|
|
113
121
|
context.setInstructionElement(declaration, element);
|
|
122
|
+
|
|
123
|
+
const attach = () => { mountPoint.parentNode?.replaceChild(element, mountPoint); };
|
|
124
|
+
|
|
125
|
+
// The placeholder lives in the live DOM — defer the swap so the
|
|
126
|
+
// whole subtree built into `element` lands in one mutation.
|
|
127
|
+
if (context.isBatching && mountPoint.isConnected) {
|
|
128
|
+
context.deferAttach(attach);
|
|
129
|
+
} else {
|
|
130
|
+
attach();
|
|
131
|
+
}
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
// Register the element in the context index
|
|
@@ -135,7 +153,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
135
153
|
) {
|
|
136
154
|
const element = context.getElementByInstruction(declaration);
|
|
137
155
|
|
|
138
|
-
|
|
156
|
+
// Skip the live-DOM detach when the element is already off-document: an
|
|
157
|
+
// ancestor removed earlier in the same unmount took this whole subtree
|
|
158
|
+
// with it, and a detached subtree is garbage-collected as a unit. The
|
|
159
|
+
// per-node removeChild would be wasted work. The index bookkeeping below
|
|
160
|
+
// must still run to drop the strong references that would pin it in memory.
|
|
161
|
+
if (element && element.parentNode && element.isConnected) {
|
|
139
162
|
element.parentNode.removeChild(element);
|
|
140
163
|
}
|
|
141
164
|
|
|
@@ -283,7 +306,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
283
306
|
|
|
284
307
|
const { name } = mutation.payload;
|
|
285
308
|
|
|
286
|
-
|
|
309
|
+
// No point stripping attributes off an element that is already
|
|
310
|
+
// detached from the live document (it is about to be discarded with
|
|
311
|
+
// its whole subtree). Only touch attributes while the element is live.
|
|
312
|
+
if (name && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
|
|
287
313
|
const colonIdx = name.indexOf(':');
|
|
288
314
|
if (colonIdx > 0) {
|
|
289
315
|
const ns = SVG_ATTRIBUTE_NS[name.slice(0, colonIdx)];
|
|
@@ -343,6 +369,10 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
343
369
|
|
|
344
370
|
if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
|
|
345
371
|
|
|
372
|
+
// A detached element is being discarded with its subtree — restoring its
|
|
373
|
+
// inline display would be pointless work. Only restore while it is live.
|
|
374
|
+
if (!element.isConnected) return;
|
|
375
|
+
|
|
346
376
|
const el = element as HTMLElement;
|
|
347
377
|
|
|
348
378
|
// Restore the cached inline display. An explicit payload display, when
|
|
@@ -510,7 +540,12 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
510
540
|
const listener = (mutation.payload as any)._callback as EventListenerOrEventListenerObject | undefined;
|
|
511
541
|
|
|
512
542
|
if (listener) {
|
|
513
|
-
element
|
|
543
|
+
// Detaching a listener from an element that is already off-document is
|
|
544
|
+
// unnecessary — the element and its listeners are collected together.
|
|
545
|
+
// Still clear the context bookkeeping so no strong reference lingers.
|
|
546
|
+
if (element.isConnected) {
|
|
547
|
+
element.removeEventListener(eventName, listener);
|
|
548
|
+
}
|
|
514
549
|
context.removeListener(element, name, listener);
|
|
515
550
|
(mutation.payload as any)._callback = undefined;
|
|
516
551
|
}
|
|
@@ -592,11 +627,73 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
592
627
|
|
|
593
628
|
if (!element) return;
|
|
594
629
|
|
|
595
|
-
|
|
630
|
+
// Off-document text nodes vanish with their detached parent subtree, so
|
|
631
|
+
// skip the removeChild and just release the index reference.
|
|
632
|
+
if (element.isConnected) {
|
|
633
|
+
element.parentNode?.removeChild(element);
|
|
634
|
+
}
|
|
596
635
|
context.removeInstructionElement(declaration);
|
|
597
636
|
}
|
|
598
637
|
|
|
599
638
|
|
|
639
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
640
|
+
// ── AddStaticHTML — Apply / Update / Revert ──────────────────────────────────
|
|
641
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// Materialises a fully static subtree (a "static island") onto its host
|
|
643
|
+
// element in a single pass. The tokenizer captured the inner markup verbatim
|
|
644
|
+
// instead of exploding it into per-element/per-text AreNodes, so here we hand
|
|
645
|
+
// the markup to the browser parser ONCE (via a cached <template>) and clone
|
|
646
|
+
// the pre-parsed fragment in. This collapses N DOM mutations into one and
|
|
647
|
+
// decodes HTML entities (e.g. ) natively.
|
|
648
|
+
@A_Frame.Define({
|
|
649
|
+
description: 'Inject a static island\'s inner markup onto its host element in one pass via a cached, browser-parsed <template> clone. Decodes HTML entities natively.'
|
|
650
|
+
})
|
|
651
|
+
@AreInterpreter.Apply(AreHTMLInstructions.AddStaticHTML)
|
|
652
|
+
@AreInterpreter.Update(AreHTMLInstructions.AddStaticHTML)
|
|
653
|
+
addStaticHTML(
|
|
654
|
+
@A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
|
|
655
|
+
@A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
|
|
656
|
+
@A_Inject(A_Logger) logger?: A_Logger,
|
|
657
|
+
) {
|
|
658
|
+
const element = context.getElementByInstruction(mutation.parent!);
|
|
659
|
+
|
|
660
|
+
if (!element || element.nodeType !== Node.ELEMENT_NODE) {
|
|
661
|
+
throw new AreInterpreterError({
|
|
662
|
+
title: 'Element Not Found',
|
|
663
|
+
description: `Could not find a DOM element associated with the instruction ASEID "${mutation.parent}". Ensure the host element is rendered before materialising its static island.`
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const el = element as HTMLElement;
|
|
668
|
+
const { html } = mutation.payload;
|
|
669
|
+
|
|
670
|
+
// Clear any previously injected content (Update path) before re-injecting.
|
|
671
|
+
el.textContent = '';
|
|
672
|
+
|
|
673
|
+
// Parse once (in the host tag's context) and clone the cached fragment.
|
|
674
|
+
const fragment = context.getStaticFragment(el.tagName.toLowerCase(), html);
|
|
675
|
+
el.appendChild(fragment.cloneNode(true));
|
|
676
|
+
|
|
677
|
+
logger?.debug('green', `Static island materialised onto <${(mutation.owner.parent ?? mutation.owner)?.aseid?.toString?.()}>`);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@A_Frame.Define({
|
|
682
|
+
description: 'Clear a static island\'s injected markup from its host element on revert.'
|
|
683
|
+
})
|
|
684
|
+
@AreInterpreter.Revert(AreHTMLInstructions.AddStaticHTML)
|
|
685
|
+
removeStaticHTML(
|
|
686
|
+
@A_Inject(A_Caller) mutation: AddStaticHTMLInstruction,
|
|
687
|
+
@A_Inject(AreHTMLEngineContext) context: AreHTMLEngineContext,
|
|
688
|
+
) {
|
|
689
|
+
const element = context.getElementByInstruction(mutation.parent!);
|
|
690
|
+
|
|
691
|
+
if (element && element.nodeType === Node.ELEMENT_NODE && element.isConnected) {
|
|
692
|
+
(element as HTMLElement).textContent = '';
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
|
|
600
697
|
|
|
601
698
|
@A_Frame.Define({
|
|
602
699
|
description: 'Add a comment node to the DOM based on the provided declaration instruction.'
|
|
@@ -667,7 +764,11 @@ export class AreHTMLInterpreter extends AreInterpreter {
|
|
|
667
764
|
|
|
668
765
|
if (!element) return;
|
|
669
766
|
|
|
670
|
-
|
|
767
|
+
// Off-document comment anchors are discarded with their detached parent
|
|
768
|
+
// subtree; skip the removeChild and just release the index reference.
|
|
769
|
+
if (element.isConnected) {
|
|
770
|
+
element.parentNode?.removeChild(element);
|
|
771
|
+
}
|
|
671
772
|
context.removeInstructionElement(declaration);
|
|
672
773
|
}
|
|
673
774
|
|
|
@@ -92,7 +92,7 @@ export class AreHTMLLifecycle extends AreLifecycle {
|
|
|
92
92
|
|
|
93
93
|
@A_Inject(A_Logger) logger?: A_Logger,
|
|
94
94
|
...args: any[]
|
|
95
|
-
) {
|
|
95
|
+
): void | Promise<void> {
|
|
96
96
|
|
|
97
97
|
logger?.debug(`[Mount] Component Trigger for <${node.aseid.entity}> with aseid :{${node.aseid.toString()}}`);
|
|
98
98
|
|
|
@@ -103,18 +103,102 @@ export class AreHTMLLifecycle extends AreLifecycle {
|
|
|
103
103
|
if (scene.isInactive) return;
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
*
|
|
106
|
+
* Open a batching scope for the whole (synchronous) mount pass. Every
|
|
107
|
+
* element created below is built into a *detached* root and attached to
|
|
108
|
+
* the live DOM only when the batch flushes — turning O(nodes) reflows into
|
|
109
|
+
* a single one per mount root. `try/finally` guarantees the batch closes
|
|
110
|
+
* (and the depth counter stays balanced) even if interpretation throws.
|
|
111
|
+
*
|
|
112
|
+
* The context is resolved from the node's scope (the scope the onMount
|
|
113
|
+
* feature runs in) so the override keeps the base `mount` signature.
|
|
107
114
|
*/
|
|
108
|
-
node.
|
|
115
|
+
const context = node.scope.resolve<AreHTMLEngineContext>(AreHTMLEngineContext);
|
|
116
|
+
|
|
117
|
+
context?.beginBatch();
|
|
118
|
+
|
|
109
119
|
/**
|
|
110
|
-
*
|
|
120
|
+
* `onAfterMount` must observe the node already connected to the live
|
|
121
|
+
* document (consumer components may measure layout / focus there). Since
|
|
122
|
+
* the subtree is built off-document and attached only when the batch
|
|
123
|
+
* flushes, we collect the post-order `onAfterMount` targets here and fire
|
|
124
|
+
* them once the flush has connected everything.
|
|
111
125
|
*/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
const afterMountQueue: AreHTMLNode[] = [];
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
/**
|
|
130
|
+
* 1. Render the root of this mount itself.
|
|
131
|
+
*/
|
|
132
|
+
node.interpret();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 2. Walk the descendant subtree iteratively with an explicit enter/exit
|
|
136
|
+
* stack. We keep the iterative (non-recursive) shape — it is cheaper
|
|
137
|
+
* than deep recursion and gives us a single, clear place that owns the
|
|
138
|
+
* per-node hook ordering:
|
|
139
|
+
* - enter → onBeforeMount, then (if active) interpret + queue children
|
|
140
|
+
* - exit → onAfterMount (fires AFTER the node's whole subtree, i.e.
|
|
141
|
+
* post-order, matching the recursive `node.mount()` contract)
|
|
142
|
+
*
|
|
143
|
+
* [!] The initial mount is intentionally ATOMIC (fully synchronous). It
|
|
144
|
+
* does NOT time-slice / yield. Yielding mid-walk exposes a partially
|
|
145
|
+
* mounted tree to the event loop: any update dispatched during a gap
|
|
146
|
+
* (signal-driven re-render, async data load, etc.) interprets a node
|
|
147
|
+
* whose parent has not been mounted yet — producing
|
|
148
|
+
* `mount-point-not-found` and out-of-order DOM. The whole page must
|
|
149
|
+
* therefore appear in the DOM in one uninterrupted pass. Heavy lists
|
|
150
|
+
* are sliced at the source instead (see AreDirectiveFor), where the
|
|
151
|
+
* batching is reentrancy-safe.
|
|
152
|
+
*/
|
|
153
|
+
interface MountFrame { node: AreHTMLNode; entered: boolean; }
|
|
154
|
+
|
|
155
|
+
const stack: MountFrame[] = [];
|
|
156
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
157
|
+
stack.push({ node: node.children[i] as AreHTMLNode, entered: false });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
while (stack.length > 0) {
|
|
161
|
+
const frame = stack[stack.length - 1];
|
|
162
|
+
const current = frame.node;
|
|
163
|
+
|
|
164
|
+
if (frame.entered) {
|
|
165
|
+
// Post-order exit: the whole subtree below `current` is mounted.
|
|
166
|
+
// Defer the onAfterMount hook until after the batch flush so the
|
|
167
|
+
// node is connected to the live DOM when it runs.
|
|
168
|
+
stack.pop();
|
|
169
|
+
afterMountQueue.push(current);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
frame.entered = true;
|
|
174
|
+
|
|
175
|
+
// onBeforeMount always fires (even for inactive nodes), matching the
|
|
176
|
+
// recursive AreNode.mount() semantics.
|
|
177
|
+
current.call(AreNodeFeatures.onBeforeMount, current.scope);
|
|
178
|
+
|
|
179
|
+
if (!current.scene.isInactive) {
|
|
180
|
+
current.interpret();
|
|
181
|
+
// Push children in reverse so they pop in document order.
|
|
182
|
+
for (let i = current.children.length - 1; i >= 0; i--) {
|
|
183
|
+
stack.push({ node: current.children[i] as AreHTMLNode, entered: false });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
// Flush the deferred attachments — the fully built subtree lands in the
|
|
189
|
+
// live DOM in a single pass.
|
|
190
|
+
context?.endBatch();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The subtree is now connected; fire onAfterMount in the original
|
|
194
|
+
// post-order, each with its node live in the document.
|
|
195
|
+
for (let i = 0; i < afterMountQueue.length; i++) {
|
|
196
|
+
const mounted = afterMountQueue[i];
|
|
197
|
+
mounted.call(AreNodeFeatures.onAfterMount, mounted.scope);
|
|
115
198
|
}
|
|
116
199
|
}
|
|
117
200
|
|
|
201
|
+
|
|
118
202
|
@A_Feature.Extend({
|
|
119
203
|
name: AreAttributeFeatures.Update,
|
|
120
204
|
scope: [AreDirectiveAttribute],
|
|
@@ -8,6 +8,8 @@ import { AreEventAttribute } from "@adaas/are-html/attributes/AreEvent.attribute
|
|
|
8
8
|
import { AreBindingAttribute } from "@adaas/are-html/attributes/AreBinding.attribute";
|
|
9
9
|
import { AreStaticAttribute } from "@adaas/are-html/attributes/AreStatic.attribute";
|
|
10
10
|
import { AreHTMLAttribute } from "../lib/AreHTMLAttribute/AreHTML.attribute";
|
|
11
|
+
import { AreHTMLNode } from "../lib/AreHTMLNode/AreHTMLNode";
|
|
12
|
+
import { isStaticMarkup } from "./AreHTML.constants";
|
|
11
13
|
import { A_Frame } from "@adaas/a-frame/core";
|
|
12
14
|
|
|
13
15
|
|
|
@@ -30,7 +32,34 @@ export class AreHTMLTokenizer extends AreTokenizer {
|
|
|
30
32
|
@A_Inject(A_Logger) logger?: A_Logger
|
|
31
33
|
): void {
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Static-island fast path.
|
|
37
|
+
*
|
|
38
|
+
* When a node's entire inner subtree is fully static — no `{{ }}`
|
|
39
|
+
* interpolations, no dynamic (`$`/`:`/`@`) attributes and only standard
|
|
40
|
+
* HTML tags — we skip exploding it into one child AreNode per element /
|
|
41
|
+
* text run and instead capture the inner markup verbatim. The interpreter
|
|
42
|
+
* later materialises it in a single pass (browser-parsed innerHTML /
|
|
43
|
+
* cached `<template>` clone), which:
|
|
44
|
+
* - eliminates N node + scope + scene + instruction allocations,
|
|
45
|
+
* - collapses N isolated DOM mutations into one, and
|
|
46
|
+
* - decodes HTML entities (e.g. ` `) for free.
|
|
47
|
+
*
|
|
48
|
+
* Routing outlets (AreRootNode) are intentionally excluded — they own
|
|
49
|
+
* their content dynamically. The node's OWN attributes are still
|
|
50
|
+
* extracted below, so a dynamic attribute on the island root itself
|
|
51
|
+
* (e.g. `<div :class="x"> …static… </div>`) keeps working.
|
|
52
|
+
*/
|
|
53
|
+
const isStaticIsland =
|
|
54
|
+
node instanceof AreComponentNode &&
|
|
55
|
+
!!node.content &&
|
|
56
|
+
isStaticMarkup(node.content);
|
|
57
|
+
|
|
58
|
+
if (isStaticIsland) {
|
|
59
|
+
(node as AreHTMLNode).markStatic(node.content);
|
|
60
|
+
} else {
|
|
61
|
+
super.tokenize(node, context, logger);
|
|
62
|
+
}
|
|
34
63
|
|
|
35
64
|
context.startPerformance('attributeExtraction');
|
|
36
65
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AreSchedulerHelper
|
|
3
|
+
*
|
|
4
|
+
* Cooperative time-slicing primitives shared by the chunked (async) render
|
|
5
|
+
* paths — the initial whole-page mount walk and the `$for` directive. Both need
|
|
6
|
+
* the SAME two capabilities:
|
|
7
|
+
* 1. a high-resolution clock to measure how long the current chunk has run, and
|
|
8
|
+
* 2. a zero-delay macrotask scheduler to yield to the browser between chunks
|
|
9
|
+
* so it can paint and process input before resuming work.
|
|
10
|
+
*
|
|
11
|
+
* Keeping these in one helper avoids duplicating the `MessageChannel` plumbing
|
|
12
|
+
* across directives/lifecycle and gives a single place to tune the strategy.
|
|
13
|
+
*/
|
|
14
|
+
export class AreSchedulerHelper {
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Lazily-created `MessageChannel` used to post zero-delay macrotasks.
|
|
18
|
+
* Created on first use so non-DOM environments (tests / SSR) that never
|
|
19
|
+
* schedule a chunk pay nothing.
|
|
20
|
+
*/
|
|
21
|
+
private static _channel?: MessageChannel;
|
|
22
|
+
|
|
23
|
+
/** FIFO queue of callbacks waiting for their posted macrotask to fire. */
|
|
24
|
+
private static readonly _queue: Array<() => void> = [];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* High-resolution wall-clock time in milliseconds. Uses `performance.now()`
|
|
28
|
+
* when available (monotonic, sub-millisecond), falling back to `Date.now()`.
|
|
29
|
+
*/
|
|
30
|
+
static now(): number {
|
|
31
|
+
return (typeof performance !== 'undefined' && typeof performance.now === 'function')
|
|
32
|
+
? performance.now()
|
|
33
|
+
: Date.now();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Schedule `fn` to run on the next macrotask.
|
|
38
|
+
*
|
|
39
|
+
* `MessageChannel` yields a true macrotask without the ~4ms clamp that nested
|
|
40
|
+
* `setTimeout(0)` calls incur, so the browser can paint between chunks with
|
|
41
|
+
* minimal scheduling overhead. Falls back to `setTimeout` in non-DOM
|
|
42
|
+
* environments (e.g. tests / SSR).
|
|
43
|
+
*/
|
|
44
|
+
static scheduleMacrotask(fn: () => void): void {
|
|
45
|
+
if (typeof MessageChannel === 'undefined') {
|
|
46
|
+
setTimeout(fn, 0);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!this._channel) {
|
|
51
|
+
this._channel = new MessageChannel();
|
|
52
|
+
this._channel.port1.onmessage = () => {
|
|
53
|
+
const next = this._queue.shift();
|
|
54
|
+
if (next) next();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this._queue.push(fn);
|
|
59
|
+
this._channel.port2.postMessage(null);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * from './instructions/AddAttribute.instruction';
|
|
|
21
21
|
export * from './instructions/AddElement.instruction';
|
|
22
22
|
export * from './instructions/AddInterpolation.instruction';
|
|
23
23
|
export * from './instructions/AddListener.instruction';
|
|
24
|
+
export * from './instructions/AddStaticHTML.instruction';
|
|
24
25
|
export * from './instructions/AddStyle.instruction';
|
|
25
26
|
export * from './instructions/AddText.instruction';
|
|
26
27
|
export * from './instructions/HideElement.instruction';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { A_Frame } from "@adaas/a-frame/core"
|
|
2
|
+
import { AreDeclaration, AreMutation, AreInstructionSerialized } from "@adaas/are";
|
|
3
|
+
import { AreHtmlAddStaticHTMLInstructionPayload } from "./AreHTML.instructions.types";
|
|
4
|
+
import { AreHTMLInstructions } from "./AreHTML.instructions.constants";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@A_Frame.Define({
|
|
8
|
+
namespace: 'a-are-html',
|
|
9
|
+
description: 'Materialises a fully static subtree (a "static island") onto its parent element in a single pass via browser-parsed innerHTML / a cached <template> clone. Apply injects the markup; revert clears it. Decodes HTML entities (e.g. ) for free.'
|
|
10
|
+
})
|
|
11
|
+
export class AddStaticHTMLInstruction extends AreMutation<AreHtmlAddStaticHTMLInstructionPayload> {
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
parent: AreDeclaration,
|
|
15
|
+
props: AreHtmlAddStaticHTMLInstructionPayload | AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>
|
|
16
|
+
) {
|
|
17
|
+
if ('aseid' in props) {
|
|
18
|
+
super(props as AreInstructionSerialized<AreHtmlAddStaticHTMLInstructionPayload>);
|
|
19
|
+
} else {
|
|
20
|
+
super(AreHTMLInstructions.AddStaticHTML, parent, props);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -34,6 +34,15 @@ export type AreHtmlAddStyleInstructionPayload = {
|
|
|
34
34
|
styles: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
export type AreHtmlAddStaticHTMLInstructionPayload = {
|
|
38
|
+
/**
|
|
39
|
+
* Verbatim inner markup of a static island, materialised on the parent
|
|
40
|
+
* element in a single pass (browser-parsed `innerHTML` / cached `<template>`
|
|
41
|
+
* clone). Decodes HTML entities (e.g. ` `) for free via the parser.
|
|
42
|
+
*/
|
|
43
|
+
html: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
export type AreHtmlHideInstructionPayload = {
|
|
38
47
|
/**
|
|
39
48
|
* Optional explicit display value to restore when the element becomes
|