@mcp-native/react-native 0.2.0 → 0.4.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",
@@ -11,21 +12,197 @@ export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
11
12
  ]);
12
13
  /** Maximum expanded native-plan nodes, including repeated component references. */
13
14
  export const A2UI_V1_NATIVE_MAX_RENDER_NODES = A2UI_V1_MAX_COMPONENTS;
15
+ /** Maximum canonical HTTP(S) URL retained for one supported local action. */
16
+ export const A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH = 8_192;
14
17
  /**
15
18
  * Converts a policy-validated A2UI v1 surface into the existing host-owned
16
19
  * native render plan. Unsupported renderer semantics fail closed.
17
20
  */
18
- export function createA2uiV1NativeRenderPlan(surface, policy) {
19
- const validated = validateA2uiV1SurfaceState(surface, policy);
20
- const context = {
21
+ export function createA2uiV1NativeRenderPlan(surface, policy, options = {}) {
22
+ return createNativeRenderPlan(surface, policy, options, false);
23
+ }
24
+ /** Internal mounted-surface path that keeps temporary renderer-local URL edits non-dispatchable. */
25
+ export function createA2uiV1NativeRenderPlanForLocalEdits(surface, policy, options = {}) {
26
+ return createNativeRenderPlan(surface, policy, options, true);
27
+ }
28
+ function createNativeRenderPlan(surface, policy, options, tolerateInvalidLocalOpenUrls) {
29
+ const parsedOptions = parseRenderPlanOptions(options);
30
+ const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls);
31
+ return adaptComponent("root", "root", context, undefined);
32
+ }
33
+ /** Resolves one validated event against the latest renderer-local data model. */
34
+ export function resolveA2uiV1NativeEvent(surface, policy, sourceComponentId, dataModel, options = {}) {
35
+ if (typeof sourceComponentId !== "string" || sourceComponentId.length === 0) {
36
+ throw new A2uiParseError("Expected a non-empty A2UI source component id");
37
+ }
38
+ const parsedOptions = parseEventResolutionOptions(options);
39
+ const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
40
+ const plan = adaptComponent("root", "root", context, undefined);
41
+ const events = findNativeEvents(plan, sourceComponentId, parsedOptions.instanceKey);
42
+ if (events.length === 0) {
43
+ const disabledEvents = findNativeEvents(plan, sourceComponentId, parsedOptions.instanceKey, true);
44
+ if (disabledEvents.length > 0) {
45
+ throw new A2uiParseError(`A2UI native event source ${JSON.stringify(sourceComponentId)} is disabled by failed renderer checks`);
46
+ }
47
+ throw new A2uiParseError(`A2UI native event source ${JSON.stringify(sourceComponentId)} is not a reachable supported Button`);
48
+ }
49
+ if (events.length > 1) {
50
+ throw new A2uiParseError(`A2UI native event source ${JSON.stringify(sourceComponentId)} is ambiguous without its template instance key`);
51
+ }
52
+ return events[0];
53
+ }
54
+ /** Resolves one supported local URL action against the latest renderer-local data model. */
55
+ export function resolveA2uiV1NativeOpenUrl(surface, policy, sourceComponentId, dataModel, options = {}) {
56
+ if (typeof sourceComponentId !== "string" || sourceComponentId.length === 0) {
57
+ throw new A2uiParseError("Expected a non-empty A2UI openUrl source component id");
58
+ }
59
+ const parsedOptions = parseOpenUrlResolutionOptions(options);
60
+ const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
61
+ const plan = adaptComponent("root", "root", context, undefined);
62
+ const openUrls = findNativeOpenUrls(plan, sourceComponentId, parsedOptions.instanceKey);
63
+ if (openUrls.length === 0) {
64
+ const disabledOpenUrls = findNativeOpenUrls(plan, sourceComponentId, parsedOptions.instanceKey, true);
65
+ if (disabledOpenUrls.length > 0) {
66
+ throw new A2uiParseError(`A2UI native openUrl source ${JSON.stringify(sourceComponentId)} is disabled by failed renderer checks`);
67
+ }
68
+ throw new A2uiParseError(`A2UI native openUrl source ${JSON.stringify(sourceComponentId)} is not a reachable supported Button`);
69
+ }
70
+ if (openUrls.length > 1) {
71
+ throw new A2uiParseError(`A2UI native openUrl source ${JSON.stringify(sourceComponentId)} is ambiguous without its template instance key`);
72
+ }
73
+ return openUrls[0];
74
+ }
75
+ function findNativeEvents(element, sourceComponentId, instanceKey, includeDisabled = false) {
76
+ const events = [];
77
+ const event = element.props.event;
78
+ if (event?.sourceComponentId === sourceComponentId &&
79
+ (includeDisabled || element.props.disabled !== true) &&
80
+ (instanceKey === undefined || event.instanceKey === instanceKey)) {
81
+ events.push(event);
82
+ }
83
+ for (const child of element.children ?? []) {
84
+ events.push(...findNativeEvents(child, sourceComponentId, instanceKey, includeDisabled));
85
+ }
86
+ return events;
87
+ }
88
+ function findNativeOpenUrls(element, sourceComponentId, instanceKey, includeDisabled = false) {
89
+ const openUrls = [];
90
+ const openUrl = element.props.openUrl;
91
+ if (openUrl?.sourceComponentId === sourceComponentId &&
92
+ (includeDisabled || element.props.disabled !== true) &&
93
+ (instanceKey === undefined || openUrl.instanceKey === instanceKey)) {
94
+ openUrls.push(openUrl);
95
+ }
96
+ for (const child of element.children ?? []) {
97
+ openUrls.push(...findNativeOpenUrls(child, sourceComponentId, instanceKey, includeDisabled));
98
+ }
99
+ return openUrls;
100
+ }
101
+ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false) {
102
+ const localDataModel = dataModel === undefined
103
+ ? parseJsonObject(surface.dataModel, "surface.dataModel")
104
+ : parseJsonObject(dataModel, "options.dataModel");
105
+ const validated = validateA2uiV1SurfaceState(dataModel === undefined ? surface : { ...surface, dataModel: localDataModel }, policy);
106
+ return {
21
107
  surface: validated,
22
108
  dataModel: parseJsonObject(validated.dataModel, "surface.dataModel"),
109
+ locale,
110
+ dateFormats: new Map(),
111
+ numberFormats: new Map(),
112
+ pluralRules: new Map(),
23
113
  visiting: new Set(),
114
+ tolerateInvalidLocalOpenUrls,
115
+ formatStringExpressionCount: 0,
116
+ formattedStringLength: 0,
117
+ openUrlLength: 0,
24
118
  renderNodeCount: 0,
119
+ validationCheckCount: 0,
120
+ validationOutputLength: 0,
121
+ };
122
+ }
123
+ function parseRenderPlanOptions(options) {
124
+ const parsed = parseOptionsObject(options, "A2UI native render plan options", [
125
+ "dataModel",
126
+ "locale",
127
+ ]);
128
+ return {
129
+ ...(parsed.dataModel === undefined
130
+ ? {}
131
+ : { dataModel: parseJsonObject(parsed.dataModel, "options.dataModel") }),
132
+ ...(parsed.locale === undefined
133
+ ? {}
134
+ : { locale: parseLocale(parsed.locale, "options.locale") }),
135
+ };
136
+ }
137
+ function parseEventResolutionOptions(options) {
138
+ const parsed = parseOptionsObject(options, "A2UI native event resolution options", [
139
+ "instanceKey",
140
+ "locale",
141
+ ]);
142
+ if (parsed.instanceKey !== undefined &&
143
+ (typeof parsed.instanceKey !== "string" || parsed.instanceKey.length === 0)) {
144
+ throw new A2uiParseError("Expected a non-empty A2UI native event instance key");
145
+ }
146
+ return {
147
+ ...(parsed.instanceKey === undefined ? {} : { instanceKey: parsed.instanceKey }),
148
+ ...(parsed.locale === undefined
149
+ ? {}
150
+ : { locale: parseLocale(parsed.locale, "options.locale") }),
151
+ };
152
+ }
153
+ function parseOpenUrlResolutionOptions(options) {
154
+ const parsed = parseOptionsObject(options, "A2UI native openUrl resolution options", [
155
+ "instanceKey",
156
+ "locale",
157
+ ]);
158
+ if (parsed.instanceKey !== undefined &&
159
+ (typeof parsed.instanceKey !== "string" || parsed.instanceKey.length === 0)) {
160
+ throw new A2uiParseError("Expected a non-empty A2UI native openUrl instance key");
161
+ }
162
+ return {
163
+ ...(parsed.instanceKey === undefined ? {} : { instanceKey: parsed.instanceKey }),
164
+ ...(parsed.locale === undefined
165
+ ? {}
166
+ : { locale: parseLocale(parsed.locale, "options.locale") }),
25
167
  };
26
- return adaptComponent("root", "root", context);
27
168
  }
