@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.
- package/dist/0.9/index.d.ts +1 -0
- package/dist/0.9/index.js +1 -0
- package/dist/0.9/interpolation/evaluator.d.ts +15 -0
- package/dist/0.9/interpolation/evaluator.js +95 -0
- package/dist/0.9/interpolation/evaluator.test.d.ts +4 -0
- package/dist/0.9/interpolation/evaluator.test.js +699 -0
- package/dist/0.9/interpolation/index.d.ts +70 -0
- package/dist/0.9/interpolation/index.js +84 -0
- package/dist/0.9/interpolation/lexer.d.ts +18 -0
- package/dist/0.9/interpolation/lexer.js +250 -0
- package/dist/0.9/interpolation/lexer.test.d.ts +4 -0
- package/dist/0.9/interpolation/lexer.test.js +360 -0
- package/dist/0.9/interpolation/parser.d.ts +14 -0
- package/dist/0.9/interpolation/parser.js +236 -0
- package/dist/0.9/interpolation/parser.test.d.ts +4 -0
- package/dist/0.9/interpolation/parser.test.js +314 -0
- package/dist/0.9/interpolation/types.d.ts +124 -0
- package/dist/0.9/interpolation/types.js +36 -0
- package/dist/0.9/interpolation.test.d.ts +5 -0
- package/dist/0.9/interpolation.test.js +154 -0
- package/dist/0.9/pathUtils.d.ts +115 -0
- package/dist/0.9/pathUtils.js +256 -0
- package/dist/0.9/pathUtils.test.d.ts +6 -0
- package/dist/0.9/pathUtils.test.js +330 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +40 -0
|
@@ -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
|
+
}
|