@klarkxy/dsh-deep-view 0.0.0-stage → 0.2.0
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/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +25 -2
- package/cordis.patch.yml +5 -0
- package/docs/README.zh-CN.md +26 -0
- package/icon.svg +6 -0
- package/lib/answer-text.d.ts +15 -0
- package/lib/answer-text.js +110 -0
- package/lib/answer-text.js.map +1 -0
- package/lib/answers.d.ts +49 -0
- package/lib/answers.js +60 -0
- package/lib/answers.js.map +1 -0
- package/lib/client.inner.cjs +3423 -0
- package/lib/client.inner.cjs.map +1 -0
- package/lib/client.js +3432 -0
- package/lib/contracts.d.ts +2 -0
- package/lib/contracts.js +12 -0
- package/lib/contracts.js.map +1 -0
- package/lib/contracts2.d.ts +103 -0
- package/lib/copy.d.ts +37 -0
- package/lib/copy.js +69 -0
- package/lib/copy.js.map +1 -0
- package/lib/core.d.ts +57 -0
- package/lib/core.js +2 -0
- package/lib/core2.js +1079 -0
- package/lib/core2.js.map +1 -0
- package/lib/index.d.ts +104 -0
- package/lib/index.js +1153 -0
- package/lib/index.js.map +1 -0
- package/lib/widget.d.ts +71 -0
- package/lib/widget.js +1365 -0
- package/lib/widget.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +152 -4
package/lib/core2.js
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
//#region src/core/formula.js
|
|
2
|
+
const FUNCTIONS = {
|
|
3
|
+
min: {
|
|
4
|
+
args: [1, Infinity],
|
|
5
|
+
run: (...values) => Math.min(...values.map(num))
|
|
6
|
+
},
|
|
7
|
+
max: {
|
|
8
|
+
args: [1, Infinity],
|
|
9
|
+
run: (...values) => Math.max(...values.map(num))
|
|
10
|
+
},
|
|
11
|
+
abs: {
|
|
12
|
+
args: [1, 1],
|
|
13
|
+
run: (value) => Math.abs(num(value))
|
|
14
|
+
},
|
|
15
|
+
floor: {
|
|
16
|
+
args: [1, 1],
|
|
17
|
+
run: (value) => Math.floor(num(value))
|
|
18
|
+
},
|
|
19
|
+
ceil: {
|
|
20
|
+
args: [1, 1],
|
|
21
|
+
run: (value) => Math.ceil(num(value))
|
|
22
|
+
},
|
|
23
|
+
sqrt: {
|
|
24
|
+
args: [1, 1],
|
|
25
|
+
run: (value) => Math.sqrt(num(value))
|
|
26
|
+
},
|
|
27
|
+
pow: {
|
|
28
|
+
args: [2, 2],
|
|
29
|
+
run: (base, power) => num(base) ** num(power)
|
|
30
|
+
},
|
|
31
|
+
round: {
|
|
32
|
+
args: [1, 2],
|
|
33
|
+
run: (value, digits = 0) => roundTo(num(value), digitsOf(digits))
|
|
34
|
+
},
|
|
35
|
+
clamp: {
|
|
36
|
+
args: [3, 3],
|
|
37
|
+
run: (value, low, high) => Math.min(Math.max(num(value), num(low)), num(high))
|
|
38
|
+
},
|
|
39
|
+
fixed: {
|
|
40
|
+
args: [1, 2],
|
|
41
|
+
run: (value, digits = 0) => roundTo(num(value), digitsOf(digits)).toFixed(digitsOf(digits))
|
|
42
|
+
},
|
|
43
|
+
format: {
|
|
44
|
+
args: [1, 2],
|
|
45
|
+
run: (value, digits = 0) => grouped(num(value), digitsOf(digits))
|
|
46
|
+
},
|
|
47
|
+
percent: {
|
|
48
|
+
args: [1, 2],
|
|
49
|
+
run: (value, digits = 0) => `${grouped(num(value) * 100, digitsOf(digits))}%`
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const FORMULA_FUNCTIONS = Object.keys(FUNCTIONS);
|
|
53
|
+
const CONSTANTS = /* @__PURE__ */ new Map([["true", true], ["false", false]]);
|
|
54
|
+
const MAX_NESTING = 64;
|
|
55
|
+
var FormulaError = class extends Error {};
|
|
56
|
+
const fail = (message) => {
|
|
57
|
+
throw new FormulaError(message);
|
|
58
|
+
};
|
|
59
|
+
function num(value) {
|
|
60
|
+
if (typeof value === "number") return value;
|
|
61
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
62
|
+
const parsed = Number(value);
|
|
63
|
+
if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(parsed)) return parsed;
|
|
64
|
+
return fail(`${JSON.stringify(value)} is not a number`);
|
|
65
|
+
}
|
|
66
|
+
const digitsOf = (digits) => {
|
|
67
|
+
const value = num(digits);
|
|
68
|
+
if (!Number.isInteger(value) || value < 0 || value > 10) fail("digits must be a whole number from 0 to 10");
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
const roundTo = (value, digits) => {
|
|
72
|
+
const scale = 10 ** digits;
|
|
73
|
+
return Math.round(value * scale) / scale;
|
|
74
|
+
};
|
|
75
|
+
function grouped(value, digits) {
|
|
76
|
+
if (!Number.isFinite(value)) return String(value);
|
|
77
|
+
const [whole, part] = Math.abs(roundTo(value, digits)).toFixed(digits).split(".");
|
|
78
|
+
return `${value < 0 && Number(whole + (part ?? "")) !== 0 ? "-" : ""}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${part ? `.${part}` : ""}`;
|
|
79
|
+
}
|
|
80
|
+
const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
81
|
+
const isName = (value) => typeof value === "string" && IDENT.test(value);
|
|
82
|
+
const OPERATORS = [
|
|
83
|
+
"==",
|
|
84
|
+
"!=",
|
|
85
|
+
"<=",
|
|
86
|
+
">=",
|
|
87
|
+
"&&",
|
|
88
|
+
"||",
|
|
89
|
+
"+",
|
|
90
|
+
"-",
|
|
91
|
+
"*",
|
|
92
|
+
"/",
|
|
93
|
+
"%",
|
|
94
|
+
"<",
|
|
95
|
+
">",
|
|
96
|
+
"!",
|
|
97
|
+
"?",
|
|
98
|
+
":",
|
|
99
|
+
"(",
|
|
100
|
+
")",
|
|
101
|
+
","
|
|
102
|
+
];
|
|
103
|
+
function tokenize(source) {
|
|
104
|
+
const tokens = [];
|
|
105
|
+
let at = 0;
|
|
106
|
+
while (at < source.length) {
|
|
107
|
+
const char = source[at];
|
|
108
|
+
if (/\s/.test(char)) {
|
|
109
|
+
at += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const number = /^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(source.slice(at));
|
|
113
|
+
if (number) {
|
|
114
|
+
tokens.push({
|
|
115
|
+
kind: "number",
|
|
116
|
+
value: Number(number[0]),
|
|
117
|
+
at
|
|
118
|
+
});
|
|
119
|
+
at += number[0].length;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const name = /^[A-Za-z_][A-Za-z0-9_]*/.exec(source.slice(at));
|
|
123
|
+
if (name) {
|
|
124
|
+
tokens.push({
|
|
125
|
+
kind: "name",
|
|
126
|
+
value: name[0],
|
|
127
|
+
at
|
|
128
|
+
});
|
|
129
|
+
at += name[0].length;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (char === "\"" || char === "'") {
|
|
133
|
+
let end = at + 1;
|
|
134
|
+
let text = "";
|
|
135
|
+
while (end < source.length && source[end] !== char) {
|
|
136
|
+
if (source[end] === "\\" && end + 1 < source.length) {
|
|
137
|
+
text += source[end + 1];
|
|
138
|
+
end += 2;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
text += source[end];
|
|
142
|
+
end += 1;
|
|
143
|
+
}
|
|
144
|
+
if (end >= source.length) fail(`unclosed string at ${at + 1}`);
|
|
145
|
+
tokens.push({
|
|
146
|
+
kind: "string",
|
|
147
|
+
value: text,
|
|
148
|
+
at
|
|
149
|
+
});
|
|
150
|
+
at = end + 1;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const operator = OPERATORS.find((op) => source.startsWith(op, at));
|
|
154
|
+
if (operator === void 0) fail(`unexpected ${JSON.stringify(char)} at ${at + 1}`);
|
|
155
|
+
tokens.push({
|
|
156
|
+
kind: "op",
|
|
157
|
+
value: operator,
|
|
158
|
+
at
|
|
159
|
+
});
|
|
160
|
+
at += operator.length;
|
|
161
|
+
}
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
const BINARY = [
|
|
165
|
+
["||"],
|
|
166
|
+
["&&"],
|
|
167
|
+
["==", "!="],
|
|
168
|
+
[
|
|
169
|
+
"<",
|
|
170
|
+
"<=",
|
|
171
|
+
">",
|
|
172
|
+
">="
|
|
173
|
+
],
|
|
174
|
+
["+", "-"],
|
|
175
|
+
[
|
|
176
|
+
"*",
|
|
177
|
+
"/",
|
|
178
|
+
"%"
|
|
179
|
+
]
|
|
180
|
+
];
|
|
181
|
+
function parse(source) {
|
|
182
|
+
const tokens = tokenize(source);
|
|
183
|
+
let index = 0;
|
|
184
|
+
let depth = 0;
|
|
185
|
+
const peek = () => tokens[index];
|
|
186
|
+
const isOp = (value) => peek()?.kind === "op" && peek().value === value;
|
|
187
|
+
const expect = (value) => {
|
|
188
|
+
if (!isOp(value)) fail(peek() ? `expected "${value}" at ${peek().at + 1}` : `expected "${value}" at the end`);
|
|
189
|
+
index += 1;
|
|
190
|
+
};
|
|
191
|
+
const nest = (read) => {
|
|
192
|
+
depth += 1;
|
|
193
|
+
if (depth > MAX_NESTING) fail(`nested more than ${MAX_NESTING} levels`);
|
|
194
|
+
const node = read();
|
|
195
|
+
depth -= 1;
|
|
196
|
+
return node;
|
|
197
|
+
};
|
|
198
|
+
const ternary = () => nest(() => {
|
|
199
|
+
const test = binary(0);
|
|
200
|
+
if (!isOp("?")) return test;
|
|
201
|
+
index += 1;
|
|
202
|
+
const yes = ternary();
|
|
203
|
+
expect(":");
|
|
204
|
+
return {
|
|
205
|
+
type: "if",
|
|
206
|
+
test,
|
|
207
|
+
yes,
|
|
208
|
+
no: ternary()
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
const binary = (level) => {
|
|
212
|
+
if (level === BINARY.length) return unary();
|
|
213
|
+
let left = binary(level + 1);
|
|
214
|
+
while (peek()?.kind === "op" && BINARY[level].includes(peek().value)) {
|
|
215
|
+
const op = peek().value;
|
|
216
|
+
index += 1;
|
|
217
|
+
left = {
|
|
218
|
+
type: "binary",
|
|
219
|
+
op,
|
|
220
|
+
left,
|
|
221
|
+
right: binary(level + 1)
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return left;
|
|
225
|
+
};
|
|
226
|
+
const unary = () => {
|
|
227
|
+
if (isOp("-") || isOp("!") || isOp("+")) {
|
|
228
|
+
const op = peek().value;
|
|
229
|
+
index += 1;
|
|
230
|
+
return nest(() => ({
|
|
231
|
+
type: "unary",
|
|
232
|
+
op,
|
|
233
|
+
value: unary()
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
return primary();
|
|
237
|
+
};
|
|
238
|
+
const primary = () => {
|
|
239
|
+
const token = peek();
|
|
240
|
+
if (token === void 0) fail("the formula ends too early");
|
|
241
|
+
index += 1;
|
|
242
|
+
if (token.kind === "number" || token.kind === "string") return {
|
|
243
|
+
type: "value",
|
|
244
|
+
value: token.value
|
|
245
|
+
};
|
|
246
|
+
if (token.kind === "op" && token.value === "(") {
|
|
247
|
+
const inner = ternary();
|
|
248
|
+
expect(")");
|
|
249
|
+
return inner;
|
|
250
|
+
}
|
|
251
|
+
if (token.kind !== "name") fail(`unexpected "${token.value}" at ${token.at + 1}`);
|
|
252
|
+
if (!isOp("(")) return CONSTANTS.has(token.value) ? {
|
|
253
|
+
type: "value",
|
|
254
|
+
value: CONSTANTS.get(token.value)
|
|
255
|
+
} : {
|
|
256
|
+
type: "name",
|
|
257
|
+
name: token.value
|
|
258
|
+
};
|
|
259
|
+
const fn = Object.hasOwn(FUNCTIONS, token.value) ? FUNCTIONS[token.value] : void 0;
|
|
260
|
+
if (fn === void 0) fail(`unknown function "${token.value}"; allowed: ${FORMULA_FUNCTIONS.join(", ")}`);
|
|
261
|
+
index += 1;
|
|
262
|
+
const args = [];
|
|
263
|
+
if (!isOp(")")) {
|
|
264
|
+
args.push(ternary());
|
|
265
|
+
while (isOp(",")) {
|
|
266
|
+
index += 1;
|
|
267
|
+
args.push(ternary());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
expect(")");
|
|
271
|
+
if (args.length < fn.args[0] || args.length > fn.args[1]) fail(`${token.value}() takes ${fn.args[0] === fn.args[1] ? fn.args[0] : fn.args[1] === Infinity ? `at least ${fn.args[0]}` : `${fn.args[0]} or ${fn.args[1]}`} argument${fn.args[1] === 1 ? "" : "s"}; got ${args.length}`);
|
|
272
|
+
return {
|
|
273
|
+
type: "call",
|
|
274
|
+
name: token.value,
|
|
275
|
+
args
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
if (tokens.length === 0) fail("the formula is empty");
|
|
279
|
+
const tree = ternary();
|
|
280
|
+
if (index < tokens.length) fail(`unexpected "${peek().value}" at ${peek().at + 1}`);
|
|
281
|
+
return tree;
|
|
282
|
+
}
|
|
283
|
+
function namesIn(node, found = /* @__PURE__ */ new Set()) {
|
|
284
|
+
if (node.type === "name") found.add(node.name);
|
|
285
|
+
else if (node.type === "unary") namesIn(node.value, found);
|
|
286
|
+
else if (node.type === "binary") {
|
|
287
|
+
namesIn(node.left, found);
|
|
288
|
+
namesIn(node.right, found);
|
|
289
|
+
} else if (node.type === "if") {
|
|
290
|
+
namesIn(node.test, found);
|
|
291
|
+
namesIn(node.yes, found);
|
|
292
|
+
namesIn(node.no, found);
|
|
293
|
+
} else if (node.type === "call") for (const arg of node.args) namesIn(arg, found);
|
|
294
|
+
return found;
|
|
295
|
+
}
|
|
296
|
+
function compileFormula(source) {
|
|
297
|
+
if (typeof source !== "string") return { error: "expected a formula string" };
|
|
298
|
+
try {
|
|
299
|
+
const tree = parse(source);
|
|
300
|
+
return {
|
|
301
|
+
tree,
|
|
302
|
+
names: [...namesIn(tree)]
|
|
303
|
+
};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
if (error instanceof FormulaError) return { error: error.message };
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function run(node, lookup) {
|
|
310
|
+
switch (node.type) {
|
|
311
|
+
case "value": return node.value;
|
|
312
|
+
case "name": return lookup(node.name);
|
|
313
|
+
case "unary": {
|
|
314
|
+
const value = run(node.value, lookup);
|
|
315
|
+
if (node.op === "!") return !truthy(value);
|
|
316
|
+
return node.op === "-" ? -num(value) : num(value);
|
|
317
|
+
}
|
|
318
|
+
case "if": return truthy(run(node.test, lookup)) ? run(node.yes, lookup) : run(node.no, lookup);
|
|
319
|
+
case "call": return FUNCTIONS[node.name].run(...node.args.map((arg) => run(arg, lookup)));
|
|
320
|
+
default: return binary(node, lookup);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const truthy = (value) => typeof value === "string" ? value !== "" : Boolean(value);
|
|
324
|
+
function binary(node, lookup) {
|
|
325
|
+
if (node.op === "&&") return truthy(run(node.left, lookup)) ? run(node.right, lookup) : false;
|
|
326
|
+
if (node.op === "||") {
|
|
327
|
+
const left = run(node.left, lookup);
|
|
328
|
+
return truthy(left) ? left : run(node.right, lookup);
|
|
329
|
+
}
|
|
330
|
+
const left = run(node.left, lookup);
|
|
331
|
+
const right = run(node.right, lookup);
|
|
332
|
+
switch (node.op) {
|
|
333
|
+
case "+": return typeof left === "string" || typeof right === "string" ? `${show(left)}${show(right)}` : num(left) + num(right);
|
|
334
|
+
case "-": return num(left) - num(right);
|
|
335
|
+
case "*": return num(left) * num(right);
|
|
336
|
+
case "/": return num(left) / num(right);
|
|
337
|
+
case "%": return num(left) % num(right);
|
|
338
|
+
case "==": return left === right;
|
|
339
|
+
case "!=": return left !== right;
|
|
340
|
+
case "<": return num(left) < num(right);
|
|
341
|
+
case "<=": return num(left) <= num(right);
|
|
342
|
+
case ">": return num(left) > num(right);
|
|
343
|
+
default: return num(left) >= num(right);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function show(value) {
|
|
347
|
+
if (typeof value === "number") return Number.isFinite(value) ? String(Number(value.toPrecision(12))) : String(value);
|
|
348
|
+
return String(value);
|
|
349
|
+
}
|
|
350
|
+
function createScope(inputs, lets) {
|
|
351
|
+
const done = /* @__PURE__ */ new Map();
|
|
352
|
+
const busy = /* @__PURE__ */ new Set();
|
|
353
|
+
const lookup = (name) => {
|
|
354
|
+
if (inputs.has(name)) return inputs.get(name);
|
|
355
|
+
if (done.has(name)) return done.get(name);
|
|
356
|
+
const tree = lets.get(name);
|
|
357
|
+
if (tree === void 0) fail(`unknown name "${name}"`);
|
|
358
|
+
if (busy.has(name)) fail(`"${name}" refers to itself`);
|
|
359
|
+
busy.add(name);
|
|
360
|
+
try {
|
|
361
|
+
const value = run(tree, lookup);
|
|
362
|
+
done.set(name, value);
|
|
363
|
+
return value;
|
|
364
|
+
} finally {
|
|
365
|
+
busy.delete(name);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
return lookup;
|
|
369
|
+
}
|
|
370
|
+
function evaluate(tree, lookup) {
|
|
371
|
+
try {
|
|
372
|
+
return { value: run(tree, lookup) };
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error instanceof FormulaError) return { error: error.message };
|
|
375
|
+
if (error instanceof RangeError) return { error: "the formula is too deep to work out" };
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region src/core/views.js
|
|
381
|
+
const LAYOUT = ["stack", "row"];
|
|
382
|
+
const DISPLAY = [
|
|
383
|
+
"text",
|
|
384
|
+
"metric",
|
|
385
|
+
"table",
|
|
386
|
+
"chart",
|
|
387
|
+
"progress",
|
|
388
|
+
"callout",
|
|
389
|
+
"sources"
|
|
390
|
+
];
|
|
391
|
+
const INPUT_TYPES = [
|
|
392
|
+
"slider",
|
|
393
|
+
"choice",
|
|
394
|
+
"toggle",
|
|
395
|
+
"number"
|
|
396
|
+
];
|
|
397
|
+
const ANSWERS = [
|
|
398
|
+
"options",
|
|
399
|
+
"confirm",
|
|
400
|
+
"form",
|
|
401
|
+
"submit"
|
|
402
|
+
];
|
|
403
|
+
const COMPONENTS = [
|
|
404
|
+
...LAYOUT,
|
|
405
|
+
...DISPLAY,
|
|
406
|
+
...INPUT_TYPES,
|
|
407
|
+
...ANSWERS
|
|
408
|
+
];
|
|
409
|
+
const TONES = [
|
|
410
|
+
"good",
|
|
411
|
+
"warn",
|
|
412
|
+
"bad"
|
|
413
|
+
];
|
|
414
|
+
const TEXT_TONES = ["muted"];
|
|
415
|
+
const CALLOUT_TONES = [
|
|
416
|
+
"info",
|
|
417
|
+
"good",
|
|
418
|
+
"warn",
|
|
419
|
+
"bad"
|
|
420
|
+
];
|
|
421
|
+
const CHART_KINDS = [
|
|
422
|
+
"bar",
|
|
423
|
+
"hbar",
|
|
424
|
+
"line",
|
|
425
|
+
"area",
|
|
426
|
+
"donut"
|
|
427
|
+
];
|
|
428
|
+
const INTERACTIVE = new Set(ANSWERS);
|
|
429
|
+
const INPUTS = new Set(INPUT_TYPES);
|
|
430
|
+
const PROPS = {
|
|
431
|
+
stack: ["children", "let"],
|
|
432
|
+
row: ["children", "let"],
|
|
433
|
+
text: ["text", "tone"],
|
|
434
|
+
metric: [
|
|
435
|
+
"label",
|
|
436
|
+
"value",
|
|
437
|
+
"tone",
|
|
438
|
+
"caption"
|
|
439
|
+
],
|
|
440
|
+
table: ["columns", "rows"],
|
|
441
|
+
chart: [
|
|
442
|
+
"kind",
|
|
443
|
+
"categories",
|
|
444
|
+
"series",
|
|
445
|
+
"unit",
|
|
446
|
+
"caption"
|
|
447
|
+
],
|
|
448
|
+
progress: [
|
|
449
|
+
"label",
|
|
450
|
+
"percent",
|
|
451
|
+
"value",
|
|
452
|
+
"max",
|
|
453
|
+
"valueLabel",
|
|
454
|
+
"tone"
|
|
455
|
+
],
|
|
456
|
+
callout: ["text", "tone"],
|
|
457
|
+
sources: ["items"],
|
|
458
|
+
slider: [
|
|
459
|
+
"id",
|
|
460
|
+
"label",
|
|
461
|
+
"min",
|
|
462
|
+
"max",
|
|
463
|
+
"step",
|
|
464
|
+
"value",
|
|
465
|
+
"unit"
|
|
466
|
+
],
|
|
467
|
+
choice: [
|
|
468
|
+
"id",
|
|
469
|
+
"label",
|
|
470
|
+
"options",
|
|
471
|
+
"value"
|
|
472
|
+
],
|
|
473
|
+
toggle: [
|
|
474
|
+
"id",
|
|
475
|
+
"label",
|
|
476
|
+
"value"
|
|
477
|
+
],
|
|
478
|
+
number: [
|
|
479
|
+
"id",
|
|
480
|
+
"label",
|
|
481
|
+
"min",
|
|
482
|
+
"max",
|
|
483
|
+
"step",
|
|
484
|
+
"value",
|
|
485
|
+
"unit"
|
|
486
|
+
],
|
|
487
|
+
options: [
|
|
488
|
+
"id",
|
|
489
|
+
"options",
|
|
490
|
+
"multi"
|
|
491
|
+
],
|
|
492
|
+
confirm: [
|
|
493
|
+
"id",
|
|
494
|
+
"confirmLabel",
|
|
495
|
+
"cancelLabel"
|
|
496
|
+
],
|
|
497
|
+
form: [
|
|
498
|
+
"id",
|
|
499
|
+
"fields",
|
|
500
|
+
"submitLabel"
|
|
501
|
+
],
|
|
502
|
+
submit: ["id", "label"]
|
|
503
|
+
};
|
|
504
|
+
const MAX_DEPTH = 8;
|
|
505
|
+
const MAX_NODES = 64;
|
|
506
|
+
const MAX_CHILDREN = 16;
|
|
507
|
+
const MAX_TEXT = 2e3;
|
|
508
|
+
const MAX_CELL = 200;
|
|
509
|
+
const MAX_COLUMNS = 8;
|
|
510
|
+
const MAX_ROWS = 50;
|
|
511
|
+
const MAX_OPTIONS = 8;
|
|
512
|
+
const MAX_FIELDS = 6;
|
|
513
|
+
const SUMMARY_PART = 40;
|
|
514
|
+
const SUMMARY_COUNT = 8;
|
|
515
|
+
const RESERVED = /* @__PURE__ */ new Set(["true", "false"]);
|
|
516
|
+
const VIEW_BYTE_LIMIT = 8192;
|
|
517
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
518
|
+
const quoted = (value) => JSON.stringify(value);
|
|
519
|
+
const allowed = (list) => list.join(", ");
|
|
520
|
+
const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
521
|
+
const isScalar = (value) => typeof value === "string" || typeof value === "boolean" || isNumber(value);
|
|
522
|
+
const isFormula = (value) => isRecord(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "=");
|
|
523
|
+
function viewBytes(title, view) {
|
|
524
|
+
return new TextEncoder().encode(`${JSON.stringify(view) ?? ""}${title ?? ""}`).length;
|
|
525
|
+
}
|
|
526
|
+
const NUMERIC = [
|
|
527
|
+
"value",
|
|
528
|
+
"percent",
|
|
529
|
+
"min",
|
|
530
|
+
"max",
|
|
531
|
+
"step",
|
|
532
|
+
"height"
|
|
533
|
+
];
|
|
534
|
+
const numeric = (value) => typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)) ? Number(value) : value;
|
|
535
|
+
function normalizeView(value) {
|
|
536
|
+
if (Array.isArray(value)) return value.map(normalizeView);
|
|
537
|
+
if (!isRecord(value)) return value;
|
|
538
|
+
const keys = Object.keys(value);
|
|
539
|
+
if (keys.length === 1 && keys[0] === "item") {
|
|
540
|
+
const inner = normalizeView(value.item);
|
|
541
|
+
if (Array.isArray(inner)) return inner;
|
|
542
|
+
}
|
|
543
|
+
const out = {};
|
|
544
|
+
for (const key of keys) Object.defineProperty(out, key, {
|
|
545
|
+
value: normalizeView(value[key]),
|
|
546
|
+
writable: true,
|
|
547
|
+
enumerable: true,
|
|
548
|
+
configurable: true
|
|
549
|
+
});
|
|
550
|
+
for (const field of NUMERIC) if (Object.hasOwn(out, field)) out[field] = numeric(out[field]);
|
|
551
|
+
if (Array.isArray(out.values)) out.values = out.values.map(numeric);
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
function validateView(view) {
|
|
555
|
+
const ctx = {
|
|
556
|
+
ids: /* @__PURE__ */ new Set(),
|
|
557
|
+
nodes: 0,
|
|
558
|
+
interactive: false,
|
|
559
|
+
inputs: /* @__PURE__ */ new Map(),
|
|
560
|
+
lets: /* @__PURE__ */ new Map(),
|
|
561
|
+
formulas: []
|
|
562
|
+
};
|
|
563
|
+
const error = checkNode(view, "view", 1, ctx) ?? checkFormulas(ctx);
|
|
564
|
+
return error === void 0 ? {
|
|
565
|
+
ok: true,
|
|
566
|
+
interactive: ctx.interactive
|
|
567
|
+
} : {
|
|
568
|
+
ok: false,
|
|
569
|
+
error
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function checkShownView(title, sent) {
|
|
573
|
+
let bytes;
|
|
574
|
+
try {
|
|
575
|
+
bytes = viewBytes(title, sent);
|
|
576
|
+
} catch {
|
|
577
|
+
return { error: "The view is not JSON data." };
|
|
578
|
+
}
|
|
579
|
+
if (bytes > 8192) return { error: `The view is ${bytes} bytes; the limit is ${VIEW_BYTE_LIMIT}.` };
|
|
580
|
+
if (title !== void 0 && title !== null && typeof title !== "string") return { error: "title: expected a string" };
|
|
581
|
+
const named = String(title ?? "").trim();
|
|
582
|
+
const view = normalizeView(sent);
|
|
583
|
+
const checked = validateView(view);
|
|
584
|
+
if (!checked.ok) return { error: checked.error };
|
|
585
|
+
return {
|
|
586
|
+
title: named,
|
|
587
|
+
view,
|
|
588
|
+
interactive: checked.interactive
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function checkNode(node, path, depth, ctx) {
|
|
592
|
+
if (!isRecord(node)) return `${path}: expected a component object`;
|
|
593
|
+
if (depth > MAX_DEPTH) return `${path}: ${depth} levels exceeds the limit of ${MAX_DEPTH}`;
|
|
594
|
+
ctx.nodes += 1;
|
|
595
|
+
if (ctx.nodes > MAX_NODES) return `${path}: ${ctx.nodes} components exceeds the limit of ${MAX_NODES}`;
|
|
596
|
+
const type = node.type;
|
|
597
|
+
if (!COMPONENTS.includes(type)) return `${path}.type: unknown component ${quoted(type)}; allowed: ${allowed(COMPONENTS)}`;
|
|
598
|
+
const props = PROPS[type];
|
|
599
|
+
for (const key of Object.keys(node)) {
|
|
600
|
+
if (key === "type" || props.includes(key)) continue;
|
|
601
|
+
return `${path}.${key}: unknown property ${quoted(key)}; allowed: ${allowed(props)}`;
|
|
602
|
+
}
|
|
603
|
+
if (INTERACTIVE.has(type)) ctx.interactive = true;
|
|
604
|
+
switch (type) {
|
|
605
|
+
case "stack":
|
|
606
|
+
case "row": return checkLayout(node, path, depth, ctx);
|
|
607
|
+
case "text": return checkText(node, path, ctx);
|
|
608
|
+
case "metric": return checkMetric(node, path, ctx);
|
|
609
|
+
case "table": return checkTable(node, path, ctx);
|
|
610
|
+
case "chart": return checkChart(node, path, ctx);
|
|
611
|
+
case "progress": return checkProgress(node, path, ctx);
|
|
612
|
+
case "callout": return checkCallout(node, path, ctx);
|
|
613
|
+
case "sources": return checkSources(node, path);
|
|
614
|
+
case "slider":
|
|
615
|
+
case "number": return checkNumberInput(node, path, ctx);
|
|
616
|
+
case "choice": return checkChoice(node, path, ctx);
|
|
617
|
+
case "toggle": return checkToggle(node, path, ctx);
|
|
618
|
+
case "options": return checkOptions(node, path, ctx);
|
|
619
|
+
case "confirm": return checkConfirm(node, path, ctx);
|
|
620
|
+
case "form": return checkForm(node, path, ctx);
|
|
621
|
+
default: return checkSubmit(node, path, ctx);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function checkLayout(node, path, depth, ctx) {
|
|
625
|
+
if (node.let !== void 0) {
|
|
626
|
+
if (depth !== 1) return `${path}.let: only the outermost component can define names`;
|
|
627
|
+
if (!isRecord(node.let)) return `${path}.let: expected an object of names to formulas`;
|
|
628
|
+
for (const [name, source] of Object.entries(node.let)) {
|
|
629
|
+
if (!isName(name) || RESERVED.has(name)) return `${path}.let.${name}: a name is letters, digits and _, and starts with a letter or _`;
|
|
630
|
+
if (typeof source !== "string") return `${path}.let.${name}: expected a formula string`;
|
|
631
|
+
ctx.lets.set(name, {
|
|
632
|
+
source,
|
|
633
|
+
path: `${path}.let.${name}`
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (!Array.isArray(node.children)) return `${path}.children: expected an array of 1 to ${MAX_CHILDREN} components`;
|
|
638
|
+
if (node.children.length < 1 || node.children.length > MAX_CHILDREN) return `${path}.children: expected 1 to ${MAX_CHILDREN} children; got ${node.children.length}`;
|
|
639
|
+
for (let index = 0; index < node.children.length; index += 1) {
|
|
640
|
+
const error = checkNode(node.children[index], `${path}.children[${index}]`, depth + 1, ctx);
|
|
641
|
+
if (error !== void 0) return error;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function checkValue(node, key, path, ctx, kind, { required = false, max = MAX_TEXT } = {}) {
|
|
645
|
+
const value = node[key];
|
|
646
|
+
const at = `${path}.${key}`;
|
|
647
|
+
if (value === void 0) return required ? `${at}: ${expected(kind)}` : void 0;
|
|
648
|
+
if (isFormula(value)) {
|
|
649
|
+
ctx.formulas.push({
|
|
650
|
+
source: value["="],
|
|
651
|
+
path: at,
|
|
652
|
+
kind
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (kind === "number") return isNumber(value) ? void 0 : `${at}: ${expected(kind)}`;
|
|
657
|
+
if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `${at}: unknown tone ${quoted(value)}; allowed: ${allowed(kind)}`;
|
|
658
|
+
if (kind === "string") {
|
|
659
|
+
if (typeof value !== "string") return `${at}: ${expected(kind)}`;
|
|
660
|
+
return value.length > max ? `${at}: ${value.length} characters exceeds the limit of ${max}` : void 0;
|
|
661
|
+
}
|
|
662
|
+
if (!isScalar(value)) return `${at}: ${expected(kind)}`;
|
|
663
|
+
}
|
|
664
|
+
const expected = (kind) => kind === "number" ? "expected a number" : kind === "string" ? "expected a string" : Array.isArray(kind) ? `expected one of ${allowed(kind)}` : "expected a string or a number";
|
|
665
|
+
function checkText(node, path, ctx) {
|
|
666
|
+
return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, TEXT_TONES);
|
|
667
|
+
}
|
|
668
|
+
function checkMetric(node, path, ctx) {
|
|
669
|
+
if (typeof node.label !== "string") return `${path}.label: expected a string`;
|
|
670
|
+
if (node.value === void 0) return `${path}.value: expected a string or a number`;
|
|
671
|
+
if (typeof node.value === "number" && !Number.isFinite(node.value)) return `${path}.value: expected a finite number`;
|
|
672
|
+
return checkValue(node, "value", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES) ?? checkValue(node, "caption", path, ctx, "string");
|
|
673
|
+
}
|
|
674
|
+
function checkTable(node, path, ctx) {
|
|
675
|
+
if (!Array.isArray(node.columns)) return `${path}.columns: expected an array of at most ${MAX_COLUMNS} columns`;
|
|
676
|
+
if (node.columns.length > MAX_COLUMNS) return `${path}.columns: ${node.columns.length} columns exceeds the limit of ${MAX_COLUMNS}`;
|
|
677
|
+
for (let index = 0; index < node.columns.length; index += 1) {
|
|
678
|
+
const column = node.columns[index];
|
|
679
|
+
const at = `${path}.columns[${index}]`;
|
|
680
|
+
if (!isRecord(column)) return `${at}: expected a column object`;
|
|
681
|
+
for (const key of Object.keys(column)) if (key !== "key" && key !== "label") return `${at}.${key}: unknown property ${quoted(key)}; allowed: key, label`;
|
|
682
|
+
if (typeof column.key !== "string" || column.key === "") return `${at}.key: expected a non-empty string`;
|
|
683
|
+
if (typeof column.label !== "string") return `${at}.label: expected a string`;
|
|
684
|
+
}
|
|
685
|
+
if (!Array.isArray(node.rows)) return `${path}.rows: expected an array of at most ${MAX_ROWS} rows`;
|
|
686
|
+
if (node.rows.length > MAX_ROWS) return `${path}.rows: ${node.rows.length} rows exceeds the limit of ${MAX_ROWS}`;
|
|
687
|
+
for (let row = 0; row < node.rows.length; row += 1) {
|
|
688
|
+
const cells = node.rows[row];
|
|
689
|
+
if (!Array.isArray(cells)) return `${path}.rows[${row}]: expected an array of cells`;
|
|
690
|
+
for (let column = 0; column < cells.length; column += 1) {
|
|
691
|
+
const at = `${path}.rows[${row}][${column}]`;
|
|
692
|
+
if (isFormula(cells[column])) {
|
|
693
|
+
ctx.formulas.push({
|
|
694
|
+
source: cells[column]["="],
|
|
695
|
+
path: at,
|
|
696
|
+
kind: "scalar"
|
|
697
|
+
});
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const text = cellText(cells[column]);
|
|
701
|
+
if (text.length > MAX_CELL) return `${at}: ${text.length} characters exceeds the limit of ${MAX_CELL}`;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
function checkChart(node, path, ctx) {
|
|
706
|
+
if (node.kind !== void 0 && !CHART_KINDS.includes(node.kind)) return `${path}.kind: unknown chart ${quoted(node.kind)}; allowed: ${allowed(CHART_KINDS)}`;
|
|
707
|
+
if (!Array.isArray(node.categories) || node.categories.length === 0) return `${path}.categories: expected a non-empty array of labels`;
|
|
708
|
+
for (let index = 0; index < node.categories.length; index += 1) {
|
|
709
|
+
const label = node.categories[index];
|
|
710
|
+
if (typeof label !== "string" && !isNumber(label)) return `${path}.categories[${index}]: expected a string`;
|
|
711
|
+
}
|
|
712
|
+
if (!Array.isArray(node.series) || node.series.length === 0) return `${path}.series: expected a non-empty array of { name?, values }`;
|
|
713
|
+
if (node.kind === "donut" && node.series.length > 1) return `${path}.series: a donut takes one series; got ${node.series.length}`;
|
|
714
|
+
for (let index = 0; index < node.series.length; index += 1) {
|
|
715
|
+
const series = node.series[index];
|
|
716
|
+
const at = `${path}.series[${index}]`;
|
|
717
|
+
if (!isRecord(series)) return `${at}: expected a series object`;
|
|
718
|
+
for (const key of Object.keys(series)) if (key !== "name" && key !== "values") return `${at}.${key}: unknown property ${quoted(key)}; allowed: name, values`;
|
|
719
|
+
if (series.name !== void 0 && typeof series.name !== "string") return `${at}.name: expected a string`;
|
|
720
|
+
if (!Array.isArray(series.values)) return `${at}.values: expected an array of numbers`;
|
|
721
|
+
if (series.values.length !== node.categories.length) return `${at}.values: ${series.values.length} values for ${node.categories.length} categories`;
|
|
722
|
+
for (let item = 0; item < series.values.length; item += 1) {
|
|
723
|
+
const value = series.values[item];
|
|
724
|
+
if (isFormula(value)) ctx.formulas.push({
|
|
725
|
+
source: value["="],
|
|
726
|
+
path: `${at}.values[${item}]`,
|
|
727
|
+
kind: "number"
|
|
728
|
+
});
|
|
729
|
+
else if (!isNumber(value)) return `${at}.values[${item}]: expected a number`;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
|
|
733
|
+
if (node.caption !== void 0 && typeof node.caption !== "string") return `${path}.caption: expected a string`;
|
|
734
|
+
}
|
|
735
|
+
function checkProgress(node, path, ctx) {
|
|
736
|
+
if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
|
|
737
|
+
if (node.percent === void 0 === (node.value === void 0)) return `${path}: give percent (0 to 100), or value with max`;
|
|
738
|
+
if (node.percent !== void 0 && !isFormula(node.percent)) {
|
|
739
|
+
if (typeof node.percent !== "number" || !Number.isFinite(node.percent) || node.percent < 0 || node.percent > 100) return `${path}.percent: expected a number from 0 to 100`;
|
|
740
|
+
}
|
|
741
|
+
if (node.value !== void 0 && node.max === void 0) return `${path}.max: expected a number with value`;
|
|
742
|
+
if (node.value === void 0 && node.max !== void 0) return `${path}.value: expected a number with max`;
|
|
743
|
+
return checkValue(node, "percent", path, ctx, "number") ?? checkValue(node, "value", path, ctx, "number") ?? checkValue(node, "max", path, ctx, "number") ?? checkValue(node, "valueLabel", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES);
|
|
744
|
+
}
|
|
745
|
+
function checkCallout(node, path, ctx) {
|
|
746
|
+
return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, CALLOUT_TONES);
|
|
747
|
+
}
|
|
748
|
+
function checkSources(node, path) {
|
|
749
|
+
if (!Array.isArray(node.items) || node.items.length === 0) return `${path}.items: expected a non-empty array of { label, url }`;
|
|
750
|
+
for (let index = 0; index < node.items.length; index += 1) {
|
|
751
|
+
const item = node.items[index];
|
|
752
|
+
const at = `${path}.items[${index}]`;
|
|
753
|
+
if (!isRecord(item)) return `${at}: expected a { label, url } object`;
|
|
754
|
+
for (const key of Object.keys(item)) if (key !== "label" && key !== "url") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, url`;
|
|
755
|
+
if (typeof item.label !== "string" || item.label.trim() === "") return `${at}.label: expected a non-empty string`;
|
|
756
|
+
if (typeof item.url !== "string" || !/^https?:\/\/[^\s]+$/i.test(item.url)) return `${at}.url: expected an http(s) address`;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
function checkInputId(node, path, ctx) {
|
|
760
|
+
if (!isName(node.id) || RESERVED.has(node.id)) return `${path}.id: an input id is letters, digits and _, and starts with a letter or _, so formulas can use it`;
|
|
761
|
+
if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
|
|
762
|
+
ctx.ids.add(node.id);
|
|
763
|
+
if (typeof node.label !== "string") return `${path}.label: expected a string`;
|
|
764
|
+
}
|
|
765
|
+
function checkNumberInput(node, path, ctx) {
|
|
766
|
+
const idError = checkInputId(node, path, ctx);
|
|
767
|
+
if (idError !== void 0) return idError;
|
|
768
|
+
for (const key of [
|
|
769
|
+
"min",
|
|
770
|
+
"max",
|
|
771
|
+
"step"
|
|
772
|
+
]) if (node[key] !== void 0 && !isNumber(node[key])) return `${path}.${key}: expected a number`;
|
|
773
|
+
if (node.type === "slider" && (node.min === void 0 || node.max === void 0)) return `${path}: a slider needs min and max`;
|
|
774
|
+
if (node.min !== void 0 && node.max !== void 0 && node.max <= node.min) return `${path}.max: expected more than min (${node.min})`;
|
|
775
|
+
if (node.step !== void 0 && node.step <= 0) return `${path}.step: expected more than 0`;
|
|
776
|
+
if (!isNumber(node.value)) return `${path}.value: expected a number`;
|
|
777
|
+
if (node.min !== void 0 && node.value < node.min || node.max !== void 0 && node.value > node.max) return `${path}.value: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}`;
|
|
778
|
+
if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
|
|
779
|
+
ctx.inputs.set(node.id, node.value);
|
|
780
|
+
}
|
|
781
|
+
function checkChoice(node, path, ctx) {
|
|
782
|
+
const idError = checkInputId(node, path, ctx);
|
|
783
|
+
if (idError !== void 0) return idError;
|
|
784
|
+
if (!Array.isArray(node.options) || node.options.length === 0) return `${path}.options: expected a non-empty array of { label, value }`;
|
|
785
|
+
const labels = /* @__PURE__ */ new Set();
|
|
786
|
+
const values = /* @__PURE__ */ new Set();
|
|
787
|
+
for (let index = 0; index < node.options.length; index += 1) {
|
|
788
|
+
const option = node.options[index];
|
|
789
|
+
const at = `${path}.options[${index}]`;
|
|
790
|
+
if (!isRecord(option)) return `${at}: expected a { label, value } object`;
|
|
791
|
+
for (const key of Object.keys(option)) if (key !== "label" && key !== "value") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, value`;
|
|
792
|
+
if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
|
|
793
|
+
if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
|
|
794
|
+
labels.add(option.label);
|
|
795
|
+
const value = option.value ?? option.label;
|
|
796
|
+
if (!isScalar(value)) return `${at}.value: expected a string, a number or true/false`;
|
|
797
|
+
if (values.has(value)) return `${at}.value: duplicate value ${quoted(value)}`;
|
|
798
|
+
values.add(value);
|
|
799
|
+
}
|
|
800
|
+
const chosen = node.value ?? choiceValue(node.options[0]);
|
|
801
|
+
if (!values.has(chosen)) return `${path}.value: expected one of the options' values`;
|
|
802
|
+
ctx.inputs.set(node.id, chosen);
|
|
803
|
+
}
|
|
804
|
+
function checkToggle(node, path, ctx) {
|
|
805
|
+
const idError = checkInputId(node, path, ctx);
|
|
806
|
+
if (idError !== void 0) return idError;
|
|
807
|
+
if (node.value !== void 0 && typeof node.value !== "boolean") return `${path}.value: expected true or false`;
|
|
808
|
+
ctx.inputs.set(node.id, node.value === true);
|
|
809
|
+
}
|
|
810
|
+
function checkOptions(node, path, ctx) {
|
|
811
|
+
const idError = checkId(node, path, ctx);
|
|
812
|
+
if (idError !== void 0) return idError;
|
|
813
|
+
if (node.multi !== void 0 && typeof node.multi !== "boolean") return `${path}.multi: expected a boolean`;
|
|
814
|
+
if (!Array.isArray(node.options)) return `${path}.options: expected an array of 1 to ${MAX_OPTIONS} options`;
|
|
815
|
+
if (node.options.length < 1 || node.options.length > MAX_OPTIONS) return `${path}.options: expected 1 to ${MAX_OPTIONS} options; got ${node.options.length}`;
|
|
816
|
+
const labels = /* @__PURE__ */ new Set();
|
|
817
|
+
for (let index = 0; index < node.options.length; index += 1) {
|
|
818
|
+
const option = node.options[index];
|
|
819
|
+
const at = `${path}.options[${index}]`;
|
|
820
|
+
if (!isRecord(option)) return `${at}: expected an option object`;
|
|
821
|
+
for (const key of Object.keys(option)) if (key !== "label" && key !== "description") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, description`;
|
|
822
|
+
if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
|
|
823
|
+
if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
|
|
824
|
+
labels.add(option.label);
|
|
825
|
+
if (option.description !== void 0 && typeof option.description !== "string") return `${at}.description: expected a string`;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function checkConfirm(node, path, ctx) {
|
|
829
|
+
const idError = checkId(node, path, ctx);
|
|
830
|
+
if (idError !== void 0) return idError;
|
|
831
|
+
if (node.confirmLabel !== void 0 && typeof node.confirmLabel !== "string") return `${path}.confirmLabel: expected a string`;
|
|
832
|
+
if (node.cancelLabel !== void 0 && typeof node.cancelLabel !== "string") return `${path}.cancelLabel: expected a string`;
|
|
833
|
+
}
|
|
834
|
+
function checkForm(node, path, ctx) {
|
|
835
|
+
const idError = checkId(node, path, ctx);
|
|
836
|
+
if (idError !== void 0) return idError;
|
|
837
|
+
if (node.submitLabel !== void 0 && typeof node.submitLabel !== "string") return `${path}.submitLabel: expected a string`;
|
|
838
|
+
if (!Array.isArray(node.fields)) return `${path}.fields: expected an array of at most ${MAX_FIELDS} fields`;
|
|
839
|
+
if (node.fields.length > MAX_FIELDS) return `${path}.fields: ${node.fields.length} fields exceeds the limit of ${MAX_FIELDS}`;
|
|
840
|
+
const seen = /* @__PURE__ */ new Set();
|
|
841
|
+
for (let index = 0; index < node.fields.length; index += 1) {
|
|
842
|
+
const field = node.fields[index];
|
|
843
|
+
const at = `${path}.fields[${index}]`;
|
|
844
|
+
if (!isRecord(field)) return `${at}: expected a field object`;
|
|
845
|
+
for (const key of Object.keys(field)) if (key !== "id" && key !== "label" && key !== "placeholder") return `${at}.${key}: unknown property ${quoted(key)}; allowed: id, label, placeholder`;
|
|
846
|
+
if (typeof field.id !== "string" || field.id === "") return `${at}.id: expected a non-empty string`;
|
|
847
|
+
if (field.id === "__proto__") return `${at}.id: "__proto__" cannot be an id`;
|
|
848
|
+
if (seen.has(field.id)) return `${at}.id: duplicate field id ${quoted(field.id)}`;
|
|
849
|
+
seen.add(field.id);
|
|
850
|
+
if (typeof field.label !== "string") return `${at}.label: expected a string`;
|
|
851
|
+
if (field.placeholder !== void 0 && typeof field.placeholder !== "string") return `${at}.placeholder: expected a string`;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function checkSubmit(node, path, ctx) {
|
|
855
|
+
const idError = checkId(node, path, ctx);
|
|
856
|
+
if (idError !== void 0) return idError;
|
|
857
|
+
if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
|
|
858
|
+
}
|
|
859
|
+
function checkId(node, path, ctx) {
|
|
860
|
+
if (typeof node.id !== "string" || node.id === "") return `${path}.id: expected a non-empty string`;
|
|
861
|
+
if (node.id === "__proto__") return `${path}.id: "__proto__" cannot be an id`;
|
|
862
|
+
if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
|
|
863
|
+
ctx.ids.add(node.id);
|
|
864
|
+
}
|
|
865
|
+
function checkFormulas(ctx) {
|
|
866
|
+
const known = /* @__PURE__ */ new Set([...ctx.inputs.keys(), ...ctx.lets.keys()]);
|
|
867
|
+
const lets = /* @__PURE__ */ new Map();
|
|
868
|
+
for (const [name, entry] of ctx.lets) {
|
|
869
|
+
if (ctx.inputs.has(name)) return `${entry.path}: ${quoted(name)} is already an input id`;
|
|
870
|
+
const compiled = compileFormula(entry.source);
|
|
871
|
+
if (compiled.error !== void 0) return `${entry.path}: ${compiled.error}`;
|
|
872
|
+
const unknown = compiled.names.find((item) => !known.has(item));
|
|
873
|
+
if (unknown !== void 0) return `${entry.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
|
|
874
|
+
lets.set(name, compiled.tree);
|
|
875
|
+
}
|
|
876
|
+
const lookup = createScope(ctx.inputs, lets);
|
|
877
|
+
for (const [name, entry] of ctx.lets) {
|
|
878
|
+
const result = evaluate(lets.get(name), lookup);
|
|
879
|
+
if (result.error !== void 0) return `${entry.path}: ${result.error}`;
|
|
880
|
+
}
|
|
881
|
+
for (const formula of ctx.formulas) {
|
|
882
|
+
const compiled = compileFormula(formula.source);
|
|
883
|
+
if (compiled.error !== void 0) return `${formula.path}: ${compiled.error}`;
|
|
884
|
+
const unknown = compiled.names.find((item) => !known.has(item));
|
|
885
|
+
if (unknown !== void 0) return `${formula.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
|
|
886
|
+
const result = evaluate(compiled.tree, lookup);
|
|
887
|
+
if (result.error !== void 0) return `${formula.path}: ${result.error}`;
|
|
888
|
+
const problem = kindProblem(result.value, formula.kind);
|
|
889
|
+
if (problem !== void 0) return `${formula.path}: the formula gives ${quoted(result.value)}; ${problem}`;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function kindProblem(value, kind) {
|
|
893
|
+
if (kind === "number") return isNumber(value) ? void 0 : "expected a number";
|
|
894
|
+
if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `expected one of ${allowed(kind)}`;
|
|
895
|
+
if (kind === "string") return isScalar(value) ? void 0 : "expected text";
|
|
896
|
+
return isScalar(value) ? void 0 : "expected a string or a number";
|
|
897
|
+
}
|
|
898
|
+
function cellText(value) {
|
|
899
|
+
if (typeof value === "string") return value;
|
|
900
|
+
const text = JSON.stringify(value);
|
|
901
|
+
return typeof text === "string" ? text : "";
|
|
902
|
+
}
|
|
903
|
+
const compiled = /* @__PURE__ */ new WeakMap();
|
|
904
|
+
const treeOf = (formula) => {
|
|
905
|
+
let entry = compiled.get(formula);
|
|
906
|
+
if (entry === void 0) {
|
|
907
|
+
entry = compileFormula(formula["="]);
|
|
908
|
+
compiled.set(formula, entry);
|
|
909
|
+
}
|
|
910
|
+
return entry;
|
|
911
|
+
};
|
|
912
|
+
const choiceValue = (option) => option?.value ?? option?.label;
|
|
913
|
+
function walk(node, visit) {
|
|
914
|
+
if (!isRecord(node)) return;
|
|
915
|
+
visit(node);
|
|
916
|
+
if (LAYOUT.includes(node.type) && Array.isArray(node.children)) for (const child of node.children) walk(child, visit);
|
|
917
|
+
}
|
|
918
|
+
function viewInputs(view) {
|
|
919
|
+
const found = [];
|
|
920
|
+
walk(view, (node) => {
|
|
921
|
+
if (INPUTS.has(node.type)) found.push(node);
|
|
922
|
+
});
|
|
923
|
+
return found;
|
|
924
|
+
}
|
|
925
|
+
function inputDefault(node) {
|
|
926
|
+
if (node.type === "toggle") return node.value === true;
|
|
927
|
+
if (node.type === "choice") return node.value ?? choiceValue(Array.isArray(node.options) ? node.options[0] : void 0);
|
|
928
|
+
return node.value;
|
|
929
|
+
}
|
|
930
|
+
const hasInputs = (view) => viewInputs(view).length > 0;
|
|
931
|
+
function viewScope(view, values = {}) {
|
|
932
|
+
const inputs = /* @__PURE__ */ new Map();
|
|
933
|
+
for (const node of viewInputs(view)) inputs.set(node.id, Object.hasOwn(values, node.id) ? values[node.id] : inputDefault(node));
|
|
934
|
+
const lets = /* @__PURE__ */ new Map();
|
|
935
|
+
for (const [name, source] of Object.entries(isRecord(view?.let) ? view.let : {})) {
|
|
936
|
+
const entry = compileFormula(source);
|
|
937
|
+
if (entry.tree !== void 0) lets.set(name, entry.tree);
|
|
938
|
+
}
|
|
939
|
+
return createScope(inputs, lets);
|
|
940
|
+
}
|
|
941
|
+
function resolve(value, lookup, fallback = void 0) {
|
|
942
|
+
if (!isFormula(value)) return value;
|
|
943
|
+
const entry = treeOf(value);
|
|
944
|
+
if (entry.tree === void 0) return fallback;
|
|
945
|
+
const result = evaluate(entry.tree, lookup);
|
|
946
|
+
return result.error === void 0 ? result.value : fallback;
|
|
947
|
+
}
|
|
948
|
+
const asText = (value) => value === void 0 || value === null ? "" : show(value);
|
|
949
|
+
function clip(text) {
|
|
950
|
+
const flat = String(text).replace(/\s+/g, " ").trim();
|
|
951
|
+
return flat.length <= SUMMARY_PART ? flat : flat.slice(0, SUMMARY_PART);
|
|
952
|
+
}
|
|
953
|
+
const choiceLabel = (node, value) => (Array.isArray(node.options) ? node.options : []).find((option) => choiceValue(option) === value)?.label ?? asText(value);
|
|
954
|
+
function inputText(node, value) {
|
|
955
|
+
if (node.type === "toggle") return value === true ? "on" : "off";
|
|
956
|
+
if (node.type === "choice") return choiceLabel(node, value);
|
|
957
|
+
return `${asText(value)}${node.unit ? ` ${node.unit}` : ""}`;
|
|
958
|
+
}
|
|
959
|
+
function progressPercent(node, lookup) {
|
|
960
|
+
if (node.percent !== void 0) return Number(resolve(node.percent, lookup, 0));
|
|
961
|
+
const max = Number(resolve(node.max, lookup, 0));
|
|
962
|
+
return max > 0 ? Number(resolve(node.value, lookup, 0)) / max * 100 : 0;
|
|
963
|
+
}
|
|
964
|
+
function componentSummary(node, lookup) {
|
|
965
|
+
const value = (item) => asText(resolve(item, lookup, ""));
|
|
966
|
+
switch (node.type) {
|
|
967
|
+
case "text": return `text: ${value(node.text)}`;
|
|
968
|
+
case "metric": return `metric: ${node.label}=${value(node.value)}`;
|
|
969
|
+
case "table": return `table: ${(Array.isArray(node.columns) ? node.columns : []).map((column) => column?.label ?? column?.key ?? "").filter(Boolean).join("/")}`;
|
|
970
|
+
case "chart": {
|
|
971
|
+
const series = Array.isArray(node.series) ? node.series[0] : void 0;
|
|
972
|
+
const values = Array.isArray(series?.values) ? series.values : [];
|
|
973
|
+
const unit = node.unit ? ` ${node.unit}` : "";
|
|
974
|
+
return `chart: ${(Array.isArray(node.categories) ? node.categories : []).map((label, index) => `${label}=${value(values[index])}${unit}`).join(", ")}`;
|
|
975
|
+
}
|
|
976
|
+
case "progress": return `progress: ${node.label ? `${node.label} ` : ""}${Math.round(progressPercent(node, lookup))}%`;
|
|
977
|
+
case "callout": return `callout: ${value(node.text)}`;
|
|
978
|
+
case "sources": return `sources: ${(Array.isArray(node.items) ? node.items : []).map((item) => item?.label).filter(Boolean).join("/")}`;
|
|
979
|
+
case "slider":
|
|
980
|
+
case "number":
|
|
981
|
+
case "choice":
|
|
982
|
+
case "toggle": return `${node.type}: ${node.label}=${inputText(node, inputDefault(node))}`;
|
|
983
|
+
case "options": return `options: ${(Array.isArray(node.options) ? node.options : []).map((option) => option?.label).filter(Boolean).join("/")}`;
|
|
984
|
+
case "confirm": return "confirm";
|
|
985
|
+
case "form": return `form: ${(Array.isArray(node.fields) ? node.fields : []).map((field) => field?.label ?? field?.id).filter(Boolean).join("/")}`;
|
|
986
|
+
case "submit": return `submit: ${node.label ?? "Send"}`;
|
|
987
|
+
default: return;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function viewSummary(view) {
|
|
991
|
+
const parts = [];
|
|
992
|
+
const lookup = viewScope(view);
|
|
993
|
+
const visit = (node) => {
|
|
994
|
+
if (parts.length >= SUMMARY_COUNT || !isRecord(node)) return;
|
|
995
|
+
const line = componentSummary(node, lookup);
|
|
996
|
+
if (line !== void 0) parts.push(clip(line));
|
|
997
|
+
if (parts.length >= SUMMARY_COUNT || !LAYOUT.includes(node.type) || !Array.isArray(node.children)) return;
|
|
998
|
+
for (const child of node.children) visit(child);
|
|
999
|
+
};
|
|
1000
|
+
visit(view);
|
|
1001
|
+
return parts.join("; ");
|
|
1002
|
+
}
|
|
1003
|
+
function inputsSummary(view, inputs) {
|
|
1004
|
+
if (!isRecord(inputs)) return "";
|
|
1005
|
+
return viewInputs(view).filter((node) => Object.hasOwn(inputs, node.id)).map((node) => `${node.label || node.id}=${inputText(node, inputs[node.id])}`).join("; ");
|
|
1006
|
+
}
|
|
1007
|
+
function interactiveNodes(node, found = []) {
|
|
1008
|
+
walk(node, (item) => {
|
|
1009
|
+
if (INTERACTIVE.has(item.type)) found.push(item);
|
|
1010
|
+
});
|
|
1011
|
+
return found;
|
|
1012
|
+
}
|
|
1013
|
+
function validateViewAnswer(view, action, values) {
|
|
1014
|
+
const nodes = interactiveNodes(view);
|
|
1015
|
+
if (typeof action !== "string" || action === "") return { error: `action: expected an interactive component id; allowed: ${allowed(nodes.map((node) => node.id))}` };
|
|
1016
|
+
const node = nodes.find((item) => item.id === action);
|
|
1017
|
+
if (node === void 0) return { error: `action: no interactive component with id ${quoted(action)}; allowed: ${allowed(nodes.map((item) => item.id))}` };
|
|
1018
|
+
if (!isRecord(values)) return { error: "values: expected an object" };
|
|
1019
|
+
const inputs = validateInputs(view, values.inputs);
|
|
1020
|
+
if (inputs.error !== void 0) return inputs;
|
|
1021
|
+
const extra = inputs.inputs === void 0 ? {} : { inputs: inputs.inputs };
|
|
1022
|
+
if (node.type === "submit") return { ...extra };
|
|
1023
|
+
const own = node.type === "options" ? validateOptions(node, values) : node.type === "confirm" ? validateConfirm(values) : validateForm(node, values);
|
|
1024
|
+
return own.error === void 0 ? {
|
|
1025
|
+
...own,
|
|
1026
|
+
...extra
|
|
1027
|
+
} : own;
|
|
1028
|
+
}
|
|
1029
|
+
function validateInputs(view, sent) {
|
|
1030
|
+
const nodes = viewInputs(view);
|
|
1031
|
+
if (nodes.length === 0) return sent === void 0 ? {} : { error: "values.inputs: this view has no inputs" };
|
|
1032
|
+
if (sent !== void 0 && !isRecord(sent)) return { error: "values.inputs: expected an object of input ids to values" };
|
|
1033
|
+
const given = sent ?? {};
|
|
1034
|
+
const ids = nodes.map((node) => node.id);
|
|
1035
|
+
const unknown = Object.keys(given).find((key) => !ids.includes(key));
|
|
1036
|
+
if (unknown !== void 0) return { error: `values.inputs.${unknown}: unknown input; allowed: ${allowed(ids)}` };
|
|
1037
|
+
const inputs = {};
|
|
1038
|
+
for (const node of nodes) {
|
|
1039
|
+
const value = Object.hasOwn(given, node.id) ? given[node.id] : inputDefault(node);
|
|
1040
|
+
const at = `values.inputs.${node.id}`;
|
|
1041
|
+
if (node.type === "toggle" && typeof value !== "boolean") return { error: `${at}: expected true or false` };
|
|
1042
|
+
if (node.type === "choice" && !(Array.isArray(node.options) ? node.options : []).some((option) => choiceValue(option) === value)) return { error: `${at}: expected one of the options' values` };
|
|
1043
|
+
if (node.type === "slider" || node.type === "number") {
|
|
1044
|
+
if (!isNumber(value)) return { error: `${at}: expected a number` };
|
|
1045
|
+
if (node.min !== void 0 && value < node.min || node.max !== void 0 && value > node.max) return { error: `${at}: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}` };
|
|
1046
|
+
}
|
|
1047
|
+
inputs[node.id] = value;
|
|
1048
|
+
}
|
|
1049
|
+
return { inputs };
|
|
1050
|
+
}
|
|
1051
|
+
function validateOptions(node, values) {
|
|
1052
|
+
if (!Array.isArray(values.selected)) return { error: "values.selected: expected an array of option labels" };
|
|
1053
|
+
const labels = new Set((node.options ?? []).map((option) => option?.label));
|
|
1054
|
+
const picked = values.selected.filter((label) => typeof label === "string" && labels.has(label));
|
|
1055
|
+
const selected = node.multi === true ? [...new Set(picked)] : picked.slice(0, 1);
|
|
1056
|
+
if (selected.length === 0) return { error: "values.selected: select at least one option" };
|
|
1057
|
+
return { selected };
|
|
1058
|
+
}
|
|
1059
|
+
function validateConfirm(values) {
|
|
1060
|
+
if (typeof values.confirmed !== "boolean") return { error: "values.confirmed: expected a boolean" };
|
|
1061
|
+
return { confirmed: values.confirmed };
|
|
1062
|
+
}
|
|
1063
|
+
function validateForm(node, values) {
|
|
1064
|
+
if (!isRecord(values.fields)) return { error: "values.fields: expected an object of field ids to strings" };
|
|
1065
|
+
const declared = (node.fields ?? []).map((field) => field?.id).filter((id) => typeof id === "string");
|
|
1066
|
+
const allowedIds = new Set(declared);
|
|
1067
|
+
const fields = {};
|
|
1068
|
+
for (const [key, value] of Object.entries(values.fields)) {
|
|
1069
|
+
if (!allowedIds.has(key)) return { error: `values.fields.${key}: unknown field; allowed: ${allowed(declared)}` };
|
|
1070
|
+
if (typeof value !== "string") return { error: `values.fields.${key}: expected a string` };
|
|
1071
|
+
if (value.length > MAX_TEXT) return { error: `values.fields.${key}: ${value.length} characters exceeds the limit of ${MAX_TEXT}` };
|
|
1072
|
+
fields[key] = value;
|
|
1073
|
+
}
|
|
1074
|
+
return { fields };
|
|
1075
|
+
}
|
|
1076
|
+
//#endregion
|
|
1077
|
+
export { show as C, isName as S, viewSummary as _, choiceValue as a, createScope as b, inputsSummary as c, resolve as d, validateView as f, viewScope as g, viewInputs as h, checkShownView as i, isFormula as l, viewBytes as m, VIEW_BYTE_LIMIT as n, hasInputs as o, validateViewAnswer as p, asText as r, inputDefault as s, CHART_KINDS as t, normalizeView as u, FORMULA_FUNCTIONS as v, evaluate as x, compileFormula as y };
|
|
1078
|
+
|
|
1079
|
+
//# sourceMappingURL=core2.js.map
|