@adaas/are-html 0.0.22 → 0.0.24
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/dist/browser/index.d.mts +194 -10
- package/dist/browser/index.mjs +696 -245
- 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 +18 -1
- package/dist/node/directives/AreDirectiveFor.directive.d.ts +18 -1
- package/dist/node/directives/AreDirectiveFor.directive.js +57 -9
- package/dist/node/directives/AreDirectiveFor.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveFor.directive.mjs +57 -9
- package/dist/node/directives/AreDirectiveFor.directive.mjs.map +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.d.mts +18 -2
- package/dist/node/directives/AreDirectiveIf.directive.d.ts +18 -2
- package/dist/node/directives/AreDirectiveIf.directive.js +29 -6
- package/dist/node/directives/AreDirectiveIf.directive.js.map +1 -1
- package/dist/node/directives/AreDirectiveIf.directive.mjs +29 -6
- package/dist/node/directives/AreDirectiveIf.directive.mjs.map +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 +4 -2
- package/dist/node/engine/AreHTML.compiler.d.ts +4 -2
- package/dist/node/engine/AreHTML.compiler.js +11 -4
- package/dist/node/engine/AreHTML.compiler.js.map +1 -1
- package/dist/node/engine/AreHTML.compiler.mjs +11 -4
- 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 +1 -8
- package/dist/node/engine/AreHTML.lifecycle.d.ts +1 -8
- package/dist/node/engine/AreHTML.lifecycle.js +29 -44
- package/dist/node/engine/AreHTML.lifecycle.js.map +1 -1
- package/dist/node/engine/AreHTML.lifecycle.mjs +29 -44
- 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/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.map +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/{mqh9ryml-xat335.js → mqiw5sqa-ypckmj.js} +403 -57
- package/examples/for-perf/dist/index.html +1 -1
- package/examples/for-perf/dist/{mqh9ryfo-6a8d0o.js → mqp8i2py-vltsx0.js} +3030 -2474
- package/examples/lazy-loading/README.md +76 -0
- package/examples/lazy-loading/concept.ts +55 -0
- package/examples/lazy-loading/containers/UI.container.ts +215 -0
- package/examples/lazy-loading/dist/app.js +3803 -0
- package/examples/{for-perf/dist/mqh9ryfq-4pf5cv.js → lazy-loading/dist/chunks/chunk-6K72IBO4.js} +2708 -5476
- package/examples/lazy-loading/dist/index.html +36 -0
- package/examples/lazy-loading/dist/lazy/about-page.js +59 -0
- package/examples/lazy-loading/dist/lazy/reports-page.js +65 -0
- package/examples/lazy-loading/dist/lazy/settings-page.js +54 -0
- package/examples/lazy-loading/public/index.html +36 -0
- package/examples/lazy-loading/src/components/AppShell.component.ts +44 -0
- package/examples/lazy-loading/src/components/HomePage.component.ts +59 -0
- package/examples/lazy-loading/src/components/LazyOutlet.component.ts +108 -0
- package/examples/lazy-loading/src/components/NavBar.component.ts +98 -0
- package/examples/lazy-loading/src/concept.ts +116 -0
- package/examples/lazy-loading/src/lazy/AboutPage.component.ts +54 -0
- package/examples/lazy-loading/src/lazy/ReportsPage.component.ts +56 -0
- package/examples/lazy-loading/src/lazy/SettingsPage.component.ts +45 -0
- package/examples/lazy-loading/src/runtime/ComponentManifest.fragment.ts +61 -0
- package/examples/lazy-loading/src/runtime/LazyComponentResolver.fragment.ts +77 -0
- package/examples/os-desktop/README.md +91 -0
- package/examples/os-desktop/concept.ts +54 -0
- package/examples/os-desktop/containers/OS.container.ts +198 -0
- package/examples/os-desktop/containers/apps/AppBackend.ts +29 -0
- package/examples/os-desktop/containers/apps/GanttApp.backend.ts +56 -0
- package/examples/os-desktop/containers/apps/MarketingApp.backend.ts +68 -0
- package/examples/os-desktop/dist/app.js +4410 -0
- package/examples/os-desktop/dist/apps/gantt/app.js +271 -0
- package/examples/os-desktop/dist/apps/marketing/app.js +346 -0
- package/examples/{for-perf/dist/mqh9ryde-m243t8.js → os-desktop/dist/chunks/chunk-6K72IBO4.js} +2708 -5476
- package/examples/os-desktop/dist/chunks/chunk-EIIGUL6N.js +30 -0
- package/examples/os-desktop/dist/chunks/chunk-WOH7L5UR.js +30 -0
- package/examples/os-desktop/dist/index.html +33 -0
- package/examples/os-desktop/public/index.html +33 -0
- package/examples/os-desktop/src/apps/gantt/GanttApp.component.ts +41 -0
- package/examples/os-desktop/src/apps/gantt/GanttChart.component.ts +126 -0
- package/examples/os-desktop/src/apps/gantt/GanttStore.ts +47 -0
- package/examples/os-desktop/src/apps/gantt/GanttToolbar.component.ts +73 -0
- package/examples/os-desktop/src/apps/gantt/index.ts +13 -0
- package/examples/os-desktop/src/apps/marketing/MarketingApp.component.ts +53 -0
- package/examples/os-desktop/src/apps/marketing/MarketingStore.ts +34 -0
- package/examples/os-desktop/src/apps/marketing/PostEditor.component.ts +153 -0
- package/examples/os-desktop/src/apps/marketing/PostPreview.component.ts +110 -0
- package/examples/os-desktop/src/apps/marketing/index.ts +16 -0
- package/examples/os-desktop/src/concept.ts +126 -0
- package/examples/os-desktop/src/os/AppStage.component.ts +112 -0
- package/examples/os-desktop/src/os/AppWindow.component.ts +102 -0
- package/examples/os-desktop/src/os/Desktop.component.ts +106 -0
- package/examples/os-desktop/src/os/Dock.component.ts +174 -0
- package/examples/os-desktop/src/os/Hud.component.ts +83 -0
- package/examples/os-desktop/src/os/Launchpad.component.ts +191 -0
- package/examples/os-desktop/src/os/MenuBar.component.ts +156 -0
- package/examples/os-desktop/src/runtime/AppComponentResolver.fragment.ts +121 -0
- package/examples/os-desktop/src/runtime/AppRegistry.fragment.ts +104 -0
- package/examples/os-desktop/src/signals/MouseState.signal.ts +34 -0
- package/examples/os-desktop/src/signals/OSRoute.signal.ts +37 -0
- package/examples/os-desktop/src/signals/SelectionState.signal.ts +34 -0
- package/examples/signal-routing/dist/index.html +1 -1
- package/examples/signal-routing/dist/{mqh9ryc9-dkcbkx.js → mqp8hgce-4d6rh0.js} +3196 -2640
- package/package.json +13 -9
- package/src/directives/AreDirectiveFor.directive.ts +99 -16
- package/src/directives/AreDirectiveIf.directive.ts +33 -4
- package/src/engine/AreHTML.compiler.ts +25 -2
- 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 +81 -74
- package/src/engine/AreHTML.tokenizer.ts +30 -1
- 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 +3 -3
- package/tests/PropPropagation.test.ts +181 -0
- package/tests/StaticIsland.test.ts +115 -0
- package/tests/jest.setup.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adaas/are-html",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
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",
|
|
@@ -71,6 +71,8 @@
|
|
|
71
71
|
"example:jumpstart": "nodemon ./examples/jumpstart/concept.ts",
|
|
72
72
|
"example:dashboard": "nodemon ./examples/dashboard/concept.ts",
|
|
73
73
|
"example:signal-routing": "nodemon ./examples/signal-routing/concept.ts",
|
|
74
|
+
"example:lazy-loading": "nodemon ./examples/lazy-loading/concept.ts",
|
|
75
|
+
"example:os-desktop": "nodemon ./examples/os-desktop/concept.ts",
|
|
74
76
|
"example:component-styles": "nodemon ./examples/component-styles/concept.ts",
|
|
75
77
|
"example:auxta": "nodemon ./examples/auxta/concept.ts",
|
|
76
78
|
"example:for-perf": "nodemon ./examples/for-perf/concept.ts",
|
|
@@ -83,16 +85,16 @@
|
|
|
83
85
|
"build": "tsup --config tsup.config.ts"
|
|
84
86
|
},
|
|
85
87
|
"peerDependencies": {
|
|
86
|
-
"@adaas/a-concept": "^0.3.
|
|
87
|
-
"@adaas/a-frame": "^0.1.
|
|
88
|
-
"@adaas/a-utils": "^0.3.
|
|
89
|
-
"@adaas/are": "^0.0.
|
|
88
|
+
"@adaas/a-concept": "^0.3.30",
|
|
89
|
+
"@adaas/a-frame": "^0.1.18",
|
|
90
|
+
"@adaas/a-utils": "^0.3.35",
|
|
91
|
+
"@adaas/are": "^0.0.25"
|
|
90
92
|
},
|
|
91
93
|
"devDependencies": {
|
|
92
|
-
"@adaas/a-concept": "^0.3.
|
|
93
|
-
"@adaas/a-frame": "^0.1.
|
|
94
|
-
"@adaas/a-utils": "^0.3.
|
|
95
|
-
"@adaas/are": "^0.0.
|
|
94
|
+
"@adaas/a-concept": "^0.3.30",
|
|
95
|
+
"@adaas/a-frame": "^0.1.18",
|
|
96
|
+
"@adaas/a-utils": "^0.3.35",
|
|
97
|
+
"@adaas/are": "^0.0.25",
|
|
96
98
|
"@types/chai": "^4.3.14",
|
|
97
99
|
"@types/jest": "^29.5.12",
|
|
98
100
|
"@types/mocha": "^10.0.6",
|
|
@@ -101,6 +103,8 @@
|
|
|
101
103
|
"chai": "^5.1.0",
|
|
102
104
|
"dotenv": "^16.4.5",
|
|
103
105
|
"jest": "^29.7.0",
|
|
106
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
107
|
+
"jsdom": "^29.1.1",
|
|
104
108
|
"mocha": "^10.4.0",
|
|
105
109
|
"nodemon": "^3.1.4",
|
|
106
110
|
"ts-jest": "^29.1.2",
|
|
@@ -8,6 +8,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
10
|
import { AreSchedulerHelper } from "@adaas/are-html/helpers/AreScheduler.helper";
|
|
11
|
+
import { AreHTMLEngineContext } from "@adaas/are-html/context";
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
type AreForExpression = {
|
|
@@ -109,7 +110,12 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
109
110
|
* Parse the $for expression and evaluate the source array.
|
|
110
111
|
*/
|
|
111
112
|
const { key, index, arrayExpr } = this.parseExpression(attribute.content);
|
|
112
|
-
|
|
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);
|
|
113
119
|
|
|
114
120
|
attribute.value = array;
|
|
115
121
|
|
|
@@ -193,9 +199,12 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
193
199
|
* Re-evaluate the source array.
|
|
194
200
|
*/
|
|
195
201
|
const { key, index, arrayExpr, trackExpr } = this.parseExpression(attribute.content);
|
|
196
|
-
const newArray = this.resolveArray(store, arrayExpr, attribute.content);
|
|
197
|
-
|
|
198
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
|
+
|
|
199
208
|
const currentChildren = [...owner.children] as AreHTMLNode[];
|
|
200
209
|
|
|
201
210
|
attribute.value = newArray;
|
|
@@ -239,15 +248,25 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
239
248
|
// render, so it is deferred into the time-sliced loop below alongside
|
|
240
249
|
// transform/compile/mount. Existing (keyed) children are reconciled in
|
|
241
250
|
// place synchronously — that is cheap and keeps reused rows stable.
|
|
242
|
-
const toCreate: Array<{ item: any; idx: number }> = [];
|
|
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);
|
|
243
260
|
|
|
244
261
|
for (let i = 0; i < newArray.length; i++) {
|
|
245
262
|
const item = newArray[i];
|
|
246
263
|
const k = computeKey(item, i);
|
|
264
|
+
orderedKeys[i] = k;
|
|
247
265
|
const existing = childByKey.get(k);
|
|
248
266
|
|
|
249
267
|
if (existing) {
|
|
250
268
|
remaining.delete(existing);
|
|
269
|
+
finalByKey.set(k, existing);
|
|
251
270
|
|
|
252
271
|
let directiveContext = existing.scope.resolveFlat(AreDirectiveContext);
|
|
253
272
|
if (!directiveContext) {
|
|
@@ -260,7 +279,7 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
260
279
|
[index || 'index']: i,
|
|
261
280
|
};
|
|
262
281
|
} else {
|
|
263
|
-
toCreate.push({ item, idx: i });
|
|
282
|
+
toCreate.push({ item, idx: i, key: k });
|
|
264
283
|
}
|
|
265
284
|
}
|
|
266
285
|
|
|
@@ -274,12 +293,13 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
274
293
|
}
|
|
275
294
|
|
|
276
295
|
// ── 4. Create + mount the new item nodes. ───────────────────────────
|
|
277
|
-
// `spawnItemNode` appends to `owner.children` immediately
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
const createItem = (desc: { item: any; idx: number }) => {
|
|
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 }) => {
|
|
282
301
|
const child = this.spawnItemNode(attribute.template!, owner, key, index, desc.item, desc.idx);
|
|
302
|
+
finalByKey.set(desc.key, child);
|
|
283
303
|
child.transform();
|
|
284
304
|
child.compile();
|
|
285
305
|
// While detached, stop after compile: the item's instructions are
|
|
@@ -292,6 +312,8 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
292
312
|
// Small lists → fully synchronous, identical to the previous behavior.
|
|
293
313
|
if (toCreate.length <= AreDirectiveFor.SYNC_THRESHOLD) {
|
|
294
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);
|
|
295
317
|
return this.finishUpdate(attribute, store, scene, state);
|
|
296
318
|
}
|
|
297
319
|
|
|
@@ -325,12 +347,55 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
325
347
|
});
|
|
326
348
|
}
|
|
327
349
|
|
|
350
|
+
// ── 5. Reorder live DOM to match the source array order ──────────
|
|
351
|
+
if (attached) this.reconcileOrder(owner, orderedKeys, finalByKey);
|
|
328
352
|
return this.finishUpdate(attribute, store, scene, state);
|
|
329
353
|
};
|
|
330
354
|
|
|
331
355
|
return processChunk();
|
|
332
356
|
}
|
|
333
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
|
+
|
|
334
399
|
/**
|
|
335
400
|
* Completes an update pass. If another update() arrived while a chunked
|
|
336
401
|
* render was streaming, run exactly one more pass now from the latest store
|
|
@@ -441,14 +506,32 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
441
506
|
* Supports both plain key lookups and function-call expressions:
|
|
442
507
|
* items → store.get('items')
|
|
443
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.
|
|
444
515
|
*/
|
|
445
|
-
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
|
+
|
|
446
529
|
let result: any;
|
|
447
530
|
const callMatch = arrayExpr.match(/^([^(]+)\((.+)\)$/);
|
|
448
531
|
|
|
449
532
|
if (callMatch) {
|
|
450
533
|
const fnName = callMatch[1].trim();
|
|
451
|
-
const fn =
|
|
534
|
+
const fn = getRoot(fnName);
|
|
452
535
|
|
|
453
536
|
if (typeof fn !== 'function')
|
|
454
537
|
throw new AreCompilerError({
|
|
@@ -465,14 +548,14 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
465
548
|
const stripped = arg.replace(/\?$/, '');
|
|
466
549
|
if (stripped.includes('.')) {
|
|
467
550
|
const parts = stripped.split('.').map(p => p.replace(/\?$/, ''));
|
|
468
|
-
let val: any =
|
|
551
|
+
let val: any = getRoot(parts[0]);
|
|
469
552
|
for (let j = 1; j < parts.length; j++) {
|
|
470
553
|
if (val == null) return undefined;
|
|
471
554
|
val = val[parts[j]];
|
|
472
555
|
}
|
|
473
556
|
return val ?? undefined;
|
|
474
557
|
}
|
|
475
|
-
return
|
|
558
|
+
return getRoot(stripped);
|
|
476
559
|
});
|
|
477
560
|
|
|
478
561
|
result = (fn as Function)(...resolvedArgs);
|
|
@@ -481,13 +564,13 @@ export class AreDirectiveFor extends AreDirective {
|
|
|
481
564
|
// Strip optional-chaining `?` suffix from each segment so that
|
|
482
565
|
// `record?.keywords` resolves the same as `record.keywords`.
|
|
483
566
|
const parts = arrayExpr.split('.').map(p => p.replace(/\?$/, ''));
|
|
484
|
-
result =
|
|
567
|
+
result = getRoot(parts[0]);
|
|
485
568
|
for (let i = 1; i < parts.length; i++) {
|
|
486
569
|
if (result == null) break;
|
|
487
570
|
result = result[parts[i]];
|
|
488
571
|
}
|
|
489
572
|
} else {
|
|
490
|
-
result =
|
|
573
|
+
result = getRoot(arrayExpr);
|
|
491
574
|
}
|
|
492
575
|
|
|
493
576
|
// null / undefined from optional-chaining expressions (e.g. `record?.keywords`)
|
|
@@ -96,9 +96,7 @@ export class AreDirectiveIf extends AreDirective {
|
|
|
96
96
|
* 1. Extract the value from the store based on the attribute content
|
|
97
97
|
* (which is the path to the value in the store)
|
|
98
98
|
*/
|
|
99
|
-
attribute.value =
|
|
100
|
-
...(directiveContext?.scope || {}),
|
|
101
|
-
});
|
|
99
|
+
attribute.value = this.evaluateCondition(syntax, attribute, store, directiveContext);
|
|
102
100
|
|
|
103
101
|
/**
|
|
104
102
|
* 2. If the value is falsy, remove the node from the scene by planning a RemoveElement instruction.
|
|
@@ -127,6 +125,7 @@ export class AreDirectiveIf extends AreDirective {
|
|
|
127
125
|
@A_Inject(A_Scope) scope: A_Scope,
|
|
128
126
|
@A_Inject(AreSyntax) syntax: AreSyntax,
|
|
129
127
|
@A_Inject(AreScene) scene: AreScene,
|
|
128
|
+
@A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
|
|
130
129
|
...args: any[]
|
|
131
130
|
): void {
|
|
132
131
|
/**
|
|
@@ -134,7 +133,7 @@ export class AreDirectiveIf extends AreDirective {
|
|
|
134
133
|
* (which is the path to the value in the store)
|
|
135
134
|
*/
|
|
136
135
|
const previous = !!attribute.value;
|
|
137
|
-
const next =
|
|
136
|
+
const next = this.evaluateCondition(syntax, attribute, store, directiveContext);
|
|
138
137
|
attribute.value = next;
|
|
139
138
|
|
|
140
139
|
// Skip when truthiness has not changed — avoids redundant mount/unmount.
|
|
@@ -149,4 +148,34 @@ export class AreDirectiveIf extends AreDirective {
|
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Evaluates the `$if` condition defensively.
|
|
153
|
+
*
|
|
154
|
+
* A condition can reference data that is momentarily unavailable — most
|
|
155
|
+
* commonly a nested `$if` (e.g. `$if="selected.fields.length"`) living
|
|
156
|
+
* inside a parent `$if="selected"` whose object has just become `null`.
|
|
157
|
+
* Because the nested directive is still subscribed to the store, its
|
|
158
|
+
* update fires on that same change and the raw expression would throw
|
|
159
|
+
* `Cannot read properties of null`, crashing the whole update pipeline.
|
|
160
|
+
*
|
|
161
|
+
* Treating an evaluation error as `false` is the correct contract for a
|
|
162
|
+
* conditional: if the condition cannot be resolved, the subtree simply
|
|
163
|
+
* stays hidden until the referenced data is present again (at which point
|
|
164
|
+
* the parent `$if` re-activates and re-evaluates this one).
|
|
165
|
+
*/
|
|
166
|
+
private evaluateCondition(
|
|
167
|
+
syntax: AreSyntax,
|
|
168
|
+
attribute: AreDirectiveAttribute,
|
|
169
|
+
store: AreStore,
|
|
170
|
+
directiveContext?: AreDirectiveContext,
|
|
171
|
+
): boolean {
|
|
172
|
+
try {
|
|
173
|
+
return !!syntax.evaluate(attribute.content, store, {
|
|
174
|
+
...(directiveContext?.scope || {}),
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
152
181
|
}
|
|
@@ -13,7 +13,9 @@ 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";
|
|
18
|
+
import { AreDirectiveContext } from "@adaas/are-html/directive/AreDirective.context";
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
|
|
@@ -39,6 +41,18 @@ export class AreHTMLCompiler extends AreCompiler {
|
|
|
39
41
|
): void {
|
|
40
42
|
super.compile(node, scene, logger, ...args);
|
|
41
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Static-island materialisation. When the tokenizer flagged this node as
|
|
46
|
+
* a static island its inner subtree was never exploded into child nodes,
|
|
47
|
+
* so there is nothing for the base compiler to walk. Emit a single
|
|
48
|
+
* AddStaticHTML instruction carrying the captured inner markup; the
|
|
49
|
+
* interpreter injects it onto the host element in one pass (and decodes
|
|
50
|
+
* HTML entities for free).
|
|
51
|
+
*/
|
|
52
|
+
if (node.isStaticIsland && scene.host) {
|
|
53
|
+
scene.plan(new AddStaticHTMLInstruction(scene.host, { html: node.staticInnerHTML! }));
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
if (node.styles?.styles) {
|
|
43
57
|
const host = scene.host;
|
|
44
58
|
if (host) {
|
|
@@ -206,6 +220,7 @@ export class AreHTMLCompiler extends AreCompiler {
|
|
|
206
220
|
@A_Inject(AreStore) parentStore: AreStore,
|
|
207
221
|
@A_Inject(AreStore) store: AreStore,
|
|
208
222
|
@A_Inject(AreSyntax) syntax: AreSyntax,
|
|
223
|
+
@A_Inject(AreDirectiveContext) directiveContext?: AreDirectiveContext,
|
|
209
224
|
...args: any[]
|
|
210
225
|
) {
|
|
211
226
|
if (!scene.host)
|
|
@@ -249,13 +264,21 @@ export class AreHTMLCompiler extends AreCompiler {
|
|
|
249
264
|
return value;
|
|
250
265
|
};
|
|
251
266
|
|
|
267
|
+
// Item-scoped variables from an enclosing directive (the `item`/`index`
|
|
268
|
+
// of a `$for`, or any scope a `$if` merged in) so a prop binding like
|
|
269
|
+
// `:title="item.name"` resolves the loop variable — checked BEFORE the
|
|
270
|
+
// store, exactly like plain attribute bindings do in the interpreter.
|
|
271
|
+
// Read `.scope` lazily inside each evaluation so keyed `$for` updates
|
|
272
|
+
// that reassign the context's scope are always reflected.
|
|
273
|
+
const directiveScope = () => directiveContext?.scope ?? {};
|
|
274
|
+
|
|
252
275
|
// The watcher entity below is registered against parentStore so that
|
|
253
276
|
// updates to the bound expression in the parent flow into the child store.
|
|
254
277
|
const watcher = {
|
|
255
278
|
update: () => {
|
|
256
279
|
try {
|
|
257
280
|
parentStore.watch(watcher);
|
|
258
|
-
const next = coerce(syntax.evaluate(attribute.content, parentStore));
|
|
281
|
+
const next = coerce(syntax.evaluate(attribute.content, parentStore, directiveScope()));
|
|
259
282
|
parentStore.unwatch(watcher);
|
|
260
283
|
store.set(propName!, next);
|
|
261
284
|
} catch (e) {
|
|
@@ -266,7 +289,7 @@ export class AreHTMLCompiler extends AreCompiler {
|
|
|
266
289
|
|
|
267
290
|
// Initial read with watch active so dependencies are recorded.
|
|
268
291
|
parentStore.watch(watcher);
|
|
269
|
-
const initial = coerce(syntax.evaluate(attribute.content, parentStore));
|
|
292
|
+
const initial = coerce(syntax.evaluate(attribute.content, parentStore, directiveScope()));
|
|
270
293
|
parentStore.unwatch(watcher);
|
|
271
294
|
|
|
272
295
|
store.set(propName, initial);
|
|
@@ -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
|
+
|