@reidelsaltres/pureper 0.1.157 → 0.1.162

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 (107) hide show
  1. package/out/foundation/Triplet.d.ts.map +1 -1
  2. package/out/foundation/Triplet.js +4 -5
  3. package/out/foundation/Triplet.js.map +1 -1
  4. package/out/foundation/api/Observer.d.ts +30 -0
  5. package/out/foundation/api/Observer.d.ts.map +1 -1
  6. package/out/foundation/api/Observer.js +48 -0
  7. package/out/foundation/api/Observer.js.map +1 -1
  8. package/out/foundation/component_api/Component.d.ts +2 -2
  9. package/out/foundation/component_api/Component.d.ts.map +1 -1
  10. package/out/foundation/component_api/Component.js.map +1 -1
  11. package/out/foundation/component_api/UniHtml.d.ts +4 -10
  12. package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
  13. package/out/foundation/component_api/UniHtml.js +7 -15
  14. package/out/foundation/component_api/UniHtml.js.map +1 -1
  15. package/out/foundation/engine/BalancedParser.d.ts +58 -0
  16. package/out/foundation/engine/BalancedParser.d.ts.map +1 -0
  17. package/out/foundation/engine/BalancedParser.js +301 -0
  18. package/out/foundation/engine/BalancedParser.js.map +1 -0
  19. package/out/foundation/engine/EscapeHandler.d.ts +27 -0
  20. package/out/foundation/engine/EscapeHandler.d.ts.map +1 -0
  21. package/out/foundation/engine/EscapeHandler.js +47 -0
  22. package/out/foundation/engine/EscapeHandler.js.map +1 -0
  23. package/out/foundation/engine/Expression.d.ts +83 -0
  24. package/out/foundation/engine/Expression.d.ts.map +1 -0
  25. package/out/foundation/engine/Expression.js +256 -0
  26. package/out/foundation/engine/Expression.js.map +1 -0
  27. package/out/foundation/engine/Rule.d.ts +85 -0
  28. package/out/foundation/engine/Rule.d.ts.map +1 -0
  29. package/out/foundation/engine/Rule.js +69 -0
  30. package/out/foundation/engine/Rule.js.map +1 -0
  31. package/out/foundation/engine/Scope.d.ts +61 -0
  32. package/out/foundation/engine/Scope.d.ts.map +1 -0
  33. package/out/foundation/engine/Scope.js +156 -0
  34. package/out/foundation/engine/Scope.js.map +1 -0
  35. package/out/foundation/engine/TemplateEngine.d.ts +96 -0
  36. package/out/foundation/engine/TemplateEngine.d.ts.map +1 -0
  37. package/out/foundation/engine/TemplateEngine.js +235 -0
  38. package/out/foundation/engine/TemplateEngine.js.map +1 -0
  39. package/out/foundation/engine/TemplateInstance.d.ts +241 -0
  40. package/out/foundation/engine/TemplateInstance.d.ts.map +1 -0
  41. package/out/foundation/engine/TemplateInstance.js +637 -0
  42. package/out/foundation/engine/TemplateInstance.js.map +1 -0
  43. package/out/foundation/engine/TemplateInstance.old.d.ts +219 -0
  44. package/out/foundation/engine/TemplateInstance.old.d.ts.map +1 -0
  45. package/out/foundation/engine/TemplateInstance.old.js +487 -0
  46. package/out/foundation/engine/TemplateInstance.old.js.map +1 -0
  47. package/out/foundation/engine/exceptions/TemplateExceptions.d.ts +21 -0
  48. package/out/foundation/engine/exceptions/TemplateExceptions.d.ts.map +1 -0
  49. package/out/foundation/engine/exceptions/TemplateExceptions.js +26 -0
  50. package/out/foundation/engine/exceptions/TemplateExceptions.js.map +1 -0
  51. package/out/foundation/engine/index.d.ts +18 -0
  52. package/out/foundation/engine/index.d.ts.map +1 -0
  53. package/out/foundation/engine/index.js +19 -0
  54. package/out/foundation/engine/index.js.map +1 -0
  55. package/out/foundation/engine/rules/attribute/EventRule.d.ts +22 -0
  56. package/out/foundation/engine/rules/attribute/EventRule.d.ts.map +1 -0
  57. package/out/foundation/engine/rules/attribute/EventRule.js +129 -0
  58. package/out/foundation/engine/rules/attribute/EventRule.js.map +1 -0
  59. package/out/foundation/engine/rules/attribute/InjectionRule.d.ts +20 -0
  60. package/out/foundation/engine/rules/attribute/InjectionRule.d.ts.map +1 -0
  61. package/out/foundation/engine/rules/attribute/InjectionRule.js +108 -0
  62. package/out/foundation/engine/rules/attribute/InjectionRule.js.map +1 -0
  63. package/out/foundation/engine/rules/attribute/RefRule.d.ts +23 -0
  64. package/out/foundation/engine/rules/attribute/RefRule.d.ts.map +1 -0
  65. package/out/foundation/engine/rules/attribute/RefRule.js +104 -0
  66. package/out/foundation/engine/rules/attribute/RefRule.js.map +1 -0
  67. package/out/foundation/engine/rules/syntax/ExpressionRule.d.ts +19 -0
  68. package/out/foundation/engine/rules/syntax/ExpressionRule.d.ts.map +1 -0
  69. package/out/foundation/engine/rules/syntax/ExpressionRule.js +82 -0
  70. package/out/foundation/engine/rules/syntax/ExpressionRule.js.map +1 -0
  71. package/out/foundation/engine/rules/syntax/ForRule.d.ts +19 -0
  72. package/out/foundation/engine/rules/syntax/ForRule.d.ts.map +1 -0
  73. package/out/foundation/engine/rules/syntax/ForRule.js +226 -0
  74. package/out/foundation/engine/rules/syntax/ForRule.js.map +1 -0
  75. package/out/foundation/engine/rules/syntax/IfRule.d.ts +17 -0
  76. package/out/foundation/engine/rules/syntax/IfRule.d.ts.map +1 -0
  77. package/out/foundation/engine/rules/syntax/IfRule.js +220 -0
  78. package/out/foundation/engine/rules/syntax/IfRule.js.map +1 -0
  79. package/out/foundation/worker/Router.d.ts.map +1 -1
  80. package/out/foundation/worker/Router.js.map +1 -1
  81. package/out/index.d.ts +2 -0
  82. package/out/index.d.ts.map +1 -1
  83. package/out/index.js +2 -0
  84. package/out/index.js.map +1 -1
  85. package/package.json +1 -1
  86. package/src/foundation/Triplet.ts +6 -6
  87. package/src/foundation/api/Observer.ts +60 -0
  88. package/src/foundation/component_api/Component.ts +2 -1
  89. package/src/foundation/component_api/UniHtml.ts +12 -22
  90. package/src/foundation/engine/BalancedParser.ts +353 -0
  91. package/src/foundation/engine/EscapeHandler.ts +54 -0
  92. package/src/foundation/engine/Expression.ts +285 -0
  93. package/src/foundation/engine/Rule.ts +138 -0
  94. package/src/foundation/engine/Scope.ts +176 -0
  95. package/src/foundation/engine/TemplateEngine.ts +318 -0
  96. package/src/foundation/engine/TemplateInstance.md +110 -0
  97. package/src/foundation/engine/TemplateInstance.old.ts +673 -0
  98. package/src/foundation/engine/TemplateInstance.ts +843 -0
  99. package/src/foundation/engine/exceptions/TemplateExceptions.ts +27 -0
  100. package/src/foundation/engine/rules/attribute/EventRule.ts +171 -0
  101. package/src/foundation/engine/rules/attribute/InjectionRule.ts +140 -0
  102. package/src/foundation/engine/rules/attribute/RefRule.ts +126 -0
  103. package/src/foundation/engine/rules/syntax/ExpressionRule.ts +102 -0
  104. package/src/foundation/engine/rules/syntax/ForRule.ts +267 -0
  105. package/src/foundation/engine/rules/syntax/IfRule.ts +261 -0
  106. package/src/foundation/worker/Router.ts +1 -1
  107. package/src/index.ts +8 -0
