@mcp-native/a2ui 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +100 -13
  2. package/dist/binding.d.ts +33 -0
  3. package/dist/binding.d.ts.map +1 -0
  4. package/dist/binding.js +83 -0
  5. package/dist/binding.js.map +1 -0
  6. package/dist/errors.d.ts +7 -0
  7. package/dist/errors.d.ts.map +1 -0
  8. package/dist/errors.js +13 -0
  9. package/dist/errors.js.map +1 -0
  10. package/dist/index.d.ts +9 -12
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +7 -13
  13. package/dist/index.js.map +1 -1
  14. package/dist/mime.d.ts +6 -0
  15. package/dist/mime.d.ts.map +1 -0
  16. package/dist/mime.js +2 -0
  17. package/dist/mime.js.map +1 -0
  18. package/dist/schema-revision.d.ts +3 -0
  19. package/dist/schema-revision.d.ts.map +1 -0
  20. package/dist/schema-revision.js +3 -0
  21. package/dist/schema-revision.js.map +1 -0
  22. package/dist/v1/action.d.ts +7 -0
  23. package/dist/v1/action.d.ts.map +1 -0
  24. package/dist/v1/action.js +86 -0
  25. package/dist/v1/action.js.map +1 -0
  26. package/dist/v1/capabilities.d.ts +13 -0
  27. package/dist/v1/capabilities.d.ts.map +1 -0
  28. package/dist/v1/capabilities.js +134 -0
  29. package/dist/v1/capabilities.js.map +1 -0
  30. package/dist/v1/format-string.d.ts +21 -0
  31. package/dist/v1/format-string.d.ts.map +1 -0
  32. package/dist/v1/format-string.js +487 -0
  33. package/dist/v1/format-string.js.map +1 -0
  34. package/dist/v1/index.d.ts +13 -0
  35. package/dist/v1/index.d.ts.map +1 -0
  36. package/dist/v1/index.js +9 -0
  37. package/dist/v1/index.js.map +1 -0
  38. package/dist/v1/parse.d.ts +12 -0
  39. package/dist/v1/parse.d.ts.map +1 -0
  40. package/dist/v1/parse.js +96 -0
  41. package/dist/v1/parse.js.map +1 -0
  42. package/dist/v1/resolve.d.ts +19 -0
  43. package/dist/v1/resolve.d.ts.map +1 -0
  44. package/dist/v1/resolve.js +112 -0
  45. package/dist/v1/resolve.js.map +1 -0
  46. package/dist/v1/schemas.d.ts +16 -0
  47. package/dist/v1/schemas.d.ts.map +1 -0
  48. package/dist/v1/schemas.js +133 -0
  49. package/dist/v1/schemas.js.map +1 -0
  50. package/dist/v1/store.d.ts +21 -0
  51. package/dist/v1/store.d.ts.map +1 -0
  52. package/dist/v1/store.js +305 -0
  53. package/dist/v1/store.js.map +1 -0
  54. package/dist/v1/types.d.ts +109 -0
  55. package/dist/v1/types.d.ts.map +1 -0
  56. package/dist/v1/types.js +10 -0
  57. package/dist/v1/types.js.map +1 -0
  58. package/dist/v1/validate.d.ts +26 -0
  59. package/dist/v1/validate.d.ts.map +1 -0
  60. package/dist/v1/validate.js +449 -0
  61. package/dist/v1/validate.js.map +1 -0
  62. package/dist/v1/vendor/agent_capabilities.json +26 -0
  63. package/dist/v1/vendor/agent_to_renderer.json +216 -0
  64. package/dist/v1/vendor/catalog.json +1346 -0
  65. package/dist/v1/vendor/common_types.json +395 -0
  66. package/dist/v1/vendor/renderer_capabilities.json +29 -0
  67. package/dist/v1/vendor/renderer_to_agent.json +161 -0
  68. package/package.json +5 -2
  69. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/CHECKSUMS.sha256 +13 -0
  70. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/PROVENANCE.md +12 -0
  71. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/catalogs/basic/catalog.json +1346 -0
  72. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/agent_capabilities.json +26 -0
  73. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/agent_to_renderer.json +216 -0
  74. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/agent_to_renderer_list.json +10 -0
  75. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/agent_to_renderer_list_wrapper.json +14 -0
  76. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/catalog_definition.json +267 -0
  77. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/common_types.json +395 -0
  78. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/renderer_capabilities.json +29 -0
  79. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/renderer_data_model.json +22 -0
  80. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/renderer_to_agent.json +161 -0
  81. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/renderer_to_agent_list.json +10 -0
  82. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/renderer_to_agent_list_wrapper.json +14 -0
  83. package/schemas/7541f953050cd58b80f0bf5d85fe2d63192af305/json/sample.json +22 -0
