@mcp-native/react-native 0.2.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 +49 -21
- package/dist/index.d.ts +45 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +365 -15
- 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 +17 -1
- package/dist/v1.d.ts.map +1 -1
- package/dist/v1.js +389 -53
- package/dist/v1.js.map +1 -1
- package/package.json +3 -3
package/dist/v1.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { A2UI_V1_MAX_COMPONENTS, A2uiParseError, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
|
|
2
|
-
import { parseJsonObject, parseJsonValue } from "@mcp-native/core";
|
|
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";
|
|
3
4
|
export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
|
|
4
5
|
"Button",
|
|
5
6
|
"Card",
|
|
@@ -15,17 +16,121 @@ 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
|
|
16
17
|
* native render plan. Unsupported renderer semantics fail closed.
|
|
17
18
|
*/
|
|
18
|
-
export function createA2uiV1NativeRenderPlan(surface, policy) {
|
|
19
|
-
const
|
|
20
|
-
const context =
|
|
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 {
|
|
21
58
|
surface: validated,
|
|
22
59
|
dataModel: parseJsonObject(validated.dataModel, "surface.dataModel"),
|
|
60
|
+
locale,
|
|
61
|
+
numberFormats: new Map(),
|
|
62
|
+
pluralRules: new Map(),
|
|
23
63
|
visiting: new Set(),
|
|
64
|
+
formatStringExpressionCount: 0,
|
|
65
|
+
formattedStringLength: 0,
|
|
24
66
|
renderNodeCount: 0,
|
|
25
67
|
};
|
|
26
|
-
return adaptComponent("root", "root", context);
|
|
27
68
|
}
|
|
28
|
-
function
|
|
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) {
|
|
29
134
|
context.renderNodeCount += 1;
|
|
30
135
|
if (context.renderNodeCount > A2UI_V1_NATIVE_MAX_RENDER_NODES) {
|
|
31
136
|
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_RENDER_NODES} nodes`);
|
|
@@ -41,19 +146,19 @@ function adaptComponent(id, key, context) {
|
|
|
41
146
|
try {
|
|
42
147
|
switch (component.component) {
|
|
43
148
|
case "Row":
|
|
44
|
-
return adaptContainer(component, key, "row", undefined, context);
|
|
149
|
+
return adaptContainer(component, key, "row", undefined, context, scope);
|
|
45
150
|
case "Column":
|
|
46
|
-
return adaptContainer(component, key, "column", undefined, context);
|
|
151
|
+
return adaptContainer(component, key, "column", undefined, context, scope);
|
|
47
152
|
case "List":
|
|
48
|
-
return adaptList(component, key, context);
|
|
153
|
+
return adaptList(component, key, context, scope);
|
|
49
154
|
case "Card":
|
|
50
|
-
return adaptCard(component, key, context);
|
|
155
|
+
return adaptCard(component, key, context, scope);
|
|
51
156
|
case "Text":
|
|
52
|
-
return adaptText(component, key, context);
|
|
157
|
+
return adaptText(component, key, context, scope);
|
|
53
158
|
case "Button":
|
|
54
|
-
return adaptButton(component, key, context);
|
|
159
|
+
return adaptButton(component, key, context, scope);
|
|
55
160
|
case "TextField":
|
|
56
|
-
return adaptTextField(component, key, context);
|
|
161
|
+
return adaptTextField(component, key, context, scope);
|
|
57
162
|
default:
|
|
58
163
|
throw new A2uiParseError(`A2UI component ${JSON.stringify(id)} uses ${JSON.stringify(component.component)}, which the native adapter does not support`);
|
|
59
164
|
}
|
|
@@ -62,7 +167,7 @@ function adaptComponent(id, key, context) {
|
|
|
62
167
|
context.visiting.delete(id);
|
|
63
168
|
}
|
|
64
169
|
}
|
|
65
|
-
function adaptContainer(component, key, layout, variant, context) {
|
|
170
|
+
function adaptContainer(component, key, layout, variant, context, scope) {
|
|
66
171
|
if (!Array.isArray(component.children)) {
|
|
67
172
|
throw new A2uiParseError(`A2UI native adapter does not yet support dynamic children at components.${component.id}.children`);
|
|
68
173
|
}
|
|
@@ -70,45 +175,77 @@ function adaptContainer(component, key, layout, variant, context) {
|
|
|
70
175
|
if (variant !== undefined) {
|
|
71
176
|
props.variant = variant;
|
|
72
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) {
|
|
73
188
|
if (component.justify !== undefined) {
|
|
74
|
-
|
|
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;
|
|
75
194
|
}
|
|
76
195
|
if (component.align !== undefined) {
|
|
77
196
|
props.align = expectString(component.align, `components.${component.id}.align`);
|
|
78
197
|
}
|
|
79
|
-
|
|
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);
|
|
80
216
|
return {
|
|
81
217
|
key,
|
|
82
218
|
component: "View",
|
|
83
219
|
props,
|
|
84
|
-
children:
|
|
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
|
+
})),
|
|
85
225
|
};
|
|
86
226
|
}
|
|
87
|
-
function
|
|
88
|
-
return adaptContainer(component, key, component.direction === "horizontal" ? "row" : "column", "list", context);
|
|
89
|
-
}
|
|
90
|
-
function adaptCard(component, key, context) {
|
|
227
|
+
function adaptCard(component, key, context, scope) {
|
|
91
228
|
const childId = expectString(component.child, `components.${component.id}.child`);
|
|
92
229
|
const props = { layout: "column", variant: "card" };
|
|
93
|
-
addCommonProps(component, props, context);
|
|
230
|
+
addCommonProps(component, props, context, scope);
|
|
94
231
|
return {
|
|
95
232
|
key,
|
|
96
233
|
component: "View",
|
|
97
234
|
props,
|
|
98
|
-
children: [adaptComponent(childId,
|
|
235
|
+
children: [adaptComponent(childId, appendInstanceKey(key, childId, 0), context, scope)],
|
|
99
236
|
};
|
|
100
237
|
}
|
|
101
|
-
function adaptText(component, key, context) {
|
|
238
|
+
function adaptText(component, key, context, scope) {
|
|
102
239
|
const props = {
|
|
103
|
-
children: resolveDynamicString(component.text, `components.${component.id}.text`, context),
|
|
240
|
+
children: resolveDynamicString(component.text, `components.${component.id}.text`, context, scope),
|
|
104
241
|
};
|
|
105
242
|
if (component.variant !== undefined) {
|
|
106
243
|
props.variant = component.variant;
|
|
107
244
|
}
|
|
108
|
-
addCommonProps(component, props, context);
|
|
245
|
+
addCommonProps(component, props, context, scope);
|
|
109
246
|
return { key, component: "Text", props };
|
|
110
247
|
}
|
|
111
|
-
function adaptButton(component, key, context) {
|
|
248
|
+
function adaptButton(component, key, context, scope) {
|
|
112
249
|
rejectUnsupportedChecks(component);
|
|
113
250
|
const childId = expectString(component.child, `components.${component.id}.child`);
|
|
114
251
|
const child = context.surface.components.get(childId);
|
|
@@ -116,38 +253,38 @@ function adaptButton(component, key, context) {
|
|
|
116
253
|
throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} requires a Text child`);
|
|
117
254
|
}
|
|
118
255
|
const props = {
|
|
119
|
-
title: resolveDynamicString(child.text, `components.${child.id}.text`, context),
|
|
120
|
-
event: resolveButtonEvent(component, context),
|
|
256
|
+
title: resolveDynamicString(child.text, `components.${child.id}.text`, context, scope),
|
|
257
|
+
event: resolveButtonEvent(component, key, context, scope),
|
|
121
258
|
};
|
|
122
259
|
if (component.variant !== undefined) {
|
|
123
260
|
props.variant = component.variant;
|
|
124
261
|
}
|
|
125
|
-
addCommonProps(component, props, context);
|
|
262
|
+
addCommonProps(component, props, context, scope);
|
|
126
263
|
if (props.accessibilityLabel === undefined) {
|
|
127
264
|
props.accessibilityLabel = props.title;
|
|
128
265
|
}
|
|
129
266
|
return { key, component: "Button", props };
|
|
130
267
|
}
|
|
131
|
-
function adaptTextField(component, key, context) {
|
|
268
|
+
function adaptTextField(component, key, context, scope) {
|
|
132
269
|
rejectUnsupportedChecks(component);
|
|
133
270
|
const componentPath = `components.${component.id}`;
|
|
134
|
-
const label = resolveDynamicString(component.label, `${componentPath}.label`, context);
|
|
271
|
+
const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
135
272
|
const props = {
|
|
136
273
|
label,
|
|
137
274
|
placeholder: component.placeholder === undefined
|
|
138
275
|
? label
|
|
139
|
-
: resolveDynamicString(component.placeholder, `${componentPath}.placeholder`, context),
|
|
276
|
+
: resolveDynamicString(component.placeholder, `${componentPath}.placeholder`, context, scope),
|
|
140
277
|
};
|
|
141
278
|
if (component.value !== undefined) {
|
|
142
|
-
props.value = resolveDynamicString(component.value, `${componentPath}.value`, context);
|
|
279
|
+
props.value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
|
|
143
280
|
if (isBinding(component.value)) {
|
|
144
|
-
props.binding =
|
|
281
|
+
props.binding = resolveBindingPointer(component.value.path, `${componentPath}.value.path`, scope);
|
|
145
282
|
}
|
|
146
283
|
}
|
|
147
284
|
if (component.variant !== undefined) {
|
|
148
285
|
props.variant = component.variant;
|
|
149
286
|
}
|
|
150
|
-
addCommonProps(component, props, context);
|
|
287
|
+
addCommonProps(component, props, context, scope);
|
|
151
288
|
if (props.accessibilityLabel === undefined) {
|
|
152
289
|
props.accessibilityLabel = label;
|
|
153
290
|
}
|
|
@@ -158,7 +295,7 @@ function rejectUnsupportedChecks(component) {
|
|
|
158
295
|
throw new A2uiParseError(`A2UI native adapter does not yet support renderer-side checks at components.${component.id}.checks`);
|
|
159
296
|
}
|
|
160
297
|
}
|
|
161
|
-
function resolveButtonEvent(component, context) {
|
|
298
|
+
function resolveButtonEvent(component, key, context, scope) {
|
|
162
299
|
const action = expectObject(component.action, `components.${component.id}.action`);
|
|
163
300
|
if (!Object.hasOwn(action, "event")) {
|
|
164
301
|
throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} does not support local function actions`);
|
|
@@ -168,70 +305,250 @@ function resolveButtonEvent(component, context) {
|
|
|
168
305
|
const eventContext = expectOptionalObject(event.context, `components.${component.id}.action.event.context`);
|
|
169
306
|
const resolvedContext = {};
|
|
170
307
|
for (const [name, value] of Object.entries(eventContext ?? {})) {
|
|
171
|
-
defineJsonProperty(resolvedContext, name, resolveDynamicValue(value, `components.${component.id}.action.event.context.${name}`, context));
|
|
308
|
+
defineJsonProperty(resolvedContext, name, resolveDynamicValue(value, `components.${component.id}.action.event.context.${name}`, context, scope));
|
|
172
309
|
}
|
|
173
310
|
const userMessage = event.userMessage === undefined
|
|
174
311
|
? undefined
|
|
175
|
-
: resolveDynamicString(event.userMessage, `components.${component.id}.action.event.userMessage`, context);
|
|
312
|
+
: resolveDynamicString(event.userMessage, `components.${component.id}.action.event.userMessage`, context, scope);
|
|
176
313
|
return {
|
|
177
314
|
name: eventName,
|
|
178
315
|
surfaceId: context.surface.surfaceId,
|
|
179
316
|
sourceComponentId: component.id,
|
|
317
|
+
instanceKey: key,
|
|
180
318
|
...(userMessage === undefined ? {} : { userMessage }),
|
|
181
319
|
context: parseJsonObject(resolvedContext, `components.${component.id}.action.event.context`),
|
|
182
320
|
};
|
|
183
321
|
}
|
|
184
|
-
function addCommonProps(component, props, context) {
|
|
322
|
+
function addCommonProps(component, props, context, scope) {
|
|
185
323
|
if (component.weight !== undefined) {
|
|
186
|
-
|
|
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;
|
|
187
329
|
}
|
|
188
330
|
if (component.accessibility === undefined) {
|
|
189
331
|
return;
|
|
190
332
|
}
|
|
191
333
|
const accessibility = expectObject(component.accessibility, `components.${component.id}.accessibility`);
|
|
192
334
|
if (accessibility.label !== undefined) {
|
|
193
|
-
props.accessibilityLabel = resolveDynamicString(accessibility.label, `components.${component.id}.accessibility.label`, context);
|
|
335
|
+
props.accessibilityLabel = resolveDynamicString(accessibility.label, `components.${component.id}.accessibility.label`, context, scope);
|
|
194
336
|
}
|
|
195
337
|
if (accessibility.description !== undefined) {
|
|
196
|
-
props.accessibilityHint = resolveDynamicString(accessibility.description, `components.${component.id}.accessibility.description`, context);
|
|
338
|
+
props.accessibilityHint = resolveDynamicString(accessibility.description, `components.${component.id}.accessibility.description`, context, scope);
|
|
197
339
|
}
|
|
198
340
|
if (accessibility.live !== undefined) {
|
|
199
341
|
props.accessibilityLive = accessibility.live;
|
|
200
342
|
}
|
|
201
343
|
if (accessibility.hidden !== undefined) {
|
|
202
|
-
props.accessibilityHidden = resolveDynamicBoolean(accessibility.hidden, `components.${component.id}.accessibility.hidden`, context);
|
|
344
|
+
props.accessibilityHidden = resolveDynamicBoolean(accessibility.hidden, `components.${component.id}.accessibility.hidden`, context, scope);
|
|
203
345
|
}
|
|
204
346
|
}
|
|
205
|
-
function resolveDynamicString(value, path, context) {
|
|
206
|
-
const resolved = resolveDynamicValue(value, path, context);
|
|
347
|
+
function resolveDynamicString(value, path, context, scope) {
|
|
348
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
207
349
|
return expectString(resolved, path);
|
|
208
350
|
}
|
|
209
|
-
function resolveDynamicBoolean(value, path, context) {
|
|
210
|
-
const resolved = resolveDynamicValue(value, path, context);
|
|
351
|
+
function resolveDynamicBoolean(value, path, context, scope) {
|
|
352
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
211
353
|
if (typeof resolved !== "boolean") {
|
|
212
354
|
throw new A2uiParseError(`Expected a boolean at ${path}`);
|
|
213
355
|
}
|
|
214
356
|
return resolved;
|
|
215
357
|
}
|
|
216
|
-
function resolveDynamicValue(value, path, context) {
|
|
358
|
+
function resolveDynamicValue(value, path, context, scope) {
|
|
217
359
|
if (value === undefined) {
|
|
218
360
|
throw new A2uiParseError(`Missing dynamic value at ${path}`);
|
|
219
361
|
}
|
|
220
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
|
+
}
|
|
221
399
|
throw new A2uiParseError(`A2UI native adapter does not execute function ${JSON.stringify(value.call)} at ${path}`);
|
|
222
400
|
}
|
|
223
401
|
if (isBinding(value)) {
|
|
224
|
-
const pointer =
|
|
225
|
-
|
|
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);
|
|
226
408
|
}
|
|
227
409
|
return parseJsonValue(value, path);
|
|
228
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
|
+
}
|
|
229
537
|
function resolveJsonPointer(document, pointer, path) {
|
|
230
538
|
if (pointer === "") {
|
|
231
539
|
return document;
|
|
232
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) {
|
|
233
550
|
let cursor = document;
|
|
234
|
-
for (const encodedToken of
|
|
551
|
+
for (const encodedToken of encodedTokens) {
|
|
235
552
|
const token = decodePointerToken(encodedToken, pointer);
|
|
236
553
|
if (Array.isArray(cursor)) {
|
|
237
554
|
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
@@ -251,6 +568,25 @@ function resolveJsonPointer(document, pointer, path) {
|
|
|
251
568
|
}
|
|
252
569
|
return cursor;
|
|
253
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
|
+
}
|
|
254
590
|
function expectAbsoluteBinding(value, path) {
|
|
255
591
|
const pointer = expectString(value, path);
|
|
256
592
|
if (pointer !== "" && !pointer.startsWith("/")) {
|