@assistant-ui/react-generative-ui 0.0.21 → 0.0.23
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 +1 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +30 -13
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/RadioGroupScope.d.ts +6 -0
- package/dist/RadioGroupScope.d.ts.map +1 -0
- package/dist/RadioGroupScope.js +25 -0
- package/dist/RadioGroupScope.js.map +1 -0
- package/dist/a2ui/BindingContext.d.ts +11 -0
- package/dist/a2ui/BindingContext.d.ts.map +1 -0
- package/dist/a2ui/BindingContext.js +12 -0
- package/dist/a2ui/BindingContext.js.map +1 -0
- package/dist/a2ui/PresentRenderer.d.ts +11 -0
- package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
- package/dist/a2ui/PresentRenderer.js +39 -0
- package/dist/a2ui/PresentRenderer.js.map +1 -0
- package/dist/a2ui/convert.d.ts +6 -0
- package/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +363 -85
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/a2ui/dataModel.d.ts +9 -0
- package/dist/a2ui/dataModel.d.ts.map +1 -0
- package/dist/a2ui/dataModel.js +44 -0
- package/dist/a2ui/dataModel.js.map +1 -0
- package/dist/a2ui/reducer.d.ts +5 -0
- package/dist/a2ui/reducer.d.ts.map +1 -1
- package/dist/a2ui/reducer.js +3 -3
- package/dist/a2ui/reducer.js.map +1 -1
- package/dist/a2ui/types.d.ts +8 -2
- package/dist/a2ui/types.d.ts.map +1 -1
- package/dist/a2ui/types.js.map +1 -1
- package/dist/a2ui/valueFunctions.d.ts +16 -0
- package/dist/a2ui/valueFunctions.d.ts.map +1 -0
- package/dist/a2ui/valueFunctions.js +332 -0
- package/dist/a2ui/valueFunctions.js.map +1 -0
- package/dist/actionRegistry.d.ts +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/answeredValues.d.ts +16 -0
- package/dist/answeredValues.d.ts.map +1 -0
- package/dist/answeredValues.js +37 -0
- package/dist/answeredValues.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -1
- package/dist/constants.js.map +1 -1
- package/dist/fieldReferences.d.ts +9 -0
- package/dist/fieldReferences.d.ts.map +1 -0
- package/dist/fieldReferences.js +53 -0
- package/dist/fieldReferences.js.map +1 -0
- package/dist/generativeUIToJSX.js +3 -3
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.server.js +2 -2
- package/dist/index.shared.js +2 -2
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js.map +1 -1
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +12 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +2 -0
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +3 -1
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts +3 -1
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.js +96 -7
- package/dist/slack/decodeBlockAction.js.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.js +43 -3
- package/dist/slack/fromSlackBlocks.js.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +244 -20
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +24 -1
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/slack.d.ts +1 -1
- package/dist/slack.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +1 -1
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +1 -1
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/decodeSubmitData.d.ts +4 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.js +29 -3
- package/dist/teams/decodeSubmitData.js.map +1 -1
- package/dist/teams/temporalId.d.ts +5 -0
- package/dist/teams/temporalId.d.ts.map +1 -0
- package/dist/teams/temporalId.js +28 -0
- package/dist/teams/temporalId.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +5 -4
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +136 -18
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.js.map +1 -1
- package/dist/teams/types.d.ts +23 -2
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/teams.d.ts +1 -1
- package/dist/teams.d.ts.map +1 -1
- package/dist/temporal.d.ts +33 -0
- package/dist/temporal.d.ts.map +1 -0
- package/dist/temporal.js +152 -0
- package/dist/temporal.js.map +1 -0
- package/dist/vocabulary/alert.d.ts +9 -2
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +116 -19
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +2 -1
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.js +15 -4
- package/dist/vocabulary/collectFormValues.js.map +1 -1
- package/dist/vocabulary/data.d.ts +22 -5
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +278 -83
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +3 -2
- package/dist/vocabulary/dispatch.d.ts.map +1 -1
- package/dist/vocabulary/dispatch.js +33 -4
- package/dist/vocabulary/dispatch.js.map +1 -1
- package/dist/vocabulary/fact.d.ts +8 -1
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/fact.js +37 -11
- package/dist/vocabulary/fact.js.map +1 -1
- package/dist/vocabulary/form.d.ts.map +1 -1
- package/dist/vocabulary/form.js +3 -3
- package/dist/vocabulary/form.js.map +1 -1
- package/dist/vocabulary/formatValue.d.ts +7 -0
- package/dist/vocabulary/formatValue.d.ts.map +1 -0
- package/dist/vocabulary/formatValue.js +76 -0
- package/dist/vocabulary/formatValue.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +1 -1
- package/dist/vocabulary/interactive.d.ts +143 -8
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +458 -107
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +4 -4
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.js +2 -2
- package/dist/vocabulary/list.js.map +1 -1
- package/dist/vocabulary/text.d.ts +1 -1
- package/package.json +8 -5
- package/src/JSONGenerativeUI.client.tsx +84 -40
- package/src/JSONGenerativeUI.test.tsx +964 -68
- package/src/RadioGroupScope.server.test.tsx +24 -0
- package/src/RadioGroupScope.tsx +34 -0
- package/src/a2ui/BindingContext.ts +26 -0
- package/src/a2ui/PresentRenderer.tsx +83 -0
- package/src/a2ui/convert.test.ts +2054 -229
- package/src/a2ui/convert.ts +665 -134
- package/src/a2ui/dataModel.ts +69 -0
- package/src/a2ui/reducer.test.ts +35 -0
- package/src/a2ui/reducer.ts +5 -4
- package/src/a2ui/types.ts +9 -2
- package/src/a2ui/valueFunctions.test.ts +271 -0
- package/src/a2ui/valueFunctions.ts +523 -0
- package/src/actionRegistry.ts +1 -1
- package/src/answeredValues.test.ts +33 -0
- package/src/answeredValues.tsx +70 -0
- package/src/constants.ts +5 -0
- package/src/fieldReferences.test.ts +66 -0
- package/src/fieldReferences.ts +83 -0
- package/src/generativeUIToJSX.test.ts +18 -0
- package/src/generativeUIToJSX.ts +4 -6
- package/src/index.server.test.ts +106 -0
- package/src/ir.ts +1 -1
- package/src/renderGenerativeUI.test.tsx +14 -5
- package/src/renderGenerativeUI.tsx +30 -1
- package/src/slack/a2uiSlack.test.ts +198 -0
- package/src/slack/constants.ts +3 -0
- package/src/slack/datePicker.test.ts +612 -0
- package/src/slack/decodeBlockAction.test.ts +98 -0
- package/src/slack/decodeBlockAction.ts +182 -16
- package/src/slack/fromSlackBlocks.test.ts +222 -3
- package/src/slack/fromSlackBlocks.ts +82 -2
- package/src/slack/toSlackBlocks.test.ts +682 -3
- package/src/slack/toSlackBlocks.ts +463 -25
- package/src/slack/types.ts +28 -0
- package/src/slack.ts +2 -0
- package/src/teams/constants.ts +1 -1
- package/src/teams/decodeSubmitData.test.ts +249 -0
- package/src/teams/decodeSubmitData.ts +57 -4
- package/src/teams/temporal.test.ts +766 -0
- package/src/teams/temporalId.ts +56 -0
- package/src/teams/toAdaptiveCard.test.ts +219 -0
- package/src/teams/toAdaptiveCard.ts +259 -21
- package/src/teams/toTeamsAttachments.ts +4 -1
- package/src/teams/types.ts +21 -1
- package/src/teams.ts +1 -0
- package/src/temporal.test.ts +319 -0
- package/src/temporal.ts +259 -0
- package/src/vocabulary/alert.test.tsx +288 -7
- package/src/vocabulary/alert.tsx +168 -26
- package/src/vocabulary/collectFormValues.test.ts +225 -1
- package/src/vocabulary/collectFormValues.ts +54 -5
- package/src/vocabulary/data.test.tsx +248 -6
- package/src/vocabulary/data.tsx +398 -100
- package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
- package/src/vocabulary/dispatch.ts +63 -2
- package/src/vocabulary/fact.test.tsx +67 -0
- package/src/vocabulary/fact.tsx +40 -9
- package/src/vocabulary/form.tsx +7 -2
- package/src/vocabulary/formatValue.test.ts +72 -0
- package/src/vocabulary/formatValue.ts +127 -0
- package/src/vocabulary/index.test.ts +2 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
- package/src/vocabulary/interactive.dom.test.tsx +1747 -0
- package/src/vocabulary/interactive.test.tsx +60 -4
- package/src/vocabulary/interactive.tsx +910 -155
- package/src/vocabulary/layout.test.tsx +6 -4
- package/src/vocabulary/layout.tsx +13 -2
- package/src/vocabulary/list.tsx +2 -2
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
//#region src/a2ui/valueFunctions.ts
|
|
2
|
+
const MAX_TEMPLATE_LENGTH = 1e4;
|
|
3
|
+
const MAX_TEMPLATE_PARTS = 1e3;
|
|
4
|
+
const MAX_EXPRESSION_DEPTH = 32;
|
|
5
|
+
var ExpressionSyntaxError = class extends Error {};
|
|
6
|
+
const FUNCTION_NAME = /^[A-Za-z_@]\w*$/;
|
|
7
|
+
const isSpace = (character) => /\s/.test(character);
|
|
8
|
+
const isPathCharacter = (character) => !isSpace(character) && !"{}(),:'\"$".includes(character);
|
|
9
|
+
const isDigit = (character) => character !== void 0 && character >= "0" && character <= "9";
|
|
10
|
+
var ExpressionReader = class {
|
|
11
|
+
text;
|
|
12
|
+
position;
|
|
13
|
+
constructor(text, position) {
|
|
14
|
+
this.text = text;
|
|
15
|
+
this.position = position;
|
|
16
|
+
}
|
|
17
|
+
expression(depth) {
|
|
18
|
+
if (depth > MAX_EXPRESSION_DEPTH) this.fail("it nests too deeply");
|
|
19
|
+
this.skipSpace();
|
|
20
|
+
const character = this.text[this.position];
|
|
21
|
+
if (this.text.startsWith("${", this.position)) {
|
|
22
|
+
this.position += 2;
|
|
23
|
+
this.skipSpace();
|
|
24
|
+
if (this.text[this.position] === "}") {
|
|
25
|
+
this.position++;
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
const value = this.expression(depth + 1);
|
|
29
|
+
this.skipSpace();
|
|
30
|
+
if (this.text[this.position] !== "}") this.fail(this.position >= this.text.length ? "an interpolation is not closed" : "characters follow a complete expression");
|
|
31
|
+
this.position++;
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
if (character === "'" || character === "\"") return this.string(character);
|
|
35
|
+
if (isDigit(character) || character === "-" && isDigit(this.text[this.position + 1])) return this.number();
|
|
36
|
+
const word = this.word();
|
|
37
|
+
if (word === "") this.fail("an expression is missing");
|
|
38
|
+
const end = this.position;
|
|
39
|
+
this.skipSpace();
|
|
40
|
+
if (this.text[this.position] === "(") {
|
|
41
|
+
if (!FUNCTION_NAME.test(word)) this.fail(`"${word}" is not a function`);
|
|
42
|
+
this.position++;
|
|
43
|
+
return {
|
|
44
|
+
call: word,
|
|
45
|
+
args: this.arguments(depth)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
this.position = end;
|
|
49
|
+
if (word === "true") return true;
|
|
50
|
+
if (word === "false") return false;
|
|
51
|
+
if (word === "null") return null;
|
|
52
|
+
return { path: word };
|
|
53
|
+
}
|
|
54
|
+
arguments(depth) {
|
|
55
|
+
const args = {};
|
|
56
|
+
this.skipSpace();
|
|
57
|
+
if (this.text[this.position] === ")") {
|
|
58
|
+
this.position++;
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
for (;;) {
|
|
62
|
+
this.skipSpace();
|
|
63
|
+
const name = this.word();
|
|
64
|
+
if (!FUNCTION_NAME.test(name)) this.fail("an argument name is missing");
|
|
65
|
+
this.skipSpace();
|
|
66
|
+
if (this.text[this.position] !== ":") this.fail(`argument "${name}" has no colon`);
|
|
67
|
+
this.position++;
|
|
68
|
+
Object.defineProperty(args, name, {
|
|
69
|
+
value: this.expression(depth + 1),
|
|
70
|
+
enumerable: true,
|
|
71
|
+
configurable: true,
|
|
72
|
+
writable: true
|
|
73
|
+
});
|
|
74
|
+
this.skipSpace();
|
|
75
|
+
const separator = this.text[this.position++];
|
|
76
|
+
if (separator === ")") return args;
|
|
77
|
+
if (separator !== ",") this.fail(separator === void 0 ? "a function call is not closed" : "arguments are not separated by commas");
|
|
78
|
+
this.skipSpace();
|
|
79
|
+
if (this.text[this.position] === ")") {
|
|
80
|
+
this.position++;
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
string(quote) {
|
|
86
|
+
let value = "";
|
|
87
|
+
this.position++;
|
|
88
|
+
for (;;) {
|
|
89
|
+
const character = this.text[this.position++];
|
|
90
|
+
if (character === void 0) this.fail("a string is not closed");
|
|
91
|
+
if (character === quote) return value;
|
|
92
|
+
if (character !== "\\") {
|
|
93
|
+
value += character;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const escaped = this.text[this.position++];
|
|
97
|
+
if (escaped === void 0) this.fail("a string is not closed");
|
|
98
|
+
value += escaped === "n" ? "\n" : escaped === "t" ? " " : escaped === "r" ? "\r" : escaped;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
number() {
|
|
102
|
+
const start = this.position;
|
|
103
|
+
if (this.text[this.position] === "-") this.position++;
|
|
104
|
+
while (isDigit(this.text[this.position])) this.position++;
|
|
105
|
+
if (this.text[this.position] === ".") {
|
|
106
|
+
this.position++;
|
|
107
|
+
while (isDigit(this.text[this.position])) this.position++;
|
|
108
|
+
}
|
|
109
|
+
const next = this.text[this.position];
|
|
110
|
+
if (next !== void 0 && isPathCharacter(next)) this.fail("a number is malformed");
|
|
111
|
+
return Number(this.text.slice(start, this.position));
|
|
112
|
+
}
|
|
113
|
+
word() {
|
|
114
|
+
const start = this.position;
|
|
115
|
+
while (this.position < this.text.length && isPathCharacter(this.text[this.position])) this.position++;
|
|
116
|
+
return this.text.slice(start, this.position);
|
|
117
|
+
}
|
|
118
|
+
skipSpace() {
|
|
119
|
+
while (this.position < this.text.length && isSpace(this.text[this.position])) this.position++;
|
|
120
|
+
}
|
|
121
|
+
fail(reason) {
|
|
122
|
+
throw new ExpressionSyntaxError(reason);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const parseExpressionTemplate = (template) => {
|
|
126
|
+
if (template.length > MAX_TEMPLATE_LENGTH) throw new ExpressionSyntaxError(`it is longer than ${MAX_TEMPLATE_LENGTH} characters`);
|
|
127
|
+
const parts = [];
|
|
128
|
+
const push = (part) => {
|
|
129
|
+
if (parts.length >= MAX_TEMPLATE_PARTS) throw new ExpressionSyntaxError(`it has more than ${MAX_TEMPLATE_PARTS} parts`);
|
|
130
|
+
parts.push(part);
|
|
131
|
+
};
|
|
132
|
+
let literal = "";
|
|
133
|
+
let index = 0;
|
|
134
|
+
while (index < template.length) if (template.startsWith("\\${", index)) {
|
|
135
|
+
literal += "${";
|
|
136
|
+
index += 3;
|
|
137
|
+
} else if (template.startsWith("${", index)) {
|
|
138
|
+
if (literal) push(literal);
|
|
139
|
+
literal = "";
|
|
140
|
+
const reader = new ExpressionReader(template, index);
|
|
141
|
+
push(reader.expression(0));
|
|
142
|
+
index = reader.position;
|
|
143
|
+
} else {
|
|
144
|
+
literal += template[index];
|
|
145
|
+
index++;
|
|
146
|
+
}
|
|
147
|
+
if (literal) push(literal);
|
|
148
|
+
return parts;
|
|
149
|
+
};
|
|
150
|
+
const isMissing = (value) => value === void 0 || value === null || value === "";
|
|
151
|
+
const toNumber = (value) => {
|
|
152
|
+
const number = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : NaN;
|
|
153
|
+
return Number.isFinite(number) ? number : void 0;
|
|
154
|
+
};
|
|
155
|
+
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
156
|
+
const toDate = (value) => {
|
|
157
|
+
if (typeof value === "number") return new Date(value);
|
|
158
|
+
if (typeof value !== "string") return void 0;
|
|
159
|
+
const dateOnly = DATE_ONLY.exec(value);
|
|
160
|
+
if (!dateOnly) return new Date(value);
|
|
161
|
+
const [year, month, day] = dateOnly.slice(1).map(Number);
|
|
162
|
+
const date = /* @__PURE__ */ new Date(0);
|
|
163
|
+
date.setFullYear(year, month - 1, day);
|
|
164
|
+
date.setHours(0, 0, 0, 0);
|
|
165
|
+
return date.getMonth() === month - 1 && date.getDate() === day ? date : void 0;
|
|
166
|
+
};
|
|
167
|
+
const toText = (value) => {
|
|
168
|
+
if (value === void 0 || value === null) return "";
|
|
169
|
+
if (typeof value !== "object") return String(value);
|
|
170
|
+
try {
|
|
171
|
+
return JSON.stringify(value) ?? "";
|
|
172
|
+
} catch {
|
|
173
|
+
return "";
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const cached = (cache, key, create) => {
|
|
177
|
+
let value = cache.get(key);
|
|
178
|
+
if (value === void 0) {
|
|
179
|
+
value = create();
|
|
180
|
+
if (cache.size >= 64) cache.clear();
|
|
181
|
+
cache.set(key, value);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
};
|
|
185
|
+
const numberFormats = /* @__PURE__ */ new Map();
|
|
186
|
+
const dateFormats = /* @__PURE__ */ new Map();
|
|
187
|
+
const pluralRules = /* @__PURE__ */ new Map();
|
|
188
|
+
const invalidArguments = (name, context) => {
|
|
189
|
+
context.warn(`A2UI function "${name}" received invalid arguments.`);
|
|
190
|
+
};
|
|
191
|
+
const formatAmount = (name, args, context, currency) => {
|
|
192
|
+
if (isMissing(args["value"])) return "";
|
|
193
|
+
const value = toNumber(args["value"]);
|
|
194
|
+
const decimals = args["decimals"] === void 0 ? void 0 : toNumber(args["decimals"]);
|
|
195
|
+
if (value === void 0 || args["decimals"] !== void 0 && (decimals === void 0 || !Number.isInteger(decimals) || decimals < 0 || decimals > 20)) return invalidArguments(name, context);
|
|
196
|
+
const options = {
|
|
197
|
+
useGrouping: args["grouping"] !== false,
|
|
198
|
+
...currency !== void 0 ? {
|
|
199
|
+
style: "currency",
|
|
200
|
+
currency
|
|
201
|
+
} : {},
|
|
202
|
+
...decimals !== void 0 ? {
|
|
203
|
+
minimumFractionDigits: decimals,
|
|
204
|
+
maximumFractionDigits: decimals
|
|
205
|
+
} : {}
|
|
206
|
+
};
|
|
207
|
+
try {
|
|
208
|
+
return cached(numberFormats, JSON.stringify(options), () => new Intl.NumberFormat(void 0, options)).format(value);
|
|
209
|
+
} catch {
|
|
210
|
+
return invalidArguments(name, context);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const dateName = (date, options, type) => {
|
|
214
|
+
try {
|
|
215
|
+
return cached(dateFormats, JSON.stringify(options), () => new Intl.DateTimeFormat(void 0, options)).formatToParts(date).find((part) => part.type === type)?.value;
|
|
216
|
+
} catch {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const pad = (value, width) => String(value).padStart(width, "0");
|
|
221
|
+
const dateField = (date, letter, width) => {
|
|
222
|
+
switch (letter) {
|
|
223
|
+
case "y": return width === 2 ? pad(date.getFullYear() % 100, 2) : pad(date.getFullYear(), width);
|
|
224
|
+
case "M": return width <= 2 ? pad(date.getMonth() + 1, width) : dateName(date, { month: width === 3 ? "short" : width === 4 ? "long" : "narrow" }, "month");
|
|
225
|
+
case "d": return pad(date.getDate(), width);
|
|
226
|
+
case "E": return dateName(date, { weekday: width <= 3 ? "short" : width === 4 ? "long" : "narrow" }, "weekday");
|
|
227
|
+
case "h": return pad(date.getHours() % 12 || 12, width);
|
|
228
|
+
case "H": return pad(date.getHours(), width);
|
|
229
|
+
case "m": return pad(date.getMinutes(), width);
|
|
230
|
+
case "s": return pad(date.getSeconds(), width);
|
|
231
|
+
case "a": return dateName(date, {
|
|
232
|
+
hour: "numeric",
|
|
233
|
+
hour12: true
|
|
234
|
+
}, "dayPeriod") ?? (date.getHours() < 12 ? "AM" : "PM");
|
|
235
|
+
default: return;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const formatDatePattern = (date, pattern) => {
|
|
239
|
+
let result = "";
|
|
240
|
+
let index = 0;
|
|
241
|
+
while (index < pattern.length) {
|
|
242
|
+
const character = pattern[index];
|
|
243
|
+
if (character === "'") {
|
|
244
|
+
index++;
|
|
245
|
+
if (pattern[index] === "'") {
|
|
246
|
+
result += "'";
|
|
247
|
+
index++;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
while (index < pattern.length) if (pattern[index] !== "'") result += pattern[index++];
|
|
251
|
+
else if (pattern[index + 1] === "'") {
|
|
252
|
+
result += "'";
|
|
253
|
+
index += 2;
|
|
254
|
+
} else {
|
|
255
|
+
index++;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
} else if (/[A-Za-z]/.test(character)) {
|
|
259
|
+
let end = index + 1;
|
|
260
|
+
while (pattern[end] === character) end++;
|
|
261
|
+
const field = dateField(date, character, end - index);
|
|
262
|
+
if (field === void 0) return void 0;
|
|
263
|
+
result += field;
|
|
264
|
+
index = end;
|
|
265
|
+
} else {
|
|
266
|
+
result += character;
|
|
267
|
+
index++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
};
|
|
272
|
+
const pluralCategory = (value) => {
|
|
273
|
+
try {
|
|
274
|
+
return cached(pluralRules, "", () => new Intl.PluralRules()).select(value);
|
|
275
|
+
} catch {
|
|
276
|
+
return value === 1 ? "one" : "other";
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const VALUE_FUNCTIONS = {
|
|
280
|
+
formatString: (args, context) => {
|
|
281
|
+
const template = args["value"];
|
|
282
|
+
if (template === void 0 || template === null) return "";
|
|
283
|
+
if (typeof template !== "string") return invalidArguments("formatString", context);
|
|
284
|
+
let parts = context.templates.get(template);
|
|
285
|
+
if (parts === void 0) {
|
|
286
|
+
try {
|
|
287
|
+
parts = parseExpressionTemplate(template);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!(error instanceof ExpressionSyntaxError)) throw error;
|
|
290
|
+
context.warn(`A2UI formatString template is malformed: ${error.message}.`);
|
|
291
|
+
parts = null;
|
|
292
|
+
}
|
|
293
|
+
context.templates.set(template, parts);
|
|
294
|
+
}
|
|
295
|
+
if (parts === null) return void 0;
|
|
296
|
+
return parts.map((part) => toText(typeof part === "object" && part !== null ? context.resolve(part) : part)).join("");
|
|
297
|
+
},
|
|
298
|
+
formatNumber: (args, context) => formatAmount("formatNumber", args, context),
|
|
299
|
+
formatCurrency: (args, context) => typeof args["currency"] === "string" ? formatAmount("formatCurrency", args, context, args["currency"]) : invalidArguments("formatCurrency", context),
|
|
300
|
+
formatDate: (args, context) => {
|
|
301
|
+
const value = args["value"];
|
|
302
|
+
if (isMissing(value)) return "";
|
|
303
|
+
const format = args["format"];
|
|
304
|
+
const date = toDate(value);
|
|
305
|
+
if (typeof format !== "string" || !date || Number.isNaN(date.getTime())) return invalidArguments("formatDate", context);
|
|
306
|
+
const formatted = formatDatePattern(date, format);
|
|
307
|
+
if (formatted !== void 0) return formatted;
|
|
308
|
+
context.warn(`A2UI function "formatDate" does not support the pattern "${format}", so it returned an ISO date.`);
|
|
309
|
+
return date.toISOString();
|
|
310
|
+
},
|
|
311
|
+
pluralize: (args, context) => {
|
|
312
|
+
if (isMissing(args["value"])) return "";
|
|
313
|
+
const value = toNumber(args["value"]);
|
|
314
|
+
if (value === void 0 || typeof args["other"] !== "string") return invalidArguments("pluralize", context);
|
|
315
|
+
const selected = args[pluralCategory(value)];
|
|
316
|
+
return typeof selected === "string" ? selected : args["other"];
|
|
317
|
+
},
|
|
318
|
+
and: (args, context) => Array.isArray(args["values"]) ? args["values"].every(Boolean) : invalidArguments("and", context),
|
|
319
|
+
or: (args, context) => Array.isArray(args["values"]) ? args["values"].some(Boolean) : invalidArguments("or", context),
|
|
320
|
+
not: (args) => !args["value"]
|
|
321
|
+
};
|
|
322
|
+
const evaluateA2uiValueFunction = (name, args, context) => {
|
|
323
|
+
if (!Object.hasOwn(VALUE_FUNCTIONS, name)) {
|
|
324
|
+
context.warn(`A2UI function "${name}" is not supported and was skipped.`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
return VALUE_FUNCTIONS[name](args, context);
|
|
328
|
+
};
|
|
329
|
+
//#endregion
|
|
330
|
+
export { ExpressionSyntaxError, evaluateA2uiValueFunction, parseExpressionTemplate };
|
|
331
|
+
|
|
332
|
+
//# sourceMappingURL=valueFunctions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"valueFunctions.js","names":[],"sources":["../../src/a2ui/valueFunctions.ts"],"sourcesContent":["const MAX_TEMPLATE_LENGTH = 10_000;\nconst MAX_TEMPLATE_PARTS = 1_000;\nconst MAX_EXPRESSION_DEPTH = 32;\n\nexport type ExpressionPart =\n | string\n | number\n | boolean\n | null\n | { readonly path: string }\n | { readonly call: string; readonly args: Record<string, ExpressionPart> };\n\nexport type ValueFunctionContext = {\n readonly resolve: (value: ExpressionPart) => unknown;\n readonly warn: (message: string) => void;\n readonly templates: Map<string, ExpressionPart[] | null>;\n};\n\nexport class ExpressionSyntaxError extends Error {}\n\nconst FUNCTION_NAME = /^[A-Za-z_@]\\w*$/;\n\nconst isSpace = (character: string) => /\\s/.test(character);\n\nconst isPathCharacter = (character: string) =>\n !isSpace(character) && !\"{}(),:'\\\"$\".includes(character);\n\nconst isDigit = (character: string | undefined) =>\n character !== undefined && character >= \"0\" && character <= \"9\";\n\nclass ExpressionReader {\n private readonly text: string;\n position: number;\n\n constructor(text: string, position: number) {\n this.text = text;\n this.position = position;\n }\n\n expression(depth: number): ExpressionPart {\n if (depth > MAX_EXPRESSION_DEPTH) this.fail(\"it nests too deeply\");\n this.skipSpace();\n const character = this.text[this.position];\n if (this.text.startsWith(\"${\", this.position)) {\n this.position += 2;\n this.skipSpace();\n if (this.text[this.position] === \"}\") {\n this.position++;\n return \"\";\n }\n const value = this.expression(depth + 1);\n this.skipSpace();\n if (this.text[this.position] !== \"}\") {\n this.fail(\n this.position >= this.text.length\n ? \"an interpolation is not closed\"\n : \"characters follow a complete expression\",\n );\n }\n this.position++;\n return value;\n }\n if (character === \"'\" || character === '\"') return this.string(character);\n if (\n isDigit(character) ||\n (character === \"-\" && isDigit(this.text[this.position + 1]))\n ) {\n return this.number();\n }\n const word = this.word();\n if (word === \"\") this.fail(\"an expression is missing\");\n const end = this.position;\n this.skipSpace();\n if (this.text[this.position] === \"(\") {\n if (!FUNCTION_NAME.test(word)) this.fail(`\"${word}\" is not a function`);\n this.position++;\n return { call: word, args: this.arguments(depth) };\n }\n this.position = end;\n if (word === \"true\") return true;\n if (word === \"false\") return false;\n if (word === \"null\") return null;\n return { path: word };\n }\n\n private arguments(depth: number): Record<string, ExpressionPart> {\n const args: Record<string, ExpressionPart> = {};\n this.skipSpace();\n if (this.text[this.position] === \")\") {\n this.position++;\n return args;\n }\n for (;;) {\n this.skipSpace();\n const name = this.word();\n if (!FUNCTION_NAME.test(name)) this.fail(\"an argument name is missing\");\n this.skipSpace();\n if (this.text[this.position] !== \":\") {\n this.fail(`argument \"${name}\" has no colon`);\n }\n this.position++;\n Object.defineProperty(args, name, {\n value: this.expression(depth + 1),\n enumerable: true,\n configurable: true,\n writable: true,\n });\n this.skipSpace();\n const separator = this.text[this.position++];\n if (separator === \")\") return args;\n if (separator !== \",\") {\n this.fail(\n separator === undefined\n ? \"a function call is not closed\"\n : \"arguments are not separated by commas\",\n );\n }\n this.skipSpace();\n if (this.text[this.position] === \")\") {\n this.position++;\n return args;\n }\n }\n }\n\n private string(quote: string): string {\n let value = \"\";\n this.position++;\n for (;;) {\n const character = this.text[this.position++];\n if (character === undefined) this.fail(\"a string is not closed\");\n if (character === quote) return value;\n if (character !== \"\\\\\") {\n value += character;\n continue;\n }\n const escaped = this.text[this.position++];\n if (escaped === undefined) this.fail(\"a string is not closed\");\n value +=\n escaped === \"n\"\n ? \"\\n\"\n : escaped === \"t\"\n ? \"\\t\"\n : escaped === \"r\"\n ? \"\\r\"\n : escaped;\n }\n }\n\n private number(): number {\n const start = this.position;\n if (this.text[this.position] === \"-\") this.position++;\n while (isDigit(this.text[this.position])) this.position++;\n if (this.text[this.position] === \".\") {\n this.position++;\n while (isDigit(this.text[this.position])) this.position++;\n }\n const next = this.text[this.position];\n if (next !== undefined && isPathCharacter(next)) {\n this.fail(\"a number is malformed\");\n }\n return Number(this.text.slice(start, this.position));\n }\n\n private word(): string {\n const start = this.position;\n while (\n this.position < this.text.length &&\n isPathCharacter(this.text[this.position]!)\n ) {\n this.position++;\n }\n return this.text.slice(start, this.position);\n }\n\n private skipSpace() {\n while (\n this.position < this.text.length &&\n isSpace(this.text[this.position]!)\n ) {\n this.position++;\n }\n }\n\n private fail(reason: string): never {\n throw new ExpressionSyntaxError(reason);\n }\n}\n\nexport const parseExpressionTemplate = (template: string): ExpressionPart[] => {\n if (template.length > MAX_TEMPLATE_LENGTH) {\n throw new ExpressionSyntaxError(\n `it is longer than ${MAX_TEMPLATE_LENGTH} characters`,\n );\n }\n const parts: ExpressionPart[] = [];\n const push = (part: ExpressionPart) => {\n if (parts.length >= MAX_TEMPLATE_PARTS) {\n throw new ExpressionSyntaxError(\n `it has more than ${MAX_TEMPLATE_PARTS} parts`,\n );\n }\n parts.push(part);\n };\n let literal = \"\";\n let index = 0;\n while (index < template.length) {\n if (template.startsWith(\"\\\\${\", index)) {\n literal += \"${\";\n index += 3;\n } else if (template.startsWith(\"${\", index)) {\n if (literal) push(literal);\n literal = \"\";\n const reader = new ExpressionReader(template, index);\n push(reader.expression(0));\n index = reader.position;\n } else {\n literal += template[index];\n index++;\n }\n }\n if (literal) push(literal);\n return parts;\n};\n\nconst isMissing = (value: unknown) =>\n value === undefined || value === null || value === \"\";\n\nconst toNumber = (value: unknown): number | undefined => {\n const number =\n typeof value === \"number\"\n ? value\n : typeof value === \"string\" && value.trim() !== \"\"\n ? Number(value)\n : Number.NaN;\n return Number.isFinite(number) ? number : undefined;\n};\n\nconst DATE_ONLY = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\nconst toDate = (value: unknown): Date | undefined => {\n if (typeof value === \"number\") return new Date(value);\n if (typeof value !== \"string\") return undefined;\n const dateOnly = DATE_ONLY.exec(value);\n if (!dateOnly) return new Date(value);\n const [year, month, day] = dateOnly.slice(1).map(Number) as [\n number,\n number,\n number,\n ];\n const date = new Date(0);\n date.setFullYear(year, month - 1, day);\n date.setHours(0, 0, 0, 0);\n return date.getMonth() === month - 1 && date.getDate() === day\n ? date\n : undefined;\n};\n\nconst toText = (value: unknown): string => {\n if (value === undefined || value === null) return \"\";\n if (typeof value !== \"object\") return String(value);\n try {\n return JSON.stringify(value) ?? \"\";\n } catch {\n return \"\";\n }\n};\n\nconst cached = <T>(cache: Map<string, T>, key: string, create: () => T): T => {\n let value = cache.get(key);\n if (value === undefined) {\n value = create();\n if (cache.size >= 64) cache.clear();\n cache.set(key, value);\n }\n return value;\n};\n\nconst numberFormats = new Map<string, Intl.NumberFormat>();\nconst dateFormats = new Map<string, Intl.DateTimeFormat>();\nconst pluralRules = new Map<string, Intl.PluralRules>();\n\nconst invalidArguments = (name: string, context: ValueFunctionContext) => {\n context.warn(`A2UI function \"${name}\" received invalid arguments.`);\n return undefined;\n};\n\nconst formatAmount = (\n name: string,\n args: Record<string, unknown>,\n context: ValueFunctionContext,\n currency?: string,\n): unknown => {\n if (isMissing(args[\"value\"])) return \"\";\n const value = toNumber(args[\"value\"]);\n const decimals =\n args[\"decimals\"] === undefined ? undefined : toNumber(args[\"decimals\"]);\n if (\n value === undefined ||\n (args[\"decimals\"] !== undefined &&\n (decimals === undefined ||\n !Number.isInteger(decimals) ||\n decimals < 0 ||\n decimals > 20))\n ) {\n return invalidArguments(name, context);\n }\n const options: Intl.NumberFormatOptions = {\n useGrouping: args[\"grouping\"] !== false,\n ...(currency !== undefined ? { style: \"currency\", currency } : {}),\n ...(decimals !== undefined\n ? { minimumFractionDigits: decimals, maximumFractionDigits: decimals }\n : {}),\n };\n try {\n return cached(\n numberFormats,\n JSON.stringify(options),\n () => new Intl.NumberFormat(undefined, options),\n ).format(value);\n } catch {\n return invalidArguments(name, context);\n }\n};\n\nconst dateName = (\n date: Date,\n options: Intl.DateTimeFormatOptions,\n type: Intl.DateTimeFormatPartTypes,\n): string | undefined => {\n try {\n return cached(\n dateFormats,\n JSON.stringify(options),\n () => new Intl.DateTimeFormat(undefined, options),\n )\n .formatToParts(date)\n .find((part) => part.type === type)?.value;\n } catch {\n return undefined;\n }\n};\n\nconst pad = (value: number, width: number) =>\n String(value).padStart(width, \"0\");\n\nconst dateField = (\n date: Date,\n letter: string,\n width: number,\n): string | undefined => {\n switch (letter) {\n case \"y\":\n return width === 2\n ? pad(date.getFullYear() % 100, 2)\n : pad(date.getFullYear(), width);\n case \"M\":\n return width <= 2\n ? pad(date.getMonth() + 1, width)\n : dateName(\n date,\n { month: width === 3 ? \"short\" : width === 4 ? \"long\" : \"narrow\" },\n \"month\",\n );\n case \"d\":\n return pad(date.getDate(), width);\n case \"E\":\n return dateName(\n date,\n { weekday: width <= 3 ? \"short\" : width === 4 ? \"long\" : \"narrow\" },\n \"weekday\",\n );\n case \"h\":\n return pad(date.getHours() % 12 || 12, width);\n case \"H\":\n return pad(date.getHours(), width);\n case \"m\":\n return pad(date.getMinutes(), width);\n case \"s\":\n return pad(date.getSeconds(), width);\n case \"a\":\n return (\n dateName(date, { hour: \"numeric\", hour12: true }, \"dayPeriod\") ??\n (date.getHours() < 12 ? \"AM\" : \"PM\")\n );\n default:\n return undefined;\n }\n};\n\nconst formatDatePattern = (date: Date, pattern: string): string | undefined => {\n let result = \"\";\n let index = 0;\n while (index < pattern.length) {\n const character = pattern[index]!;\n if (character === \"'\") {\n index++;\n if (pattern[index] === \"'\") {\n result += \"'\";\n index++;\n continue;\n }\n while (index < pattern.length) {\n if (pattern[index] !== \"'\") {\n result += pattern[index++];\n } else if (pattern[index + 1] === \"'\") {\n result += \"'\";\n index += 2;\n } else {\n index++;\n break;\n }\n }\n } else if (/[A-Za-z]/.test(character)) {\n let end = index + 1;\n while (pattern[end] === character) end++;\n const field = dateField(date, character, end - index);\n if (field === undefined) return undefined;\n result += field;\n index = end;\n } else {\n result += character;\n index++;\n }\n }\n return result;\n};\n\nconst pluralCategory = (value: number): string => {\n try {\n return cached(pluralRules, \"\", () => new Intl.PluralRules()).select(value);\n } catch {\n return value === 1 ? \"one\" : \"other\";\n }\n};\n\ntype ValueFunction = (\n args: Record<string, unknown>,\n context: ValueFunctionContext,\n) => unknown;\n\nconst VALUE_FUNCTIONS: Readonly<Record<string, ValueFunction>> = {\n formatString: (args, context) => {\n const template = args[\"value\"];\n if (template === undefined || template === null) return \"\";\n if (typeof template !== \"string\") {\n return invalidArguments(\"formatString\", context);\n }\n let parts = context.templates.get(template);\n if (parts === undefined) {\n try {\n parts = parseExpressionTemplate(template);\n } catch (error) {\n if (!(error instanceof ExpressionSyntaxError)) throw error;\n context.warn(\n `A2UI formatString template is malformed: ${error.message}.`,\n );\n parts = null;\n }\n context.templates.set(template, parts);\n }\n if (parts === null) return undefined;\n return parts\n .map((part) =>\n toText(\n typeof part === \"object\" && part !== null\n ? context.resolve(part)\n : part,\n ),\n )\n .join(\"\");\n },\n formatNumber: (args, context) => formatAmount(\"formatNumber\", args, context),\n formatCurrency: (args, context) =>\n typeof args[\"currency\"] === \"string\"\n ? formatAmount(\"formatCurrency\", args, context, args[\"currency\"])\n : invalidArguments(\"formatCurrency\", context),\n formatDate: (args, context) => {\n const value = args[\"value\"];\n if (isMissing(value)) return \"\";\n const format = args[\"format\"];\n const date = toDate(value);\n if (typeof format !== \"string\" || !date || Number.isNaN(date.getTime())) {\n return invalidArguments(\"formatDate\", context);\n }\n const formatted = formatDatePattern(date, format);\n if (formatted !== undefined) return formatted;\n context.warn(\n `A2UI function \"formatDate\" does not support the pattern \"${format}\", so it returned an ISO date.`,\n );\n return date.toISOString();\n },\n pluralize: (args, context) => {\n if (isMissing(args[\"value\"])) return \"\";\n const value = toNumber(args[\"value\"]);\n if (value === undefined || typeof args[\"other\"] !== \"string\") {\n return invalidArguments(\"pluralize\", context);\n }\n const selected = args[pluralCategory(value)];\n return typeof selected === \"string\" ? selected : args[\"other\"];\n },\n and: (args, context) =>\n Array.isArray(args[\"values\"])\n ? args[\"values\"].every(Boolean)\n : invalidArguments(\"and\", context),\n or: (args, context) =>\n Array.isArray(args[\"values\"])\n ? args[\"values\"].some(Boolean)\n : invalidArguments(\"or\", context),\n not: (args) => !args[\"value\"],\n};\n\nexport const evaluateA2uiValueFunction = (\n name: string,\n args: Record<string, unknown>,\n context: ValueFunctionContext,\n): unknown => {\n if (!Object.hasOwn(VALUE_FUNCTIONS, name)) {\n context.warn(`A2UI function \"${name}\" is not supported and was skipped.`);\n return undefined;\n }\n return VALUE_FUNCTIONS[name]!(args, context);\n};\n"],"mappings":";AAAA,MAAM,sBAAsB;AAC5B,MAAM,qBAAqB;AAC3B,MAAM,uBAAuB;AAgB7B,IAAa,wBAAb,cAA2C,MAAM,CAAC;AAElD,MAAM,gBAAgB;AAEtB,MAAM,WAAW,cAAsB,KAAK,KAAK,SAAS;AAE1D,MAAM,mBAAmB,cACvB,CAAC,QAAQ,SAAS,KAAK,CAAC,aAAa,SAAS,SAAS;AAEzD,MAAM,WAAW,cACf,cAAc,KAAA,KAAa,aAAa,OAAO,aAAa;AAE9D,IAAM,mBAAN,MAAuB;CACrB;CACA;CAEA,YAAY,MAAc,UAAkB;EAC1C,KAAK,OAAO;EACZ,KAAK,WAAW;CAClB;CAEA,WAAW,OAA+B;EACxC,IAAI,QAAQ,sBAAsB,KAAK,KAAK,qBAAqB;EACjE,KAAK,UAAU;EACf,MAAM,YAAY,KAAK,KAAK,KAAK;EACjC,IAAI,KAAK,KAAK,WAAW,MAAM,KAAK,QAAQ,GAAG;GAC7C,KAAK,YAAY;GACjB,KAAK,UAAU;GACf,IAAI,KAAK,KAAK,KAAK,cAAc,KAAK;IACpC,KAAK;IACL,OAAO;GACT;GACA,MAAM,QAAQ,KAAK,WAAW,QAAQ,CAAC;GACvC,KAAK,UAAU;GACf,IAAI,KAAK,KAAK,KAAK,cAAc,KAC/B,KAAK,KACH,KAAK,YAAY,KAAK,KAAK,SACvB,mCACA,yCACN;GAEF,KAAK;GACL,OAAO;EACT;EACA,IAAI,cAAc,OAAO,cAAc,MAAK,OAAO,KAAK,OAAO,SAAS;EACxE,IACE,QAAQ,SAAS,KAChB,cAAc,OAAO,QAAQ,KAAK,KAAK,KAAK,WAAW,EAAE,GAE1D,OAAO,KAAK,OAAO;EAErB,MAAM,OAAO,KAAK,KAAK;EACvB,IAAI,SAAS,IAAI,KAAK,KAAK,0BAA0B;EACrD,MAAM,MAAM,KAAK;EACjB,KAAK,UAAU;EACf,IAAI,KAAK,KAAK,KAAK,cAAc,KAAK;GACpC,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,KAAK,KAAK,IAAI,KAAK,oBAAoB;GACtE,KAAK;GACL,OAAO;IAAE,MAAM;IAAM,MAAM,KAAK,UAAU,KAAK;GAAE;EACnD;EACA,KAAK,WAAW;EAChB,IAAI,SAAS,QAAQ,OAAO;EAC5B,IAAI,SAAS,SAAS,OAAO;EAC7B,IAAI,SAAS,QAAQ,OAAO;EAC5B,OAAO,EAAE,MAAM,KAAK;CACtB;CAEA,UAAkB,OAA+C;EAC/D,MAAM,OAAuC,CAAC;EAC9C,KAAK,UAAU;EACf,IAAI,KAAK,KAAK,KAAK,cAAc,KAAK;GACpC,KAAK;GACL,OAAO;EACT;EACA,SAAS;GACP,KAAK,UAAU;GACf,MAAM,OAAO,KAAK,KAAK;GACvB,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,KAAK,KAAK,6BAA6B;GACtE,KAAK,UAAU;GACf,IAAI,KAAK,KAAK,KAAK,cAAc,KAC/B,KAAK,KAAK,aAAa,KAAK,eAAe;GAE7C,KAAK;GACL,OAAO,eAAe,MAAM,MAAM;IAChC,OAAO,KAAK,WAAW,QAAQ,CAAC;IAChC,YAAY;IACZ,cAAc;IACd,UAAU;GACZ,CAAC;GACD,KAAK,UAAU;GACf,MAAM,YAAY,KAAK,KAAK,KAAK;GACjC,IAAI,cAAc,KAAK,OAAO;GAC9B,IAAI,cAAc,KAChB,KAAK,KACH,cAAc,KAAA,IACV,kCACA,uCACN;GAEF,KAAK,UAAU;GACf,IAAI,KAAK,KAAK,KAAK,cAAc,KAAK;IACpC,KAAK;IACL,OAAO;GACT;EACF;CACF;CAEA,OAAe,OAAuB;EACpC,IAAI,QAAQ;EACZ,KAAK;EACL,SAAS;GACP,MAAM,YAAY,KAAK,KAAK,KAAK;GACjC,IAAI,cAAc,KAAA,GAAW,KAAK,KAAK,wBAAwB;GAC/D,IAAI,cAAc,OAAO,OAAO;GAChC,IAAI,cAAc,MAAM;IACtB,SAAS;IACT;GACF;GACA,MAAM,UAAU,KAAK,KAAK,KAAK;GAC/B,IAAI,YAAY,KAAA,GAAW,KAAK,KAAK,wBAAwB;GAC7D,SACE,YAAY,MACR,OACA,YAAY,MACV,MACA,YAAY,MACV,OACA;EACZ;CACF;CAEA,SAAyB;EACvB,MAAM,QAAQ,KAAK;EACnB,IAAI,KAAK,KAAK,KAAK,cAAc,KAAK,KAAK;EAC3C,OAAO,QAAQ,KAAK,KAAK,KAAK,SAAS,GAAG,KAAK;EAC/C,IAAI,KAAK,KAAK,KAAK,cAAc,KAAK;GACpC,KAAK;GACL,OAAO,QAAQ,KAAK,KAAK,KAAK,SAAS,GAAG,KAAK;EACjD;EACA,MAAM,OAAO,KAAK,KAAK,KAAK;EAC5B,IAAI,SAAS,KAAA,KAAa,gBAAgB,IAAI,GAC5C,KAAK,KAAK,uBAAuB;EAEnC,OAAO,OAAO,KAAK,KAAK,MAAM,OAAO,KAAK,QAAQ,CAAC;CACrD;CAEA,OAAuB;EACrB,MAAM,QAAQ,KAAK;EACnB,OACE,KAAK,WAAW,KAAK,KAAK,UAC1B,gBAAgB,KAAK,KAAK,KAAK,SAAU,GAEzC,KAAK;EAEP,OAAO,KAAK,KAAK,MAAM,OAAO,KAAK,QAAQ;CAC7C;CAEA,YAAoB;EAClB,OACE,KAAK,WAAW,KAAK,KAAK,UAC1B,QAAQ,KAAK,KAAK,KAAK,SAAU,GAEjC,KAAK;CAET;CAEA,KAAa,QAAuB;EAClC,MAAM,IAAI,sBAAsB,MAAM;CACxC;AACF;AAEA,MAAa,2BAA2B,aAAuC;CAC7E,IAAI,SAAS,SAAS,qBACpB,MAAM,IAAI,sBACR,qBAAqB,oBAAoB,YAC3C;CAEF,MAAM,QAA0B,CAAC;CACjC,MAAM,QAAQ,SAAyB;EACrC,IAAI,MAAM,UAAU,oBAClB,MAAM,IAAI,sBACR,oBAAoB,mBAAmB,OACzC;EAEF,MAAM,KAAK,IAAI;CACjB;CACA,IAAI,UAAU;CACd,IAAI,QAAQ;CACZ,OAAO,QAAQ,SAAS,QACtB,IAAI,SAAS,WAAW,QAAQ,KAAK,GAAG;EACtC,WAAW;EACX,SAAS;CACX,OAAO,IAAI,SAAS,WAAW,MAAM,KAAK,GAAG;EAC3C,IAAI,SAAS,KAAK,OAAO;EACzB,UAAU;EACV,MAAM,SAAS,IAAI,iBAAiB,UAAU,KAAK;EACnD,KAAK,OAAO,WAAW,CAAC,CAAC;EACzB,QAAQ,OAAO;CACjB,OAAO;EACL,WAAW,SAAS;EACpB;CACF;CAEF,IAAI,SAAS,KAAK,OAAO;CACzB,OAAO;AACT;AAEA,MAAM,aAAa,UACjB,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU;AAErD,MAAM,YAAY,UAAuC;CACvD,MAAM,SACJ,OAAO,UAAU,WACb,QACA,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,KAC5C,OAAO,KAAK,IACZ;CACR,OAAO,OAAO,SAAS,MAAM,IAAI,SAAS,KAAA;AAC5C;AAEA,MAAM,YAAY;AAElB,MAAM,UAAU,UAAqC;CACnD,IAAI,OAAO,UAAU,UAAU,OAAO,IAAI,KAAK,KAAK;CACpD,IAAI,OAAO,UAAU,UAAU,OAAO,KAAA;CACtC,MAAM,WAAW,UAAU,KAAK,KAAK;CACrC,IAAI,CAAC,UAAU,OAAO,IAAI,KAAK,KAAK;CACpC,MAAM,CAAC,MAAM,OAAO,OAAO,SAAS,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM;CAKvD,MAAM,uBAAO,IAAI,KAAK,CAAC;CACvB,KAAK,YAAY,MAAM,QAAQ,GAAG,GAAG;CACrC,KAAK,SAAS,GAAG,GAAG,GAAG,CAAC;CACxB,OAAO,KAAK,SAAS,MAAM,QAAQ,KAAK,KAAK,QAAQ,MAAM,MACvD,OACA,KAAA;AACN;AAEA,MAAM,UAAU,UAA2B;CACzC,IAAI,UAAU,KAAA,KAAa,UAAU,MAAM,OAAO;CAClD,IAAI,OAAO,UAAU,UAAU,OAAO,OAAO,KAAK;CAClD,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,KAAK;CAClC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,UAAa,OAAuB,KAAa,WAAuB;CAC5E,IAAI,QAAQ,MAAM,IAAI,GAAG;CACzB,IAAI,UAAU,KAAA,GAAW;EACvB,QAAQ,OAAO;EACf,IAAI,MAAM,QAAQ,IAAI,MAAM,MAAM;EAClC,MAAM,IAAI,KAAK,KAAK;CACtB;CACA,OAAO;AACT;AAEA,MAAM,gCAAgB,IAAI,IAA+B;AACzD,MAAM,8BAAc,IAAI,IAAiC;AACzD,MAAM,8BAAc,IAAI,IAA8B;AAEtD,MAAM,oBAAoB,MAAc,YAAkC;CACxE,QAAQ,KAAK,kBAAkB,KAAK,8BAA8B;AAEpE;AAEA,MAAM,gBACJ,MACA,MACA,SACA,aACY;CACZ,IAAI,UAAU,KAAK,QAAQ,GAAG,OAAO;CACrC,MAAM,QAAQ,SAAS,KAAK,QAAQ;CACpC,MAAM,WACJ,KAAK,gBAAgB,KAAA,IAAY,KAAA,IAAY,SAAS,KAAK,WAAW;CACxE,IACE,UAAU,KAAA,KACT,KAAK,gBAAgB,KAAA,MACnB,aAAa,KAAA,KACZ,CAAC,OAAO,UAAU,QAAQ,KAC1B,WAAW,KACX,WAAW,KAEf,OAAO,iBAAiB,MAAM,OAAO;CAEvC,MAAM,UAAoC;EACxC,aAAa,KAAK,gBAAgB;EAClC,GAAI,aAAa,KAAA,IAAY;GAAE,OAAO;GAAY;EAAS,IAAI,CAAC;EAChE,GAAI,aAAa,KAAA,IACb;GAAE,uBAAuB;GAAU,uBAAuB;EAAS,IACnE,CAAC;CACP;CACA,IAAI;EACF,OAAO,OACL,eACA,KAAK,UAAU,OAAO,SAChB,IAAI,KAAK,aAAa,KAAA,GAAW,OAAO,CAChD,CAAC,CAAC,OAAO,KAAK;CAChB,QAAQ;EACN,OAAO,iBAAiB,MAAM,OAAO;CACvC;AACF;AAEA,MAAM,YACJ,MACA,SACA,SACuB;CACvB,IAAI;EACF,OAAO,OACL,aACA,KAAK,UAAU,OAAO,SAChB,IAAI,KAAK,eAAe,KAAA,GAAW,OAAO,CAClD,CAAC,CACE,cAAc,IAAI,CAAC,CACnB,MAAM,SAAS,KAAK,SAAS,IAAI,CAAC,EAAE;CACzC,QAAQ;EACN;CACF;AACF;AAEA,MAAM,OAAO,OAAe,UAC1B,OAAO,KAAK,CAAC,CAAC,SAAS,OAAO,GAAG;AAEnC,MAAM,aACJ,MACA,QACA,UACuB;CACvB,QAAQ,QAAR;EACE,KAAK,KACH,OAAO,UAAU,IACb,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,IAC/B,IAAI,KAAK,YAAY,GAAG,KAAK;EACnC,KAAK,KACH,OAAO,SAAS,IACZ,IAAI,KAAK,SAAS,IAAI,GAAG,KAAK,IAC9B,SACE,MACA,EAAE,OAAO,UAAU,IAAI,UAAU,UAAU,IAAI,SAAS,SAAS,GACjE,OACF;EACN,KAAK,KACH,OAAO,IAAI,KAAK,QAAQ,GAAG,KAAK;EAClC,KAAK,KACH,OAAO,SACL,MACA,EAAE,SAAS,SAAS,IAAI,UAAU,UAAU,IAAI,SAAS,SAAS,GAClE,SACF;EACF,KAAK,KACH,OAAO,IAAI,KAAK,SAAS,IAAI,MAAM,IAAI,KAAK;EAC9C,KAAK,KACH,OAAO,IAAI,KAAK,SAAS,GAAG,KAAK;EACnC,KAAK,KACH,OAAO,IAAI,KAAK,WAAW,GAAG,KAAK;EACrC,KAAK,KACH,OAAO,IAAI,KAAK,WAAW,GAAG,KAAK;EACrC,KAAK,KACH,OACE,SAAS,MAAM;GAAE,MAAM;GAAW,QAAQ;EAAK,GAAG,WAAW,MAC5D,KAAK,SAAS,IAAI,KAAK,OAAO;EAEnC,SACE;CACJ;AACF;AAEA,MAAM,qBAAqB,MAAY,YAAwC;CAC7E,IAAI,SAAS;CACb,IAAI,QAAQ;CACZ,OAAO,QAAQ,QAAQ,QAAQ;EAC7B,MAAM,YAAY,QAAQ;EAC1B,IAAI,cAAc,KAAK;GACrB;GACA,IAAI,QAAQ,WAAW,KAAK;IAC1B,UAAU;IACV;IACA;GACF;GACA,OAAO,QAAQ,QAAQ,QACrB,IAAI,QAAQ,WAAW,KACrB,UAAU,QAAQ;QACb,IAAI,QAAQ,QAAQ,OAAO,KAAK;IACrC,UAAU;IACV,SAAS;GACX,OAAO;IACL;IACA;GACF;EAEJ,OAAO,IAAI,WAAW,KAAK,SAAS,GAAG;GACrC,IAAI,MAAM,QAAQ;GAClB,OAAO,QAAQ,SAAS,WAAW;GACnC,MAAM,QAAQ,UAAU,MAAM,WAAW,MAAM,KAAK;GACpD,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;GAChC,UAAU;GACV,QAAQ;EACV,OAAO;GACL,UAAU;GACV;EACF;CACF;CACA,OAAO;AACT;AAEA,MAAM,kBAAkB,UAA0B;CAChD,IAAI;EACF,OAAO,OAAO,aAAa,UAAU,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,OAAO,KAAK;CAC3E,QAAQ;EACN,OAAO,UAAU,IAAI,QAAQ;CAC/B;AACF;AAOA,MAAM,kBAA2D;CAC/D,eAAe,MAAM,YAAY;EAC/B,MAAM,WAAW,KAAK;EACtB,IAAI,aAAa,KAAA,KAAa,aAAa,MAAM,OAAO;EACxD,IAAI,OAAO,aAAa,UACtB,OAAO,iBAAiB,gBAAgB,OAAO;EAEjD,IAAI,QAAQ,QAAQ,UAAU,IAAI,QAAQ;EAC1C,IAAI,UAAU,KAAA,GAAW;GACvB,IAAI;IACF,QAAQ,wBAAwB,QAAQ;GAC1C,SAAS,OAAO;IACd,IAAI,EAAE,iBAAiB,wBAAwB,MAAM;IACrD,QAAQ,KACN,4CAA4C,MAAM,QAAQ,EAC5D;IACA,QAAQ;GACV;GACA,QAAQ,UAAU,IAAI,UAAU,KAAK;EACvC;EACA,IAAI,UAAU,MAAM,OAAO,KAAA;EAC3B,OAAO,MACJ,KAAK,SACJ,OACE,OAAO,SAAS,YAAY,SAAS,OACjC,QAAQ,QAAQ,IAAI,IACpB,IACN,CACF,CAAC,CACA,KAAK,EAAE;CACZ;CACA,eAAe,MAAM,YAAY,aAAa,gBAAgB,MAAM,OAAO;CAC3E,iBAAiB,MAAM,YACrB,OAAO,KAAK,gBAAgB,WACxB,aAAa,kBAAkB,MAAM,SAAS,KAAK,WAAW,IAC9D,iBAAiB,kBAAkB,OAAO;CAChD,aAAa,MAAM,YAAY;EAC7B,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAK,GAAG,OAAO;EAC7B,MAAM,SAAS,KAAK;EACpB,MAAM,OAAO,OAAO,KAAK;EACzB,IAAI,OAAO,WAAW,YAAY,CAAC,QAAQ,OAAO,MAAM,KAAK,QAAQ,CAAC,GACpE,OAAO,iBAAiB,cAAc,OAAO;EAE/C,MAAM,YAAY,kBAAkB,MAAM,MAAM;EAChD,IAAI,cAAc,KAAA,GAAW,OAAO;EACpC,QAAQ,KACN,4DAA4D,OAAO,+BACrE;EACA,OAAO,KAAK,YAAY;CAC1B;CACA,YAAY,MAAM,YAAY;EAC5B,IAAI,UAAU,KAAK,QAAQ,GAAG,OAAO;EACrC,MAAM,QAAQ,SAAS,KAAK,QAAQ;EACpC,IAAI,UAAU,KAAA,KAAa,OAAO,KAAK,aAAa,UAClD,OAAO,iBAAiB,aAAa,OAAO;EAE9C,MAAM,WAAW,KAAK,eAAe,KAAK;EAC1C,OAAO,OAAO,aAAa,WAAW,WAAW,KAAK;CACxD;CACA,MAAM,MAAM,YACV,MAAM,QAAQ,KAAK,SAAS,IACxB,KAAK,SAAS,CAAC,MAAM,OAAO,IAC5B,iBAAiB,OAAO,OAAO;CACrC,KAAK,MAAM,YACT,MAAM,QAAQ,KAAK,SAAS,IACxB,KAAK,SAAS,CAAC,KAAK,OAAO,IAC3B,iBAAiB,MAAM,OAAO;CACpC,MAAM,SAAS,CAAC,KAAK;AACvB;AAEA,MAAa,6BACX,MACA,MACA,YACY;CACZ,IAAI,CAAC,OAAO,OAAO,iBAAiB,IAAI,GAAG;EACzC,QAAQ,KAAK,kBAAkB,KAAK,oCAAoC;EACxE;CACF;CACA,OAAO,gBAAgB,KAAK,CAAE,MAAM,OAAO;AAC7C"}
|
package/dist/actionRegistry.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type ActionDispatchContext = {
|
|
|
15
15
|
*/
|
|
16
16
|
export type ActionHandler = (ctx: ActionDispatchContext) => unknown | Promise<unknown>;
|
|
17
17
|
/**
|
|
18
|
-
* The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. When a registry is used in a `JSONGenerativeUI` tool, each dispatched payload, including `$input`, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and interaction log limits, and never delays or prevents dispatch. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.
|
|
18
|
+
* The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`, or a `CheckboxGroup`'s array of checked values), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. When a registry is used in a `JSONGenerativeUI` tool, each dispatched payload, including `$input`, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and interaction log limits, and never delays or prevents dispatch. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.
|
|
19
19
|
*/
|
|
20
20
|
export type ActionRegistry = {
|
|
21
21
|
dispatch(action: Action): unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actionRegistry.js","names":[],"sources":["../src/actionRegistry.ts"],"sourcesContent":["import type { Action } from \"./ir\";\n\n/** Context handed to an {@link ActionHandler} when an `$action` fires. */\nexport type ActionDispatchContext = {\n /**\n * The action payload. For fire-and-forget actions this is the `$action` as the model emitted it. For an interactive component the user's runtime input is merged in under the reserved `$input` key: a single value for a standalone control's dispatch, or an object keyed by each field's `name` for a `Form` or a `Card` with `asForm` set. A model-supplied `value` field is never clobbered.\n */\n readonly payload: Action;\n};\n\n/**\n * Resolves a single `$action.type`. Fire-and-forget actions return `void` or a\n * promise that resolves to it. For a `prompt_user` tool whose call has no\n * result yet, a non-`undefined` return value becomes the tool result. Whether\n * that resumes the run depends on the runtime, including how it treats human\n * tools. The return value is handed back to `dispatch`'s caller as `unknown`.\n */\nexport type ActionHandler = (\n ctx: ActionDispatchContext,\n) => unknown | Promise<unknown>;\n\n/**\n * The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. When a registry is used in a `JSONGenerativeUI` tool, each dispatched payload, including `$input`, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and interaction log limits, and never delays or prevents dispatch. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.\n */\nexport type ActionRegistry = {\n dispatch(action: Action): unknown;\n has(type: string): boolean;\n};\n\nexport function createActionRegistry(\n handlers: Readonly<Record<string, ActionHandler>>,\n): ActionRegistry {\n const map = new Map(Object.entries(handlers));\n return {\n dispatch(action) {\n if (typeof action?.type !== \"string\") {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[@assistant-ui/react-generative-ui] Skipping malformed action; \" +\n \"`$action.type` must be a string. \" +\n `Received ${formatReceivedType(action?.type)}. Update the emitted ` +\n \"`$action` payload.\",\n );\n }\n return undefined;\n }\n\n const handler = map.get(action.type);\n if (!handler) {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n const actionTypes = [...map.keys()];\n // eslint-disable-next-line no-console\n console.warn(\n `[@assistant-ui/react-generative-ui] Action \"${action.type}\" has ` +\n `no registered handler. ${formatRegisteredActions(actionTypes)} ` +\n \"Register it with createActionRegistry(...) or update the emitted \" +\n \"`$action.type`.\",\n );\n }\n return undefined;\n }\n return handler({ payload: action });\n },\n has(type) {\n return map.has(type);\n },\n };\n}\n\n/** A no-op registry used when no handlers are provided. Dispatch resolves to\n * `undefined` and logs a warning in dev for unknown types, so a model-emitted\n * action with no handler degrades to \"does nothing\" rather than throwing. */\nexport const emptyActionRegistry: ActionRegistry = createActionRegistry({});\n\nfunction formatRegisteredActions(actionTypes: string[]) {\n if (actionTypes.length === 0) return \"No actions are registered.\";\n\n return `Registered actions: ${actionTypes\n .map((type) => `\"${type}\"`)\n .join(\", \")}.`;\n}\n\nfunction formatReceivedType(value: unknown) {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n return typeof value;\n}\n"],"mappings":";AA6BA,SAAgB,qBACd,UACgB;CAChB,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ,QAAQ,CAAC;CAC5C,OAAO;EACL,SAAS,QAAQ;GACf,IAAI,OAAO,QAAQ,SAAS,UAAU;IACpC,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,KACN,+GAEc,mBAAmB,QAAQ,IAAI,EAAE,2CAEjD;IAEF;GACF;GAEA,MAAM,UAAU,IAAI,IAAI,OAAO,IAAI;GACnC,IAAI,CAAC,SAAS;IACZ,IAAI,QAAQ,IAAI,gBAAgB,cAAc;KAC5C,MAAM,cAAc,CAAC,GAAG,IAAI,KAAK,CAAC;KAElC,QAAQ,KACN,+CAA+C,OAAO,KAAK,+BAC/B,wBAAwB,WAAW,EAAE,qFAGnE;IACF;IACA;GACF;GACA,OAAO,QAAQ,EAAE,SAAS,OAAO,CAAC;EACpC;EACA,IAAI,MAAM;GACR,OAAO,IAAI,IAAI,IAAI;EACrB;CACF;AACF;;;;AAKA,MAAa,sBAAsC,qBAAqB,CAAC,CAAC;AAE1E,SAAS,wBAAwB,aAAuB;CACtD,IAAI,YAAY,WAAW,GAAG,OAAO;CAErC,OAAO,uBAAuB,YAC3B,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAC1B,KAAK,IAAI,EAAE;AAChB;AAEA,SAAS,mBAAmB,OAAgB;CAC1C,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,OAAO,OAAO;AAChB"}
|
|
1
|
+
{"version":3,"file":"actionRegistry.js","names":[],"sources":["../src/actionRegistry.ts"],"sourcesContent":["import type { Action } from \"./ir\";\n\n/** Context handed to an {@link ActionHandler} when an `$action` fires. */\nexport type ActionDispatchContext = {\n /**\n * The action payload. For fire-and-forget actions this is the `$action` as the model emitted it. For an interactive component the user's runtime input is merged in under the reserved `$input` key: a single value for a standalone control's dispatch, or an object keyed by each field's `name` for a `Form` or a `Card` with `asForm` set. A model-supplied `value` field is never clobbered.\n */\n readonly payload: Action;\n};\n\n/**\n * Resolves a single `$action.type`. Fire-and-forget actions return `void` or a\n * promise that resolves to it. For a `prompt_user` tool whose call has no\n * result yet, a non-`undefined` return value becomes the tool result. Whether\n * that resumes the run depends on the runtime, including how it treats human\n * tools. The return value is handed back to `dispatch`'s caller as `unknown`.\n */\nexport type ActionHandler = (\n ctx: ActionDispatchContext,\n) => unknown | Promise<unknown>;\n\n/**\n * The host-provided map from `$action.type` to {@link ActionHandler}. This is the single dispatch target for rendered generative UI: every interactive component fires its `$action` here through the injected `$dispatch` (a submit Button defers to its ancestor form's dispatch instead). The payload's `$input` takes one of two shapes: a single value for a standalone control's dispatch (`Select`, `Input`, `DatePicker`, `Checkbox`, `RadioGroup`, or a `CheckboxGroup`'s array of checked values), or an object keyed by field `name` for a `Form` or a `Card` with `asForm` set. When a registry is used in a `JSONGenerativeUI` tool, each dispatched payload, including `$input`, is recorded on the tool call on a best-effort basis. Recording depends on the runtime and interaction log limits, and never delays or prevents dispatch. Construct it with {@link createActionRegistry} and pass it to `JSONGenerativeUI`.\n */\nexport type ActionRegistry = {\n dispatch(action: Action): unknown;\n has(type: string): boolean;\n};\n\nexport function createActionRegistry(\n handlers: Readonly<Record<string, ActionHandler>>,\n): ActionRegistry {\n const map = new Map(Object.entries(handlers));\n return {\n dispatch(action) {\n if (typeof action?.type !== \"string\") {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[@assistant-ui/react-generative-ui] Skipping malformed action; \" +\n \"`$action.type` must be a string. \" +\n `Received ${formatReceivedType(action?.type)}. Update the emitted ` +\n \"`$action` payload.\",\n );\n }\n return undefined;\n }\n\n const handler = map.get(action.type);\n if (!handler) {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n const actionTypes = [...map.keys()];\n // eslint-disable-next-line no-console\n console.warn(\n `[@assistant-ui/react-generative-ui] Action \"${action.type}\" has ` +\n `no registered handler. ${formatRegisteredActions(actionTypes)} ` +\n \"Register it with createActionRegistry(...) or update the emitted \" +\n \"`$action.type`.\",\n );\n }\n return undefined;\n }\n return handler({ payload: action });\n },\n has(type) {\n return map.has(type);\n },\n };\n}\n\n/** A no-op registry used when no handlers are provided. Dispatch resolves to\n * `undefined` and logs a warning in dev for unknown types, so a model-emitted\n * action with no handler degrades to \"does nothing\" rather than throwing. */\nexport const emptyActionRegistry: ActionRegistry = createActionRegistry({});\n\nfunction formatRegisteredActions(actionTypes: string[]) {\n if (actionTypes.length === 0) return \"No actions are registered.\";\n\n return `Registered actions: ${actionTypes\n .map((type) => `\"${type}\"`)\n .join(\", \")}.`;\n}\n\nfunction formatReceivedType(value: unknown) {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"array\";\n return typeof value;\n}\n"],"mappings":";AA6BA,SAAgB,qBACd,UACgB;CAChB,MAAM,MAAM,IAAI,IAAI,OAAO,QAAQ,QAAQ,CAAC;CAC5C,OAAO;EACL,SAAS,QAAQ;GACf,IAAI,OAAO,QAAQ,SAAS,UAAU;IACpC,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,KACN,+GAEc,mBAAmB,QAAQ,IAAI,EAAE,2CAEjD;IAEF;GACF;GAEA,MAAM,UAAU,IAAI,IAAI,OAAO,IAAI;GACnC,IAAI,CAAC,SAAS;IACZ,IAAI,QAAQ,IAAI,gBAAgB,cAAc;KAC5C,MAAM,cAAc,CAAC,GAAG,IAAI,KAAK,CAAC;KAElC,QAAQ,KACN,+CAA+C,OAAO,KAAK,+BAC/B,wBAAwB,WAAW,EAAE,qFAGnE;IACF;IACA;GACF;GACA,OAAO,QAAQ,EAAE,SAAS,OAAO,CAAC;EACpC;EACA,IAAI,MAAM;GACR,OAAO,IAAI,IAAI,IAAI;EACrB;CACF;AACF;;;;AAKA,MAAa,sBAAsC,qBAAqB,CAAC,CAAC;AAE1E,SAAS,wBAAwB,aAAuB;CACtD,IAAI,YAAY,WAAW,GAAG,OAAO;CAErC,OAAO,uBAAuB,YAC3B,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAC1B,KAAK,IAAI,EAAE;AAChB;AAEA,SAAS,mBAAmB,OAAgB;CAC1C,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,MAAM,QAAQ,KAAK,GAAG,OAAO;CACjC,OAAO,OAAO;AAChB"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type AnsweredValues = Readonly<Record<string, unknown>>;
|
|
3
|
+
type ToolInteractionLog = {
|
|
4
|
+
readonly entries: readonly {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly payload: unknown;
|
|
7
|
+
}[];
|
|
8
|
+
};
|
|
9
|
+
export declare const findAnsweredValues: (interactions: ToolInteractionLog | undefined) => AnsweredValues | undefined;
|
|
10
|
+
export declare const AnsweredValuesProvider: ({ values, children, }: {
|
|
11
|
+
values: AnsweredValues | undefined;
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}) => string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
|
|
14
|
+
export declare const useAnsweredValue: (name: string | undefined) => unknown;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=answeredValues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answeredValues.d.ts","sourceRoot":"","sources":["../src/answeredValues.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,KAAK,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAExD,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,OAAO,EAAE,SAAS;QACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;KAC3B,EAAE,CAAC;CACL,CAAC;AAuBF,eAAO,MAAM,kBAAkB,GAC7B,cAAc,kBAAkB,GAAG,SAAS,KAC3C,cAAc,GAAG,SAanB,CAAC;AAEF,eAAO,MAAM,sBAAsB,GAAI,uBAGpC;IACD,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B,oSAOA,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,GAAG,SAAS,KAAG,OAK3D,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
//#region src/answeredValues.tsx
|
|
4
|
+
let answeredValuesContext;
|
|
5
|
+
const supportsContext = typeof React.createContext === "function";
|
|
6
|
+
const useValuesContext = supportsContext ? React.useContext : () => void 0;
|
|
7
|
+
const getAnsweredValuesContext = () => supportsContext ? answeredValuesContext ??= React.createContext(void 0) : void 0;
|
|
8
|
+
const isPlainObject = (value) => {
|
|
9
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
10
|
+
const prototype = Object.getPrototypeOf(value);
|
|
11
|
+
return prototype === Object.prototype || prototype === null;
|
|
12
|
+
};
|
|
13
|
+
const findAnsweredValues = (interactions) => {
|
|
14
|
+
const entries = interactions?.entries;
|
|
15
|
+
if (!entries) return void 0;
|
|
16
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
17
|
+
const interaction = entries[index];
|
|
18
|
+
if (interaction.type !== "action" || !isPlainObject(interaction.payload)) continue;
|
|
19
|
+
const input = interaction.payload["$input"];
|
|
20
|
+
if (isPlainObject(input)) return input;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const AnsweredValuesProvider = ({ values, children }) => {
|
|
24
|
+
const Context = getAnsweredValuesContext();
|
|
25
|
+
return Context ? /* @__PURE__ */ jsx(Context.Provider, {
|
|
26
|
+
value: values,
|
|
27
|
+
children
|
|
28
|
+
}) : children;
|
|
29
|
+
};
|
|
30
|
+
const useAnsweredValue = (name) => {
|
|
31
|
+
const values = useValuesContext(getAnsweredValuesContext());
|
|
32
|
+
return name !== void 0 && values && Object.hasOwn(values, name) ? values[name] : void 0;
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { AnsweredValuesProvider, findAnsweredValues, useAnsweredValue };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=answeredValues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answeredValues.js","names":[],"sources":["../src/answeredValues.tsx"],"sourcesContent":["import * as React from \"react\";\n\ntype AnsweredValues = Readonly<Record<string, unknown>>;\n\ntype ToolInteractionLog = {\n readonly entries: readonly {\n readonly type: string;\n readonly payload: unknown;\n }[];\n};\n\nlet answeredValuesContext:\n | React.Context<AnsweredValues | undefined>\n | undefined;\nconst supportsContext = typeof React.createContext === \"function\";\nconst useValuesContext = supportsContext ? React.useContext : () => undefined;\n\nconst getAnsweredValuesContext = () =>\n supportsContext\n ? (answeredValuesContext ??= React.createContext<\n AnsweredValues | undefined\n >(undefined))\n : undefined;\n\nconst isPlainObject = (value: unknown): value is AnsweredValues => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n return prototype === Object.prototype || prototype === null;\n};\n\nexport const findAnsweredValues = (\n interactions: ToolInteractionLog | undefined,\n): AnsweredValues | undefined => {\n const entries = interactions?.entries;\n if (!entries) return undefined;\n\n for (let index = entries.length - 1; index >= 0; index--) {\n const interaction = entries[index]!;\n if (interaction.type !== \"action\" || !isPlainObject(interaction.payload)) {\n continue;\n }\n const input = interaction.payload[\"$input\"];\n if (isPlainObject(input)) return input;\n }\n return undefined;\n};\n\nexport const AnsweredValuesProvider = ({\n values,\n children,\n}: {\n values: AnsweredValues | undefined;\n children: React.ReactNode;\n}) => {\n const Context = getAnsweredValuesContext();\n return Context ? (\n <Context.Provider value={values}>{children}</Context.Provider>\n ) : (\n children\n );\n};\n\nexport const useAnsweredValue = (name: string | undefined): unknown => {\n const values = useValuesContext(getAnsweredValuesContext()!);\n return name !== undefined && values && Object.hasOwn(values, name)\n ? values[name]\n : undefined;\n};\n"],"mappings":";;;AAWA,IAAI;AAGJ,MAAM,kBAAkB,OAAO,MAAM,kBAAkB;AACvD,MAAM,mBAAmB,kBAAkB,MAAM,mBAAmB,KAAA;AAEpE,MAAM,iCACJ,kBACK,0BAA0B,MAAM,cAE/B,KAAA,CAAS,IACX,KAAA;AAEN,MAAM,iBAAiB,UAA4C;CACjE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO;CAET,MAAM,YAAY,OAAO,eAAe,KAAK;CAC7C,OAAO,cAAc,OAAO,aAAa,cAAc;AACzD;AAEA,MAAa,sBACX,iBAC+B;CAC/B,MAAM,UAAU,cAAc;CAC9B,IAAI,CAAC,SAAS,OAAO,KAAA;CAErB,KAAK,IAAI,QAAQ,QAAQ,SAAS,GAAG,SAAS,GAAG,SAAS;EACxD,MAAM,cAAc,QAAQ;EAC5B,IAAI,YAAY,SAAS,YAAY,CAAC,cAAc,YAAY,OAAO,GACrE;EAEF,MAAM,QAAQ,YAAY,QAAQ;EAClC,IAAI,cAAc,KAAK,GAAG,OAAO;CACnC;AAEF;AAEA,MAAa,0BAA0B,EACrC,QACA,eAII;CACJ,MAAM,UAAU,yBAAyB;CACzC,OAAO,UACL,oBAAC,QAAQ,UAAT;EAAkB,OAAO;EAAS;CAA2B,CAAA,IAE7D;AAEJ;AAEA,MAAa,oBAAoB,SAAsC;CACrE,MAAM,SAAS,iBAAiB,yBAAyB,CAAE;CAC3D,OAAO,SAAS,KAAA,KAAa,UAAU,OAAO,OAAO,QAAQ,IAAI,IAC7D,OAAO,QACP,KAAA;AACN"}
|
package/dist/constants.d.ts
CHANGED
|
@@ -9,4 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const TYPE_KEY = "$type";
|
|
11
11
|
export declare const GENERATED_NAME_ATTR = "data-aui-generated-name";
|
|
12
|
+
export declare const FIELD_NAME_ATTR = "data-aui-field-name";
|
|
13
|
+
export declare const FIELD_VALUE_ATTR = "data-aui-field-value";
|
|
14
|
+
export declare const CHECKBOX_GROUP_ATTR = "data-aui-checkbox-group";
|
|
12
15
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC,eAAO,MAAM,mBAAmB,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAE7D,eAAO,MAAM,eAAe,wBAAwB,CAAC;AACrD,eAAO,MAAM,gBAAgB,yBAAyB,CAAC;AAEvD,eAAO,MAAM,mBAAmB,4BAA4B,CAAC"}
|
package/dist/constants.js
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
const TYPE_KEY = "$type";
|
|
12
12
|
const GENERATED_NAME_ATTR = "data-aui-generated-name";
|
|
13
|
+
const FIELD_NAME_ATTR = "data-aui-field-name";
|
|
14
|
+
const FIELD_VALUE_ATTR = "data-aui-field-value";
|
|
15
|
+
const CHECKBOX_GROUP_ATTR = "data-aui-checkbox-group";
|
|
13
16
|
//#endregion
|
|
14
|
-
export { GENERATED_NAME_ATTR, TYPE_KEY };
|
|
17
|
+
export { CHECKBOX_GROUP_ATTR, FIELD_NAME_ATTR, FIELD_VALUE_ATTR, GENERATED_NAME_ATTR, TYPE_KEY };
|
|
15
18
|
|
|
16
19
|
//# sourceMappingURL=constants.js.map
|
package/dist/constants.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["/**\n * The wire key naming the component to render.\n *\n * The model emits nodes as flat objects `{ [TYPE_KEY]: name, ...props }`. We\n * use `$type` rather than `type` so a component's own `type` prop (e.g. a\n * button's `type=\"submit\"`) flows through as an ordinary prop without\n * colliding with the discriminator. It mirrors the `$type` discriminator\n * convention used by JSON polymorphism elsewhere.\n */\nexport const TYPE_KEY = \"$type\";\n\nexport const GENERATED_NAME_ATTR = \"data-aui-generated-name\";\n"],"mappings":";;;;;;;;;;AASA,MAAa,WAAW;AAExB,MAAa,sBAAsB"}
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["/**\n * The wire key naming the component to render.\n *\n * The model emits nodes as flat objects `{ [TYPE_KEY]: name, ...props }`. We\n * use `$type` rather than `type` so a component's own `type` prop (e.g. a\n * button's `type=\"submit\"`) flows through as an ordinary prop without\n * colliding with the discriminator. It mirrors the `$type` discriminator\n * convention used by JSON polymorphism elsewhere.\n */\nexport const TYPE_KEY = \"$type\";\n\nexport const GENERATED_NAME_ATTR = \"data-aui-generated-name\";\n\nexport const FIELD_NAME_ATTR = \"data-aui-field-name\";\nexport const FIELD_VALUE_ATTR = \"data-aui-field-value\";\n\nexport const CHECKBOX_GROUP_ATTR = \"data-aui-checkbox-group\";\n"],"mappings":";;;;;;;;;;AASA,MAAa,WAAW;AAExB,MAAa,sBAAsB;AAEnC,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAEhC,MAAa,sBAAsB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `value` contains a `{ "$field": name }` or `{ "$field": name, "fallback": value }` reference within 64 levels.
|
|
3
|
+
*/
|
|
4
|
+
export declare const hasFieldReference: (value: unknown) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Replaces each `{ "$field": name }` reference inside `value` with the own property `name` of `fields`, or with the reference's own `fallback` when that is `undefined`. A reference that still resolves to `undefined` is dropped from its object or array, and a cyclic reference or anything nested deeper than 64 levels is kept as is.
|
|
7
|
+
*/
|
|
8
|
+
export declare const resolveFieldReferences: (value: unknown, fields: Readonly<Record<string, unknown>>) => unknown;
|
|
9
|
+
//# sourceMappingURL=fieldReferences.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldReferences.d.ts","sourceRoot":"","sources":["../src/fieldReferences.ts"],"names":[],"mappings":"AAYA;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,OAAO,OAAO,KAAG,OAgBlD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,OAAO,EACd,QAAQ,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,KACxC,OA2CF,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/fieldReferences.ts
|
|
2
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
const MAX_DEPTH = 64;
|
|
4
|
+
const isFieldReference = (value) => isRecord(value) && typeof value["$field"] === "string" && Object.keys(value).every((key) => key === "$field" || key === "fallback");
|
|
5
|
+
/**
|
|
6
|
+
* Whether `value` contains a `{ "$field": name }` or `{ "$field": name, "fallback": value }` reference within 64 levels.
|
|
7
|
+
*/
|
|
8
|
+
const hasFieldReference = (value) => {
|
|
9
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10
|
+
const visit = (entry, depth) => {
|
|
11
|
+
if (typeof entry !== "object" || entry === null || depth > MAX_DEPTH || seen.has(entry)) return false;
|
|
12
|
+
if (isFieldReference(entry)) return true;
|
|
13
|
+
seen.add(entry);
|
|
14
|
+
return Object.values(entry).some((item) => visit(item, depth + 1));
|
|
15
|
+
};
|
|
16
|
+
return visit(value, 0);
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Replaces each `{ "$field": name }` reference inside `value` with the own property `name` of `fields`, or with the reference's own `fallback` when that is `undefined`. A reference that still resolves to `undefined` is dropped from its object or array, and a cyclic reference or anything nested deeper than 64 levels is kept as is.
|
|
20
|
+
*/
|
|
21
|
+
const resolveFieldReferences = (value, fields) => {
|
|
22
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
23
|
+
const resolve = (entry, depth) => {
|
|
24
|
+
if (isFieldReference(entry)) {
|
|
25
|
+
const current = Object.hasOwn(fields, entry.$field) ? fields[entry.$field] : void 0;
|
|
26
|
+
if (current !== void 0) return current;
|
|
27
|
+
return Object.hasOwn(entry, "fallback") ? entry.fallback : void 0;
|
|
28
|
+
}
|
|
29
|
+
if (typeof entry !== "object" || entry === null || depth > MAX_DEPTH || ancestors.has(entry)) return entry;
|
|
30
|
+
ancestors.add(entry);
|
|
31
|
+
try {
|
|
32
|
+
if (Array.isArray(entry)) return entry.map((item) => resolve(item, depth + 1)).filter((item) => item !== void 0);
|
|
33
|
+
const result = {};
|
|
34
|
+
for (const [key, item] of Object.entries(entry)) {
|
|
35
|
+
const resolved = resolve(item, depth + 1);
|
|
36
|
+
if (resolved !== void 0) Object.defineProperty(result, key, {
|
|
37
|
+
value: resolved,
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
} finally {
|
|
45
|
+
ancestors.delete(entry);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return resolve(value, 0);
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { hasFieldReference, resolveFieldReferences };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=fieldReferences.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldReferences.js","names":[],"sources":["../src/fieldReferences.ts"],"sourcesContent":["const isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst MAX_DEPTH = 64;\n\nconst isFieldReference = (\n value: unknown,\n): value is { readonly $field: string; readonly fallback?: unknown } =>\n isRecord(value) &&\n typeof value[\"$field\"] === \"string\" &&\n Object.keys(value).every((key) => key === \"$field\" || key === \"fallback\");\n\n/**\n * Whether `value` contains a `{ \"$field\": name }` or `{ \"$field\": name, \"fallback\": value }` reference within 64 levels.\n */\nexport const hasFieldReference = (value: unknown): boolean => {\n const seen = new Set<object>();\n const visit = (entry: unknown, depth: number): boolean => {\n if (\n typeof entry !== \"object\" ||\n entry === null ||\n depth > MAX_DEPTH ||\n seen.has(entry)\n ) {\n return false;\n }\n if (isFieldReference(entry)) return true;\n seen.add(entry);\n return Object.values(entry).some((item) => visit(item, depth + 1));\n };\n return visit(value, 0);\n};\n\n/**\n * Replaces each `{ \"$field\": name }` reference inside `value` with the own property `name` of `fields`, or with the reference's own `fallback` when that is `undefined`. A reference that still resolves to `undefined` is dropped from its object or array, and a cyclic reference or anything nested deeper than 64 levels is kept as is.\n */\nexport const resolveFieldReferences = (\n value: unknown,\n fields: Readonly<Record<string, unknown>>,\n): unknown => {\n const ancestors = new Set<object>();\n const resolve = (entry: unknown, depth: number): unknown => {\n if (isFieldReference(entry)) {\n const current = Object.hasOwn(fields, entry.$field)\n ? fields[entry.$field]\n : undefined;\n if (current !== undefined) return current;\n return Object.hasOwn(entry, \"fallback\") ? entry.fallback : undefined;\n }\n if (\n typeof entry !== \"object\" ||\n entry === null ||\n depth > MAX_DEPTH ||\n ancestors.has(entry)\n ) {\n return entry;\n }\n ancestors.add(entry);\n try {\n if (Array.isArray(entry)) {\n return entry\n .map((item) => resolve(item, depth + 1))\n .filter((item) => item !== undefined);\n }\n const result: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(entry)) {\n const resolved = resolve(item, depth + 1);\n if (resolved !== undefined) {\n Object.defineProperty(result, key, {\n value: resolved,\n enumerable: true,\n configurable: true,\n writable: true,\n });\n }\n }\n return result;\n } finally {\n ancestors.delete(entry);\n }\n };\n return resolve(value, 0);\n};\n"],"mappings":";AAAA,MAAM,YAAY,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAErE,MAAM,YAAY;AAElB,MAAM,oBACJ,UAEA,SAAS,KAAK,KACd,OAAO,MAAM,cAAc,YAC3B,OAAO,KAAK,KAAK,CAAC,CAAC,OAAO,QAAQ,QAAQ,YAAY,QAAQ,UAAU;;;;AAK1E,MAAa,qBAAqB,UAA4B;CAC5D,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAS,OAAgB,UAA2B;EACxD,IACE,OAAO,UAAU,YACjB,UAAU,QACV,QAAQ,aACR,KAAK,IAAI,KAAK,GAEd,OAAO;EAET,IAAI,iBAAiB,KAAK,GAAG,OAAO;EACpC,KAAK,IAAI,KAAK;EACd,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,MAAM,SAAS,MAAM,MAAM,QAAQ,CAAC,CAAC;CACnE;CACA,OAAO,MAAM,OAAO,CAAC;AACvB;;;;AAKA,MAAa,0BACX,OACA,WACY;CACZ,MAAM,4BAAY,IAAI,IAAY;CAClC,MAAM,WAAW,OAAgB,UAA2B;EAC1D,IAAI,iBAAiB,KAAK,GAAG;GAC3B,MAAM,UAAU,OAAO,OAAO,QAAQ,MAAM,MAAM,IAC9C,OAAO,MAAM,UACb,KAAA;GACJ,IAAI,YAAY,KAAA,GAAW,OAAO;GAClC,OAAO,OAAO,OAAO,OAAO,UAAU,IAAI,MAAM,WAAW,KAAA;EAC7D;EACA,IACE,OAAO,UAAU,YACjB,UAAU,QACV,QAAQ,aACR,UAAU,IAAI,KAAK,GAEnB,OAAO;EAET,UAAU,IAAI,KAAK;EACnB,IAAI;GACF,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MACJ,KAAK,SAAS,QAAQ,MAAM,QAAQ,CAAC,CAAC,CAAC,CACvC,QAAQ,SAAS,SAAS,KAAA,CAAS;GAExC,MAAM,SAAkC,CAAC;GACzC,KAAK,MAAM,CAAC,KAAK,SAAS,OAAO,QAAQ,KAAK,GAAG;IAC/C,MAAM,WAAW,QAAQ,MAAM,QAAQ,CAAC;IACxC,IAAI,aAAa,KAAA,GACf,OAAO,eAAe,QAAQ,KAAK;KACjC,OAAO;KACP,YAAY;KACZ,cAAc;KACd,UAAU;IACZ,CAAC;GAEL;GACA,OAAO;EACT,UAAU;GACR,UAAU,OAAO,KAAK;EACxB;CACF;CACA,OAAO,QAAQ,OAAO,CAAC;AACzB"}
|
|
@@ -32,7 +32,7 @@ function toJSX(node, depth, escape, pretty, indent) {
|
|
|
32
32
|
if (typeof node !== "object") return "";
|
|
33
33
|
const { [TYPE_KEY]: type, $key, children, ...props } = node;
|
|
34
34
|
if (typeof type !== "string") return "";
|
|
35
|
-
const attrs = formatAttr("key", $key) + Object.entries(props).map(([key, value]) => formatAttr(key, value)).join("");
|
|
35
|
+
const attrs = formatAttr("key", $key, escape) + Object.entries(props).map(([key, value]) => formatAttr(key, value, escape)).join("");
|
|
36
36
|
if (!pretty) {
|
|
37
37
|
const inner = children === void 0 ? "" : toJSX(children, depth + 1, escape, false, 0);
|
|
38
38
|
return inner === "" ? `<${type}${attrs} />` : `<${type}${attrs}>${inner}</${type}>`;
|
|
@@ -64,10 +64,10 @@ function formatChildText(value, escape) {
|
|
|
64
64
|
return escape && UNSAFE_CHILD_CHARS.test(value) ? `{${JSON.stringify(value)}}` : value;
|
|
65
65
|
}
|
|
66
66
|
/** Formats one prop as a JSX attribute (`id="x"`, `count={3}`, `open`, …). */
|
|
67
|
-
function formatAttr(key, value) {
|
|
67
|
+
function formatAttr(key, value, escape) {
|
|
68
68
|
if (value === void 0) return "";
|
|
69
69
|
if (value === true) return ` ${key}`;
|
|
70
|
-
if (typeof value === "string") return /["\n]/.test(value) ? ` ${key}={${JSON.stringify(value)}}` : ` ${key}="${value}"`;
|
|
70
|
+
if (typeof value === "string") return /["\n]/.test(value) || escape && value.includes("&") ? ` ${key}={${JSON.stringify(value)}}` : ` ${key}="${value}"`;
|
|
71
71
|
return ` ${key}={${JSON.stringify(value)}}`;
|
|
72
72
|
}
|
|
73
73
|
//#endregion
|