@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
package/jest.config.ts
CHANGED
|
@@ -17,6 +17,7 @@ const config: Config.InitialOptions = {
|
|
|
17
17
|
"@adaas/are-html/instructions/(.*)": "<rootDir>/src/instructions/$1",
|
|
18
18
|
"@adaas/are-html/watchers/(.*)": "<rootDir>/src/watchers/$1",
|
|
19
19
|
"@adaas/are-html/signals/(.*)": "<rootDir>/src/signals/$1",
|
|
20
|
+
"@adaas/are-html/helpers/(.*)": "<rootDir>/src/helpers/$1",
|
|
20
21
|
// Custom lib exports
|
|
21
22
|
"@adaas/are-html/style/(.*)": "<rootDir>/src/lib/AreStyle/$1",
|
|
22
23
|
"@adaas/are-html/directive/(.*)": "<rootDir>/src/lib/AreDirective/$1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaas/are-html",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"description": "A-Concept Rendering Engine (ARE) is a powerful rendering engine designed to work seamlessly with the A-Concept framework. This library provides an HTML engine implementation of ARE, enabling developers to create dynamic and interactive user interfaces for web applications using standard HTML syntax.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a-concept",
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
"example:signal-routing": "nodemon ./examples/signal-routing/concept.ts",
|
|
74
74
|
"example:component-styles": "nodemon ./examples/component-styles/concept.ts",
|
|
75
75
|
"example:auxta": "nodemon ./examples/auxta/concept.ts",
|
|
76
|
+
"example:for-perf": "nodemon ./examples/for-perf/concept.ts",
|
|
76
77
|
"test:test": "nodemon ./src/test/app.ts",
|
|
77
78
|
"release": " npm run build && git add . && git commit -m \"new version created :: $(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g')\" && npm version patch && npm publish --access public",
|
|
78
79
|
"preversion": "echo test",
|
|
@@ -82,16 +83,16 @@
|
|
|
82
83
|
"build": "tsup --config tsup.config.ts"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
85
|
-
"@adaas/a-concept": "^0.3.
|
|
86
|
-
"@adaas/a-frame": "^0.1.
|
|
87
|
-
"@adaas/a-utils": "^0.3.
|
|
88
|
-
"@adaas/are": "^0.0.
|
|
86
|
+
"@adaas/a-concept": "^0.3.29",
|
|
87
|
+
"@adaas/a-frame": "^0.1.18",
|
|
88
|
+
"@adaas/a-utils": "^0.3.34",
|
|
89
|
+
"@adaas/are": "^0.0.24"
|
|
89
90
|
},
|
|
90
91
|
"devDependencies": {
|
|
91
|
-
"@adaas/a-concept": "^0.3.
|
|
92
|
-
"@adaas/a-frame": "^0.1.
|
|
93
|
-
"@adaas/a-utils": "^0.3.
|
|
94
|
-
"@adaas/are": "^0.0.
|
|
92
|
+
"@adaas/a-concept": "^0.3.29",
|
|
93
|
+
"@adaas/a-frame": "^0.1.18",
|
|
94
|
+
"@adaas/a-utils": "^0.3.34",
|
|
95
|
+
"@adaas/are": "^0.0.24",
|
|
95
96
|
"@types/chai": "^4.3.14",
|
|
96
97
|
"@types/jest": "^29.5.12",
|
|
97
98
|
"@types/mocha": "^10.0.6",
|
|
@@ -7,6 +7,8 @@ import { AddCommentInstruction } from "@adaas/are-html/instructions/AddComment.i
|
|
|
7
7
|
import { AreHTMLNode } from "@adaas/are-html/node";
|
|
8
8
|
import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
|
|
9
9
|
import { A_Frame } from "@adaas/a-frame/core";
|
|
10
|
+
import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
|
|
11
|
+
import { AreHTMLEngineContext } from "@adaas/are-html/context";
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
type AreForExpression = {
|
|
@@ -17,6 +19,12 @@ type AreForExpression = {
|
|
|
17
19
|
trackExpr: string | undefined;
|
|
18
20
|
};
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Per-`$for` reentrancy state used to serialize chunked (async) renders.
|
|
24
|
+
* Keyed by the directive attribute instance (one per `$for` in the template).
|
|
25
|
+
*/
|
|
26
|
+
type AreForRenderState = { running: boolean; pending: boolean };
|
|
27
|
+
|
|
20
28
|
|
|
21
29
|
@A_Frame.Define({
|
|
22
30
|
namespace: 'a-are-html',
|
|
@@ -25,6 +33,32 @@ type AreForExpression = {
|
|
|
25
33
|
@AreDirective.Priority(1)
|
|
26
34
|
export class AreDirectiveFor extends AreDirective {
|
|
27
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Lists whose number of NEW item nodes is at or below this threshold render
|
|
38
|
+
* fully synchronously — byte-for-byte the previous behavior. Typical UIs
|
|
39
|
+
* (menus, small tables) are therefore completely unaffected; only genuinely
|
|
40
|
+
* large lists pay the (tiny) scheduling cost to keep the main thread responsive.
|
|
41
|
+
*/
|
|
42
|
+
private static readonly SYNC_THRESHOLD = 100;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Per-chunk time budget (ms). During a large-list render we mount item nodes
|
|
46
|
+
* until this much time has elapsed, then yield to the browser so it can paint
|
|
47
|
+
* and process input before the next chunk. ~16ms targets one animation frame.
|
|
48
|
+
*/
|
|
49
|
+
private static readonly CHUNK_BUDGET_MS = 16;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Per-attribute serialization state. A new update() that arrives while a
|
|
53
|
+
* chunked render of the SAME `$for` is still in flight does NOT start a second
|
|
54
|
+
* concurrent pass (which could interleave mutations on the shared children
|
|
55
|
+
* list); instead it marks `pending` and the in-flight run re-runs once more
|
|
56
|
+
* with the latest data when it finishes. This guarantees the children list is
|
|
57
|
+
* only ever mutated by one pass at a time and the final state always reflects
|
|
58
|
+
* the most recent store value.
|
|
59
|
+
*/
|
|
60
|
+
private static readonly renderState = new WeakMap<object, AreForRenderState>();
|
|
61
|
+
|
|
28
62
|
|
|
29
63
|
@AreDirective.Transform
|
|
30
64
|
transform(
|
|
@@ -76,7 +110,12 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
76
110
|
* Parse the $for expression and evaluate the source array.
|
|
77
111
|
*/
|
|
78
112
|
const { key, index, arrayExpr } = this.parseExpression(attribute.content);
|
|
79
|
-
|
|
113
|
+
// Item-scoped variables from an enclosing directive (e.g. the `row` of an
|
|
114
|
+
// outer `$for`) so a nested `$for="cell in row.cells"` resolves correctly.
|
|
115
|
+
// Use resolve() (not resolveFlat) so the ENCLOSING item's context — which
|
|
116
|
+
// lives on an ancestor scope, not on this directive's own node — is found.
|
|
117
|
+
const contextScope = attribute.owner.scope.resolve(AreDirectiveContext)?.scope || {};
|
|
118
|
+
const array = this.resolveArray(store, arrayExpr, attribute.content, contextScope);
|
|
80
119
|
|
|
81
120
|
attribute.value = array;
|
|
82
121
|
|
|
@@ -122,14 +161,50 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
122
161
|
@A_Inject(AreStore) store: AreStore,
|
|
123
162
|
@A_Inject(AreScene) scene: AreScene,
|
|
124
163
|
...args: any[]
|
|
125
|
-
): void {
|
|
164
|
+
): void | Promise<void> {
|
|
165
|
+
/**
|
|
166
|
+
* Serialize chunked renders per `$for`. If a previous large-list render
|
|
167
|
+
* is still streaming item nodes across macrotasks, do NOT start a second
|
|
168
|
+
* concurrent pass — that would interleave two diffs over the same shared
|
|
169
|
+
* children list (and leave half-compiled item nodes that the next diff
|
|
170
|
+
* would wrongly "reuse"). Mark a pass as pending instead; the in-flight
|
|
171
|
+
* run re-diffs once more from the latest store value when it completes.
|
|
172
|
+
*/
|
|
173
|
+
let state = AreDirectiveFor.renderState.get(attribute);
|
|
174
|
+
if (!state) {
|
|
175
|
+
state = { running: false, pending: false };
|
|
176
|
+
AreDirectiveFor.renderState.set(attribute, state);
|
|
177
|
+
}
|
|
178
|
+
if (state.running) {
|
|
179
|
+
state.pending = true;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return this.performUpdate(attribute, store, scene, state);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Core of the `$for` update: re-diff the source array against the current
|
|
188
|
+
* children, reconcile reused/removed items, then mount the new ones (small
|
|
189
|
+
* lists synchronously, large lists time-sliced). Never called while another
|
|
190
|
+
* pass for the same `$for` is in flight (see `update`).
|
|
191
|
+
*/
|
|
192
|
+
private performUpdate(
|
|
193
|
+
attribute: AreDirectiveAttribute,
|
|
194
|
+
store: AreStore,
|
|
195
|
+
scene: AreScene,
|
|
196
|
+
state: AreForRenderState,
|
|
197
|
+
): void | Promise<void> {
|
|
126
198
|
/**
|
|
127
199
|
* Re-evaluate the source array.
|
|
128
200
|
*/
|
|
129
201
|
const { key, index, arrayExpr, trackExpr } = this.parseExpression(attribute.content);
|
|
130
|
-
const newArray = this.resolveArray(store, arrayExpr, attribute.content);
|
|
131
|
-
|
|
132
202
|
const owner = attribute.owner;
|
|
203
|
+
// Item-scoped variables from an enclosing directive (see transform()).
|
|
204
|
+
// resolve() walks ancestor scopes to find the enclosing item's context.
|
|
205
|
+
const contextScope = owner.scope.resolve(AreDirectiveContext)?.scope || {};
|
|
206
|
+
const newArray = this.resolveArray(store, arrayExpr, attribute.content, contextScope);
|
|
207
|
+
|
|
133
208
|
const currentChildren = [...owner.children] as AreHTMLNode[];
|
|
134
209
|
|
|
135
210
|
attribute.value = newArray;
|
|
@@ -167,17 +242,31 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
167
242
|
remaining.add(child);
|
|
168
243
|
}
|
|
169
244
|
|
|
170
|
-
// ── 2. Walk desired list; reuse existing or
|
|
171
|
-
|
|
172
|
-
|
|
245
|
+
// ── 2. Walk desired list; reuse existing or record items to create ──
|
|
246
|
+
// NOTE: new item nodes are NOT spawned here. Spawning (cloneWithScope +
|
|
247
|
+
// subtree init + scene activation) is the dominant cost of a large
|
|
248
|
+
// render, so it is deferred into the time-sliced loop below alongside
|
|
249
|
+
// transform/compile/mount. Existing (keyed) children are reconciled in
|
|
250
|
+
// place synchronously — that is cheap and keeps reused rows stable.
|
|
251
|
+
const toCreate: Array<{ item: any; idx: number; key: any }> = [];
|
|
252
|
+
|
|
253
|
+
// Final identity → node map covering BOTH reused and newly created item
|
|
254
|
+
// nodes, plus the desired key order. After all items are mounted these
|
|
255
|
+
// drive the DOM reorder pass (step 5) so the rendered order always
|
|
256
|
+
// matches the source array — making prepend / shuffle / arbitrary
|
|
257
|
+
// reorders move existing rows instead of only appending at the end.
|
|
258
|
+
const finalByKey = new Map<any, AreHTMLNode>();
|
|
259
|
+
const orderedKeys: any[] = new Array(newArray.length);
|
|
173
260
|
|
|
174
261
|
for (let i = 0; i < newArray.length; i++) {
|
|
175
262
|
const item = newArray[i];
|
|
176
263
|
const k = computeKey(item, i);
|
|
264
|
+
orderedKeys[i] = k;
|
|
177
265
|
const existing = childByKey.get(k);
|
|
178
266
|
|
|
179
267
|
if (existing) {
|
|
180
268
|
remaining.delete(existing);
|
|
269
|
+
finalByKey.set(k, existing);
|
|
181
270
|
|
|
182
271
|
let directiveContext = existing.scope.resolveFlat(AreDirectiveContext);
|
|
183
272
|
if (!directiveContext) {
|
|
@@ -189,11 +278,8 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
189
278
|
[key]: item,
|
|
190
279
|
[index || 'index']: i,
|
|
191
280
|
};
|
|
192
|
-
desired.push(existing);
|
|
193
281
|
} else {
|
|
194
|
-
|
|
195
|
-
desired.push(itemNode);
|
|
196
|
-
newOnes.push(itemNode);
|
|
282
|
+
toCreate.push({ item, idx: i, key: k });
|
|
197
283
|
}
|
|
198
284
|
}
|
|
199
285
|
|
|
@@ -206,8 +292,14 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
206
292
|
owner.removeChild(child);
|
|
207
293
|
}
|
|
208
294
|
|
|
209
|
-
// ── 4.
|
|
210
|
-
|
|
295
|
+
// ── 4. Create + mount the new item nodes. ───────────────────────────
|
|
296
|
+
// `spawnItemNode` appends to `owner.children` immediately; new rows are
|
|
297
|
+
// therefore mounted at the end (just before the anchor comment). The
|
|
298
|
+
// reorder pass (step 5) then moves any out-of-position node so the final
|
|
299
|
+
// DOM order matches the source array.
|
|
300
|
+
const createItem = (desc: { item: any; idx: number; key: any }) => {
|
|
301
|
+
const child = this.spawnItemNode(attribute.template!, owner, key, index, desc.item, desc.idx);
|
|
302
|
+
finalByKey.set(desc.key, child);
|
|
211
303
|
child.transform();
|
|
212
304
|
child.compile();
|
|
213
305
|
// While detached, stop after compile: the item's instructions are
|
|
@@ -215,6 +307,110 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
215
307
|
// the correct container once the condition becomes truthy. Mounting
|
|
216
308
|
// here would hoist the item to the nearest mounted ancestor.
|
|
217
309
|
if (attached) child.mount();
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Small lists → fully synchronous, identical to the previous behavior.
|
|
313
|
+
if (toCreate.length <= AreDirectiveFor.SYNC_THRESHOLD) {
|
|
314
|
+
for (const desc of toCreate) createItem(desc);
|
|
315
|
+
// ── 5. Reorder live DOM to match the source array order ──────────
|
|
316
|
+
if (attached) this.reconcileOrder(owner, orderedKeys, finalByKey);
|
|
317
|
+
return this.finishUpdate(attribute, store, scene, state);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Large lists → time-sliced render. Create item nodes until the frame
|
|
321
|
+
// budget elapses, then yield to the browser (zero-delay macrotask) so
|
|
322
|
+
// it can paint and stay responsive instead of blocking for the whole
|
|
323
|
+
// batch. The `state.running` flag (see `update`) prevents any other
|
|
324
|
+
// update() for this `$for` from interleaving while we stream.
|
|
325
|
+
state.running = true;
|
|
326
|
+
let cursor = 0;
|
|
327
|
+
|
|
328
|
+
const processChunk = (): void | Promise<void> => {
|
|
329
|
+
try {
|
|
330
|
+
const start = AreSchedulerHelper.now();
|
|
331
|
+
while (cursor < toCreate.length) {
|
|
332
|
+
createItem(toCreate[cursor]);
|
|
333
|
+
cursor++;
|
|
334
|
+
if (AreSchedulerHelper.now() - start >= AreDirectiveFor.CHUNK_BUDGET_MS) break;
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
// Never leave the `$for` wedged in the running state on failure,
|
|
338
|
+
// or every future update would be silently deferred forever.
|
|
339
|
+
state.running = false;
|
|
340
|
+
state.pending = false;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (cursor < toCreate.length) {
|
|
345
|
+
return new Promise<void>(resolve => {
|
|
346
|
+
AreSchedulerHelper.scheduleMacrotask(() => resolve(processChunk()));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── 5. Reorder live DOM to match the source array order ──────────
|
|
351
|
+
if (attached) this.reconcileOrder(owner, orderedKeys, finalByKey);
|
|
352
|
+
return this.finishUpdate(attribute, store, scene, state);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return processChunk();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Repositions the item nodes' DOM elements so the rendered order matches the
|
|
360
|
+
* source array order. The keyed diff (steps 1–4) reuses existing nodes in
|
|
361
|
+
* place and mounts new ones at the end; without this pass a `prepend` or
|
|
362
|
+
* `shuffle` would leave reused rows where they were and pile new rows at the
|
|
363
|
+
* bottom. We walk the desired order RIGHT-TO-LEFT, keeping a `ref` pointer to
|
|
364
|
+
* the element each item must precede (starting at the `$for` anchor comment),
|
|
365
|
+
* and only call `insertBefore` when an element is not already in position —
|
|
366
|
+
* so a plain `append` (already-correct order) performs ZERO DOM moves.
|
|
367
|
+
*/
|
|
368
|
+
private reconcileOrder(
|
|
369
|
+
owner: AreHTMLNode,
|
|
370
|
+
orderedKeys: any[],
|
|
371
|
+
finalByKey: Map<any, AreHTMLNode>,
|
|
372
|
+
): void {
|
|
373
|
+
const context = owner.scope.resolve<AreHTMLEngineContext>(AreHTMLEngineContext);
|
|
374
|
+
if (!context) return;
|
|
375
|
+
|
|
376
|
+
const anchor = context.getNodeElement(owner);
|
|
377
|
+
if (!anchor || !anchor.parentNode) return;
|
|
378
|
+
|
|
379
|
+
const parent = anchor.parentNode;
|
|
380
|
+
let ref: Node = anchor;
|
|
381
|
+
|
|
382
|
+
for (let i = orderedKeys.length - 1; i >= 0; i--) {
|
|
383
|
+
const node = finalByKey.get(orderedKeys[i]);
|
|
384
|
+
if (!node) continue;
|
|
385
|
+
|
|
386
|
+
const element = context.getNodeElement(node);
|
|
387
|
+
// Element may be missing if the item is still detached/unmounted.
|
|
388
|
+
if (!element || element.parentNode !== parent) continue;
|
|
389
|
+
|
|
390
|
+
// Already immediately before `ref` — no move needed.
|
|
391
|
+
if (element.nextSibling !== ref) {
|
|
392
|
+
parent.insertBefore(element, ref);
|
|
393
|
+
}
|
|
394
|
+
ref = element;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Completes an update pass. If another update() arrived while a chunked
|
|
401
|
+
* render was streaming, run exactly one more pass now from the latest store
|
|
402
|
+
* value so the final DOM always reflects the most recent data.
|
|
403
|
+
*/
|
|
404
|
+
private finishUpdate(
|
|
405
|
+
attribute: AreDirectiveAttribute,
|
|
406
|
+
store: AreStore,
|
|
407
|
+
scene: AreScene,
|
|
408
|
+
state: AreForRenderState,
|
|
409
|
+
): void | Promise<void> {
|
|
410
|
+
state.running = false;
|
|
411
|
+
if (state.pending) {
|
|
412
|
+
state.pending = false;
|
|
413
|
+
return this.performUpdate(attribute, store, scene, state);
|
|
218
414
|
}
|
|
219
415
|
}
|
|
220
416
|
|
|
@@ -310,14 +506,32 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
310
506
|
* Supports both plain key lookups and function-call expressions:
|
|
311
507
|
* items → store.get('items')
|
|
312
508
|
* filter(items) → store.get('filter')(store.get('items'))
|
|
509
|
+
*
|
|
510
|
+
* `contextScope` carries item-scoped variables introduced by an enclosing
|
|
511
|
+
* directive (e.g. the `row` of an outer `$for`). It is consulted BEFORE the
|
|
512
|
+
* store so a nested `$for="cell in row.cells"` resolves `row` from the
|
|
513
|
+
* parent iteration instead of looking for a (non-existent) top-level store
|
|
514
|
+
* key. Leading identifiers not present in the context fall back to the store.
|
|
313
515
|
*/
|
|
314
|
-
private resolveArray(
|
|
516
|
+
private resolveArray(
|
|
517
|
+
store: AreStore,
|
|
518
|
+
arrayExpr: string,
|
|
519
|
+
fullContent: string,
|
|
520
|
+
contextScope: Record<string, any> = {},
|
|
521
|
+
): any[] {
|
|
522
|
+
// Resolve a leading identifier from the directive context first, then
|
|
523
|
+
// the store — mirrors how bindings/interpolations evaluate scoped vars.
|
|
524
|
+
const getRoot = (rawKey: string): any => {
|
|
525
|
+
const k = rawKey.replace(/\?$/, '');
|
|
526
|
+
return (k in contextScope) ? contextScope[k] : store.get(k as any);
|
|
527
|
+
};
|
|
528
|
+
|
|
315
529
|
let result: any;
|
|
316
530
|
const callMatch = arrayExpr.match(/^([^(]+)\((.+)\)$/);
|
|
317
531
|
|
|
318
532
|
if (callMatch) {
|
|
319
533
|
const fnName = callMatch[1].trim();
|
|
320
|
-
const fn =
|
|
534
|
+
const fn = getRoot(fnName);
|
|
321
535
|
|
|
322
536
|
if (typeof fn !== 'function')
|
|
323
537
|
throw new AreCompilerError({
|
|
@@ -334,14 +548,14 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
334
548
|
const stripped = arg.replace(/\?$/, '');
|
|
335
549
|
if (stripped.includes('.')) {
|
|
336
550
|
const parts = stripped.split('.').map(p => p.replace(/\?$/, ''));
|
|
337
|
-
let val: any =
|
|
551
|
+
let val: any = getRoot(parts[0]);
|
|
338
552
|
for (let j = 1; j < parts.length; j++) {
|
|
339
553
|
if (val == null) return undefined;
|
|
340
554
|
val = val[parts[j]];
|
|
341
555
|
}
|
|
342
556
|
return val ?? undefined;
|
|
343
557
|
}
|
|
344
|
-
return
|
|
558
|
+
return getRoot(stripped);
|
|
345
559
|
});
|
|
346
560
|
|
|
347
561
|
result = (fn as Function)(...resolvedArgs);
|
|
@@ -350,13 +564,13 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
350
564
|
// Strip optional-chaining `?` suffix from each segment so that
|
|
351
565
|
// `record?.keywords` resolves the same as `record.keywords`.
|
|
352
566
|
const parts = arrayExpr.split('.').map(p => p.replace(/\?$/, ''));
|
|
353
|
-
result =
|
|
567
|
+
result = getRoot(parts[0]);
|
|
354
568
|
for (let i = 1; i < parts.length; i++) {
|
|
355
569
|
if (result == null) break;
|
|
356
570
|
result = result[parts[i]];
|
|
357
571
|
}
|
|
358
572
|
} else {
|
|
359
|
-
result =
|
|
573
|
+
result = getRoot(arrayExpr);
|
|
360
574
|
}
|
|
361
575
|
|
|
362
576
|
// null / undefined from optional-chaining expressions (e.g. `record?.keywords`)
|
|
@@ -13,6 +13,7 @@ import { AddAttributeInstruction} from "@adaas/are-html/instructions/AddAttribut
|
|
|
13
13
|
import { AddTextInstruction} from "@adaas/are-html/instructions/AddText.instruction";
|
|
14
14
|
import { AddListenerInstruction} from "@adaas/are-html/instructions/AddListener.instruction";
|
|
15
15
|
import { AddStyleInstruction } from "@adaas/are-html/instructions/AddStyle.instruction";
|
|
16
|
+
import { AddStaticHTMLInstruction } from "@adaas/are-html/instructions/AddStaticHTML.instruction";
|
|
16
17
|
import { AreHTMLNode } from "@adaas/are-html/node";
|
|
17
18
|
|
|
18
19
|
|
|
@@ -39,6 +40,18 @@ export class AreHTMLCompiler extends AreCompiler {
|
|
|
39
40
|
): void {
|
|
40
41
|
super.compile(node, scene, logger, ...args);
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Static-island materialisation. When the tokenizer flagged this node as
|
|
45
|
+
* a static island its inner subtree was never exploded into child nodes,
|
|
46
|
+
* so there is nothing for the base compiler to walk. Emit a single
|
|
47
|
+
* AddStaticHTML instruction carrying the captured inner markup; the
|
|
48
|
+
* interpreter injects it onto the host element in one pass (and decodes
|
|
49
|
+
* HTML entities for free).
|
|
50
|
+
*/
|
|
51
|
+
if (node.isStaticIsland && scene.host) {
|
|
52
|
+
scene.plan(new AddStaticHTMLInstruction(scene.host, { html: node.staticInnerHTML! }));
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
if (node.styles?.styles) {
|
|
43
56
|
const host = scene.host;
|
|
44
57
|
if (host) {
|
|
@@ -181,3 +181,145 @@ export function toDOMString(value: any): string {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
// ── Static-island detection ──────────────────────────────────────────────────
|
|
186
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Standard HTML element names that are safe to materialise wholesale via
|
|
190
|
+
* `innerHTML` / a cached `<template>` clone.
|
|
191
|
+
*
|
|
192
|
+
* The set is intentionally an allow-list of plain HTML flow/phrasing/table/list
|
|
193
|
+
* /form-display tags. Anything NOT in this set — custom elements, registered
|
|
194
|
+
* ARE components (resolved by PascalCase tag), and SVG/MathML elements — is
|
|
195
|
+
* excluded so those subtrees keep flowing through the normal per-node pipeline
|
|
196
|
+
* (SVG needs createElementNS; components need their own lifecycle).
|
|
197
|
+
*/
|
|
198
|
+
export const STANDARD_HTML_TAGS = new Set<string>([
|
|
199
|
+
// root / sections
|
|
200
|
+
'html', 'body', 'header', 'footer', 'main', 'nav', 'section', 'article',
|
|
201
|
+
'aside', 'address', 'hgroup',
|
|
202
|
+
// headings
|
|
203
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
|
204
|
+
// grouping
|
|
205
|
+
'div', 'p', 'span', 'pre', 'blockquote', 'figure', 'figcaption',
|
|
206
|
+
'hr', 'br', 'wbr',
|
|
207
|
+
// lists
|
|
208
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'menu',
|
|
209
|
+
// text-level / phrasing
|
|
210
|
+
'a', 'b', 'i', 'u', 's', 'em', 'strong', 'small', 'mark', 'abbr', 'cite',
|
|
211
|
+
'q', 'code', 'kbd', 'samp', 'var', 'sub', 'sup', 'time', 'data', 'dfn',
|
|
212
|
+
'bdi', 'bdo', 'ruby', 'rt', 'rp', 'del', 'ins',
|
|
213
|
+
// media / embedded (no special namespace handling needed)
|
|
214
|
+
'img', 'picture', 'source', 'figure', 'audio', 'video', 'track',
|
|
215
|
+
// tables
|
|
216
|
+
'table', 'caption', 'colgroup', 'col', 'thead', 'tbody', 'tfoot',
|
|
217
|
+
'tr', 'th', 'td',
|
|
218
|
+
// forms (display only — these still render fine from innerHTML)
|
|
219
|
+
'label', 'fieldset', 'legend', 'datalist', 'option', 'optgroup', 'output',
|
|
220
|
+
'progress', 'meter',
|
|
221
|
+
// interactive
|
|
222
|
+
'details', 'summary', 'dialog',
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Detects whether an inner-markup string is a fully *static island* — i.e. it
|
|
227
|
+
* contains no ARE-reactive constructs and therefore can be rendered in one shot
|
|
228
|
+
* (browser-parsed `innerHTML` / cached `<template>` clone) instead of being
|
|
229
|
+
* exploded into one AreNode per element/text/interpolation.
|
|
230
|
+
*
|
|
231
|
+
* A subtree is static iff it contains:
|
|
232
|
+
* 1. no `{{ }}` interpolations, and
|
|
233
|
+
* 2. no dynamic attributes (`$`-directive / `:`-binding / `@`-event), and
|
|
234
|
+
* 3. only standard HTML tags (no custom elements, ARE components or SVG).
|
|
235
|
+
*
|
|
236
|
+
* The scanner is quote-aware so a `:` / `@` / `$` inside an attribute *value*
|
|
237
|
+
* (e.g. `href="http://…"`, `style="color:red"`) is never mistaken for a
|
|
238
|
+
* dynamic-attribute prefix. The detector is deliberately conservative: any
|
|
239
|
+
* ambiguity resolves to `false` (skip the optimisation, keep the safe path).
|
|
240
|
+
*
|
|
241
|
+
* NOTE: pure-text content (no tags at all) is also considered static — this is
|
|
242
|
+
* what lets ` `, `&`, ` ` and friends decode correctly, since the
|
|
243
|
+
* browser HTML parser handles entities that hand-built text nodes do not.
|
|
244
|
+
*/
|
|
245
|
+
export function isStaticMarkup(inner: string): boolean {
|
|
246
|
+
if (!inner) return false;
|
|
247
|
+
// 1. interpolations make the subtree dynamic
|
|
248
|
+
if (inner.indexOf('{{') !== -1) return false;
|
|
249
|
+
|
|
250
|
+
const n = inner.length;
|
|
251
|
+
let i = 0;
|
|
252
|
+
|
|
253
|
+
while (i < n) {
|
|
254
|
+
const lt = inner.indexOf('<', i);
|
|
255
|
+
if (lt === -1) break; // remaining content is plain text — safe
|
|
256
|
+
|
|
257
|
+
// HTML comment — inert, skip over it
|
|
258
|
+
if (inner.startsWith('<!--', lt)) {
|
|
259
|
+
const end = inner.indexOf('-->', lt + 4);
|
|
260
|
+
if (end === -1) return false; // malformed
|
|
261
|
+
i = end + 3;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// closing tag, doctype or processing instruction — skip to its '>'
|
|
266
|
+
if (inner[lt + 1] === '/' || inner[lt + 1] === '!' || inner[lt + 1] === '?') {
|
|
267
|
+
const gt = inner.indexOf('>', lt);
|
|
268
|
+
if (gt === -1) return false;
|
|
269
|
+
i = gt + 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// opening tag — extract the tag name
|
|
274
|
+
const nameMatch = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(inner.slice(lt));
|
|
275
|
+
if (!nameMatch) { i = lt + 1; continue; }
|
|
276
|
+
|
|
277
|
+
const tag = nameMatch[1].toLowerCase();
|
|
278
|
+
// custom element / ARE component / non-standard (incl. SVG) → not static
|
|
279
|
+
if (tag.indexOf('-') !== -1 || !STANDARD_HTML_TAGS.has(tag)) return false;
|
|
280
|
+
|
|
281
|
+
// walk the opening tag (quote-aware) to find its closing '>' and inspect
|
|
282
|
+
// attribute-name boundaries for dynamic prefixes
|
|
283
|
+
let j = lt + nameMatch[0].length;
|
|
284
|
+
let inSingle = false;
|
|
285
|
+
let inDouble = false;
|
|
286
|
+
let atNameBoundary = true; // true right after whitespace / '/' inside a tag
|
|
287
|
+
let tagEnd = -1;
|
|
288
|
+
|
|
289
|
+
while (j < n) {
|
|
290
|
+
const ch = inner[j];
|
|
291
|
+
|
|
292
|
+
if (inDouble) {
|
|
293
|
+
if (ch === '"') inDouble = false;
|
|
294
|
+
} else if (inSingle) {
|
|
295
|
+
if (ch === "'") inSingle = false;
|
|
296
|
+
} else if (ch === '"') {
|
|
297
|
+
inDouble = true;
|
|
298
|
+
atNameBoundary = false;
|
|
299
|
+
} else if (ch === "'") {
|
|
300
|
+
inSingle = true;
|
|
301
|
+
atNameBoundary = false;
|
|
302
|
+
} else if (ch === '>') {
|
|
303
|
+
tagEnd = j;
|
|
304
|
+
break;
|
|
305
|
+
} else if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '/') {
|
|
306
|
+
atNameBoundary = true;
|
|
307
|
+
} else {
|
|
308
|
+
// a dynamic-attribute prefix only counts when it STARTS an
|
|
309
|
+
// attribute name (i.e. sits at a name boundary, outside quotes)
|
|
310
|
+
if (atNameBoundary && (ch === '$' || ch === ':' || ch === '@')) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
atNameBoundary = false;
|
|
314
|
+
}
|
|
315
|
+
j++;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (tagEnd === -1) return false; // unterminated tag — bail to safe path
|
|
319
|
+
i = tagEnd + 1;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|