@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/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 validated = validateA2uiV1SurfaceState(surface, policy);
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 adaptComponent(id, key, context) {
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
- props.justify = expectString(component.justify, `components.${component.id}.justify`);
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
- addCommonProps(component, props, context);
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: component.children.map((childId, index) => adaptComponent(childId, `${key}/${childId}:${index}`, context)),
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 adaptList(component, key, context) {
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, `${key}/${childId}:0`, context)],
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 = expectAbsoluteBinding(component.value.path, `${componentPath}.value.path`);
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
- props.weight = expectFiniteNumber(component.weight, `components.${component.id}.weight`);
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 = expectAbsoluteBinding(value.path, `${path}.path`);
225
- return parseJsonValue(resolveJsonPointer(context.dataModel, pointer, path), path);
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 pointer.slice(1).split("/")) {
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("/")) {