@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.
Files changed (88) hide show
  1. package/out/foundation/api/Observer.d.ts +30 -0
  2. package/out/foundation/api/Observer.d.ts.map +1 -1
  3. package/out/foundation/api/Observer.js +48 -0
  4. package/out/foundation/api/Observer.js.map +1 -1
  5. package/out/foundation/engine/BalancedParser.d.ts +58 -0
  6. package/out/foundation/engine/BalancedParser.d.ts.map +1 -0
  7. package/out/foundation/engine/BalancedParser.js +301 -0
  8. package/out/foundation/engine/BalancedParser.js.map +1 -0
  9. package/out/foundation/engine/EscapeHandler.d.ts +27 -0
  10. package/out/foundation/engine/EscapeHandler.d.ts.map +1 -0
  11. package/out/foundation/engine/EscapeHandler.js +47 -0
  12. package/out/foundation/engine/EscapeHandler.js.map +1 -0
  13. package/out/foundation/engine/Expression.d.ts +83 -0
  14. package/out/foundation/engine/Expression.d.ts.map +1 -0
  15. package/out/foundation/engine/Expression.js +256 -0
  16. package/out/foundation/engine/Expression.js.map +1 -0
  17. package/out/foundation/engine/Rule.d.ts +83 -0
  18. package/out/foundation/engine/Rule.d.ts.map +1 -0
  19. package/out/foundation/engine/Rule.js +69 -0
  20. package/out/foundation/engine/Rule.js.map +1 -0
  21. package/out/foundation/engine/Scope.d.ts +57 -0
  22. package/out/foundation/engine/Scope.d.ts.map +1 -0
  23. package/out/foundation/engine/Scope.js +147 -0
  24. package/out/foundation/engine/Scope.js.map +1 -0
  25. package/out/foundation/engine/TemplateEngine.d.ts +79 -0
  26. package/out/foundation/engine/TemplateEngine.d.ts.map +1 -0
  27. package/out/foundation/engine/TemplateEngine.js +187 -0
  28. package/out/foundation/engine/TemplateEngine.js.map +1 -0
  29. package/out/foundation/engine/TemplateInstance.d.ts +121 -0
  30. package/out/foundation/engine/TemplateInstance.d.ts.map +1 -0
  31. package/out/foundation/engine/TemplateInstance.js +255 -0
  32. package/out/foundation/engine/TemplateInstance.js.map +1 -0
  33. package/out/foundation/engine/exceptions/TemplateExceptions.d.ts +21 -0
  34. package/out/foundation/engine/exceptions/TemplateExceptions.d.ts.map +1 -0
  35. package/out/foundation/engine/exceptions/TemplateExceptions.js +26 -0
  36. package/out/foundation/engine/exceptions/TemplateExceptions.js.map +1 -0
  37. package/out/foundation/engine/index.d.ts +18 -0
  38. package/out/foundation/engine/index.d.ts.map +1 -0
  39. package/out/foundation/engine/index.js +19 -0
  40. package/out/foundation/engine/index.js.map +1 -0
  41. package/out/foundation/engine/rules/attribute/EventRule.d.ts +22 -0
  42. package/out/foundation/engine/rules/attribute/EventRule.d.ts.map +1 -0
  43. package/out/foundation/engine/rules/attribute/EventRule.js +129 -0
  44. package/out/foundation/engine/rules/attribute/EventRule.js.map +1 -0
  45. package/out/foundation/engine/rules/attribute/InjectionRule.d.ts +20 -0
  46. package/out/foundation/engine/rules/attribute/InjectionRule.d.ts.map +1 -0
  47. package/out/foundation/engine/rules/attribute/InjectionRule.js +108 -0
  48. package/out/foundation/engine/rules/attribute/InjectionRule.js.map +1 -0
  49. package/out/foundation/engine/rules/attribute/RefRule.d.ts +23 -0
  50. package/out/foundation/engine/rules/attribute/RefRule.d.ts.map +1 -0
  51. package/out/foundation/engine/rules/attribute/RefRule.js +98 -0
  52. package/out/foundation/engine/rules/attribute/RefRule.js.map +1 -0
  53. package/out/foundation/engine/rules/syntax/ExpressionRule.d.ts +19 -0
  54. package/out/foundation/engine/rules/syntax/ExpressionRule.d.ts.map +1 -0
  55. package/out/foundation/engine/rules/syntax/ExpressionRule.js +82 -0
  56. package/out/foundation/engine/rules/syntax/ExpressionRule.js.map +1 -0
  57. package/out/foundation/engine/rules/syntax/ForRule.d.ts +19 -0
  58. package/out/foundation/engine/rules/syntax/ForRule.d.ts.map +1 -0
  59. package/out/foundation/engine/rules/syntax/ForRule.js +226 -0
  60. package/out/foundation/engine/rules/syntax/ForRule.js.map +1 -0
  61. package/out/foundation/engine/rules/syntax/IfRule.d.ts +17 -0
  62. package/out/foundation/engine/rules/syntax/IfRule.d.ts.map +1 -0
  63. package/out/foundation/engine/rules/syntax/IfRule.js +220 -0
  64. package/out/foundation/engine/rules/syntax/IfRule.js.map +1 -0
  65. package/out/foundation/worker/Router.d.ts.map +1 -1
  66. package/out/foundation/worker/Router.js.map +1 -1
  67. package/out/index.d.ts +3 -0
  68. package/out/index.d.ts.map +1 -1
  69. package/out/index.js +3 -0
  70. package/out/index.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/foundation/api/Observer.ts +60 -0
  73. package/src/foundation/engine/BalancedParser.ts +353 -0
  74. package/src/foundation/engine/EscapeHandler.ts +54 -0
  75. package/src/foundation/engine/Expression.ts +285 -0
  76. package/src/foundation/engine/Rule.ts +136 -0
  77. package/src/foundation/engine/Scope.ts +166 -0
  78. package/src/foundation/engine/TemplateEngine.ts +243 -0
  79. package/src/foundation/engine/TemplateInstance.ts +355 -0
  80. package/src/foundation/engine/exceptions/TemplateExceptions.ts +27 -0
  81. package/src/foundation/engine/rules/attribute/EventRule.ts +171 -0
  82. package/src/foundation/engine/rules/attribute/InjectionRule.ts +140 -0
  83. package/src/foundation/engine/rules/attribute/RefRule.ts +119 -0
  84. package/src/foundation/engine/rules/syntax/ExpressionRule.ts +102 -0
  85. package/src/foundation/engine/rules/syntax/ForRule.ts +267 -0
  86. package/src/foundation/engine/rules/syntax/IfRule.ts +261 -0
  87. package/src/foundation/worker/Router.ts +1 -1
  88. 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
+ }