@reidelsaltres/pureper 0.1.157 → 0.1.160
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/out/foundation/api/Observer.d.ts +30 -0
- package/out/foundation/api/Observer.d.ts.map +1 -1
- package/out/foundation/api/Observer.js +48 -0
- package/out/foundation/api/Observer.js.map +1 -1
- package/out/foundation/engine/BalancedParser.d.ts +58 -0
- package/out/foundation/engine/BalancedParser.d.ts.map +1 -0
- package/out/foundation/engine/BalancedParser.js +301 -0
- package/out/foundation/engine/BalancedParser.js.map +1 -0
- package/out/foundation/engine/EscapeHandler.d.ts +27 -0
- package/out/foundation/engine/EscapeHandler.d.ts.map +1 -0
- package/out/foundation/engine/EscapeHandler.js +47 -0
- package/out/foundation/engine/EscapeHandler.js.map +1 -0
- package/out/foundation/engine/Expression.d.ts +83 -0
- package/out/foundation/engine/Expression.d.ts.map +1 -0
- package/out/foundation/engine/Expression.js +256 -0
- package/out/foundation/engine/Expression.js.map +1 -0
- package/out/foundation/engine/Rule.d.ts +83 -0
- package/out/foundation/engine/Rule.d.ts.map +1 -0
- package/out/foundation/engine/Rule.js +69 -0
- package/out/foundation/engine/Rule.js.map +1 -0
- package/out/foundation/engine/Scope.d.ts +57 -0
- package/out/foundation/engine/Scope.d.ts.map +1 -0
- package/out/foundation/engine/Scope.js +147 -0
- package/out/foundation/engine/Scope.js.map +1 -0
- package/out/foundation/engine/TemplateEngine.d.ts +79 -0
- package/out/foundation/engine/TemplateEngine.d.ts.map +1 -0
- package/out/foundation/engine/TemplateEngine.js +187 -0
- package/out/foundation/engine/TemplateEngine.js.map +1 -0
- package/out/foundation/engine/TemplateInstance.d.ts +121 -0
- package/out/foundation/engine/TemplateInstance.d.ts.map +1 -0
- package/out/foundation/engine/TemplateInstance.js +255 -0
- package/out/foundation/engine/TemplateInstance.js.map +1 -0
- package/out/foundation/engine/exceptions/TemplateExceptions.d.ts +21 -0
- package/out/foundation/engine/exceptions/TemplateExceptions.d.ts.map +1 -0
- package/out/foundation/engine/exceptions/TemplateExceptions.js +26 -0
- package/out/foundation/engine/exceptions/TemplateExceptions.js.map +1 -0
- package/out/foundation/engine/index.d.ts +18 -0
- package/out/foundation/engine/index.d.ts.map +1 -0
- package/out/foundation/engine/index.js +19 -0
- package/out/foundation/engine/index.js.map +1 -0
- package/out/foundation/engine/rules/attribute/EventRule.d.ts +22 -0
- package/out/foundation/engine/rules/attribute/EventRule.d.ts.map +1 -0
- package/out/foundation/engine/rules/attribute/EventRule.js +129 -0
- package/out/foundation/engine/rules/attribute/EventRule.js.map +1 -0
- package/out/foundation/engine/rules/attribute/InjectionRule.d.ts +20 -0
- package/out/foundation/engine/rules/attribute/InjectionRule.d.ts.map +1 -0
- package/out/foundation/engine/rules/attribute/InjectionRule.js +108 -0
- package/out/foundation/engine/rules/attribute/InjectionRule.js.map +1 -0
- package/out/foundation/engine/rules/attribute/RefRule.d.ts +23 -0
- package/out/foundation/engine/rules/attribute/RefRule.d.ts.map +1 -0
- package/out/foundation/engine/rules/attribute/RefRule.js +98 -0
- package/out/foundation/engine/rules/attribute/RefRule.js.map +1 -0
- package/out/foundation/engine/rules/syntax/ExpressionRule.d.ts +19 -0
- package/out/foundation/engine/rules/syntax/ExpressionRule.d.ts.map +1 -0
- package/out/foundation/engine/rules/syntax/ExpressionRule.js +82 -0
- package/out/foundation/engine/rules/syntax/ExpressionRule.js.map +1 -0
- package/out/foundation/engine/rules/syntax/ForRule.d.ts +19 -0
- package/out/foundation/engine/rules/syntax/ForRule.d.ts.map +1 -0
- package/out/foundation/engine/rules/syntax/ForRule.js +226 -0
- package/out/foundation/engine/rules/syntax/ForRule.js.map +1 -0
- package/out/foundation/engine/rules/syntax/IfRule.d.ts +17 -0
- package/out/foundation/engine/rules/syntax/IfRule.d.ts.map +1 -0
- package/out/foundation/engine/rules/syntax/IfRule.js +220 -0
- package/out/foundation/engine/rules/syntax/IfRule.js.map +1 -0
- package/out/foundation/worker/Router.d.ts.map +1 -1
- package/out/foundation/worker/Router.js.map +1 -1
- package/out/index.d.ts +3 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +3 -0
- package/out/index.js.map +1 -1
- package/package.json +1 -1
- package/src/foundation/api/Observer.ts +60 -0
- package/src/foundation/engine/BalancedParser.ts +353 -0
- package/src/foundation/engine/EscapeHandler.ts +54 -0
- package/src/foundation/engine/Expression.ts +285 -0
- package/src/foundation/engine/Rule.ts +136 -0
- package/src/foundation/engine/Scope.ts +166 -0
- package/src/foundation/engine/TemplateEngine.ts +243 -0
- package/src/foundation/engine/TemplateInstance.ts +355 -0
- package/src/foundation/engine/exceptions/TemplateExceptions.ts +27 -0
- package/src/foundation/engine/rules/attribute/EventRule.ts +171 -0
- package/src/foundation/engine/rules/attribute/InjectionRule.ts +140 -0
- package/src/foundation/engine/rules/attribute/RefRule.ts +119 -0
- package/src/foundation/engine/rules/syntax/ExpressionRule.ts +102 -0
- package/src/foundation/engine/rules/syntax/ForRule.ts +267 -0
- package/src/foundation/engine/rules/syntax/IfRule.ts +261 -0
- package/src/foundation/worker/Router.ts +1 -1
- package/src/index.ts +9 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BalancedParser - утилита для парсинга сбалансированных скобок (), {}.
|
|
3
|
+
* Корректно обрабатывает строки и комментарии.
|
|
4
|
+
*/
|
|
5
|
+
export default class BalancedParser {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Найти все сбалансированные выражения с заданным opener.
|
|
9
|
+
* @param input - входная строка
|
|
10
|
+
* @param opener - открывающая последовательность, например '@(' или '@for('
|
|
11
|
+
* @returns массив объектов с content и позициями
|
|
12
|
+
*/
|
|
13
|
+
public static parseBalanced(
|
|
14
|
+
input: string,
|
|
15
|
+
opener: string,
|
|
16
|
+
closerChar: ')' | '}' = ')'
|
|
17
|
+
): Array<{ content: string; start: number; end: number }> {
|
|
18
|
+
const results: Array<{ content: string; start: number; end: number }> = [];
|
|
19
|
+
const openerChar = closerChar === ')' ? '(' : '{';
|
|
20
|
+
let i = 0;
|
|
21
|
+
|
|
22
|
+
while (i < input.length) {
|
|
23
|
+
const idx = input.indexOf(opener, i);
|
|
24
|
+
if (idx === -1) break;
|
|
25
|
+
|
|
26
|
+
const contentStart = idx + opener.length;
|
|
27
|
+
let pos = contentStart;
|
|
28
|
+
let depth = 1;
|
|
29
|
+
|
|
30
|
+
while (pos < input.length && depth > 0) {
|
|
31
|
+
const ch = input[pos];
|
|
32
|
+
|
|
33
|
+
// Skip string literals
|
|
34
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
35
|
+
pos = this.skipString(input, pos, ch);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Skip single-line comments
|
|
40
|
+
if (ch === '/' && input[pos + 1] === '/') {
|
|
41
|
+
pos = this.skipLineComment(input, pos);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Skip multi-line comments
|
|
46
|
+
if (ch === '/' && input[pos + 1] === '*') {
|
|
47
|
+
pos = this.skipBlockComment(input, pos);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ch === openerChar) depth++;
|
|
52
|
+
else if (ch === closerChar) depth--;
|
|
53
|
+
|
|
54
|
+
pos++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (depth === 0) {
|
|
58
|
+
results.push({
|
|
59
|
+
content: input.slice(contentStart, pos - 1),
|
|
60
|
+
start: idx,
|
|
61
|
+
end: pos
|
|
62
|
+
});
|
|
63
|
+
i = pos;
|
|
64
|
+
} else {
|
|
65
|
+
// Unbalanced, skip this opener and continue
|
|
66
|
+
i = idx + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Парсить блочные Rule типа @for(...) { ... }
|
|
75
|
+
* @returns объект с condition (содержимое скобок) и block (содержимое фигурных скобок)
|
|
76
|
+
*/
|
|
77
|
+
public static parseBlockRule(
|
|
78
|
+
input: string,
|
|
79
|
+
opener: string // например '@for', '@if'
|
|
80
|
+
): Array<{ condition: string; block: string; start: number; end: number }> {
|
|
81
|
+
const results: Array<{ condition: string; block: string; start: number; end: number }> = [];
|
|
82
|
+
const openerLower = opener.toLowerCase();
|
|
83
|
+
let i = 0;
|
|
84
|
+
|
|
85
|
+
while (i < input.length) {
|
|
86
|
+
// Case-insensitive search
|
|
87
|
+
const lowerInput = input.toLowerCase();
|
|
88
|
+
let idx = lowerInput.indexOf(openerLower, i);
|
|
89
|
+
if (idx === -1) break;
|
|
90
|
+
|
|
91
|
+
// Check for @@ escape
|
|
92
|
+
if (idx > 0 && input[idx - 1] === '@') {
|
|
93
|
+
i = idx + 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find opening parenthesis
|
|
98
|
+
let parenStart = idx + opener.length;
|
|
99
|
+
while (parenStart < input.length && /\s/.test(input[parenStart])) {
|
|
100
|
+
parenStart++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (input[parenStart] !== '(') {
|
|
104
|
+
i = idx + 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parse balanced parentheses for condition
|
|
109
|
+
let pos = parenStart + 1;
|
|
110
|
+
let depth = 1;
|
|
111
|
+
|
|
112
|
+
while (pos < input.length && depth > 0) {
|
|
113
|
+
const ch = input[pos];
|
|
114
|
+
|
|
115
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
116
|
+
pos = this.skipString(input, pos, ch);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (ch === '/' && input[pos + 1] === '/') {
|
|
120
|
+
pos = this.skipLineComment(input, pos);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '/' && input[pos + 1] === '*') {
|
|
124
|
+
pos = this.skipBlockComment(input, pos);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (ch === '(') depth++;
|
|
129
|
+
else if (ch === ')') depth--;
|
|
130
|
+
pos++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (depth !== 0) {
|
|
134
|
+
i = idx + 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const condition = input.slice(parenStart + 1, pos - 1);
|
|
139
|
+
const conditionEnd = pos;
|
|
140
|
+
|
|
141
|
+
// Find opening brace for block
|
|
142
|
+
let braceStart = conditionEnd;
|
|
143
|
+
while (braceStart < input.length && /\s/.test(input[braceStart])) {
|
|
144
|
+
braceStart++;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (input[braceStart] !== '{') {
|
|
148
|
+
i = idx + 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parse balanced braces for block
|
|
153
|
+
pos = braceStart + 1;
|
|
154
|
+
depth = 1;
|
|
155
|
+
|
|
156
|
+
while (pos < input.length && depth > 0) {
|
|
157
|
+
const ch = input[pos];
|
|
158
|
+
|
|
159
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
160
|
+
pos = this.skipString(input, pos, ch);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === '/' && input[pos + 1] === '/') {
|
|
164
|
+
pos = this.skipLineComment(input, pos);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === '/' && input[pos + 1] === '*') {
|
|
168
|
+
pos = this.skipBlockComment(input, pos);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (ch === '{') depth++;
|
|
173
|
+
else if (ch === '}') depth--;
|
|
174
|
+
pos++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (depth !== 0) {
|
|
178
|
+
i = idx + 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const block = input.slice(braceStart + 1, pos - 1);
|
|
183
|
+
|
|
184
|
+
results.push({
|
|
185
|
+
condition,
|
|
186
|
+
block,
|
|
187
|
+
start: idx,
|
|
188
|
+
end: pos
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
i = pos;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Парсить @if/@elseif/@else цепочки
|
|
199
|
+
*/
|
|
200
|
+
public static parseIfChain(input: string): Array<{
|
|
201
|
+
type: 'if' | 'elseif' | 'else';
|
|
202
|
+
condition?: string;
|
|
203
|
+
block: string;
|
|
204
|
+
start: number;
|
|
205
|
+
end: number;
|
|
206
|
+
}> {
|
|
207
|
+
const results: Array<{
|
|
208
|
+
type: 'if' | 'elseif' | 'else';
|
|
209
|
+
condition?: string;
|
|
210
|
+
block: string;
|
|
211
|
+
start: number;
|
|
212
|
+
end: number;
|
|
213
|
+
}> = [];
|
|
214
|
+
|
|
215
|
+
// Find @if first
|
|
216
|
+
const ifMatches = this.parseBlockRule(input, '@if');
|
|
217
|
+
|
|
218
|
+
for (const ifMatch of ifMatches) {
|
|
219
|
+
results.push({
|
|
220
|
+
type: 'if',
|
|
221
|
+
condition: ifMatch.condition,
|
|
222
|
+
block: ifMatch.block,
|
|
223
|
+
start: ifMatch.start,
|
|
224
|
+
end: ifMatch.end
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Look for @elseif/@else after this @if
|
|
228
|
+
let searchPos = ifMatch.end;
|
|
229
|
+
|
|
230
|
+
while (searchPos < input.length) {
|
|
231
|
+
// Skip whitespace
|
|
232
|
+
while (searchPos < input.length && /\s/.test(input[searchPos])) {
|
|
233
|
+
searchPos++;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const remaining = input.slice(searchPos).toLowerCase();
|
|
237
|
+
|
|
238
|
+
if (remaining.startsWith('@elseif')) {
|
|
239
|
+
const elseifMatches = this.parseBlockRule(input.slice(searchPos), '@elseif');
|
|
240
|
+
if (elseifMatches.length > 0) {
|
|
241
|
+
const m = elseifMatches[0];
|
|
242
|
+
results.push({
|
|
243
|
+
type: 'elseif',
|
|
244
|
+
condition: m.condition,
|
|
245
|
+
block: m.block,
|
|
246
|
+
start: searchPos + m.start,
|
|
247
|
+
end: searchPos + m.end
|
|
248
|
+
});
|
|
249
|
+
searchPos = searchPos + m.end;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
} else if (remaining.startsWith('@else')) {
|
|
253
|
+
// @else without condition
|
|
254
|
+
let pos = searchPos + 5; // length of '@else'
|
|
255
|
+
while (pos < input.length && /\s/.test(input[pos])) {
|
|
256
|
+
pos++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (input[pos] === '{') {
|
|
260
|
+
let bracePos = pos + 1;
|
|
261
|
+
let depth = 1;
|
|
262
|
+
|
|
263
|
+
while (bracePos < input.length && depth > 0) {
|
|
264
|
+
const ch = input[bracePos];
|
|
265
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
266
|
+
bracePos = this.skipString(input, bracePos, ch);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (ch === '{') depth++;
|
|
270
|
+
else if (ch === '}') depth--;
|
|
271
|
+
bracePos++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (depth === 0) {
|
|
275
|
+
results.push({
|
|
276
|
+
type: 'else',
|
|
277
|
+
block: input.slice(pos + 1, bracePos - 1),
|
|
278
|
+
start: searchPos,
|
|
279
|
+
end: bracePos
|
|
280
|
+
});
|
|
281
|
+
searchPos = bracePos;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
break; // No more @elseif/@else found
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Skip over a string literal (handles escape sequences)
|
|
296
|
+
*/
|
|
297
|
+
private static skipString(input: string, pos: number, quote: string): number {
|
|
298
|
+
pos++; // skip opening quote
|
|
299
|
+
while (pos < input.length) {
|
|
300
|
+
if (input[pos] === '\\') {
|
|
301
|
+
pos += 2; // skip escape sequence
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (input[pos] === quote) {
|
|
305
|
+
return pos + 1;
|
|
306
|
+
}
|
|
307
|
+
// Handle template literal ${...}
|
|
308
|
+
if (quote === '`' && input[pos] === '$' && input[pos + 1] === '{') {
|
|
309
|
+
pos += 2;
|
|
310
|
+
let depth = 1;
|
|
311
|
+
while (pos < input.length && depth > 0) {
|
|
312
|
+
if (input[pos] === '{') depth++;
|
|
313
|
+
else if (input[pos] === '}') depth--;
|
|
314
|
+
pos++;
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
pos++;
|
|
319
|
+
}
|
|
320
|
+
return pos;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Skip single-line comment
|
|
325
|
+
*/
|
|
326
|
+
private static skipLineComment(input: string, pos: number): number {
|
|
327
|
+
while (pos < input.length && input[pos] !== '\n') {
|
|
328
|
+
pos++;
|
|
329
|
+
}
|
|
330
|
+
return pos + 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Skip block comment
|
|
335
|
+
*/
|
|
336
|
+
private static skipBlockComment(input: string, pos: number): number {
|
|
337
|
+
pos += 2; // skip /*
|
|
338
|
+
while (pos < input.length - 1) {
|
|
339
|
+
if (input[pos] === '*' && input[pos + 1] === '/') {
|
|
340
|
+
return pos + 2;
|
|
341
|
+
}
|
|
342
|
+
pos++;
|
|
343
|
+
}
|
|
344
|
+
return pos;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Извлечь простые @(expression) без блоков
|
|
349
|
+
*/
|
|
350
|
+
public static parseExpressions(input: string): Array<{ content: string; start: number; end: number }> {
|
|
351
|
+
return this.parseBalanced(input, '@(', ')');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EscapeHandler - обработка escape-последовательностей.
|
|
3
|
+
* @@ -> @
|
|
4
|
+
* @@@@ -> @@
|
|
5
|
+
*/
|
|
6
|
+
export default class EscapeHandler {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Заменить @@ на специальный placeholder перед парсингом
|
|
10
|
+
*/
|
|
11
|
+
public static escapeDoubleAt(input: string): { result: string; placeholder: string } {
|
|
12
|
+
// Use a unique placeholder that won't appear in normal code
|
|
13
|
+
const placeholder = '\x00AT_ESCAPE\x00';
|
|
14
|
+
const result = input.replace(/@@/g, placeholder);
|
|
15
|
+
return { result, placeholder };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Восстановить @ из placeholder после парсинга
|
|
20
|
+
*/
|
|
21
|
+
public static restoreEscapes(input: string, placeholder: string): string {
|
|
22
|
+
return input.replace(new RegExp(placeholder.replace(/\x00/g, '\\x00'), 'g'), '@');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Полный цикл: escape -> process -> restore
|
|
27
|
+
*/
|
|
28
|
+
public static process(
|
|
29
|
+
input: string,
|
|
30
|
+
processor: (escaped: string) => string
|
|
31
|
+
): string {
|
|
32
|
+
const { result: escaped, placeholder } = this.escapeDoubleAt(input);
|
|
33
|
+
const processed = processor(escaped);
|
|
34
|
+
return this.restoreEscapes(processed, placeholder);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Проверить, является ли позиция escaped (предшествует @@)
|
|
39
|
+
*/
|
|
40
|
+
public static isEscaped(input: string, position: number): boolean {
|
|
41
|
+
if (position === 0) return false;
|
|
42
|
+
|
|
43
|
+
// Count consecutive @ before this position
|
|
44
|
+
let count = 0;
|
|
45
|
+
let i = position - 1;
|
|
46
|
+
while (i >= 0 && input[i] === '@') {
|
|
47
|
+
count++;
|
|
48
|
+
i--;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If odd number of @ before, this @ is escaped
|
|
52
|
+
return count % 2 === 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import Scope from './Scope.js';
|
|
2
|
+
import Observable, { isObservable } from '../api/Observer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Expression - класс для выполнения JS-кода в контексте Scope.
|
|
6
|
+
* Поддерживает:
|
|
7
|
+
* - Простые выражения: value, user.name
|
|
8
|
+
* - Вызовы функций: doSomething()
|
|
9
|
+
* - Сложный JS-код: encodeURIComponent(JSON.stringify(subject))
|
|
10
|
+
* - Код с return: const f = ""; return f;
|
|
11
|
+
* - Async/await: await fetchData()
|
|
12
|
+
*
|
|
13
|
+
* Для Observable автоматически разворачивает значения:
|
|
14
|
+
* user.name -> user.getObject().name (если user - Observable)
|
|
15
|
+
*/
|
|
16
|
+
export default class Expression {
|
|
17
|
+
private readonly code: string;
|
|
18
|
+
private readonly isAsync: boolean;
|
|
19
|
+
private readonly hasReturn: boolean;
|
|
20
|
+
|
|
21
|
+
constructor(code: string) {
|
|
22
|
+
this.code = code.trim();
|
|
23
|
+
this.isAsync = this.detectAsync(this.code);
|
|
24
|
+
this.hasReturn = this.detectReturn(this.code);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Определить, содержит ли код await
|
|
29
|
+
*/
|
|
30
|
+
private detectAsync(code: string): boolean {
|
|
31
|
+
// Simple check for await keyword (not in string)
|
|
32
|
+
return /\bawait\s+/.test(code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Определить, содержит ли код return
|
|
37
|
+
*/
|
|
38
|
+
private detectReturn(code: string): boolean {
|
|
39
|
+
// Check for return keyword followed by space, semicolon, or end
|
|
40
|
+
return /\breturn\s+/.test(code) || /\breturn;/.test(code) || /\breturn$/.test(code);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Получить исходный код выражения
|
|
45
|
+
*/
|
|
46
|
+
public getCode(): string {
|
|
47
|
+
return this.code;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Найти все Observable, используемые в выражении.
|
|
52
|
+
* Например, для "user.name + user.age" вернёт [Observable(user)]
|
|
53
|
+
*/
|
|
54
|
+
public findObservables(scope: Scope): Observable<any>[] {
|
|
55
|
+
const context = scope.getVariables();
|
|
56
|
+
const observables: Observable<any>[] = [];
|
|
57
|
+
const seen = new Set<Observable<any>>();
|
|
58
|
+
|
|
59
|
+
// Извлекаем идентификаторы из выражения
|
|
60
|
+
const identifiers = this.extractIdentifiers(this.code);
|
|
61
|
+
|
|
62
|
+
for (const id of identifiers) {
|
|
63
|
+
const value = context[id];
|
|
64
|
+
if (isObservable(value) && !seen.has(value)) {
|
|
65
|
+
seen.add(value);
|
|
66
|
+
observables.push(value);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return observables;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Извлечь идентификаторы верхнего уровня из выражения.
|
|
75
|
+
* "user.name + count" -> ["user", "count"]
|
|
76
|
+
*/
|
|
77
|
+
private extractIdentifiers(code: string): string[] {
|
|
78
|
+
const identifiers: string[] = [];
|
|
79
|
+
// Regex для идентификаторов (не после точки)
|
|
80
|
+
const regex = /(?<![.\w])([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
81
|
+
|
|
82
|
+
// Список встроенных глобальных объектов, которые нужно игнорировать
|
|
83
|
+
const builtins = new Set([
|
|
84
|
+
'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
|
|
85
|
+
'Math', 'Date', 'JSON', 'Array', 'Object', 'String', 'Number', 'Boolean',
|
|
86
|
+
'parseInt', 'parseFloat', 'isNaN', 'isFinite',
|
|
87
|
+
'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
|
|
88
|
+
'console', 'window', 'document', 'this',
|
|
89
|
+
'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue',
|
|
90
|
+
'return', 'function', 'const', 'let', 'var', 'new', 'typeof', 'instanceof',
|
|
91
|
+
'in', 'of', 'async', 'await', 'try', 'catch', 'finally', 'throw'
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
let match;
|
|
95
|
+
while ((match = regex.exec(code)) !== null) {
|
|
96
|
+
const id = match[1];
|
|
97
|
+
if (!builtins.has(id) && !identifiers.includes(id)) {
|
|
98
|
+
identifiers.push(id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return identifiers;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Трансформировать код, разворачивая Observable.
|
|
107
|
+
* "user.name" -> "user.getObject().name" (если user - Observable)
|
|
108
|
+
*/
|
|
109
|
+
public transformCode(scope: Scope): string {
|
|
110
|
+
const context = scope.getVariables();
|
|
111
|
+
let transformedCode = this.code;
|
|
112
|
+
|
|
113
|
+
// Находим Observable переменные
|
|
114
|
+
const observableVars = new Set<string>();
|
|
115
|
+
for (const [key, value] of Object.entries(context)) {
|
|
116
|
+
if (isObservable(value)) {
|
|
117
|
+
observableVars.add(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (observableVars.size === 0) {
|
|
122
|
+
return transformedCode;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Трансформируем: user.name -> user.getObject().name
|
|
126
|
+
// и user (само по себе) -> user.getObject()
|
|
127
|
+
for (const varName of observableVars) {
|
|
128
|
+
// user.property -> user.getObject().property
|
|
129
|
+
const propRegex = new RegExp(`\\b${varName}\\.(?!getObject|setObject|subscribe|unsubscribe|getObserver|getMutationObserver|subscribeMutation|unsubscribeMutation)`, 'g');
|
|
130
|
+
transformedCode = transformedCode.replace(propRegex, `${varName}.getObject().`);
|
|
131
|
+
|
|
132
|
+
// Если просто user без вызова метода, не трансформируем (может быть намеренно)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return transformedCode;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Выполнить выражение в контексте Scope.
|
|
140
|
+
* @param scope - Scope с переменными и функциями
|
|
141
|
+
* @param extraVars - дополнительные переменные (например, event для @on)
|
|
142
|
+
* @returns результат выполнения
|
|
143
|
+
*/
|
|
144
|
+
public execute(scope: Scope, extraVars?: Record<string, any>): any {
|
|
145
|
+
const context = scope.getVariables();
|
|
146
|
+
|
|
147
|
+
if (extraVars) {
|
|
148
|
+
Object.assign(context, extraVars);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
// Трансформируем код для автоматического разворачивания Observable
|
|
153
|
+
const transformedCode = this.transformCode(scope);
|
|
154
|
+
return this.executeInContext(context, transformedCode);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error(`[Expression] Error executing: ${this.code}`);
|
|
157
|
+
console.error(error);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Выполнить выражение асинхронно
|
|
164
|
+
*/
|
|
165
|
+
public async executeAsync(scope: Scope, extraVars?: Record<string, any>): Promise<any> {
|
|
166
|
+
const context = scope.getVariables();
|
|
167
|
+
|
|
168
|
+
if (extraVars) {
|
|
169
|
+
Object.assign(context, extraVars);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const transformedCode = this.transformCode(scope);
|
|
174
|
+
return await this.executeInContextAsync(context, transformedCode);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.error(`[Expression] Error executing async: ${this.code}`);
|
|
177
|
+
console.error(error);
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Выполнить в контексте (синхронно)
|
|
184
|
+
*/
|
|
185
|
+
private executeInContext(context: Record<string, any>, codeOverride?: string): any {
|
|
186
|
+
const keys = Object.keys(context);
|
|
187
|
+
const values = Object.values(context);
|
|
188
|
+
const codeToExecute = codeOverride ?? this.code;
|
|
189
|
+
|
|
190
|
+
let functionBody: string;
|
|
191
|
+
|
|
192
|
+
if (this.hasReturn) {
|
|
193
|
+
// Code contains return, wrap in function directly
|
|
194
|
+
functionBody = codeToExecute;
|
|
195
|
+
} else {
|
|
196
|
+
// No return, add implicit return
|
|
197
|
+
functionBody = `return (${codeToExecute})`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
// Create function with context variables as parameters
|
|
202
|
+
const fn = new Function(...keys, functionBody);
|
|
203
|
+
return fn.apply(null, values);
|
|
204
|
+
} catch (syntaxError) {
|
|
205
|
+
// If implicit return fails, try without it (for statements)
|
|
206
|
+
if (!this.hasReturn) {
|
|
207
|
+
try {
|
|
208
|
+
const fn = new Function(...keys, codeToExecute);
|
|
209
|
+
return fn.apply(null, values);
|
|
210
|
+
} catch {
|
|
211
|
+
throw syntaxError;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw syntaxError;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Выполнить в контексте (асинхронно)
|
|
220
|
+
*/
|
|
221
|
+
private async executeInContextAsync(context: Record<string, any>, codeOverride?: string): Promise<any> {
|
|
222
|
+
const keys = Object.keys(context);
|
|
223
|
+
const values = Object.values(context);
|
|
224
|
+
const codeToExecute = codeOverride ?? this.code;
|
|
225
|
+
|
|
226
|
+
let functionBody: string;
|
|
227
|
+
|
|
228
|
+
if (this.hasReturn) {
|
|
229
|
+
functionBody = codeToExecute;
|
|
230
|
+
} else {
|
|
231
|
+
functionBody = `return (${codeToExecute})`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
// Create async function
|
|
236
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
237
|
+
const fn = new AsyncFunction(...keys, functionBody);
|
|
238
|
+
return await fn.apply(null, values);
|
|
239
|
+
} catch (syntaxError) {
|
|
240
|
+
if (!this.hasReturn) {
|
|
241
|
+
try {
|
|
242
|
+
const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor;
|
|
243
|
+
const fn = new AsyncFunction(...keys, codeToExecute);
|
|
244
|
+
return await fn.apply(null, values);
|
|
245
|
+
} catch {
|
|
246
|
+
throw syntaxError;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw syntaxError;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Проверить, является ли выражение асинхронным
|
|
255
|
+
*/
|
|
256
|
+
public isAsyncExpression(): boolean {
|
|
257
|
+
return this.isAsync;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Выполнить выражение (авто-выбор sync/async)
|
|
262
|
+
*/
|
|
263
|
+
public eval(scope: Scope, extraVars?: Record<string, any>): any | Promise<any> {
|
|
264
|
+
if (this.isAsync) {
|
|
265
|
+
return this.executeAsync(scope, extraVars);
|
|
266
|
+
}
|
|
267
|
+
return this.execute(scope, extraVars);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Статический метод для быстрого выполнения
|
|
272
|
+
*/
|
|
273
|
+
public static evaluate(code: string, scope: Scope, extraVars?: Record<string, any>): any {
|
|
274
|
+
const expr = new Expression(code);
|
|
275
|
+
return expr.eval(scope, extraVars);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Статический метод для асинхронного выполнения
|
|
280
|
+
*/
|
|
281
|
+
public static async evaluateAsync(code: string, scope: Scope, extraVars?: Record<string, any>): Promise<any> {
|
|
282
|
+
const expr = new Expression(code);
|
|
283
|
+
return await expr.executeAsync(scope, extraVars);
|
|
284
|
+
}
|
|
285
|
+
}
|