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,128 @@
|
|
|
1
|
+
const CACHE = new Map();
|
|
2
|
+
|
|
3
|
+
function upnum(r: number, n: number) {
|
|
4
|
+
const rr = ((r << 5) - r) + n;
|
|
5
|
+
return rr & rr;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const COLON_NUM = ":".charCodeAt(0);
|
|
9
|
+
|
|
10
|
+
function stringHash(x: string) {
|
|
11
|
+
let h = 0;
|
|
12
|
+
if (CACHE.has(x)) {
|
|
13
|
+
h = CACHE.get(x) as number;
|
|
14
|
+
} else {
|
|
15
|
+
for (let i = 0; i < x.length; i++) {
|
|
16
|
+
h = upnum(h, x.charCodeAt(i));
|
|
17
|
+
}
|
|
18
|
+
CACHE.set(x, h);
|
|
19
|
+
}
|
|
20
|
+
return h;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class FingerprintMashine {
|
|
24
|
+
r = 0;
|
|
25
|
+
x?: any;
|
|
26
|
+
$path?: Set<unknown>;
|
|
27
|
+
|
|
28
|
+
get path() {
|
|
29
|
+
return this.$path ?? (this.$path = new Set());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
upNumber(n: number) {
|
|
33
|
+
return (this.r = upnum(this.r, n));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
upString(x: string) {
|
|
37
|
+
return this.upNumber(stringHash(x))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
upObject(x) {
|
|
41
|
+
|
|
42
|
+
if (x.$hash) return x.$hash;
|
|
43
|
+
|
|
44
|
+
if (x instanceof Date) return this.upNumber(x.getTime());
|
|
45
|
+
|
|
46
|
+
if (this.path.size > 10 || this.path.has(x)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.path.add(x);
|
|
51
|
+
|
|
52
|
+
if (x instanceof Map) {
|
|
53
|
+
this.upString("$$$Map$$$");
|
|
54
|
+
x.forEach((v, k) => {
|
|
55
|
+
this.upString(k);
|
|
56
|
+
this.upNumber(COLON_NUM);
|
|
57
|
+
this.upAny(v);
|
|
58
|
+
})
|
|
59
|
+
} else if (x instanceof Set) {
|
|
60
|
+
this.upString("$$$Set$$$");
|
|
61
|
+
x.forEach((v) => this.upAny(v))
|
|
62
|
+
} else if (Array.isArray(x)) {
|
|
63
|
+
this.upString("$$$Array$$$");
|
|
64
|
+
x.forEach((v) => this.upAny(v))
|
|
65
|
+
} else {
|
|
66
|
+
this.upString("$$$Object$$$");
|
|
67
|
+
for (const k in x) {
|
|
68
|
+
if (Object.prototype.hasOwnProperty.call(x, k) && k[0] !== '$') {
|
|
69
|
+
this.upString(k)
|
|
70
|
+
this.upNumber(COLON_NUM);
|
|
71
|
+
this.upAny(x[k]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.path.delete(x);
|
|
77
|
+
|
|
78
|
+
return this.r;
|
|
79
|
+
}
|
|
80
|
+
upAny(x: any): void {
|
|
81
|
+
|
|
82
|
+
if (x == null) {
|
|
83
|
+
this.upString(String(x));
|
|
84
|
+
} else if (typeof x === 'number') {
|
|
85
|
+
if (isNaN(x)) {
|
|
86
|
+
this.upString(`$$$NaN$$$`);
|
|
87
|
+
} else {
|
|
88
|
+
this.upNumber(x);
|
|
89
|
+
}
|
|
90
|
+
} else if (typeof x === 'object') {
|
|
91
|
+
this.upObject(x);
|
|
92
|
+
} else if (typeof x === 'string') {
|
|
93
|
+
this.upString(x);
|
|
94
|
+
} else if (typeof x === 'boolean') {
|
|
95
|
+
this.upString(`$$$Boolean$$$${x}`);
|
|
96
|
+
} else if (typeof x === 'function') {
|
|
97
|
+
// this.upString(`$$$Function$$$${x}`);
|
|
98
|
+
} else {
|
|
99
|
+
this.upString(String(x))
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
do(x: any) {
|
|
104
|
+
|
|
105
|
+
if (x == null) return x;
|
|
106
|
+
|
|
107
|
+
if (typeof x !== "object") return x;
|
|
108
|
+
|
|
109
|
+
if (Object.isFrozen(x)) return x;
|
|
110
|
+
|
|
111
|
+
if (x.$hash) return x.$hash;
|
|
112
|
+
|
|
113
|
+
this.r = 0;
|
|
114
|
+
this.x = x;
|
|
115
|
+
if (this.$path) {
|
|
116
|
+
this.$path?.clear();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.upObject(x);
|
|
120
|
+
|
|
121
|
+
return this.r;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const FM = new FingerprintMashine();
|
|
127
|
+
|
|
128
|
+
export const objectFingerprint = (x: any) => FM.do(x)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { capitalize } from "ultimus";
|
|
2
|
+
|
|
3
|
+
import type { IComponent } from "../../types";
|
|
4
|
+
|
|
5
|
+
export function applyStateChangedToImpl(changes: Map<string, unknown>, impl: IComponent) {
|
|
6
|
+
if (impl.__stateChanged) {
|
|
7
|
+
impl.__stateChanged(changes);
|
|
8
|
+
} else {
|
|
9
|
+
changes.forEach((v, k) => {
|
|
10
|
+
const setter = impl[`set${capitalize(k)}`];
|
|
11
|
+
if (typeof setter === "function") {
|
|
12
|
+
setter.call(impl, v);
|
|
13
|
+
} else {
|
|
14
|
+
impl[k] = v;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -1,9 +1,88 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type {
|
|
4
|
-
|
|
1
|
+
import type { IArrmatron } from "arrmatura/types";
|
|
2
|
+
import { scalarParse, gettus } from "ultimus";
|
|
3
|
+
import type { ExpressionText, Fn, StringHash } from "ultimus/types";
|
|
4
|
+
|
|
5
|
+
type PipeFn = (c: IArrmatron, argValue: unknown) => unknown;
|
|
6
|
+
|
|
7
|
+
// function tokenize(expression) {
|
|
8
|
+
// return expression.split(/[\(\)\<\>=\?!:']+/g);
|
|
9
|
+
// }
|
|
10
|
+
// class Node {
|
|
11
|
+
// constructor(public type, public value: string | null = null, public left = null, public right = null) {
|
|
12
|
+
// }
|
|
13
|
+
// }
|
|
14
|
+
|
|
15
|
+
// function parse(tokens) {
|
|
16
|
+
// let current = 0;
|
|
17
|
+
|
|
18
|
+
// function walk(priority = 0) {
|
|
19
|
+
// const token = tokens[current++];
|
|
20
|
+
// const nextToken = tokens[current];
|
|
21
|
+
|
|
22
|
+
// if (priority >= 0 && token === '(') {
|
|
23
|
+
// const node = walk();
|
|
24
|
+
// current++;
|
|
25
|
+
// return node;
|
|
26
|
+
// }
|
|
27
|
+
|
|
28
|
+
// if (priority >= 0 && ['>', '<', '==', '!=', '&&', '||', '??'].includes(nextToken)) {
|
|
29
|
+
// return new Node('binary', nextToken, walk(), walk());
|
|
30
|
+
// }
|
|
31
|
+
|
|
32
|
+
// if (priority >= 0 && ['>', '<', '==', '!=', '&&', '||', '??'].includes(nextToken)) {
|
|
33
|
+
// return new Node('binary', nextToken, walk(), walk());
|
|
34
|
+
// }
|
|
35
|
+
|
|
36
|
+
// if (priority >= 0 && ['>', '<', '==', '!=', '&&', '||', '??'].includes(nextToken)) {
|
|
37
|
+
// return new Node('binary', nextToken, walk(), walk());
|
|
38
|
+
// }
|
|
39
|
+
|
|
40
|
+
// if (priority >= 0 && nextToken === '?') {
|
|
41
|
+
// const node = new Node('coalesce', walk());
|
|
42
|
+
// if (tokens[current] !== ':') {
|
|
43
|
+
// throw new TypeError('Expected colon after "?" in coalesce operation');
|
|
44
|
+
// }
|
|
45
|
+
// current++;
|
|
46
|
+
// node.right = new Node('colon', token, walk(), walk());
|
|
47
|
+
// return node;
|
|
48
|
+
// }
|
|
49
|
+
// return new Node('operand', (token));;
|
|
50
|
+
// }
|
|
51
|
+
|
|
52
|
+
// return walk();
|
|
53
|
+
// }
|
|
54
|
+
|
|
55
|
+
// function evaluate(node) {
|
|
56
|
+
// switch (node.type) {
|
|
57
|
+
// case 'number':
|
|
58
|
+
// return node.value;
|
|
59
|
+
// case 'operator':
|
|
60
|
+
// switch (node.value) {
|
|
61
|
+
// case '>':
|
|
62
|
+
// return evaluate(node.left) > evaluate(node.right);
|
|
63
|
+
// case '<':
|
|
64
|
+
// return evaluate(node.left) < evaluate(node.right);
|
|
65
|
+
// case '==':
|
|
66
|
+
// return evaluate(node.left) == evaluate(node.right);
|
|
67
|
+
// case '!=':
|
|
68
|
+
// return evaluate(node.left) != evaluate(node.right);
|
|
69
|
+
// default:
|
|
70
|
+
// throw new TypeError(`Unknown operator: ${node.value}`);
|
|
71
|
+
// }
|
|
72
|
+
// case 'coalesce':
|
|
73
|
+
// const condition = evaluate(node.left);
|
|
74
|
+
// return condition ? evaluate(node.middle) : evaluate(node.right);
|
|
75
|
+
// default:
|
|
76
|
+
// throw new TypeError(`Unknown node type: ${node.type}`);
|
|
77
|
+
// }
|
|
78
|
+
// }
|
|
79
|
+
|
|
80
|
+
// // Example usage
|
|
81
|
+
// const expression = '1 < 2 ? 3 : 4';
|
|
82
|
+
// const tokens = tokenize(expression);
|
|
83
|
+
// const ast = parse(tokens);
|
|
84
|
+
// console.log(evaluate(ast)); // Outputs: 3
|
|
5
85
|
|
|
6
|
-
type PipeFn = (c: ICtx, argValue: unknown) => unknown;
|
|
7
86
|
|
|
8
87
|
const OP_SHORTCUTS: StringHash = {
|
|
9
88
|
"&&": "|and:",
|
|
@@ -20,27 +99,34 @@ const OP_SHORTCUTS: StringHash = {
|
|
|
20
99
|
|
|
21
100
|
const withNoPipes = (_: unknown, v: unknown) => v;
|
|
22
101
|
|
|
23
|
-
|
|
102
|
+
const isConstKey = (key: string) => "'+-0123456789".includes(key[0]) || key === 'true' || key === 'false';
|
|
103
|
+
|
|
104
|
+
export function compileConstant(a: string) {
|
|
24
105
|
const vv = scalarParse(a[0] === "'" ? a.slice(1, a.length - 1) : a);
|
|
25
106
|
return () => vv;
|
|
26
107
|
}
|
|
27
108
|
|
|
28
109
|
const compilePropGet = (key: string) => {
|
|
29
|
-
return (c:
|
|
110
|
+
return (c: IArrmatron) => c.getFromScope(key);
|
|
30
111
|
};
|
|
31
112
|
|
|
32
|
-
const compileArgument = (a: string) => {
|
|
33
|
-
if (a[0]
|
|
113
|
+
const compileArgument = (a: string, strings?: string[]) => {
|
|
114
|
+
if (a[0] === "!") {
|
|
115
|
+
const getter = compilePropGet(a.slice(1));
|
|
116
|
+
return (c: IArrmatron) => !getter(c);
|
|
117
|
+
};
|
|
34
118
|
|
|
35
|
-
if (a
|
|
36
|
-
const
|
|
37
|
-
return (
|
|
119
|
+
if (a[0] === "#" && a[1] === "#") {
|
|
120
|
+
const vv = strings?.[Number(a.slice(2))];
|
|
121
|
+
return () => vv;
|
|
38
122
|
}
|
|
39
123
|
|
|
40
|
-
return
|
|
124
|
+
if (isConstKey(a)) return compileConstant(a);
|
|
125
|
+
|
|
126
|
+
return compilePropGet(a);
|
|
41
127
|
};
|
|
42
128
|
|
|
43
|
-
const splitExpression = (v:
|
|
129
|
+
const splitExpression = (v: ExpressionText) =>
|
|
44
130
|
v
|
|
45
131
|
.replace(/(&&|!=|==|>|<|\[|\]|\|\||\?\??)/g, (_, s) => `${OP_SHORTCUTS[s]}`)
|
|
46
132
|
.split("|")
|
|
@@ -50,22 +136,37 @@ function withPipes(pipes: string[]) {
|
|
|
50
136
|
if (!pipes.length) return withNoPipes;
|
|
51
137
|
|
|
52
138
|
const compiledPipes = pipes.map((key) => {
|
|
53
|
-
const [
|
|
139
|
+
const strings: string[] = [];
|
|
140
|
+
|
|
141
|
+
const expr = key.replaceAll(/'[^']*'/g, (s) => {
|
|
142
|
+
strings.push(s.slice(1, -1))
|
|
143
|
+
return `##${strings.length - 1}`
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const [pipeId, ...args] = expr.split(":").map((s) => s.trim());
|
|
54
147
|
return {
|
|
55
148
|
pipeId,
|
|
56
|
-
args: args.map((a) => compileArgument(a)),
|
|
149
|
+
args: args.map((a) => compileArgument(a, strings)),
|
|
57
150
|
};
|
|
58
151
|
});
|
|
59
152
|
|
|
60
|
-
return (c:
|
|
153
|
+
return (c: IArrmatron, initialValue: unknown) =>
|
|
61
154
|
compiledPipes.reduce((value, { pipeId, args }) => {
|
|
62
155
|
try {
|
|
63
156
|
const fn = c.platform.getFunction(pipeId);
|
|
64
157
|
if (typeof fn !== "function") {
|
|
65
158
|
throw new Error(`${fn != null ? "must be a function" : "not found"}`);
|
|
66
159
|
}
|
|
67
|
-
|
|
68
|
-
|
|
160
|
+
if (value instanceof Promise) {
|
|
161
|
+
return value.then((val) => {
|
|
162
|
+
const args0 = [val, ...args.map((fn) => fn(c))];
|
|
163
|
+
const result = fn.apply(c, args0);
|
|
164
|
+
return result;
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
const args0 = [value, ...args.map((fn) => fn(c))];
|
|
168
|
+
return fn.apply(c, args0);
|
|
169
|
+
}
|
|
69
170
|
} catch (ex) {
|
|
70
171
|
c.logError(`ERROR: Function ${pipeId || "<empty>"}`, ex);
|
|
71
172
|
return value;
|
|
@@ -73,54 +174,42 @@ function withPipes(pipes: string[]) {
|
|
|
73
174
|
}, initialValue);
|
|
74
175
|
}
|
|
75
176
|
|
|
76
|
-
function withHardValue(hardValue:
|
|
77
|
-
if (hardValue ===
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
177
|
+
function withHardValue(hardValue: ExpressionText, pipec: PipeFn): PipeFn {
|
|
178
|
+
if (!hardValue || hardValue === '*') return pipec;
|
|
179
|
+
|
|
180
|
+
if (hardValue.startsWith('*.')) {
|
|
181
|
+
const propKey = hardValue.slice(2)
|
|
182
|
+
return (c: IArrmatron, ini: any) => pipec(c, gettus(ini, propKey));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const iniFn = isConstKey(hardValue) ? compileConstant(hardValue) : compilePlaceholder(hardValue);
|
|
186
|
+
|
|
187
|
+
return (c: IArrmatron) => pipec(c, iniFn(c));
|
|
83
188
|
}
|
|
84
189
|
|
|
85
|
-
export function compilePipeExpression(v:
|
|
190
|
+
export function compilePipeExpression(v: ExpressionText) {
|
|
191
|
+
if (v.endsWith(')')) {
|
|
192
|
+
const parts = v.slice(0, -1).split('(')
|
|
193
|
+
return compilePipeExpression(`${parts[0]}=${parts[1]}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
86
196
|
const [_key, ...pipes] = splitExpression(v);
|
|
87
197
|
const [key, hardValue] = _key.split("=").map((s) => s.trim());
|
|
198
|
+
|
|
88
199
|
return { key, pipec: withHardValue(hardValue, withPipes(pipes)) };
|
|
89
200
|
}
|
|
90
201
|
|
|
91
202
|
export function compilePlaceholder(expr: string): Fn {
|
|
92
|
-
|
|
93
203
|
const [key, ...pipesExpressions] = splitExpression(expr);
|
|
94
|
-
const pipec = withPipes(pipesExpressions);
|
|
95
204
|
|
|
96
|
-
|
|
205
|
+
const pipec = withPipes(pipesExpressions);
|
|
97
206
|
|
|
98
|
-
|
|
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
|
-
}
|
|
207
|
+
const fn = compileArgument(key);
|
|
106
208
|
|
|
107
|
-
return (c) => pipec(c as
|
|
209
|
+
return (c) => pipec(c as IArrmatron, fn(c));
|
|
108
210
|
}
|
|
109
211
|
|
|
110
|
-
const JS = {}
|
|
111
212
|
export function compileJsExpression(x: string) {
|
|
112
|
-
return
|
|
213
|
+
return new Function("c", x.replaceAll(/[^'a-z]([a-z][a-z0-9-.]+)/gi, 'c.prop("$1")'));
|
|
113
214
|
}
|
|
114
215
|
|
|
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
|
-
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { objectFingerprint } from './FingerprintMashine';
|
|
2
|
+
|
|
3
|
+
describe('objectFingerprint', () => {
|
|
4
|
+
it('should return 0 for null or undefined input', () => {
|
|
5
|
+
expect(objectFingerprint(null)).toBe(null);
|
|
6
|
+
expect(objectFingerprint(undefined)).toBe(undefined);
|
|
7
|
+
expect(objectFingerprint(0)).toBe(0);
|
|
8
|
+
expect(objectFingerprint("")).toBe("");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return the same hash value for the same input string, regardless of the case of its characters', () => {
|
|
12
|
+
const str1 = 'Hello World';
|
|
13
|
+
const str2 = 'Hello World';
|
|
14
|
+
const str3 = 'Hello world';
|
|
15
|
+
expect(objectFingerprint(str1)).toBe(objectFingerprint(str2));
|
|
16
|
+
expect(objectFingerprint(str1)).not.toBe(objectFingerprint(str3));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should return a unique hash value for different input objects', () => {
|
|
20
|
+
const obj1 = { foo: 'bar' };
|
|
21
|
+
const obj2 = { bar: 'baz' };
|
|
22
|
+
expect(objectFingerprint(obj1)).not.toBe(objectFingerprint(obj2));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should return the same hash value for the same input object, regardless of the order of its properties', () => {
|
|
26
|
+
const obj0 = { foo: 'bar', baz: 'qux' };
|
|
27
|
+
const obj1 = { foo: 'bar', baz: 'qux' };
|
|
28
|
+
const obj2 = { baz: 'qux', foo: 'bar' };
|
|
29
|
+
expect(objectFingerprint(obj1)).toBe(objectFingerprint(obj0));
|
|
30
|
+
expect(objectFingerprint(obj1)).not.toBe(objectFingerprint(obj2));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should return the same hash value for the same input array, regardless of the order of its elements', () => {
|
|
34
|
+
const arr0 = [1, 2, 3];
|
|
35
|
+
const arr1 = [1, 2, 3];
|
|
36
|
+
const arr2 = [3, 2, 1];
|
|
37
|
+
expect(objectFingerprint(arr1)).toBe(objectFingerprint(arr0));
|
|
38
|
+
expect(objectFingerprint(arr1)).not.toBe(objectFingerprint(arr2));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return the same hash value for the same input map, regardless of the order of its entries', () => {
|
|
42
|
+
const map0 = new Map([[1, 'one'], [2, 'two']]);
|
|
43
|
+
const map1 = new Map([[1, 'one'], [2, 'two']]);
|
|
44
|
+
const map2 = new Map([[2, 'two'], [1, 'one']]);
|
|
45
|
+
expect(objectFingerprint(map1)).toBe(objectFingerprint(map0));
|
|
46
|
+
expect(objectFingerprint(map1)).not.toBe(objectFingerprint(map2));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return the same hash value for the same input set, regardless of the order of its elements', () => {
|
|
50
|
+
const set0 = new Set([1, 2, 3]);
|
|
51
|
+
const set1 = new Set([1, 2, 3]);
|
|
52
|
+
const set2 = new Set([3, 2, 1]);
|
|
53
|
+
expect(objectFingerprint(set1)).toBe(objectFingerprint(set0));
|
|
54
|
+
expect(objectFingerprint(set1)).not.toBe(objectFingerprint(set2));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
it('should return the same hash value for the same input number, regardless of its precision', () => {
|
|
59
|
+
const num1 = 3.14159;
|
|
60
|
+
const num2 = 3.14159;
|
|
61
|
+
const num3 = 3.14158;
|
|
62
|
+
expect(objectFingerprint(num1)).toBe(objectFingerprint(num2));
|
|
63
|
+
expect(objectFingerprint(num1)).not.toBe(objectFingerprint(num3));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return the same hash value for the same input boolean, regardless of its value', () => {
|
|
67
|
+
expect(objectFingerprint(true)).toBe(objectFingerprint(true));
|
|
68
|
+
expect(objectFingerprint(false)).toBe(objectFingerprint(false))
|
|
69
|
+
});
|
|
70
|
+
})
|
package/src/utils/stringify.ts
CHANGED
|
@@ -2,9 +2,7 @@ import { Hash } from "ultimus";
|
|
|
2
2
|
const attributesToString = (v?: Hash): string => {
|
|
3
3
|
if (typeof v === "object") {
|
|
4
4
|
if (Array.isArray(v)) {
|
|
5
|
-
return !v.length
|
|
6
|
-
? "[]"
|
|
7
|
-
: `[${attributesToString(v[0]) ?? ""} x${String(v.length)}]`;
|
|
5
|
+
return !v.length ? "[]" : `[${attributesToString(v[0]) ?? ""} x${String(v.length)}]`;
|
|
8
6
|
}
|
|
9
7
|
return `{${Object.keys(v).toString()}}`;
|
|
10
8
|
}
|
|
@@ -29,11 +27,7 @@ const stringifyState = (attrs?: object | Hash) => {
|
|
|
29
27
|
export const stringify = (T: any, tab = ""): string => {
|
|
30
28
|
const { uid, displayName: tag = uid, component: impl, children } = T;
|
|
31
29
|
|
|
32
|
-
const ssubs: string = children?.size
|
|
33
|
-
? [...children.values()].map((c) => stringify(c, ` ${tab}`)).join("\n")
|
|
34
|
-
: "";
|
|
30
|
+
const ssubs: string = children?.size ? [...children.values()].map((c) => stringify(c, ` ${tab}`)).join("\n") : "";
|
|
35
31
|
|
|
36
|
-
return `${tab}<${tag}${stringifyState(impl)}${
|
|
37
|
-
!ssubs ? "/>" : `>\n${ssubs}\n${tab}</${tag}>`
|
|
38
|
-
}`;
|
|
32
|
+
return `${tab}<${tag}${stringifyState(impl)}${!ssubs ? "/>" : `>\n${ssubs}\n${tab}</${tag}>`}`;
|
|
39
33
|
};
|
package/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Uid, Delta, Data, LogEntry, Hash, XmlNode } from "ultimus/types";
|
|
2
|
+
|
|
3
|
+
import { Arrmatron } from "./src/core/Arrmatron";
|
|
4
|
+
|
|
5
|
+
export type IComponent = {
|
|
6
|
+
// optional Lifecycle method called when the component is done.
|
|
7
|
+
done?: (c: IArrmatron) => void;
|
|
8
|
+
// optional Lifecycle method called during initialization.
|
|
9
|
+
|
|
10
|
+
init?: (c: IArrmatron) => Delta | null | undefined | unknown | void;
|
|
11
|
+
// optional Lifecycle method called when the state changes.
|
|
12
|
+
__stateChanged?: (changes: Map<string, unknown>) => void;
|
|
13
|
+
// state key/values
|
|
14
|
+
[key: string]: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type IArrmatron = {
|
|
18
|
+
isDone: any;
|
|
19
|
+
readonly uid: Uid;
|
|
20
|
+
parent?: IArrmatron;
|
|
21
|
+
children?: Map<Uid, IArrmatron>;
|
|
22
|
+
scope?: IArrmatron;
|
|
23
|
+
$component: IComponent;
|
|
24
|
+
root: IArrmatron;
|
|
25
|
+
platform: IPlatform;
|
|
26
|
+
|
|
27
|
+
// state access
|
|
28
|
+
get: (propId: string) => unknown;
|
|
29
|
+
getFromScope: (key: string) => unknown;
|
|
30
|
+
up: (d: Delta | null | void | unknown, force?: boolean) => void;
|
|
31
|
+
|
|
32
|
+
touch: () => void;
|
|
33
|
+
emit: (key: string, arg1: Delta) => void;
|
|
34
|
+
|
|
35
|
+
// done life-cycle hook
|
|
36
|
+
done: () => void;
|
|
37
|
+
defer: (fn: () => void) => void;
|
|
38
|
+
|
|
39
|
+
// get registered arrmatron instance by reference. look up recurrsively from current to root scopes.
|
|
40
|
+
readonly refId?: string;
|
|
41
|
+
getByRef: (refId: string) => IArrmatron | undefined;
|
|
42
|
+
|
|
43
|
+
log: (val: unknown, ...arg1: unknown[]) => void;
|
|
44
|
+
logError: (error: unknown, ...arg1: unknown[]) => void;
|
|
45
|
+
toast(t: LogEntry): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type IManifestNode = {
|
|
49
|
+
connectors?: Map<string, (c: IArrmatron, x: unknown) => unknown>;
|
|
50
|
+
refId?: string;
|
|
51
|
+
readonly uid: Uid;
|
|
52
|
+
getSubNodes(platform: IPlatform): Map<Uid, IManifestNode> | undefined;
|
|
53
|
+
resolveProps: (c: IArrmatron) => Delta | Promise<unknown> | undefined;
|
|
54
|
+
resolveInitialProps: (c: IArrmatron) => Hash;
|
|
55
|
+
|
|
56
|
+
createArrmatron: (platform: IPlatform, parent?: Arrmatron, scope?: Arrmatron) => Arrmatron;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type IPlatform = {
|
|
60
|
+
log(arg0: LogEntry): void;
|
|
61
|
+
toast(t: LogEntry, ctx: IArrmatron): void;
|
|
62
|
+
getCompiledNodes(tag: string): Map<Uid, IManifestNode>;
|
|
63
|
+
getCompiledNodes(tag: XmlNode): IManifestNode;
|
|
64
|
+
getCompiledNodes(tag: XmlNode[]): Map<Uid, IManifestNode>;
|
|
65
|
+
updateResources(delta: any): void;
|
|
66
|
+
getResource(key: string | string[]): Data;
|
|
67
|
+
getFunction(id: string): (...args: unknown[]) => unknown;
|
|
68
|
+
redraw(source: IArrmatron, root: IArrmatron): void;
|
|
69
|
+
createComponent(def: IComponentDescriptor | string, initials: Hash, ctx: IArrmatron): IComponent | Hash;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ComponentConstructor = new (initials: Hash, c: IArrmatron) => IComponent;
|
|
73
|
+
|
|
74
|
+
export type IComponentDescriptor = {
|
|
75
|
+
tag?: string;
|
|
76
|
+
template?: string;
|
|
77
|
+
Ctor?: ComponentConstructor;
|
|
78
|
+
unknown?: boolean;
|
|
79
|
+
native?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type EContent = Map<Uid, IManifestNode>;
|
package/src/core/elementary.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { CtxNode } from "../ctx/CtxNode";
|
|
2
|
-
import { Ctx } from "../ctx/Ctx";
|
|
3
|
-
import type { Hash, XmlNode } from "ultimus/types";
|
|
4
|
-
import { IPlatform } from "arrmatura-api/types";
|
|
5
|
-
|
|
6
|
-
class Element extends Ctx<CElementNode> {
|
|
7
|
-
get displayName(): string {
|
|
8
|
-
return this.node.tag;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
createComponent(initials: Hash) {
|
|
12
|
-
return this.platform.createComponent(
|
|
13
|
-
{ tag: this.node.tag, native: true },
|
|
14
|
-
initials,
|
|
15
|
-
this
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class CElementNode extends CtxNode {
|
|
21
|
-
#nodes: XmlNode[] | undefined;
|
|
22
|
-
|
|
23
|
-
constructor({ tag, attrs, nodes, text }: XmlNode) {
|
|
24
|
-
super();
|
|
25
|
-
this.tag = tag;
|
|
26
|
-
this.compileAttributes(attrs);
|
|
27
|
-
if (text) {
|
|
28
|
-
this.compileAttribute("#text", text);
|
|
29
|
-
}
|
|
30
|
-
this.#nodes = nodes;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
getContent(platform: IPlatform) {
|
|
34
|
-
return this.#nodes ? platform.getCompiledNodes(this.#nodes) : undefined;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
get EntityConstructor() {
|
|
38
|
-
return Element;
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/core/fragment.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { CtxNode } from "../ctx/CtxNode";
|
|
2
|
-
import { Ctx } from "../ctx/Ctx";
|
|
3
|
-
import type { XmlNode } from "ultimus/types";
|
|
4
|
-
import type { IPlatform } from "arrmatura-api/types";
|
|
5
|
-
|
|
6
|
-
export class CFragmentNode extends CtxNode {
|
|
7
|
-
#nodes: XmlNode[] | undefined;
|
|
8
|
-
|
|
9
|
-
constructor({ nodes }: XmlNode) {
|
|
10
|
-
super();
|
|
11
|
-
this.#nodes = nodes;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
getContent(platform: IPlatform) {
|
|
15
|
-
return this.#nodes ? platform.getCompiledNodes(this.#nodes) : undefined;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
get EntityConstructor() {
|
|
19
|
-
return Ctx;
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/core/index.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { CForNode } from "./iterations";
|
|
2
|
-
import { CCompositeNode } from "./composition";
|
|
3
|
-
import { CIfNode } from "./conditionals";
|
|
4
|
-
import { CElementNode } from "./elementary";
|
|
5
|
-
import { CDynamicTagNode } from "./routing";
|
|
6
|
-
import { CSlotNode } from "./slot";
|
|
7
|
-
import { CFragmentNode } from "./fragment";
|
|
8
|
-
import { CSelectorNode } from "./selection";
|
|
9
|
-
|
|
10
|
-
export const Registry = {
|
|
11
|
-
CForNode,
|
|
12
|
-
CIfNode,
|
|
13
|
-
CDynamicTagNode,
|
|
14
|
-
CSlotNode,
|
|
15
|
-
CFragmentNode,
|
|
16
|
-
CCompositeNode,
|
|
17
|
-
CSelectorNode,
|
|
18
|
-
CElementNode,
|
|
19
|
-
};
|