@mcp-native/react-native 0.6.0 → 0.8.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 +77 -44
- package/dist/component-adapters.d.ts +201 -3
- package/dist/component-adapters.d.ts.map +1 -1
- package/dist/component-adapters.js +143 -0
- package/dist/component-adapters.js.map +1 -1
- package/dist/index.d.ts +40 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +721 -24
- package/dist/index.js.map +1 -1
- package/dist/v1.d.ts +65 -0
- package/dist/v1.d.ts.map +1 -1
- package/dist/v1.js +876 -12
- package/dist/v1.js.map +1 -1
- package/package.json +4 -4
package/dist/v1.js
CHANGED
|
@@ -1,19 +1,53 @@
|
|
|
1
|
-
import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
|
|
1
|
+
import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, validateA2uiV1HostExtensionComponent, 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([
|
|
6
|
+
"AudioPlayer",
|
|
5
7
|
"Button",
|
|
6
8
|
"Card",
|
|
9
|
+
"CheckBox",
|
|
10
|
+
"ChoicePicker",
|
|
7
11
|
"Column",
|
|
12
|
+
"DateTimeInput",
|
|
13
|
+
"Divider",
|
|
14
|
+
"Icon",
|
|
15
|
+
"Image",
|
|
8
16
|
"List",
|
|
17
|
+
"Modal",
|
|
9
18
|
"Row",
|
|
19
|
+
"Slider",
|
|
20
|
+
"Tabs",
|
|
10
21
|
"Text",
|
|
11
22
|
"TextField",
|
|
23
|
+
"Video",
|
|
12
24
|
]);
|
|
13
25
|
/** Maximum expanded native-plan nodes, including repeated component references. */
|
|
14
26
|
export const A2UI_V1_NATIVE_MAX_RENDER_NODES = A2UI_V1_MAX_COMPONENTS;
|
|
15
27
|
/** Maximum canonical HTTP(S) URL retained for one supported local action. */
|
|
16
28
|
export const A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH = 8_192;
|
|
29
|
+
/** Maximum canonical HTTP(S) image URL retained for one image component. */
|
|
30
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH = 8_192;
|
|
31
|
+
/** Maximum choice options expanded into trusted host props in one render pass. */
|
|
32
|
+
export const A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS = 1_024;
|
|
33
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS = 64;
|
|
34
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_BYTES = 104_857_600;
|
|
35
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION = 16_384;
|
|
36
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_PIXELS = 268_435_456;
|
|
37
|
+
export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS = 10;
|
|
38
|
+
/** Maximum reachable Image instances in one expanded surface. Checked before host policy calls. */
|
|
39
|
+
export const A2UI_V1_NATIVE_MAX_IMAGES = 64;
|
|
40
|
+
/** Maximum sum of host-granted transfer bytes across one expanded render plan. */
|
|
41
|
+
export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES = A2UI_V1_NATIVE_MAX_IMAGE_BYTES;
|
|
42
|
+
/** Maximum sum of host-granted decoded pixels across one expanded render plan. */
|
|
43
|
+
export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS = A2UI_V1_NATIVE_MAX_IMAGE_PIXELS;
|
|
44
|
+
export const A2UI_V1_NATIVE_MAX_MEDIA = 16;
|
|
45
|
+
export const A2UI_V1_NATIVE_MAX_MEDIA_URL_LENGTH = 8_192;
|
|
46
|
+
export const A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES = 32;
|
|
47
|
+
export const A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS = 64;
|
|
48
|
+
export const A2UI_V1_NATIVE_MAX_MEDIA_REDIRECTS = 10;
|
|
49
|
+
export const A2UI_V1_NATIVE_MAX_MEDIA_BYTES = 2_147_483_648;
|
|
50
|
+
export const A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES = 2_147_483_648;
|
|
17
51
|
/**
|
|
18
52
|
* Converts a policy-validated A2UI v1 surface into the existing host-owned
|
|
19
53
|
* native render plan. Unsupported renderer semantics fail closed.
|
|
@@ -27,7 +61,7 @@ export function createA2uiV1NativeRenderPlanForLocalEdits(surface, policy, optio
|
|
|
27
61
|
}
|
|
28
62
|
function createNativeRenderPlan(surface, policy, options, tolerateInvalidLocalOpenUrls) {
|
|
29
63
|
const parsedOptions = parseRenderPlanOptions(options);
|
|
30
|
-
const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls);
|
|
64
|
+
const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls, parsedOptions.imagePolicy, parsedOptions.mediaPolicy, parsedOptions.hostExtensionPolicy);
|
|
31
65
|
return adaptComponent("root", "root", context, undefined);
|
|
32
66
|
}
|
|
33
67
|
/** Resolves one validated event against the latest renderer-local data model. */
|
|
@@ -36,7 +70,7 @@ export function resolveA2uiV1NativeEvent(surface, policy, sourceComponentId, dat
|
|
|
36
70
|
throw new A2uiParseError("Expected a non-empty A2UI source component id");
|
|
37
71
|
}
|
|
38
72
|
const parsedOptions = parseEventResolutionOptions(options);
|
|
39
|
-
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
|
|
73
|
+
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, undefined, undefined, false);
|
|
40
74
|
const plan = adaptComponent("root", "root", context, undefined);
|
|
41
75
|
const events = findNativeEvents(plan, sourceComponentId, parsedOptions.instanceKey);
|
|
42
76
|
if (events.length === 0) {
|
|
@@ -57,7 +91,7 @@ export function resolveA2uiV1NativeOpenUrl(surface, policy, sourceComponentId, d
|
|
|
57
91
|
throw new A2uiParseError("Expected a non-empty A2UI openUrl source component id");
|
|
58
92
|
}
|
|
59
93
|
const parsedOptions = parseOpenUrlResolutionOptions(options);
|
|
60
|
-
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
|
|
94
|
+
const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, undefined, undefined, false);
|
|
61
95
|
const plan = adaptComponent("root", "root", context, undefined);
|
|
62
96
|
const openUrls = findNativeOpenUrls(plan, sourceComponentId, parsedOptions.instanceKey);
|
|
63
97
|
if (openUrls.length === 0) {
|
|
@@ -98,7 +132,7 @@ function findNativeOpenUrls(element, sourceComponentId, instanceKey, includeDisa
|
|
|
98
132
|
}
|
|
99
133
|
return openUrls;
|
|
100
134
|
}
|
|
101
|
-
function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false) {
|
|
135
|
+
function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false, imagePolicy, mediaPolicy, hostExtensionPolicy, authorizeResources = true) {
|
|
102
136
|
const localDataModel = dataModel === undefined
|
|
103
137
|
? parseJsonObject(surface.dataModel, "surface.dataModel")
|
|
104
138
|
: parseJsonObject(dataModel, "options.dataModel");
|
|
@@ -112,9 +146,29 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
|
|
|
112
146
|
pluralRules: new Map(),
|
|
113
147
|
visiting: new Set(),
|
|
114
148
|
tolerateInvalidLocalOpenUrls,
|
|
149
|
+
imagePolicy,
|
|
150
|
+
mediaPolicy,
|
|
151
|
+
hostExtensionPolicy,
|
|
152
|
+
hostExtensions: policy.hostExtensions,
|
|
153
|
+
authorizeResources,
|
|
154
|
+
choiceOptionCount: 0,
|
|
155
|
+
choiceOptionOutputLength: 0,
|
|
115
156
|
formatStringExpressionCount: 0,
|
|
116
157
|
formattedStringLength: 0,
|
|
117
158
|
openUrlLength: 0,
|
|
159
|
+
imageUrlLength: 0,
|
|
160
|
+
imageCount: 0,
|
|
161
|
+
imageMaximumBytes: 0,
|
|
162
|
+
imageMaximumDecodedPixels: 0,
|
|
163
|
+
imageRedirectOriginCount: 0,
|
|
164
|
+
imageResourcePolicyOutputLength: 0,
|
|
165
|
+
mediaCount: 0,
|
|
166
|
+
mediaUrlLength: 0,
|
|
167
|
+
mediaMaximumBytes: 0,
|
|
168
|
+
mediaMimeTypeCount: 0,
|
|
169
|
+
mediaRedirectOriginCount: 0,
|
|
170
|
+
mediaPolicyOutputLength: 0,
|
|
171
|
+
hostExtensionCounts: new Map(),
|
|
118
172
|
renderNodeCount: 0,
|
|
119
173
|
validationCheckCount: 0,
|
|
120
174
|
validationOutputLength: 0,
|
|
@@ -123,8 +177,14 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
|
|
|
123
177
|
function parseRenderPlanOptions(options) {
|
|
124
178
|
const parsed = parseOptionsObject(options, "A2UI native render plan options", [
|
|
125
179
|
"dataModel",
|
|
180
|
+
"hostExtensionPolicy",
|
|
181
|
+
"imagePolicy",
|
|
126
182
|
"locale",
|
|
183
|
+
"mediaPolicy",
|
|
127
184
|
]);
|
|
185
|
+
const imagePolicy = parseOptionalImagePolicy(parsed.imagePolicy, "options.imagePolicy");
|
|
186
|
+
const mediaPolicy = parseOptionalPolicy(parsed.mediaPolicy, "options.mediaPolicy");
|
|
187
|
+
const hostExtensionPolicy = parseOptionalPolicy(parsed.hostExtensionPolicy, "options.hostExtensionPolicy");
|
|
128
188
|
return {
|
|
129
189
|
...(parsed.dataModel === undefined
|
|
130
190
|
? {}
|
|
@@ -132,6 +192,9 @@ function parseRenderPlanOptions(options) {
|
|
|
132
192
|
...(parsed.locale === undefined
|
|
133
193
|
? {}
|
|
134
194
|
: { locale: parseLocale(parsed.locale, "options.locale") }),
|
|
195
|
+
...(imagePolicy === undefined ? {} : { imagePolicy }),
|
|
196
|
+
...(mediaPolicy === undefined ? {} : { mediaPolicy }),
|
|
197
|
+
...(hostExtensionPolicy === undefined ? {} : { hostExtensionPolicy }),
|
|
135
198
|
};
|
|
136
199
|
}
|
|
137
200
|
function parseEventResolutionOptions(options) {
|
|
@@ -166,6 +229,18 @@ function parseOpenUrlResolutionOptions(options) {
|
|
|
166
229
|
: { locale: parseLocale(parsed.locale, "options.locale") }),
|
|
167
230
|
};
|
|
168
231
|
}
|
|
232
|
+
function parseOptionalImagePolicy(value, path) {
|
|
233
|
+
if (value !== undefined && typeof value !== "function") {
|
|
234
|
+
throw new A2uiParseError(`Expected a function at ${path}`);
|
|
235
|
+
}
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
function parseOptionalPolicy(value, path) {
|
|
239
|
+
if (value !== undefined && typeof value !== "function") {
|
|
240
|
+
throw new A2uiParseError(`Expected a function at ${path}`);
|
|
241
|
+
}
|
|
242
|
+
return value;
|
|
243
|
+
}
|
|
169
244
|
function parseOptionsObject(value, label, allowedKeys) {
|
|
170
245
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
171
246
|
throw new A2uiParseError(`Expected ${label} to be an object`);
|
|
@@ -227,12 +302,34 @@ function adaptComponent(id, key, context, scope) {
|
|
|
227
302
|
return adaptCard(component, key, context, scope);
|
|
228
303
|
case "Text":
|
|
229
304
|
return adaptText(component, key, context, scope);
|
|
305
|
+
case "Image":
|
|
306
|
+
return adaptImage(component, key, context, scope);
|
|
307
|
+
case "Video":
|
|
308
|
+
return adaptMedia(component, key, "video", context, scope);
|
|
309
|
+
case "AudioPlayer":
|
|
310
|
+
return adaptMedia(component, key, "audio", context, scope);
|
|
311
|
+
case "Icon":
|
|
312
|
+
return adaptIcon(component, key, context, scope);
|
|
313
|
+
case "Divider":
|
|
314
|
+
return adaptDivider(component, key, context, scope);
|
|
230
315
|
case "Button":
|
|
231
316
|
return adaptButton(component, key, context, scope);
|
|
232
317
|
case "TextField":
|
|
233
318
|
return adaptTextField(component, key, context, scope);
|
|
319
|
+
case "CheckBox":
|
|
320
|
+
return adaptCheckBox(component, key, context, scope);
|
|
321
|
+
case "ChoicePicker":
|
|
322
|
+
return adaptChoicePicker(component, key, context, scope);
|
|
323
|
+
case "Slider":
|
|
324
|
+
return adaptSlider(component, key, context, scope);
|
|
325
|
+
case "DateTimeInput":
|
|
326
|
+
return adaptDateTimeInput(component, key, context, scope);
|
|
327
|
+
case "Tabs":
|
|
328
|
+
return adaptTabs(component, key, context, scope);
|
|
329
|
+
case "Modal":
|
|
330
|
+
return adaptModal(component, key, context, scope);
|
|
234
331
|
default:
|
|
235
|
-
|
|
332
|
+
return adaptHostExtension(component, key, context, scope);
|
|
236
333
|
}
|
|
237
334
|
}
|
|
238
335
|
finally {
|
|
@@ -317,6 +414,221 @@ function adaptText(component, key, context, scope) {
|
|
|
317
414
|
addCommonProps(component, props, context, scope);
|
|
318
415
|
return { key, component: "Text", props };
|
|
319
416
|
}
|
|
417
|
+
function adaptImage(component, key, context, scope) {
|
|
418
|
+
context.imageCount += 1;
|
|
419
|
+
if (context.imageCount > A2UI_V1_NATIVE_MAX_IMAGES) {
|
|
420
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_IMAGES} images`);
|
|
421
|
+
}
|
|
422
|
+
const path = `components.${component.id}.url`;
|
|
423
|
+
const url = normalizeHttpUrl(resolveDynamicString(component.url, path, context, scope), path, "image", A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH);
|
|
424
|
+
context.imageUrlLength += url.length;
|
|
425
|
+
if (context.imageUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
426
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image URL length of ${A2UI_V1_MAX_SOURCE_LENGTH} at ${path}`);
|
|
427
|
+
}
|
|
428
|
+
const props = {
|
|
429
|
+
uri: url,
|
|
430
|
+
fit: component.fit ?? "fill",
|
|
431
|
+
variant: component.variant ?? "mediumFeature",
|
|
432
|
+
};
|
|
433
|
+
if (context.authorizeResources) {
|
|
434
|
+
props.resourcePolicy = authorizeImageResource(component.id, key, url, `components.${component.id}.imagePolicy`, context);
|
|
435
|
+
}
|
|
436
|
+
if (component.description !== undefined) {
|
|
437
|
+
props.description = resolveDynamicString(component.description, `components.${component.id}.description`, context, scope);
|
|
438
|
+
}
|
|
439
|
+
addCommonProps(component, props, context, scope);
|
|
440
|
+
if (props.accessibilityLabel === undefined && props.description !== undefined) {
|
|
441
|
+
props.accessibilityLabel = props.description;
|
|
442
|
+
}
|
|
443
|
+
return { key, component: "Image", props };
|
|
444
|
+
}
|
|
445
|
+
function authorizeImageResource(componentId, key, url, path, context) {
|
|
446
|
+
const request = Object.freeze({
|
|
447
|
+
url,
|
|
448
|
+
surfaceId: context.surface.surfaceId,
|
|
449
|
+
sourceComponentId: componentId,
|
|
450
|
+
instanceKey: key,
|
|
451
|
+
});
|
|
452
|
+
if (context.imagePolicy === undefined) {
|
|
453
|
+
throw new A2uiParseError(`A2UI image resource ${JSON.stringify(componentId)} requires an explicit host image policy`);
|
|
454
|
+
}
|
|
455
|
+
let decision;
|
|
456
|
+
try {
|
|
457
|
+
decision = context.imagePolicy(request);
|
|
458
|
+
}
|
|
459
|
+
catch (cause) {
|
|
460
|
+
throw new A2uiParseError(`A2UI image resource ${JSON.stringify(componentId)} host policy failed`, { cause });
|
|
461
|
+
}
|
|
462
|
+
if (decision === false) {
|
|
463
|
+
throw new A2uiParseError(`A2UI image resource ${JSON.stringify(componentId)} URL is denied by host policy`);
|
|
464
|
+
}
|
|
465
|
+
const resourcePolicy = parseImageResourcePolicy(decision, path, context);
|
|
466
|
+
context.imageMaximumBytes += resourcePolicy.maximumBytes;
|
|
467
|
+
if (context.imageMaximumBytes > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES) {
|
|
468
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total image transfer budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES} bytes`);
|
|
469
|
+
}
|
|
470
|
+
context.imageMaximumDecodedPixels += resourcePolicy.maximumDecodedPixels;
|
|
471
|
+
if (context.imageMaximumDecodedPixels > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS) {
|
|
472
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total decoded image budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS} pixels`);
|
|
473
|
+
}
|
|
474
|
+
return resourcePolicy;
|
|
475
|
+
}
|
|
476
|
+
function adaptMedia(component, key, kind, context, scope) {
|
|
477
|
+
context.mediaCount += 1;
|
|
478
|
+
if (context.mediaCount > A2UI_V1_NATIVE_MAX_MEDIA) {
|
|
479
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_MEDIA} media components`);
|
|
480
|
+
}
|
|
481
|
+
const path = `components.${component.id}.url`;
|
|
482
|
+
const url = normalizeHttpUrl(resolveDynamicString(component.url, path, context, scope), path, kind, A2UI_V1_NATIVE_MAX_MEDIA_URL_LENGTH);
|
|
483
|
+
context.mediaUrlLength += url.length;
|
|
484
|
+
if (context.mediaUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
485
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds cumulative media URL length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
486
|
+
}
|
|
487
|
+
const props = { uri: url };
|
|
488
|
+
if (context.authorizeResources) {
|
|
489
|
+
props.resourcePolicy = authorizeMediaResource(component.id, key, kind, url, context);
|
|
490
|
+
}
|
|
491
|
+
if (kind === "video" && component.posterUrl !== undefined) {
|
|
492
|
+
const posterPath = `components.${component.id}.posterUrl`;
|
|
493
|
+
const posterUrl = normalizeHttpUrl(resolveDynamicString(component.posterUrl, posterPath, context, scope), posterPath, "video poster", A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH);
|
|
494
|
+
context.imageCount += 1;
|
|
495
|
+
if (context.imageCount > A2UI_V1_NATIVE_MAX_IMAGES) {
|
|
496
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_IMAGES} image resources`);
|
|
497
|
+
}
|
|
498
|
+
context.imageUrlLength += posterUrl.length;
|
|
499
|
+
if (context.imageUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
500
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds cumulative image URL length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
501
|
+
}
|
|
502
|
+
props.posterUri = posterUrl;
|
|
503
|
+
if (context.authorizeResources) {
|
|
504
|
+
props.posterResourcePolicy = authorizeImageResource(component.id, `${key}:poster`, posterUrl, `components.${component.id}.posterPolicy`, context);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (kind === "audio" && component.description !== undefined) {
|
|
508
|
+
props.description = resolveDynamicString(component.description, `components.${component.id}.description`, context, scope);
|
|
509
|
+
}
|
|
510
|
+
addCommonProps(component, props, context, scope);
|
|
511
|
+
if (props.accessibilityLabel === undefined) {
|
|
512
|
+
props.accessibilityLabel =
|
|
513
|
+
kind === "audio" && typeof props.description === "string"
|
|
514
|
+
? props.description
|
|
515
|
+
: kind === "audio"
|
|
516
|
+
? "Audio"
|
|
517
|
+
: "Video";
|
|
518
|
+
}
|
|
519
|
+
return { key, component: kind === "video" ? "Video" : "AudioPlayer", props };
|
|
520
|
+
}
|
|
521
|
+
function authorizeMediaResource(componentId, key, kind, url, context) {
|
|
522
|
+
if (context.mediaPolicy === undefined) {
|
|
523
|
+
throw new A2uiParseError(`A2UI ${kind} ${JSON.stringify(componentId)} requires an explicit host media policy`);
|
|
524
|
+
}
|
|
525
|
+
const sourceOrigin = parseHttpUrlOrigin(url, `components.${componentId}.url`);
|
|
526
|
+
const request = Object.freeze({
|
|
527
|
+
kind,
|
|
528
|
+
url,
|
|
529
|
+
sourceOrigin,
|
|
530
|
+
surfaceId: context.surface.surfaceId,
|
|
531
|
+
sourceComponentId: componentId,
|
|
532
|
+
instanceKey: key,
|
|
533
|
+
});
|
|
534
|
+
let decision;
|
|
535
|
+
try {
|
|
536
|
+
decision = context.mediaPolicy(request);
|
|
537
|
+
}
|
|
538
|
+
catch (cause) {
|
|
539
|
+
throw new A2uiParseError(`A2UI ${kind} ${JSON.stringify(componentId)} host policy failed`, {
|
|
540
|
+
cause,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
if (decision === false) {
|
|
544
|
+
throw new A2uiParseError(`A2UI ${kind} ${JSON.stringify(componentId)} URL is denied by host policy`);
|
|
545
|
+
}
|
|
546
|
+
const resourcePolicy = parseMediaResourcePolicy(decision, request, `components.${componentId}.mediaPolicy`, context);
|
|
547
|
+
context.mediaMaximumBytes += resourcePolicy.maximumBytes;
|
|
548
|
+
if (context.mediaMaximumBytes > A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES) {
|
|
549
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total media transfer budget of ${A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES} bytes`);
|
|
550
|
+
}
|
|
551
|
+
return resourcePolicy;
|
|
552
|
+
}
|
|
553
|
+
function adaptHostExtension(component, key, context, scope) {
|
|
554
|
+
const hostExtensions = context.hostExtensions;
|
|
555
|
+
if (hostExtensions === undefined) {
|
|
556
|
+
throw new A2uiParseError(`A2UI component ${JSON.stringify(component.id)} uses an unavailable host extension`);
|
|
557
|
+
}
|
|
558
|
+
const validated = validateA2uiV1HostExtensionComponent(hostExtensions, component, `components.${component.id}`);
|
|
559
|
+
const manifest = validated.manifest;
|
|
560
|
+
const countKey = `${manifest.catalogId}\u0000${manifest.componentName}`;
|
|
561
|
+
const count = (context.hostExtensionCounts.get(countKey) ?? 0) + 1;
|
|
562
|
+
if (count > manifest.limits.maximumInstances) {
|
|
563
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${manifest.limits.maximumInstances} instances for ${JSON.stringify(manifest.componentName)}`);
|
|
564
|
+
}
|
|
565
|
+
context.hostExtensionCounts.set(countKey, count);
|
|
566
|
+
const props = {
|
|
567
|
+
extensionId: manifest.extensionId,
|
|
568
|
+
catalogId: manifest.catalogId,
|
|
569
|
+
schemaVersion: manifest.schemaVersion,
|
|
570
|
+
componentName: manifest.componentName,
|
|
571
|
+
manifestFingerprint: validated.manifestFingerprint,
|
|
572
|
+
semanticProps: validated.props,
|
|
573
|
+
sourceComponentId: component.id,
|
|
574
|
+
surfaceId: context.surface.surfaceId,
|
|
575
|
+
};
|
|
576
|
+
if (context.authorizeResources) {
|
|
577
|
+
if (context.hostExtensionPolicy === undefined) {
|
|
578
|
+
throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} requires an explicit capability policy`);
|
|
579
|
+
}
|
|
580
|
+
const request = Object.freeze({
|
|
581
|
+
extensionId: manifest.extensionId,
|
|
582
|
+
catalogId: manifest.catalogId,
|
|
583
|
+
schemaVersion: manifest.schemaVersion,
|
|
584
|
+
componentName: manifest.componentName,
|
|
585
|
+
surfaceId: context.surface.surfaceId,
|
|
586
|
+
sourceComponentId: component.id,
|
|
587
|
+
instanceKey: key,
|
|
588
|
+
platform: hostExtensions.platform,
|
|
589
|
+
semanticProps: validated.props,
|
|
590
|
+
permissionNeeds: manifest.permissionNeeds,
|
|
591
|
+
resourceNeeds: manifest.resourceNeeds,
|
|
592
|
+
});
|
|
593
|
+
let decision;
|
|
594
|
+
try {
|
|
595
|
+
decision = context.hostExtensionPolicy(request);
|
|
596
|
+
}
|
|
597
|
+
catch (cause) {
|
|
598
|
+
throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} capability policy failed`, { cause });
|
|
599
|
+
}
|
|
600
|
+
if (decision === false) {
|
|
601
|
+
throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} is denied by capability policy`);
|
|
602
|
+
}
|
|
603
|
+
props.capabilityGrant = parseHostExtensionCapabilityGrant(decision, manifest.permissionNeeds, manifest.resourceNeeds, `components.${component.id}.hostExtensionPolicy`);
|
|
604
|
+
}
|
|
605
|
+
addCommonProps(component, props, context, scope);
|
|
606
|
+
if (manifest.accessibility.requiresLabel && props.accessibilityLabel === undefined) {
|
|
607
|
+
throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} requires an accessibility label`);
|
|
608
|
+
}
|
|
609
|
+
return { key, component: "HostExtension", props };
|
|
610
|
+
}
|
|
611
|
+
function adaptIcon(component, key, context, scope) {
|
|
612
|
+
const path = `components.${component.id}.name`;
|
|
613
|
+
if (component.name !== null &&
|
|
614
|
+
typeof component.name === "object" &&
|
|
615
|
+
!Array.isArray(component.name) &&
|
|
616
|
+
Object.hasOwn(component.name, "svgPath")) {
|
|
617
|
+
throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
|
|
618
|
+
}
|
|
619
|
+
const name = resolveDynamicString(component.name, path, context, scope);
|
|
620
|
+
if (!A2UI_V1_NATIVE_ICON_NAMES.includes(name)) {
|
|
621
|
+
throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
|
|
622
|
+
}
|
|
623
|
+
const props = { name };
|
|
624
|
+
addCommonProps(component, props, context, scope);
|
|
625
|
+
return { key, component: "Icon", props };
|
|
626
|
+
}
|
|
627
|
+
function adaptDivider(component, key, context, scope) {
|
|
628
|
+
const props = { axis: component.axis ?? "horizontal" };
|
|
629
|
+
addCommonProps(component, props, context, scope);
|
|
630
|
+
return { key, component: "Divider", props };
|
|
631
|
+
}
|
|
320
632
|
function adaptButton(component, key, context, scope) {
|
|
321
633
|
const childId = expectString(component.child, `components.${component.id}.child`);
|
|
322
634
|
const child = context.surface.components.get(childId);
|
|
@@ -364,6 +676,244 @@ function adaptTextField(component, key, context, scope) {
|
|
|
364
676
|
}
|
|
365
677
|
return { key, component: "TextInput", props };
|
|
366
678
|
}
|
|
679
|
+
function adaptCheckBox(component, key, context, scope) {
|
|
680
|
+
const componentPath = `components.${component.id}`;
|
|
681
|
+
const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
682
|
+
const props = {
|
|
683
|
+
label,
|
|
684
|
+
value: resolveDynamicBoolean(component.value, `${componentPath}.value`, context, scope),
|
|
685
|
+
};
|
|
686
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
687
|
+
addCommonProps(component, props, context, scope);
|
|
688
|
+
addValidationProps(component, props, context, scope, "input");
|
|
689
|
+
if (props.accessibilityLabel === undefined) {
|
|
690
|
+
props.accessibilityLabel = label;
|
|
691
|
+
}
|
|
692
|
+
return { key, component: "CheckBox", props };
|
|
693
|
+
}
|
|
694
|
+
function adaptChoicePicker(component, key, context, scope) {
|
|
695
|
+
const componentPath = `components.${component.id}`;
|
|
696
|
+
if (!Array.isArray(component.options)) {
|
|
697
|
+
throw new A2uiParseError(`Expected an array at ${componentPath}.options`);
|
|
698
|
+
}
|
|
699
|
+
context.choiceOptionCount += component.options.length;
|
|
700
|
+
if (context.choiceOptionCount > A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS) {
|
|
701
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS} choice options`);
|
|
702
|
+
}
|
|
703
|
+
const optionValues = new Set();
|
|
704
|
+
const options = component.options.map((value, index) => {
|
|
705
|
+
const option = expectObject(value, `${componentPath}.options[${index}]`);
|
|
706
|
+
const label = resolveDynamicString(option.label, `${componentPath}.options[${index}].label`, context, scope);
|
|
707
|
+
const optionValue = expectString(option.value, `${componentPath}.options[${index}].value`);
|
|
708
|
+
if (optionValues.has(optionValue)) {
|
|
709
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate option value ${JSON.stringify(optionValue)}`);
|
|
710
|
+
}
|
|
711
|
+
optionValues.add(optionValue);
|
|
712
|
+
context.choiceOptionOutputLength += label.length + optionValue.length;
|
|
713
|
+
if (context.choiceOptionOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
714
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum choice-option output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
715
|
+
}
|
|
716
|
+
return Object.freeze({ label, value: optionValue });
|
|
717
|
+
});
|
|
718
|
+
const selected = resolveDynamicStringList(component.value, `${componentPath}.value`, context, scope);
|
|
719
|
+
if (new Set(selected).size !== selected.length) {
|
|
720
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate selected values`);
|
|
721
|
+
}
|
|
722
|
+
for (const value of selected) {
|
|
723
|
+
if (!optionValues.has(value)) {
|
|
724
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} selects unknown value ${JSON.stringify(value)}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const variant = component.variant ?? "mutuallyExclusive";
|
|
728
|
+
if (variant === "mutuallyExclusive" && selected.length > 1) {
|
|
729
|
+
throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} allows at most one selected value`);
|
|
730
|
+
}
|
|
731
|
+
const props = {
|
|
732
|
+
options: Object.freeze(options),
|
|
733
|
+
value: Object.freeze(selected),
|
|
734
|
+
variant,
|
|
735
|
+
displayStyle: component.displayStyle ?? "checkbox",
|
|
736
|
+
filterable: component.filterable ?? false,
|
|
737
|
+
};
|
|
738
|
+
if (component.label !== undefined) {
|
|
739
|
+
props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
740
|
+
}
|
|
741
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
742
|
+
addCommonProps(component, props, context, scope);
|
|
743
|
+
addValidationProps(component, props, context, scope, "input");
|
|
744
|
+
if (props.accessibilityLabel === undefined) {
|
|
745
|
+
if (props.label === undefined) {
|
|
746
|
+
throw new A2uiParseError(`A2UI native ChoicePicker ${JSON.stringify(component.id)} requires label or accessibility.label`);
|
|
747
|
+
}
|
|
748
|
+
props.accessibilityLabel = props.label;
|
|
749
|
+
}
|
|
750
|
+
return { key, component: "ChoicePicker", props };
|
|
751
|
+
}
|
|
752
|
+
function adaptSlider(component, key, context, scope) {
|
|
753
|
+
const componentPath = `components.${component.id}`;
|
|
754
|
+
const minimum = component.min === undefined ? 0 : expectFiniteNumber(component.min, `${componentPath}.min`);
|
|
755
|
+
const maximum = expectFiniteNumber(component.max, `${componentPath}.max`);
|
|
756
|
+
if (minimum >= maximum) {
|
|
757
|
+
throw new A2uiParseError(`Expected ${componentPath}.min to be less than max`);
|
|
758
|
+
}
|
|
759
|
+
const value = resolveDynamicNumber(component.value, `${componentPath}.value`, context, scope);
|
|
760
|
+
if (value < minimum || value > maximum) {
|
|
761
|
+
throw new A2uiParseError(`Expected ${componentPath}.value to be within min and max`);
|
|
762
|
+
}
|
|
763
|
+
let step;
|
|
764
|
+
if (component.steps !== undefined) {
|
|
765
|
+
const steps = expectFiniteNumber(component.steps, `${componentPath}.steps`);
|
|
766
|
+
if (!Number.isInteger(steps) || steps < 1 || steps > JSON_MAX_VALUES) {
|
|
767
|
+
throw new A2uiParseError(`Expected ${componentPath}.steps to be an integer from 1 through ${JSON_MAX_VALUES}`);
|
|
768
|
+
}
|
|
769
|
+
step = (maximum - minimum) / steps;
|
|
770
|
+
if (!Number.isFinite(step) || step <= 0) {
|
|
771
|
+
throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} has invalid range`);
|
|
772
|
+
}
|
|
773
|
+
if (!isSliderStepValue(value, minimum, step)) {
|
|
774
|
+
throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} value does not align with its steps`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const props = {
|
|
778
|
+
value,
|
|
779
|
+
minimum,
|
|
780
|
+
maximum,
|
|
781
|
+
...(step === undefined ? {} : { step }),
|
|
782
|
+
};
|
|
783
|
+
if (component.label !== undefined) {
|
|
784
|
+
props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
785
|
+
}
|
|
786
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
787
|
+
addCommonProps(component, props, context, scope);
|
|
788
|
+
addValidationProps(component, props, context, scope, "input");
|
|
789
|
+
if (props.accessibilityLabel === undefined) {
|
|
790
|
+
if (props.label === undefined) {
|
|
791
|
+
throw new A2uiParseError(`A2UI native Slider ${JSON.stringify(component.id)} requires label or accessibility.label`);
|
|
792
|
+
}
|
|
793
|
+
props.accessibilityLabel = props.label;
|
|
794
|
+
}
|
|
795
|
+
return { key, component: "Slider", props };
|
|
796
|
+
}
|
|
797
|
+
function isSliderStepValue(value, minimum, step) {
|
|
798
|
+
const offset = (value - minimum) / step;
|
|
799
|
+
return (Math.abs(offset - Math.round(offset)) <= Number.EPSILON * Math.max(1, Math.abs(offset)) * 8);
|
|
800
|
+
}
|
|
801
|
+
function adaptDateTimeInput(component, key, context, scope) {
|
|
802
|
+
const componentPath = `components.${component.id}`;
|
|
803
|
+
const enableDate = component.enableDate === true;
|
|
804
|
+
const enableTime = component.enableTime === true;
|
|
805
|
+
if (!enableDate && !enableTime) {
|
|
806
|
+
throw new A2uiParseError(`A2UI DateTimeInput ${JSON.stringify(component.id)} must enable date, time, or both`);
|
|
807
|
+
}
|
|
808
|
+
const value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
|
|
809
|
+
validateDateTimeInputValue(value, enableDate, enableTime, `${componentPath}.value`, true);
|
|
810
|
+
const minimum = component.min === undefined
|
|
811
|
+
? undefined
|
|
812
|
+
: resolveDynamicString(component.min, `${componentPath}.min`, context, scope);
|
|
813
|
+
const maximum = component.max === undefined
|
|
814
|
+
? undefined
|
|
815
|
+
: resolveDynamicString(component.max, `${componentPath}.max`, context, scope);
|
|
816
|
+
if (minimum !== undefined) {
|
|
817
|
+
validateDateTimeInputValue(minimum, enableDate, enableTime, `${componentPath}.min`, false);
|
|
818
|
+
}
|
|
819
|
+
if (maximum !== undefined) {
|
|
820
|
+
validateDateTimeInputValue(maximum, enableDate, enableTime, `${componentPath}.max`, false);
|
|
821
|
+
}
|
|
822
|
+
if (minimum !== undefined &&
|
|
823
|
+
maximum !== undefined &&
|
|
824
|
+
dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`) >
|
|
825
|
+
dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
|
|
826
|
+
throw new A2uiParseError(`Expected ${componentPath}.min not to exceed max`);
|
|
827
|
+
}
|
|
828
|
+
const valueSort = value.length === 0
|
|
829
|
+
? undefined
|
|
830
|
+
: dateTimeInputSortValue(value, enableDate, enableTime, `${componentPath}.value`);
|
|
831
|
+
if (valueSort !== undefined &&
|
|
832
|
+
minimum !== undefined &&
|
|
833
|
+
valueSort < dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`)) {
|
|
834
|
+
throw new A2uiParseError(`Expected ${componentPath}.value not to be earlier than min`);
|
|
835
|
+
}
|
|
836
|
+
if (valueSort !== undefined &&
|
|
837
|
+
maximum !== undefined &&
|
|
838
|
+
valueSort > dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
|
|
839
|
+
throw new A2uiParseError(`Expected ${componentPath}.value not to be later than max`);
|
|
840
|
+
}
|
|
841
|
+
const props = {
|
|
842
|
+
value,
|
|
843
|
+
enableDate,
|
|
844
|
+
enableTime,
|
|
845
|
+
...(minimum === undefined ? {} : { minimum }),
|
|
846
|
+
...(maximum === undefined ? {} : { maximum }),
|
|
847
|
+
};
|
|
848
|
+
if (component.label !== undefined) {
|
|
849
|
+
props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
|
|
850
|
+
}
|
|
851
|
+
addBindingProp(component.value, `${componentPath}.value`, props, scope);
|
|
852
|
+
addCommonProps(component, props, context, scope);
|
|
853
|
+
addValidationProps(component, props, context, scope, "input");
|
|
854
|
+
if (props.accessibilityLabel === undefined) {
|
|
855
|
+
if (props.label === undefined) {
|
|
856
|
+
throw new A2uiParseError(`A2UI native DateTimeInput ${JSON.stringify(component.id)} requires label or accessibility.label`);
|
|
857
|
+
}
|
|
858
|
+
props.accessibilityLabel = props.label;
|
|
859
|
+
}
|
|
860
|
+
return { key, component: "DateTimeInput", props };
|
|
861
|
+
}
|
|
862
|
+
function adaptTabs(component, key, context, scope) {
|
|
863
|
+
const componentPath = `components.${component.id}`;
|
|
864
|
+
if (!Array.isArray(component.tabs) || component.tabs.length === 0) {
|
|
865
|
+
throw new A2uiParseError(`Expected a non-empty array at ${componentPath}.tabs`);
|
|
866
|
+
}
|
|
867
|
+
const tabs = component.tabs.map((value, index) => {
|
|
868
|
+
const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
|
|
869
|
+
return Object.freeze({
|
|
870
|
+
title: resolveDynamicString(tab.title, `${componentPath}.tabs[${index}].title`, context, scope),
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
const props = { tabs: Object.freeze(tabs) };
|
|
874
|
+
addCommonProps(component, props, context, scope);
|
|
875
|
+
return {
|
|
876
|
+
key,
|
|
877
|
+
component: "Tabs",
|
|
878
|
+
props,
|
|
879
|
+
children: component.tabs.map((value, index) => {
|
|
880
|
+
const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
|
|
881
|
+
const childId = expectString(tab.child, `${componentPath}.tabs[${index}].child`);
|
|
882
|
+
return adaptComponent(childId, appendInstanceKey(key, childId, index), context, scope);
|
|
883
|
+
}),
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
function adaptModal(component, key, context, scope) {
|
|
887
|
+
const componentPath = `components.${component.id}`;
|
|
888
|
+
const triggerId = expectString(component.trigger, `${componentPath}.trigger`);
|
|
889
|
+
const contentId = expectString(component.content, `${componentPath}.content`);
|
|
890
|
+
if (context.surface.components.get(triggerId)?.component !== "Button") {
|
|
891
|
+
throw new A2uiParseError(`A2UI native Modal ${JSON.stringify(component.id)} requires a Button trigger`);
|
|
892
|
+
}
|
|
893
|
+
const props = {};
|
|
894
|
+
addCommonProps(component, props, context, scope);
|
|
895
|
+
return {
|
|
896
|
+
key,
|
|
897
|
+
component: "Modal",
|
|
898
|
+
props,
|
|
899
|
+
children: [
|
|
900
|
+
adaptComponent(triggerId, appendInstanceKey(key, triggerId, 0), context, scope),
|
|
901
|
+
adaptComponent(contentId, appendInstanceKey(key, contentId, 1), context, scope),
|
|
902
|
+
],
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
function addBindingProp(value, path, props, scope) {
|
|
906
|
+
if (value !== undefined && isBinding(value)) {
|
|
907
|
+
props.binding = resolveBindingPointer(value.path, `${path}.path`, scope);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function resolveDynamicStringList(value, path, context, scope) {
|
|
911
|
+
const resolved = resolveDynamicValue(value, path, context, scope);
|
|
912
|
+
if (!Array.isArray(resolved) || resolved.some((item) => typeof item !== "string")) {
|
|
913
|
+
throw new A2uiParseError(`Expected an array of strings at ${path}`);
|
|
914
|
+
}
|
|
915
|
+
return [...resolved];
|
|
916
|
+
}
|
|
367
917
|
function addValidationProps(component, props, context, scope, target) {
|
|
368
918
|
if (component.checks === undefined) {
|
|
369
919
|
return;
|
|
@@ -608,15 +1158,18 @@ function resolveButtonAction(component, key, context, scope) {
|
|
|
608
1158
|
};
|
|
609
1159
|
}
|
|
610
1160
|
function normalizeOpenUrl(value, path) {
|
|
611
|
-
|
|
612
|
-
|
|
1161
|
+
return normalizeHttpUrl(value, path, "openUrl", A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH);
|
|
1162
|
+
}
|
|
1163
|
+
function normalizeHttpUrl(value, path, label, maximumLength) {
|
|
1164
|
+
if (value.length === 0 || value.length > maximumLength) {
|
|
1165
|
+
throw new A2uiParseError(`Expected an HTTP(S) URL up to ${maximumLength} characters at ${path}`);
|
|
613
1166
|
}
|
|
614
1167
|
if (/\s|\p{Cf}/u.test(value) || hasAsciiControlCharacter(value)) {
|
|
615
1168
|
throw new A2uiParseError(`Expected an HTTP(S) URL without whitespace, control, or Unicode format characters at ${path}`);
|
|
616
1169
|
}
|
|
617
1170
|
const UrlConstructor = globalThis.URL;
|
|
618
1171
|
if (UrlConstructor === undefined) {
|
|
619
|
-
throw new A2uiParseError(`The host runtime cannot validate an
|
|
1172
|
+
throw new A2uiParseError(`The host runtime cannot validate an ${label} value at ${path}`);
|
|
620
1173
|
}
|
|
621
1174
|
let parsed;
|
|
622
1175
|
try {
|
|
@@ -629,13 +1182,231 @@ function normalizeOpenUrl(value, path) {
|
|
|
629
1182
|
throw new A2uiParseError(`Expected an HTTP(S) URL at ${path}`);
|
|
630
1183
|
}
|
|
631
1184
|
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
632
|
-
throw new A2uiParseError(`A2UI
|
|
1185
|
+
throw new A2uiParseError(`A2UI ${label} does not allow URL credentials at ${path}`);
|
|
633
1186
|
}
|
|
634
|
-
if (parsed.href.length >
|
|
635
|
-
throw new A2uiParseError(`Canonical A2UI
|
|
1187
|
+
if (parsed.href.length > maximumLength) {
|
|
1188
|
+
throw new A2uiParseError(`Canonical A2UI ${label} at ${path} exceeds maximum length of ${maximumLength}`);
|
|
636
1189
|
}
|
|
637
1190
|
return parsed.href;
|
|
638
1191
|
}
|
|
1192
|
+
function parseImageResourcePolicy(value, path, context) {
|
|
1193
|
+
const policy = parseJsonObject(value, path);
|
|
1194
|
+
const allowedKeys = new Set([
|
|
1195
|
+
"allowedRedirectOrigins",
|
|
1196
|
+
"cacheMode",
|
|
1197
|
+
"maximumBytes",
|
|
1198
|
+
"maximumDecodedHeight",
|
|
1199
|
+
"maximumDecodedPixels",
|
|
1200
|
+
"maximumDecodedWidth",
|
|
1201
|
+
"maximumRedirects",
|
|
1202
|
+
]);
|
|
1203
|
+
for (const key of Object.keys(policy)) {
|
|
1204
|
+
if (!allowedKeys.has(key)) {
|
|
1205
|
+
throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (!Array.isArray(policy.allowedRedirectOrigins) ||
|
|
1209
|
+
policy.allowedRedirectOrigins.length > A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS) {
|
|
1210
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS} redirect origins at ${path}.allowedRedirectOrigins`);
|
|
1211
|
+
}
|
|
1212
|
+
context.imageRedirectOriginCount += policy.allowedRedirectOrigins.length;
|
|
1213
|
+
if (context.imageRedirectOriginCount > JSON_MAX_VALUES) {
|
|
1214
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} image redirect origins`);
|
|
1215
|
+
}
|
|
1216
|
+
const origins = policy.allowedRedirectOrigins.map((origin, index) => parseExactHttpOrigin(origin, `${path}.allowedRedirectOrigins[${index}]`));
|
|
1217
|
+
if (new Set(origins).size !== origins.length) {
|
|
1218
|
+
throw new A2uiParseError(`Expected unique redirect origins at ${path}.allowedRedirectOrigins`);
|
|
1219
|
+
}
|
|
1220
|
+
context.imageResourcePolicyOutputLength += origins.reduce((length, origin) => length + origin.length, 0);
|
|
1221
|
+
if (context.imageResourcePolicyOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
1222
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image-policy output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
1223
|
+
}
|
|
1224
|
+
const cacheMode = policy.cacheMode;
|
|
1225
|
+
if (cacheMode !== "default" && cacheMode !== "no-store") {
|
|
1226
|
+
throw new A2uiParseError(`Expected a closed cache mode at ${path}.cacheMode`);
|
|
1227
|
+
}
|
|
1228
|
+
const maximumBytes = parseBoundedPositiveInteger(policy.maximumBytes, A2UI_V1_NATIVE_MAX_IMAGE_BYTES, `${path}.maximumBytes`);
|
|
1229
|
+
const maximumDecodedWidth = parseBoundedPositiveInteger(policy.maximumDecodedWidth, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedWidth`);
|
|
1230
|
+
const maximumDecodedHeight = parseBoundedPositiveInteger(policy.maximumDecodedHeight, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedHeight`);
|
|
1231
|
+
const maximumDecodedPixels = parseBoundedPositiveInteger(policy.maximumDecodedPixels, A2UI_V1_NATIVE_MAX_IMAGE_PIXELS, `${path}.maximumDecodedPixels`);
|
|
1232
|
+
if (maximumDecodedPixels > maximumDecodedWidth * maximumDecodedHeight) {
|
|
1233
|
+
throw new A2uiParseError(`Expected ${path}.maximumDecodedPixels not to exceed the declared dimensions`);
|
|
1234
|
+
}
|
|
1235
|
+
const maximumRedirects = parseBoundedNonNegativeInteger(policy.maximumRedirects, A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS, `${path}.maximumRedirects`);
|
|
1236
|
+
return Object.freeze({
|
|
1237
|
+
allowedRedirectOrigins: Object.freeze(origins),
|
|
1238
|
+
cacheMode,
|
|
1239
|
+
maximumBytes,
|
|
1240
|
+
maximumDecodedHeight,
|
|
1241
|
+
maximumDecodedPixels,
|
|
1242
|
+
maximumDecodedWidth,
|
|
1243
|
+
maximumRedirects,
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
function parseMediaResourcePolicy(value, request, path, context) {
|
|
1247
|
+
const policy = parseJsonObject(value, path);
|
|
1248
|
+
const allowedKeys = new Set([
|
|
1249
|
+
"allowedMimeTypes",
|
|
1250
|
+
"allowedRedirectOrigins",
|
|
1251
|
+
"allowsAutoplay",
|
|
1252
|
+
"allowsBackgroundPlayback",
|
|
1253
|
+
"allowsExternalRoutes",
|
|
1254
|
+
"maximumBytes",
|
|
1255
|
+
"maximumRedirects",
|
|
1256
|
+
"requiresUserActivation",
|
|
1257
|
+
"sourceOrigin",
|
|
1258
|
+
]);
|
|
1259
|
+
for (const key of Object.keys(policy)) {
|
|
1260
|
+
if (!allowedKeys.has(key)) {
|
|
1261
|
+
throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const sourceOrigin = parseExactHttpOrigin(policy.sourceOrigin, `${path}.sourceOrigin`);
|
|
1265
|
+
if (sourceOrigin !== request.sourceOrigin) {
|
|
1266
|
+
throw new A2uiParseError(`Expected ${path}.sourceOrigin to match the requested media origin`);
|
|
1267
|
+
}
|
|
1268
|
+
if (!Array.isArray(policy.allowedRedirectOrigins) ||
|
|
1269
|
+
policy.allowedRedirectOrigins.length > A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS) {
|
|
1270
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS} redirect origins at ${path}.allowedRedirectOrigins`);
|
|
1271
|
+
}
|
|
1272
|
+
context.mediaRedirectOriginCount += policy.allowedRedirectOrigins.length;
|
|
1273
|
+
if (context.mediaRedirectOriginCount > JSON_MAX_VALUES) {
|
|
1274
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} media redirect origins`);
|
|
1275
|
+
}
|
|
1276
|
+
const allowedRedirectOrigins = policy.allowedRedirectOrigins.map((origin, index) => parseExactHttpOrigin(origin, `${path}.allowedRedirectOrigins[${index}]`));
|
|
1277
|
+
if (new Set(allowedRedirectOrigins).size !== allowedRedirectOrigins.length) {
|
|
1278
|
+
throw new A2uiParseError(`Expected unique redirect origins at ${path}.allowedRedirectOrigins`);
|
|
1279
|
+
}
|
|
1280
|
+
if (!Array.isArray(policy.allowedMimeTypes) ||
|
|
1281
|
+
policy.allowedMimeTypes.length === 0 ||
|
|
1282
|
+
policy.allowedMimeTypes.length > A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES) {
|
|
1283
|
+
throw new A2uiParseError(`Expected one through ${A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES} MIME types at ${path}.allowedMimeTypes`);
|
|
1284
|
+
}
|
|
1285
|
+
context.mediaMimeTypeCount += policy.allowedMimeTypes.length;
|
|
1286
|
+
if (context.mediaMimeTypeCount > JSON_MAX_VALUES) {
|
|
1287
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} media MIME types`);
|
|
1288
|
+
}
|
|
1289
|
+
const mimePrefix = request.kind === "video" ? "video/" : "audio/";
|
|
1290
|
+
const allowedMimeTypes = policy.allowedMimeTypes.map((mimeType, index) => {
|
|
1291
|
+
const parsed = expectString(mimeType, `${path}.allowedMimeTypes[${index}]`);
|
|
1292
|
+
if (parsed !== parsed.toLowerCase() ||
|
|
1293
|
+
!parsed.startsWith(mimePrefix) ||
|
|
1294
|
+
!/^(?:audio|video)\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(parsed)) {
|
|
1295
|
+
throw new A2uiParseError(`Expected an exact lower-case ${request.kind} MIME type at ${path}.allowedMimeTypes[${index}]`);
|
|
1296
|
+
}
|
|
1297
|
+
return parsed;
|
|
1298
|
+
});
|
|
1299
|
+
if (new Set(allowedMimeTypes).size !== allowedMimeTypes.length) {
|
|
1300
|
+
throw new A2uiParseError(`Expected unique MIME types at ${path}.allowedMimeTypes`);
|
|
1301
|
+
}
|
|
1302
|
+
context.mediaPolicyOutputLength +=
|
|
1303
|
+
sourceOrigin.length +
|
|
1304
|
+
allowedRedirectOrigins.reduce((total, origin) => total + origin.length, 0) +
|
|
1305
|
+
allowedMimeTypes.reduce((total, mimeType) => total + mimeType.length, 0);
|
|
1306
|
+
if (context.mediaPolicyOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
|
|
1307
|
+
throw new A2uiParseError(`Expanded A2UI native plan exceeds media-policy output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
|
|
1308
|
+
}
|
|
1309
|
+
const maximumBytes = parseBoundedPositiveInteger(policy.maximumBytes, A2UI_V1_NATIVE_MAX_MEDIA_BYTES, `${path}.maximumBytes`);
|
|
1310
|
+
const maximumRedirects = parseBoundedNonNegativeInteger(policy.maximumRedirects, A2UI_V1_NATIVE_MAX_MEDIA_REDIRECTS, `${path}.maximumRedirects`);
|
|
1311
|
+
const allowsAutoplay = parsePolicyBoolean(policy.allowsAutoplay, `${path}.allowsAutoplay`);
|
|
1312
|
+
const allowsBackgroundPlayback = parsePolicyBoolean(policy.allowsBackgroundPlayback, `${path}.allowsBackgroundPlayback`);
|
|
1313
|
+
const allowsExternalRoutes = parsePolicyBoolean(policy.allowsExternalRoutes, `${path}.allowsExternalRoutes`);
|
|
1314
|
+
const requiresUserActivation = parsePolicyBoolean(policy.requiresUserActivation, `${path}.requiresUserActivation`);
|
|
1315
|
+
if (allowsAutoplay && requiresUserActivation) {
|
|
1316
|
+
throw new A2uiParseError(`A2UI media policy cannot allow autoplay while requiring user activation at ${path}`);
|
|
1317
|
+
}
|
|
1318
|
+
return Object.freeze({
|
|
1319
|
+
sourceOrigin,
|
|
1320
|
+
allowedRedirectOrigins: Object.freeze(allowedRedirectOrigins),
|
|
1321
|
+
allowedMimeTypes: Object.freeze(allowedMimeTypes),
|
|
1322
|
+
maximumBytes,
|
|
1323
|
+
maximumRedirects,
|
|
1324
|
+
allowsAutoplay,
|
|
1325
|
+
allowsBackgroundPlayback,
|
|
1326
|
+
allowsExternalRoutes,
|
|
1327
|
+
requiresUserActivation,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
function parseHostExtensionCapabilityGrant(value, permissionNeeds, resourceNeeds, path) {
|
|
1331
|
+
const grant = parseJsonObject(value, path);
|
|
1332
|
+
const keys = Object.keys(grant);
|
|
1333
|
+
if (keys.some((key) => key !== "permissions" && key !== "resources")) {
|
|
1334
|
+
throw new A2uiParseError(`Unexpected host-extension capability grant field at ${path}`);
|
|
1335
|
+
}
|
|
1336
|
+
const permissions = parseExactGrantNames(grant.permissions, permissionNeeds, `${path}.permissions`);
|
|
1337
|
+
const resources = parseExactGrantNames(grant.resources, resourceNeeds, `${path}.resources`);
|
|
1338
|
+
return Object.freeze({ permissions, resources });
|
|
1339
|
+
}
|
|
1340
|
+
function parseExactGrantNames(value, expected, path) {
|
|
1341
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
1342
|
+
throw new A2uiParseError(`Expected an array of capability identifiers at ${path}`);
|
|
1343
|
+
}
|
|
1344
|
+
const names = value;
|
|
1345
|
+
if (new Set(names).size !== names.length) {
|
|
1346
|
+
throw new A2uiParseError(`Expected unique capability identifiers at ${path}`);
|
|
1347
|
+
}
|
|
1348
|
+
const actualSet = new Set(names);
|
|
1349
|
+
if (actualSet.size !== expected.length || expected.some((name) => !actualSet.has(name))) {
|
|
1350
|
+
throw new A2uiParseError(`Expected ${path} to exactly grant the manifest-declared needs`);
|
|
1351
|
+
}
|
|
1352
|
+
return Object.freeze([...expected]);
|
|
1353
|
+
}
|
|
1354
|
+
function parsePolicyBoolean(value, path) {
|
|
1355
|
+
if (typeof value !== "boolean") {
|
|
1356
|
+
throw new A2uiParseError(`Expected a boolean at ${path}`);
|
|
1357
|
+
}
|
|
1358
|
+
return value;
|
|
1359
|
+
}
|
|
1360
|
+
function parseHttpUrlOrigin(value, path) {
|
|
1361
|
+
const UrlConstructor = globalThis.URL;
|
|
1362
|
+
if (UrlConstructor === undefined) {
|
|
1363
|
+
throw new A2uiParseError(`The host runtime cannot validate a media origin at ${path}`);
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
return new UrlConstructor(value).origin;
|
|
1367
|
+
}
|
|
1368
|
+
catch (cause) {
|
|
1369
|
+
throw new A2uiParseError(`Expected an HTTP(S) media URL at ${path}`, { cause });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
function parseExactHttpOrigin(value, path) {
|
|
1373
|
+
const origin = expectString(value, path);
|
|
1374
|
+
if (/\s|\p{Cf}/u.test(origin) || hasAsciiControlCharacter(origin)) {
|
|
1375
|
+
throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
|
|
1376
|
+
}
|
|
1377
|
+
const UrlConstructor = globalThis.URL;
|
|
1378
|
+
if (UrlConstructor === undefined) {
|
|
1379
|
+
throw new A2uiParseError(`The host runtime cannot validate a resource origin at ${path}`);
|
|
1380
|
+
}
|
|
1381
|
+
let parsed;
|
|
1382
|
+
try {
|
|
1383
|
+
parsed = new UrlConstructor(origin);
|
|
1384
|
+
}
|
|
1385
|
+
catch (cause) {
|
|
1386
|
+
throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`, { cause });
|
|
1387
|
+
}
|
|
1388
|
+
if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
|
|
1389
|
+
parsed.username.length > 0 ||
|
|
1390
|
+
parsed.password.length > 0 ||
|
|
1391
|
+
parsed.origin !== origin) {
|
|
1392
|
+
throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
|
|
1393
|
+
}
|
|
1394
|
+
return parsed.origin;
|
|
1395
|
+
}
|
|
1396
|
+
function parseBoundedPositiveInteger(value, maximum, path) {
|
|
1397
|
+
const result = expectFiniteNumber(value, path);
|
|
1398
|
+
if (!Number.isInteger(result) || result < 1 || result > maximum) {
|
|
1399
|
+
throw new A2uiParseError(`Expected an integer from 1 through ${maximum} at ${path}`);
|
|
1400
|
+
}
|
|
1401
|
+
return result;
|
|
1402
|
+
}
|
|
1403
|
+
function parseBoundedNonNegativeInteger(value, maximum, path) {
|
|
1404
|
+
const result = expectFiniteNumber(value, path);
|
|
1405
|
+
if (!Number.isInteger(result) || result < 0 || result > maximum) {
|
|
1406
|
+
throw new A2uiParseError(`Expected an integer from 0 through ${maximum} at ${path}`);
|
|
1407
|
+
}
|
|
1408
|
+
return result;
|
|
1409
|
+
}
|
|
639
1410
|
function hasAsciiControlCharacter(value) {
|
|
640
1411
|
for (let index = 0; index < value.length; index += 1) {
|
|
641
1412
|
const code = value.charCodeAt(index);
|
|
@@ -803,6 +1574,7 @@ function resolveNumberFormat(call, path, context, scope) {
|
|
|
803
1574
|
}
|
|
804
1575
|
const DATE_NUMBER = /^[+-]?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
|
|
805
1576
|
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
1577
|
+
const TIME_ONLY = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|([+-])(\d{2}):(\d{2}))?$/;
|
|
806
1578
|
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
1579
|
const A2UI_V1_MAX_DATE_PATTERN_TOKENS = 128;
|
|
808
1580
|
const DATE_PATTERN_TOKENS = Object.freeze([
|
|
@@ -825,6 +1597,98 @@ const DATE_PATTERN_TOKENS = Object.freeze([
|
|
|
825
1597
|
"a",
|
|
826
1598
|
]);
|
|
827
1599
|
const DATE_PATTERN_TOKEN_SET = new Set(DATE_PATTERN_TOKENS);
|
|
1600
|
+
function validateDateTimeInputValue(value, enableDate, enableTime, path, allowEmpty) {
|
|
1601
|
+
if (allowEmpty && value.length === 0) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (enableDate && enableTime) {
|
|
1605
|
+
parseDateValue(value, path);
|
|
1606
|
+
if (RFC_3339_TIMESTAMP.exec(value) === null) {
|
|
1607
|
+
throw new A2uiParseError(`Expected an RFC 3339 date-time at ${path}`);
|
|
1608
|
+
}
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (enableDate) {
|
|
1612
|
+
const match = DATE_ONLY.exec(value);
|
|
1613
|
+
if (match === null) {
|
|
1614
|
+
throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
|
|
1615
|
+
}
|
|
1616
|
+
validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
parseTimeOnly(value, path);
|
|
1620
|
+
}
|
|
1621
|
+
/** Internal mounted-renderer guard for values emitted by a trusted host date/time component. */
|
|
1622
|
+
export function validateA2uiV1NativeDateTimeInputChange(value, enableDate, enableTime, minimum, maximum, path) {
|
|
1623
|
+
if (!enableDate && !enableTime) {
|
|
1624
|
+
throw new A2uiParseError(`A2UI DateTimeInput at ${path} must enable date, time, or both`);
|
|
1625
|
+
}
|
|
1626
|
+
validateDateTimeInputValue(value, enableDate, enableTime, `${path}.value`, true);
|
|
1627
|
+
if (minimum !== undefined) {
|
|
1628
|
+
validateDateTimeInputValue(minimum, enableDate, enableTime, `${path}.minimum`, false);
|
|
1629
|
+
}
|
|
1630
|
+
if (maximum !== undefined) {
|
|
1631
|
+
validateDateTimeInputValue(maximum, enableDate, enableTime, `${path}.maximum`, false);
|
|
1632
|
+
}
|
|
1633
|
+
const minimumSort = minimum === undefined
|
|
1634
|
+
? undefined
|
|
1635
|
+
: dateTimeInputSortValue(minimum, enableDate, enableTime, `${path}.minimum`);
|
|
1636
|
+
const maximumSort = maximum === undefined
|
|
1637
|
+
? undefined
|
|
1638
|
+
: dateTimeInputSortValue(maximum, enableDate, enableTime, `${path}.maximum`);
|
|
1639
|
+
if (minimumSort !== undefined && maximumSort !== undefined && minimumSort > maximumSort) {
|
|
1640
|
+
throw new A2uiParseError(`Expected ${path}.minimum not to exceed maximum`);
|
|
1641
|
+
}
|
|
1642
|
+
if (value.length === 0) {
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
const valueSort = dateTimeInputSortValue(value, enableDate, enableTime, `${path}.value`);
|
|
1646
|
+
if (minimumSort !== undefined && valueSort < minimumSort) {
|
|
1647
|
+
throw new A2uiParseError(`Expected ${path}.value not to be earlier than minimum`);
|
|
1648
|
+
}
|
|
1649
|
+
if (maximumSort !== undefined && valueSort > maximumSort) {
|
|
1650
|
+
throw new A2uiParseError(`Expected ${path}.value not to be later than maximum`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function dateTimeInputSortValue(value, enableDate, enableTime, path) {
|
|
1654
|
+
if (enableDate && enableTime) {
|
|
1655
|
+
return parseDateValue(value, path).getTime();
|
|
1656
|
+
}
|
|
1657
|
+
if (enableDate) {
|
|
1658
|
+
const match = DATE_ONLY.exec(value);
|
|
1659
|
+
if (match === null) {
|
|
1660
|
+
throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
|
|
1661
|
+
}
|
|
1662
|
+
validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
|
|
1663
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
|
|
1664
|
+
}
|
|
1665
|
+
return parseTimeOnly(value, path);
|
|
1666
|
+
}
|
|
1667
|
+
function parseTimeOnly(value, path) {
|
|
1668
|
+
const match = TIME_ONLY.exec(value);
|
|
1669
|
+
if (match === null) {
|
|
1670
|
+
throw new A2uiParseError(`Expected an ISO 8601 time at ${path}`);
|
|
1671
|
+
}
|
|
1672
|
+
const hour = Number(match[1]);
|
|
1673
|
+
const minute = Number(match[2]);
|
|
1674
|
+
const second = Number(match[3] ?? "0");
|
|
1675
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
1676
|
+
throw new A2uiParseError(`Invalid ISO 8601 time at ${path}`);
|
|
1677
|
+
}
|
|
1678
|
+
const fraction = match[4] ?? "";
|
|
1679
|
+
const millisecond = Number(fraction.slice(0, 3).padEnd(3, "0"));
|
|
1680
|
+
let result = ((hour * 60 + minute) * 60 + second) * 1_000 + millisecond;
|
|
1681
|
+
if (match[5] !== undefined && match[5] !== "Z") {
|
|
1682
|
+
const offsetHour = Number(match[7]);
|
|
1683
|
+
const offsetMinute = Number(match[8]);
|
|
1684
|
+
if (offsetHour > 23 || offsetMinute > 59) {
|
|
1685
|
+
throw new A2uiParseError(`Invalid ISO 8601 time offset at ${path}`);
|
|
1686
|
+
}
|
|
1687
|
+
const direction = match[6] === "+" ? 1 : -1;
|
|
1688
|
+
result -= direction * (offsetHour * 60 + offsetMinute) * 60_000;
|
|
1689
|
+
}
|
|
1690
|
+
return result;
|
|
1691
|
+
}
|
|
828
1692
|
function resolveDateFormat(call, path, context, scope) {
|
|
829
1693
|
const args = expectObject(call.args, `${path}.args`);
|
|
830
1694
|
const value = resolveDynamicValue(args.value, `${path}.args.value`, context, scope);
|