@threadplane/a2ui 0.0.57 → 0.0.58
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/package.json +1 -1
- package/src/index.d.ts +5 -2
- package/src/index.d.ts.map +1 -1
- package/src/index.js +4 -1
- package/src/lib/functions.d.ts +29 -0
- package/src/lib/functions.d.ts.map +1 -0
- package/src/lib/functions.js +418 -0
- package/src/lib/guards.d.ts +4 -11
- package/src/lib/guards.d.ts.map +1 -1
- package/src/lib/guards.js +4 -11
- package/src/lib/parser.d.ts +9 -5
- package/src/lib/parser.d.ts.map +1 -1
- package/src/lib/parser.js +14 -7
- package/src/lib/pointer.d.ts.map +1 -1
- package/src/lib/pointer.js +6 -0
- package/src/lib/resolve.d.ts +15 -6
- package/src/lib/resolve.d.ts.map +1 -1
- package/src/lib/resolve.js +33 -32
- package/src/lib/types.d.ts +189 -183
- package/src/lib/types.d.ts.map +1 -1
- package/src/lib/types.js +7 -1
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { A2UI_WIRE_VERSION, A2UI_MIME_TYPE, A2UI_BASIC_CATALOG_ID } from './lib/types.js';
|
|
2
|
+
export type { A2uiTheme, A2uiPathRef, A2uiFunctionCall, DynamicString, DynamicNumber, DynamicBoolean, DynamicStringList, DynamicValue, A2uiChildren, A2uiAction, A2uiEventAction, A2uiFunctionAction, A2uiCheck, A2uiComponent, A2uiComponentBase, A2uiCatalogComponent, A2uiCheckable, A2uiText, A2uiImage, A2uiIcon, A2uiVideo, A2uiAudioPlayer, A2uiRow, A2uiColumn, A2uiList, A2uiCard, A2uiTabs, A2uiDivider, A2uiModal, A2uiButton, A2uiCheckBox, A2uiTextField, A2uiDateTimeInput, A2uiChoicePicker, A2uiSlider, A2uiCreateSurface, A2uiUpdateComponents, A2uiUpdateDataModel, A2uiDeleteSurface, A2uiMessage, A2uiSurface, A2uiClientDataModel, A2uiClientCapabilities, A2uiActionMessage, A2uiErrorMessage, } from './lib/types.js';
|
|
2
3
|
export { getByPointer, setByPointer, deleteByPointer } from './lib/pointer.js';
|
|
3
4
|
export { createA2uiMessageParser } from './lib/parser.js';
|
|
4
5
|
export type { A2uiMessageParser } from './lib/parser.js';
|
|
5
6
|
export { resolveDynamic } from './lib/resolve.js';
|
|
6
7
|
export type { A2uiScope } from './lib/resolve.js';
|
|
7
|
-
export {
|
|
8
|
+
export { createA2uiFunctionRegistry } from './lib/functions.js';
|
|
9
|
+
export type { A2uiFunctionRegistry, A2uiFunctionImpl, A2uiFunctionContext } from './lib/functions.js';
|
|
10
|
+
export { isPathRef, isFunctionCall } from './lib/guards.js';
|
|
8
11
|
//# sourceMappingURL=index.d.ts.map
|
package/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/a2ui/src/index.ts"],"names":[],"mappings":"AACA,YAAY,EACV,SAAS,EACT,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../libs/a2ui/src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAC1F,YAAY,EACV,SAAS,EACT,WAAW,EAAE,gBAAgB,EAC7B,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,iBAAiB,EAAE,YAAY,EAC7E,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,SAAS,EACxE,aAAa,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,aAAa,EACrE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,eAAe,EACzD,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,EAAE,SAAS,EACzE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,UAAU,EACxF,iBAAiB,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,iBAAiB,EAC/E,WAAW,EAAE,WAAW,EACxB,mBAAmB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,gBAAgB,GACjF,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,YAAY,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,YAAY,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAChE,YAAY,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACtG,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
export { A2UI_WIRE_VERSION, A2UI_MIME_TYPE, A2UI_BASIC_CATALOG_ID } from './lib/types.js';
|
|
1
3
|
export { getByPointer, setByPointer, deleteByPointer } from './lib/pointer.js';
|
|
2
4
|
export { createA2uiMessageParser } from './lib/parser.js';
|
|
3
5
|
export { resolveDynamic } from './lib/resolve.js';
|
|
4
|
-
export {
|
|
6
|
+
export { createA2uiFunctionRegistry } from './lib/functions.js';
|
|
7
|
+
export { isPathRef, isFunctionCall } from './lib/guards.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Execution context handed to every function implementation. */
|
|
2
|
+
export interface A2uiFunctionContext {
|
|
3
|
+
/** Resolve a (possibly dynamic) argument value — bare literal, `{ path }`
|
|
4
|
+
* binding, or nested `{ call }` — against the current data model/scope. */
|
|
5
|
+
resolveArg(value: unknown): unknown;
|
|
6
|
+
/** BCP 47 locale for Intl-based formatting; host default when undefined. */
|
|
7
|
+
locale?: string;
|
|
8
|
+
}
|
|
9
|
+
export type A2uiFunctionImpl = (args: Record<string, unknown>, ctx: A2uiFunctionContext) => unknown;
|
|
10
|
+
export type A2uiFunctionRegistry = ReadonlyMap<string, A2uiFunctionImpl>;
|
|
11
|
+
/** @internal Used by resolveDynamic to make nested `${fn(...)}` calls inside
|
|
12
|
+
* formatString dispatch through the same registry. */
|
|
13
|
+
export declare function withActiveRegistry<T>(registry: A2uiFunctionRegistry, fn: () => T): T;
|
|
14
|
+
/** @internal One-time warning helper shared with resolveDynamic. */
|
|
15
|
+
export declare function warnUnknownA2uiFunction(name: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Creates an A2UI client-side function registry containing the standard
|
|
18
|
+
* basic-catalog functions (`formatString`, `formatNumber`, `formatCurrency`,
|
|
19
|
+
* `formatDate`, `pluralize`, `and`, `or`, `not`), optionally extended or
|
|
20
|
+
* overridden with custom implementations.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const registry = createA2uiFunctionRegistry();
|
|
25
|
+
* resolveDynamic({ call: 'formatCurrency', args: { value: 42, currency: 'USD' } }, {}, undefined, registry);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function createA2uiFunctionRegistry(overrides?: Record<string, A2uiFunctionImpl>): A2uiFunctionRegistry;
|
|
29
|
+
//# sourceMappingURL=functions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"functions.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/functions.ts"],"names":[],"mappings":"AAKA,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC;+EAC2E;IAC3E,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IACpC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,mBAAmB,KACrB,OAAO,CAAC;AAEb,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAsWzE;sDACsD;AACtD,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,QAAQ,EAAE,oBAAoB,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAQpF;AAED,oEAAoE;AACpE,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE1D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAC3C,oBAAoB,CAMtB"}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// A2UI v0.9 client-side functions (basic catalog `functions` map).
|
|
3
|
+
// Arg shapes follow the official catalog schema at
|
|
4
|
+
// https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json.
|
|
5
|
+
function toNumber(v) {
|
|
6
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
7
|
+
return v;
|
|
8
|
+
if (typeof v === 'string' && v.trim() !== '') {
|
|
9
|
+
const n = Number(v);
|
|
10
|
+
if (Number.isFinite(n))
|
|
11
|
+
return n;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
function toDate(v) {
|
|
16
|
+
if (v instanceof Date)
|
|
17
|
+
return Number.isNaN(v.getTime()) ? undefined : v;
|
|
18
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
19
|
+
return new Date(v);
|
|
20
|
+
if (typeof v === 'string') {
|
|
21
|
+
const d = new Date(v);
|
|
22
|
+
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// --- formatDate: Unicode TR35 pattern subset ---
|
|
27
|
+
const DATE_TOKENS = [
|
|
28
|
+
'yyyy', 'yy', 'MMMM', 'MMM', 'MM', 'M', 'dd', 'd', 'EEEE', 'E',
|
|
29
|
+
'HH', 'H', 'hh', 'h', 'mm', 'm', 'ss', 's', 'a',
|
|
30
|
+
];
|
|
31
|
+
function dateToken(token, d, locale) {
|
|
32
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
33
|
+
switch (token) {
|
|
34
|
+
case 'yyyy': return String(d.getFullYear());
|
|
35
|
+
case 'yy': return pad(d.getFullYear() % 100);
|
|
36
|
+
case 'MMMM': return new Intl.DateTimeFormat(locale ?? 'en-US', { month: 'long' }).format(d);
|
|
37
|
+
case 'MMM': return new Intl.DateTimeFormat(locale ?? 'en-US', { month: 'short' }).format(d);
|
|
38
|
+
case 'MM': return pad(d.getMonth() + 1);
|
|
39
|
+
case 'M': return String(d.getMonth() + 1);
|
|
40
|
+
case 'dd': return pad(d.getDate());
|
|
41
|
+
case 'd': return String(d.getDate());
|
|
42
|
+
case 'EEEE': return new Intl.DateTimeFormat(locale ?? 'en-US', { weekday: 'long' }).format(d);
|
|
43
|
+
case 'E': return new Intl.DateTimeFormat(locale ?? 'en-US', { weekday: 'short' }).format(d);
|
|
44
|
+
case 'HH': return pad(d.getHours());
|
|
45
|
+
case 'H': return String(d.getHours());
|
|
46
|
+
case 'hh': return pad(((d.getHours() + 11) % 12) + 1);
|
|
47
|
+
case 'h': return String(((d.getHours() + 11) % 12) + 1);
|
|
48
|
+
case 'mm': return pad(d.getMinutes());
|
|
49
|
+
case 'm': return String(d.getMinutes());
|
|
50
|
+
case 'ss': return pad(d.getSeconds());
|
|
51
|
+
case 's': return String(d.getSeconds());
|
|
52
|
+
case 'a': return d.getHours() < 12 ? 'AM' : 'PM';
|
|
53
|
+
default: return token;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function formatDatePattern(d, pattern, locale) {
|
|
57
|
+
let out = '';
|
|
58
|
+
let i = 0;
|
|
59
|
+
while (i < pattern.length) {
|
|
60
|
+
const token = DATE_TOKENS.find((t) => pattern.startsWith(t, i));
|
|
61
|
+
if (token) {
|
|
62
|
+
out += dateToken(token, d, locale);
|
|
63
|
+
i += token.length;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
out += pattern[i];
|
|
67
|
+
i += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Find the `}` closing the `${` that starts at `start` (index of `$`),
|
|
73
|
+
* honoring nested `${...}` and single-quoted strings. Returns -1 if
|
|
74
|
+
* unterminated. */
|
|
75
|
+
function findClosingBrace(s, start) {
|
|
76
|
+
let depth = 0;
|
|
77
|
+
let i = start;
|
|
78
|
+
let inQuote = false;
|
|
79
|
+
while (i < s.length) {
|
|
80
|
+
const c = s[i];
|
|
81
|
+
if (inQuote) {
|
|
82
|
+
if (c === "'")
|
|
83
|
+
inQuote = false;
|
|
84
|
+
i += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (c === "'") {
|
|
88
|
+
inQuote = true;
|
|
89
|
+
i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (c === '$' && s[i + 1] === '{') {
|
|
93
|
+
depth += 1;
|
|
94
|
+
i += 2;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === '}') {
|
|
98
|
+
depth -= 1;
|
|
99
|
+
if (depth === 0)
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
i += 1;
|
|
103
|
+
}
|
|
104
|
+
return -1;
|
|
105
|
+
}
|
|
106
|
+
/** Split a call's argument list on top-level commas (nesting + quote aware). */
|
|
107
|
+
function splitArgs(s) {
|
|
108
|
+
const parts = [];
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let inQuote = false;
|
|
111
|
+
let cur = '';
|
|
112
|
+
for (let i = 0; i < s.length; i++) {
|
|
113
|
+
const c = s[i];
|
|
114
|
+
if (inQuote) {
|
|
115
|
+
cur += c;
|
|
116
|
+
if (c === "'")
|
|
117
|
+
inQuote = false;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (c === "'") {
|
|
121
|
+
inQuote = true;
|
|
122
|
+
cur += c;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (c === '(' || c === '{')
|
|
126
|
+
depth += 1;
|
|
127
|
+
else if (c === ')' || c === '}')
|
|
128
|
+
depth -= 1;
|
|
129
|
+
else if (c === ',' && depth === 0) {
|
|
130
|
+
parts.push(cur);
|
|
131
|
+
cur = '';
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
cur += c;
|
|
135
|
+
}
|
|
136
|
+
if (cur.trim().length > 0)
|
|
137
|
+
parts.push(cur);
|
|
138
|
+
return parts;
|
|
139
|
+
}
|
|
140
|
+
const CALL_HEAD = /^([A-Za-z_][A-Za-z0-9_]*)\(/;
|
|
141
|
+
/** Evaluate one expression (the text inside `${...}`). */
|
|
142
|
+
function evalExpr(raw, env) {
|
|
143
|
+
const s = raw.trim();
|
|
144
|
+
if (s.length === 0)
|
|
145
|
+
return undefined;
|
|
146
|
+
// Quoted string
|
|
147
|
+
if (s.startsWith("'") && s.endsWith("'") && s.length >= 2) {
|
|
148
|
+
return s.slice(1, -1);
|
|
149
|
+
}
|
|
150
|
+
if (s === 'true')
|
|
151
|
+
return true;
|
|
152
|
+
if (s === 'false')
|
|
153
|
+
return false;
|
|
154
|
+
const asNumber = toNumber(s);
|
|
155
|
+
if (asNumber !== undefined && /^-?[\d.]+$/.test(s))
|
|
156
|
+
return asNumber;
|
|
157
|
+
// Nested `${...}` wrapper
|
|
158
|
+
if (s.startsWith('${') && s.endsWith('}')) {
|
|
159
|
+
return evalExpr(s.slice(2, -1), env);
|
|
160
|
+
}
|
|
161
|
+
// Function call with named args
|
|
162
|
+
const head = CALL_HEAD.exec(s);
|
|
163
|
+
if (head && s.endsWith(')')) {
|
|
164
|
+
const name = head[1];
|
|
165
|
+
const argsText = s.slice(head[0].length, -1);
|
|
166
|
+
const args = {};
|
|
167
|
+
for (const part of splitArgs(argsText)) {
|
|
168
|
+
const colon = topLevelColonIndex(part);
|
|
169
|
+
if (colon === -1)
|
|
170
|
+
continue;
|
|
171
|
+
const key = part.slice(0, colon).trim();
|
|
172
|
+
const valueText = part.slice(colon + 1).trim();
|
|
173
|
+
if (key)
|
|
174
|
+
args[key] = evalExpr(valueText, env);
|
|
175
|
+
}
|
|
176
|
+
const impl = env.registry.get(name);
|
|
177
|
+
if (!impl) {
|
|
178
|
+
warnUnknownFunction(name);
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
// Args are already evaluated to plain values here.
|
|
182
|
+
return impl(args, { ...env.ctx, resolveArg: (v) => v });
|
|
183
|
+
}
|
|
184
|
+
// JSON-pointer path (absolute or relative)
|
|
185
|
+
return env.resolvePath(s);
|
|
186
|
+
}
|
|
187
|
+
/** Index of the first top-level `:` (outside quotes/parens/braces). */
|
|
188
|
+
function topLevelColonIndex(s) {
|
|
189
|
+
let depth = 0;
|
|
190
|
+
let inQuote = false;
|
|
191
|
+
for (let i = 0; i < s.length; i++) {
|
|
192
|
+
const c = s[i];
|
|
193
|
+
if (inQuote) {
|
|
194
|
+
if (c === "'")
|
|
195
|
+
inQuote = false;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (c === "'")
|
|
199
|
+
inQuote = true;
|
|
200
|
+
else if (c === '(' || c === '{')
|
|
201
|
+
depth += 1;
|
|
202
|
+
else if (c === ')' || c === '}')
|
|
203
|
+
depth -= 1;
|
|
204
|
+
else if (c === ':' && depth === 0)
|
|
205
|
+
return i;
|
|
206
|
+
}
|
|
207
|
+
return -1;
|
|
208
|
+
}
|
|
209
|
+
function interpolate(template, env) {
|
|
210
|
+
let out = '';
|
|
211
|
+
let i = 0;
|
|
212
|
+
while (i < template.length) {
|
|
213
|
+
if (template[i] === '\\' && template.startsWith('${', i + 1)) {
|
|
214
|
+
out += '${';
|
|
215
|
+
i += 3;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (template[i] === '$' && template[i + 1] === '{') {
|
|
219
|
+
const close = findClosingBrace(template, i);
|
|
220
|
+
if (close === -1) {
|
|
221
|
+
out += template.slice(i);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
const value = evalExpr(template.slice(i + 2, close), env);
|
|
225
|
+
out += value == null ? '' : String(value);
|
|
226
|
+
i = close + 1;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
out += template[i];
|
|
230
|
+
i += 1;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
// --- Standard function implementations ---
|
|
235
|
+
const warnedFunctions = new Set();
|
|
236
|
+
function warnUnknownFunction(name) {
|
|
237
|
+
if (warnedFunctions.has(name))
|
|
238
|
+
return;
|
|
239
|
+
warnedFunctions.add(name);
|
|
240
|
+
console.warn(`[a2ui] unknown client-side function "${name}" — resolving to undefined`);
|
|
241
|
+
}
|
|
242
|
+
function numberFormatOptions(args, ctx, extra) {
|
|
243
|
+
const options = { ...extra };
|
|
244
|
+
const decimals = toNumber(ctx.resolveArg(args['decimals']));
|
|
245
|
+
if (decimals !== undefined) {
|
|
246
|
+
options.minimumFractionDigits = decimals;
|
|
247
|
+
options.maximumFractionDigits = decimals;
|
|
248
|
+
}
|
|
249
|
+
const grouping = ctx.resolveArg(args['grouping']);
|
|
250
|
+
if (typeof grouping === 'boolean')
|
|
251
|
+
options.useGrouping = grouping;
|
|
252
|
+
return options;
|
|
253
|
+
}
|
|
254
|
+
const STANDARD_FUNCTIONS = {
|
|
255
|
+
formatString(args, ctx) {
|
|
256
|
+
const template = ctx.resolveArg(args['value']);
|
|
257
|
+
if (typeof template !== 'string')
|
|
258
|
+
return undefined;
|
|
259
|
+
const env = {
|
|
260
|
+
ctx,
|
|
261
|
+
registry: currentRegistry ?? new Map(),
|
|
262
|
+
resolvePath: (p) => ctx.resolveArg({ path: p }),
|
|
263
|
+
};
|
|
264
|
+
return interpolate(template, env);
|
|
265
|
+
},
|
|
266
|
+
formatNumber(args, ctx) {
|
|
267
|
+
const value = toNumber(ctx.resolveArg(args['value']));
|
|
268
|
+
if (value === undefined)
|
|
269
|
+
return undefined;
|
|
270
|
+
return new Intl.NumberFormat(ctx.locale, numberFormatOptions(args, ctx)).format(value);
|
|
271
|
+
},
|
|
272
|
+
formatCurrency(args, ctx) {
|
|
273
|
+
const value = toNumber(ctx.resolveArg(args['value']));
|
|
274
|
+
const currency = ctx.resolveArg(args['currency']);
|
|
275
|
+
if (value === undefined || typeof currency !== 'string' || currency.length === 0)
|
|
276
|
+
return undefined;
|
|
277
|
+
try {
|
|
278
|
+
return new Intl.NumberFormat(ctx.locale, numberFormatOptions(args, ctx, { style: 'currency', currency })).format(value);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
formatDate(args, ctx) {
|
|
285
|
+
const date = toDate(ctx.resolveArg(args['value']));
|
|
286
|
+
const format = ctx.resolveArg(args['format']);
|
|
287
|
+
if (!date || typeof format !== 'string')
|
|
288
|
+
return undefined;
|
|
289
|
+
return formatDatePattern(date, format, ctx.locale);
|
|
290
|
+
},
|
|
291
|
+
pluralize(args, ctx) {
|
|
292
|
+
const value = toNumber(ctx.resolveArg(args['value']));
|
|
293
|
+
if (value === undefined)
|
|
294
|
+
return undefined;
|
|
295
|
+
const category = new Intl.PluralRules(ctx.locale).select(value);
|
|
296
|
+
const explicitZero = value === 0 && args['zero'] !== undefined ? 'zero' : undefined;
|
|
297
|
+
const pick = explicitZero ?? category;
|
|
298
|
+
const chosen = args[pick] !== undefined ? args[pick] : args['other'];
|
|
299
|
+
const resolved = ctx.resolveArg(chosen);
|
|
300
|
+
return resolved === undefined ? undefined : String(resolved);
|
|
301
|
+
},
|
|
302
|
+
and(args, ctx) {
|
|
303
|
+
const values = args['values'];
|
|
304
|
+
if (!Array.isArray(values))
|
|
305
|
+
return undefined;
|
|
306
|
+
return values.every((v) => ctx.resolveArg(v) === true);
|
|
307
|
+
},
|
|
308
|
+
or(args, ctx) {
|
|
309
|
+
const values = args['values'];
|
|
310
|
+
if (!Array.isArray(values))
|
|
311
|
+
return undefined;
|
|
312
|
+
return values.some((v) => ctx.resolveArg(v) === true);
|
|
313
|
+
},
|
|
314
|
+
not(args, ctx) {
|
|
315
|
+
return ctx.resolveArg(args['value']) !== true;
|
|
316
|
+
},
|
|
317
|
+
// --- Validators (check-rule conditions) ---
|
|
318
|
+
required(args, ctx) {
|
|
319
|
+
const value = ctx.resolveArg(args['value']);
|
|
320
|
+
if (value == null)
|
|
321
|
+
return false;
|
|
322
|
+
if (typeof value === 'string')
|
|
323
|
+
return value.length > 0;
|
|
324
|
+
if (Array.isArray(value))
|
|
325
|
+
return value.length > 0;
|
|
326
|
+
return true;
|
|
327
|
+
},
|
|
328
|
+
regex(args, ctx) {
|
|
329
|
+
const value = ctx.resolveArg(args['value']);
|
|
330
|
+
const pattern = ctx.resolveArg(args['pattern']);
|
|
331
|
+
if (typeof value !== 'string' || typeof pattern !== 'string')
|
|
332
|
+
return false;
|
|
333
|
+
try {
|
|
334
|
+
return new RegExp(pattern).test(value);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
length(args, ctx) {
|
|
341
|
+
const value = ctx.resolveArg(args['value']);
|
|
342
|
+
if (typeof value !== 'string')
|
|
343
|
+
return false;
|
|
344
|
+
const min = toNumber(ctx.resolveArg(args['min']));
|
|
345
|
+
const max = toNumber(ctx.resolveArg(args['max']));
|
|
346
|
+
if (min !== undefined && value.length < min)
|
|
347
|
+
return false;
|
|
348
|
+
if (max !== undefined && value.length > max)
|
|
349
|
+
return false;
|
|
350
|
+
return true;
|
|
351
|
+
},
|
|
352
|
+
numeric(args, ctx) {
|
|
353
|
+
const value = toNumber(ctx.resolveArg(args['value']));
|
|
354
|
+
if (value === undefined)
|
|
355
|
+
return false;
|
|
356
|
+
const min = toNumber(ctx.resolveArg(args['min']));
|
|
357
|
+
const max = toNumber(ctx.resolveArg(args['max']));
|
|
358
|
+
if (min !== undefined && value < min)
|
|
359
|
+
return false;
|
|
360
|
+
if (max !== undefined && value > max)
|
|
361
|
+
return false;
|
|
362
|
+
return true;
|
|
363
|
+
},
|
|
364
|
+
email(args, ctx) {
|
|
365
|
+
const value = ctx.resolveArg(args['value']);
|
|
366
|
+
if (typeof value !== 'string')
|
|
367
|
+
return false;
|
|
368
|
+
// Linear-time shape check: local@domain.tld with a dotted domain.
|
|
369
|
+
const at = value.indexOf('@');
|
|
370
|
+
if (at <= 0 || at !== value.lastIndexOf('@') || at === value.length - 1)
|
|
371
|
+
return false;
|
|
372
|
+
const domain = value.slice(at + 1);
|
|
373
|
+
const dot = domain.lastIndexOf('.');
|
|
374
|
+
if (dot <= 0 || dot === domain.length - 1)
|
|
375
|
+
return false;
|
|
376
|
+
return !/\s/.test(value);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
// formatString needs the registry that owns it to evaluate nested calls;
|
|
380
|
+
// tracked per-invocation via resolveDynamic's wiring (see resolve.ts),
|
|
381
|
+
// with a module fallback for direct registry use.
|
|
382
|
+
let currentRegistry = null;
|
|
383
|
+
/** @internal Used by resolveDynamic to make nested `${fn(...)}` calls inside
|
|
384
|
+
* formatString dispatch through the same registry. */
|
|
385
|
+
export function withActiveRegistry(registry, fn) {
|
|
386
|
+
const prev = currentRegistry;
|
|
387
|
+
currentRegistry = registry;
|
|
388
|
+
try {
|
|
389
|
+
return fn();
|
|
390
|
+
}
|
|
391
|
+
finally {
|
|
392
|
+
currentRegistry = prev;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** @internal One-time warning helper shared with resolveDynamic. */
|
|
396
|
+
export function warnUnknownA2uiFunction(name) {
|
|
397
|
+
warnUnknownFunction(name);
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Creates an A2UI client-side function registry containing the standard
|
|
401
|
+
* basic-catalog functions (`formatString`, `formatNumber`, `formatCurrency`,
|
|
402
|
+
* `formatDate`, `pluralize`, `and`, `or`, `not`), optionally extended or
|
|
403
|
+
* overridden with custom implementations.
|
|
404
|
+
*
|
|
405
|
+
* @example
|
|
406
|
+
* ```ts
|
|
407
|
+
* const registry = createA2uiFunctionRegistry();
|
|
408
|
+
* resolveDynamic({ call: 'formatCurrency', args: { value: 42, currency: 'USD' } }, {}, undefined, registry);
|
|
409
|
+
* ```
|
|
410
|
+
*/
|
|
411
|
+
export function createA2uiFunctionRegistry(overrides) {
|
|
412
|
+
const map = new Map(Object.entries(STANDARD_FUNCTIONS));
|
|
413
|
+
if (overrides) {
|
|
414
|
+
for (const [name, impl] of Object.entries(overrides))
|
|
415
|
+
map.set(name, impl);
|
|
416
|
+
}
|
|
417
|
+
return map;
|
|
418
|
+
}
|
package/src/lib/guards.d.ts
CHANGED
|
@@ -2,16 +2,9 @@
|
|
|
2
2
|
export declare function isPathRef(value: unknown): value is {
|
|
3
3
|
path: string;
|
|
4
4
|
};
|
|
5
|
-
/** Returns true when `value` is an A2UI
|
|
6
|
-
export declare function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** Returns true when `value` is an A2UI number literal wrapper. */
|
|
10
|
-
export declare function isLiteralNumber(value: unknown): value is {
|
|
11
|
-
literalNumber: number;
|
|
12
|
-
};
|
|
13
|
-
/** Returns true when `value` is an A2UI boolean literal wrapper. */
|
|
14
|
-
export declare function isLiteralBoolean(value: unknown): value is {
|
|
15
|
-
literalBoolean: boolean;
|
|
5
|
+
/** Returns true when `value` is an A2UI client-side function call. */
|
|
6
|
+
export declare function isFunctionCall(value: unknown): value is {
|
|
7
|
+
call: string;
|
|
8
|
+
args?: Record<string, unknown>;
|
|
16
9
|
};
|
|
17
10
|
//# sourceMappingURL=guards.d.ts.map
|
package/src/lib/guards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/guards.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAGnE;AAED,
|
|
1
|
+
{"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/guards.ts"],"names":[],"mappings":"AAEA,mEAAmE;AACnE,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAGnE;AAED,sEAAsE;AACtE,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,GACb,KAAK,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAG3D"}
|
package/src/lib/guards.js
CHANGED
|
@@ -4,15 +4,8 @@ export function isPathRef(value) {
|
|
|
4
4
|
return typeof value === 'object' && value !== null
|
|
5
5
|
&& 'path' in value && typeof value.path === 'string';
|
|
6
6
|
}
|
|
7
|
-
/** Returns true when `value` is an A2UI
|
|
8
|
-
export function
|
|
9
|
-
return typeof value === 'object' && value !== null
|
|
10
|
-
|
|
11
|
-
/** Returns true when `value` is an A2UI number literal wrapper. */
|
|
12
|
-
export function isLiteralNumber(value) {
|
|
13
|
-
return typeof value === 'object' && value !== null && 'literalNumber' in value;
|
|
14
|
-
}
|
|
15
|
-
/** Returns true when `value` is an A2UI boolean literal wrapper. */
|
|
16
|
-
export function isLiteralBoolean(value) {
|
|
17
|
-
return typeof value === 'object' && value !== null && 'literalBoolean' in value;
|
|
7
|
+
/** Returns true when `value` is an A2UI client-side function call. */
|
|
8
|
+
export function isFunctionCall(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null
|
|
10
|
+
&& 'call' in value && typeof value.call === 'string';
|
|
18
11
|
}
|
package/src/lib/parser.d.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type A2uiMessage } from './types.js';
|
|
2
2
|
export interface A2uiMessageParser {
|
|
3
3
|
/** Push a JSONL stream chunk and return every complete A2UI envelope parsed from it. */
|
|
4
4
|
push(chunk: string): A2uiMessage[];
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Creates a stateful parser for newline-delimited A2UI message streams.
|
|
7
|
+
* Creates a stateful parser for newline-delimited A2UI v0.9 message streams.
|
|
8
8
|
*
|
|
9
9
|
* The parser buffers incomplete lines, skips malformed JSON, and returns only
|
|
10
|
-
* recognized A2UI envelopes: `
|
|
11
|
-
* `
|
|
10
|
+
* recognized A2UI envelopes: `createSurface`, `updateComponents`,
|
|
11
|
+
* `updateDataModel`, and `deleteSurface`. Unknown envelope keys (e.g. future
|
|
12
|
+
* v1.0 messages) are skipped rather than treated as errors. A missing
|
|
13
|
+
* `version` field defaults to `v0.9`.
|
|
12
14
|
*
|
|
13
15
|
* @example
|
|
14
16
|
* ```ts
|
|
15
17
|
* const parser = createA2uiMessageParser();
|
|
16
|
-
* const messages = parser.push(
|
|
18
|
+
* const messages = parser.push(
|
|
19
|
+
* '{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"basic"}}\n',
|
|
20
|
+
* );
|
|
17
21
|
* ```
|
|
18
22
|
*/
|
|
19
23
|
export declare function createA2uiMessageParser(): A2uiMessageParser;
|
package/src/lib/parser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/parser.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAIjE,MAAM,WAAW,iBAAiB;IAChC,wFAAwF;IACxF,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,IAAI,iBAAiB,CAuC3D"}
|
package/src/lib/parser.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import { A2UI_WIRE_VERSION } from './types.js';
|
|
3
|
+
const ENVELOPE_KEYS = ['createSurface', 'updateComponents', 'updateDataModel', 'deleteSurface'];
|
|
2
4
|
/**
|
|
3
|
-
* Creates a stateful parser for newline-delimited A2UI message streams.
|
|
5
|
+
* Creates a stateful parser for newline-delimited A2UI v0.9 message streams.
|
|
4
6
|
*
|
|
5
7
|
* The parser buffers incomplete lines, skips malformed JSON, and returns only
|
|
6
|
-
* recognized A2UI envelopes: `
|
|
7
|
-
* `
|
|
8
|
+
* recognized A2UI envelopes: `createSurface`, `updateComponents`,
|
|
9
|
+
* `updateDataModel`, and `deleteSurface`. Unknown envelope keys (e.g. future
|
|
10
|
+
* v1.0 messages) are skipped rather than treated as errors. A missing
|
|
11
|
+
* `version` field defaults to `v0.9`.
|
|
8
12
|
*
|
|
9
13
|
* @example
|
|
10
14
|
* ```ts
|
|
11
15
|
* const parser = createA2uiMessageParser();
|
|
12
|
-
* const messages = parser.push(
|
|
16
|
+
* const messages = parser.push(
|
|
17
|
+
* '{"version":"v0.9","createSurface":{"surfaceId":"s1","catalogId":"basic"}}\n',
|
|
18
|
+
* );
|
|
13
19
|
* ```
|
|
14
20
|
*/
|
|
15
21
|
export function createA2uiMessageParser() {
|
|
@@ -17,8 +23,9 @@ export function createA2uiMessageParser() {
|
|
|
17
23
|
function parseEnvelope(json) {
|
|
18
24
|
for (const key of ENVELOPE_KEYS) {
|
|
19
25
|
if (key in json && typeof json[key] === 'object' && json[key] !== null) {
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
const version = typeof json['version'] === 'string' ? json['version'] : A2UI_WIRE_VERSION;
|
|
27
|
+
// A2uiMessage is a discriminated union of single-envelope-key objects.
|
|
28
|
+
return { version, [key]: json[key] };
|
|
22
29
|
}
|
|
23
30
|
}
|
|
24
31
|
return null;
|
package/src/lib/pointer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pointer.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/pointer.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"pointer.d.ts","sourceRoot":"","sources":["../../../../../libs/a2ui/src/lib/pointer.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAQrF;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,OAAO,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwBzB"}
|
package/src/lib/pointer.js
CHANGED
|
@@ -76,6 +76,12 @@ export function deleteByPointer(model, pointer) {
|
|
|
76
76
|
const parent = getByPointer(model, '/' + parentPath.join('/'));
|
|
77
77
|
if (parent == null || typeof parent !== 'object')
|
|
78
78
|
return model;
|
|
79
|
+
if (Array.isArray(parent)) {
|
|
80
|
+
// v0.9 rule: deleting an array index sets it to undefined, preserving length.
|
|
81
|
+
const parentCopy = [...parent];
|
|
82
|
+
parentCopy[Number(key)] = undefined;
|
|
83
|
+
return setByPointer(model, '/' + parentPath.join('/'), parentCopy);
|
|
84
|
+
}
|
|
79
85
|
const parentCopy = { ...parent };
|
|
80
86
|
delete parentCopy[key];
|
|
81
87
|
return setByPointer(model, '/' + parentPath.join('/'), parentCopy);
|