28
- function adaptComponent(id, key, context) {
169
+ function parseOptionsObject(value, label, allowedKeys) {
170
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
171
+ throw new A2uiParseError(`Expected ${label} to be an object`);
172
+ }
173
+ const prototype = Object.getPrototypeOf(value);
174
+ if (prototype !== Object.prototype && prototype !== null) {
175
+ throw new A2uiParseError(`Expected plain ${label}`);
176
+ }
177
+ const parsed = value;
178
+ const unknownKey = Object.keys(parsed).find((key) => !allowedKeys.includes(key));
179
+ if (unknownKey !== undefined) {
180
+ throw new A2uiParseError(`Unexpected ${label.slice(0, -1)} ${JSON.stringify(unknownKey)}`);
181
+ }
182
+ return parsed;
183
+ }
184
+ function parseLocale(value, path) {
185
+ if (typeof value !== "string" || value.length === 0 || value.length > 128) {
186
+ throw new A2uiParseError(`Expected a non-empty BCP 47 locale at ${path}`);
187
+ }
188
+ try {
189
+ const canonicalLocale = Intl.getCanonicalLocales(value)[0];
190
+ if (Intl.NumberFormat.supportedLocalesOf(canonicalLocale, { localeMatcher: "lookup" }).length ===
191
+ 0) {
192
+ throw new A2uiParseError(`Unsupported BCP 47 locale ${JSON.stringify(value)} at ${path}`);
193
+ }
194
+ return canonicalLocale;
195
+ }
196
+ catch (cause) {
197
+ if (cause instanceof A2uiParseError) {
198
+ throw cause;
199
+ }
200
+ throw new A2uiParseError(`Invalid BCP 47 locale ${JSON.stringify(value)} at ${path}`, {
201
+ cause,
202
+ });
203
+ }
204
+ }
205
+ function adaptComponent(id, key, context, scope) {
29
206
  context.renderNodeCount += 1;
30
207
  if (context.renderNodeCount > A2UI_V1_NATIVE_MAX_RENDER_NODES) {
31
208
  throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_RENDER_NODES} nodes`);
@@ -41,19 +218,19 @@ function adaptComponent(id, key, context) {
41
218
  try {
42
219
  switch (component.component) {
43
220
  case "Row":
44
- return adaptContainer(component, key, "row", undefined, context);
221
+ return adaptContainer(component, key, "row", undefined, context, scope);
45
222
  case "Column":
46
- return adaptContainer(component, key, "column", undefined, context);
223
+ return adaptContainer(component, key, "column", undefined, context, scope);
47
224
  case "List":
48
- return adaptList(component, key, context);
225
+ return adaptList(component, key, context, scope);
49
226
  case "Card":
50
- return adaptCard(component, key, context);
227
+ return adaptCard(component, key, context, scope);
51
228
  case "Text":
52
- return adaptText(component, key, context);
229
+ return adaptText(component, key, context, scope);
53
230
  case "Button":
54
- return adaptButton(component, key, context);
231
+ return adaptButton(component, key, context, scope);
55
232
  case "TextField":
56
- return adaptTextField(component, key, context);
233
+ return adaptTextField(component, key, context, scope);
57
234
  default:
58
235
  throw new A2uiParseError(`A2UI component ${JSON.stringify(id)} uses ${JSON.stringify(component.component)}, which the native adapter does not support`);
59
236
  }
@@ -62,7 +239,7 @@ function adaptComponent(id, key, context) {
62
239
  context.visiting.delete(id);
63
240
  }
64
241
  }
65
- function adaptContainer(component, key, layout, variant, context) {
242
+ function adaptContainer(component, key, layout, variant, context, scope) {
66
243
  if (!Array.isArray(component.children)) {
67
244
  throw new A2uiParseError(`A2UI native adapter does not yet support dynamic children at components.${component.id}.children`);
68
245
  }
@@ -70,168 +247,976 @@ function adaptContainer(component, key, layout, variant, context) {
70
247
  if (variant !== undefined) {
71
248
  props.variant = variant;
72
249
  }
250
+ addContainerLayoutProps(component, props);
251
+ addCommonProps(component, props, context, scope);
252
+ return {
253
+ key,
254
+ component: "View",
255
+ props,
256
+ children: component.children.map((childId, index) => adaptComponent(childId, appendInstanceKey(key, childId, index), context, scope)),
257
+ };
258
+ }
259
+ function addContainerLayoutProps(component, props) {
73
260
  if (component.justify !== undefined) {
74
- props.justify = expectString(component.justify, `components.${component.id}.justify`);
261
+ const justify = expectString(component.justify, `components.${component.id}.justify`);
262
+ if (justify === "stretch") {
263
+ throw new A2uiParseError(`A2UI native adapter does not support main-axis stretch at components.${component.id}.justify`);
264
+ }
265
+ props.justify = justify;
75
266
  }
76
267
  if (component.align !== undefined) {
77
268
  props.align = expectString(component.align, `components.${component.id}.align`);
78
269
  }
79
- addCommonProps(component, props, context);
270
+ }
271
+ function adaptList(component, key, context, scope) {
272
+ if (Array.isArray(component.children)) {
273
+ return adaptContainer(component, key, component.direction === "horizontal" ? "row" : "column", "list", context, scope);
274
+ }
275
+ const template = expectObject(component.children, `components.${component.id}.children`);
276
+ const pointer = expectAbsoluteBinding(template.path, `components.${component.id}.children.path`);
277
+ const componentId = expectString(template.componentId, `components.${component.id}.children.componentId`);
278
+ const value = parseJsonValue(resolveJsonPointer(context.dataModel, pointer, `components.${component.id}.children`), `components.${component.id}.children`);
279
+ if (!Array.isArray(value)) {
280
+ throw new A2uiParseError(`Expected an array at components.${component.id}.children path ${JSON.stringify(pointer)}`);
281
+ }
282
+ const props = {
283
+ layout: component.direction === "horizontal" ? "row" : "column",
284
+ variant: "list",
285
+ };
286
+ addContainerLayoutProps(component, props);
287
+ addCommonProps(component, props, context, scope);
80
288
  return {
81
289
  key,
82
290
  component: "View",
83
291
  props,
84
- children: component.children.map((childId, index) => adaptComponent(childId, `${key}/${childId}:${index}`, context)),
292
+ children: value.map((item, index) => adaptComponent(componentId, appendInstanceKey(key, componentId, index), context, {
293
+ value: item,
294
+ pointer: appendPointerToken(pointer, String(index)),
295
+ index,
296
+ })),
85
297
  };
86
298
  }
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) {
299
+ function adaptCard(component, key, context, scope) {
91
300
  const childId = expectString(component.child, `components.${component.id}.child`);
92
301
  const props = { layout: "column", variant: "card" };
93
- addCommonProps(component, props, context);
302
+ addCommonProps(component, props, context, scope);
94
303
  return {
95
304
  key,
96
305
  component: "View",
97
306
  props,
98
- children: [adaptComponent(childId, `${key}/${childId}:0`, context)],
307
+ children: [adaptComponent(childId, appendInstanceKey(key, childId, 0), context, scope)],
99
308
  };
100
309
  }
101
- function adaptText(component, key, context) {
310
+ function adaptText(component, key, context, scope) {
102
311
  const props = {
103
- children: resolveDynamicString(component.text, `components.${component.id}.text`, context),
312
+ children: resolveDynamicString(component.text, `components.${component.id}.text`, context, scope),
104
313
  };
105
314
  if (component.variant !== undefined) {
106
315
  props.variant = component.variant;
107
316
  }
108
- addCommonProps(component, props, context);
317
+ addCommonProps(component, props, context, scope);
109
318
  return { key, component: "Text", props };
110
319
  }
111
- function adaptButton(component, key, context) {
112
- rejectUnsupportedChecks(component);
320
+ function adaptButton(component, key, context, scope) {
113
321
  const childId = expectString(component.child, `components.${component.id}.child`);
114
322
  const child = context.surface.components.get(childId);
115
323
  if (child?.component !== "Text") {
116
324
  throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} requires a Text child`);