@@ -0,0 +1,134 @@
1
+ import { parseJsonObject } from "@mcp-native/core";
2
+ import { A2uiParseError } from "../errors.js";
3
+ import { formatAjvErrors, getA2uiV1AgentCapabilitiesValidator, getA2uiV1RendererCapabilitiesValidator, } from "./schemas.js";
4
+ import { A2UI_V1_MAX_SOURCE_LENGTH } from "./types.js";
5
+ const VERSION_KEY = "v1.0";
6
+ /** Parses untrusted agent capability metadata and applies the normative v1 requirements. */
7
+ export function parseA2uiV1AgentCapabilities(input) {
8
+ const source = reconstructCapabilities(input, "agent capabilities");
9
+ const version = expectClosedVersionObject(source, [
10
+ "acceptsInlineCatalogs",
11
+ "supportedCatalogIds",
12
+ ]);
13
+ const validate = getA2uiV1AgentCapabilitiesValidator();
14
+ if (!validate(source)) {
15
+ throw new A2uiParseError(`A2UI v1 agent capability schema validation failed: ${formatAjvErrors(validate)}`);
16
+ }
17
+ if (!Object.hasOwn(version, "supportedCatalogIds")) {
18
+ throw new A2uiParseError("A2UI v1 agent capabilities must declare supportedCatalogIds as required by the protocol");
19
+ }
20
+ return freezeAgentCapabilities(parseCatalogIds(version.supportedCatalogIds, "agent capabilities.v1.0.supportedCatalogIds"), version.acceptsInlineCatalogs === true);
21
+ }
22
+ /**
23
+ * Parses renderer capability metadata without activating inline catalogs.
24
+ * Catalog IDs are declarations only; callers must advertise only fully implemented host catalogs.
25
+ */
26
+ export function parseA2uiV1RendererCapabilities(input) {
27
+ const source = reconstructCapabilities(input, "renderer capabilities");
28
+ const version = expectClosedVersionObject(source, ["inlineCatalogs", "supportedCatalogIds"]);
29
+ if (Object.hasOwn(version, "inlineCatalogs")) {
30
+ throw new A2uiParseError("A2UI v1 inline renderer catalogs are not supported without an explicit host catalog policy");
31
+ }
32
+ const validate = getA2uiV1RendererCapabilitiesValidator();
33
+ if (!validate(source)) {
34
+ throw new A2uiParseError(`A2UI v1 renderer capability schema validation failed: ${formatAjvErrors(validate)}`);
35
+ }
36
+ return freezeRendererCapabilities(parseCatalogIds(version.supportedCatalogIds, "renderer capabilities.v1.0.supportedCatalogIds"));
37
+ }
38
+ /** Constructs closed renderer metadata from host-owned catalog declarations. */
39
+ export function createA2uiV1RendererCapabilities(options) {
40
+ if (options === null || typeof options !== "object" || Array.isArray(options)) {
41
+ throw new A2uiParseError("Expected A2UI v1 renderer capability options to be an object");
42
+ }
43
+ const source = options;
44
+ rejectUnknownKeys(source, ["supportedCatalogIds"], "renderer capability options");
45
+ return parseA2uiV1RendererCapabilities({
46
+ [VERSION_KEY]: { supportedCatalogIds: source.supportedCatalogIds },
47
+ });
48
+ }
49
+ /** Finds exact catalog IDs that both peers explicitly declare. */
50
+ export function negotiateA2uiV1Capabilities(agentInput, rendererInput) {
51
+ const agent = parseA2uiV1AgentCapabilities(agentInput);
52
+ const renderer = parseA2uiV1RendererCapabilities(rendererInput);
53
+ const agentCatalogIds = new Set(agent[VERSION_KEY].supportedCatalogIds);
54
+ const supportedCatalogIds = renderer[VERSION_KEY].supportedCatalogIds.filter((catalogId) => agentCatalogIds.has(catalogId));
55
+ if (supportedCatalogIds.length === 0) {
56
+ return Object.freeze({
57
+ kind: "fallback",
58
+ protocolVersion: VERSION_KEY,
59
+ reason: "no-shared-catalog",
60
+ });
61
+ }
62
+ return Object.freeze({
63
+ kind: "negotiated",
64
+ protocolVersion: VERSION_KEY,
65
+ supportedCatalogIds: Object.freeze(supportedCatalogIds),
66
+ inlineCatalogsEnabled: false,
67
+ });
68
+ }
69
+ function reconstructCapabilities(input, path) {
70
+ let value = input;
71
+ if (typeof input === "string") {
72
+ if (input.length > A2UI_V1_MAX_SOURCE_LENGTH) {
73
+ throw new A2uiParseError(`A2UI v1 ${path} exceed maximum length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
74
+ }
75
+ try {
76
+ value = JSON.parse(input);
77
+ }
78
+ catch (error) {
79
+ const message = error instanceof Error ? error.message : "unknown JSON error";
80
+ throw new A2uiParseError(`Invalid A2UI v1 ${path} JSON: ${message}`, { cause: error });
81
+ }
82
+ }
83
+ try {
84
+ return parseJsonObject(value, path);
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : `Invalid A2UI v1 ${path}`;
88
+ throw new A2uiParseError(message, { cause: error });
89
+ }
90
+ }
91
+ function expectClosedVersionObject(source, allowedVersionKeys) {
92
+ rejectUnknownKeys(source, [VERSION_KEY], "capabilities");
93
+ const version = source[VERSION_KEY];
94
+ if (version === null || typeof version !== "object" || Array.isArray(version)) {
95
+ throw new A2uiParseError(`Expected an object at capabilities.${VERSION_KEY}`);
96
+ }
97
+ rejectUnknownKeys(version, allowedVersionKeys, `capabilities.${VERSION_KEY}`);
98
+ return version;
99
+ }
100
+ function parseCatalogIds(value, path) {
101
+ if (!Array.isArray(value)) {
102
+ throw new A2uiParseError(`Expected an array at ${path}`);
103
+ }
104
+ const ids = new Set();
105
+ for (const [index, catalogId] of value.entries()) {
106
+ if (typeof catalogId !== "string" || catalogId.length === 0) {
107
+ throw new A2uiParseError(`Expected a non-empty string at ${path}[${index}]`);
108
+ }
109
+ if (ids.has(catalogId)) {
110
+ throw new A2uiParseError(`Duplicate A2UI catalog ID ${JSON.stringify(catalogId)} at ${path}`);
111
+ }
112
+ ids.add(catalogId);
113
+ }
114
+ return Object.freeze([...ids]);
115
+ }
116
+ function freezeAgentCapabilities(supportedCatalogIds, acceptsInlineCatalogs) {
117
+ return Object.freeze({
118
+ [VERSION_KEY]: Object.freeze({ supportedCatalogIds, acceptsInlineCatalogs }),
119
+ });
120
+ }
121
+ function freezeRendererCapabilities(supportedCatalogIds) {
122
+ return Object.freeze({
123
+ [VERSION_KEY]: Object.freeze({ supportedCatalogIds }),
124
+ });
125
+ }
126
+ function rejectUnknownKeys(value, allowed, path) {
127
+ const allowedKeys = new Set(allowed);
128
+ for (const key of Object.keys(value)) {
129
+ if (!allowedKeys.has(key)) {
130
+ throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
131
+ }
132
+ }
133
+ }
134
+ //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.js","sourceRoot":"","sources":["../../src/v1/capabilities.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import type { JsonValue } from "@mcp-native/core";
2
+ export interface A2uiV1ParsedFormatString {
3
+ readonly expressionCount: number;
4
+ readonly expressions: readonly JsonValue[];
5
+ readonly parts: readonly A2uiV1FormatStringPart[];
6
+ }
7
+ type A2uiV1FormatStringPart = {
8
+ readonly kind: "expression";
9
+ readonly value: JsonValue;
10
+ } | {
11
+ readonly kind: "literal";
12
+ readonly value: string;
13
+ };
14
+ export type A2uiV1FormatStringExpressionResolver = (expression: JsonValue, index: number) => JsonValue | undefined;
15
+ export type A2uiV1FormatStringExpressionBudgetConsumer = (expressionCount: number) => void;
16
+ /** Parses the expression language embedded in one pinned-catalog formatString value. */
17
+ export declare function parseA2uiV1FormatString(source: string, path: string): A2uiV1ParsedFormatString;
18
+ /** Evaluates one parsed formatString source with a host-owned expression resolver. */
19
+ export declare function evaluateA2uiV1FormatString(source: string, resolveExpression: A2uiV1FormatStringExpressionResolver, path?: string, consumeExpressionBudget?: A2uiV1FormatStringExpressionBudgetConsumer): string;
20
+ export {};
21
+ //# sourceMappingURL=format-string.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format-string.d.ts","sourceRoot":"","sources":["../../src/v1/format-string.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAI9D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,SAAS,SAAS,EAAE,CAAC;IAC3C,QAAQ,CAAC,KAAK,EAAE,SAAS,sBAAsB,EAAE,CAAC;CACnD;AAED,KAAK,sBAAsB,GACvB;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;CAAE,GAC1D;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzD,MAAM,MAAM,oCAAoC,GAAG,CACjD,UAAU,EAAE,SAAS,EACrB,KAAK,EAAE,MAAM,KACV,SAAS,GAAG,SAAS,CAAC;AAE3B,MAAM,MAAM,0CAA0C,GAAG,CAAC,eAAe,EAAE,MAAM,KAAK,IAAI,CAAC;AAW3F,wFAAwF;AACxF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,wBAAwB,CAG9F;AAED,sFAAsF;AACtF,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,iBAAiB,EAAE,oCAAoC,EACvD,IAAI,SAAiB,EACrB,uBAAuB,CAAC,EAAE,0CAA0C,GACnE,MAAM,CAwCR"}
@@ -0,0 +1,487 @@
1
+ import { JSON_MAX_DEPTH, JSON_MAX_STRING_LENGTH, JSON_MAX_VALUES, parseJsonValue, } from "@mcp-native/core";
2
+ import { A2uiParseError } from "../errors.js";
3
+ const IDENTIFIER = /^[\p{XID_Start}_][\p{XID_Continue}]*$/u;
4
+ const JSON_NUMBER = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
5
+ /** Parses the expression language embedded in one pinned-catalog formatString value. */
6
+ export function parseA2uiV1FormatString(source, path) {
7
+ const parser = new FormatStringParser(path);
8
+ return parser.parse(source);
9
+ }
10
+ /** Evaluates one parsed formatString source with a host-owned expression resolver. */
11
+ export function evaluateA2uiV1FormatString(source, resolveExpression, path = "formatString", consumeExpressionBudget) {
12
+ if (typeof source !== "string") {
13
+ throw new A2uiParseError(`Expected a string at ${path}`);
14
+ }
15
+ if (source.length > JSON_MAX_STRING_LENGTH) {
16
+ throw new A2uiParseError(`A2UI formatString source at ${path} exceeds maximum length of ${JSON_MAX_STRING_LENGTH}`);
17
+ }
18
+ if (typeof resolveExpression !== "function") {
19
+ throw new A2uiParseError(`Expected an A2UI formatString expression resolver at ${path}`);
20
+ }
21
+ if (typeof path !== "string" || path.length === 0) {
22
+ throw new A2uiParseError("Expected a non-empty A2UI formatString diagnostic path");
23
+ }
24
+ if (consumeExpressionBudget !== undefined && typeof consumeExpressionBudget !== "function") {
25
+ throw new A2uiParseError(`Expected an A2UI formatString budget consumer at ${path}`);
26
+ }
27
+ const parsed = parseA2uiV1FormatString(source, path);
28
+ consumeExpressionBudget?.(parsed.expressionCount);
29
+ const chunks = [];
30
+ let outputLength = 0;
31
+ let expressionIndex = 0;
32
+ for (const part of parsed.parts) {
33
+ const chunk = part.kind === "literal"
34
+ ? part.value
35
+ : coerceFormatStringValue(resolveExpression(part.value, expressionIndex++), `${path}.interpolations[${expressionIndex - 1}]`, JSON_MAX_STRING_LENGTH - outputLength);
36
+ outputLength += chunk.length;
37
+ if (outputLength > JSON_MAX_STRING_LENGTH) {
38
+ throwFormatStringOutputLimit(path);
39
+ }
40
+ chunks.push(chunk);
41
+ }
42
+ return chunks.join("");
43
+ }
44
+ class FormatStringParser {
45
+ #path;
46
+ #expressionCount = 0;
47
+ constructor(path) {
48
+ this.#path = path;
49
+ }
50
+ parse(source) {
51
+ const expressions = [];
52
+ const parts = [];
53
+ let literalStart = 0;
54
+ for (let index = 0; index < source.length; index += 1) {
55
+ if (source[index] !== "$" || source[index + 1] !== "{" || isEscaped(source, index)) {
56
+ continue;
57
+ }
58
+ const interpolation = this.#readInterpolation(source, index);
59
+ appendLiteralPart(parts, source.slice(literalStart, index));
60
+ const expression = this.#parseExpression(interpolation.content, interpolation.offset, 1);
61
+ expressions.push(expression);
62
+ parts.push({ kind: "expression", value: expression });
63
+ index = interpolation.end - 1;
64
+ literalStart = interpolation.end;
65
+ }
66
+ appendLiteralPart(parts, source.slice(literalStart));
67
+ return {
68
+ expressionCount: this.#expressionCount,
69
+ expressions,
70
+ parts,
71
+ };
72
+ }
73
+ #parseExpression(source, offset, depth) {
74
+ this.#expressionCount += 1;
75
+ if (this.#expressionCount > JSON_MAX_VALUES) {
76
+ this.#fail(`exceeds maximum of ${JSON_MAX_VALUES} interpolation expressions`, offset);
77
+ }
78
+ if (depth > JSON_MAX_DEPTH) {
79
+ this.#fail(`exceeds maximum expression depth of ${JSON_MAX_DEPTH}`, offset);
80
+ }
81
+ const expression = source.trim();
82
+ if (expression.length === 0) {
83
+ this.#fail("contains an empty interpolation expression", offset);
84
+ }
85
+ if (expression.startsWith("${")) {
86
+ const interpolation = this.#readInterpolation(expression, 0);
87
+ if (expression.slice(interpolation.end).trim().length !== 0) {
88
+ this.#fail("contains trailing content after a nested expression", offset);
89
+ }
90
+ return this.#parseExpression(interpolation.content, offset + interpolation.offset, depth + 1);
91
+ }
92
+ if (expression[0] === '"' || expression[0] === "'") {
93
+ return parseQuotedLiteral(expression, this.#path, offset);
94
+ }
95
+ if (expression === "true") {
96
+ return true;
97
+ }
98
+ if (expression === "false") {
99
+ return false;
100
+ }
101
+ if (expression === "null") {
102
+ return null;
103
+ }
104
+ if (JSON_NUMBER.test(expression)) {
105
+ const value = Number(expression);
106
+ if (!Number.isFinite(value)) {
107
+ this.#fail("contains a non-finite numeric literal", offset);
108
+ }
109
+ return value;
110
+ }
111
+ const openParenthesis = findFirstParenthesis(expression);
112
+ if (openParenthesis === -1) {
113
+ return { path: expression };
114
+ }
115
+ const functionName = expression.slice(0, openParenthesis).trim();
116
+ if (!isFunctionIdentifier(functionName)) {
117
+ return { path: expression };
118
+ }
119
+ const closeParenthesis = findClosingParenthesis(expression, openParenthesis, this.#path);
120
+ if (expression.slice(closeParenthesis + 1).trim().length !== 0) {
121
+ this.#fail("contains trailing content after a function call", offset);
122
+ }
123
+ const args = this.#parseArguments(expression.slice(openParenthesis + 1, closeParenthesis), offset + openParenthesis + 1, depth);
124
+ return {
125
+ call: functionName,
126
+ ...(Object.keys(args).length === 0 ? {} : { args }),
127
+ };
128
+ }
129
+ #parseArguments(source, offset, depth) {
130
+ const args = {};
131
+ if (source.trim().length === 0) {
132
+ return args;
133
+ }
134
+ for (const segment of splitTopLevel(source, ",", this.#path)) {
135
+ const separator = findTopLevelCharacter(segment.value, ":", this.#path);
136
+ if (separator === -1) {
137
+ this.#fail("requires every function argument to be named", offset + segment.offset);
138
+ }
139
+ const name = segment.value.slice(0, separator).trim();
140
+ if (!IDENTIFIER.test(name)) {
141
+ this.#fail(`contains invalid argument name ${JSON.stringify(name)}`, offset + segment.offset);
142
+ }
143
+ if (Object.hasOwn(args, name)) {
144
+ this.#fail(`contains duplicate argument ${JSON.stringify(name)}`, offset + segment.offset);
145
+ }
146
+ const valueSource = segment.value.slice(separator + 1).trim();
147
+ if (valueSource.length === 0) {
148
+ this.#fail(`contains an empty value for argument ${JSON.stringify(name)}`, offset + segment.offset);
149
+ }
150
+ defineJsonProperty(args, name, this.#parseArgumentValue(valueSource, offset + segment.offset + separator + 1, depth + 1));
151
+ }
152
+ return args;
153
+ }
154
+ #parseArgumentValue(source, offset, depth) {
155
+ if (source.startsWith("${")) {
156
+ const interpolation = this.#readInterpolation(source, 0);
157
+ if (source.slice(interpolation.end).trim().length !== 0) {
158
+ this.#fail("contains trailing content after a nested expression", offset);
159
+ }
160
+ return this.#parseExpression(interpolation.content, offset + interpolation.offset, depth);
161
+ }
162
+ if (source[0] === '"' || source[0] === "'") {
163
+ return parseQuotedLiteral(source, this.#path, offset);
164
+ }
165
+ if (source === "true") {
166
+ return true;
167
+ }
168
+ if (source === "false") {
169
+ return false;
170
+ }
171
+ if (JSON_NUMBER.test(source)) {
172
+ const value = Number(source);
173
+ if (!Number.isFinite(value)) {
174
+ this.#fail("contains a non-finite numeric literal", offset);
175
+ }
176
+ return value;
177
+ }
178
+ this.#fail(`contains unsupported argument literal ${JSON.stringify(source)}`, offset);
179
+ }
180
+ #readInterpolation(source, start) {
181
+ let nesting = 1;
182
+ let quote;
183
+ let escaped = false;
184
+ for (let index = start + 2; index < source.length; index += 1) {
185
+ const character = source[index];
186
+ if (quote !== undefined) {
187
+ if (escaped) {
188
+ escaped = false;
189
+ }
190
+ else if (character === "\\") {
191
+ escaped = true;
192
+ }
193
+ else if (character === quote) {
194
+ quote = undefined;
195
+ }
196
+ continue;
197
+ }
198
+ if (character === '"' || character === "'") {
199
+ quote = character;
200
+ continue;
201
+ }
202
+ if (character === "$" && source[index + 1] === "{" && !isEscaped(source, index)) {
203
+ nesting += 1;
204
+ index += 1;
205
+ continue;
206
+ }
207
+ if (character === "}") {
208
+ nesting -= 1;
209
+ if (nesting === 0) {
210
+ return {
211
+ content: source.slice(start + 2, index),
212
+ end: index + 1,
213
+ offset: start + 2,
214
+ };
215
+ }
216
+ }
217
+ }
218
+ this.#fail("contains an unterminated interpolation expression", start);
219
+ }
220
+ #fail(message, offset) {
221
+ throw new A2uiParseError(`A2UI formatString at ${this.#path} ${message} near offset ${offset}`);
222
+ }
223
+ }
224
+ function appendLiteralPart(parts, source) {
225
+ if (source.length === 0) {
226
+ return;
227
+ }
228
+ parts.push({ kind: "literal", value: source.replaceAll("\\${", "${") });
229
+ }
230
+ function coerceFormatStringValue(value, path, remainingLength) {
231
+ if (value === undefined || value === null) {
232
+ return "";
233
+ }
234
+ const reconstructed = parseJsonValue(value, path);
235
+ if (typeof reconstructed === "string") {
236
+ return reconstructed;
237
+ }
238
+ if (typeof reconstructed === "number" || typeof reconstructed === "boolean") {
239
+ return String(reconstructed);
240
+ }
241
+ measureJsonSerialization(reconstructed, remainingLength, path);
242
+ const serialized = JSON.stringify(reconstructed);
243
+ if (serialized === undefined) {
244
+ throw new A2uiParseError(`Expected a JSON value at ${path}`);
245
+ }
246
+ return serialized;
247
+ }
248
+ function measureJsonSerialization(value, limit, path) {
249
+ let length;
250
+ if (value === null) {
251
+ length = 4;
252
+ }
253
+ else if (typeof value === "boolean") {
254
+ length = value ? 4 : 5;
255
+ }
256
+ else if (typeof value === "number") {
257
+ length = String(value).length;
258
+ }
259
+ else if (typeof value === "string") {
260
+ length = JSON.stringify(value).length;
261
+ }
262
+ else if (Array.isArray(value)) {
263
+ length = 2;
264
+ for (const [index, child] of value.entries()) {
265
+ length += index === 0 ? 0 : 1;
266
+ length += measureJsonSerialization(child, limit - length, `${path}[${index}]`);
267
+ if (length > limit) {
268
+ throwFormatStringOutputLimit(path);
269
+ }
270
+ }
271
+ }
272
+ else {
273
+ length = 2;
274
+ let index = 0;
275
+ for (const [key, child] of Object.entries(value)) {
276
+ length += index++ === 0 ? 0 : 1;
277
+ length += JSON.stringify(key).length + 1;
278
+ length += measureJsonSerialization(child, limit - length, `${path}.${key}`);
279
+ if (length > limit) {
280
+ throwFormatStringOutputLimit(path);
281
+ }
282
+ }
283
+ }
284
+ if (length > limit) {
285
+ throwFormatStringOutputLimit(path);
286
+ }
287
+ return length;
288
+ }
289
+ function throwFormatStringOutputLimit(path) {
290
+ throw new A2uiParseError(`A2UI formatString output at ${path} exceeds maximum length of ${JSON_MAX_STRING_LENGTH}`);
291
+ }
292
+ function findFirstParenthesis(source) {
293
+ for (let index = 0; index < source.length; index += 1) {
294
+ if (source[index] === "(") {
295
+ return index;
296
+ }
297
+ }
298
+ return -1;
299
+ }
300
+ function findClosingParenthesis(source, open, path) {
301
+ let depth = 1;
302
+ let quote;
303
+ let escaped = false;
304
+ for (let index = open + 1; index < source.length; index += 1) {
305
+ const character = source[index];
306
+ if (quote !== undefined) {
307
+ if (escaped) {
308
+ escaped = false;
309
+ }
310
+ else if (character === "\\") {
311
+ escaped = true;
312
+ }
313
+ else if (character === quote) {
314
+ quote = undefined;
315
+ }
316
+ continue;
317
+ }
318
+ if (character === '"' || character === "'") {
319
+ quote = character;
320
+ }
321
+ else if (character === "(") {
322
+ depth += 1;
323
+ }
324
+ else if (character === ")") {
325
+ depth -= 1;
326
+ if (depth === 0) {
327
+ return index;
328
+ }
329
+ }
330
+ }
331
+ throw new A2uiParseError(`A2UI formatString at ${path} contains an unclosed function call`);
332
+ }
333
+ function splitTopLevel(source, separator, path) {
334
+ const segments = [];
335
+ let start = 0;
336
+ walkTopLevel(source, path, (character, index, isTopLevel) => {
337
+ if (isTopLevel && character === separator) {
338
+ segments.push({ offset: start, value: source.slice(start, index) });
339
+ start = index + 1;
340
+ }
341
+ });
342
+ segments.push({ offset: start, value: source.slice(start) });
343
+ return segments;
344
+ }
345
+ function findTopLevelCharacter(source, target, path) {
346
+ let result = -1;
347
+ walkTopLevel(source, path, (character, index, isTopLevel) => {
348
+ if (result === -1 && isTopLevel && character === target) {
349
+ result = index;
350
+ }
351
+ });
352
+ return result;
353
+ }
354
+ function walkTopLevel(source, path, visit) {
355
+ let interpolationDepth = 0;
356
+ let parenthesisDepth = 0;
357
+ let quote;
358
+ let escaped = false;
359
+ for (let index = 0; index < source.length; index += 1) {
360
+ const character = source[index];
361
+ if (quote !== undefined) {
362
+ if (escaped) {
363
+ escaped = false;
364
+ }
365
+ else if (character === "\\") {
366
+ escaped = true;
367
+ }
368
+ else if (character === quote) {
369
+ quote = undefined;
370
+ }
371
+ visit(character, index, false);
372
+ continue;
373
+ }
374
+ if (character === '"' || character === "'") {
375
+ quote = character;
376
+ visit(character, index, false);
377
+ continue;
378
+ }
379
+ if (character === "$" && source[index + 1] === "{") {
380
+ interpolationDepth += 1;
381
+ visit(character, index, false);
382
+ index += 1;
383
+ continue;
384
+ }
385
+ if (character === "}" && interpolationDepth > 0) {
386
+ interpolationDepth -= 1;
387
+ visit(character, index, false);
388
+ continue;
389
+ }
390
+ if (character === "(" && interpolationDepth === 0) {
391
+ parenthesisDepth += 1;
392
+ }
393
+ else if (character === ")" && interpolationDepth === 0) {
394
+ parenthesisDepth -= 1;
395
+ if (parenthesisDepth < 0) {
396
+ throw new A2uiParseError(`A2UI formatString at ${path} contains an unmatched ")"`);
397
+ }
398
+ }
399
+ visit(character, index, interpolationDepth === 0 && parenthesisDepth === 0);
400
+ }
401
+ if (quote !== undefined || interpolationDepth !== 0 || parenthesisDepth !== 0) {
402
+ throw new A2uiParseError(`A2UI formatString at ${path} contains unbalanced argument syntax`);
403
+ }
404
+ }
405
+ function parseQuotedLiteral(source, path, offset) {
406
+ const quote = source[0];
407
+ let result = "";
408
+ for (let index = 1; index < source.length; index += 1) {
409
+ const character = source[index];
410
+ if (character === quote) {
411
+ if (source.slice(index + 1).trim().length !== 0) {
412
+ throw new A2uiParseError(`A2UI formatString at ${path} contains trailing quoted-literal content near offset ${offset}`);
413
+ }
414
+ return result;
415
+ }
416
+ if (character.charCodeAt(0) < 0x20) {
417
+ throw new A2uiParseError(`A2UI formatString at ${path} contains a control character near offset ${offset + index}`);
418
+ }
419
+ if (character !== "\\") {
420
+ result += character;
421
+ continue;
422
+ }
423
+ const escaped = source[index + 1];
424
+ if (escaped === undefined) {
425
+ break;
426
+ }
427
+ index += 1;
428
+ switch (escaped) {
429
+ case "\\":
430
+ case "/":
431
+ result += escaped;
432
+ break;
433
+ case '"':
434
+ result += '"';
435
+ break;
436
+ case "'":
437
+ result += "'";
438
+ break;
439
+ case "b":
440
+ result += "\b";
441
+ break;
442
+ case "f":
443
+ result += "\f";
444
+ break;
445
+ case "n":
446
+ result += "\n";
447
+ break;
448
+ case "r":
449
+ result += "\r";
450
+ break;
451
+ case "t":
452
+ result += "\t";
453
+ break;
454
+ case "u": {
455
+ const digits = source.slice(index + 1, index + 5);
456
+ if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
457
+ throw new A2uiParseError(`A2UI formatString at ${path} contains an invalid Unicode escape near offset ${offset + index}`);
458
+ }
459
+ result += String.fromCharCode(Number.parseInt(digits, 16));
460
+ index += 4;
461
+ break;
462
+ }
463
+ default:
464
+ throw new A2uiParseError(`A2UI formatString at ${path} contains unsupported escape ${JSON.stringify(`\\${escaped}`)} near offset ${offset + index}`);
465
+ }
466
+ }
467
+ throw new A2uiParseError(`A2UI formatString at ${path} contains an unterminated quoted literal near offset ${offset}`);
468
+ }
469
+ function isFunctionIdentifier(value) {
470
+ return value === "@index" || IDENTIFIER.test(value);
471
+ }
472
+ function isEscaped(source, index) {
473
+ let backslashes = 0;
474
+ for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) {
475
+ backslashes += 1;
476
+ }
477
+ return backslashes % 2 === 1;
478
+ }
479
+ function defineJsonProperty(object, key, value) {
480
+ Object.defineProperty(object, key, {
481
+ configurable: true,
482
+ enumerable: true,
483
+ value,
484
+ writable: true,
485
+ });
486
+ }
487
+ //# sourceMappingURL=format-string.js.map