@pactor-app/expression 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.cjs +606 -0
- package/dist/index.d.cts +119 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +575 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
// src/error.ts
|
|
2
|
+
var ExpressionError = class extends Error {
|
|
3
|
+
constructor(message, pos) {
|
|
4
|
+
super(pos === void 0 ? message : `${message} (at position ${pos})`);
|
|
5
|
+
this.pos = pos;
|
|
6
|
+
this.name = "ExpressionError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// src/tokenizer.ts
|
|
11
|
+
var PUNCTUATORS = [
|
|
12
|
+
"===",
|
|
13
|
+
"!==",
|
|
14
|
+
"&&",
|
|
15
|
+
"||",
|
|
16
|
+
"==",
|
|
17
|
+
"!=",
|
|
18
|
+
"<=",
|
|
19
|
+
">=",
|
|
20
|
+
"(",
|
|
21
|
+
")",
|
|
22
|
+
",",
|
|
23
|
+
".",
|
|
24
|
+
"?",
|
|
25
|
+
":",
|
|
26
|
+
"<",
|
|
27
|
+
">",
|
|
28
|
+
"+",
|
|
29
|
+
"-",
|
|
30
|
+
"*",
|
|
31
|
+
"/",
|
|
32
|
+
"%",
|
|
33
|
+
"!",
|
|
34
|
+
"[",
|
|
35
|
+
"]",
|
|
36
|
+
"{",
|
|
37
|
+
"}"
|
|
38
|
+
];
|
|
39
|
+
var IDENTIFIER_START = /[A-Za-z_$]/;
|
|
40
|
+
var IDENTIFIER_PART = /[A-Za-z0-9_$]/;
|
|
41
|
+
var DIGIT = /[0-9]/;
|
|
42
|
+
function tokenize(source) {
|
|
43
|
+
const tokens = [];
|
|
44
|
+
let i = 0;
|
|
45
|
+
while (i < source.length) {
|
|
46
|
+
const ch = source[i];
|
|
47
|
+
if (/\s/.test(ch)) {
|
|
48
|
+
i += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (DIGIT.test(ch)) {
|
|
52
|
+
const start = i;
|
|
53
|
+
while (i < source.length && DIGIT.test(source[i])) i += 1;
|
|
54
|
+
if (source[i] === ".") {
|
|
55
|
+
i += 1;
|
|
56
|
+
while (i < source.length && DIGIT.test(source[i])) i += 1;
|
|
57
|
+
}
|
|
58
|
+
tokens.push({ type: "number", value: source.slice(start, i), pos: start });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (ch === "'" || ch === '"') {
|
|
62
|
+
const start = i;
|
|
63
|
+
const quote = ch;
|
|
64
|
+
i += 1;
|
|
65
|
+
let value = "";
|
|
66
|
+
let closed = false;
|
|
67
|
+
while (i < source.length) {
|
|
68
|
+
const c = source[i];
|
|
69
|
+
if (c === "\\") {
|
|
70
|
+
const next = source[i + 1];
|
|
71
|
+
if (next === void 0) break;
|
|
72
|
+
value += next === "n" ? "\n" : next === "t" ? " " : next;
|
|
73
|
+
i += 2;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (c === quote) {
|
|
77
|
+
closed = true;
|
|
78
|
+
i += 1;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
value += c;
|
|
82
|
+
i += 1;
|
|
83
|
+
}
|
|
84
|
+
if (!closed) {
|
|
85
|
+
throw new ExpressionError("unterminated string literal", start);
|
|
86
|
+
}
|
|
87
|
+
tokens.push({ type: "string", value, pos: start });
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (IDENTIFIER_START.test(ch)) {
|
|
91
|
+
const start = i;
|
|
92
|
+
while (i < source.length && IDENTIFIER_PART.test(source[i])) i += 1;
|
|
93
|
+
tokens.push({ type: "identifier", value: source.slice(start, i), pos: start });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const punct = PUNCTUATORS.find((p) => source.startsWith(p, i));
|
|
97
|
+
if (punct) {
|
|
98
|
+
tokens.push({ type: "punctuator", value: punct, pos: i });
|
|
99
|
+
i += punct.length;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw new ExpressionError(`unexpected character "${ch}"`, i);
|
|
103
|
+
}
|
|
104
|
+
return tokens;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/parser.ts
|
|
108
|
+
function parseExpression(source) {
|
|
109
|
+
const parser = new Parser(tokenize(source));
|
|
110
|
+
const node = parser.parseConditional();
|
|
111
|
+
parser.expectEnd();
|
|
112
|
+
return node;
|
|
113
|
+
}
|
|
114
|
+
var Parser = class {
|
|
115
|
+
constructor(tokens) {
|
|
116
|
+
this.tokens = tokens;
|
|
117
|
+
this.index = 0;
|
|
118
|
+
}
|
|
119
|
+
peek() {
|
|
120
|
+
return this.tokens[this.index];
|
|
121
|
+
}
|
|
122
|
+
next() {
|
|
123
|
+
const token = this.tokens[this.index];
|
|
124
|
+
if (!token) {
|
|
125
|
+
throw new ExpressionError("unexpected end of expression");
|
|
126
|
+
}
|
|
127
|
+
this.index += 1;
|
|
128
|
+
return token;
|
|
129
|
+
}
|
|
130
|
+
isPunctuator(value) {
|
|
131
|
+
const token = this.peek();
|
|
132
|
+
return token?.type === "punctuator" && token.value === value;
|
|
133
|
+
}
|
|
134
|
+
eatPunctuator(value) {
|
|
135
|
+
if (this.isPunctuator(value)) {
|
|
136
|
+
this.index += 1;
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
expectPunctuator(value) {
|
|
142
|
+
const token = this.peek();
|
|
143
|
+
if (!this.eatPunctuator(value)) {
|
|
144
|
+
throw new ExpressionError(`expected "${value}" but got "${token?.value ?? "end of expression"}"`, token?.pos);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
expectEnd() {
|
|
148
|
+
const token = this.peek();
|
|
149
|
+
if (token) {
|
|
150
|
+
throw new ExpressionError(`unexpected token "${token.value}"`, token.pos);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
parseConditional() {
|
|
154
|
+
const test = this.parseLogicalOr();
|
|
155
|
+
if (this.eatPunctuator("?")) {
|
|
156
|
+
const consequent = this.parseConditional();
|
|
157
|
+
this.expectPunctuator(":");
|
|
158
|
+
const alternate = this.parseConditional();
|
|
159
|
+
return { type: "ConditionalExpression", test, consequent, alternate };
|
|
160
|
+
}
|
|
161
|
+
return test;
|
|
162
|
+
}
|
|
163
|
+
parseLogicalOr() {
|
|
164
|
+
return this.parseBinaryChain(
|
|
165
|
+
() => this.parseLogicalAnd(),
|
|
166
|
+
["||"],
|
|
167
|
+
"LogicalExpression"
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
parseLogicalAnd() {
|
|
171
|
+
return this.parseBinaryChain(
|
|
172
|
+
() => this.parseEquality(),
|
|
173
|
+
["&&"],
|
|
174
|
+
"LogicalExpression"
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
parseEquality() {
|
|
178
|
+
return this.parseBinaryChain(
|
|
179
|
+
() => this.parseRelational(),
|
|
180
|
+
["==", "!=", "===", "!=="],
|
|
181
|
+
"BinaryExpression"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
parseRelational() {
|
|
185
|
+
return this.parseBinaryChain(
|
|
186
|
+
() => this.parseAdditive(),
|
|
187
|
+
["<", "<=", ">", ">="],
|
|
188
|
+
"BinaryExpression"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
parseAdditive() {
|
|
192
|
+
return this.parseBinaryChain(
|
|
193
|
+
() => this.parseMultiplicative(),
|
|
194
|
+
["+", "-"],
|
|
195
|
+
"BinaryExpression"
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
parseMultiplicative() {
|
|
199
|
+
return this.parseBinaryChain(
|
|
200
|
+
() => this.parseUnary(),
|
|
201
|
+
["*", "/", "%"],
|
|
202
|
+
"BinaryExpression"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
parseBinaryChain(parseOperand, operators, kind) {
|
|
206
|
+
let left = parseOperand();
|
|
207
|
+
for (; ; ) {
|
|
208
|
+
const token = this.peek();
|
|
209
|
+
if (token?.type !== "punctuator" || !operators.includes(token.value)) {
|
|
210
|
+
return left;
|
|
211
|
+
}
|
|
212
|
+
this.next();
|
|
213
|
+
const right = parseOperand();
|
|
214
|
+
if (kind === "LogicalExpression") {
|
|
215
|
+
left = {
|
|
216
|
+
type: "LogicalExpression",
|
|
217
|
+
operator: token.value,
|
|
218
|
+
left,
|
|
219
|
+
right
|
|
220
|
+
};
|
|
221
|
+
} else {
|
|
222
|
+
left = {
|
|
223
|
+
type: "BinaryExpression",
|
|
224
|
+
operator: token.value,
|
|
225
|
+
left,
|
|
226
|
+
right
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
parseUnary() {
|
|
232
|
+
const token = this.peek();
|
|
233
|
+
if (token?.type === "punctuator" && (token.value === "!" || token.value === "-" || token.value === "+")) {
|
|
234
|
+
this.next();
|
|
235
|
+
return {
|
|
236
|
+
type: "UnaryExpression",
|
|
237
|
+
operator: token.value,
|
|
238
|
+
argument: this.parseUnary()
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return this.parseMemberOrCall();
|
|
242
|
+
}
|
|
243
|
+
parseMemberOrCall() {
|
|
244
|
+
let node = this.parsePrimary();
|
|
245
|
+
for (; ; ) {
|
|
246
|
+
if (this.eatPunctuator(".")) {
|
|
247
|
+
const property = this.next();
|
|
248
|
+
if (property.type !== "identifier") {
|
|
249
|
+
throw new ExpressionError(
|
|
250
|
+
`expected property name after "." but got "${property.value}"`,
|
|
251
|
+
property.pos
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
node = {
|
|
255
|
+
type: "MemberExpression",
|
|
256
|
+
object: node,
|
|
257
|
+
property: { type: "Identifier", name: property.value },
|
|
258
|
+
computed: false
|
|
259
|
+
};
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (this.eatPunctuator("[")) {
|
|
263
|
+
const property = this.parseConditional();
|
|
264
|
+
this.expectPunctuator("]");
|
|
265
|
+
node = {
|
|
266
|
+
type: "MemberExpression",
|
|
267
|
+
object: node,
|
|
268
|
+
property,
|
|
269
|
+
computed: true
|
|
270
|
+
};
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (this.isPunctuator("(")) {
|
|
274
|
+
if (node.type !== "Identifier") {
|
|
275
|
+
throw new ExpressionError(
|
|
276
|
+
"member method calls are not allowed; only whitelisted functions can be called",
|
|
277
|
+
this.peek()?.pos
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
this.next();
|
|
281
|
+
const args = [];
|
|
282
|
+
if (!this.isPunctuator(")")) {
|
|
283
|
+
for (; ; ) {
|
|
284
|
+
args.push(this.parseConditional());
|
|
285
|
+
if (!this.eatPunctuator(",")) break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
this.expectPunctuator(")");
|
|
289
|
+
node = { type: "CallExpression", callee: node, arguments: args };
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
return node;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
parsePrimary() {
|
|
296
|
+
const token = this.next();
|
|
297
|
+
if (token.type === "number") {
|
|
298
|
+
return { type: "Literal", value: Number(token.value) };
|
|
299
|
+
}
|
|
300
|
+
if (token.type === "string") {
|
|
301
|
+
return { type: "Literal", value: token.value };
|
|
302
|
+
}
|
|
303
|
+
if (token.type === "identifier") {
|
|
304
|
+
switch (token.value) {
|
|
305
|
+
case "true":
|
|
306
|
+
return { type: "Literal", value: true };
|
|
307
|
+
case "false":
|
|
308
|
+
return { type: "Literal", value: false };
|
|
309
|
+
case "null":
|
|
310
|
+
return { type: "Literal", value: null };
|
|
311
|
+
case "undefined":
|
|
312
|
+
return { type: "Literal", value: void 0 };
|
|
313
|
+
default:
|
|
314
|
+
return { type: "Identifier", name: token.value };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (token.type === "punctuator" && token.value === "(") {
|
|
318
|
+
const node = this.parseConditional();
|
|
319
|
+
this.expectPunctuator(")");
|
|
320
|
+
return node;
|
|
321
|
+
}
|
|
322
|
+
if (token.type === "punctuator" && token.value === "{") {
|
|
323
|
+
const properties = [];
|
|
324
|
+
if (!this.isPunctuator("}")) {
|
|
325
|
+
for (; ; ) {
|
|
326
|
+
const keyToken = this.next();
|
|
327
|
+
if (keyToken.type !== "identifier" && keyToken.type !== "string") {
|
|
328
|
+
throw new ExpressionError(
|
|
329
|
+
`expected object key but got "${keyToken.value}"`,
|
|
330
|
+
keyToken.pos
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
this.expectPunctuator(":");
|
|
334
|
+
properties.push({
|
|
335
|
+
key: keyToken.value,
|
|
336
|
+
value: this.parseConditional()
|
|
337
|
+
});
|
|
338
|
+
if (!this.eatPunctuator(",")) break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.expectPunctuator("}");
|
|
342
|
+
return { type: "ObjectExpression", properties };
|
|
343
|
+
}
|
|
344
|
+
throw new ExpressionError(`unexpected token "${token.value}"`, token.pos);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// src/evaluator.ts
|
|
349
|
+
import { Registry } from "@pactor-app/core";
|
|
350
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
351
|
+
function isRecord(value) {
|
|
352
|
+
return typeof value === "object" && value !== null;
|
|
353
|
+
}
|
|
354
|
+
function readProperty(source, key) {
|
|
355
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(source)) {
|
|
359
|
+
if (key === "length") return source.length;
|
|
360
|
+
const index = Number(key);
|
|
361
|
+
if (Number.isInteger(index)) return source[index];
|
|
362
|
+
return void 0;
|
|
363
|
+
}
|
|
364
|
+
if (isRecord(source)) {
|
|
365
|
+
return Object.prototype.hasOwnProperty.call(source, key) ? source[key] : void 0;
|
|
366
|
+
}
|
|
367
|
+
return void 0;
|
|
368
|
+
}
|
|
369
|
+
function resolveFunction(name, scope, options) {
|
|
370
|
+
const { functions } = options;
|
|
371
|
+
let candidate;
|
|
372
|
+
if (functions instanceof Registry) {
|
|
373
|
+
candidate = functions.get(name);
|
|
374
|
+
} else if (functions) {
|
|
375
|
+
candidate = readProperty(functions, name);
|
|
376
|
+
}
|
|
377
|
+
if (typeof candidate !== "function") {
|
|
378
|
+
candidate = readProperty(scope, name);
|
|
379
|
+
}
|
|
380
|
+
if (typeof candidate !== "function") {
|
|
381
|
+
throw new ExpressionError(`function "${name}" is not registered`);
|
|
382
|
+
}
|
|
383
|
+
return candidate;
|
|
384
|
+
}
|
|
385
|
+
function evaluateNode(node, scope, options) {
|
|
386
|
+
switch (node.type) {
|
|
387
|
+
case "Literal":
|
|
388
|
+
return node.value;
|
|
389
|
+
case "Identifier":
|
|
390
|
+
return readProperty(scope, node.name);
|
|
391
|
+
case "MemberExpression": {
|
|
392
|
+
const object = evaluateNode(node.object, scope, options);
|
|
393
|
+
if (object === null || object === void 0) {
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
const property = node.computed ? evaluateNode(node.property, scope, options) : node.property.name;
|
|
397
|
+
if (typeof property !== "string" && typeof property !== "number") {
|
|
398
|
+
return void 0;
|
|
399
|
+
}
|
|
400
|
+
return readProperty(object, String(property));
|
|
401
|
+
}
|
|
402
|
+
case "UnaryExpression": {
|
|
403
|
+
const argument = evaluateNode(node.argument, scope, options);
|
|
404
|
+
switch (node.operator) {
|
|
405
|
+
case "!":
|
|
406
|
+
return !argument;
|
|
407
|
+
case "-":
|
|
408
|
+
return -argument;
|
|
409
|
+
case "+":
|
|
410
|
+
return +argument;
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
case "BinaryExpression": {
|
|
415
|
+
const left = evaluateNode(node.left, scope, options);
|
|
416
|
+
const right = evaluateNode(node.right, scope, options);
|
|
417
|
+
switch (node.operator) {
|
|
418
|
+
case "==":
|
|
419
|
+
return left == right;
|
|
420
|
+
case "!=":
|
|
421
|
+
return left != right;
|
|
422
|
+
case "===":
|
|
423
|
+
return left === right;
|
|
424
|
+
case "!==":
|
|
425
|
+
return left !== right;
|
|
426
|
+
case "<":
|
|
427
|
+
return left < right;
|
|
428
|
+
case "<=":
|
|
429
|
+
return left <= right;
|
|
430
|
+
case ">":
|
|
431
|
+
return left > right;
|
|
432
|
+
case ">=":
|
|
433
|
+
return left >= right;
|
|
434
|
+
case "+":
|
|
435
|
+
return left + right;
|
|
436
|
+
case "-":
|
|
437
|
+
return left - right;
|
|
438
|
+
case "*":
|
|
439
|
+
return left * right;
|
|
440
|
+
case "/":
|
|
441
|
+
return left / right;
|
|
442
|
+
case "%":
|
|
443
|
+
return left % right;
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case "LogicalExpression":
|
|
448
|
+
if (node.operator === "&&") {
|
|
449
|
+
const left = evaluateNode(node.left, scope, options);
|
|
450
|
+
return left ? evaluateNode(node.right, scope, options) : left;
|
|
451
|
+
}
|
|
452
|
+
{
|
|
453
|
+
const left = evaluateNode(node.left, scope, options);
|
|
454
|
+
return left ? left : evaluateNode(node.right, scope, options);
|
|
455
|
+
}
|
|
456
|
+
case "ConditionalExpression":
|
|
457
|
+
return evaluateNode(node.test, scope, options) ? evaluateNode(node.consequent, scope, options) : evaluateNode(node.alternate, scope, options);
|
|
458
|
+
case "CallExpression": {
|
|
459
|
+
const fn = resolveFunction(node.callee.name, scope, options);
|
|
460
|
+
const args = node.arguments.map((arg) => evaluateNode(arg, scope, options));
|
|
461
|
+
return fn(...args);
|
|
462
|
+
}
|
|
463
|
+
case "ObjectExpression": {
|
|
464
|
+
const out = {};
|
|
465
|
+
for (const { key, value } of node.properties) {
|
|
466
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
467
|
+
throw new ExpressionError(`object literal key "${key}" is not allowed`);
|
|
468
|
+
}
|
|
469
|
+
out[key] = evaluateNode(value, scope, options);
|
|
470
|
+
}
|
|
471
|
+
return out;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function evaluateExpression(source, scope = {}, options = {}) {
|
|
476
|
+
const node = typeof source === "string" ? parseExpression(source) : source;
|
|
477
|
+
return evaluateNode(node, scope, options);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/template.ts
|
|
481
|
+
function hasExpression(value) {
|
|
482
|
+
return value.includes("${");
|
|
483
|
+
}
|
|
484
|
+
function splitTemplate(value) {
|
|
485
|
+
const segments = [];
|
|
486
|
+
let textStart = 0;
|
|
487
|
+
let i = 0;
|
|
488
|
+
while (i < value.length) {
|
|
489
|
+
if (value[i] === "$" && value[i + 1] === "{") {
|
|
490
|
+
if (i > textStart) {
|
|
491
|
+
segments.push({ kind: "text", text: value.slice(textStart, i) });
|
|
492
|
+
}
|
|
493
|
+
const exprPos = i;
|
|
494
|
+
i += 2;
|
|
495
|
+
let depth = 1;
|
|
496
|
+
let quote;
|
|
497
|
+
let source = "";
|
|
498
|
+
let closed = false;
|
|
499
|
+
while (i < value.length) {
|
|
500
|
+
const c = value[i];
|
|
501
|
+
if (quote !== void 0) {
|
|
502
|
+
if (c === "\\" && i + 1 < value.length) {
|
|
503
|
+
source += c + value[i + 1];
|
|
504
|
+
i += 2;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (c === quote) {
|
|
508
|
+
quote = void 0;
|
|
509
|
+
}
|
|
510
|
+
source += c;
|
|
511
|
+
i += 1;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (c === "'" || c === '"') {
|
|
515
|
+
quote = c;
|
|
516
|
+
source += c;
|
|
517
|
+
i += 1;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (c === "{") {
|
|
521
|
+
depth += 1;
|
|
522
|
+
source += c;
|
|
523
|
+
i += 1;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (c === "}") {
|
|
527
|
+
depth -= 1;
|
|
528
|
+
if (depth === 0) {
|
|
529
|
+
closed = true;
|
|
530
|
+
i += 1;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
source += c;
|
|
534
|
+
i += 1;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
source += c;
|
|
538
|
+
i += 1;
|
|
539
|
+
}
|
|
540
|
+
if (!closed) {
|
|
541
|
+
throw new ExpressionError('unterminated "${" in template', exprPos);
|
|
542
|
+
}
|
|
543
|
+
segments.push({ kind: "expr", source, pos: exprPos });
|
|
544
|
+
textStart = i;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
i += 1;
|
|
548
|
+
}
|
|
549
|
+
if (textStart < value.length) {
|
|
550
|
+
segments.push({ kind: "text", text: value.slice(textStart) });
|
|
551
|
+
}
|
|
552
|
+
return segments;
|
|
553
|
+
}
|
|
554
|
+
function stringify(value) {
|
|
555
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
556
|
+
}
|
|
557
|
+
function resolveTemplate(value, scope = {}, options = {}) {
|
|
558
|
+
if (!hasExpression(value)) {
|
|
559
|
+
return value;
|
|
560
|
+
}
|
|
561
|
+
const segments = splitTemplate(value);
|
|
562
|
+
if (segments.length === 1 && segments[0].kind === "expr") {
|
|
563
|
+
return evaluateExpression(segments[0].source, scope, options);
|
|
564
|
+
}
|
|
565
|
+
return segments.map(
|
|
566
|
+
(segment) => segment.kind === "text" ? segment.text : stringify(evaluateExpression(segment.source, scope, options))
|
|
567
|
+
).join("");
|
|
568
|
+
}
|
|
569
|
+
export {
|
|
570
|
+
ExpressionError,
|
|
571
|
+
evaluateExpression,
|
|
572
|
+
hasExpression,
|
|
573
|
+
parseExpression,
|
|
574
|
+
resolveTemplate
|
|
575
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pactor-app/expression",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pactor expression: controlled AST expression engine (no eval, no global access)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pactor",
|
|
7
|
+
"dsl",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"expression",
|
|
10
|
+
"template",
|
|
11
|
+
"sandbox",
|
|
12
|
+
"parser"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/426-330/pactor.git",
|
|
18
|
+
"directory": "packages/expression"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/426-330/pactor/tree/main/packages/expression",
|
|
21
|
+
"bugs": "https://github.com/426-330/pactor/issues",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.cjs",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": {
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"require": {
|
|
33
|
+
"types": "./dist/index.d.cts",
|
|
34
|
+
"default": "./dist/index.cjs"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@pactor-app/core": "0.1.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsup",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"typecheck": "tsc --noEmit"
|
|
55
|
+
}
|
|
56
|
+
}
|