117
325
  }
118
326
  const props = {
119
- title: resolveDynamicString(child.text, `components.${child.id}.text`, context),
120
- event: resolveButtonEvent(component, context),
327
+ title: resolveDynamicString(child.text, `components.${child.id}.text`, context, scope),
328
+ // Validate action input even while checks disable dispatch, so inactive state cannot conceal
329
+ // malformed or host-denied dynamic semantics.
330
+ ...resolveButtonAction(component, key, context, scope),
121
331
  };
122
332
  if (component.variant !== undefined) {
123
333
  props.variant = component.variant;
124
334
  }
125
- addCommonProps(component, props, context);
335
+ addCommonProps(component, props, context, scope);
336
+ addValidationProps(component, props, context, scope, "button");
126
337
  if (props.accessibilityLabel === undefined) {
127
338
  props.accessibilityLabel = props.title;
128
339
  }
129
340
  return { key, component: "Button", props };
130
341
  }
131
- function adaptTextField(component, key, context) {
132
- rejectUnsupportedChecks(component);
342
+ function adaptTextField(component, key, context, scope) {
133
343
  const componentPath = `components.${component.id}`;
134
- const label = resolveDynamicString(component.label, `${componentPath}.label`, context);
344
+ const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
135
345
  const props = {
136
346
  label,
137
347
  placeholder: component.placeholder === undefined
138
348
  ? label
139
- : resolveDynamicString(component.placeholder, `${componentPath}.placeholder`, context),
349
+ : resolveDynamicString(component.placeholder, `${componentPath}.placeholder`, context, scope),
140
350
  };
141
351
  if (component.value !== undefined) {
142
- props.value = resolveDynamicString(component.value, `${componentPath}.value`, context);
352
+ props.value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
143
353
  if (isBinding(component.value)) {
144
- props.binding = expectAbsoluteBinding(component.value.path, `${componentPath}.value.path`);
354
+ props.binding = resolveBindingPointer(component.value.path, `${componentPath}.value.path`, scope);
145
355
  }
146
356
  }
147
357
  if (component.variant !== undefined) {
148
358
  props.variant = component.variant;
149
359
  }
150
- addCommonProps(component, props, context);
360
+ addCommonProps(component, props, context, scope);
361
+ addValidationProps(component, props, context, scope, "input");
151
362
  if (props.accessibilityLabel === undefined) {
152
363
  props.accessibilityLabel = label;
153
364
  }
154
365
  return { key, component: "TextInput", props };
155
366
  }
156
- function rejectUnsupportedChecks(component) {
157
- if (component.checks !== undefined) {
158
- throw new A2uiParseError(`A2UI native adapter does not yet support renderer-side checks at components.${component.id}.checks`);
367
+ function addValidationProps(component, props, context, scope, target) {
368
+ if (component.checks === undefined) {
369
+ return;
370
+ }
371
+ if (!Array.isArray(component.checks)) {
372
+ throw new A2uiParseError(`Expected an array at components.${component.id}.checks`);
373
+ }
374
+ const messages = [];
375
+ let valid = true;
376
+ for (const [index, value] of component.checks.entries()) {
377
+ context.validationCheckCount += 1;
378
+ if (context.validationCheckCount > JSON_MAX_VALUES) {
379
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} renderer checks`);
380
+ }
381
+ const path = `components.${component.id}.checks[${index}]`;
382
+ const check = expectObject(value, path);
383
+ // The pinned Candidate's CheckRule prose says ValidationResult object, but its Checkable
384
+ // contract and reference implementation use a boolean. Follow that executable contract.
385
+ if (!resolveDynamicBoolean(check.condition, `${path}.condition`, context, scope)) {
386
+ valid = false;
387
+ if (check.message !== undefined) {
388
+ messages.push(expectString(check.message, `${path}.message`));
389
+ }
390
+ }
391
+ }
392
+ if (valid) {
393
+ return;
394
+ }
395
+ props[target === "button" ? "disabled" : "invalid"] = true;
396
+ if (messages.length === 0) {
397
+ return;
159
398
  }
399
+ const existingHint = typeof props.accessibilityHint === "string" ? props.accessibilityHint : undefined;
400
+ const validationHintLength = messages.reduce((length, message, index) => length + message.length + (index === 0 ? 0 : 1), 0);
401
+ const outputLength = validationHintLength + (existingHint === undefined ? 0 : existingHint.length + 1);
402
+ if (outputLength > JSON_MAX_STRING_LENGTH) {
403
+ throw new A2uiParseError(`A2UI validation output at components.${component.id}.checks exceeds maximum length of ${JSON_MAX_STRING_LENGTH}`);
404
+ }
405
+ context.validationOutputLength += outputLength;
406
+ if (context.validationOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
407
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum validation-output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
408
+ }
409
+ props.validationMessages = Object.freeze(messages);
410
+ const validationHint = messages.join(" ");
411
+ props.accessibilityHint =
412
+ existingHint === undefined ? validationHint : `${existingHint} ${validationHint}`;
413
+ }
414
+ const VALIDATION_FUNCTION_NAMES = new Set([
415
+ "required",
416
+ "regex",
417
+ "length",
418
+ "numeric",
419
+ "email",
420
+ ]);
421
+ const A2UI_V1_MAX_REGEX_PATTERN_LENGTH = 256;
422
+ const A2UI_V1_MAX_REGEX_INPUT_LENGTH = 4_096;
423
+ const A2UI_V1_MAX_REGEX_REPEAT = 4_096;
424
+ const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
425
+ function resolveValidationFunction(call, path, context, scope) {
426
+ const args = expectObject(call.args, `${path}.args`);
427
+ if (call.call === "required") {
428
+ const value = resolveDynamicValue(args.value, `${path}.args.value`, context, scope);
429
+ return !(value === null ||
430
+ (typeof value === "string" && value.length === 0) ||
431
+ (Array.isArray(value) && value.length === 0));
432
+ }
433
+ if (call.call === "regex") {
434
+ const value = resolveDynamicString(args.value, `${path}.args.value`, context, scope);
435
+ const pattern = expectString(args.pattern, `${path}.args.pattern`);
436
+ if (value.length > A2UI_V1_MAX_REGEX_INPUT_LENGTH) {
437
+ return false;
438
+ }
439
+ return compileValidationRegex(pattern, path).test(value);
440
+ }
441
+ if (call.call === "length") {
442
+ const value = resolveDynamicString(args.value, `${path}.args.value`, context, scope);
443
+ const bounds = parseValidationBounds(args, path, true);
444
+ return ((bounds.min === undefined || value.length >= bounds.min) &&
445
+ (bounds.max === undefined || value.length <= bounds.max));
446
+ }
447
+ if (call.call === "numeric") {
448
+ const value = resolveDynamicNumber(args.value, `${path}.args.value`, context, scope);
449
+ const bounds = parseValidationBounds(args, path, false);
450
+ return ((bounds.min === undefined || value >= bounds.min) &&
451
+ (bounds.max === undefined || value <= bounds.max));
452
+ }
453
+ const value = resolveDynamicString(args.value, `${path}.args.value`, context, scope);
454
+ return value.length <= 320 && EMAIL_PATTERN.test(value);
455
+ }
456
+ function parseValidationBounds(args, path, integer) {
457
+ const parseBound = (name) => {
458
+ if (args[name] === undefined) {
459
+ return undefined;
460
+ }
461
+ const value = expectFiniteNumber(args[name], `${path}.args.${name}`);
462
+ if (integer && (!Number.isSafeInteger(value) || value < 0)) {
463
+ throw new A2uiParseError(`Expected a non-negative safe integer at ${path}.args.${name}`);
464
+ }
465
+ return value;
466
+ };
467
+ const min = parseBound("min");
468
+ const max = parseBound("max");
469
+ if (min === undefined && max === undefined) {
470
+ throw new A2uiParseError(`Expected min or max at ${path}.args`);
471
+ }
472
+ if (min !== undefined && max !== undefined && min > max) {
473
+ throw new A2uiParseError(`Expected min not to exceed max at ${path}.args`);
474
+ }
475
+ return { min, max };
476
+ }
477
+ function compileValidationRegex(pattern, path) {
478
+ if (pattern.length > A2UI_V1_MAX_REGEX_PATTERN_LENGTH) {
479
+ throw new A2uiParseError(`A2UI regex pattern at ${path}.args.pattern exceeds maximum length of ${A2UI_V1_MAX_REGEX_PATTERN_LENGTH}`);
480
+ }
481
+ let escaped = false;
482
+ let inCharacterClass = false;
483
+ let variableRepeatCount = 0;
484
+ for (let index = 0; index < pattern.length; index += 1) {
485
+ const character = pattern[index];
486
+ if (escaped) {
487
+ if (/[1-9kPp]/.test(character)) {
488
+ throwUnsupportedValidationRegex(pattern, path);
489
+ }
490
+ escaped = false;
491
+ continue;
492
+ }
493
+ if (character === "\\") {
494
+ escaped = true;
495
+ continue;
496
+ }
497
+ if (inCharacterClass) {
498
+ if (character === "]") {
499
+ inCharacterClass = false;
500
+ }
501
+ continue;
502
+ }
503
+ if (character === "[") {
504
+ inCharacterClass = true;
505
+ continue;
506
+ }
507
+ if (character === "(" || character === ")" || character === "|") {
508
+ throwUnsupportedValidationRegex(pattern, path);
509
+ }
510
+ if (character === "*" || character === "+" || character === "?") {
511
+ variableRepeatCount += 1;
512
+ continue;
513
+ }
514
+ if (character === "{") {
515
+ const repeat = /^\{(0|[1-9][0-9]*)(?:,(0|[1-9][0-9]*)?)?\}/.exec(pattern.slice(index));
516
+ if (repeat === null) {
517
+ throwUnsupportedValidationRegex(pattern, path);
518
+ }
519
+ const min = Number(repeat[1]);
520
+ const max = repeat[2] === undefined || repeat[2] === "" ? undefined : Number(repeat[2]);
521
+ if (min > A2UI_V1_MAX_REGEX_REPEAT ||
522
+ (max !== undefined && (max < min || max > A2UI_V1_MAX_REGEX_REPEAT))) {
523
+ throwUnsupportedValidationRegex(pattern, path);
524
+ }
525
+ if (repeat[0].includes(",") && max !== min) {
526
+ variableRepeatCount += 1;
527
+ }
528
+ index += repeat[0].length - 1;
529
+ continue;
530
+ }
531
+ if (character === "}") {
532
+ throwUnsupportedValidationRegex(pattern, path);
533
+ }
534
+ }
535
+ if (variableRepeatCount > 1) {
536
+ throwUnsupportedValidationRegex(pattern, path);
537
+ }
538
+ try {
539
+ return new RegExp(pattern);
540
+ }
541
+ catch (cause) {
542
+ throw new A2uiParseError(`Invalid regex pattern at ${path}.args.pattern`, { cause });
543
+ }
544
+ }
545
+ function throwUnsupportedValidationRegex(pattern, path) {
546
+ throw new A2uiParseError(`Unsupported potentially expensive regex pattern ${JSON.stringify(pattern)} at ${path}.args.pattern`);
160
547
  }
161
- function resolveButtonEvent(component, context) {
548
+ function resolveButtonAction(component, key, context, scope) {
162
549
  const action = expectObject(component.action, `components.${component.id}.action`);
550
+ if (Object.hasOwn(action, "functionCall")) {
551
+ const call = expectObject(action.functionCall, `components.${component.id}.action.functionCall`);
552
+ const name = expectString(call.call, `components.${component.id}.action.functionCall.call`);
553
+ if (name !== "openUrl") {
554
+ throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} does not support local function ${JSON.stringify(name)}`);
555
+ }
556
+ const args = expectObject(call.args, `components.${component.id}.action.functionCall.args`);
557
+ const path = `components.${component.id}.action.functionCall.args.url`;
558
+ const resolvedUrl = resolveDynamicString(args.url, path, context, scope);
559
+ let normalizedUrl;
560
+ try {
561
+ normalizedUrl = normalizeOpenUrl(resolvedUrl, path);
562
+ }
563
+ catch (error) {
564
+ if (!context.tolerateInvalidLocalOpenUrls || !(error instanceof A2uiParseError)) {
565
+ throw error;
566
+ }
567
+ return {
568
+ disabled: true,
569
+ invalidLocalOpenUrl: {
570
+ surfaceId: context.surface.surfaceId,
571
+ sourceComponentId: component.id,
572
+ instanceKey: key,
573
+ },
574
+ };
575
+ }
576
+ const url = recordOpenUrl(normalizedUrl, path, context);
577
+ return {
578
+ openUrl: {
579
+ url,
580
+ surfaceId: context.surface.surfaceId,
581
+ sourceComponentId: component.id,
582
+ instanceKey: key,
583
+ },
584
+ };
585
+ }
163
586
  if (!Object.hasOwn(action, "event")) {
164
- throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} does not support local function actions`);
587
+ throw new A2uiParseError(`A2UI native Button ${JSON.stringify(component.id)} requires an event or supported local function action`);
165
588
  }
166
589
  const event = expectObject(action.event, `components.${component.id}.action.event`);
167
590
  const eventName = expectString(event.name, `components.${component.id}.action.event.name`);
168
591
  const eventContext = expectOptionalObject(event.context, `components.${component.id}.action.event.context`);
169
592
  const resolvedContext = {};
170
593
  for (const [name, value] of Object.entries(eventContext ?? {})) {
171
- defineJsonProperty(resolvedContext, name, resolveDynamicValue(value, `components.${component.id}.action.event.context.${name}`, context));
594
+ defineJsonProperty(resolvedContext, name, resolveDynamicValue(value, `components.${component.id}.action.event.context.${name}`, context, scope));
172
595
  }
173
596
  const userMessage = event.userMessage === undefined
174
597
  ? undefined
175
- : resolveDynamicString(event.userMessage, `components.${component.id}.action.event.userMessage`, context);
598
+ : resolveDynamicString(event.userMessage, `components.${component.id}.action.event.userMessage`, context, scope);
599
+ return {
600
+ event: {
601
+ name: eventName,
602
+ surfaceId: context.surface.surfaceId,
603
+ sourceComponentId: component.id,
604
+ instanceKey: key,
605
+ ...(userMessage === undefined ? {} : { userMessage }),
606
+ context: parseJsonObject(resolvedContext, `components.${component.id}.action.event.context`),
607
+ },
608
+ };
609
+ }
610
+ function normalizeOpenUrl(value, path) {
611
+ if (value.length === 0 || value.length > A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH) {
612
+ throw new A2uiParseError(`Expected an HTTP(S) URL up to ${A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH} characters at ${path}`);
613
+ }
614
+ if (/\s|\p{Cf}/u.test(value) || hasAsciiControlCharacter(value)) {
615
+ throw new A2uiParseError(`Expected an HTTP(S) URL without whitespace, control, or Unicode format characters at ${path}`);
616
+ }
617
+ const UrlConstructor = globalThis.URL;
618
+ if (UrlConstructor === undefined) {
619
+ throw new A2uiParseError(`The host runtime cannot validate an openUrl value at ${path}`);
620
+ }
621
+ let parsed;
622
+ try {
623
+ parsed = new UrlConstructor(value);
624
+ }
625
+ catch (cause) {
626
+ throw new A2uiParseError(`Expected an absolute HTTP(S) URL at ${path}`, { cause });
627
+ }
628
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
629
+ throw new A2uiParseError(`Expected an HTTP(S) URL at ${path}`);
630
+ }
631
+ if (parsed.username.length > 0 || parsed.password.length > 0) {
632
+ throw new A2uiParseError(`A2UI openUrl does not allow URL credentials at ${path}`);
633
+ }
634
+ if (parsed.href.length > A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH) {
635
+ throw new A2uiParseError(`Canonical A2UI openUrl at ${path} exceeds maximum length of ${A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH}`);
636
+ }
637
+ return parsed.href;
638
+ }
639
+ function hasAsciiControlCharacter(value) {
640
+ for (let index = 0; index < value.length; index += 1) {
641
+ const code = value.charCodeAt(index);
642
+ if (code <= 31 || code === 127) {
643
+ return true;
644
+ }
645
+ }
646
+ return false;
647
+ }
648
+ function recordOpenUrl(value, path, context) {
649
+ context.openUrlLength += value.length;
650
+ if (context.openUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
651
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum openUrl length of ${A2UI_V1_MAX_SOURCE_LENGTH} at ${path}`);
652
+ }
653
+ return value;
654
+ }
655
+ /** Revalidates an untrusted URL descriptor before it crosses into a host component callback. */
656
+ export function parseA2uiV1NativeOpenUrlDescriptor(value, path) {
657
+ const descriptor = parseJsonObject(value, path);
658
+ const allowedKeys = new Set(["instanceKey", "sourceComponentId", "surfaceId", "url"]);
659
+ for (const key of Object.keys(descriptor)) {
660
+ if (!allowedKeys.has(key)) {
661
+ throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
662
+ }
663
+ }
664
+ const instanceKey = descriptor.instanceKey;
665
+ if (instanceKey !== undefined && (typeof instanceKey !== "string" || instanceKey.length === 0)) {
666
+ throw new A2uiParseError(`Expected a non-empty string at ${path}.instanceKey`);
667
+ }
176
668
  return {
177
- name: eventName,
178
- surfaceId: context.surface.surfaceId,
179
- sourceComponentId: component.id,
180
- ...(userMessage === undefined ? {} : { userMessage }),
181
- context: parseJsonObject(resolvedContext, `components.${component.id}.action.event.context`),
669
+ url: normalizeOpenUrl(expectString(descriptor.url, `${path}.url`), `${path}.url`),
670
+ surfaceId: expectNonEmptyString(descriptor.surfaceId, `${path}.surfaceId`),
671
+ sourceComponentId: expectNonEmptyString(descriptor.sourceComponentId, `${path}.sourceComponentId`),
672
+ ...(instanceKey === undefined ? {} : { instanceKey }),
182
673
  };
183
674
  }
