@mcp-native/react-native 0.1.0 → 0.3.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/README.md +72 -14
- package/dist/index.d.ts +45 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +365 -14
- package/dist/index.js.map +1 -1
- package/dist/iso-4217.d.ts +8 -0
- package/dist/iso-4217.d.ts.map +1 -0
- package/dist/iso-4217.js +187 -0
- package/dist/iso-4217.js.map +1 -0
- package/dist/v1.d.ts +36 -0
- package/dist/v1.d.ts.map +1 -0
- package/dist/v1.js +651 -0
- package/dist/v1.js.map +1 -0
- package/package.json +3 -3
package/dist/v1.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
|
|
2
|
+
import { JSON_MAX_STRING_LENGTH, JSON_MAX_VALUES, parseJsonObject, parseJsonValue, } from "@mcp-native/core";
|
|
3
|
+
import { ISO_4217_CURRENCY_CODES } from "./iso-4217.js";
|
|
4
|
+
export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
|
|
5
|
+
"Button",
|
|
6
|
+
"Card",
|
|
7
|
+
"Column",
|
|
8
|
+
"List",
|
|
9
|
+
"Row",
|
|
10
|
+
"Text",
|
|
11
|
+
"TextField",
|
|
12
|
+
]);
|
|
13
|
+
/** Maximum expanded native-plan nodes, including repeated component references. */
|
|
14
|
+
export const A2UI_V1_NATIVE_MAX_RENDER_NODES = A2UI_V1_MAX_COMPONENTS;
|
|
15
|
+
/**
|
|
16
|
+
* Converts a policy-validated A2UI v1 surface into the existing host-owned
|
|
17
|
+
* native render plan. Unsupported renderer semantics fail closed.
|
|
18
|
+
*/
|
|
19
|
+
export function createA2uiV1NativeRenderPlan(surface, policy, options = {}) {
|
|
20
|
+
const parsedOptions = parseRenderPlanOptions(options);
|
|
21
|
+
const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale);
|
|
22
|
+
return adaptComponent("root", "root", context, undefined);
|
|
23
|
+
}
|
|
24
|
+
/** Resolves one validated event against the latest renderer-local data model. */
|
|
25
|
+
export function resolveA2uiV1NativeEvent(surface, policy, sourceComponentId, dataModel, options = {}) {
|
|
26
|
+
if (typeof sourceComponentId !== "string" || sourceComponentId.length === 0) {
|
|
27
|
+
throw new A2uiParseError("Expected a non-empty A2UI source component id");
|
|
28
|
+
}
|
|
29
|
+
const parsedOptions = parseEventResolutionOptions(options);
|
|
30
|
+
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
|
|
31
|
+
const events = findNativeEvents(adaptComponent("root", "root", context, undefined), sourceComponentId, parsedOptions.instanceKey);
|
|
32
|
+
if (events.length === 0) {
|
|
33
|
+
throw new A2uiParseError(`A2UI native event source ${JSON.stringify(sourceComponentId)} is not a reachable supported Button`);
|
|
34
|
+
}
|
|
35
|
+
if (events.length > 1) {
|
|
36
|
+
throw new A2uiParseError(`A2UI native event source ${JSON.stringify(sourceComponentId)} is ambiguous without its template instance key`);
|
|
37
|
+
}
|
|
38
|
+
return events[0];
|
|
39
|
+
}
|
|
40
|
+
function findNativeEvents(element, sourceComponentId, instanceKey) {
|
|
41
|
+
const events = [];
|
|
42
|
+
const event = element.props.event;
|
|
43
|
+
if (event?.sourceComponentId === sourceComponentId &&
|
|
44
|
+
(instanceKey === undefined || event.instanceKey === instanceKey)) {
|
|
45
|
+
events.push(event);
|
|
46
|
+
}
|
|
47
|
+
for (const child of element.children ?? []) {
|
|
48
|
+
events.push(...findNativeEvents(child, sourceComponentId, instanceKey));
|
|
49
|
+
}
|
|
50
|
+
return events;
|
|
51
|
+
}
|
|
52
|
+
function createAdapterContext(surface, policy, dataModel, locale) {
|
|
53
|
+
const localDataModel = dataModel === undefined
|
|
54
|
+
? parseJsonObject(surface.dataModel, "surface.dataModel")
|
|
55
|
+
: parseJsonObject(dataModel, "options.dataModel");
|
|
56
|
+
const validated = validateA2uiV1SurfaceState(dataModel === undefined ? surface : { ...surface, dataModel: localDataModel }, policy);
|
|
57
|
+
return {
|
|
58
|
+
surface: validated,
|
|
59
|
+
dataModel: parseJsonObject(validated.dataModel, "surface.dataModel"),
|
|
60
|
+
locale,
|
|
61
|
+
numberFormats: new Map(),
|
|
62
|
+
pluralRules: new Map(),
|
|
63
|
+
visiting: new Set(),
|
|
64
|
+
formatStringExpressionCount: 0,
|
|
65
|
+
formattedStringLength: 0,
|
|
66
|
+
renderNodeCount: 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parseRenderPlanOptions(options) {
|
|
70
|
+
const parsed = parseOptionsObject(options, "A2UI native render plan options", [
|
|
71
|
+
"dataModel",
|
|
72
|
+
"locale",
|
|
73
|
+
]);
|
|
74
|
+
return {
|
|
75
|
+
...(parsed.dataModel === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { dataModel: parseJsonObject(parsed.dataModel, "options.dataModel") }),
|
|
78
|
+
...(parsed.locale === undefined
|
|
79
|
+
? {}
|
|
80
|
+
: { locale: parseLocale(parsed.locale, "options.locale") }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function parseEventResolutionOptions(options) {
|
|
84
|
+
const parsed = parseOptionsObject(options, "A2UI native event resolution options", [
|
|
85
|
+
"instanceKey",
|
|
86
|
+
"locale",
|
|
87
|
+
]);
|
|
88
|
+
if (parsed.instanceKey !== undefined &&
|
|
89
|
+
(typeof parsed.instanceKey !== "string" || parsed.instanceKey.length === 0)) {
|
|
90
|
+
throw new A2uiParseError("Expected a non-empty A2UI native event instance key");
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
...(parsed.instanceKey === undefined ? {} : { instanceKey: parsed.instanceKey }),
|
|
94
|
+
...(parsed.locale === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { locale: parseLocale(parsed.locale, "options.locale") }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function parseOptionsObject(value, label, allowedKeys) {
|
|
100
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
101
|
+
throw new A2uiParseError(`Expected ${label} to be an object`);
|
|
102
|
+
}
|
|
103
|
+
const prototype = Object.getPrototypeOf(value);
|
|
104
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
105
|
+
throw new A2uiParseError(`Expected plain ${label}`);
|
|
106
|
+
}
|
|
107
|
+
const parsed = value;
|
|
108
|
+
const unknownKey = Object.keys(parsed).find((key) => !allowedKeys.includes(key));
|
|
109
|
+
if (unknownKey !== undefined) {
|
|
110
|
+
throw new A2uiParseError(`Unexpected ${label.slice(0, -1)} ${JSON.stringify(unknownKey)}`);
|
|
111
|
+
}
|
|
112
|
+
return parsed;
|
|
113
|
+
}
|
|
114
|
+
function parseLocale(value, path) {
|
|
115
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 128) {
|
|
116
|
+
throw new A2uiParseError(`Expected a non-empty BCP 47 locale at ${path}`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
if (Intl.NumberFormat.supportedLocalesOf(value, { localeMatcher: "lookup" }).length === 0) {
|
|
120
|
+
throw new A2uiParseError(`Unsupported BCP 47 locale ${JSON.stringify(value)} at ${path}`);
|
|
121
|
+
}
|
|
122
|
+
return new Intl.NumberFormat(value).resolvedOptions().locale;
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
if (cause instanceof A2uiParseError) {
|
|
126
|
+
throw cause;
|
|
127
|
+
}
|
|
128
|
+
throw new A2uiParseError(`Invalid BCP 47 locale ${JSON.stringify(value)} at ${path}`, {
|
|
129
|
+
cause,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function adaptComponent(id, key, context, scope) {
|
|
134
|
+
context.renderNodeCount += 1;
|
|
135
|
+
if (context.renderNodeCount > A2UI_V1_NATIVE_MAX_RENDER_NODES) {
|
|
136
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_RENDER_NODES} nodes`);
|
|
137
|
+
}
|
|
138
|
+
if (context.visiting.has(id)) {
|
|
139
|
+
throw new A2uiParseError(`A2UI native adapter encountered a cycle at ${JSON.stringify(id)}`);
|
|
140
|
+
}
|
|
141
|
+
const component = context.surface.components.get(id);
|
|
142
|
+
if (component === undefined) {
|
|
143
|
+
throw new A2uiParseError(`A2UI native adapter cannot find component ${JSON.stringify(id)}`);
|
|
144
|
+
}
|
|
145
|
+
context.visiting.add(id);
|
|
146
|
+
try {
|
|
147
|
+
switch (component.component) {
|
|
148
|
+
case "Row":
|
|
149
|
+
return adaptContainer(component, key, "row", undefined, context, scope);
|
|
150
|
+
case "Column":
|
|
151
|
+
return adaptContainer(component, key, "column", undefined, context, scope);
|
|
152
|
+
case "List":
|
|
153
|
+
return adaptList(component, key, context, scope);
|
|
154
|
+
case "Card":
|
|
155
|
+
return adaptCard(component, key, context, scope);
|
|
156
|
+
case "Text":
|
|
157
|
+
return adaptText(component, key, context, scope);
|
|
158
|
+
case "Button":
|
|
159
|
+
return adaptButton(component, key, context, scope);
|
|
160
|
+
case "TextField":
|
|
161
|
+
return adaptTextField(component, key, context, scope);
|
|
162
|
+
default:
|
|
163
|
+
throw new A2uiParseError(`A2UI component ${JSON.stringify(id)} uses ${JSON.stringify(component.component)}, which the native adapter does not support`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
context.visiting.delete(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function adaptContainer(component, key, layout, variant, context, scope) {
|
|
171
|
+
if (!Array.isArray(component.children)) {
|
|
172
|
+
throw new A2uiParseError(`A2UI native adapter does not yet support dynamic children at components.${component.id}.children`);
|
|
173
|
+
}
|
|
174
|
+
const props = { layout };
|
|
175
|
+
if (variant !== undefined) {
|
|
176
|
+
props.variant = variant;
|
|
177
|
+
}
|
|
178
|
+
addContainerLayoutProps(component, props);
|
|
179
|
+
addCommonProps(component, props, context, scope);
|
|
180
|
+
return {
|
|
181
|
+
key,
|
|
182
|
+
component: "View",
|
|
183
|
+
props,
|
|
184
|
+
children: component.children.map((childId, index) => adaptComponent(childId, appendInstanceKey(key, childId, index), context, scope)),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function addContainerLayoutProps(component, props) {
|
|
188
|
+
if (component.justify !== undefined) {
|
|
189
|
+
const justify = expectString(component.justify, `components.${component.id}.justify`);
|
|
190
|
+
if (justify === "stretch") {
|
|
191
|
+
throw new A2uiParseError(`A2UI native adapter does not support main-axis stretch at components.${component.id}.justify`);
|
|
192
|
+
}
|
|
193
|
+
props.justify = justify;
|
|
194
|
+
}
|
|
195
|
+
if (component.align !== undefined) {
|
|
196
|
+
props.align = expectString(component.align, `components.${component.id}.align`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function adaptList(component, key, context, scope) {
|
|
200
|
+
if (Array.isArray(component.children)) {
|
|
201
|
+
return adaptContainer(component, key, component.direction === "horizontal" ? "row" : "column", "list", context, scope);
|
|
202
|
+
}
|
|
203
|
+
const template = expectObject(component.children, `components.${component.id}.children`);
|
|
204
|
+
const pointer = expectAbsoluteBinding(template.path, `components.${component.id}.children.path`);
|
|
205
|
+
const componentId = expectString(template.componentId, `components.${component.id}.children.componentId`);
|
|
206
|
+
const value = parseJsonValue(resolveJsonPointer(context.dataModel, pointer, `components.${component.id}.children`), `components.${component.id}.children`);
|
|
207
|
+
if (!Array.isArray(value)) {
|
|
208
|
+
throw new A2uiParseError(`Expected an array at components.${component.id}.children path ${JSON.stringify(pointer)}`);
|
|
209
|
+
}
|
|
210
|
+
const props = {
|
|
211
|
+
layout: component.direction === "horizontal" ? "row" : "column",
|
|
212
|
+
variant: "list",
|
|
213
|
+
};
|
|
214
|
+
addContainerLayoutProps(component, props);
|
|
215
|
+
addCommonProps(component, props, context, scope);
|
|
216
|
+
return {
|
|
217
|
+
key,
|
|
218
|
+
component: "View",
|
|
219
|
+
props,
|
|
220
|
+
children: value.map((item, index) => adaptComponent(componentId, appendInstanceKey(key, componentId, index), context, {
|
|
221
|
+
value: item,
|
|
222
|
+
pointer: appendPointerToken(pointer, String(index)),
|
|
223
|
+
index,
|
|
224
|
+
})),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function adaptCard(component, key, context, scope) {
|
|
228
|
+
const childId = expectString(component.child, `components.${component.id}.child`);
|
|
229
|
+
const props = { layout: "column", variant: "card" };
|
|
230
|
+
addCommonProps(component, props, context, scope);
|
|
231
|
+
return {
|
|
232
|
+
key,
|
|
233
|
+
component: "View",
|
|
234
|
+
props,
|
|
235
|
+
children: [adaptComponent(childId, appendInstanceKey(key, childId, 0), context, scope)],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function adaptText(component, key, context, scope) {
|
|
239
|
+
const props = {
|
|
240
|
+
children: resolveDynamicString(component.text, `components.${component.id}.text`, context, scope),
|
|
241
|
+
};
|
|
242
|
+
if (component.variant !== undefined) {
|
|
243
|
+
props.variant = component.variant;
|
|
244
|
+
}
|
|
245
|
+
addCommonProps(component, props, context, scope);
|
|
246
|
+
return { key, component: "Text", props };
|
|
247
|
+
}
|
|
248
|
+
function adaptButton(component, key, context, scope) {
|
|
249
|
+
rejectUnsupportedChecks(component);
|
|
250
|
+
const childId = expectString(component.child, `components.${component.id}.child`);
|
|
251
|
+
const child = context.surface.components.get(childId);
|
|
252
|
+
if (child?.component !== "Text") {
|
|
253
|
+
throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} requires a Text child`);
|
|
254
|
+
}
|
|
255
|
+
const props = {
|
|
256
|
+
title: resolveDynamicString(child.text, `components.${child.id}.text`, context, scope),
|
|
257
|
+
event: resolveButtonEvent(component, key, context, scope),
|
|
258
|
+
};
|
|
259
|
+
if (component.variant !== undefined) {
|
|
260
|
+
props.variant = component.variant;
|
|
261
|
+
}
|
|
262
|
+
addCommonProps(component, props, context, scope);
|
|
263
|
+
if (props.accessibilityLabel === undefined) {
|
|
264
|
+
props.accessibilityLabel = props.title;
|
|
265
|
+
}
|
|
266
|
+
return { key, component: "Button", props };
|
|
267
|
+
}
|
|
268
|
+
function adaptTextField(component, key, context, scope) {
|
|
269
|
+
rejectUnsupportedChecks(component);
|
|
270
|
+
const componentPath = `components.${component.id}`;
|
|
271
|
+
const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
272
|
+
const props = {
|
|
273
|
+
label,
|
|
274
|
+
placeholder: component.placeholder === undefined
|
|
275
|
+
? label
|
|
276
|
+
: resolveDynamicString(component.placeholder, `${componentPath}.placeholder`, context, scope),
|
|
277
|
+
};
|
|
278
|
+
if (component.value !== undefined) {
|
|
279
|
+
props.value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
|
|
280
|
+
if (isBinding(component.value)) {
|
|
281
|
+
props.binding = resolveBindingPointer(component.value.path, `${componentPath}.value.path`, scope);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (component.variant !== undefined) {
|
|
285
|
+
props.variant = component.variant;
|
|
286
|
+
}
|
|
287
|
+
addCommonProps(component, props, context, scope);
|
|
288
|
+
if (props.accessibilityLabel === undefined) {
|
|
289
|
+
props.accessibilityLabel = label;
|
|
290
|
+
}
|
|
291
|
+
return { key, component: "TextInput", props };
|
|
292
|
+
}
|
|
293
|
+
function rejectUnsupportedChecks(component) {
|
|
294
|
+
if (component.checks !== undefined) {
|
|
295
|
+
throw new A2uiParseError(`A2UI native adapter does not yet support renderer-side checks at components.${component.id}.checks`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function resolveButtonEvent(component, key, context, scope) {
|
|
299
|
+
const action = expectObject(component.action, `components.${component.id}.action`);
|
|
300
|
+
if (!Object.hasOwn(action, "event")) {
|
|
301
|
+
throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} does not support local function actions`);
|
|
302
|
+
}
|
|
303
|
+
const event = expectObject(action.event, `components.${component.id}.action.event`);
|
|
304
|
+
const eventName = expectString(event.name, `components.${component.id}.action.event.name`);
|
|
305
|
+
const eventContext = expectOptionalObject(event.context, `components.${component.id}.action.event.context`);
|
|
306
|
+
const resolvedContext = {};
|
|
307
|
+
for (const [name, value] of Object.entries(eventContext ?? {})) {
|
|
308
|
+
defineJsonProperty(resolvedContext, name, resolveDynamicValue(value, `components.${component.id}.action.event.context.${name}`, context, scope));
|
|
309
|
+
}
|
|
310
|
+
const userMessage = event.userMessage === undefined
|
|
311
|
+
? undefined
|
|
312
|
+
: resolveDynamicString(event.userMessage, `components.${component.id}.action.event.userMessage`, context, scope);
|
|
313
|
+
return {
|
|
314
|
+
name: eventName,
|
|
315
|
+
surfaceId: context.surface.surfaceId,
|
|
316
|
+
sourceComponentId: component.id,
|
|
317
|
+
instanceKey: key,
|
|
318
|
+
...(userMessage === undefined ? {} : { userMessage }),
|
|
319
|
+
context: parseJsonObject(resolvedContext, `components.${component.id}.action.event.context`),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function addCommonProps(component, props, context, scope) {
|
|
323
|
+
if (component.weight !== undefined) {
|
|
324
|
+
const weight = expectFiniteNumber(component.weight, `components.${component.id}.weight`);
|
|
325
|
+
if (weight < 0) {
|
|
326
|
+
throw new A2uiParseError(`A2UI native adapter does not support negative weight at components.${component.id}.weight`);
|
|
327
|
+
}
|
|
328
|
+
props.weight = weight;
|
|
329
|
+
}
|
|
330
|
+
if (component.accessibility === undefined) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const accessibility = expectObject(component.accessibility, `components.${component.id}.accessibility`);
|
|
334
|
+
if (accessibility.label !== undefined) {
|
|
335
|
+
props.accessibilityLabel = resolveDynamicString(accessibility.label, `components.${component.id}.accessibility.label`, context, scope);
|
|
336
|
+
}
|
|
337
|
+
if (accessibility.description !== undefined) {
|
|
338
|
+
props.accessibilityHint = resolveDynamicString(accessibility.description, `components.${component.id}.accessibility.description`, context, scope);
|
|
339
|
+
}
|
|
340
|
+
if (accessibility.live !== undefined) {
|
|
341
|
+
props.accessibilityLive = accessibility.live;
|
|
342
|
+
}
|
|
343
|
+
if (accessibility.hidden !== undefined) {
|
|
344
|
+
props.accessibilityHidden = resolveDynamicBoolean(accessibility.hidden, `components.${component.id}.accessibility.hidden`, context, scope);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function resolveDynamicString(value, path, context, scope) {
|
|
348
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
349
|
+
return expectString(resolved, path);
|
|
350
|
+
}
|
|
351
|
+
function resolveDynamicBoolean(value, path, context, scope) {
|
|
352
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
353
|
+
if (typeof resolved !== "boolean") {
|
|
354
|
+
throw new A2uiParseError(`Expected a boolean at ${path}`);
|
|
355
|
+
}
|
|
356
|
+
return resolved;
|
|
357
|
+
}
|
|
358
|
+
function resolveDynamicValue(value, path, context, scope) {
|
|
359
|
+
if (value === undefined) {
|
|
360
|
+
throw new A2uiParseError(`Missing dynamic value at ${path}`);
|
|
361
|
+
}
|
|
362
|
+
if (isFunctionCall(value)) {
|
|
363
|
+
if (value.call === "@index") {
|
|
364
|
+
if (scope === undefined) {
|
|
365
|
+
throw new A2uiParseError(`A2UI native adapter cannot evaluate @index outside a template at ${path}`);
|
|
366
|
+
}
|
|
367
|
+
const args = value.args === undefined ? undefined : expectObject(value.args, `${path}.args`);
|
|
368
|
+
const offset = args?.offset === undefined
|
|
369
|
+
? 0
|
|
370
|
+
: resolveDynamicNumber(args.offset, `${path}.args.offset`, context, scope);
|
|
371
|
+
return scope.index + offset;
|
|
372
|
+
}
|
|
373
|
+
if (value.call === "formatNumber" || value.call === "formatCurrency") {
|
|
374
|
+
return resolveNumberFormat(value, path, context, scope);
|
|
375
|
+
}
|
|
376
|
+
if (value.call === "pluralize") {
|
|
377
|
+
return resolvePluralize(value, path, context, scope);
|
|
378
|
+
}
|
|
379
|
+
if (value.call === "and" || value.call === "or") {
|
|
380
|
+
return resolveBooleanList(value, path, context, scope);
|
|
381
|
+
}
|
|
382
|
+
if (value.call === "not") {
|
|
383
|
+
const args = expectObject(value.args, `${path}.args`);
|
|
384
|
+
return !resolveDynamicBoolean(args.value, `${path}.args.value`, context, scope);
|
|
385
|
+
}
|
|
386
|
+
if (value.call === "formatString") {
|
|
387
|
+
const args = expectObject(value.args, `${path}.args`);
|
|
388
|
+
const source = expectString(args.value, `${path}.args.value`);
|
|
389
|
+
const result = evaluateA2uiV1FormatString(source, (expression, index) => {
|
|
390
|
+
return resolveDynamicValue(expression, `${path}.args.value.interpolations[${index}]`, context, scope);
|
|
391
|
+
}, `${path}.args.value`, (expressionCount) => {
|
|
392
|
+
context.formatStringExpressionCount += expressionCount;
|
|
393
|
+
if (context.formatStringExpressionCount > JSON_MAX_VALUES) {
|
|
394
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} formatString expressions`);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return recordFormattedString(result, path, context);
|
|
398
|
+
}
|
|
399
|
+
throw new A2uiParseError(`A2UI native adapter does not execute function ${JSON.stringify(value.call)} at ${path}`);
|
|
400
|
+
}
|
|
401
|
+
if (isBinding(value)) {
|
|
402
|
+
const pointer = expectString(value.path, `${path}.path`);
|
|
403
|
+
if (pointer.startsWith("/") || scope === undefined) {
|
|
404
|
+
const absolutePointer = expectAbsoluteBinding(pointer, `${path}.path`);
|
|
405
|
+
return parseJsonValue(resolveJsonPointer(context.dataModel, absolutePointer, path), path);
|
|
406
|
+
}
|
|
407
|
+
return parseJsonValue(resolveRelativePointer(scope.value, pointer, path), path);
|
|
408
|
+
}
|
|
409
|
+
return parseJsonValue(value, path);
|
|
410
|
+
}
|
|
411
|
+
function resolveDynamicNumber(value, path, context, scope) {
|
|
412
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
413
|
+
if (typeof resolved !== "number" || !Number.isFinite(resolved)) {
|
|
414
|
+
throw new A2uiParseError(`Expected a finite number at ${path}`);
|
|
415
|
+
}
|
|
416
|
+
return resolved;
|
|
417
|
+
}
|
|
418
|
+
function resolveNumberFormat(call, path, context, scope) {
|
|
419
|
+
const args = expectObject(call.args, `${path}.args`);
|
|
420
|
+
const value = resolveDynamicNumber(args.value, `${path}.args.value`, context, scope);
|
|
421
|
+
const decimals = args.decimals === undefined
|
|
422
|
+
? undefined
|
|
423
|
+
: parseDecimalPlaces(resolveDynamicNumber(args.decimals, `${path}.args.decimals`, context, scope), `${path}.args.decimals`);
|
|
424
|
+
const grouping = args.grouping === undefined
|
|
425
|
+
? true
|
|
426
|
+
: resolveDynamicBoolean(args.grouping, `${path}.args.grouping`, context, scope);
|
|
427
|
+
const currency = call.call === "formatCurrency"
|
|
428
|
+
? parseCurrencyCode(resolveDynamicString(args.currency, `${path}.args.currency`, context, scope), `${path}.args.currency`)
|
|
429
|
+
: undefined;
|
|
430
|
+
const formatter = getNumberFormat(context, decimals, grouping, currency, path);
|
|
431
|
+
try {
|
|
432
|
+
return recordFormattedString(formatter.format(value), path, context);
|
|
433
|
+
}
|
|
434
|
+
catch (cause) {
|
|
435
|
+
throw new A2uiParseError(`A2UI native adapter could not execute ${JSON.stringify(call.call)} at ${path}`, { cause });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const PLURAL_CATEGORIES = Object.freeze(["zero", "one", "two", "few", "many", "other"]);
|
|
439
|
+
function resolvePluralize(call, path, context, scope) {
|
|
440
|
+
const args = expectObject(call.args, `${path}.args`);
|
|
441
|
+
const value = resolveDynamicNumber(args.value, `${path}.args.value`, context, scope);
|
|
442
|
+
const forms = new Map();
|
|
443
|
+
for (const category of PLURAL_CATEGORIES) {
|
|
444
|
+
if (args[category] !== undefined) {
|
|
445
|
+
forms.set(category, resolveDynamicString(args[category], `${path}.args.${category}`, context, scope));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const other = forms.get("other");
|
|
449
|
+
if (other === undefined) {
|
|
450
|
+
throw new A2uiParseError(`Missing plural fallback at ${path}.args.other`);
|
|
451
|
+
}
|
|
452
|
+
const category = getPluralRules(context, path).select(value);
|
|
453
|
+
return recordFormattedString(forms.get(category) ?? other, path, context);
|
|
454
|
+
}
|
|
455
|
+
function getPluralRules(context, path) {
|
|
456
|
+
const key = context.locale ?? "";
|
|
457
|
+
const cached = context.pluralRules.get(key);
|
|
458
|
+
if (cached !== undefined) {
|
|
459
|
+
return cached;
|
|
460
|
+
}
|
|
461
|
+
try {
|
|
462
|
+
if (context.locale !== undefined &&
|
|
463
|
+
Intl.PluralRules.supportedLocalesOf(context.locale, { localeMatcher: "lookup" }).length === 0) {
|
|
464
|
+
throw new A2uiParseError(`Locale ${JSON.stringify(context.locale)} does not support plural rules at ${path}`);
|
|
465
|
+
}
|
|
466
|
+
const rules = new Intl.PluralRules(context.locale, { type: "cardinal" });
|
|
467
|
+
context.pluralRules.set(key, rules);
|
|
468
|
+
return rules;
|
|
469
|
+
}
|
|
470
|
+
catch (cause) {
|
|
471
|
+
if (cause instanceof A2uiParseError) {
|
|
472
|
+
throw cause;
|
|
473
|
+
}
|
|
474
|
+
throw new A2uiParseError(`A2UI native adapter could not construct plural rules at ${path}`, {
|
|
475
|
+
cause,
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function resolveBooleanList(call, path, context, scope) {
|
|
480
|
+
const args = expectObject(call.args, `${path}.args`);
|
|
481
|
+
if (!Array.isArray(args.values) || args.values.length < 2) {
|
|
482
|
+
throw new A2uiParseError(`Expected at least two boolean values at ${path}.args.values`);
|
|
483
|
+
}
|
|
484
|
+
const values = args.values.map((value, index) => resolveDynamicBoolean(value, `${path}.args.values[${index}]`, context, scope));
|
|
485
|
+
return call.call === "and" ? values.every(Boolean) : values.some(Boolean);
|
|
486
|
+
}
|
|
487
|
+
function parseDecimalPlaces(value, path) {
|
|
488
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 100) {
|
|
489
|
+
throw new A2uiParseError(`Expected decimal places from 0 through 100 at ${path}`);
|
|
490
|
+
}
|
|
491
|
+
return value;
|
|
492
|
+
}
|
|
493
|
+
function parseCurrencyCode(value, path) {
|
|
494
|
+
const currency = value.toUpperCase();
|
|
495
|
+
if (!/^[A-Z]{3}$/.test(currency) || !ISO_4217_CURRENCY_CODES.has(currency)) {
|
|
496
|
+
throw new A2uiParseError(`Expected a current ISO 4217 currency code at ${path}`);
|
|
497
|
+
}
|
|
498
|
+
return currency;
|
|
499
|
+
}
|
|
500
|
+
function getNumberFormat(context, decimals, grouping, currency, path) {
|
|
501
|
+
const key = JSON.stringify([
|
|
502
|
+
context.locale ?? null,
|
|
503
|
+
currency ?? null,
|
|
504
|
+
decimals ?? null,
|
|
505
|
+
grouping,
|
|
506
|
+
]);
|
|
507
|
+
const cached = context.numberFormats.get(key);
|
|
508
|
+
if (cached !== undefined) {
|
|
509
|
+
return cached;
|
|
510
|
+
}
|
|
511
|
+
const options = {
|
|
512
|
+
useGrouping: grouping,
|
|
513
|
+
...(currency === undefined ? {} : { style: "currency", currency }),
|
|
514
|
+
...(decimals === undefined
|
|
515
|
+
? {}
|
|
516
|
+
: { minimumFractionDigits: decimals, maximumFractionDigits: decimals }),
|
|
517
|
+
};
|
|
518
|
+
try {
|
|
519
|
+
const formatter = new Intl.NumberFormat(context.locale, options);
|
|
520
|
+
context.numberFormats.set(key, formatter);
|
|
521
|
+
return formatter;
|
|
522
|
+
}
|
|
523
|
+
catch (cause) {
|
|
524
|
+
throw new A2uiParseError(`Invalid number-format options at ${path}`, { cause });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function recordFormattedString(value, path, context) {
|
|
528
|
+
if (value.length > JSON_MAX_STRING_LENGTH) {
|
|
529
|
+
throw new A2uiParseError(`A2UI formatted output at ${path} exceeds maximum length of ${JSON_MAX_STRING_LENGTH}`);
|
|
530
|
+
}
|
|
531
|
+
context.formattedStringLength += value.length;
|
|
532
|
+
if (context.formattedStringLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
533
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum formatted-string length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
534
|
+
}
|
|
535
|
+
return value;
|
|
536
|
+
}
|
|
537
|
+
function resolveJsonPointer(document, pointer, path) {
|
|
538
|
+
if (pointer === "") {
|
|
539
|
+
return document;
|
|
540
|
+
}
|
|
541
|
+
return resolvePointerTokens(document, pointer.slice(1).split("/"), pointer, path);
|
|
542
|
+
}
|
|
543
|
+
function resolveRelativePointer(document, pointer, path) {
|
|
544
|
+
if (pointer === "") {
|
|
545
|
+
return document;
|
|
546
|
+
}
|
|
547
|
+
return resolvePointerTokens(document, pointer.split("/"), pointer, path);
|
|
548
|
+
}
|
|
549
|
+
function resolvePointerTokens(document, encodedTokens, pointer, path) {
|
|
550
|
+
let cursor = document;
|
|
551
|
+
for (const encodedToken of encodedTokens) {
|
|
552
|
+
const token = decodePointerToken(encodedToken, pointer);
|
|
553
|
+
if (Array.isArray(cursor)) {
|
|
554
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
555
|
+
throw new A2uiParseError(`Invalid array binding index in ${JSON.stringify(pointer)} at ${path}`);
|
|
556
|
+
}
|
|
557
|
+
const index = Number(token);
|
|
558
|
+
if (!Number.isSafeInteger(index) || index >= cursor.length) {
|
|
559
|
+
throw new A2uiParseError(`A2UI binding ${JSON.stringify(pointer)} is missing at ${path}`);
|
|
560
|
+
}
|
|
561
|
+
cursor = cursor[index];
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
if (cursor === null || typeof cursor !== "object" || !Object.hasOwn(cursor, token)) {
|
|
565
|
+
throw new A2uiParseError(`A2UI binding ${JSON.stringify(pointer)} is missing at ${path}`);
|
|
566
|
+
}
|
|
567
|
+
cursor = cursor[token];
|
|
568
|
+
}
|
|
569
|
+
return cursor;
|
|
570
|
+
}
|
|
571
|
+
function resolveBindingPointer(value, path, scope) {
|
|
572
|
+
const pointer = expectString(value, path);
|
|
573
|
+
if (pointer.startsWith("/") || scope === undefined) {
|
|
574
|
+
return expectAbsoluteBinding(pointer, path);
|
|
575
|
+
}
|
|
576
|
+
return pointer === "" ? scope.pointer : `${scope.pointer}/${pointer}`;
|
|
577
|
+
}
|
|
578
|
+
function appendPointerToken(pointer, encodedToken) {
|
|
579
|
+
return `${pointer}/${encodedToken}`;
|
|
580
|
+
}
|
|
581
|
+
function appendInstanceKey(parentKey, componentId, index) {
|
|
582
|
+
// Component IDs are arbitrary strings. Escape every key delimiter, including the escape marker,
|
|
583
|
+
// so distinct component paths cannot collapse to the same renderer-only dispatch identity.
|
|
584
|
+
const encodedId = componentId
|
|
585
|
+
.replaceAll("%", "%25")
|
|
586
|
+
.replaceAll("/", "%2F")
|
|
587
|
+
.replaceAll(":", "%3A");
|
|
588
|
+
return `${parentKey}/${encodedId}:${index}`;
|
|
589
|
+
}
|
|
590
|
+
function expectAbsoluteBinding(value, path) {
|
|
591
|
+
const pointer = expectString(value, path);
|
|
592
|
+
if (pointer !== "" && !pointer.startsWith("/")) {
|
|
593
|
+
throw new A2uiParseError(`A2UI native adapter requires an absolute binding at ${path}; dynamic templates are not supported`);
|
|
594
|
+
}
|
|
595
|
+
return pointer;
|
|
596
|
+
}
|
|
597
|
+
function decodePointerToken(token, pointer) {
|
|
598
|
+
for (let index = 0; index < token.length; index += 1) {
|
|
599
|
+
if (token[index] !== "~") {
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const escaped = token[index + 1];
|
|
603
|
+
if (escaped !== "0" && escaped !== "1") {
|
|
604
|
+
throw new A2uiParseError(`Invalid JSON Pointer escape in ${JSON.stringify(pointer)}`);
|
|
605
|
+
}
|
|
606
|
+
index += 1;
|
|
607
|
+
}
|
|
608
|
+
return token.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
609
|
+
}
|
|
610
|
+
function isBinding(value) {
|
|
611
|
+
return (value !== null &&
|
|
612
|
+
!Array.isArray(value) &&
|
|
613
|
+
typeof value === "object" &&
|
|
614
|
+
Object.hasOwn(value, "path"));
|
|
615
|
+
}
|
|
616
|
+
function isFunctionCall(value) {
|
|
617
|
+
return (value !== null &&
|
|
618
|
+
!Array.isArray(value) &&
|
|
619
|
+
typeof value === "object" &&
|
|
620
|
+
Object.hasOwn(value, "call"));
|
|
621
|
+
}
|
|
622
|
+
function expectObject(value, path) {
|
|
623
|
+
if (value === null || value === undefined || typeof value !== "object" || Array.isArray(value)) {
|
|
624
|
+
throw new A2uiParseError(`Expected an object at ${path}`);
|
|
625
|
+
}
|
|
626
|
+
return value;
|
|
627
|
+
}
|
|
628
|
+
function expectOptionalObject(value, path) {
|
|
629
|
+
return value === undefined ? undefined : expectObject(value, path);
|
|
630
|
+
}
|
|
631
|
+
function expectString(value, path) {
|
|
632
|
+
if (typeof value !== "string") {
|
|
633
|
+
throw new A2uiParseError(`Expected a string at ${path}`);
|
|
634
|
+
}
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
function expectFiniteNumber(value, path) {
|
|
638
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
639
|
+
throw new A2uiParseError(`Expected a finite number at ${path}`);
|
|
640
|
+
}
|
|
641
|
+
return value;
|
|
642
|
+
}
|
|
643
|
+
function defineJsonProperty(object, key, value) {
|
|
644
|
+
Object.defineProperty(object, key, {
|
|
645
|
+
configurable: true,
|
|
646
|
+
enumerable: true,
|
|
647
|
+
value,
|
|
648
|
+
writable: true,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
//# sourceMappingURL=v1.js.map
|