@mcp-native/react-native 0.6.0 → 0.7.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 +57 -43
- package/dist/component-adapters.d.ts +137 -2
- package/dist/component-adapters.d.ts.map +1 -1
- package/dist/component-adapters.js +97 -0
- package/dist/component-adapters.js.map +1 -1
- package/dist/index.d.ts +15 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +511 -23
- package/dist/index.js.map +1 -1
- package/dist/v1.d.ts +29 -0
- package/dist/v1.d.ts.map +1 -1
- package/dist/v1.js +577 -10
- package/dist/v1.js.map +1 -1
- package/package.json +3 -3
package/dist/v1.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
|
|
2
2
|
import { JSON_MAX_STRING_LENGTH, JSON_MAX_VALUES, parseJsonObject, parseJsonValue, } from "@mcp-native/core";
|
|
3
3
|
import { ISO_4217_CURRENCY_CODES } from "./iso-4217.js";
|
|
4
|
+
import { A2UI_V1_NATIVE_ICON_NAMES } from "./component-adapters.js";
|
|
4
5
|
export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
|
|
5
6
|
"Button",
|
|
6
7
|
"Card",
|
|
8
|
+
"CheckBox",
|
|
9
|
+
"ChoicePicker",
|
|
7
10
|
"Column",
|
|
11
|
+
"DateTimeInput",
|
|
12
|
+
"Divider",
|
|
13
|
+
"Icon",
|
|
14
|
+
"Image",
|
|
8
15
|
"List",
|
|
16
|
+
"Modal",
|
|
9
17
|
"Row",
|
|
18
|
+
"Slider",
|
|
19
|
+
"Tabs",
|
|
10
20
|
"Text",
|
|
11
21
|
"TextField",
|
|
12
22
|
]);
|
|
@@ -14,6 +24,21 @@ export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
|
|
|
14
24
|
export const A2UI_V1_NATIVE_MAX_RENDER_NODES = A2UI_V1_MAX_COMPONENTS;
|
|
15
25
|
/** Maximum canonical HTTP(S) URL retained for one supported local action. */
|
|
16
26
|
export const A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH = 8_192;
|
|
27
|
+
/** Maximum canonical HTTP(S) image URL retained for one image component. */
|
|
28
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH = 8_192;
|
|
29
|
+
/** Maximum choice options expanded into trusted host props in one render pass. */
|
|
30
|
+
export const A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS = 1_024;
|
|
31
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS = 64;
|
|
32
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_BYTES = 104_857_600;
|
|
33
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION = 16_384;
|
|
34
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_PIXELS = 268_435_456;
|
|
35
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS = 10;
|
|
36
|
+
/** Maximum reachable Image instances in one expanded surface. Checked before host policy calls. */
|
|
37
|
+
export const A2UI_V1_NATIVE_MAX_IMAGES = 64;
|
|
38
|
+
/** Maximum sum of host-granted transfer bytes across one expanded render plan. */
|
|
39
|
+
export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES = A2UI_V1_NATIVE_MAX_IMAGE_BYTES;
|
|
40
|
+
/** Maximum sum of host-granted decoded pixels across one expanded render plan. */
|
|
41
|
+
export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS = A2UI_V1_NATIVE_MAX_IMAGE_PIXELS;
|
|
17
42
|
/**
|
|
18
43
|
* Converts a policy-validated A2UI v1 surface into the existing host-owned
|
|
19
44
|
* native render plan. Unsupported renderer semantics fail closed.
|
|
@@ -27,7 +52,7 @@ export function createA2uiV1NativeRenderPlanForLocalEdits(surface, policy, optio
|
|
|
27
52
|
}
|
|
28
53
|
function createNativeRenderPlan(surface, policy, options, tolerateInvalidLocalOpenUrls) {
|
|
29
54
|
const parsedOptions = parseRenderPlanOptions(options);
|
|
30
|
-
const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls);
|
|
55
|
+
const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls, parsedOptions.imagePolicy);
|
|
31
56
|
return adaptComponent("root", "root", context, undefined);
|
|
32
57
|
}
|
|
33
58
|
/** Resolves one validated event against the latest renderer-local data model. */
|
|
@@ -36,7 +61,7 @@ export function resolveA2uiV1NativeEvent(surface, policy, sourceComponentId, dat
|
|
|
36
61
|
throw new A2uiParseError("Expected a non-empty A2UI source component id");
|
|
37
62
|
}
|
|
38
63
|
const parsedOptions = parseEventResolutionOptions(options);
|
|
39
|
-
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
|
|
64
|
+
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, false);
|
|
40
65
|
const plan = adaptComponent("root", "root", context, undefined);
|
|
41
66
|
const events = findNativeEvents(plan, sourceComponentId, parsedOptions.instanceKey);
|
|
42
67
|
if (events.length === 0) {
|
|
@@ -57,7 +82,7 @@ export function resolveA2uiV1NativeOpenUrl(surface, policy, sourceComponentId, d
|
|
|
57
82
|
throw new A2uiParseError("Expected a non-empty A2UI openUrl source component id");
|
|
58
83
|
}
|
|
59
84
|
const parsedOptions = parseOpenUrlResolutionOptions(options);
|
|
60
|
-
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
|
|
85
|
+
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, false);
|
|
61
86
|
const plan = adaptComponent("root", "root", context, undefined);
|
|
62
87
|
const openUrls = findNativeOpenUrls(plan, sourceComponentId, parsedOptions.instanceKey);
|
|
63
88
|
if (openUrls.length === 0) {
|
|
@@ -98,7 +123,7 @@ function findNativeOpenUrls(element, sourceComponentId, instanceKey, includeDisa
|
|
|
98
123
|
}
|
|
99
124
|
return openUrls;
|
|
100
125
|
}
|
|
101
|
-
function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false) {
|
|
126
|
+
function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false, imagePolicy, authorizeImageResources = true) {
|
|
102
127
|
const localDataModel = dataModel === undefined
|
|
103
128
|
? parseJsonObject(surface.dataModel, "surface.dataModel")
|
|
104
129
|
: parseJsonObject(dataModel, "options.dataModel");
|
|
@@ -112,9 +137,19 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
|
|
|
112
137
|
pluralRules: new Map(),
|
|
113
138
|
visiting: new Set(),
|
|
114
139
|
tolerateInvalidLocalOpenUrls,
|
|
140
|
+
imagePolicy,
|
|
141
|
+
authorizeImageResources,
|
|
142
|
+
choiceOptionCount: 0,
|
|
143
|
+
choiceOptionOutputLength: 0,
|
|
115
144
|
formatStringExpressionCount: 0,
|
|
116
145
|
formattedStringLength: 0,
|
|
117
146
|
openUrlLength: 0,
|
|
147
|
+
imageUrlLength: 0,
|
|
148
|
+
imageCount: 0,
|
|
149
|
+
imageMaximumBytes: 0,
|
|
150
|
+
imageMaximumDecodedPixels: 0,
|
|
151
|
+
imageRedirectOriginCount: 0,
|
|
152
|
+
imageResourcePolicyOutputLength: 0,
|
|
118
153
|
renderNodeCount: 0,
|
|
119
154
|
validationCheckCount: 0,
|
|
120
155
|
validationOutputLength: 0,
|
|
@@ -123,8 +158,10 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
|
|
|
123
158
|
function parseRenderPlanOptions(options) {
|
|
124
159
|
const parsed = parseOptionsObject(options, "A2UI native render plan options", [
|
|
125
160
|
"dataModel",
|
|
161
|
+
"imagePolicy",
|
|
126
162
|
"locale",
|
|
127
163
|
]);
|
|
164
|
+
const imagePolicy = parseOptionalImagePolicy(parsed.imagePolicy, "options.imagePolicy");
|
|
128
165
|
return {
|
|
129
166
|
...(parsed.dataModel === undefined
|
|
130
167
|
? {}
|
|
@@ -132,6 +169,7 @@ function parseRenderPlanOptions(options) {
|
|
|
132
169
|
...(parsed.locale === undefined
|
|
133
170
|
? {}
|
|
134
171
|
: { locale: parseLocale(parsed.locale, "options.locale") }),
|
|
172
|
+
...(imagePolicy === undefined ? {} : { imagePolicy }),
|
|
135
173
|
};
|
|
136
174
|
}
|
|
137
175
|
function parseEventResolutionOptions(options) {
|
|
@@ -166,6 +204,12 @@ function parseOpenUrlResolutionOptions(options) {
|
|
|
166
204
|
: { locale: parseLocale(parsed.locale, "options.locale") }),
|
|
167
205
|
};
|
|
168
206
|
}
|
|
207
|
+
function parseOptionalImagePolicy(value, path) {
|
|
208
|
+
if (value !== undefined && typeof value !== "function") {
|
|
209
|
+
throw new A2uiParseError(`Expected a function at ${path}`);
|
|
210
|
+
}
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
169
213
|
function parseOptionsObject(value, label, allowedKeys) {
|
|
170
214
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
171
215
|
throw new A2uiParseError(`Expected ${label} to be an object`);
|
|
@@ -227,10 +271,28 @@ function adaptComponent(id, key, context, scope) {
|
|
|
227
271
|
return adaptCard(component, key, context, scope);
|
|
228
272
|
case "Text":
|
|
229
273
|
return adaptText(component, key, context, scope);
|
|
274
|
+
case "Image":
|
|
275
|
+
return adaptImage(component, key, context, scope);
|
|
276
|
+
case "Icon":
|
|
277
|
+
return adaptIcon(component, key, context, scope);
|
|
278
|
+
case "Divider":
|
|
279
|
+
return adaptDivider(component, key, context, scope);
|
|
230
280
|
case "Button":
|
|
231
281
|
return adaptButton(component, key, context, scope);
|
|
232
282
|
case "TextField":
|
|
233
283
|
return adaptTextField(component, key, context, scope);
|
|
284
|
+
case "CheckBox":
|
|
285
|
+
return adaptCheckBox(component, key, context, scope);
|
|
286
|
+
case "ChoicePicker":
|
|
287
|
+
return adaptChoicePicker(component, key, context, scope);
|
|
288
|
+
case "Slider":
|
|
289
|
+
return adaptSlider(component, key, context, scope);
|
|
290
|
+
case "DateTimeInput":
|
|
291
|
+
return adaptDateTimeInput(component, key, context, scope);
|
|
292
|
+
case "Tabs":
|
|
293
|
+
return adaptTabs(component, key, context, scope);
|
|
294
|
+
case "Modal":
|
|
295
|
+
return adaptModal(component, key, context, scope);
|
|
234
296
|
default:
|
|
235
297
|
throw new A2uiParseError(`A2UI component ${JSON.stringify(id)} uses ${JSON.stringify(component.component)}, which the native adapter does not support`);
|
|
236
298
|
}
|
|
@@ -317,6 +379,85 @@ function adaptText(component, key, context, scope) {
|
|
|
317
379
|
addCommonProps(component, props, context, scope);
|
|
318
380
|
return { key, component: "Text", props };
|
|
319
381
|
}
|
|
382
|
+
function adaptImage(component, key, context, scope) {
|
|
383
|
+
context.imageCount += 1;
|
|
384
|
+
if (context.imageCount > A2UI_V1_NATIVE_MAX_IMAGES) {
|
|
385
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_IMAGES} images`);
|
|
386
|
+
}
|
|
387
|
+
const path = `components.${component.id}.url`;
|
|
388
|
+
const url = normalizeHttpUrl(resolveDynamicString(component.url, path, context, scope), path, "image", A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH);
|
|
389
|
+
context.imageUrlLength += url.length;
|
|
390
|
+
if (context.imageUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
391
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image URL length of ${A2UI_V1_MAX_SOURCE_LENGTH} at ${path}`);
|
|
392
|
+
}
|
|
393
|
+
const props = {
|
|
394
|
+
uri: url,
|
|
395
|
+
fit: component.fit ?? "fill",
|
|
396
|
+
variant: component.variant ?? "mediumFeature",
|
|
397
|
+
};
|
|
398
|
+
if (context.authorizeImageResources) {
|
|
399
|
+
const request = {
|
|
400
|
+
url,
|
|
401
|
+
surfaceId: context.surface.surfaceId,
|
|
402
|
+
sourceComponentId: component.id,
|
|
403
|
+
instanceKey: key,
|
|
404
|
+
};
|
|
405
|
+
if (context.imagePolicy === undefined) {
|
|
406
|
+
throw new A2uiParseError(`A2UI Image ${JSON.stringify(component.id)} requires an explicit host image policy`);
|
|
407
|
+
}
|
|
408
|
+
let decision;
|
|
409
|
+
try {
|
|
410
|
+
decision = context.imagePolicy(request);
|
|
411
|
+
}
|
|
412
|
+
catch (cause) {
|
|
413
|
+
throw new A2uiParseError(`A2UI Image ${JSON.stringify(component.id)} host policy failed`, {
|
|
414
|
+
cause,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (decision === false) {
|
|
418
|
+
throw new A2uiParseError(`A2UI Image ${JSON.stringify(component.id)} URL is denied by host policy`);
|
|
419
|
+
}
|
|
420
|
+
const resourcePolicy = parseImageResourcePolicy(decision, `components.${component.id}.imagePolicy`, context);
|
|
421
|
+
context.imageMaximumBytes += resourcePolicy.maximumBytes;
|
|
422
|
+
if (context.imageMaximumBytes > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES) {
|
|
423
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total image transfer budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES} bytes`);
|
|
424
|
+
}
|
|
425
|
+
context.imageMaximumDecodedPixels += resourcePolicy.maximumDecodedPixels;
|
|
426
|
+
if (context.imageMaximumDecodedPixels > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS) {
|
|
427
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total decoded image budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS} pixels`);
|
|
428
|
+
}
|
|
429
|
+
props.resourcePolicy = resourcePolicy;
|
|
430
|
+
}
|
|
431
|
+
if (component.description !== undefined) {
|
|
432
|
+
props.description = resolveDynamicString(component.description, `components.${component.id}.description`, context, scope);
|
|
433
|
+
}
|
|
434
|
+
addCommonProps(component, props, context, scope);
|
|
435
|
+
if (props.accessibilityLabel === undefined && props.description !== undefined) {
|
|
436
|
+
props.accessibilityLabel = props.description;
|
|
437
|
+
}
|
|
438
|
+
return { key, component: "Image", props };
|
|
439
|
+
}
|
|
440
|
+
function adaptIcon(component, key, context, scope) {
|
|
441
|
+
const path = `components.${component.id}.name`;
|
|
442
|
+
if (component.name !== null &&
|
|
443
|
+
typeof component.name === "object" &&
|
|
444
|
+
!Array.isArray(component.name) &&
|
|
445
|
+
Object.hasOwn(component.name, "svgPath")) {
|
|
446
|
+
throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
|
|
447
|
+
}
|
|
448
|
+
const name = resolveDynamicString(component.name, path, context, scope);
|
|
449
|
+
if (!A2UI_V1_NATIVE_ICON_NAMES.includes(name)) {
|
|
450
|
+
throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
|
|
451
|
+
}
|
|
452
|
+
const props = { name };
|
|
453
|
+
addCommonProps(component, props, context, scope);
|
|
454
|
+
return { key, component: "Icon", props };
|
|
455
|
+
}
|
|
456
|
+
function adaptDivider(component, key, context, scope) {
|
|
457
|
+
const props = { axis: component.axis ?? "horizontal" };
|
|
458
|
+
addCommonProps(component, props, context, scope);
|
|
459
|
+
return { key, component: "Divider", props };
|
|
460
|
+
}
|
|
320
461
|
function adaptButton(component, key, context, scope) {
|
|
321
462
|
const childId = expectString(component.child, `components.${component.id}.child`);
|
|
322
463
|
const child = context.surface.components.get(childId);
|
|
@@ -364,6 +505,244 @@ function adaptTextField(component, key, context, scope) {
|
|
|
364
505
|
}
|
|
365
506
|
return { key, component: "TextInput", props };
|
|
366
507
|
}
|
|
508
|
+
function adaptCheckBox(component, key, context, scope) {
|
|
509
|
+
const componentPath = `components.${component.id}`;
|
|
510
|
+
const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
511
|
+
const props = {
|
|
512
|
+
label,
|
|
513
|
+
value: resolveDynamicBoolean(component.value, `${componentPath}.value`, context, scope),
|
|
514
|
+
};
|
|
515
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
516
|
+
addCommonProps(component, props, context, scope);
|
|
517
|
+
addValidationProps(component, props, context, scope, "input");
|
|
518
|
+
if (props.accessibilityLabel === undefined) {
|
|
519
|
+
props.accessibilityLabel = label;
|
|
520
|
+
}
|
|
521
|
+
return { key, component: "CheckBox", props };
|
|
522
|
+
}
|
|
523
|
+
function adaptChoicePicker(component, key, context, scope) {
|
|
524
|
+
const componentPath = `components.${component.id}`;
|
|
525
|
+
if (!Array.isArray(component.options)) {
|
|
526
|
+
throw new A2uiParseError(`Expected an array at ${componentPath}.options`);
|
|
527
|
+
}
|
|
528
|
+
context.choiceOptionCount += component.options.length;
|
|
529
|
+
if (context.choiceOptionCount > A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS) {
|
|
530
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS} choice options`);
|
|
531
|
+
}
|
|
532
|
+
const optionValues = new Set();
|
|
533
|
+
const options = component.options.map((value, index) => {
|
|
534
|
+
const option = expectObject(value, `${componentPath}.options[${index}]`);
|
|
535
|
+
const label = resolveDynamicString(option.label, `${componentPath}.options[${index}].label`, context, scope);
|
|
536
|
+
const optionValue = expectString(option.value, `${componentPath}.options[${index}].value`);
|
|
537
|
+
if (optionValues.has(optionValue)) {
|
|
538
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate option value ${JSON.stringify(optionValue)}`);
|
|
539
|
+
}
|
|
540
|
+
optionValues.add(optionValue);
|
|
541
|
+
context.choiceOptionOutputLength += label.length + optionValue.length;
|
|
542
|
+
if (context.choiceOptionOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
543
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum choice-option output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
544
|
+
}
|
|
545
|
+
return Object.freeze({ label, value: optionValue });
|
|
546
|
+
});
|
|
547
|
+
const selected = resolveDynamicStringList(component.value, `${componentPath}.value`, context, scope);
|
|
548
|
+
if (new Set(selected).size !== selected.length) {
|
|
549
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate selected values`);
|
|
550
|
+
}
|
|
551
|
+
for (const value of selected) {
|
|
552
|
+
if (!optionValues.has(value)) {
|
|
553
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} selects unknown value ${JSON.stringify(value)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const variant = component.variant ?? "mutuallyExclusive";
|
|
557
|
+
if (variant === "mutuallyExclusive" && selected.length > 1) {
|
|
558
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} allows at most one selected value`);
|
|
559
|
+
}
|
|
560
|
+
const props = {
|
|
561
|
+
options: Object.freeze(options),
|
|
562
|
+
value: Object.freeze(selected),
|
|
563
|
+
variant,
|
|
564
|
+
displayStyle: component.displayStyle ?? "checkbox",
|
|
565
|
+
filterable: component.filterable ?? false,
|
|
566
|
+
};
|
|
567
|
+
if (component.label !== undefined) {
|
|
568
|
+
props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
569
|
+
}
|
|
570
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
571
|
+
addCommonProps(component, props, context, scope);
|
|
572
|
+
addValidationProps(component, props, context, scope, "input");
|
|
573
|
+
if (props.accessibilityLabel === undefined) {
|
|
574
|
+
if (props.label === undefined) {
|
|
575
|
+
throw new A2uiParseError(`A2UI native ChoicePicker ${JSON.stringify(component.id)} requires label or accessibility.label`);
|
|
576
|
+
}
|
|
577
|
+
props.accessibilityLabel = props.label;
|
|
578
|
+
}
|
|
579
|
+
return { key, component: "ChoicePicker", props };
|
|
580
|
+
}
|
|
581
|
+
function adaptSlider(component, key, context, scope) {
|
|
582
|
+
const componentPath = `components.${component.id}`;
|
|
583
|
+
const minimum = component.min === undefined ? 0 : expectFiniteNumber(component.min, `${componentPath}.min`);
|
|
584
|
+
const maximum = expectFiniteNumber(component.max, `${componentPath}.max`);
|
|
585
|
+
if (minimum >= maximum) {
|
|
586
|
+
throw new A2uiParseError(`Expected ${componentPath}.min to be less than max`);
|
|
587
|
+
}
|
|
588
|
+
const value = resolveDynamicNumber(component.value, `${componentPath}.value`, context, scope);
|
|
589
|
+
if (value < minimum || value > maximum) {
|
|
590
|
+
throw new A2uiParseError(`Expected ${componentPath}.value to be within min and max`);
|
|
591
|
+
}
|
|
592
|
+
let step;
|
|
593
|
+
if (component.steps !== undefined) {
|
|
594
|
+
const steps = expectFiniteNumber(component.steps, `${componentPath}.steps`);
|
|
595
|
+
if (!Number.isInteger(steps) || steps < 1 || steps > JSON_MAX_VALUES) {
|
|
596
|
+
throw new A2uiParseError(`Expected ${componentPath}.steps to be an integer from 1 through ${JSON_MAX_VALUES}`);
|
|
597
|
+
}
|
|
598
|
+
step = (maximum - minimum) / steps;
|
|
599
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
600
|
+
throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} has invalid range`);
|
|
601
|
+
}
|
|
602
|
+
if (!isSliderStepValue(value, minimum, step)) {
|
|
603
|
+
throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} value does not align with its steps`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const props = {
|
|
607
|
+
value,
|
|
608
|
+
minimum,
|
|
609
|
+
maximum,
|
|
610
|
+
...(step === undefined ? {} : { step }),
|
|
611
|
+
};
|
|
612
|
+
if (component.label !== undefined) {
|
|
613
|
+
props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
614
|
+
}
|
|
615
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
616
|
+
addCommonProps(component, props, context, scope);
|
|
617
|
+
addValidationProps(component, props, context, scope, "input");
|
|
618
|
+
if (props.accessibilityLabel === undefined) {
|
|
619
|
+
if (props.label === undefined) {
|
|
620
|
+
throw new A2uiParseError(`A2UI native Slider ${JSON.stringify(component.id)} requires label or accessibility.label`);
|
|
621
|
+
}
|
|
622
|
+
props.accessibilityLabel = props.label;
|
|
623
|
+
}
|
|
624
|
+
return { key, component: "Slider", props };
|
|
625
|
+
}
|
|
626
|
+
function isSliderStepValue(value, minimum, step) {
|
|
627
|
+
const offset = (value - minimum) / step;
|
|
628
|
+
return (Math.abs(offset - Math.round(offset)) <= Number.EPSILON * Math.max(1, Math.abs(offset)) * 8);
|
|
629
|
+
}
|
|
630
|
+
function adaptDateTimeInput(component, key, context, scope) {
|
|
631
|
+
const componentPath = `components.${component.id}`;
|
|
632
|
+
const enableDate = component.enableDate === true;
|
|
633
|
+
const enableTime = component.enableTime === true;
|
|
634
|
+
if (!enableDate && !enableTime) {
|
|
635
|
+
throw new A2uiParseError(`A2UI DateTimeInput ${JSON.stringify(component.id)} must enable date, time, or both`);
|
|
636
|
+
}
|
|
637
|
+
const value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
|
|
638
|
+
validateDateTimeInputValue(value, enableDate, enableTime, `${componentPath}.value`, true);
|
|
639
|
+
const minimum = component.min === undefined
|
|
640
|
+
? undefined
|
|
641
|
+
: resolveDynamicString(component.min, `${componentPath}.min`, context, scope);
|
|
642
|
+
const maximum = component.max === undefined
|
|
643
|
+
? undefined
|
|
644
|
+
: resolveDynamicString(component.max, `${componentPath}.max`, context, scope);
|
|
645
|
+
if (minimum !== undefined) {
|
|
646
|
+
validateDateTimeInputValue(minimum, enableDate, enableTime, `${componentPath}.min`, false);
|
|
647
|
+
}
|
|
648
|
+
if (maximum !== undefined) {
|
|
649
|
+
validateDateTimeInputValue(maximum, enableDate, enableTime, `${componentPath}.max`, false);
|
|
650
|
+
}
|
|
651
|
+
if (minimum !== undefined &&
|
|
652
|
+
maximum !== undefined &&
|
|
653
|
+
dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`) >
|
|
654
|
+
dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
|
|
655
|
+
throw new A2uiParseError(`Expected ${componentPath}.min not to exceed max`);
|
|
656
|
+
}
|
|
657
|
+
const valueSort = value.length === 0
|
|
658
|
+
? undefined
|
|
659
|
+
: dateTimeInputSortValue(value, enableDate, enableTime, `${componentPath}.value`);
|
|
660
|
+
if (valueSort !== undefined &&
|
|
661
|
+
minimum !== undefined &&
|
|
662
|
+
valueSort < dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`)) {
|
|
663
|
+
throw new A2uiParseError(`Expected ${componentPath}.value not to be earlier than min`);
|
|
664
|
+
}
|
|
665
|
+
if (valueSort !== undefined &&
|
|
666
|
+
maximum !== undefined &&
|
|
667
|
+
valueSort > dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
|
|
668
|
+
throw new A2uiParseError(`Expected ${componentPath}.value not to be later than max`);
|
|
669
|
+
}
|
|
670
|
+
const props = {
|
|
671
|
+
value,
|
|
672
|
+
enableDate,
|
|
673
|
+
enableTime,
|
|
674
|
+
...(minimum === undefined ? {} : { minimum }),
|
|
675
|
+
...(maximum === undefined ? {} : { maximum }),
|
|
676
|
+
};
|
|
677
|
+
if (component.label !== undefined) {
|
|
678
|
+
props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
679
|
+
}
|
|
680
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
681
|
+
addCommonProps(component, props, context, scope);
|
|
682
|
+
addValidationProps(component, props, context, scope, "input");
|
|
683
|
+
if (props.accessibilityLabel === undefined) {
|
|
684
|
+
if (props.label === undefined) {
|
|
685
|
+
throw new A2uiParseError(`A2UI native DateTimeInput ${JSON.stringify(component.id)} requires label or accessibility.label`);
|
|
686
|
+
}
|
|
687
|
+
props.accessibilityLabel = props.label;
|
|
688
|
+
}
|
|
689
|
+
return { key, component: "DateTimeInput", props };
|
|
690
|
+
}
|
|
691
|
+
function adaptTabs(component, key, context, scope) {
|
|
692
|
+
const componentPath = `components.${component.id}`;
|
|
693
|
+
if (!Array.isArray(component.tabs) || component.tabs.length === 0) {
|
|
694
|
+
throw new A2uiParseError(`Expected a non-empty array at ${componentPath}.tabs`);
|
|
695
|
+
}
|
|
696
|
+
const tabs = component.tabs.map((value, index) => {
|
|
697
|
+
const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
|
|
698
|
+
return Object.freeze({
|
|
699
|
+
title: resolveDynamicString(tab.title, `${componentPath}.tabs[${index}].title`, context, scope),
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
const props = { tabs: Object.freeze(tabs) };
|
|
703
|
+
addCommonProps(component, props, context, scope);
|
|
704
|
+
return {
|
|
705
|
+
key,
|
|
706
|
+
component: "Tabs",
|
|
707
|
+
props,
|
|
708
|
+
children: component.tabs.map((value, index) => {
|
|
709
|
+
const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
|
|
710
|
+
const childId = expectString(tab.child, `${componentPath}.tabs[${index}].child`);
|
|
711
|
+
return adaptComponent(childId, appendInstanceKey(key, childId, index), context, scope);
|
|
712
|
+
}),
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
function adaptModal(component, key, context, scope) {
|
|
716
|
+
const componentPath = `components.${component.id}`;
|
|
717
|
+
const triggerId = expectString(component.trigger, `${componentPath}.trigger`);
|
|
718
|
+
const contentId = expectString(component.content, `${componentPath}.content`);
|
|
719
|
+
if (context.surface.components.get(triggerId)?.component !== "Button") {
|
|
720
|
+
throw new A2uiParseError(`A2UI native Modal ${JSON.stringify(component.id)} requires a Button trigger`);
|
|
721
|
+
}
|
|
722
|
+
const props = {};
|
|
723
|
+
addCommonProps(component, props, context, scope);
|
|
724
|
+
return {
|
|
725
|
+
key,
|
|
726
|
+
component: "Modal",
|
|
727
|
+
props,
|
|
728
|
+
children: [
|
|
729
|
+
adaptComponent(triggerId, appendInstanceKey(key, triggerId, 0), context, scope),
|
|
730
|
+
adaptComponent(contentId, appendInstanceKey(key, contentId, 1), context, scope),
|
|
731
|
+
],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
function addBindingProp(value, path, props, scope) {
|
|
735
|
+
if (value !== undefined && isBinding(value)) {
|
|
736
|
+
props.binding = resolveBindingPointer(value.path, `${path}.path`, scope);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function resolveDynamicStringList(value, path, context, scope) {
|
|
740
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
741
|
+
if (!Array.isArray(resolved) || resolved.some((item) => typeof item !== "string")) {
|
|
742
|
+
throw new A2uiParseError(`Expected an array of strings at ${path}`);
|
|
743
|
+
}
|
|
744
|
+
return [...resolved];
|
|
745
|
+
}
|
|
367
746
|
function addValidationProps(component, props, context, scope, target) {
|
|
368
747
|
if (component.checks === undefined) {
|
|
369
748
|
return;
|
|
@@ -608,15 +987,18 @@ function resolveButtonAction(component, key, context, scope) {
|
|
|
608
987
|
};
|
|
609
988
|
}
|
|
610
989
|
function normalizeOpenUrl(value, path) {
|
|
611
|
-
|
|
612
|
-
|
|
990
|
+
return normalizeHttpUrl(value, path, "openUrl", A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH);
|
|
991
|
+
}
|
|
992
|
+
function normalizeHttpUrl(value, path, label, maximumLength) {
|
|
993
|
+
if (value.length === 0 || value.length > maximumLength) {
|
|
994
|
+
throw new A2uiParseError(`Expected an HTTP(S) URL up to ${maximumLength} characters at ${path}`);
|
|
613
995
|
}
|
|
614
996
|
if (/\s|\p{Cf}/u.test(value) || hasAsciiControlCharacter(value)) {
|
|
615
997
|
throw new A2uiParseError(`Expected an HTTP(S) URL without whitespace, control, or Unicode format characters at ${path}`);
|
|
616
998
|
}
|
|
617
999
|
const UrlConstructor = globalThis.URL;
|
|
618
1000
|
if (UrlConstructor === undefined) {
|
|
619
|
-
throw new A2uiParseError(`The host runtime cannot validate an
|
|
1001
|
+
throw new A2uiParseError(`The host runtime cannot validate an ${label} value at ${path}`);
|
|
620
1002
|
}
|
|
621
1003
|
let parsed;
|
|
622
1004
|
try {
|
|
@@ -629,13 +1011,105 @@ function normalizeOpenUrl(value, path) {
|
|
|
629
1011
|
throw new A2uiParseError(`Expected an HTTP(S) URL at ${path}`);
|
|
630
1012
|
}
|
|
631
1013
|
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
632
|
-
throw new A2uiParseError(`A2UI
|
|
1014
|
+
throw new A2uiParseError(`A2UI ${label} does not allow URL credentials at ${path}`);
|
|
633
1015
|
}
|
|
634
|
-
if (parsed.href.length >
|
|
635
|
-
throw new A2uiParseError(`Canonical A2UI
|
|
1016
|
+
if (parsed.href.length > maximumLength) {
|
|
1017
|
+
throw new A2uiParseError(`Canonical A2UI ${label} at ${path} exceeds maximum length of ${maximumLength}`);
|
|
636
1018
|
}
|
|
637
1019
|
return parsed.href;
|
|
638
1020
|
}
|
|
1021
|
+
function parseImageResourcePolicy(value, path, context) {
|
|
1022
|
+
const policy = parseJsonObject(value, path);
|
|
1023
|
+
const allowedKeys = new Set([
|
|
1024
|
+
"allowedRedirectOrigins",
|
|
1025
|
+
"cacheMode",
|
|
1026
|
+
"maximumBytes",
|
|
1027
|
+
"maximumDecodedHeight",
|
|
1028
|
+
"maximumDecodedPixels",
|
|
1029
|
+
"maximumDecodedWidth",
|
|
1030
|
+
"maximumRedirects",
|
|
1031
|
+
]);
|
|
1032
|
+
for (const key of Object.keys(policy)) {
|
|
1033
|
+
if (!allowedKeys.has(key)) {
|
|
1034
|
+
throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
if (!Array.isArray(policy.allowedRedirectOrigins) ||
|
|
1038
|
+
policy.allowedRedirectOrigins.length > A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS) {
|
|
1039
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS} redirect origins at ${path}.allowedRedirectOrigins`);
|
|
1040
|
+
}
|
|
1041
|
+
context.imageRedirectOriginCount += policy.allowedRedirectOrigins.length;
|
|
1042
|
+
if (context.imageRedirectOriginCount > JSON_MAX_VALUES) {
|
|
1043
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} image redirect origins`);
|
|
1044
|
+
}
|
|
1045
|
+
const origins = policy.allowedRedirectOrigins.map((origin, index) => parseExactHttpOrigin(origin, `${path}.allowedRedirectOrigins[${index}]`));
|
|
1046
|
+
if (new Set(origins).size !== origins.length) {
|
|
1047
|
+
throw new A2uiParseError(`Expected unique redirect origins at ${path}.allowedRedirectOrigins`);
|
|
1048
|
+
}
|
|
1049
|
+
context.imageResourcePolicyOutputLength += origins.reduce((length, origin) => length + origin.length, 0);
|
|
1050
|
+
if (context.imageResourcePolicyOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
1051
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image-policy output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
1052
|
+
}
|
|
1053
|
+
const cacheMode = policy.cacheMode;
|
|
1054
|
+
if (cacheMode !== "default" && cacheMode !== "no-store") {
|
|
1055
|
+
throw new A2uiParseError(`Expected a closed cache mode at ${path}.cacheMode`);
|
|
1056
|
+
}
|
|
1057
|
+
const maximumBytes = parseBoundedPositiveInteger(policy.maximumBytes, A2UI_V1_NATIVE_MAX_IMAGE_BYTES, `${path}.maximumBytes`);
|
|
1058
|
+
const maximumDecodedWidth = parseBoundedPositiveInteger(policy.maximumDecodedWidth, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedWidth`);
|
|
1059
|
+
const maximumDecodedHeight = parseBoundedPositiveInteger(policy.maximumDecodedHeight, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedHeight`);
|
|
1060
|
+
const maximumDecodedPixels = parseBoundedPositiveInteger(policy.maximumDecodedPixels, A2UI_V1_NATIVE_MAX_IMAGE_PIXELS, `${path}.maximumDecodedPixels`);
|
|
1061
|
+
if (maximumDecodedPixels > maximumDecodedWidth * maximumDecodedHeight) {
|
|
1062
|
+
throw new A2uiParseError(`Expected ${path}.maximumDecodedPixels not to exceed the declared dimensions`);
|
|
1063
|
+
}
|
|
1064
|
+
const maximumRedirects = parseBoundedNonNegativeInteger(policy.maximumRedirects, A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS, `${path}.maximumRedirects`);
|
|
1065
|
+
return Object.freeze({
|
|
1066
|
+
allowedRedirectOrigins: Object.freeze(origins),
|
|
1067
|
+
cacheMode,
|
|
1068
|
+
maximumBytes,
|
|
1069
|
+
maximumDecodedHeight,
|
|
1070
|
+
maximumDecodedPixels,
|
|
1071
|
+
maximumDecodedWidth,
|
|
1072
|
+
maximumRedirects,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
function parseExactHttpOrigin(value, path) {
|
|
1076
|
+
const origin = expectString(value, path);
|
|
1077
|
+
if (/\s|\p{Cf}/u.test(origin) || hasAsciiControlCharacter(origin)) {
|
|
1078
|
+
throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
|
|
1079
|
+
}
|
|
1080
|
+
const UrlConstructor = globalThis.URL;
|
|
1081
|
+
if (UrlConstructor === undefined) {
|
|
1082
|
+
throw new A2uiParseError(`The host runtime cannot validate an image origin at ${path}`);
|
|
1083
|
+
}
|
|
1084
|
+
let parsed;
|
|
1085
|
+
try {
|
|
1086
|
+
parsed = new UrlConstructor(origin);
|
|
1087
|
+
}
|
|
1088
|
+
catch (cause) {
|
|
1089
|
+
throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`, { cause });
|
|
1090
|
+
}
|
|
1091
|
+
if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
|
|
1092
|
+
parsed.username.length > 0 ||
|
|
1093
|
+
parsed.password.length > 0 ||
|
|
1094
|
+
parsed.origin !== origin) {
|
|
1095
|
+
throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
|
|
1096
|
+
}
|
|
1097
|
+
return parsed.origin;
|
|
1098
|
+
}
|
|
1099
|
+
function parseBoundedPositiveInteger(value, maximum, path) {
|
|
1100
|
+
const result = expectFiniteNumber(value, path);
|
|
1101
|
+
if (!Number.isInteger(result) || result < 1 || result > maximum) {
|
|
1102
|
+
throw new A2uiParseError(`Expected an integer from 1 through ${maximum} at ${path}`);
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
function parseBoundedNonNegativeInteger(value, maximum, path) {
|
|
1107
|
+
const result = expectFiniteNumber(value, path);
|
|
1108
|
+
if (!Number.isInteger(result) || result < 0 || result > maximum) {
|
|
1109
|
+
throw new A2uiParseError(`Expected an integer from 0 through ${maximum} at ${path}`);
|
|
1110
|
+
}
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
639
1113
|
function hasAsciiControlCharacter(value) {
|
|
640
1114
|
for (let index = 0; index < value.length; index += 1) {
|
|
641
1115
|
const code = value.charCodeAt(index);
|
|
@@ -803,6 +1277,7 @@ function resolveNumberFormat(call, path, context, scope) {
|
|
|
803
1277
|
}
|
|
804
1278
|
const DATE_NUMBER = /^[+-]?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
805
1279
|
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
1280
|
+
const TIME_ONLY = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|([+-])(\d{2}):(\d{2}))?$/;
|
|
806
1281
|
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
1282
|
const A2UI_V1_MAX_DATE_PATTERN_TOKENS = 128;
|
|
808
1283
|
const DATE_PATTERN_TOKENS = Object.freeze([
|
|
@@ -825,6 +1300,98 @@ const DATE_PATTERN_TOKENS = Object.freeze([
|
|
|
825
1300
|
"a",
|
|
826
1301
|
]);
|
|
827
1302
|
const DATE_PATTERN_TOKEN_SET = new Set(DATE_PATTERN_TOKENS);
|
|
1303
|
+
function validateDateTimeInputValue(value, enableDate, enableTime, path, allowEmpty) {
|
|
1304
|
+
if (allowEmpty && value.length === 0) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (enableDate && enableTime) {
|
|
1308
|
+
parseDateValue(value, path);
|
|
1309
|
+
if (RFC_3339_TIMESTAMP.exec(value) === null) {
|
|
1310
|
+
throw new A2uiParseError(`Expected an RFC 3339 date-time at ${path}`);
|
|
1311
|
+
}
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
if (enableDate) {
|
|
1315
|
+
const match = DATE_ONLY.exec(value);
|
|
1316
|
+
if (match === null) {
|
|
1317
|
+
throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
|
|
1318
|
+
}
|
|
1319
|
+
validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
parseTimeOnly(value, path);
|
|
1323
|
+
}
|
|
1324
|
+
/** Internal mounted-renderer guard for values emitted by a trusted host date/time component. */
|
|
1325
|
+
export function validateA2uiV1NativeDateTimeInputChange(value, enableDate, enableTime, minimum, maximum, path) {
|
|
1326
|
+
if (!enableDate && !enableTime) {
|
|
1327
|
+
throw new A2uiParseError(`A2UI DateTimeInput at ${path} must enable date, time, or both`);
|
|
1328
|
+
}
|
|
1329
|
+
validateDateTimeInputValue(value, enableDate, enableTime, `${path}.value`, true);
|
|
1330
|
+
if (minimum !== undefined) {
|
|
1331
|
+
validateDateTimeInputValue(minimum, enableDate, enableTime, `${path}.minimum`, false);
|
|
1332
|
+
}
|
|
1333
|
+
if (maximum !== undefined) {
|
|
1334
|
+
validateDateTimeInputValue(maximum, enableDate, enableTime, `${path}.maximum`, false);
|
|
1335
|
+
}
|
|
1336
|
+
const minimumSort = minimum === undefined
|
|
1337
|
+
? undefined
|
|
1338
|
+
: dateTimeInputSortValue(minimum, enableDate, enableTime, `${path}.minimum`);
|
|
1339
|
+
const maximumSort = maximum === undefined
|
|
1340
|
+
? undefined
|
|
1341
|
+
: dateTimeInputSortValue(maximum, enableDate, enableTime, `${path}.maximum`);
|
|
1342
|
+
if (minimumSort !== undefined && maximumSort !== undefined && minimumSort > maximumSort) {
|
|
1343
|
+
throw new A2uiParseError(`Expected ${path}.minimum not to exceed maximum`);
|
|
1344
|
+
}
|
|
1345
|
+
if (value.length === 0) {
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
const valueSort = dateTimeInputSortValue(value, enableDate, enableTime, `${path}.value`);
|
|
1349
|
+
if (minimumSort !== undefined && valueSort < minimumSort) {
|
|
1350
|
+
throw new A2uiParseError(`Expected ${path}.value not to be earlier than minimum`);
|
|
1351
|
+
}
|
|
1352
|
+
if (maximumSort !== undefined && valueSort > maximumSort) {
|
|
1353
|
+
throw new A2uiParseError(`Expected ${path}.value not to be later than maximum`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
function dateTimeInputSortValue(value, enableDate, enableTime, path) {
|
|
1357
|
+
if (enableDate && enableTime) {
|
|
1358
|
+
return parseDateValue(value, path).getTime();
|
|
1359
|
+
}
|
|
1360
|
+
if (enableDate) {
|
|
1361
|
+
const match = DATE_ONLY.exec(value);
|
|
1362
|
+
if (match === null) {
|
|
1363
|
+
throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
|
|
1364
|
+
}
|
|
1365
|
+
validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
|
|
1366
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
1367
|
+
}
|
|
1368
|
+
return parseTimeOnly(value, path);
|
|
1369
|
+
}
|
|
1370
|
+
function parseTimeOnly(value, path) {
|
|
1371
|
+
const match = TIME_ONLY.exec(value);
|
|
1372
|
+
if (match === null) {
|
|
1373
|
+
throw new A2uiParseError(`Expected an ISO 8601 time at ${path}`);
|
|
1374
|
+
}
|
|
1375
|
+
const hour = Number(match[1]);
|
|
1376
|
+
const minute = Number(match[2]);
|
|
1377
|
+
const second = Number(match[3] ?? "0");
|
|
1378
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
1379
|
+
throw new A2uiParseError(`Invalid ISO 8601 time at ${path}`);
|
|
1380
|
+
}
|
|
1381
|
+
const fraction = match[4] ?? "";
|
|
1382
|
+
const millisecond = Number(fraction.slice(0, 3).padEnd(3, "0"));
|
|
1383
|
+
let result = ((hour * 60 + minute) * 60 + second) * 1_000 + millisecond;
|
|
1384
|
+
if (match[5] !== undefined && match[5] !== "Z") {
|
|
1385
|
+
const offsetHour = Number(match[7]);
|
|
1386
|
+
const offsetMinute = Number(match[8]);
|
|
1387
|
+
if (offsetHour > 23 || offsetMinute > 59) {
|
|
1388
|
+
throw new A2uiParseError(`Invalid ISO 8601 time offset at ${path}`);
|
|
1389
|
+
}
|
|
1390
|
+
const direction = match[6] === "+" ? 1 : -1;
|
|
1391
|
+
result -= direction * (offsetHour * 60 + offsetMinute) * 60_000;
|
|
1392
|
+
}
|
|
1393
|
+
return result;
|
|
1394
|
+
}
|
|
828
1395
|
function resolveDateFormat(call, path, context, scope) {
|
|
829
1396
|
const args = expectObject(call.args, `${path}.args`);
|
|
830
1397
|
const value = resolveDynamicValue(args.value, `${path}.args.value`, context, scope);
|