arrmatura 5.2.0 → 6.3.1
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/README.md +29 -38
- package/docs/glossary.md +19 -0
- package/docs/hello.md +46 -0
- package/docs/manual.md +294 -0
- package/index.ts +21 -0
- package/package.json +16 -44
- package/src/core/Component.ts +67 -0
- package/src/core/Entitron.ts +406 -0
- package/src/core/ManifestNode.ts +139 -0
- package/src/core/resolveExpression.ts +20 -0
- package/src/registry/composition.ts +66 -0
- package/src/registry/conditionals.ts +75 -0
- package/src/registry/elementary.ts +36 -0
- package/src/registry/fragment.ts +21 -0
- package/src/registry/index.ts +69 -0
- package/src/registry/iterations.ts +122 -0
- package/src/registry/root.ts +46 -0
- package/src/registry/routing.ts +31 -0
- package/src/registry/selection.ts +57 -0
- package/src/registry/slot.ts +36 -0
- package/src/utils/compileExpression.ts +108 -0
- package/src/utils/stringify.ts +33 -0
- package/types.ts +73 -0
- package/LICENSE +0 -21
- package/commons/auth/AuthService.ts +0 -111
- package/commons/auth/UserService.ts +0 -57
- package/commons/auth/index.ts +0 -5
- package/commons/auth/user.xml +0 -64
- package/commons/components/buttons.xml +0 -25
- package/commons/components/index.ts +0 -13
- package/commons/components/inputs.xml +0 -33
- package/commons/components/layouts.xml +0 -77
- package/commons/components/media.xml +0 -21
- package/commons/components/modal.xml +0 -37
- package/commons/components/navigation.xml +0 -73
- package/commons/components/popups.xml +0 -12
- package/commons/components/searchbar.xml +0 -54
- package/commons/components/table.xml +0 -140
- package/commons/components/toasts.xml +0 -18
- package/commons/components/viewport.xml +0 -48
- package/commons/forms/FieldItem.xml +0 -20
- package/commons/forms/FormController.ts +0 -87
- package/commons/forms/Item.ts +0 -132
- package/commons/forms/ItemCollectionController.ts +0 -113
- package/commons/forms/ItemController.ts +0 -91
- package/commons/forms/MultiEnumField.xml +0 -44
- package/commons/forms/MultivalueController.ts +0 -49
- package/commons/forms/NewItemController.ts +0 -46
- package/commons/forms/SmartareaField.xml +0 -51
- package/commons/forms/SuggestionController.ts +0 -80
- package/commons/forms/SuggestionField.xml +0 -28
- package/commons/forms/fields.xml +0 -151
- package/commons/forms/form.xml +0 -37
- package/commons/forms/index.ts +0 -29
- package/commons/index.ts +0 -6
- package/commons/services/ApiClientService.ts +0 -32
- package/commons/services/ErrorHandlingService.ts +0 -11
- package/commons/services/FirebaseService.ts +0 -185
- package/commons/services/GSheetsService.ts +0 -120
- package/commons/services/HeartbeatService.ts +0 -35
- package/commons/services/Invoke.ts +0 -29
- package/commons/services/JsonpService.ts +0 -18
- package/commons/services/Loader.ts +0 -17
- package/commons/services/MetadataService.ts +0 -50
- package/commons/services/NavigationService.ts +0 -74
- package/commons/services/Service.ts +0 -62
- package/commons/services/ServiceWorker.ts +0 -111
- package/commons/services/Storage.ts +0 -55
- package/commons/services/ToastService.ts +0 -61
- package/commons/services/index.ts +0 -13
- package/core/Arrmatura.ts +0 -79
- package/core/CNode.ts +0 -111
- package/core/Component.ts +0 -424
- package/core/cnodes/composition.ts +0 -100
- package/core/cnodes/conditionals.ts +0 -52
- package/core/cnodes/elementary.ts +0 -44
- package/core/cnodes/iterations.ts +0 -92
- package/core/cnodes/routing.ts +0 -25
- package/core/compileNode.ts +0 -39
- package/core/expression.ts +0 -73
- package/core/index.ts +0 -1
- package/core/register.ts +0 -43
- package/core/utils.ts +0 -23
- package/core-web/WebPlatform.ts +0 -25
- package/core-web/attributes.ts +0 -218
- package/core-web/elements.ts +0 -81
- package/core-web/index.ts +0 -28
- package/core-web/reflow.ts +0 -30
- package/core-web/support.ts +0 -27
- package/core-web/type.ts +0 -18
- package/docs/index.js +0 -485
- package/lib/array/arrayGroupBy.ts +0 -19
- package/lib/array/arraySortBy.ts +0 -49
- package/lib/array/arrayToObject.ts +0 -18
- package/lib/array/arrayish.ts +0 -7
- package/lib/array/index.ts +0 -4
- package/lib/crypto/index.ts +0 -25
- package/lib/date/index.ts +0 -127
- package/lib/func/curry.ts +0 -11
- package/lib/func/fx.ts +0 -56
- package/lib/func/identity.ts +0 -6
- package/lib/func/nextId.ts +0 -9
- package/lib/func/noop.ts +0 -6
- package/lib/geo/index.js +0 -22
- package/lib/index.ts +0 -17
- package/lib/object/index.ts +0 -69
- package/lib/parsing/codus.ts +0 -25
- package/lib/parsing/compiler.ts +0 -26
- package/lib/parsing/scalarParse.ts +0 -25
- package/lib/parsing/urls.ts +0 -91
- package/lib/parsing/xml.ts +0 -117
- package/lib/resources/l10n.ts +0 -40
- package/lib/resources/resources.ru.ts +0 -20
- package/lib/resources/resources.ts +0 -20
- package/lib/string/index.ts +0 -58
- package/lib/support/createListeners.ts +0 -12
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { areEqual, capitalize, identity } from "ultimus";
|
|
2
|
+
import { stringify } from "../utils/stringify";
|
|
3
|
+
import type { Delta, Fn, Hash, Proc, Uid } from "ultimus/types";
|
|
4
|
+
import type { IEntitron, IManifestNode, IComponent, IPlatform } from "arrmatura/types";
|
|
5
|
+
|
|
6
|
+
type EProcedure = (c: IEntitron) => void;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Entitron is the essential the framework concept.
|
|
10
|
+
*
|
|
11
|
+
* It creates a own component instance,
|
|
12
|
+
* manages its state and behavior,
|
|
13
|
+
* and ties it up with others.
|
|
14
|
+
*/
|
|
15
|
+
export class Entitron<T extends IManifestNode = IManifestNode> implements IEntitron {
|
|
16
|
+
readonly component: IComponent;
|
|
17
|
+
$children?: Map<Uid, IEntitron>;
|
|
18
|
+
readonly root: IEntitron;
|
|
19
|
+
readonly scope: IEntitron;
|
|
20
|
+
isDone = false;
|
|
21
|
+
|
|
22
|
+
#isInited = false;
|
|
23
|
+
#state: Hash = {};
|
|
24
|
+
#initialState: Hash = {};
|
|
25
|
+
#listeners?: Set<(c: Entitron) => void>;
|
|
26
|
+
#defered?: EProcedure[] = [];
|
|
27
|
+
#weak?: Map<string, number>;
|
|
28
|
+
#refs?: Hash<IEntitron>;
|
|
29
|
+
#propFnMap?: Map<string, Proc>;
|
|
30
|
+
|
|
31
|
+
constructor(readonly platform: IPlatform, protected readonly manifest: T, private _parent?: IEntitron, scope?: IEntitron) {
|
|
32
|
+
this.scope = scope ?? this;
|
|
33
|
+
this.root = scope?.root ?? this;
|
|
34
|
+
this.#initialState = this.manifest.resolveInitialProps(this);
|
|
35
|
+
this.component = this.createComponent(this.#initialState);
|
|
36
|
+
|
|
37
|
+
if (this.refId) {
|
|
38
|
+
this.scope.addReference(this.refId, this);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get parent(): IEntitron | undefined {
|
|
43
|
+
return this._parent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get uid(): string {
|
|
47
|
+
return this.manifest.uid;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get displayName(): string {
|
|
51
|
+
return "🔹";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
createComponent(initials: Hash): IComponent {
|
|
55
|
+
// by default, a component is just a plain object.
|
|
56
|
+
return { ...initials };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// --- State
|
|
60
|
+
|
|
61
|
+
// updates component state
|
|
62
|
+
up(Δ?: Delta | Promise<any> | null | void | unknown, force = false): void {
|
|
63
|
+
if (!Δ || this.isDone) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (Δ instanceof Promise) {
|
|
68
|
+
this.upAsync(Δ, "*");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const changes = new Map<string, unknown>();
|
|
73
|
+
Object.entries(Δ).forEach(([k, v]) => {
|
|
74
|
+
if (v instanceof Promise) {
|
|
75
|
+
this.upAsync(v, k);
|
|
76
|
+
} else if (k && typeof v !== "undefined" && !areEqual(v, this.#state[k])) {
|
|
77
|
+
this.#state[k] = v;
|
|
78
|
+
changes.set(k, v);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (changes.size || force) {
|
|
83
|
+
this.stateChanged(changes);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private upAsync(promise: Promise<any>, key: string): void {
|
|
88
|
+
const racer = this.raceCondition(`set:${key}`);
|
|
89
|
+
if (key[0] !== "*") {
|
|
90
|
+
const akey = key.replace("Promise", "");
|
|
91
|
+
void promise.then((val) => racer(() => this.up({ [akey]: val })));
|
|
92
|
+
} else {
|
|
93
|
+
void promise.then((r: Delta) => racer(() => this.up(r)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private stateChanged(changes: Map<string, unknown>) {
|
|
98
|
+
if (this.component.stateChanged) {
|
|
99
|
+
this.component.stateChanged(changes);
|
|
100
|
+
} else {
|
|
101
|
+
changes.forEach((v, k) => {
|
|
102
|
+
const setter = this.component[`set${capitalize(k)}`];
|
|
103
|
+
if (typeof setter === "function") {
|
|
104
|
+
setter.call(this.component, v);
|
|
105
|
+
} else {
|
|
106
|
+
this.component[k] = v;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
this.touch();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// do recontent and notify all
|
|
114
|
+
touch() {
|
|
115
|
+
this.recontent();
|
|
116
|
+
this.notify();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// gets component property by key
|
|
120
|
+
get(propId: string): unknown {
|
|
121
|
+
const map = this.#propFnMap ?? (this.#propFnMap = new Map());
|
|
122
|
+
if (map.has(propId)) {
|
|
123
|
+
return map.get(propId)();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let fn: Fn | null = null;
|
|
127
|
+
const impl = this.component;
|
|
128
|
+
const instant = impl[propId];
|
|
129
|
+
if (instant && typeof instant === "function") {
|
|
130
|
+
const bound = instant.bind(impl);
|
|
131
|
+
fn = () => bound;
|
|
132
|
+
} else {
|
|
133
|
+
const [pk, ...path] = propId.split(".");
|
|
134
|
+
if (pk[0] === "@") {
|
|
135
|
+
path.unshift(pk.slice(1));
|
|
136
|
+
const val = this.platform.getResource(path) ?? null;
|
|
137
|
+
fn = () => val;
|
|
138
|
+
} else {
|
|
139
|
+
const gettr = impl[`get${capitalize(pk)}`];
|
|
140
|
+
const fn0 = typeof gettr === "function" ? () => gettr.call(impl) ?? null : () => impl[pk] ?? null;
|
|
141
|
+
|
|
142
|
+
fn = !path.length ? fn0 : () => path.reduce((r, p) => r?.[p] ?? null, fn0());
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
map.set(propId, fn);
|
|
147
|
+
|
|
148
|
+
return fn();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getFromScope(propId: string): unknown {
|
|
152
|
+
return this.scope.get(propId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// --- Left Arrow.
|
|
156
|
+
|
|
157
|
+
notify() {
|
|
158
|
+
this.#listeners?.forEach((fn) => fn(this));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// add listener to componet state changes
|
|
162
|
+
subscribe(handler: (src: Entitron) => any): Proc {
|
|
163
|
+
const listeners = this.#listeners ?? (this.#listeners = new Set());
|
|
164
|
+
listeners.add(handler);
|
|
165
|
+
return () => {
|
|
166
|
+
listeners.delete(handler);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- Right Arrow.
|
|
171
|
+
|
|
172
|
+
// emits action event to this component
|
|
173
|
+
emit(key: string, data: Delta): void {
|
|
174
|
+
try {
|
|
175
|
+
// when key.endsWith("...") it will produce empty target, so don't need to hande it peculiarly
|
|
176
|
+
let [refId, target] = key.split(".");
|
|
177
|
+
if (!target) {
|
|
178
|
+
target = refId;
|
|
179
|
+
refId = 'this'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ref = this.getByRef(refId);
|
|
183
|
+
if (!ref) {
|
|
184
|
+
throw new Error(`No such reference: ${refId}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!target) { // spread '...'
|
|
188
|
+
ref.up(data);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (target[0] === "@") {
|
|
193
|
+
ref.up({ [target.slice(1)]: data });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const impl = ref.component;
|
|
198
|
+
const method = impl[target] ?? impl[`on${capitalize(target)}`];
|
|
199
|
+
|
|
200
|
+
// deprecated. Use @-prefix instead.
|
|
201
|
+
if (!method) {
|
|
202
|
+
const propKey = target[0] === "@" ? target.slice(1) : target;
|
|
203
|
+
ref.up({ [propKey]: data });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!(typeof method === "function")) {
|
|
208
|
+
throw new Error(`Not a method: ${refId}.${target}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = method.call(impl, data, impl, ref);
|
|
212
|
+
|
|
213
|
+
this.log(`-> ${refId}.${target}`, result, data, impl);
|
|
214
|
+
|
|
215
|
+
ref.up(result);
|
|
216
|
+
} catch (ex) {
|
|
217
|
+
this.logError(`emit ${key}:`, ex);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Done life-cycle hook.
|
|
223
|
+
done() {
|
|
224
|
+
if (this.isDone) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.isDone = true;
|
|
228
|
+
|
|
229
|
+
this.component.done?.(this);
|
|
230
|
+
|
|
231
|
+
this.children?.forEach((c) => c.done());
|
|
232
|
+
|
|
233
|
+
this.#defered?.forEach((f) => f(this));
|
|
234
|
+
this.#defered = undefined;
|
|
235
|
+
|
|
236
|
+
this._parent?.children?.delete(this.uid);
|
|
237
|
+
this._parent = undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// map of children contexts
|
|
241
|
+
get children(): Map<Uid, IEntitron> | undefined {
|
|
242
|
+
return this.$children;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// map of chidren of its node
|
|
246
|
+
get contentManifests() {
|
|
247
|
+
return this.manifest.getSubNodes(this.platform);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// actual scope for re-content
|
|
251
|
+
get recontentScope() {
|
|
252
|
+
return this.scope;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
settleAsChild() {
|
|
256
|
+
if (!this.#isInited) {
|
|
257
|
+
this.#isInited = true;
|
|
258
|
+
|
|
259
|
+
this.initConnectors();
|
|
260
|
+
|
|
261
|
+
this.up(this.#initialState, true);
|
|
262
|
+
|
|
263
|
+
this.up(this.component.init?.(this));
|
|
264
|
+
} else {
|
|
265
|
+
void this.up(this.manifest.resolveProps(this), true);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// perform children instantiation and updates
|
|
270
|
+
private recontent() {
|
|
271
|
+
const nodes = this.contentManifests;
|
|
272
|
+
|
|
273
|
+
this.platform.redraw(this, this.root);
|
|
274
|
+
|
|
275
|
+
this.children?.forEach((e, uid) => {
|
|
276
|
+
if (!nodes?.get(uid)) {
|
|
277
|
+
e.done();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const children = new Map<Uid, IEntitron>();
|
|
282
|
+
|
|
283
|
+
nodes?.forEach((node: IManifestNode, uid: Uid) => {
|
|
284
|
+
const e = this.children?.get(uid) ?? node.createEntitron(this.platform, this, this.recontentScope);
|
|
285
|
+
children.set(e.uid, e);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.$children = children;
|
|
289
|
+
|
|
290
|
+
for (const ch of children.values()) {
|
|
291
|
+
ch.settleAsChild();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// --- Referencing.
|
|
296
|
+
|
|
297
|
+
get refId() {
|
|
298
|
+
return this.manifest.refId;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// gets any context instance in current or upper scope by key
|
|
302
|
+
getByRef(refId: string): IEntitron | undefined {
|
|
303
|
+
if (!refId) return undefined;
|
|
304
|
+
|
|
305
|
+
if (refId === "this") {
|
|
306
|
+
return this;
|
|
307
|
+
}
|
|
308
|
+
const local = this.#refs?.[refId];
|
|
309
|
+
if (local) {
|
|
310
|
+
return local;
|
|
311
|
+
}
|
|
312
|
+
const ref = this === this.scope ? null : this.scope.getByRef(refId);
|
|
313
|
+
return ref as Entitron;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
addReference(refId: string, c: IEntitron) {
|
|
317
|
+
(this.#refs ?? (this.#refs = {}))[refId] = c;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- Routines -------------------------
|
|
321
|
+
|
|
322
|
+
private initConnectors() {
|
|
323
|
+
if (this.manifest.connectors) {
|
|
324
|
+
for (const [key, pipes] of this.manifest.connectors.entries()) {
|
|
325
|
+
const [srcIds, targetPropName] = key.split("|");
|
|
326
|
+
const [refId, sourcePropName] = srcIds.split(".");
|
|
327
|
+
|
|
328
|
+
const ref = this.scope.getByRef(refId) as Entitron;
|
|
329
|
+
if (!ref) {
|
|
330
|
+
this.logError(`Connect: No such ref: ${refId}`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!sourcePropName) {
|
|
335
|
+
this.logError(`Connect: No prop specified: ${key}`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const propName = sourcePropName[0] === '@' ? sourcePropName.slice(1) : sourcePropName;
|
|
340
|
+
const applicator = targetPropName ? (rr: any) => ({ [targetPropName]: rr }) : identity;
|
|
341
|
+
|
|
342
|
+
this.defer(
|
|
343
|
+
ref.subscribe((source: Entitron) => {
|
|
344
|
+
try {
|
|
345
|
+
const r = applicator(pipes(this, source.get(propName)));
|
|
346
|
+
this.up(r as Delta);
|
|
347
|
+
} catch (ex) {
|
|
348
|
+
source.logError("Notify ", ex);
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
// hot subscription
|
|
354
|
+
Object.assign(this.#initialState, applicator(pipes(ref, ref.get(propName))));
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
raceCondition(key: string) {
|
|
360
|
+
const COUNTERS = this.#weak ?? (this.#weak = new Map<string, number>());
|
|
361
|
+
let counter = 1 + (COUNTERS.get(key) ?? 0);
|
|
362
|
+
COUNTERS.set(key, counter);
|
|
363
|
+
return (fn: () => unknown) => {
|
|
364
|
+
if (counter === COUNTERS.get(key)) {
|
|
365
|
+
counter = 0;
|
|
366
|
+
return fn();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// register callback to be called on done
|
|
372
|
+
defer(fn: EProcedure) {
|
|
373
|
+
if (fn && typeof fn === "function") {
|
|
374
|
+
(this.#defered ?? (this.#defered = [])).push(fn);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
log(val: any, ...args: unknown[]) {
|
|
379
|
+
this.platform.log({
|
|
380
|
+
level: "log",
|
|
381
|
+
source: `${this.displayName}:${this.uid}`,
|
|
382
|
+
message: val,
|
|
383
|
+
params: args,
|
|
384
|
+
});
|
|
385
|
+
return val;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
logError(error: any, ...args: unknown[]) {
|
|
389
|
+
this.platform.log({
|
|
390
|
+
level: "error",
|
|
391
|
+
source: `${this.displayName}:${this.uid}`,
|
|
392
|
+
error,
|
|
393
|
+
message: `${String(error.message || error)}\n${this.toString().slice(0, 120)}`,
|
|
394
|
+
params: args,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// access to resource by key
|
|
399
|
+
res(key: string | string[]) {
|
|
400
|
+
return this.platform.getResource(key);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
toString(): string {
|
|
404
|
+
return stringify(this);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { nextId } from "ultimus";
|
|
2
|
+
import { compilePipeExpression } from "../utils/compileExpression";
|
|
3
|
+
import type { Delta, ExpressionText, Hash, Uid } from "ultimus/types";
|
|
4
|
+
import type { IPlatform, IEntitron, IManifestNode } from "arrmatura/types";
|
|
5
|
+
import { resolveExpression } from "./resolveExpression";
|
|
6
|
+
|
|
7
|
+
type PropertyResolver = (c: IEntitron, acc: Delta) => Delta;
|
|
8
|
+
|
|
9
|
+
type PropertyGetter = (c: IEntitron) => unknown;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ManifestNode is a abstract factory for Entitron.
|
|
13
|
+
*
|
|
14
|
+
* Each specific kind of Entitron has its own descending implementation of ManifestNode.
|
|
15
|
+
*/
|
|
16
|
+
export abstract class ManifestNode implements IManifestNode {
|
|
17
|
+
protected $content?: Map<Uid, IManifestNode> | undefined;
|
|
18
|
+
uid: Uid = nextId("N");
|
|
19
|
+
refId?: string;
|
|
20
|
+
private readonly propertyResolvers: PropertyResolver[] = [];
|
|
21
|
+
private initialState: Hash<(c: IEntitron) => any> = {};
|
|
22
|
+
connectors?: Map<string, (c: IEntitron, x: unknown) => unknown>;
|
|
23
|
+
tag: any;
|
|
24
|
+
|
|
25
|
+
abstract get EntitronConstructor(): new (platform: IPlatform, node: typeof this, parent?: IEntitron, scope?: IEntitron) => IEntitron;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a context object.
|
|
29
|
+
*
|
|
30
|
+
* @param {IPlatform} platform - The platform object.
|
|
31
|
+
* @param {IEntitron} parent - The parent context object (optional).
|
|
32
|
+
* @param {IEntitron} scope - The scope context object (optional).
|
|
33
|
+
* @return {IEntitron} The created context object.
|
|
34
|
+
*/
|
|
35
|
+
createEntitron(platform: IPlatform, parent?: IEntitron, scope?: IEntitron): IEntitron {
|
|
36
|
+
return new this.EntitronConstructor(platform, this, parent, scope);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getSubNodes(_platform: IPlatform): Map<Uid, IManifestNode> | undefined {
|
|
40
|
+
return this.$content;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addPropertyResolver(getter: PropertyGetter, propKey: string) {
|
|
44
|
+
this.propertyResolvers.push((c: IEntitron, delta: Delta) => {
|
|
45
|
+
delta[propKey] = getter(c);
|
|
46
|
+
return delta;
|
|
47
|
+
});
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private addDataPropertyResolver(getter: PropertyGetter, propKey: string) {
|
|
52
|
+
this.propertyResolvers.push((c: IEntitron, delta: Delta) => {
|
|
53
|
+
const val = getter(c);
|
|
54
|
+
delta.data = Object.assign((delta.data as object) || {}, {
|
|
55
|
+
[propKey]: val,
|
|
56
|
+
});
|
|
57
|
+
return delta;
|
|
58
|
+
});
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private addPropertiesResolver(getter: PropertyGetter) {
|
|
63
|
+
this.propertyResolvers.push((c: IEntitron, delta: Delta) => {
|
|
64
|
+
const value = getter(c);
|
|
65
|
+
if (value && typeof value === "object") {
|
|
66
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
67
|
+
delta[key] = val;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return delta;
|
|
71
|
+
});
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
addConnector(expr: ExpressionText, propName: string) {
|
|
76
|
+
const { key, pipec } = compilePipeExpression(expr);
|
|
77
|
+
(this.connectors ?? (this.connectors = new Map())).set(`${key}|${propName}`, pipec);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private addEmitter(expr: ExpressionText, k: string) {
|
|
81
|
+
const { key, pipec } = compilePipeExpression(expr);
|
|
82
|
+
this.initialState[k] = (c) => (data: unknown) => c.scope?.emit(key, pipec(c, data) as Delta);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
resolveInitialProps(c: IEntitron) {
|
|
86
|
+
return this.propertyResolvers.reduce(
|
|
87
|
+
(delta, resolver) => resolver(c, delta),
|
|
88
|
+
Object.entries(this.initialState).reduce((acc, [key, fn]) => {
|
|
89
|
+
acc[key] = fn(c);
|
|
90
|
+
return acc;
|
|
91
|
+
}, {} as Hash)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
resolveProps(c: IEntitron) {
|
|
96
|
+
return this.propertyResolvers.reduce((delta, resolver) => resolver(c, delta), {});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
compileAttribute(k: string, v: unknown) {
|
|
100
|
+
if (k.startsWith("data-")) {
|
|
101
|
+
this.addDataPropertyResolver(resolveExpression(v), k.slice(5));
|
|
102
|
+
} else if (k === "ref") {
|
|
103
|
+
this.refId = String(v);
|
|
104
|
+
} else if (k === "props" || k.startsWith('{...')) {
|
|
105
|
+
const sv = k.startsWith('{...') ? k.slice(4, -1) : String(v);
|
|
106
|
+
if (sv[0] === "<" && sv[1] === "-") {
|
|
107
|
+
this.addConnector(sv.slice(2), "");
|
|
108
|
+
} else {
|
|
109
|
+
this.addPropertiesResolver(resolveExpression(sv));
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
if (typeof v !== "string") {
|
|
113
|
+
this.initialState[k] = () => v;
|
|
114
|
+
} else {
|
|
115
|
+
if (v[0] === "<" && v[1] === "-") {
|
|
116
|
+
this.addConnector(v.slice(2), k);
|
|
117
|
+
} else if (v[0] === "-" && v[1] === ">") {
|
|
118
|
+
this.addEmitter(v.slice(2), k);
|
|
119
|
+
} else {
|
|
120
|
+
if (!v.includes("{") && v[0] !== "@") {
|
|
121
|
+
this.initialState[k] = () => v;
|
|
122
|
+
} else {
|
|
123
|
+
if (v[0] === "@" && v[1] === "@" && v.match(/^@@[a-z0-9\-_.]+$/i) != null) {
|
|
124
|
+
this.initialState[k] = (c) => c.platform.getResource(v.slice(2));
|
|
125
|
+
} else {
|
|
126
|
+
this.addPropertyResolver(resolveExpression(v), k);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
compileAttributes(attrs?: Hash) {
|
|
135
|
+
if (attrs) {
|
|
136
|
+
Object.entries(attrs).forEach((keyValue) => this.compileAttribute(...keyValue));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createStringInterpolator } from "ultimus/src/parsing/createStringInterpolator";
|
|
2
|
+
import { compileConstant, compileJsExpression, compilePlaceholder } from "../utils/compileExpression";
|
|
3
|
+
|
|
4
|
+
const CACHE = new Map();
|
|
5
|
+
|
|
6
|
+
function compileExpression(v: unknown) {
|
|
7
|
+
if (typeof v !== "string") return () => v;
|
|
8
|
+
|
|
9
|
+
if (v[0] === "@") return compilePlaceholder(v);
|
|
10
|
+
|
|
11
|
+
if (v.startsWith("js:")) return compileJsExpression(v.slice(3));
|
|
12
|
+
|
|
13
|
+
if (v.includes("{")) return createStringInterpolator(v.replace(/\s+/g, " "), compilePlaceholder);
|
|
14
|
+
|
|
15
|
+
return compileConstant(v);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const resolveExpression = (x: unknown) => {
|
|
19
|
+
return CACHE.has(x) ? CACHE.get(x) : CACHE.set(x, compileExpression(x)).get(x);
|
|
20
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ManifestNode } from "../core/ManifestNode";
|
|
2
|
+
import { Entitron } from "../core/Entitron";
|
|
3
|
+
import type { Hash, XmlNode } from "ultimus/types";
|
|
4
|
+
import type { EContent, IManifestNode, IPlatform } from "arrmatura/types";
|
|
5
|
+
|
|
6
|
+
export class Composite extends Entitron<CCompositeNode> {
|
|
7
|
+
get displayName(): string {
|
|
8
|
+
return `💠${this.manifest.tag}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get recontentScope(): Entitron {
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
slotContent(slotId?: string): EContent | undefined {
|
|
16
|
+
return this.manifest.getSlotContent(this.platform, slotId);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createComponent(initials: Hash) {
|
|
20
|
+
return this.platform.createComponent(this.manifest.tag, initials, this);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class CCompositeNode extends ManifestNode {
|
|
25
|
+
slotContent: Hash<EContent> | null = null;
|
|
26
|
+
tag: string;
|
|
27
|
+
nodes?: XmlNode["nodes"];
|
|
28
|
+
#content?: Map<string, IManifestNode>;
|
|
29
|
+
|
|
30
|
+
constructor({ tag, attrs, nodes }: XmlNode) {
|
|
31
|
+
super();
|
|
32
|
+
this.tag = tag;
|
|
33
|
+
this.nodes = nodes;
|
|
34
|
+
this.compileAttributes(attrs);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getSlotContent(platform: IPlatform, slotId?: string): EContent | undefined {
|
|
38
|
+
if (!this.slotContent) {
|
|
39
|
+
const slots = this.nodes?.reduce<Hash<XmlNode[]>>((groups, e) => {
|
|
40
|
+
let gId = "default";
|
|
41
|
+
let isContainer = false;
|
|
42
|
+
if (e.tag.startsWith(`${this.tag}:`)) {
|
|
43
|
+
gId = e.tag.split(":")[1];
|
|
44
|
+
isContainer = true;
|
|
45
|
+
}
|
|
46
|
+
groups[gId] = (groups[gId] || []).concat(isContainer ? e.nodes ?? [] : e);
|
|
47
|
+
return groups;
|
|
48
|
+
}, {});
|
|
49
|
+
|
|
50
|
+
this.slotContent = Object.entries(slots ?? {}).reduce<Hash<EContent>>((groups, [gId, nodes]) => {
|
|
51
|
+
groups[gId] = platform.getCompiledNodes(nodes);
|
|
52
|
+
return groups;
|
|
53
|
+
}, {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return this.slotContent[slotId ?? "default"] ?? undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
getSubNodes(platform: IPlatform) {
|
|
60
|
+
return this.#content ?? (this.#content = platform.getCompiledNodes(this.tag));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get EntitronConstructor() {
|
|
64
|
+
return Composite;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ManifestNode } from "../core/ManifestNode";
|
|
2
|
+
import { Composite } from "./composition";
|
|
3
|
+
import { Entitron } from "../core/Entitron";
|
|
4
|
+
import type { EContent, IEntitron, IPlatform } from "arrmatura/types";
|
|
5
|
+
import type { XmlNode } from "ultimus/types";
|
|
6
|
+
import { compilePlaceholder } from "../utils/compileExpression";
|
|
7
|
+
|
|
8
|
+
class Conditional extends Entitron<CIfNode> {
|
|
9
|
+
get contentManifests() {
|
|
10
|
+
const branches = this.manifest.getBranches(this.platform);
|
|
11
|
+
return this.component.condition ? branches.then : branches.else;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get displayName(): string {
|
|
15
|
+
return "🔹if";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class CIfNode extends ManifestNode {
|
|
20
|
+
#xml: XmlNode;
|
|
21
|
+
#branches?: { then?: EContent; else?: EContent };
|
|
22
|
+
constructor(xml: XmlNode, expr: string) {
|
|
23
|
+
super();
|
|
24
|
+
this.#xml = xml;
|
|
25
|
+
this.compileCondition(expr);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get EntitronConstructor() {
|
|
29
|
+
return Conditional;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getBranches(platform: IPlatform) {
|
|
33
|
+
if (this.#branches) {
|
|
34
|
+
return this.#branches;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.#branches = {};
|
|
38
|
+
|
|
39
|
+
const { nodes } = this.#xml;
|
|
40
|
+
|
|
41
|
+
let $then: XmlNode[] | undefined = [this.#xml];
|
|
42
|
+
if (nodes?.length) {
|
|
43
|
+
const ifElse = nodes.find((e) => e.tag === "Else");
|
|
44
|
+
const ifThen = nodes.find((e) => e.tag === "Then");
|
|
45
|
+
if (ifElse) {
|
|
46
|
+
this.#branches.else = ifElse.nodes ? platform.getCompiledNodes(ifElse.nodes) : undefined;
|
|
47
|
+
$then = ifThen
|
|
48
|
+
? ifThen.nodes
|
|
49
|
+
: [
|
|
50
|
+
Object.assign(this.#xml, {
|
|
51
|
+
nodes: nodes.filter((e) => e !== ifElse),
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
} else if (ifThen) {
|
|
55
|
+
$then = ifThen.nodes;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.#branches.then = $then ? platform.getCompiledNodes($then) : undefined;
|
|
59
|
+
|
|
60
|
+
return this.#branches;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
compileCondition(expr: string): void {
|
|
64
|
+
if (expr[0] === "<" && expr[1] === "-") {
|
|
65
|
+
this.addConnector(expr.slice(2), "condition");
|
|
66
|
+
} else if (expr.slice(0, 5) === "slot(") {
|
|
67
|
+
const slotId = expr.slice(5, -1);
|
|
68
|
+
// DO NOT place inside iterations.
|
|
69
|
+
this.addPropertyResolver((c: IEntitron) => !!(c.scope as Composite).slotContent?.(slotId)?.size, "condition");
|
|
70
|
+
} else {
|
|
71
|
+
const resolver = compilePlaceholder(expr);
|
|
72
|
+
this.addPropertyResolver((c) => !!resolver(c), "condition");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|