arrmatura 6.2.1 → 6.3.2
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 +41 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/docs/glossary.md +19 -0
- package/docs/hello.md +46 -0
- package/docs/manual.md +312 -0
- package/index.ts +9 -7
- package/package.json +13 -5
- package/src/core/Arrmatron.ts +417 -0
- package/src/core/Component.ts +90 -0
- package/src/core/ManifestNode.ts +150 -0
- package/src/core/resolveExpression.ts +25 -0
- package/src/{core → registry}/composition.ts +21 -25
- package/src/{core → registry}/conditionals.ts +19 -22
- package/src/registry/connector.ts +47 -0
- package/src/registry/elementary.ts +37 -0
- package/src/registry/fragment.ts +22 -0
- package/src/registry/index.ts +65 -0
- package/src/{core → registry}/iterations.ts +49 -36
- package/src/{core → registry}/root.ts +12 -10
- package/src/registry/routing.ts +32 -0
- package/src/registry/selection.ts +59 -0
- package/src/registry/slot.ts +37 -0
- package/src/utils/FingerprintMashine.ts +128 -0
- package/src/utils/applyStateChangedToImpl.ts +18 -0
- package/src/utils/compileExpression.ts +142 -53
- package/src/utils/objectFingerprint.spec.ts +70 -0
- package/src/utils/stringify.ts +3 -9
- package/types.ts +82 -0
- package/src/core/elementary.ts +0 -40
- package/src/core/fragment.ts +0 -21
- package/src/core/index.ts +0 -19
- package/src/core/routing.ts +0 -37
- package/src/core/selection.ts +0 -57
- package/src/core/slot.ts +0 -36
- package/src/ctx/Ctx.ts +0 -424
- package/src/ctx/CtxNode.ts +0 -159
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { capitalize, identity } from "ultimus";
|
|
2
|
+
import type { Delta, Fn, Hash, LogEntry, Proc, Uid } from "ultimus/types";
|
|
3
|
+
|
|
4
|
+
import type { IArrmatron, IManifestNode, IComponent, IPlatform } from "../../types";
|
|
5
|
+
import { objectFingerprint } from "../utils/FingerprintMashine";
|
|
6
|
+
import { applyStateChangedToImpl } from "../utils/applyStateChangedToImpl";
|
|
7
|
+
import { stringify } from "../utils/stringify";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Arrmatron is the essential concept of the framework.
|
|
11
|
+
* It creates a component instance, manages its state and keeps in touch with others.
|
|
12
|
+
*/
|
|
13
|
+
export class Arrmatron<T extends IManifestNode = IManifestNode> implements IArrmatron {
|
|
14
|
+
readonly $component: IComponent;
|
|
15
|
+
readonly root: IArrmatron;
|
|
16
|
+
readonly scope: Arrmatron;
|
|
17
|
+
|
|
18
|
+
$children?: Map<Uid, Arrmatron>;
|
|
19
|
+
isDone = false;
|
|
20
|
+
#isInited = false;
|
|
21
|
+
#fprints: Hash = {};
|
|
22
|
+
#initialState: Hash = {};
|
|
23
|
+
#listeners?: Set<(c: Arrmatron) => void>;
|
|
24
|
+
#defered?: Array<(c: IArrmatron) => void> = [];
|
|
25
|
+
#weak?: Map<string, number>;
|
|
26
|
+
#refs?: Hash<IArrmatron>;
|
|
27
|
+
#propFnMap?: Map<string, Proc>;
|
|
28
|
+
|
|
29
|
+
constructor(readonly platform: IPlatform, protected readonly manifest: T, private _parent?: Arrmatron, scope?: Arrmatron) {
|
|
30
|
+
this.scope = scope ?? this;
|
|
31
|
+
this.root = scope?.root ?? this;
|
|
32
|
+
this.#initialState = this.manifest.resolveInitialProps(this);
|
|
33
|
+
this.$component = this.createComponent(this.#initialState);
|
|
34
|
+
|
|
35
|
+
if (this.refId) {
|
|
36
|
+
// this.log('addReference', this.refId)
|
|
37
|
+
this.scope.addReference(this.refId, this);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get parent(): IArrmatron | undefined {
|
|
42
|
+
return this._parent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get uid(): string {
|
|
46
|
+
return this.manifest.uid;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get displayName(): string {
|
|
50
|
+
return "🔹";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
createComponent(initials: Hash): IComponent {
|
|
54
|
+
// by default, a component is just a plain object.
|
|
55
|
+
return { ...initials };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- State
|
|
59
|
+
|
|
60
|
+
// updates component state
|
|
61
|
+
up(delta?: Delta | Promise<any> | null | void | unknown, force = false): void {
|
|
62
|
+
if (!delta || this.isDone) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (delta instanceof Promise) {
|
|
67
|
+
const racer = this.raceCondition(`set`);
|
|
68
|
+
delta.then((val) => racer(() => this.up(val)));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const changes = new Map<string, unknown>();
|
|
73
|
+
Object.entries(delta).forEach(([k, v]) => {
|
|
74
|
+
if (v instanceof Promise) {
|
|
75
|
+
const isSpreading = !k || k.startsWith("...");
|
|
76
|
+
const racer = this.raceCondition(`set:${k}`);
|
|
77
|
+
void v.then((val) => racer(() => this.up(isSpreading ? val : { [k]: val })));
|
|
78
|
+
|
|
79
|
+
} else if (k && typeof v !== "undefined") {
|
|
80
|
+
if (k[0] === "$") {
|
|
81
|
+
if (this.$component[k] !== v) {
|
|
82
|
+
changes.set(k.slice(1), v);
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
|
|
86
|
+
const fprint = objectFingerprint(v);
|
|
87
|
+
if (!(k in this.#fprints) || fprint !== this.#fprints[k]) {
|
|
88
|
+
this.#fprints[k] = fprint;
|
|
89
|
+
changes.set(k, v);
|
|
90
|
+
// } else {
|
|
91
|
+
// if (v && typeof v === "object" && v !== this.get(k)) {
|
|
92
|
+
|
|
93
|
+
// this.log('same skip', k, '\n ', v, '==', this.get(k), '\n ', this.#fprints[k], '==', fprint)
|
|
94
|
+
// }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (changes.size || force) {
|
|
101
|
+
applyStateChangedToImpl(changes, this.$component);
|
|
102
|
+
this.touch();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// do recontent and notify all
|
|
107
|
+
touch() {
|
|
108
|
+
this.recontent();
|
|
109
|
+
this.notify();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Retrieves the component value associated with the given property ID.
|
|
114
|
+
* Dot-sepated pathes to properties are supported.
|
|
115
|
+
* Supported getters like `get<Name>()`.
|
|
116
|
+
* use '@' prefix to access platform resources.
|
|
117
|
+
* Functional values are bounded and cached.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} propId - The ID of the property.
|
|
120
|
+
* @return {unknown} The value associated with the property ID.
|
|
121
|
+
*/
|
|
122
|
+
get(propId: string): unknown {
|
|
123
|
+
const map = this.#propFnMap ?? (this.#propFnMap = new Map());
|
|
124
|
+
if (map.has(propId)) {
|
|
125
|
+
return map.get(propId)();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let fn: Fn | null = null;
|
|
129
|
+
const impl = this.$component;
|
|
130
|
+
const instant = impl[propId];
|
|
131
|
+
|
|
132
|
+
if (instant && typeof instant === "function") {
|
|
133
|
+
const bound = instant.bind(impl);
|
|
134
|
+
fn = () => bound;
|
|
135
|
+
} else if (impl.__getProperty) {
|
|
136
|
+
fn = () => (impl.__getProperty as (p: string) => unknown)(propId);
|
|
137
|
+
} else {
|
|
138
|
+
const [pk, ...path] = propId.split(".");
|
|
139
|
+
if (pk === "R") {
|
|
140
|
+
const val = this.platform.getResource(path) ?? null;
|
|
141
|
+
fn = () => val;
|
|
142
|
+
} else {
|
|
143
|
+
const gettr = impl[`get${capitalize(pk)}`];
|
|
144
|
+
const fn0 = typeof gettr === "function" ? () => gettr.call(impl) ?? null : () => impl[pk] ?? null;
|
|
145
|
+
|
|
146
|
+
fn = !path.length ? fn0 : () => path.reduce((r, p) => r?.[p] ?? null, fn0());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
map.set(propId, fn);
|
|
151
|
+
|
|
152
|
+
return fn();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getFromScope(propId: string): unknown {
|
|
156
|
+
return this.scope.get(propId);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// --- Left Arrow.
|
|
160
|
+
|
|
161
|
+
notify() {
|
|
162
|
+
this.#listeners?.forEach((fn) => fn(this));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// add listener to componet state changes
|
|
166
|
+
subscribe(handler: (src: Arrmatron) => any): Proc {
|
|
167
|
+
const listeners = this.#listeners ?? (this.#listeners = new Set());
|
|
168
|
+
listeners.add(handler);
|
|
169
|
+
return () => {
|
|
170
|
+
listeners.delete(handler);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- Right Arrow.
|
|
175
|
+
|
|
176
|
+
// emits action event to this component
|
|
177
|
+
emit(key: string, data: Delta): void {
|
|
178
|
+
if (this.isDone) return;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
if (key.endsWith(")")) {
|
|
182
|
+
const [refId, target] = key.split("(")[0].split(".");
|
|
183
|
+
const ref = this.getByRef(refId);
|
|
184
|
+
if (!ref) {
|
|
185
|
+
throw new Error(`No such reference: ${refId}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const impl = ref.$component;
|
|
189
|
+
|
|
190
|
+
const method = impl[target];
|
|
191
|
+
if (!(typeof method === "function")) {
|
|
192
|
+
throw new Error(`Not a method: ${refId}.${target}()`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const result = method.call(impl, data);
|
|
196
|
+
|
|
197
|
+
// this.log(`-> ${refId}.${target}(data)`, result, data, impl);
|
|
198
|
+
|
|
199
|
+
ref.up(result);
|
|
200
|
+
} else {
|
|
201
|
+
let [refId, target] = key.split(".");
|
|
202
|
+
if (!target) {
|
|
203
|
+
target = refId;
|
|
204
|
+
refId = "this";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const ref = this.getByRef(refId);
|
|
208
|
+
if (!ref) {
|
|
209
|
+
throw new Error(`No such reference: ${refId}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ref.up(target === "*" ? data : { [target]: data });
|
|
213
|
+
|
|
214
|
+
// this.log(`-> ${refId}.${target} = `, data);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
} catch (ex) {
|
|
218
|
+
this.logError(`emit ${key}:`, ex);
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// --- life-cycle.
|
|
223
|
+
|
|
224
|
+
// Done hook.
|
|
225
|
+
done() {
|
|
226
|
+
if (this.isDone) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.isDone = true;
|
|
230
|
+
|
|
231
|
+
this.$component.done?.(this);
|
|
232
|
+
|
|
233
|
+
this.children?.forEach((c) => c.done());
|
|
234
|
+
|
|
235
|
+
this.#defered?.forEach((f) => f(this));
|
|
236
|
+
this.#defered = undefined;
|
|
237
|
+
|
|
238
|
+
this._parent?.children?.delete(this.uid);
|
|
239
|
+
this._parent = undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// register callback to be called on done
|
|
243
|
+
defer(fn: (c: IArrmatron) => void) {
|
|
244
|
+
if (fn && typeof fn === "function") {
|
|
245
|
+
(this.#defered ?? (this.#defered = [])).push(fn);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private settleAsChild() {
|
|
250
|
+
if (!this.#isInited) {
|
|
251
|
+
this.#isInited = true;
|
|
252
|
+
|
|
253
|
+
this.initConnectors();
|
|
254
|
+
|
|
255
|
+
this.up(this.#initialState, true);
|
|
256
|
+
|
|
257
|
+
this.up(this.$component.init?.(this));
|
|
258
|
+
} else {
|
|
259
|
+
void this.up(this.manifest.resolveProps(this), true);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// --- Content.
|
|
264
|
+
|
|
265
|
+
// map of children contexts
|
|
266
|
+
get children(): Map<Uid, IArrmatron> | undefined {
|
|
267
|
+
return this.$children;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// map of chidren of its node
|
|
271
|
+
get contentManifests() {
|
|
272
|
+
return this.manifest.getSubNodes(this.platform);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// actual scope for re-content
|
|
276
|
+
get recontentScope() {
|
|
277
|
+
return this.scope;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// perform children instantiation and updates
|
|
281
|
+
private recontent() {
|
|
282
|
+
const nodes = this.contentManifests;
|
|
283
|
+
|
|
284
|
+
this.platform.redraw(this, this.root);
|
|
285
|
+
|
|
286
|
+
this.children?.forEach((e, uid) => {
|
|
287
|
+
if (!nodes?.get(uid)) {
|
|
288
|
+
e.done();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const children = new Map<Uid, Arrmatron>();
|
|
293
|
+
|
|
294
|
+
nodes?.forEach((node: IManifestNode, uid: Uid) => {
|
|
295
|
+
const e = this.$children?.get(uid) ?? node.createArrmatron(this.platform, this, this.recontentScope);
|
|
296
|
+
children.set(e.uid, e);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.$children = children;
|
|
300
|
+
|
|
301
|
+
for (const ch of children.values()) {
|
|
302
|
+
ch.settleAsChild();
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --- Referencing.
|
|
307
|
+
|
|
308
|
+
get refId() {
|
|
309
|
+
return this.manifest.refId;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// gets any context instance in current or upper scope by key
|
|
313
|
+
getByRef(refId: string): IArrmatron | undefined {
|
|
314
|
+
if (!refId) return undefined;
|
|
315
|
+
|
|
316
|
+
if (refId === "this") {
|
|
317
|
+
return this;
|
|
318
|
+
}
|
|
319
|
+
const local = this.#refs?.[refId];
|
|
320
|
+
if (local) {
|
|
321
|
+
return local;
|
|
322
|
+
}
|
|
323
|
+
return this.parent?.getByRef(refId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private addReference(refId: string, c: IArrmatron) {
|
|
327
|
+
(this.#refs ?? (this.#refs = {}))[refId] = c;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// --- Connectors -------------------------
|
|
331
|
+
|
|
332
|
+
private initConnectors() {
|
|
333
|
+
if (this.manifest.connectors) {
|
|
334
|
+
for (const [key, pipes] of this.manifest.connectors.entries()) {
|
|
335
|
+
const [srcIds, targetPropName] = key.split("|");
|
|
336
|
+
const [refId, sourcePropName] = srcIds.split(".");
|
|
337
|
+
|
|
338
|
+
const ref = this.parent?.getByRef(refId) as Arrmatron;
|
|
339
|
+
if (!ref) {
|
|
340
|
+
this.logError(`Connect: No such ref: ${refId}`);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!sourcePropName) {
|
|
345
|
+
this.logError(`Connect: No prop specified: ${key}`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const applicator = targetPropName ? (rr: any) => ({ [targetPropName]: rr }) : identity;
|
|
350
|
+
|
|
351
|
+
this.defer((() => {
|
|
352
|
+
let popre;
|
|
353
|
+
return ref.subscribe(async (source: Arrmatron) => {
|
|
354
|
+
try {
|
|
355
|
+
const val = await source.get(sourcePropName);
|
|
356
|
+
const fingerprint = objectFingerprint(val);
|
|
357
|
+
|
|
358
|
+
if (popre !== undefined && popre === fingerprint) return;
|
|
359
|
+
|
|
360
|
+
popre = fingerprint;
|
|
361
|
+
const r = applicator(pipes(this, val));
|
|
362
|
+
this.up(r as Delta);
|
|
363
|
+
} catch (ex) {
|
|
364
|
+
source.logError("Notify ", ex);
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
})()
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// hot subscription
|
|
371
|
+
Object.assign(this.#initialState, applicator(pipes(ref, ref.get(sourcePropName))));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// --- Routines -------------------------
|
|
377
|
+
|
|
378
|
+
raceCondition(key: string) {
|
|
379
|
+
const COUNTERS = this.#weak ?? (this.#weak = new Map<string, number>());
|
|
380
|
+
let counter = 1 + (COUNTERS.get(key) ?? 0);
|
|
381
|
+
COUNTERS.set(key, counter);
|
|
382
|
+
return (fn: () => unknown) => {
|
|
383
|
+
if (counter === COUNTERS.get(key)) {
|
|
384
|
+
counter = 0;
|
|
385
|
+
return fn();
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
log(val: any, ...args: unknown[]) {
|
|
391
|
+
this.platform.log({
|
|
392
|
+
level: "log",
|
|
393
|
+
source: `${this.displayName}:${this.uid}`,
|
|
394
|
+
message: val,
|
|
395
|
+
params: args,
|
|
396
|
+
});
|
|
397
|
+
return val;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
logError(error: any, ...args: unknown[]) {
|
|
401
|
+
this.platform.log({
|
|
402
|
+
level: "error",
|
|
403
|
+
source: `${this.displayName}:${this.uid}`,
|
|
404
|
+
error,
|
|
405
|
+
message: `${String(error.message || error)}\n${this.toString().slice(0, 120)}`,
|
|
406
|
+
params: args,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
toast = (t: LogEntry) => {
|
|
411
|
+
this.platform.toast(t, this);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
toString(): string {
|
|
415
|
+
return stringify(this);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Delta, Hash, LogEntry } from "ultimus/types";
|
|
2
|
+
|
|
3
|
+
import type { IComponent, IArrmatron } from "../../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Base Ancestor for custom components.
|
|
7
|
+
*/
|
|
8
|
+
export abstract class Component implements IComponent {
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
|
|
11
|
+
constructor(_: Hash, private readonly $ctx: IArrmatron) {
|
|
12
|
+
//no-op
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
get platform() {
|
|
16
|
+
return this.$ctx.platform;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get isDone() {
|
|
20
|
+
return this.$ctx.isDone;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// hook on done
|
|
24
|
+
done(_: IArrmatron): void {
|
|
25
|
+
//no-op
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// hook on init
|
|
29
|
+
// returned value will be used to update state
|
|
30
|
+
init(_: IArrmatron): Delta | null | undefined | unknown {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// get from ctx state
|
|
35
|
+
get(key: string) {
|
|
36
|
+
return this.$ctx.get(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// update ctx state
|
|
40
|
+
up(d: Delta) {
|
|
41
|
+
return this.$ctx.up(d);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
touch() {
|
|
45
|
+
return this.$ctx.touch();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// emit action event to another component by key
|
|
49
|
+
emit(key: string, data: Delta) {
|
|
50
|
+
return this.$ctx.emit(key, data);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// register callback to be called on done
|
|
54
|
+
defer(fn: () => void) {
|
|
55
|
+
this.$ctx.defer(fn);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// register callback to be called on done
|
|
59
|
+
defineCalculatedProperty(key, fn: (...args: any[]) => any, deps?: string[]) {
|
|
60
|
+
let depKey = "";
|
|
61
|
+
let depValue = undefined;
|
|
62
|
+
Object.defineProperty(this, key, {
|
|
63
|
+
get() {
|
|
64
|
+
const args: any[] = deps?.length ? deps.map((k) => this.$ctx.get(k)) : [];
|
|
65
|
+
const newKey = args.join(':');
|
|
66
|
+
if (depKey !== newKey) {
|
|
67
|
+
depKey = newKey;
|
|
68
|
+
depValue = fn.apply(this, args);
|
|
69
|
+
}
|
|
70
|
+
return depValue;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
toast(entry: LogEntry) {
|
|
76
|
+
this.$ctx.toast(entry);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
log(val: unknown, ...args: unknown[]) {
|
|
80
|
+
this.$ctx.log(val, ...args);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logError(val: unknown, ...args: unknown[]) {
|
|
84
|
+
this.$ctx.logError(val, ...args);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
toString() {
|
|
88
|
+
return this.$ctx.toString();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { IPlatform, IArrmatron, IManifestNode } from "arrmatura/types";
|
|
2
|
+
import { nextId } from "ultimus";
|
|
3
|
+
import type { Delta, ExpressionText, Hash, Uid } from "ultimus/types";
|
|
4
|
+
|
|
5
|
+
import { compilePipeExpression } from "../utils/compileExpression";
|
|
6
|
+
|
|
7
|
+
import { Arrmatron } from "./Arrmatron";
|
|
8
|
+
import { resolveExpression } from "./resolveExpression";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
type PropertyResolver = (c: IArrmatron, acc: Delta) => Delta;
|
|
12
|
+
|
|
13
|
+
type PropertyGetter = (c: IArrmatron) => unknown;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ManifestNode is a abstract factory for Arrmatron.
|
|
17
|
+
*
|
|
18
|
+
* Each specific kind of Arrmatron has its own descending implementation of ManifestNode.
|
|
19
|
+
*/
|
|
20
|
+
export abstract class ManifestNode implements IManifestNode {
|
|
21
|
+
protected $content?: Map<Uid, IManifestNode> | undefined;
|
|
22
|
+
uid: Uid;
|
|
23
|
+
refId?: string;
|
|
24
|
+
private readonly propertyResolvers: PropertyResolver[] = [];
|
|
25
|
+
private initialState: Hash<(c: IArrmatron) => any> = {};
|
|
26
|
+
connectors?: Map<string, (c: IArrmatron, x: unknown) => unknown>;
|
|
27
|
+
tag: any;
|
|
28
|
+
|
|
29
|
+
constructor(xid: Uid) {
|
|
30
|
+
this.uid = `${nextId("N")}:${xid}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
abstract get EntitronConstructor(): new (platform: IPlatform, node: typeof this, parent?: Arrmatron, scope?: Arrmatron) => Arrmatron;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create a context object.
|
|
37
|
+
*
|
|
38
|
+
* @param {IPlatform} platform - The platform object.
|
|
39
|
+
* @param {IArrmatron} parent - The parent context object (optional).
|
|
40
|
+
* @param {IArrmatron} scope - The scope context object (optional).
|
|
41
|
+
* @return {IArrmatron} The created context object.
|
|
42
|
+
*/
|
|
43
|
+
createArrmatron(platform: IPlatform, parent?: Arrmatron, scope?: Arrmatron): Arrmatron {
|
|
44
|
+
return new this.EntitronConstructor(platform, this, parent, scope);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getSubNodes(_platform: IPlatform): Map<Uid, IManifestNode> | undefined {
|
|
48
|
+
return this.$content;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
addPropertyResolver(getter: PropertyGetter, propKey: string) {
|
|
52
|
+
this.propertyResolvers.push((c: IArrmatron, delta: Delta) => {
|
|
53
|
+
delta[propKey] = getter(c);
|
|
54
|
+
return delta;
|
|
55
|
+
});
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private addDataPropertyResolver(getter: PropertyGetter, propKey: string) {
|
|
60
|
+
this.propertyResolvers.push((c: IArrmatron, delta: Delta) => {
|
|
61
|
+
const val = getter(c);
|
|
62
|
+
delta.data = Object.assign((delta.data as object) || {}, {
|
|
63
|
+
[propKey]: val,
|
|
64
|
+
});
|
|
65
|
+
return delta;
|
|
66
|
+
});
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private addPropertiesResolver(getter: PropertyGetter) {
|
|
71
|
+
this.propertyResolvers.push((c: IArrmatron, delta: Delta) => {
|
|
72
|
+
const value = getter(c);
|
|
73
|
+
if (value && typeof value === "object") {
|
|
74
|
+
Object.entries(value).forEach(([key, val]) => {
|
|
75
|
+
delta[key] = val;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return delta;
|
|
79
|
+
});
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
addConnector(expr: ExpressionText, propName: string) {
|
|
84
|
+
const { key, pipec } = compilePipeExpression(expr);
|
|
85
|
+
(this.connectors ?? (this.connectors = new Map())).set(`${key}|${propName}`, pipec);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private addEmitter(expr: ExpressionText, k: string) {
|
|
89
|
+
const { key, pipec } = compilePipeExpression(expr);
|
|
90
|
+
const rkey = key + (expr.endsWith(")") ? "()" : '');
|
|
91
|
+
this.initialState[k] = (c) => (data: unknown) => c.scope?.emit(rkey, pipec(c, data) as Delta);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
resolveInitialProps(c: IArrmatron) {
|
|
95
|
+
return this.propertyResolvers.reduce(
|
|
96
|
+
(delta, resolver) => resolver(c, delta),
|
|
97
|
+
Object.entries(this.initialState).reduce((acc, [key, fn]) => {
|
|
98
|
+
acc[key] = fn(c);
|
|
99
|
+
return acc;
|
|
100
|
+
}, {} as Hash)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
resolveProps(c: IArrmatron) {
|
|
105
|
+
return this.propertyResolvers.reduce((delta, resolver) => resolver(c, delta), {});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
compileAttribute(k: string, v: unknown) {
|
|
109
|
+
if (k.startsWith("data-")) {
|
|
110
|
+
this.addDataPropertyResolver(resolveExpression(v), k.slice(5));
|
|
111
|
+
} else if (k === "Ref") {
|
|
112
|
+
this.refId = String(v);
|
|
113
|
+
} else if (k === '(...)') {
|
|
114
|
+
const sv = String(v);
|
|
115
|
+
if (sv[0] === "<" && sv[1] === "-") {
|
|
116
|
+
this.addConnector(sv.slice(2), "");
|
|
117
|
+
} else {
|
|
118
|
+
this.addPropertiesResolver(resolveExpression(sv));
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (typeof v !== "string") {
|
|
122
|
+
this.initialState[k] = () => v;
|
|
123
|
+
} else {
|
|
124
|
+
if (v[0] === "<" && v[1] === "-") {
|
|
125
|
+
this.addConnector(v.slice(2), k);
|
|
126
|
+
} else if (v[0] === "-" && v[1] === ">") {
|
|
127
|
+
this.addEmitter(v.slice(2), k);
|
|
128
|
+
} else if (v[0] === "d" && v.startsWith("data->")) {
|
|
129
|
+
this.addEmitter(v.slice(6), k);
|
|
130
|
+
} else {
|
|
131
|
+
if (!v.includes("{")) {
|
|
132
|
+
this.initialState[k] = () => v;
|
|
133
|
+
} else {
|
|
134
|
+
if (v[0] === "R" && v[1] === "." && v.match(/^@@[a-z0-9\-_.]+$/i) != null) {
|
|
135
|
+
this.initialState[k] = (c) => c.platform.getResource(v.slice(2));
|
|
136
|
+
} else {
|
|
137
|
+
this.addPropertyResolver(resolveExpression(v), k);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
compileAttributes(attrs?: Hash) {
|
|
146
|
+
if (attrs) {
|
|
147
|
+
Object.entries(attrs).forEach((keyValue) => this.compileAttribute(...keyValue));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createStringInterpolator } from "ultimus/src/parsing/createStringInterpolator";
|
|
2
|
+
|
|
3
|
+
import { compileConstant, compileJsExpression, compilePlaceholder } from "../utils/compileExpression";
|
|
4
|
+
// \{\{([\w\.]+)\}\}
|
|
5
|
+
|
|
6
|
+
const CACHE = new Map();
|
|
7
|
+
|
|
8
|
+
function compileExpression(v: unknown) {
|
|
9
|
+
if (typeof v !== "string") return () => v;
|
|
10
|
+
|
|
11
|
+
if (v.startsWith("js:")) return compileJsExpression(v.slice(3));
|
|
12
|
+
|
|
13
|
+
if (v.includes("{")) {
|
|
14
|
+
const vCutOne = v.slice(1, -1);
|
|
15
|
+
if (v[0] === "{" && v[v.length - 1] === "}" && !vCutOne.includes("{")) {
|
|
16
|
+
return compilePlaceholder(vCutOne)
|
|
17
|
+
}
|
|
18
|
+
return createStringInterpolator(v.replace(/\s+/g, " "), compilePlaceholder);
|
|
19
|
+
}
|
|
20
|
+
return compileConstant(v);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const resolveExpression = (x: unknown) => {
|
|
24
|
+
return CACHE.has(x) ? CACHE.get(x) : CACHE.set(x, compileExpression(x)).get(x);
|
|
25
|
+
}
|