@@ -0,0 +1,27 @@
1
+ /**
2
+ * InvalidDynamicRuleUsage - исключение при использовании Observable
3
+ * в правилах, которые не поддерживают динамическое обновление.
4
+ * Например: @[ref], @injection
5
+ */
6
+ export class InvalidDynamicRuleUsage extends Error {
7
+ constructor(ruleName: string, message?: string) {
8
+ super(message ?? `Rule "${ruleName}" does not support Observable values. Use a static value instead.`);
9
+ this.name = 'InvalidDynamicRuleUsage';
10
+ }
11
+ }
12
+
13
+ /**
14
+ * InvalidTemplateEngineSyntaxException - исключение при синтаксических ошибках
15
+ * в шаблоне. Например: @if без boolean, @for с неверным типом.
16
+ */
17
+ export class InvalidTemplateEngineSyntaxException extends Error {
18
+ public readonly line?: number;
19
+ public readonly column?: number;
20
+
21
+ constructor(message: string, options?: { line?: number; column?: number }) {
22
+ super(message);
23
+ this.name = 'InvalidTemplateEngineSyntaxException';
24
+ this.line = options?.line;
25
+ this.column = options?.column;
26
+ }
27
+ }
@@ -0,0 +1,171 @@
1
+ import { AttributeRule } from '../../Rule.js';
2
+ import type { RuleMatch, RuleResult } from '../../Rule.js';
3
+ import Scope from '../../Scope.js';
4
+ import Expression from '../../Expression.js';
5
+
6
+ interface EventMatch extends RuleMatch {
7
+ data: {
8
+ eventName: string;
9
+ expression: string;
10
+ attributeMatch: string;
11
+ };
12
+ }
13
+
14
+ /**
15
+ * EventRule - обработка @on[eventName]="expression"
16
+ * Подписывает элемент на событие.
17
+ */
18
+ export default class EventRule extends AttributeRule {
19
+ public readonly name = 'event';
20
+ public readonly priority = 30;
21
+
22
+ public find(template: string): RuleMatch[] {
23
+ const results: RuleMatch[] = [];
24
+ // Match @on[eventName]="
25
+ const pattern = /@on\[([a-zA-Z]+)\]\s*=/gi;
26
+ let match: RegExpExecArray | null;
27
+
28
+ while ((match = pattern.exec(template)) !== null) {
29
+ const idx = match.index;
30
+
31
+ // Check for @@ escape
32
+ if (idx > 0 && template[idx - 1] === '@') {
33
+ continue;
34
+ }
35
+
36
+ const eventName = match[1].toLowerCase();
37
+
38
+ // Find quote char after =
39
+ let pos = idx + match[0].length;
40
+ while (pos < template.length && /\s/.test(template[pos])) {
41
+ pos++;
42
+ }
43
+
44
+ const quoteChar = template[pos];
45
+ if (quoteChar !== '"' && quoteChar !== "'") {
46
+ continue;
47
+ }
48
+
49
+ // Find matching closing quote
50
+ const contentStart = pos + 1;
51
+ pos++;
52
+ while (pos < template.length && template[pos] !== quoteChar) {
53
+ pos++;
54
+ }
55
+
56
+ if (pos >= template.length) continue;
57
+
58
+ const content = template.slice(contentStart, pos);
59
+ const fullMatch = template.slice(idx, pos + 1);
60
+
61
+ results.push({
62
+ fullMatch,
63
+ start: idx,
64
+ end: pos + 1,
65
+ data: {
66
+ eventName,
67
+ expression: content,
68
+ attributeMatch: fullMatch
69
+ }
70
+ });
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ public execute(match: RuleMatch, scope: Scope): RuleResult {
77
+ const data = (match as EventMatch).data;
78
+
79
+ // Generate data attribute for later DOM binding
80
+ // Actual event binding happens in postprocessing
81
+ const encodedExpr = encodeURIComponent(data.expression);
82
+
83
+ return {
84
+ output: `data-event-${data.eventName}="${encodedExpr}"`,
85
+ observables: []
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Постобработка: привязать события к элементам
91
+ */
92
+ public static bindEvents(element: Element, scope: Scope): (() => void)[] {
93
+ const unbinders: (() => void)[] = [];
94
+
95
+ // Find all data-event-* attributes
96
+ const attributes = Array.from(element.attributes);
97
+
98
+ for (const attr of attributes) {
99
+ if (attr.name.startsWith('data-event-')) {
100
+ const eventName = attr.name.slice('data-event-'.length);
101
+ const exprCode = decodeURIComponent(attr.value);
102
+
103
+ const handler = (event: Event) => {
104
+ // Create local scope with event
105
+ const localScope = scope.createChild({ event });
106
+ const expr = new Expression(exprCode);
107
+
108
+ try {
109
+ expr.execute(localScope);
110
+ } catch (error) {
111
+ console.error(`[EventRule] Error executing handler for ${eventName}:`, error);
112
+ }
113
+ };
114
+
115
+ element.addEventListener(eventName, handler);
116
+ unbinders.push(() => element.removeEventListener(eventName, handler));
117
+
118
+ // Optionally remove the data attribute
119
+ // element.removeAttribute(attr.name);
120
+ }
121
+ }
122
+
123
+ return unbinders;
124
+ }
125
+
126
+ /**
127
+ * Привязать событие с поддержкой Observable
128
+ */
129
+ public static bindEventWithObservable(
130
+ element: Element,
131
+ eventName: string,
132
+ exprCode: string,
133
+ scope: Scope
134
+ ): () => void {
135
+ let currentHandler: ((event: Event) => void) | null = null;
136
+
137
+ const setupHandler = () => {
138
+ // Remove old handler if exists
139
+ if (currentHandler) {
140
+ element.removeEventListener(eventName, currentHandler);
141
+ }
142
+
143
+ currentHandler = (event: Event) => {
144
+ const localScope = scope.createChild({ event });
145
+ const expr = new Expression(exprCode);
146
+
147
+ try {
148
+ const result = expr.execute(localScope);
149
+
150
+ // If result is Observable, handle it
151
+ if (result && typeof result === 'object' && typeof result.subscribe === 'function') {
152
+ // Re-setup handler when Observable changes
153
+ result.subscribe(() => setupHandler());
154
+ }
155
+ } catch (error) {
156
+ console.error(`[EventRule] Error executing handler for ${eventName}:`, error);
157
+ }
158
+ };
159
+
160
+ element.addEventListener(eventName, currentHandler);
161
+ };
162
+
163
+ setupHandler();
164
+
165
+ return () => {
166
+ if (currentHandler) {
167
+ element.removeEventListener(eventName, currentHandler);
168
+ }
169
+ };
170
+ }
171
+ }
@@ -0,0 +1,140 @@
1
+ import { AttributeRule } from '../../Rule.js';
2
+ import type { RuleMatch, RuleResult } from '../../Rule.js';
3
+ import Scope from '../../Scope.js';
4
+ import Expression from '../../Expression.js';
5
+ import { InvalidDynamicRuleUsage } from '../../exceptions/TemplateExceptions.js';
6
+
7
+ interface InjectionMatch extends RuleMatch {
8
+ data: {
9
+ type: 'head' | 'tail';
10
+ expression: string;
11
+ attributeMatch: string;
12
+ };
13
+ }
14
+
15
+ /**
16
+ * InjectionRule - обработка @injection[type]="expression"
17
+ * Инжектирует элемент в целевой элемент (найденный по @[ref]).
18
+ * type: 'head' = prepend, 'tail' = append
19
+ */
20
+ export default class InjectionRule extends AttributeRule {
21
+ public readonly name = 'injection';
22
+ public readonly priority = 200; // Выполняется в самом конце
23
+
24
+ public find(template: string): RuleMatch[] {
25
+ const results: RuleMatch[] = [];
26
+ // Match @injection[head] or @injection[tail]
27
+ const pattern = /@injection\[(head|tail)\]\s*=/gi;
28
+ let match: RegExpExecArray | null;
29
+
30
+ while ((match = pattern.exec(template)) !== null) {
31
+ const idx = match.index;
32
+
33
+ // Check for @@ escape
34
+ if (idx > 0 && template[idx - 1] === '@') {
35
+ continue;
36
+ }
37
+
38
+ const injectionType = match[1].toLowerCase() as 'head' | 'tail';
39
+
40
+ // Find quote char after =
41
+ let pos = idx + match[0].length;
42
+ while (pos < template.length && /\s/.test(template[pos])) {
43
+ pos++;
44
+ }
45
+
46
+ const quoteChar = template[pos];
47
+ if (quoteChar !== '"' && quoteChar !== "'") {
48
+ continue;
49
+ }
50
+
51
+ // Find matching closing quote
52
+ const contentStart = pos + 1;
53
+ pos++;
54
+ while (pos < template.length && template[pos] !== quoteChar) {
55
+ pos++;
56
+ }
57
+
58
+ if (pos >= template.length) continue;
59
+
60
+ const content = template.slice(contentStart, pos);
61
+ const fullMatch = template.slice(idx, pos + 1);
62
+
63
+ results.push({
64
+ fullMatch,
65
+ start: idx,
66
+ end: pos + 1,
67
+ data: {
68
+ type: injectionType,
69
+ expression: content,
70
+ attributeMatch: fullMatch
71
+ }
72
+ });
73
+ }
74
+
75
+ return results;
76
+ }
77
+
78
+ public execute(match: RuleMatch, scope: Scope): RuleResult {
79
+ const data = (match as InjectionMatch).data;
80
+ const expr = new Expression(data.expression);
81
+ const targetRefName = expr.execute(scope);
82
+
83
+ // Check if Observable - not allowed for @injection
84
+ if (targetRefName && typeof targetRefName === 'object' && typeof targetRefName.subscribe === 'function') {
85
+ throw new InvalidDynamicRuleUsage('@injection',
86
+ '@injection does not support Observable values. The target reference must be static.');
87
+ }
88
+
89
+ if (typeof targetRefName !== 'string') {
90
+ console.error(`[InjectionRule] Expression must return a string (reference name), got: ${typeof targetRefName}`);
91
+ return { output: '' };
92
+ }
93
+
94
+ // Store injection info for postprocessing
95
+ const encodedTarget = encodeURIComponent(targetRefName);
96
+
97
+ return {
98
+ output: `data-injection-type="${data.type}" data-injection-target="${encodedTarget}"`,
99
+ observables: []
100
+ };
101
+ }
102
+
103
+ public supportsObservable(): boolean {
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * Постобработка: выполнить инжекцию элементов
109
+ */
110
+ public static processInjections(root: Element | DocumentFragment, scope: Scope): void {
111
+ // Find all elements with injection attributes
112
+ const injectElements = root.querySelectorAll('[data-injection-type][data-injection-target]');
113
+
114
+ for (const element of Array.from(injectElements)) {
115
+ const type = element.getAttribute('data-injection-type') as 'head' | 'tail';
116
+ const targetRefName = decodeURIComponent(element.getAttribute('data-injection-target') || '');
117
+
118
+ if (!targetRefName) continue;
119
+
120
+ // Get target element from scope
121
+ const targetElement = scope.get(targetRefName);
122
+
123
+ if (!targetElement || !(targetElement instanceof Element)) {
124
+ console.warn(`[InjectionRule] Target element "${targetRefName}" not found in scope or is not an Element`);
125
+ continue;
126
+ }
127
+
128
+ // Remove injection attributes
129
+ element.removeAttribute('data-injection-type');
130
+ element.removeAttribute('data-injection-target');
131
+
132
+ // Perform injection
133
+ if (type === 'head') {
134
+ targetElement.prepend(element);
135
+ } else {
136
+ targetElement.append(element);
137
+ }
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,126 @@
1
+ import { AttributeRule } from '../../Rule.js';
2
+ import type { RuleMatch, RuleResult } from '../../Rule.js';
3
+ import Scope from '../../Scope.js';
4
+ import Expression from '../../Expression.js';
5
+ import { InvalidDynamicRuleUsage } from '../../exceptions/TemplateExceptions.js';
6
+
7
+ /**
8
+ * RefRule - обработка @[ref]="expression"
9
+ * Добавляет HTML элемент в Scope под указанным именем.
10
+ */
11
+ export default class RefRule extends AttributeRule {
12
+ public readonly name = 'ref';
13
+ public readonly priority = 5; // Выполняется очень рано
14
+
15
+ public find(template: string): RuleMatch[] {
16
+ const results: RuleMatch[] = [];
17
+ // Match @[ref]=" or @[ref]='
18
+ const opener = '@[ref]=';
19
+ let i = 0;
20
+
21
+ while (i < template.length) {
22
+ const idx = template.toLowerCase().indexOf(opener.toLowerCase(), i);
23
+ if (idx === -1) break;
24
+
25
+ // Check for @@ escape
26
+ if (idx > 0 && template[idx - 1] === '@') {
27
+ i = idx + 1;
28
+ continue;
29
+ }
30
+
31
+ // Find quote char after =
32
+ let pos = idx + opener.length;
33
+ while (pos < template.length && /\s/.test(template[pos])) {
34
+ pos++;
35
+ }
36
+
37
+ const quoteChar = template[pos];
38
+ if (quoteChar !== '"' && quoteChar !== "'") {
39
+ i = idx + 1;
40
+ continue;
41
+ }
42
+
43
+ // Find matching closing quote (handle nested quotes)
44
+ const contentStart = pos + 1;
45
+ pos++;
46
+
47
+ // Simple: find the closing quote that matches
48
+ while (pos < template.length && template[pos] !== quoteChar) {
49
+ pos++;
50
+ }
51
+
52
+ if (pos >= template.length) {
53
+ i = idx + 1;
54
+ continue;
55
+ }
56
+
57
+ const content = template.slice(contentStart, pos);
58
+ const fullMatch = template.slice(idx, pos + 1);
59
+
60
+ results.push({
61
+ fullMatch,
62
+ start: idx,
63
+ end: pos + 1,
64
+ data: {
65
+ expression: content,
66
+ attributeMatch: fullMatch
67
+ }
68
+ });
69
+
70
+ i = pos + 1;
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ public execute(match: RuleMatch, scope: Scope): RuleResult {
77
+ const exprCode = match.data?.expression as string;
78
+ const expr = new Expression(exprCode);
79
+ const refName = expr.execute(scope);
80
+
81
+ // Check if Observable - not allowed for @[ref]
82
+ if (refName && typeof refName === 'object' && typeof refName.subscribe === 'function') {
83
+ throw new InvalidDynamicRuleUsage('@[ref]',
84
+ '@[ref] does not support Observable values. The reference name must be static.');
85
+ }
86
+
87
+ if (typeof refName !== 'string') {
88
+ console.error(`[RefRule] Expression must return a string (variable name), got: ${typeof refName}`);
89
+ return { output: '' };
90
+ }
91
+
92
+ // Сразу регистрируем ref в scope с null значением
93
+ // Будет заполнен реальным элементом при bindRefs()
94
+ if (!scope.has(refName)) {
95
+ scope.set(refName, null);
96
+ }
97
+
98
+ // Store placeholder - actual element will be set during DOM processing
99
+ // For now, return the attribute without the @[ref] syntax
100
+ return {
101
+ output: `data-ref="${refName}"`,
102
+ observables: [],
103
+ data: { refName } // Сохраняем имя ref для быстрого доступа
104
+ };
105
+ }
106
+
107
+ public supportsObservable(): boolean {
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Постобработка: привязать реальный элемент к Scope
113
+ */
114
+ public static bindElement(element: Element, refName: string, scope: Scope): void {
115
+ scope.set(refName, element);
116
+ }
117
+
118
+ /**
119
+ * Очистка: установить ref в null если элемент удалён
120
+ */
121
+ public static unbindElement(refName: string, scope: Scope): void {
122
+ if (scope.has(refName)) {
123
+ scope.set(refName, null);
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,102 @@
1
+ import { SyntaxRule } from '../../Rule.js';
2
+ import type { RuleMatch, RuleResult } from '../../Rule.js';
3
+ import Scope from '../../Scope.js';
4
+ import Expression from '../../Expression.js';
5
+ import BalancedParser from '../../BalancedParser.js';
6
+ import { isObservable } from '../../../api/Observer.js';
7
+
8
+ /**
9
+ * ExpressionRule - обработка @(Expression)
10
+ * Выводит результат выражения как строку.
11
+ * Автоматически отслеживает Observable и разворачивает их значения.
12
+ */
13
+ export default class ExpressionRule extends SyntaxRule {
14
+ public readonly name = 'expression';
15
+ public readonly priority = 50; // Выполняется после блочных правил
16
+
17
+ public find(template: string): RuleMatch[] {
18
+ const matches = BalancedParser.parseExpressions(template);
19
+
20
+ return matches.map(m => ({
21
+ fullMatch: template.slice(m.start, m.end),
22
+ start: m.start,
23
+ end: m.end,
24
+ data: { expression: m.content }
25
+ }));
26
+ }
27
+
28
+ public execute(match: RuleMatch, scope: Scope): RuleResult {
29
+ const code = match.data?.expression as string;
30
+
31
+ if (!code || code.trim() === '') {
32
+ return { output: '' };
33
+ }
34
+
35
+ const expr = new Expression(code);
36
+
37
+ // Находим Observable, используемые в выражении
38
+ const observables = expr.findObservables(scope);
39
+
40
+ // Check for async
41
+ if (expr.isAsyncExpression()) {
42
+ // Return promise wrapper - engine should handle this
43
+ const promise = expr.executeAsync(scope).then(result => {
44
+ if (result === undefined || result === null) {
45
+ return '';
46
+ }
47
+ return String(result);
48
+ });
49
+
50
+ // For now, return placeholder - proper async handling in engine
51
+ return {
52
+ output: '',
53
+ observables,
54
+ children: []
55
+ };
56
+ }
57
+
58
+ const result = expr.execute(scope);
59
+
60
+ // Если результат сам Observable (например @(counter)), отслеживаем его
61
+ if (isObservable(result)) {
62
+ return {
63
+ output: String(result.getObject?.() ?? result),
64
+ observables: [...observables, result]
65
+ };
66
+ }
67
+
68
+ if (result === undefined || result === null) {
69
+ return { output: '', observables };
70
+ }
71
+
72
+ return { output: String(result), observables };
73
+ }
74
+
75
+ /**
76
+ * Асинхронная версия execute
77
+ */
78
+ public async executeAsync(match: RuleMatch, scope: Scope): Promise<RuleResult> {
79
+ const code = match.data?.expression as string;
80
+
81
+ if (!code || code.trim() === '') {
82
+ return { output: '' };
83
+ }
84
+
85
+ const expr = new Expression(code);
86
+ const observables = expr.findObservables(scope);
87
+ const result = await expr.executeAsync(scope);
88
+
89
+ if (isObservable(result)) {
90
+ return {
91
+ output: String(result.getObject?.() ?? result),
92
+ observables: [...observables, result]
93
+ };
94
+ }
95
+
96
+ if (result === undefined || result === null) {
97
+ return { output: '', observables };
98
+ }
99
+
100
+ return { output: String(result), observables };
101
+ }
102
+ }