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