@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,119 @@
|
|
|
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
|
+
// Store placeholder - actual element will be set during DOM processing
|
|
93
|
+
// For now, return the attribute without the @[ref] syntax
|
|
94
|
+
return {
|
|
95
|
+
output: `data-ref="${refName}"`,
|
|
96
|
+
observables: []
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public supportsObservable(): boolean {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Постобработка: привязать реальный элемент к Scope
|
|
106
|
+
*/
|
|
107
|
+
public static bindElement(element: Element, refName: string, scope: Scope): void {
|
|
108
|
+
scope.set(refName, element);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Очистка: установить ref в null если элемент удалён
|
|
113
|
+
*/
|
|
114
|
+
public static unbindElement(refName: string, scope: Scope): void {
|
|
115
|
+
if (scope.has(refName)) {
|
|
116
|
+
scope.set(refName, null);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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 { InvalidTemplateEngineSyntaxException } from '../../exceptions/TemplateExceptions.js';
|
|
6
|
+
import { isObservable } from '../../../api/Observer.js';
|
|
7
|
+
|
|
8
|
+
interface ForMatch extends RuleMatch {
|
|
9
|
+
data: {
|
|
10
|
+
/** Вариант: 'single' (item in expr), 'indexed' (idx, item in expr), 'numeric' (i in number) */
|
|
11
|
+
variant: 'single' | 'indexed' | 'numeric';
|
|
12
|
+
/** Имена локальных переменных */
|
|
13
|
+
variables: string[];
|
|
14
|
+
/** Выражение коллекции/числа */
|
|
15
|
+
expression: string;
|
|
16
|
+
/** Тело цикла */
|
|
17
|
+
block: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* ForRule - обработка @for
|
|
23
|
+
* Варианты:
|
|
24
|
+
* 1. @for(item in collection) { ... }
|
|
25
|
+
* 2. @for(idx, item in collection) { ... }
|
|
26
|
+
* 3. @for(i in 5) { ... } - числовая итерация 0..4
|
|
27
|
+
*/
|
|
28
|
+
export default class ForRule extends SyntaxRule {
|
|
29
|
+
public readonly name = 'for';
|
|
30
|
+
public readonly priority = 10; // Выполняется раньше всех
|
|
31
|
+
|
|
32
|
+
public find(template: string): RuleMatch[] {
|
|
33
|
+
const results: RuleMatch[] = [];
|
|
34
|
+
const lowerTemplate = template.toLowerCase();
|
|
35
|
+
let i = 0;
|
|
36
|
+
|
|
37
|
+
while (i < template.length) {
|
|
38
|
+
const idx = lowerTemplate.indexOf('@for', i);
|
|
39
|
+
if (idx === -1) break;
|
|
40
|
+
|
|
41
|
+
// Check for @@ escape
|
|
42
|
+
if (idx > 0 && template[idx - 1] === '@') {
|
|
43
|
+
i = idx + 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parsed = this.parseForStatement(template, idx);
|
|
48
|
+
if (parsed) {
|
|
49
|
+
results.push({
|
|
50
|
+
fullMatch: template.slice(parsed.start, parsed.end),
|
|
51
|
+
start: parsed.start,
|
|
52
|
+
end: parsed.end,
|
|
53
|
+
data: {
|
|
54
|
+
variant: parsed.variant,
|
|
55
|
+
variables: parsed.variables,
|
|
56
|
+
expression: parsed.expression,
|
|
57
|
+
block: parsed.block
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
i = parsed.end;
|
|
61
|
+
} else {
|
|
62
|
+
i = idx + 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private parseForStatement(template: string, start: number): {
|
|
70
|
+
start: number;
|
|
71
|
+
end: number;
|
|
72
|
+
variant: 'single' | 'indexed' | 'numeric';
|
|
73
|
+
variables: string[];
|
|
74
|
+
expression: string;
|
|
75
|
+
block: string;
|
|
76
|
+
} | null {
|
|
77
|
+
let pos = start + 4; // '@for'.length
|
|
78
|
+
|
|
79
|
+
// Skip whitespace
|
|
80
|
+
while (pos < template.length && /\s/.test(template[pos])) {
|
|
81
|
+
pos++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Expect (
|
|
85
|
+
if (template[pos] !== '(') return null;
|
|
86
|
+
|
|
87
|
+
// Parse balanced parentheses
|
|
88
|
+
const conditionStart = pos + 1;
|
|
89
|
+
pos++;
|
|
90
|
+
let depth = 1;
|
|
91
|
+
|
|
92
|
+
while (pos < template.length && depth > 0) {
|
|
93
|
+
const ch = template[pos];
|
|
94
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
95
|
+
pos = this.skipString(template, pos, ch);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (ch === '(') depth++;
|
|
99
|
+
else if (ch === ')') depth--;
|
|
100
|
+
pos++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (depth !== 0) return null;
|
|
104
|
+
const conditionContent = template.slice(conditionStart, pos - 1).trim();
|
|
105
|
+
|
|
106
|
+
// Parse condition: "var in expr" or "idx, var in expr"
|
|
107
|
+
const inMatch = conditionContent.match(/^(.+?)\s+in\s+(.+)$/);
|
|
108
|
+
if (!inMatch) return null;
|
|
109
|
+
|
|
110
|
+
const varPart = inMatch[1].trim();
|
|
111
|
+
const expression = inMatch[2].trim();
|
|
112
|
+
|
|
113
|
+
// Parse variables
|
|
114
|
+
let variables: string[];
|
|
115
|
+
let variant: 'single' | 'indexed' | 'numeric';
|
|
116
|
+
|
|
117
|
+
if (varPart.includes(',')) {
|
|
118
|
+
// Indexed variant: "idx, item"
|
|
119
|
+
variables = varPart.split(',').map(v => v.trim());
|
|
120
|
+
if (variables.length !== 2) return null;
|
|
121
|
+
variant = 'indexed';
|
|
122
|
+
} else {
|
|
123
|
+
variables = [varPart];
|
|
124
|
+
variant = 'single'; // Will be determined at execution time if numeric
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Skip whitespace
|
|
128
|
+
while (pos < template.length && /\s/.test(template[pos])) {
|
|
129
|
+
pos++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Expect {
|
|
133
|
+
if (template[pos] !== '{') return null;
|
|
134
|
+
|
|
135
|
+
// Parse balanced braces
|
|
136
|
+
const blockStart = pos + 1;
|
|
137
|
+
pos++;
|
|
138
|
+
depth = 1;
|
|
139
|
+
|
|
140
|
+
while (pos < template.length && depth > 0) {
|
|
141
|
+
const ch = template[pos];
|
|
142
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
143
|
+
pos = this.skipString(template, pos, ch);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (ch === '{') depth++;
|
|
147
|
+
else if (ch === '}') depth--;
|
|
148
|
+
pos++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (depth !== 0) return null;
|
|
152
|
+
const block = template.slice(blockStart, pos - 1);
|
|
153
|
+
|
|
154
|
+
return { start, end: pos, variant, variables, expression, block };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private skipString(input: string, pos: number, quote: string): number {
|
|
158
|
+
pos++;
|
|
159
|
+
while (pos < input.length) {
|
|
160
|
+
if (input[pos] === '\\') {
|
|
161
|
+
pos += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (input[pos] === quote) {
|
|
165
|
+
return pos + 1;
|
|
166
|
+
}
|
|
167
|
+
pos++;
|
|
168
|
+
}
|
|
169
|
+
return pos;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public execute(match: RuleMatch, scope: Scope, engine?: any): RuleResult {
|
|
173
|
+
const data = (match as ForMatch).data;
|
|
174
|
+
const observables: any[] = [];
|
|
175
|
+
const outputs: string[] = [];
|
|
176
|
+
|
|
177
|
+
// Evaluate expression
|
|
178
|
+
const expr = new Expression(data.expression);
|
|
179
|
+
|
|
180
|
+
// Находим Observable в выражении
|
|
181
|
+
const exprObservables = expr.findObservables(scope);
|
|
182
|
+
observables.push(...exprObservables);
|
|
183
|
+
|
|
184
|
+
let collection = expr.execute(scope);
|
|
185
|
+
|
|
186
|
+
// Check if Observable
|
|
187
|
+
if (isObservable(collection)) {
|
|
188
|
+
observables.push(collection);
|
|
189
|
+
collection = collection.getObject?.() ?? collection;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Determine iteration type
|
|
193
|
+
if (typeof collection === 'number') {
|
|
194
|
+
// Numeric iteration: 0 to collection-1
|
|
195
|
+
if (data.variant === 'indexed') {
|
|
196
|
+
throw new InvalidTemplateEngineSyntaxException(
|
|
197
|
+
'@for with numeric value does not support indexed variant (idx, var). Use single variable.'
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const count = Math.floor(collection);
|
|
202
|
+
if (count < 0) {
|
|
203
|
+
throw new InvalidTemplateEngineSyntaxException(
|
|
204
|
+
`@for numeric value must be non-negative, got: ${count}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < count; i++) {
|
|
209
|
+
const localScope = scope.createChild({ [data.variables[0]]: i });
|
|
210
|
+
|
|
211
|
+
if (engine) {
|
|
212
|
+
const result = engine.processTemplate(data.block, localScope);
|
|
213
|
+
outputs.push(result.output);
|
|
214
|
+
if (result.observables) observables.push(...result.observables);
|
|
215
|
+
} else {
|
|
216
|
+
outputs.push(data.block);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} else if (Array.isArray(collection) || (collection && typeof collection[Symbol.iterator] === 'function')) {
|
|
220
|
+
// Array or iterable
|
|
221
|
+
const items = Array.isArray(collection) ? collection : Array.from(collection);
|
|
222
|
+
|
|
223
|
+
if (data.variant === 'indexed') {
|
|
224
|
+
// (idx, item in collection)
|
|
225
|
+
items.forEach((item, idx) => {
|
|
226
|
+
const localScope = scope.createChild({
|
|
227
|
+
[data.variables[0]]: idx,
|
|
228
|
+
[data.variables[1]]: item
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (engine) {
|
|
232
|
+
const result = engine.processTemplate(data.block, localScope);
|
|
233
|
+
outputs.push(result.output);
|
|
234
|
+
if (result.observables) observables.push(...result.observables);
|
|
235
|
+
} else {
|
|
236
|
+
outputs.push(data.block);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
// (item in collection)
|
|
241
|
+
items.forEach((item) => {
|
|
242
|
+
const localScope = scope.createChild({ [data.variables[0]]: item });
|
|
243
|
+
|
|
244
|
+
if (engine) {
|
|
245
|
+
const result = engine.processTemplate(data.block, localScope);
|
|
246
|
+
outputs.push(result.output);
|
|
247
|
+
if (result.observables) observables.push(...result.observables);
|
|
248
|
+
} else {
|
|
249
|
+
outputs.push(data.block);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
} else if (typeof collection === 'string') {
|
|
254
|
+
throw new InvalidTemplateEngineSyntaxException(
|
|
255
|
+
`@for does not support string iteration. Got: "${collection}"`
|
|
256
|
+
);
|
|
257
|
+
} else if (collection === null || collection === undefined) {
|
|
258
|
+
// Empty result
|
|
259
|
+
} else {
|
|
260
|
+
throw new InvalidTemplateEngineSyntaxException(
|
|
261
|
+
`@for expression must return a number, array, or iterable. Got: ${typeof collection}`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { output: outputs.join(''), observables };
|
|
266
|
+
}
|
|
267
|
+
}
|