@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.
- package/out/foundation/Triplet.d.ts.map +1 -1
- package/out/foundation/Triplet.js +4 -5
- package/out/foundation/Triplet.js.map +1 -1
- 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/component_api/Component.d.ts +2 -2
- package/out/foundation/component_api/Component.d.ts.map +1 -1
- package/out/foundation/component_api/Component.js.map +1 -1
- package/out/foundation/component_api/UniHtml.d.ts +4 -10
- package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
- package/out/foundation/component_api/UniHtml.js +7 -15
- package/out/foundation/component_api/UniHtml.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 +85 -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 +61 -0
- package/out/foundation/engine/Scope.d.ts.map +1 -0
- package/out/foundation/engine/Scope.js +156 -0
- package/out/foundation/engine/Scope.js.map +1 -0
- package/out/foundation/engine/TemplateEngine.d.ts +96 -0
- package/out/foundation/engine/TemplateEngine.d.ts.map +1 -0
- package/out/foundation/engine/TemplateEngine.js +235 -0
- package/out/foundation/engine/TemplateEngine.js.map +1 -0
- package/out/foundation/engine/TemplateInstance.d.ts +241 -0
- package/out/foundation/engine/TemplateInstance.d.ts.map +1 -0
- package/out/foundation/engine/TemplateInstance.js +637 -0
- package/out/foundation/engine/TemplateInstance.js.map +1 -0
- package/out/foundation/engine/TemplateInstance.old.d.ts +219 -0
- package/out/foundation/engine/TemplateInstance.old.d.ts.map +1 -0
- package/out/foundation/engine/TemplateInstance.old.js +487 -0
- package/out/foundation/engine/TemplateInstance.old.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 +104 -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 +2 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +2 -0
- package/out/index.js.map +1 -1
- package/package.json +1 -1
- package/src/foundation/Triplet.ts +6 -6
- package/src/foundation/api/Observer.ts +60 -0
- package/src/foundation/component_api/Component.ts +2 -1
- package/src/foundation/component_api/UniHtml.ts +12 -22
- 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 +138 -0
- package/src/foundation/engine/Scope.ts +176 -0
- package/src/foundation/engine/TemplateEngine.ts +318 -0
- package/src/foundation/engine/TemplateInstance.md +110 -0
- package/src/foundation/engine/TemplateInstance.old.ts +673 -0
- package/src/foundation/engine/TemplateInstance.ts +843 -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 +126 -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 +8 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import Scope from './Scope.js';
|
|
2
|
+
import Expression from './Expression.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* RuleMatch - результат поиска Rule в шаблоне
|
|
6
|
+
*/
|
|
7
|
+
export interface RuleMatch {
|
|
8
|
+
/** Полное совпадение включая синтаксис Rule */
|
|
9
|
+
fullMatch: string;
|
|
10
|
+
/** Начальная позиция в исходной строке */
|
|
11
|
+
start: number;
|
|
12
|
+
/** Конечная позиция в исходной строке */
|
|
13
|
+
end: number;
|
|
14
|
+
/** Дополнительные данные специфичные для Rule */
|
|
15
|
+
data?: Record<string, any>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* RuleResult - результат выполнения Rule
|
|
20
|
+
*/
|
|
21
|
+
export interface RuleResult {
|
|
22
|
+
/** HTML-результат для замены */
|
|
23
|
+
output: string;
|
|
24
|
+
/** Использованные Observable для отслеживания */
|
|
25
|
+
observables?: any[];
|
|
26
|
+
/** Дочерние Rule (для вложенных структур) */
|
|
27
|
+
children?: Rule[];
|
|
28
|
+
/** Дополнительные данные (например, refName для RefRule) */
|
|
29
|
+
data?: Record<string, any>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* RuleType - тип Rule
|
|
34
|
+
*/
|
|
35
|
+
export type RuleType = 'syntax' | 'attribute';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Rule - базовый абстрактный класс для всех правил шаблонизатора.
|
|
39
|
+
*/
|
|
40
|
+
export default abstract class Rule {
|
|
41
|
+
/** Уникальное имя правила */
|
|
42
|
+
public abstract readonly name: string;
|
|
43
|
+
|
|
44
|
+
/** Тип правила: syntax или attribute */
|
|
45
|
+
public abstract readonly type: RuleType;
|
|
46
|
+
|
|
47
|
+
/** Приоритет выполнения (меньше = раньше) */
|
|
48
|
+
public readonly priority: number = 100;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Найти все вхождения этого Rule в шаблоне.
|
|
52
|
+
* @param template - исходный шаблон
|
|
53
|
+
* @returns массив найденных совпадений
|
|
54
|
+
*/
|
|
55
|
+
public abstract find(template: string): RuleMatch[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Выполнить Rule и вернуть результат.
|
|
59
|
+
* @param match - найденное совпадение
|
|
60
|
+
* @param scope - текущий Scope
|
|
61
|
+
* @param engine - ссылка на TemplateEngine (для рекурсивной обработки)
|
|
62
|
+
*/
|
|
63
|
+
public abstract execute(
|
|
64
|
+
match: RuleMatch,
|
|
65
|
+
scope: Scope,
|
|
66
|
+
engine?: any // TemplateEngine, circular dependency workaround
|
|
67
|
+
): RuleResult | Promise<RuleResult>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Проверить, поддерживает ли Rule Observable значения
|
|
71
|
+
*/
|
|
72
|
+
public supportsObservable(): boolean {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* SyntaxRule - базовый класс для синтаксических правил.
|
|
79
|
+
* Синтаксические правила могут быть в любом месте шаблона (кроме атрибутов).
|
|
80
|
+
*/
|
|
81
|
+
export abstract class SyntaxRule extends Rule {
|
|
82
|
+
public readonly type: RuleType = 'syntax';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* AttributeRule - базовый класс для атрибутивных правил.
|
|
87
|
+
* Атрибутивные правила работают только внутри HTML-тегов.
|
|
88
|
+
*/
|
|
89
|
+
export abstract class AttributeRule extends Rule {
|
|
90
|
+
public readonly type: RuleType = 'attribute';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Получить элемент, к которому применяется атрибут.
|
|
94
|
+
* @param template - шаблон
|
|
95
|
+
* @param attributePosition - позиция атрибута
|
|
96
|
+
* @returns информация об элементе
|
|
97
|
+
*/
|
|
98
|
+
protected findParentElement(
|
|
99
|
+
template: string,
|
|
100
|
+
attributePosition: number
|
|
101
|
+
): { tagName: string; tagStart: number; tagEnd: number } | null {
|
|
102
|
+
// Find opening < before attribute
|
|
103
|
+
let tagStart = attributePosition;
|
|
104
|
+
while (tagStart > 0 && template[tagStart] !== '<') {
|
|
105
|
+
tagStart--;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (template[tagStart] !== '<') {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Extract tag name
|
|
113
|
+
let nameEnd = tagStart + 1;
|
|
114
|
+
while (nameEnd < template.length && /[a-zA-Z0-9_-]/.test(template[nameEnd])) {
|
|
115
|
+
nameEnd++;
|
|
116
|
+
}
|
|
117
|
+
const tagName = template.slice(tagStart + 1, nameEnd);
|
|
118
|
+
|
|
119
|
+
// Find closing >
|
|
120
|
+
let tagEnd = attributePosition;
|
|
121
|
+
let inString = false;
|
|
122
|
+
let stringChar = '';
|
|
123
|
+
while (tagEnd < template.length) {
|
|
124
|
+
const ch = template[tagEnd];
|
|
125
|
+
if (!inString && (ch === '"' || ch === "'")) {
|
|
126
|
+
inString = true;
|
|
127
|
+
stringChar = ch;
|
|
128
|
+
} else if (inString && ch === stringChar) {
|
|
129
|
+
inString = false;
|
|
130
|
+
} else if (!inString && ch === '>') {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
tagEnd++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { tagName, tagStart, tagEnd: tagEnd + 1 };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope - класс для работы с контекстом переменных и функций.
|
|
3
|
+
* Поддерживает объединение нескольких Scope, извлечение полей из объектов и классов.
|
|
4
|
+
*/
|
|
5
|
+
export default class Scope {
|
|
6
|
+
private variables: Record<string, any> = {};
|
|
7
|
+
private readonly debugWarnings: boolean;
|
|
8
|
+
/** Оригинальный объект для синхронизации refs */
|
|
9
|
+
private originalSource: object | null = null;
|
|
10
|
+
|
|
11
|
+
constructor(options?: { debugWarnings?: boolean }) {
|
|
12
|
+
this.debugWarnings = options?.debugWarnings ?? true;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Получить все переменные Scope как Record
|
|
17
|
+
*/
|
|
18
|
+
public getVariables(): Record<string, any> {
|
|
19
|
+
return { ...this.variables };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Установить переменную в Scope
|
|
24
|
+
* @param syncToOriginal - синхронизировать с оригинальным объектом (по умолчанию true)
|
|
25
|
+
*/
|
|
26
|
+
public set(key: string, value: any, syncToOriginal: boolean = true): void {
|
|
27
|
+
if (this.debugWarnings && key in this.variables && this.variables[key] !== undefined) {
|
|
28
|
+
// Не предупреждаем при установке ref (значение было undefined)
|
|
29
|
+
}
|
|
30
|
+
this.variables[key] = value;
|
|
31
|
+
|
|
32
|
+
// Синхронизируем с оригинальным объектом если он есть
|
|
33
|
+
if (syncToOriginal && this.originalSource) {
|
|
34
|
+
(this.originalSource as any)[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Получить переменную из Scope
|
|
40
|
+
*/
|
|
41
|
+
public get(key: string): any {
|
|
42
|
+
return this.variables[key];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Проверить наличие переменной
|
|
47
|
+
*/
|
|
48
|
+
public has(key: string): boolean {
|
|
49
|
+
return key in this.variables;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Удалить переменную из Scope
|
|
54
|
+
*/
|
|
55
|
+
public delete(key: string): boolean {
|
|
56
|
+
if (key in this.variables) {
|
|
57
|
+
delete this.variables[key];
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Объединить другой Scope или объект в текущий Scope.
|
|
65
|
+
* При конфликте имён выводится предупреждение, последнее значение имеет приоритет.
|
|
66
|
+
*/
|
|
67
|
+
public merge(source: Scope | Record<string, any> | object): Scope {
|
|
68
|
+
const sourceVars = source instanceof Scope
|
|
69
|
+
? source.getVariables()
|
|
70
|
+
: this.extractFromObject(source);
|
|
71
|
+
|
|
72
|
+
for (const [key, value] of Object.entries(sourceVars)) {
|
|
73
|
+
if (this.debugWarnings && key in this.variables) {
|
|
74
|
+
console.warn(`[Scope] Warning: Variable "${key}" conflict during merge, using new value`);
|
|
75
|
+
}
|
|
76
|
+
this.variables[key] = value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Создать дочерний (локальный) Scope на основе текущего.
|
|
84
|
+
* Изменения в дочернем Scope не влияют на родительский.
|
|
85
|
+
*/
|
|
86
|
+
public createChild(localVariables?: Record<string, any>): Scope {
|
|
87
|
+
const child = new Scope({ debugWarnings: this.debugWarnings });
|
|
88
|
+
child.variables = { ...this.variables };
|
|
89
|
+
|
|
90
|
+
if (localVariables) {
|
|
91
|
+
for (const [key, value] of Object.entries(localVariables)) {
|
|
92
|
+
child.variables[key] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return child;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Извлечь поля и методы из объекта или экземпляра класса.
|
|
101
|
+
* Пропускает нативные DOM/браузерные прототипы.
|
|
102
|
+
*/
|
|
103
|
+
public extractFromObject(obj: object): Record<string, any> {
|
|
104
|
+
const ctx: Record<string, any> = {};
|
|
105
|
+
|
|
106
|
+
if (!obj || typeof obj !== 'object') {
|
|
107
|
+
return ctx;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Copy own properties
|
|
111
|
+
for (const key of Object.keys(obj)) {
|
|
112
|
+
ctx[key] = (obj as any)[key];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Copy prototype methods (for class instances)
|
|
116
|
+
let proto = Object.getPrototypeOf(obj);
|
|
117
|
+
while (proto && proto !== Object.prototype) {
|
|
118
|
+
// Avoid copying host (DOM/native) prototype methods which may throw
|
|
119
|
+
const ctorName = proto?.constructor
|
|
120
|
+
? String(proto.constructor.name ?? '')
|
|
121
|
+
: '';
|
|
122
|
+
|
|
123
|
+
if (/HTMLElement|Element|Node|EventTarget|Window|GlobalThis/i.test(ctorName)) {
|
|
124
|
+
proto = Object.getPrototypeOf(proto);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
129
|
+
if (key === 'constructor' || key in ctx) continue;
|
|
130
|
+
|
|
131
|
+
let desc: PropertyDescriptor | undefined;
|
|
132
|
+
try {
|
|
133
|
+
desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!desc) continue;
|
|
138
|
+
|
|
139
|
+
// Only bind plain function values — don't copy getters/setters
|
|
140
|
+
if (typeof desc.value === 'function') {
|
|
141
|
+
try {
|
|
142
|
+
ctx[key] = (desc.value as Function).bind(obj);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
proto = Object.getPrototypeOf(proto);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return ctx;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Статический метод для создания Scope из объекта
|
|
157
|
+
* Сохраняет ссылку на оригинальный объект для синхронизации refs
|
|
158
|
+
*/
|
|
159
|
+
public static from(source: object | Record<string, any>, options?: { debugWarnings?: boolean }): Scope {
|
|
160
|
+
const scope = new Scope(options);
|
|
161
|
+
scope.originalSource = source;
|
|
162
|
+
scope.merge(source);
|
|
163
|
+
return scope;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Статический метод для объединения нескольких Scope/объектов
|
|
168
|
+
*/
|
|
169
|
+
public static combine(...sources: (Scope | Record<string, any> | object)[]): Scope {
|
|
170
|
+
const scope = new Scope();
|
|
171
|
+
for (const source of sources) {
|
|
172
|
+
scope.merge(source);
|
|
173
|
+
}
|
|
174
|
+
return scope;
|
|
175
|
+
}
|
|
176
|
+
}
|