@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,136 @@
|
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* RuleType - тип Rule
|
|
32
|
+
*/
|
|
33
|
+
export type RuleType = 'syntax' | 'attribute';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Rule - базовый абстрактный класс для всех правил шаблонизатора.
|
|
37
|
+
*/
|
|
38
|
+
export default abstract class Rule {
|
|
39
|
+
/** Уникальное имя правила */
|
|
40
|
+
public abstract readonly name: string;
|
|
41
|
+
|
|
42
|
+
/** Тип правила: syntax или attribute */
|
|
43
|
+
public abstract readonly type: RuleType;
|
|
44
|
+
|
|
45
|
+
/** Приоритет выполнения (меньше = раньше) */
|
|
46
|
+
public readonly priority: number = 100;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Найти все вхождения этого Rule в шаблоне.
|
|
50
|
+
* @param template - исходный шаблон
|
|
51
|
+
* @returns массив найденных совпадений
|
|
52
|
+
*/
|
|
53
|
+
public abstract find(template: string): RuleMatch[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Выполнить Rule и вернуть результат.
|
|
57
|
+
* @param match - найденное совпадение
|
|
58
|
+
* @param scope - текущий Scope
|
|
59
|
+
* @param engine - ссылка на TemplateEngine (для рекурсивной обработки)
|
|
60
|
+
*/
|
|
61
|
+
public abstract execute(
|
|
62
|
+
match: RuleMatch,
|
|
63
|
+
scope: Scope,
|
|
64
|
+
engine?: any // TemplateEngine, circular dependency workaround
|
|
65
|
+
): RuleResult | Promise<RuleResult>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Проверить, поддерживает ли Rule Observable значения
|
|
69
|
+
*/
|
|
70
|
+
public supportsObservable(): boolean {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* SyntaxRule - базовый класс для синтаксических правил.
|
|
77
|
+
* Синтаксические правила могут быть в любом месте шаблона (кроме атрибутов).
|
|
78
|
+
*/
|
|
79
|
+
export abstract class SyntaxRule extends Rule {
|
|
80
|
+
public readonly type: RuleType = 'syntax';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* AttributeRule - базовый класс для атрибутивных правил.
|
|
85
|
+
* Атрибутивные правила работают только внутри HTML-тегов.
|
|
86
|
+
*/
|
|
87
|
+
export abstract class AttributeRule extends Rule {
|
|
88
|
+
public readonly type: RuleType = 'attribute';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Получить элемент, к которому применяется атрибут.
|
|
92
|
+
* @param template - шаблон
|
|
93
|
+
* @param attributePosition - позиция атрибута
|
|
94
|
+
* @returns информация об элементе
|
|
95
|
+
*/
|
|
96
|
+
protected findParentElement(
|
|
97
|
+
template: string,
|
|
98
|
+
attributePosition: number
|
|
99
|
+
): { tagName: string; tagStart: number; tagEnd: number } | null {
|
|
100
|
+
// Find opening < before attribute
|
|
101
|
+
let tagStart = attributePosition;
|
|
102
|
+
while (tagStart > 0 && template[tagStart] !== '<') {
|
|
103
|
+
tagStart--;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (template[tagStart] !== '<') {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Extract tag name
|
|
111
|
+
let nameEnd = tagStart + 1;
|
|
112
|
+
while (nameEnd < template.length && /[a-zA-Z0-9_-]/.test(template[nameEnd])) {
|
|
113
|
+
nameEnd++;
|
|
114
|
+
}
|
|
115
|
+
const tagName = template.slice(tagStart + 1, nameEnd);
|
|
116
|
+
|
|
117
|
+
// Find closing >
|
|
118
|
+
let tagEnd = attributePosition;
|
|
119
|
+
let inString = false;
|
|
120
|
+
let stringChar = '';
|
|
121
|
+
while (tagEnd < template.length) {
|
|
122
|
+
const ch = template[tagEnd];
|
|
123
|
+
if (!inString && (ch === '"' || ch === "'")) {
|
|
124
|
+
inString = true;
|
|
125
|
+
stringChar = ch;
|
|
126
|
+
} else if (inString && ch === stringChar) {
|
|
127
|
+
inString = false;
|
|
128
|
+
} else if (!inString && ch === '>') {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
tagEnd++;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { tagName, tagStart, tagEnd: tagEnd + 1 };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope - класс для работы с контекстом переменных и функций.
|
|
3
|
+
* Поддерживает объединение нескольких Scope, извлечение полей из объектов и классов.
|
|
4
|
+
*/
|
|
5
|
+
export default class Scope {
|
|
6
|
+
private variables: Record<string, any> = {};
|
|
7
|
+
private readonly debugWarnings: boolean;
|
|
8
|
+
|
|
9
|
+
constructor(options?: { debugWarnings?: boolean }) {
|
|
10
|
+
this.debugWarnings = options?.debugWarnings ?? true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Получить все переменные Scope как Record
|
|
15
|
+
*/
|
|
16
|
+
public getVariables(): Record<string, any> {
|
|
17
|
+
return { ...this.variables };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Установить переменную в Scope
|
|
22
|
+
*/
|
|
23
|
+
public set(key: string, value: any): void {
|
|
24
|
+
if (this.debugWarnings && key in this.variables) {
|
|
25
|
+
console.warn(`[Scope] Warning: Variable "${key}" is being overwritten`);
|
|
26
|
+
}
|
|
27
|
+
this.variables[key] = value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Получить переменную из Scope
|
|
32
|
+
*/
|
|
33
|
+
public get(key: string): any {
|
|
34
|
+
return this.variables[key];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Проверить наличие переменной
|
|
39
|
+
*/
|
|
40
|
+
public has(key: string): boolean {
|
|
41
|
+
return key in this.variables;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Удалить переменную из Scope
|
|
46
|
+
*/
|
|
47
|
+
public delete(key: string): boolean {
|
|
48
|
+
if (key in this.variables) {
|
|
49
|
+
delete this.variables[key];
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Объединить другой Scope или объект в текущий Scope.
|
|
57
|
+
* При конфликте имён выводится предупреждение, последнее значение имеет приоритет.
|
|
58
|
+
*/
|
|
59
|
+
public merge(source: Scope | Record<string, any> | object): Scope {
|
|
60
|
+
const sourceVars = source instanceof Scope
|
|
61
|
+
? source.getVariables()
|
|
62
|
+
: this.extractFromObject(source);
|
|
63
|
+
|
|
64
|
+
for (const [key, value] of Object.entries(sourceVars)) {
|
|
65
|
+
if (this.debugWarnings && key in this.variables) {
|
|
66
|
+
console.warn(`[Scope] Warning: Variable "${key}" conflict during merge, using new value`);
|
|
67
|
+
}
|
|
68
|
+
this.variables[key] = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Создать дочерний (локальный) Scope на основе текущего.
|
|
76
|
+
* Изменения в дочернем Scope не влияют на родительский.
|
|
77
|
+
*/
|
|
78
|
+
public createChild(localVariables?: Record<string, any>): Scope {
|
|
79
|
+
const child = new Scope({ debugWarnings: this.debugWarnings });
|
|
80
|
+
child.variables = { ...this.variables };
|
|
81
|
+
|
|
82
|
+
if (localVariables) {
|
|
83
|
+
for (const [key, value] of Object.entries(localVariables)) {
|
|
84
|
+
child.variables[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return child;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Извлечь поля и методы из объекта или экземпляра класса.
|
|
93
|
+
* Пропускает нативные DOM/браузерные прототипы.
|
|
94
|
+
*/
|
|
95
|
+
public extractFromObject(obj: object): Record<string, any> {
|
|
96
|
+
const ctx: Record<string, any> = {};
|
|
97
|
+
|
|
98
|
+
if (!obj || typeof obj !== 'object') {
|
|
99
|
+
return ctx;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Copy own properties
|
|
103
|
+
for (const key of Object.keys(obj)) {
|
|
104
|
+
ctx[key] = (obj as any)[key];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Copy prototype methods (for class instances)
|
|
108
|
+
let proto = Object.getPrototypeOf(obj);
|
|
109
|
+
while (proto && proto !== Object.prototype) {
|
|
110
|
+
// Avoid copying host (DOM/native) prototype methods which may throw
|
|
111
|
+
const ctorName = proto?.constructor
|
|
112
|
+
? String(proto.constructor.name ?? '')
|
|
113
|
+
: '';
|
|
114
|
+
|
|
115
|
+
if (/HTMLElement|Element|Node|EventTarget|Window|GlobalThis/i.test(ctorName)) {
|
|
116
|
+
proto = Object.getPrototypeOf(proto);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
121
|
+
if (key === 'constructor' || key in ctx) continue;
|
|
122
|
+
|
|
123
|
+
let desc: PropertyDescriptor | undefined;
|
|
124
|
+
try {
|
|
125
|
+
desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
126
|
+
} catch {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!desc) continue;
|
|
130
|
+
|
|
131
|
+
// Only bind plain function values — don't copy getters/setters
|
|
132
|
+
if (typeof desc.value === 'function') {
|
|
133
|
+
try {
|
|
134
|
+
ctx[key] = (desc.value as Function).bind(obj);
|
|
135
|
+
} catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
proto = Object.getPrototypeOf(proto);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return ctx;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Статический метод для создания Scope из объекта
|
|
149
|
+
*/
|
|
150
|
+
public static from(source: object | Record<string, any>, options?: { debugWarnings?: boolean }): Scope {
|
|
151
|
+
const scope = new Scope(options);
|
|
152
|
+
scope.merge(source);
|
|
153
|
+
return scope;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Статический метод для объединения нескольких Scope/объектов
|
|
158
|
+
*/
|
|
159
|
+
public static combine(...sources: (Scope | Record<string, any> | object)[]): Scope {
|
|
160
|
+
const scope = new Scope();
|
|
161
|
+
for (const source of sources) {
|
|
162
|
+
scope.merge(source);
|
|
163
|
+
}
|
|
164
|
+
return scope;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import Scope from './Scope.js';
|
|
2
|
+
import Expression from './Expression.js';
|
|
3
|
+
import EscapeHandler from './EscapeHandler.js';
|
|
4
|
+
import TemplateInstance, { TemplateSection } from './TemplateInstance.js';
|
|
5
|
+
import Rule, { RuleMatch, RuleResult, SyntaxRule, AttributeRule } from './Rule.js';
|
|
6
|
+
import Observable from '../api/Observer.js';
|
|
7
|
+
|
|
8
|
+
// Import rules
|
|
9
|
+
import ExpressionRule from './rules/syntax/ExpressionRule.js';
|
|
10
|
+
import IfRule from './rules/syntax/IfRule.js';
|
|
11
|
+
import ForRule from './rules/syntax/ForRule.js';
|
|
12
|
+
import RefRule from './rules/attribute/RefRule.js';
|
|
13
|
+
import EventRule from './rules/attribute/EventRule.js';
|
|
14
|
+
import InjectionRule from './rules/attribute/InjectionRule.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* TemplateEngineOptions - настройки TemplateEngine
|
|
18
|
+
*/
|
|
19
|
+
export interface TemplateEngineOptions {
|
|
20
|
+
/** Оставлять синтаксис атрибутивных Rule в финальном HTML */
|
|
21
|
+
showAttributeRule?: boolean;
|
|
22
|
+
/** Включить предупреждения отладки */
|
|
23
|
+
debugWarnings?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* ProcessResult - результат обработки шаблона
|
|
28
|
+
*/
|
|
29
|
+
export interface ProcessResult {
|
|
30
|
+
output: string;
|
|
31
|
+
observables: Observable<any>[];
|
|
32
|
+
sections: TemplateSection[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* TemplateEngine - главный класс шаблонизатора.
|
|
37
|
+
* Обрабатывает HTML-шаблон с Rule и создаёт TemplateInstance.
|
|
38
|
+
*/
|
|
39
|
+
export default class TemplateEngine {
|
|
40
|
+
private readonly scope: Scope;
|
|
41
|
+
private readonly options: TemplateEngineOptions;
|
|
42
|
+
private readonly syntaxRules: SyntaxRule[] = [];
|
|
43
|
+
private readonly attributeRules: AttributeRule[] = [];
|
|
44
|
+
|
|
45
|
+
constructor(scope: Scope | object, options?: TemplateEngineOptions) {
|
|
46
|
+
this.scope = scope instanceof Scope ? scope : Scope.from(scope);
|
|
47
|
+
this.options = {
|
|
48
|
+
showAttributeRule: false,
|
|
49
|
+
debugWarnings: true,
|
|
50
|
+
...options
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Register default rules (sorted by priority)
|
|
54
|
+
this.registerDefaultRules();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Зарегистрировать стандартные правила
|
|
59
|
+
*/
|
|
60
|
+
private registerDefaultRules(): void {
|
|
61
|
+
// Syntax rules
|
|
62
|
+
this.syntaxRules.push(new ForRule());
|
|
63
|
+
this.syntaxRules.push(new IfRule());
|
|
64
|
+
this.syntaxRules.push(new ExpressionRule());
|
|
65
|
+
|
|
66
|
+
// Attribute rules
|
|
67
|
+
this.attributeRules.push(new RefRule());
|
|
68
|
+
this.attributeRules.push(new EventRule());
|
|
69
|
+
this.attributeRules.push(new InjectionRule());
|
|
70
|
+
|
|
71
|
+
// Sort by priority
|
|
72
|
+
this.syntaxRules.sort((a, b) => a.priority - b.priority);
|
|
73
|
+
this.attributeRules.sort((a, b) => a.priority - b.priority);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Добавить пользовательское правило
|
|
78
|
+
*/
|
|
79
|
+
public addRule(rule: Rule): void {
|
|
80
|
+
if (rule.type === 'syntax') {
|
|
81
|
+
this.syntaxRules.push(rule as SyntaxRule);
|
|
82
|
+
this.syntaxRules.sort((a, b) => a.priority - b.priority);
|
|
83
|
+
} else {
|
|
84
|
+
this.attributeRules.push(rule as AttributeRule);
|
|
85
|
+
this.attributeRules.sort((a, b) => a.priority - b.priority);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Получить Scope
|
|
91
|
+
*/
|
|
92
|
+
public getScope(): Scope {
|
|
93
|
+
return this.scope;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Обработать шаблон и вернуть TemplateInstance
|
|
98
|
+
*/
|
|
99
|
+
public parse(template: string): TemplateInstance {
|
|
100
|
+
const result = this.processTemplate(template, this.scope);
|
|
101
|
+
const templateInstance = new TemplateInstance(result.output, this.scope);
|
|
102
|
+
|
|
103
|
+
// Add sections
|
|
104
|
+
for (const section of result.sections) {
|
|
105
|
+
templateInstance.addSection(section);
|
|
106
|
+
|
|
107
|
+
// Track observables
|
|
108
|
+
for (const observable of section.result.observables || []) {
|
|
109
|
+
templateInstance.trackObservable(observable, section, (s) => {
|
|
110
|
+
return this.processTemplate(s.sourceTemplate, this.scope);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return templateInstance;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Обработать шаблон (внутренний метод, используется Rule для рекурсии)
|
|
120
|
+
*/
|
|
121
|
+
public processTemplate(template: string, scope: Scope): ProcessResult {
|
|
122
|
+
const allObservables: Observable<any>[] = [];
|
|
123
|
+
const allSections: TemplateSection[] = [];
|
|
124
|
+
|
|
125
|
+
// Step 1: Handle @@ escape sequences
|
|
126
|
+
let processed = EscapeHandler.process(template, (escaped) => {
|
|
127
|
+
let result = escaped;
|
|
128
|
+
|
|
129
|
+
// Step 2: Process syntax rules (in priority order)
|
|
130
|
+
for (const rule of this.syntaxRules) {
|
|
131
|
+
result = this.processRule(rule, result, scope, allObservables, allSections);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Step 3: Process attribute rules
|
|
135
|
+
for (const rule of this.attributeRules) {
|
|
136
|
+
// Skip injection - processed last
|
|
137
|
+
if (rule.name === 'injection') continue;
|
|
138
|
+
result = this.processRule(rule, result, scope, allObservables, allSections);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return result;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Step 4: Process injection rules last
|
|
145
|
+
const injectionRule = this.attributeRules.find(r => r.name === 'injection');
|
|
146
|
+
if (injectionRule) {
|
|
147
|
+
processed = this.processRule(injectionRule, processed, scope, allObservables, allSections);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Step 5: Handle showAttributeRule option
|
|
151
|
+
if (!this.options.showAttributeRule) {
|
|
152
|
+
processed = this.removeAttributeSyntax(processed);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
output: processed,
|
|
157
|
+
observables: allObservables,
|
|
158
|
+
sections: allSections
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Обработать одно правило
|
|
164
|
+
*/
|
|
165
|
+
private processRule(
|
|
166
|
+
rule: Rule,
|
|
167
|
+
template: string,
|
|
168
|
+
scope: Scope,
|
|
169
|
+
observables: Observable<any>[],
|
|
170
|
+
sections: TemplateSection[]
|
|
171
|
+
): string {
|
|
172
|
+
const matches = rule.find(template);
|
|
173
|
+
|
|
174
|
+
// Sort matches by position (reverse to process from end)
|
|
175
|
+
matches.sort((a, b) => b.start - a.start);
|
|
176
|
+
|
|
177
|
+
let result = template;
|
|
178
|
+
|
|
179
|
+
for (const match of matches) {
|
|
180
|
+
try {
|
|
181
|
+
const ruleResult = rule.execute(match, scope, this) as RuleResult;
|
|
182
|
+
|
|
183
|
+
// Track observables
|
|
184
|
+
if (ruleResult.observables) {
|
|
185
|
+
observables.push(...ruleResult.observables);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create section
|
|
189
|
+
const section: TemplateSection = {
|
|
190
|
+
rule,
|
|
191
|
+
match,
|
|
192
|
+
result: ruleResult,
|
|
193
|
+
sourceTemplate: match.fullMatch,
|
|
194
|
+
children: [],
|
|
195
|
+
subscriptions: []
|
|
196
|
+
};
|
|
197
|
+
sections.push(section);
|
|
198
|
+
|
|
199
|
+
// Replace in template
|
|
200
|
+
result = result.slice(0, match.start) + ruleResult.output + result.slice(match.end);
|
|
201
|
+
|
|
202
|
+
} catch (error) {
|
|
203
|
+
console.error(`[TemplateEngine] Error processing rule "${rule.name}":`, error);
|
|
204
|
+
// Continue processing other rules
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Удалить синтаксис атрибутивных Rule из финального HTML
|
|
213
|
+
*/
|
|
214
|
+
private removeAttributeSyntax(template: string): string {
|
|
215
|
+
// Remove @[ref], @on[...], @injection[...] patterns that weren't processed
|
|
216
|
+
return template
|
|
217
|
+
.replace(/@\[ref\]\s*=\s*["'][^"']*["']/gi, '')
|
|
218
|
+
.replace(/@on\[[a-zA-Z]+\]\s*=\s*["'][^"']*["']/gi, '')
|
|
219
|
+
.replace(/@injection\[(head|tail)\]\s*=\s*["'][^"']*["']/gi, '');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Статический метод для быстрой обработки
|
|
224
|
+
*/
|
|
225
|
+
public static process(template: string, scope: object, options?: TemplateEngineOptions): string {
|
|
226
|
+
const engine = new TemplateEngine(scope, options);
|
|
227
|
+
const TemplateInstance = engine.parse(template);
|
|
228
|
+
return TemplateInstance.getTemplate();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Создать TemplateInstance из шаблона
|
|
233
|
+
*/
|
|
234
|
+
public static create(template: string, scope: object, options?: TemplateEngineOptions): TemplateInstance {
|
|
235
|
+
const engine = new TemplateEngine(scope, options);
|
|
236
|
+
return engine.parse(template);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Re-export useful types
|
|
241
|
+
export { Scope, Expression, TemplateInstance, Rule, SyntaxRule, AttributeRule };
|
|
242
|
+
export { ExpressionRule, IfRule, ForRule, RefRule, EventRule, InjectionRule };
|
|
243
|
+
export * from './exceptions/TemplateExceptions.js';
|