184
- function addCommonProps(component, props, context) {
675
+ function expectNonEmptyString(value, path) {
676
+ const result = expectString(value, path);
677
+ if (result.length === 0) {
678
+ throw new A2uiParseError(`Expected a non-empty string at ${path}`);
679
+ }
680
+ return result;
681
+ }
682
+ function addCommonProps(component, props, context, scope) {
185
683
  if (component.weight !== undefined) {
186
- props.weight = expectFiniteNumber(component.weight, `components.${component.id}.weight`);
684
+ const weight = expectFiniteNumber(component.weight, `components.${component.id}.weight`);
685
+ if (weight < 0) {
686
+ throw new A2uiParseError(`A2UI native adapter does not support negative weight at components.${component.id}.weight`);
687
+ }
688
+ props.weight = weight;
187
689
  }
188
690
  if (component.accessibility === undefined) {
189
691
  return;
190
692
  }
191
693
  const accessibility = expectObject(component.accessibility, `components.${component.id}.accessibility`);
192
694
  if (accessibility.label !== undefined) {
193
- props.accessibilityLabel = resolveDynamicString(accessibility.label, `components.${component.id}.accessibility.label`, context);
695
+ props.accessibilityLabel = resolveDynamicString(accessibility.label, `components.${component.id}.accessibility.label`, context, scope);
194
696
  }
195
697
  if (accessibility.description !== undefined) {
196
- props.accessibilityHint = resolveDynamicString(accessibility.description, `components.${component.id}.accessibility.description`, context);
698
+ props.accessibilityHint = resolveDynamicString(accessibility.description, `components.${component.id}.accessibility.description`, context, scope);
197
699
  }
198
700
  if (accessibility.live !== undefined) {
199
701
  props.accessibilityLive = accessibility.live;
200
702
  }
201
703
  if (accessibility.hidden !== undefined) {
202
- props.accessibilityHidden = resolveDynamicBoolean(accessibility.hidden, `components.${component.id}.accessibility.hidden`, context);
704
+ props.accessibilityHidden = resolveDynamicBoolean(accessibility.hidden, `components.${component.id}.accessibility.hidden`, context, scope);
203
705
  }
204
706
  }
205
- function resolveDynamicString(value, path, context) {
206
- const resolved = resolveDynamicValue(value, path, context);
707
+ function resolveDynamicString(value, path, context, scope) {
708
+ const resolved = resolveDynamicValue(value, path, context, scope);
207
709
  return expectString(resolved, path);
208
710
  }
209
- function resolveDynamicBoolean(value, path, context) {
210
- const resolved = resolveDynamicValue(value, path, context);
711
+ function resolveDynamicBoolean(value, path, context, scope) {
712
+ const resolved = resolveDynamicValue(value, path, context, scope);
211
713
  if (typeof resolved !== "boolean") {
212
714
  throw new A2uiParseError(`Expected a boolean at ${path}`);
213
715
  }
214
716
  return resolved;
215
717
  }
216
- function resolveDynamicValue(value, path, context) {
718
+ function resolveDynamicValue(value, path, context, scope) {
217
719
  if (value === undefined) {
218
720
  throw new A2uiParseError(`Missing dynamic value at ${path}`);
219
721
  }
220
722
  if (isFunctionCall(value)) {
723
+ if (value.call === "@index") {
724
+ if (scope === undefined) {
725
+ throw new A2uiParseError(`A2UI native adapter cannot evaluate @index outside a template at ${path}`);
726
+ }
727
+ const args = value.args === undefined ? undefined : expectObject(value.args, `${path}.args`);
728
+ const offset = args?.offset === undefined
729
+ ? 0
730
+ : resolveDynamicNumber(args.offset, `${path}.args.offset`, context, scope);
731
+ return scope.index + offset;
732
+ }
733
+ if (VALIDATION_FUNCTION_NAMES.has(value.call)) {
734
+ return resolveValidationFunction(value, path, context, scope);
735
+ }
736
+ if (value.call === "formatNumber" || value.call === "formatCurrency") {
737
+ return resolveNumberFormat(value, path, context, scope);
738
+ }
739
+ if (value.call === "formatDate") {
740
+ return resolveDateFormat(value, path, context, scope);
741
+ }
742
+ if (value.call === "pluralize") {
743
+ return resolvePluralize(value, path, context, scope);
744
+ }
745
+ if (value.call === "and" || value.call === "or") {
746
+ return resolveBooleanList(value, path, context, scope);
747
+ }
748
+ if (value.call === "not") {
749
+ const args = expectObject(value.args, `${path}.args`);
750
+ return !resolveDynamicBoolean(args.value, `${path}.args.value`, context, scope);
751
+ }
752
+ if (value.call === "formatString") {
753
+ const args = expectObject(value.args, `${path}.args`);
754
+ const source = expectString(args.value, `${path}.args.value`);
755
+ const result = evaluateA2uiV1FormatString(source, (expression, index) => {
756
+ return resolveDynamicValue(expression, `${path}.args.value.interpolations[${index}]`, context, scope);
757
+ }, `${path}.args.value`, (expressionCount) => {
758
+ context.formatStringExpressionCount += expressionCount;
759
+ if (context.formatStringExpressionCount > JSON_MAX_VALUES) {
760
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} formatString expressions`);
761
+ }
762
+ });
763
+ return recordFormattedString(result, path, context);
764
+ }
221
765
  throw new A2uiParseError(`A2UI native adapter does not execute function ${JSON.stringify(value.call)} at ${path}`);
222
766
  }
223
767
  if (isBinding(value)) {
224
- const pointer = expectAbsoluteBinding(value.path, `${path}.path`);
225
- return parseJsonValue(resolveJsonPointer(context.dataModel, pointer, path), path);
768
+ const pointer = expectString(value.path, `${path}.path`);
769
+ if (pointer.startsWith("/") || scope === undefined) {
770
+ const absolutePointer = expectAbsoluteBinding(pointer, `${path}.path`);
771
+ return parseJsonValue(resolveJsonPointer(context.dataModel, absolutePointer, path), path);
772
+ }
773
+ return parseJsonValue(resolveRelativePointer(scope.value, pointer, path), path);
226
774
  }
227
775
  return parseJsonValue(value, path);
228
776
  }
777
+ function resolveDynamicNumber(value, path, context, scope) {
778
+ const resolved = resolveDynamicValue(value, path, context, scope);
779
+ if (typeof resolved !== "number" || !Number.isFinite(resolved)) {
780
+ throw new A2uiParseError(`Expected a finite number at ${path}`);
781
+ }
782
+ return resolved;
783
+ }
784
+ function resolveNumberFormat(call, path, context, scope) {
785
+ const args = expectObject(call.args, `${path}.args`);
786
+ const value = resolveDynamicNumber(args.value, `${path}.args.value`, context, scope);
787
+ const decimals = args.decimals === undefined
788
+ ? undefined
789
+ : parseDecimalPlaces(resolveDynamicNumber(args.decimals, `${path}.args.decimals`, context, scope), `${path}.args.decimals`);
790
+ const grouping = args.grouping === undefined
791
+ ? true
792
+ : resolveDynamicBoolean(args.grouping, `${path}.args.grouping`, context, scope);
793
+ const currency = call.call === "formatCurrency"
794
+ ? parseCurrencyCode(resolveDynamicString(args.currency, `${path}.args.currency`, context, scope), `${path}.args.currency`)
795
+ : undefined;
796
+ const formatter = getNumberFormat(context, decimals, grouping, currency, path);
797
+ try {
798
+ return recordFormattedString(formatter.format(value), path, context);
799
+ }
800
+ catch (cause) {
801
+ throw new A2uiParseError(`A2UI native adapter could not execute ${JSON.stringify(call.call)} at ${path}`, { cause });
802
+ }
803
+ }
804
+ const DATE_NUMBER = /^[+-]?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
805
+ const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
806
+ const RFC_3339_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/;
807
+ const A2UI_V1_MAX_DATE_PATTERN_TOKENS = 128;
808
+ const DATE_PATTERN_TOKENS = Object.freeze([
809
+ "MMMM",
810
+ "EEEE",
811
+ "yyyy",
812
+ "MMM",
813
+ "MM",
814
+ "dd",
815
+ "hh",
816
+ "HH",
817
+ "mm",
818
+ "ss",
819
+ "yy",
820
+ "M",
821
+ "d",
822
+ "E",
823
+ "h",
824
+ "H",
825
+ "a",
826
+ ]);
827
+ const DATE_PATTERN_TOKEN_SET = new Set(DATE_PATTERN_TOKENS);
828
+ function resolveDateFormat(call, path, context, scope) {
829
+ const args = expectObject(call.args, `${path}.args`);
830
+ const value = resolveDynamicValue(args.value, `${path}.args.value`, context, scope);
831
+ const pattern = resolveDynamicString(args.format, `${path}.args.format`, context, scope);
832
+ const date = parseDateValue(value, `${path}.args.value`);
833
+ const result = formatDatePattern(date, pattern, path, context);
834
+ return recordFormattedString(result, path, context);
835
+ }
836
+ function parseDateValue(value, path) {
837
+ if (typeof value === "number") {
838
+ return dateFromEpoch(value, path);
839
+ }
840
+ if (typeof value !== "string") {
841
+ throw new A2uiParseError(`Expected a date string or finite epoch number at ${path}`);
842
+ }
843
+ if (DATE_NUMBER.test(value)) {
844
+ return dateFromEpoch(Number(value), path);
845
+ }
846
+ const dateOnly = DATE_ONLY.exec(value);
847
+ if (dateOnly !== null) {
848
+ const year = Number(dateOnly[1]);
849
+ const month = Number(dateOnly[2]);
850
+ const day = Number(dateOnly[3]);
851
+ validateCalendarDate(year, month, day, path);
852
+ const date = new Date(0);
853
+ date.setFullYear(year, month - 1, day);
854
+ date.setHours(0, 0, 0, 0);
855
+ return date;
856
+ }
857
+ const timestamp = RFC_3339_TIMESTAMP.exec(value);
858
+ if (timestamp === null) {
859
+ throw new A2uiParseError(`Expected an RFC 3339 timestamp, yyyy-MM-dd date, or finite epoch number at ${path}`);
860
+ }
861
+ const year = Number(timestamp[1]);
862
+ const month = Number(timestamp[2]);
863
+ const day = Number(timestamp[3]);
864
+ const hour = Number(timestamp[4]);
865
+ const minute = Number(timestamp[5]);
866
+ const second = Number(timestamp[6]);
867
+ validateCalendarDate(year, month, day, path);
868
+ if (hour > 23 || minute > 59 || second > 59) {
869
+ throw new A2uiParseError(`Invalid RFC 3339 time at ${path}`);
870
+ }
871
+ const fraction = timestamp[7] ?? "";
872
+ const millisecond = Number(fraction.slice(0, 3).padEnd(3, "0"));
873
+ const date = new Date(0);
874
+ date.setUTCFullYear(year, month - 1, day);
875
+ date.setUTCHours(hour, minute, second, millisecond);
876
+ if (timestamp[8] !== "Z") {
877
+ const offsetHour = Number(timestamp[10]);
878
+ const offsetMinute = Number(timestamp[11]);
879
+ if (offsetHour > 23 || offsetMinute > 59) {
880
+ throw new A2uiParseError(`Invalid RFC 3339 offset at ${path}`);
881
+ }
882
+ const direction = timestamp[9] === "+" ? 1 : -1;
883
+ date.setTime(date.getTime() - direction * (offsetHour * 60 + offsetMinute) * 60_000);
884
+ }
885
+ if (!Number.isFinite(date.getTime())) {
886
+ throw new A2uiParseError(`Date value is outside the supported range at ${path}`);
887
+ }
888
+ return date;
889
+ }
890
+ function dateFromEpoch(value, path) {
891
+ if (!Number.isFinite(value)) {
892
+ throw new A2uiParseError(`Expected a date string or finite epoch number at ${path}`);
893
+ }
894
+ const milliseconds = Math.abs(value) > 10_000_000_000 ? value : value * 1_000;
895
+ const date = new Date(milliseconds);
896
+ if (!Number.isFinite(date.getTime())) {
897
+ throw new A2uiParseError(`Epoch value is outside the supported date range at ${path}`);
898
+ }
899
+ return date;
900
+ }
901
+ function validateCalendarDate(year, month, day, path) {
902
+ const candidate = new Date(0);
903
+ candidate.setUTCFullYear(year, month - 1, day);
904
+ candidate.setUTCHours(0, 0, 0, 0);
905
+ if (year < 1 ||
906
+ month < 1 ||
907
+ month > 12 ||
908
+ day < 1 ||
909
+ candidate.getUTCFullYear() !== year ||
910
+ candidate.getUTCMonth() !== month - 1 ||
911
+ candidate.getUTCDate() !== day) {
912
+ throw new A2uiParseError(`Invalid calendar date at ${path}`);
913
+ }
914
+ }
915
+ function formatDatePattern(date, pattern, path, context) {
916
+ const parts = parseDatePattern(pattern, path);
917
+ const hasDay = parts.some((part) => part.kind === "token" && (part.value === "d" || part.value === "dd"));
918
+ return parts
919
+ .map((part) => part.kind === "literal"
920
+ ? part.value
921
+ : formatDateToken(date, part.value, hasDay, path, context))
922
+ .join("");
923
+ }
924
+ function parseDatePattern(pattern, path) {
925
+ const parts = [];
926
+ let tokenCount = 0;
927
+ for (let index = 0; index < pattern.length;) {
928
+ if (pattern[index] === "'") {
929
+ const literal = readQuotedDateLiteral(pattern, index, path);
930
+ parts.push({ kind: "literal", value: literal.value });
931
+ index = literal.end;
932
+ continue;
933
+ }
934
+ const character = pattern[index];
935
+ if (/[A-Za-z]/.test(character)) {
936
+ let end = index + 1;
937
+ while (pattern[end] === character) {
938
+ end += 1;
939
+ }
940
+ const field = pattern.slice(index, end);
941
+ if (!DATE_PATTERN_TOKEN_SET.has(field)) {
942
+ throw new A2uiParseError(`Unsupported Unicode date pattern token ${JSON.stringify(field)} at ${path}.args.format`);
943
+ }
944
+ tokenCount += 1;
945
+ if (tokenCount > A2UI_V1_MAX_DATE_PATTERN_TOKENS) {
946
+ throw new A2uiParseError(`A2UI date pattern at ${path}.args.format exceeds maximum of ${A2UI_V1_MAX_DATE_PATTERN_TOKENS} tokens`);
947
+ }
948
+ parts.push({ kind: "token", value: field });
949
+ index = end;
950
+ continue;
951
+ }
952
+ const previous = parts.at(-1);
953
+ if (previous?.kind === "literal") {
954
+ parts[parts.length - 1] = { kind: "literal", value: previous.value + character };
955
+ }
956
+ else {
957
+ parts.push({ kind: "literal", value: character });
958
+ }
959
+ index += 1;
960
+ }
961
+ if (parts.some((part) => part.kind === "token" && (part.value === "h" || part.value === "hh")) &&
962
+ !parts.some((part) => part.kind === "token" && part.value === "a")) {
963
+ throw new A2uiParseError(`A2UI date pattern at ${path}.args.format requires token "a" when using "h" or "hh"`);
964
+ }
965
+ return parts;
966
+ }
967
+ function readQuotedDateLiteral(pattern, start, path) {
968
+ if (pattern[start + 1] === "'") {
969
+ return { end: start + 2, value: "'" };
970
+ }
971
+ let value = "";
972
+ for (let index = start + 1; index < pattern.length; index += 1) {
973
+ if (pattern[index] !== "'") {
974
+ value += pattern[index];
975
+ continue;
976
+ }
977
+ if (pattern[index + 1] === "'") {
978
+ value += "'";
979
+ index += 1;
980
+ continue;
981
+ }
982
+ return { end: index + 1, value };
983
+ }
984
+ throw new A2uiParseError(`Unterminated quoted literal at ${path}.args.format`);
985
+ }
986
+ function formatDateToken(date, token, hasDay, path, context) {
987
+ const cacheKey = `${token}:${hasDay ? "day" : "standalone"}`;
988
+ const cached = context.dateFormats.get(cacheKey);
989
+ const formatter = cached ?? createDateTokenFormatter(token, hasDay, path, context.locale);
990
+ context.dateFormats.set(cacheKey, formatter);
991
+ const partType = datePartType(token);
992
+ const part = formatter.formatToParts(date).find((candidate) => candidate.type === partType);
993
+ if (part === undefined) {
994
+ throw new A2uiParseError(`A2UI native adapter could not format token ${token} at ${path}`);
995
+ }
996
+ if (token === "yyyy") {
997
+ return normalizeLocalizedDateNumber(part.value, 4, path, context);
998
+ }
999
+ if (token === "MM" ||
1000
+ token === "dd" ||
1001
+ token === "hh" ||
1002
+ token === "HH" ||
1003
+ token === "mm" ||
1004
+ token === "ss") {
1005
+ return normalizeLocalizedDateNumber(part.value, 2, path, context);
1006
+ }
1007
+ if (token === "M" || token === "d" || token === "h" || token === "H") {
1008
+ return normalizeLocalizedDateNumber(part.value, 1, path, context);
1009
+ }
1010
+ return part.value;
1011
+ }
1012
+ function normalizeLocalizedDateNumber(value, width, path, context) {
1013
+ const length = Array.from(value).length;
1014
+ if ((width === 1 && length === 1) || (width > 1 && length >= width)) {
1015
+ return value;
1016
+ }
1017
+ const key = JSON.stringify([context.locale ?? null, "date-zero"]);
1018
+ let formatter = context.numberFormats.get(key);
1019
+ if (formatter === undefined) {
1020
+ try {
1021
+ formatter = new Intl.NumberFormat(context.locale, { useGrouping: false });
1022
+ context.numberFormats.set(key, formatter);
1023
+ }
1024
+ catch (cause) {
1025
+ throw new A2uiParseError(`Invalid year-format options at ${path}`, { cause });
1026
+ }
1027
+ }
1028
+ const zero = formatter.formatToParts(0).find((part) => part.type === "integer")?.value;
1029
+ if (zero === undefined) {
1030
+ throw new A2uiParseError(`A2UI native adapter could not localize a padded year at ${path}`);
1031
+ }
1032
+ if (width === 1) {
1033
+ let normalized = value;
1034
+ while (Array.from(normalized).length > 1 && normalized.startsWith(zero)) {
1035
+ normalized = normalized.slice(zero.length);
1036
+ }
1037
+ return normalized;
1038
+ }
1039
+ return `${zero.repeat(width - length)}${value}`;
1040
+ }
1041
+ function createDateTokenFormatter(token, hasDay, path, locale) {
1042
+ const options = token === "yy"
1043
+ ? { year: "2-digit" }
1044
+ : token === "yyyy"
1045
+ ? { year: "numeric" }
1046
+ : token === "M"
1047
+ ? { month: "numeric", ...(hasDay ? { day: "numeric" } : {}) }
1048
+ : token === "MM"
1049
+ ? { month: "2-digit", ...(hasDay ? { day: "numeric" } : {}) }
1050
+ : token === "MMM"
1051
+ ? { month: "short", ...(hasDay ? { day: "numeric" } : {}) }
1052
+ : token === "MMMM"
1053
+ ? { month: "long", ...(hasDay ? { day: "numeric" } : {}) }
1054
+ : token === "d"
1055
+ ? { day: "numeric" }
1056
+ : token === "dd"
1057
+ ? { day: "2-digit" }
1058
+ : token === "E"
1059
+ ? { weekday: "short" }
1060
+ : token === "EEEE"
1061
+ ? { weekday: "long" }
1062
+ : token === "h" || token === "hh" || token === "a"
1063
+ ? {
1064
+ hour: token === "hh" ? "2-digit" : "numeric",
1065
+ hourCycle: "h12",
1066
+ }
1067
+ : token === "H" || token === "HH"
1068
+ ? {
1069
+ hour: token === "HH" ? "2-digit" : "numeric",
1070
+ hourCycle: "h23",
1071
+ }
1072
+ : token === "mm"
1073
+ ? { minute: "2-digit" }
1074
+ : { second: "2-digit" };
1075
+ try {
1076
+ return new Intl.DateTimeFormat(locale, options);
1077
+ }
1078
+ catch (cause) {
1079
+ throw new A2uiParseError(`Invalid date-format options at ${path}`, { cause });
1080
+ }
1081
+ }
1082
+ function datePartType(token) {
1083
+ if (token === "yy" || token === "yyyy") {
1084
+ return "year";
1085
+ }
1086
+ if (token === "M" || token === "MM" || token === "MMM" || token === "MMMM") {
1087
+ return "month";
1088
+ }
1089
+ if (token === "d" || token === "dd") {
1090
+ return "day";
1091
+ }
1092
+ if (token === "E" || token === "EEEE") {
1093
+ return "weekday";
1094
+ }
1095
+ if (token === "h" || token === "hh" || token === "H" || token === "HH") {
1096
+ return "hour";
1097
+ }
1098
+ if (token === "mm") {
1099
+ return "minute";
1100
+ }
1101
+ if (token === "ss") {
1102
+ return "second";
1103
+ }
1104
+ return "dayPeriod";
1105
+ }
1106
+ const PLURAL_CATEGORIES = Object.freeze(["zero", "one", "two", "few", "many", "other"]);
1107
+ function resolvePluralize(call, path, context, scope) {
1108
+ const args = expectObject(call.args, `${path}.args`);
1109
+ const value = resolveDynamicNumber(args.value, `${path}.args.value`, context, scope);
1110
+ const forms = new Map();
1111
+ for (const category of PLURAL_CATEGORIES) {
1112
+ if (args[category] !== undefined) {
1113
+ forms.set(category, resolveDynamicString(args[category], `${path}.args.${category}`, context, scope));
1114
+ }
1115
+ }
1116
+ const other = forms.get("other");
1117
+ if (other === undefined) {
1118
+ throw new A2uiParseError(`Missing plural fallback at ${path}.args.other`);
1119
+ }
1120
+ const category = getPluralRules(context, path).select(value);
1121
+ return recordFormattedString(forms.get(category) ?? other, path, context);
1122
+ }
1123
+ function getPluralRules(context, path) {
1124
+ const key = context.locale ?? "";
1125
+ const cached = context.pluralRules.get(key);
1126
+ if (cached !== undefined) {
1127
+ return cached;
1128
+ }
1129
+ try {
1130
+ if (context.locale !== undefined &&
1131
+ Intl.PluralRules.supportedLocalesOf(context.locale, { localeMatcher: "lookup" }).length === 0) {
1132
+ throw new A2uiParseError(`Locale ${JSON.stringify(context.locale)} does not support plural rules at ${path}`);
1133
+ }
1134
+ const rules = new Intl.PluralRules(context.locale, { type: "cardinal" });
1135
+ context.pluralRules.set(key, rules);
1136
+ return rules;
1137
+ }
1138
+ catch (cause) {
1139
+ if (cause instanceof A2uiParseError) {
1140
+ throw cause;
1141
+ }
1142
+ throw new A2uiParseError(`A2UI native adapter could not construct plural rules at ${path}`, {
1143
+ cause,
1144
+ });
1145
+ }
1146
+ }
1147
+ function resolveBooleanList(call, path, context, scope) {
1148
+ const args = expectObject(call.args, `${path}.args`);
1149
+ if (!Array.isArray(args.values) || args.values.length < 2) {
1150
+ throw new A2uiParseError(`Expected at least two boolean values at ${path}.args.values`);
1151
+ }
1152
+ const values = args.values.map((value, index) => resolveDynamicBoolean(value, `${path}.args.values[${index}]`, context, scope));
1153
+ return call.call === "and" ? values.every(Boolean) : values.some(Boolean);
1154
+ }
1155
+ function parseDecimalPlaces(value, path) {
1156
+ if (!Number.isSafeInteger(value) || value < 0 || value > 100) {
1157
+ throw new A2uiParseError(`Expected decimal places from 0 through 100 at ${path}`);
1158
+ }
1159
+ return value;
1160
+ }
1161
+ function parseCurrencyCode(value, path) {
1162
+ const currency = value.toUpperCase();
1163
+ if (!/^[A-Z]{3}$/.test(currency) || !ISO_4217_CURRENCY_CODES.has(currency)) {
1164
+ throw new A2uiParseError(`Expected a current ISO 4217 currency code at ${path}`);
1165
+ }
1166
+ return currency;
1167
+ }
1168
+ function getNumberFormat(context, decimals, grouping, currency, path) {
1169
+ const key = JSON.stringify([
1170
+ context.locale ?? null,
1171
+ currency ?? null,
1172
+ decimals ?? null,
1173
+ grouping,
1174
+ ]);
1175
+ const cached = context.numberFormats.get(key);
1176
+ if (cached !== undefined) {
1177
+ return cached;
1178
+ }
1179
+ const options = {
1180
+ useGrouping: grouping,
1181
+ ...(currency === undefined ? {} : { style: "currency", currency }),
1182
+ ...(decimals === undefined
1183
+ ? {}
1184
+ : { minimumFractionDigits: decimals, maximumFractionDigits: decimals }),
1185
+ };
1186
+ try {
1187
+ const formatter = new Intl.NumberFormat(context.locale, options);
1188
+ context.numberFormats.set(key, formatter);
1189
+ return formatter;
1190
+ }
1191
+ catch (cause) {
1192
+ throw new A2uiParseError(`Invalid number-format options at ${path}`, { cause });
1193
+ }
1194
+ }
1195
+ function recordFormattedString(value, path, context) {
1196
+ if (value.length > JSON_MAX_STRING_LENGTH) {
1197
+ throw new A2uiParseError(`A2UI formatted output at ${path} exceeds maximum length of ${JSON_MAX_STRING_LENGTH}`);
1198
+ }
1199
+ context.formattedStringLength += value.length;
1200
+ if (context.formattedStringLength > A2UI_V1_MAX_SOURCE_LENGTH) {
1201
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum formatted-string length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
1202
+ }
1203
+ return value;
1204
+ }
229
1205
  function resolveJsonPointer(document, pointer, path) {
230
1206
  if (pointer === "") {
231
1207
  return document;
232
1208
  }
1209
+ return resolvePointerTokens(document, pointer.slice(1).split("/"), pointer, path);
1210
+ }
1211
+ function resolveRelativePointer(document, pointer, path) {
1212
+ if (pointer === "") {
1213
+ return document;
1214
+ }
1215
+ return resolvePointerTokens(document, pointer.split("/"), pointer, path);
1216
+ }
1217
+ function resolvePointerTokens(document, encodedTokens, pointer, path) {
233
1218
  let cursor = document;
234
- for (const encodedToken of pointer.slice(1).split("/")) {
1219
+ for (const encodedToken of encodedTokens) {
235
1220
  const token = decodePointerToken(encodedToken, pointer);
236
1221
  if (Array.isArray(cursor)) {
237
1222
  if (!/^(0|[1-9][0-9]*)$/.test(token)) {
@@ -251,6 +1236,25 @@ function resolveJsonPointer(document, pointer, path) {
251
1236
  }
252
1237
  return cursor;
253
1238
  }
1239
+ function resolveBindingPointer(value, path, scope) {
1240
+ const pointer = expectString(value, path);
1241
+ if (pointer.startsWith("/") || scope === undefined) {
1242
+ return expectAbsoluteBinding(pointer, path);
1243
+ }
1244
+ return pointer === "" ? scope.pointer : `${scope.pointer}/${pointer}`;
1245
+ }
1246
+ function appendPointerToken(pointer, encodedToken) {
1247
+ return `${pointer}/${encodedToken}`;
1248
+ }
1249
+ function appendInstanceKey(parentKey, componentId, index) {
1250
+ // Component IDs are arbitrary strings. Escape every key delimiter, including the escape marker,
1251
+ // so distinct component paths cannot collapse to the same renderer-only dispatch identity.
1252
+ const encodedId = componentId
1253
+ .replaceAll("%", "%25")
1254
+ .replaceAll("/", "%2F")
1255
+ .replaceAll(":", "%3A");
1256
+ return `${parentKey}/${encodedId}:${index}`;
1257
+ }
254
1258
  function expectAbsoluteBinding(value, path) {
255
1259
  const pointer = expectString(value, path);
256
1260
  if (pointer !== "" && !pointer.startsWith("/")) {