@a2ui-sdk/utils 0.0.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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * String Interpolation Parser - Public API
3
+ *
4
+ * This module provides the public interface for parsing and evaluating
5
+ * interpolated strings with the A2UI 0.9 `${expression}` syntax.
6
+ *
7
+ * @example
8
+ * import { interpolate } from '@a2ui-sdk/utils/0.9'
9
+ *
10
+ * // Interpolate with data model
11
+ * const result = interpolate('Hello, ${/user/name}!', { user: { name: 'John' } })
12
+ * // result: "Hello, John!"
13
+ */
14
+ export type { TokenType, Token, ASTNode, LiteralNode, PathNode, FunctionCallNode, InterpolatedStringNode, ParseError, DataModel, FunctionRegistry, InterpolationFunction, EvaluationContext, } from './types.js';
15
+ export { tokenize } from './lexer.js';
16
+ export { parse } from './parser.js';
17
+ export { evaluate } from './evaluator.js';
18
+ import type { InterpolatedStringNode, DataModel, FunctionRegistry } from './types.js';
19
+ export declare function hasInterpolation(value: string): boolean;
20
+ /**
21
+ * Parses a template string and returns the AST.
22
+ *
23
+ * This function tokenizes and parses the input, returning an AST that
24
+ * represents the structure of the interpolated string. Errors are handled
25
+ * gracefully - malformed expressions become empty literals.
26
+ *
27
+ * @param template - The template string with ${...} expressions
28
+ * @returns The parsed AST (InterpolatedStringNode)
29
+ *
30
+ * @example
31
+ * const ast = parseInterpolation('Hello, ${/name}!')
32
+ * // Returns:
33
+ * // {
34
+ * // type: 'interpolatedString',
35
+ * // parts: [
36
+ * // { type: 'literal', value: 'Hello, ' },
37
+ * // { type: 'path', path: '/name', absolute: true },
38
+ * // { type: 'literal', value: '!' }
39
+ * // ]
40
+ * // }
41
+ */
42
+ export declare function parseInterpolation(template: string): InterpolatedStringNode;
43
+ /**
44
+ * Parses and evaluates a template string, returning the interpolated result.
45
+ *
46
+ * This is the main entry point for string interpolation. It combines
47
+ * parsing and evaluation in one call for convenience.
48
+ *
49
+ * @param template - The template string with ${...} expressions
50
+ * @param dataModel - The data model for path lookups
51
+ * @param basePath - Optional base path for relative path resolution
52
+ * @param functions - Optional custom function registry
53
+ * @returns The interpolated string result
54
+ *
55
+ * @example
56
+ * const model = { user: { name: 'John' } }
57
+ *
58
+ * // Simple path
59
+ * interpolate('Hello, ${/user/name}!', model)
60
+ * // Returns: "Hello, John!"
61
+ *
62
+ * // Relative path with basePath
63
+ * interpolate('Hello, ${name}!', model, '/user')
64
+ * // Returns: "Hello, John!"
65
+ *
66
+ * // Function call
67
+ * interpolate('${upper(${/user/name})}', model)
68
+ * // Returns: "JOHN"
69
+ */
70
+ export declare function interpolate(template: string, dataModel: DataModel, basePath?: string | null, functions?: FunctionRegistry): string;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * String Interpolation Parser - Public API
3
+ *
4
+ * This module provides the public interface for parsing and evaluating
5
+ * interpolated strings with the A2UI 0.9 `${expression}` syntax.
6
+ *
7
+ * @example
8
+ * import { interpolate } from '@a2ui-sdk/utils/0.9'
9
+ *
10
+ * // Interpolate with data model
11
+ * const result = interpolate('Hello, ${/user/name}!', { user: { name: 'John' } })
12
+ * // result: "Hello, John!"
13
+ */
14
+ export { tokenize } from './lexer.js';
15
+ export { parse } from './parser.js';
16
+ export { evaluate } from './evaluator.js';
17
+ import { tokenize } from './lexer.js';
18
+ import { parse } from './parser.js';
19
+ import { evaluate } from './evaluator.js';
20
+ // Check for interpolation using simple string check
21
+ export function hasInterpolation(value) {
22
+ // Check for ${...} no matter if it is escaped or not
23
+ return value.includes('${');
24
+ }
25
+ /**
26
+ * Parses a template string and returns the AST.
27
+ *
28
+ * This function tokenizes and parses the input, returning an AST that
29
+ * represents the structure of the interpolated string. Errors are handled
30
+ * gracefully - malformed expressions become empty literals.
31
+ *
32
+ * @param template - The template string with ${...} expressions
33
+ * @returns The parsed AST (InterpolatedStringNode)
34
+ *
35
+ * @example
36
+ * const ast = parseInterpolation('Hello, ${/name}!')
37
+ * // Returns:
38
+ * // {
39
+ * // type: 'interpolatedString',
40
+ * // parts: [
41
+ * // { type: 'literal', value: 'Hello, ' },
42
+ * // { type: 'path', path: '/name', absolute: true },
43
+ * // { type: 'literal', value: '!' }
44
+ * // ]
45
+ * // }
46
+ */
47
+ export function parseInterpolation(template) {
48
+ const tokens = tokenize(template);
49
+ return parse(tokens);
50
+ }
51
+ /**
52
+ * Parses and evaluates a template string, returning the interpolated result.
53
+ *
54
+ * This is the main entry point for string interpolation. It combines
55
+ * parsing and evaluation in one call for convenience.
56
+ *
57
+ * @param template - The template string with ${...} expressions
58
+ * @param dataModel - The data model for path lookups
59
+ * @param basePath - Optional base path for relative path resolution
60
+ * @param functions - Optional custom function registry
61
+ * @returns The interpolated string result
62
+ *
63
+ * @example
64
+ * const model = { user: { name: 'John' } }
65
+ *
66
+ * // Simple path
67
+ * interpolate('Hello, ${/user/name}!', model)
68
+ * // Returns: "Hello, John!"
69
+ *
70
+ * // Relative path with basePath
71
+ * interpolate('Hello, ${name}!', model, '/user')
72
+ * // Returns: "Hello, John!"
73
+ *
74
+ * // Function call
75
+ * interpolate('${upper(${/user/name})}', model)
76
+ * // Returns: "JOHN"
77
+ */
78
+ export function interpolate(template, dataModel, basePath = null, functions) {
79
+ if (!hasInterpolation(template)) {
80
+ return template;
81
+ }
82
+ const ast = parseInterpolation(template);
83
+ return evaluate(ast, { dataModel, basePath, functions });
84
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Lexer (Tokenizer) for string interpolation expressions.
3
+ *
4
+ * Converts an input string into a sequence of tokens that can be
5
+ * consumed by the parser.
6
+ */
7
+ import { Token } from './types.js';
8
+ /**
9
+ * Tokenizes an interpolated string into a sequence of tokens.
10
+ *
11
+ * The lexer operates as a state machine with two main modes:
12
+ * - TEXT mode: Collecting literal text outside ${...}
13
+ * - EXPRESSION mode: Tokenizing content inside ${...}
14
+ *
15
+ * @param input - The input string to tokenize
16
+ * @returns Array of tokens
17
+ */
18
+ export declare function tokenize(input: string): Token[];
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Lexer (Tokenizer) for string interpolation expressions.
3
+ *
4
+ * Converts an input string into a sequence of tokens that can be
5
+ * consumed by the parser.
6
+ */
7
+ import { TokenType } from './types.js';
8
+ /**
9
+ * Tokenizes an interpolated string into a sequence of tokens.
10
+ *
11
+ * The lexer operates as a state machine with two main modes:
12
+ * - TEXT mode: Collecting literal text outside ${...}
13
+ * - EXPRESSION mode: Tokenizing content inside ${...}
14
+ *
15
+ * @param input - The input string to tokenize
16
+ * @returns Array of tokens
17
+ */
18
+ export function tokenize(input) {
19
+ const tokens = [];
20
+ let pos = 0;
21
+ let expressionDepth = 0; // Track nesting depth
22
+ while (pos < input.length) {
23
+ if (expressionDepth > 0) {
24
+ // Skip whitespace inside expressions
25
+ while (pos < input.length && /\s/.test(input[pos])) {
26
+ pos++;
27
+ }
28
+ if (pos >= input.length) {
29
+ break;
30
+ }
31
+ const char = input[pos];
32
+ // Expression end
33
+ if (char === '}') {
34
+ tokens.push({
35
+ type: TokenType.EXPR_END,
36
+ value: '}',
37
+ start: pos,
38
+ end: pos + 1,
39
+ });
40
+ pos++;
41
+ expressionDepth--; // Decrement depth instead of setting to false
42
+ continue;
43
+ }
44
+ // Left parenthesis
45
+ if (char === '(') {
46
+ tokens.push({
47
+ type: TokenType.LPAREN,
48
+ value: '(',
49
+ start: pos,
50
+ end: pos + 1,
51
+ });
52
+ pos++;
53
+ continue;
54
+ }
55
+ // Right parenthesis
56
+ if (char === ')') {
57
+ tokens.push({
58
+ type: TokenType.RPAREN,
59
+ value: ')',
60
+ start: pos,
61
+ end: pos + 1,
62
+ });
63
+ pos++;
64
+ continue;
65
+ }
66
+ // Comma
67
+ if (char === ',') {
68
+ tokens.push({
69
+ type: TokenType.COMMA,
70
+ value: ',',
71
+ start: pos,
72
+ end: pos + 1,
73
+ });
74
+ pos++;
75
+ continue;
76
+ }
77
+ // String literal
78
+ if (char === "'") {
79
+ const start = pos;
80
+ pos++; // skip opening quote
81
+ let value = '';
82
+ while (pos < input.length && input[pos] !== "'") {
83
+ if (input[pos] === '\\' &&
84
+ pos + 1 < input.length &&
85
+ input[pos + 1] === "'") {
86
+ value += "'";
87
+ pos += 2;
88
+ }
89
+ else {
90
+ value += input[pos];
91
+ pos++;
92
+ }
93
+ }
94
+ pos++; // skip closing quote
95
+ tokens.push({ type: TokenType.STRING, value, start, end: pos });
96
+ continue;
97
+ }
98
+ // Boolean literal
99
+ if (input.slice(pos, pos + 4) === 'true' &&
100
+ !isIdentifierChar(input[pos + 4])) {
101
+ tokens.push({
102
+ type: TokenType.BOOLEAN,
103
+ value: 'true',
104
+ start: pos,
105
+ end: pos + 4,
106
+ });
107
+ pos += 4;
108
+ continue;
109
+ }
110
+ if (input.slice(pos, pos + 5) === 'false' &&
111
+ !isIdentifierChar(input[pos + 5])) {
112
+ tokens.push({
113
+ type: TokenType.BOOLEAN,
114
+ value: 'false',
115
+ start: pos,
116
+ end: pos + 5,
117
+ });
118
+ pos += 5;
119
+ continue;
120
+ }
121
+ // Number literal (including negative)
122
+ if (char === '-' || isDigit(char)) {
123
+ const start = pos;
124
+ if (char === '-')
125
+ pos++;
126
+ while (pos < input.length && isDigit(input[pos])) {
127
+ pos++;
128
+ }
129
+ if (pos < input.length && input[pos] === '.') {
130
+ pos++;
131
+ while (pos < input.length && isDigit(input[pos])) {
132
+ pos++;
133
+ }
134
+ }
135
+ const value = input.slice(start, pos);
136
+ // Only treat as number if it's more than just '-'
137
+ if (value !== '-') {
138
+ tokens.push({ type: TokenType.NUMBER, value, start, end: pos });
139
+ continue;
140
+ }
141
+ // Reset if just '-'
142
+ pos = start;
143
+ }
144
+ // Nested expression start
145
+ if (input.slice(pos, pos + 2) === '${') {
146
+ tokens.push({
147
+ type: TokenType.EXPR_START,
148
+ value: '${',
149
+ start: pos,
150
+ end: pos + 2,
151
+ });
152
+ pos += 2;
153
+ expressionDepth++; // Increment depth for nesting
154
+ continue;
155
+ }
156
+ // Path (starts with / for absolute, or identifier chars for relative)
157
+ if (char === '/') {
158
+ const start = pos;
159
+ pos++; // skip leading /
160
+ while (pos < input.length && isPathChar(input[pos])) {
161
+ pos++;
162
+ }
163
+ const value = input.slice(start, pos);
164
+ tokens.push({ type: TokenType.PATH, value, start, end: pos });
165
+ continue;
166
+ }
167
+ // Identifier or relative path
168
+ if (isIdentifierStart(char)) {
169
+ const start = pos;
170
+ while (pos < input.length && isPathChar(input[pos])) {
171
+ pos++;
172
+ }
173
+ const value = input.slice(start, pos);
174
+ // Check if it's a function call (followed by '(')
175
+ let lookahead = pos;
176
+ while (lookahead < input.length && /\s/.test(input[lookahead])) {
177
+ lookahead++;
178
+ }
179
+ if (input[lookahead] === '(') {
180
+ tokens.push({ type: TokenType.IDENTIFIER, value, start, end: pos });
181
+ }
182
+ else {
183
+ // It's a relative path
184
+ tokens.push({ type: TokenType.PATH, value, start, end: pos });
185
+ }
186
+ continue;
187
+ }
188
+ // Unknown character - skip
189
+ pos++;
190
+ }
191
+ else {
192
+ // TEXT mode - collect literal text until ${ or end
193
+ const start = pos;
194
+ let text = '';
195
+ while (pos < input.length) {
196
+ // Check for escaped expression: \${
197
+ if (input[pos] === '\\' && input.slice(pos + 1, pos + 3) === '${') {
198
+ text += '${';
199
+ pos += 3;
200
+ continue;
201
+ }
202
+ // Check for expression start: ${
203
+ if (input.slice(pos, pos + 2) === '${') {
204
+ break;
205
+ }
206
+ text += input[pos];
207
+ pos++;
208
+ }
209
+ if (text.length > 0) {
210
+ tokens.push({ type: TokenType.TEXT, value: text, start, end: pos });
211
+ }
212
+ // Expression start
213
+ if (input.slice(pos, pos + 2) === '${') {
214
+ tokens.push({
215
+ type: TokenType.EXPR_START,
216
+ value: '${',
217
+ start: pos,
218
+ end: pos + 2,
219
+ });
220
+ pos += 2;
221
+ expressionDepth++; // Increment depth
222
+ }
223
+ }
224
+ }
225
+ tokens.push({ type: TokenType.EOF, value: '', start: pos, end: pos });
226
+ return tokens;
227
+ }
228
+ function isDigit(char) {
229
+ return char !== undefined && char >= '0' && char <= '9';
230
+ }
231
+ function isIdentifierStart(char) {
232
+ return (char !== undefined &&
233
+ ((char >= 'a' && char <= 'z') ||
234
+ (char >= 'A' && char <= 'Z') ||
235
+ char === '_'));
236
+ }
237
+ function isIdentifierChar(char) {
238
+ return isIdentifierStart(char) || isDigit(char);
239
+ }
240
+ function isPathChar(char) {
241
+ if (char === undefined)
242
+ return false;
243
+ // Path can contain: letters, digits, underscore, hyphen, dot, tilde, slash
244
+ // But not: }, (, ), ,, ', whitespace
245
+ return (isIdentifierChar(char) ||
246
+ char === '/' ||
247
+ char === '-' ||
248
+ char === '.' ||
249
+ char === '~');
250
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for the lexer (tokenizer).
3
+ */
4
+ export {};