@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
|
@@ -28,9 +28,50 @@ export class Observer<T> {
|
|
|
28
28
|
this.listeners.forEach(listener => listener(data));
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
export class MutationObserver<T> implements IMutationObserver<T> {
|
|
33
|
+
private listeners: Array<(oldValue: T, newValue: T) => void> = [];
|
|
34
|
+
|
|
35
|
+
public subscribe(listener: (oldValue: T, newValue: T) => void): void {
|
|
36
|
+
this.listeners.push(listener);
|
|
37
|
+
}
|
|
38
|
+
public unsubscribe(listener: (oldValue: T, newValue: T) => void): void {
|
|
39
|
+
this.listeners = this.listeners.filter(l => l !== listener);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public notify(oldValue: T, newValue: T): void {
|
|
43
|
+
this.listeners.forEach(listener => listener(oldValue, newValue));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Symbol to identify Observable instances
|
|
48
|
+
export const OBSERVABLE_SYMBOL = Symbol.for('Observable');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a value is an Observable
|
|
52
|
+
*/
|
|
53
|
+
export function isObservable<T = any>(value: any): value is Observable<T> {
|
|
54
|
+
return value && value[OBSERVABLE_SYMBOL] === true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Observable - простой реактивный контейнер без Proxy.
|
|
59
|
+
*
|
|
60
|
+
* Для доступа к значению используйте getObject():
|
|
61
|
+
* const user = new Observable({ name: 'Alice', age: 25 });
|
|
62
|
+
* user.getObject().name // 'Alice'
|
|
63
|
+
* user.setObject({ name: 'Bob', age: 30 }); // triggers subscribers
|
|
64
|
+
*
|
|
65
|
+
* В шаблонах синтаксис прозрачный:
|
|
66
|
+
* @(user.name) - автоматически распознаётся как user.getObject().name
|
|
67
|
+
*/
|
|
31
68
|
export default class Observable<T> {
|
|
32
69
|
private object: T;
|
|
33
70
|
private observer: Observer<T> = new Observer<T>();
|
|
71
|
+
private mutationObserver: MutationObserver<T> = new MutationObserver<T>();
|
|
72
|
+
|
|
73
|
+
// Mark as Observable
|
|
74
|
+
public readonly [OBSERVABLE_SYMBOL] = true;
|
|
34
75
|
|
|
35
76
|
constructor(object: T) {
|
|
36
77
|
this.object = object;
|
|
@@ -39,19 +80,38 @@ export default class Observable<T> {
|
|
|
39
80
|
public getObject(): T {
|
|
40
81
|
return this.object;
|
|
41
82
|
}
|
|
83
|
+
|
|
42
84
|
public getObserver(): Observer<T> {
|
|
43
85
|
return this.observer;
|
|
44
86
|
}
|
|
87
|
+
|
|
88
|
+
public getMutationObserver(): MutationObserver<T> {
|
|
89
|
+
return this.mutationObserver;
|
|
90
|
+
}
|
|
45
91
|
|
|
46
92
|
public subscribe(listener: (data: T) => void): void {
|
|
47
93
|
this.observer.subscribe(listener);
|
|
48
94
|
}
|
|
95
|
+
|
|
49
96
|
public unsubscribe(listener: (data: T) => void): void {
|
|
50
97
|
this.observer.unsubscribe(listener);
|
|
51
98
|
}
|
|
52
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Subscribe to mutation events (oldValue, newValue)
|
|
102
|
+
*/
|
|
103
|
+
public subscribeMutation(listener: (oldValue: T, newValue: T) => void): void {
|
|
104
|
+
this.mutationObserver.subscribe(listener);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public unsubscribeMutation(listener: (oldValue: T, newValue: T) => void): void {
|
|
108
|
+
this.mutationObserver.unsubscribe(listener);
|
|
109
|
+
}
|
|
110
|
+
|
|
53
111
|
public setObject(object: T): void {
|
|
112
|
+
const oldObject = this.object;
|
|
54
113
|
this.object = object;
|
|
55
114
|
this.observer.notify(this.object);
|
|
115
|
+
this.mutationObserver.notify(oldObject, this.object);
|
|
56
116
|
}
|
|
57
117
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import IElementHolder from "../api/ElementHolder.js";
|
|
2
2
|
import UniHtml from "../component_api/UniHtml.js";
|
|
3
|
+
import TemplateInstance from "../engine/TemplateInstance.js";
|
|
3
4
|
import Attribute from "./Attribute.js";
|
|
4
5
|
import { Class, Mixined } from "./mixin/Proto.js";
|
|
5
6
|
|
|
@@ -56,7 +57,7 @@ export default class Component extends Class(HTMLElement).extend(UniHtml).build(
|
|
|
56
57
|
|
|
57
58
|
this.load(this.shadowRoot);
|
|
58
59
|
}
|
|
59
|
-
protected render(element:
|
|
60
|
+
protected render(element: TemplateInstance, renderTarget: HTMLElement | ShadowRoot): Promise<void> {
|
|
60
61
|
(this.getMixin(UniHtml)?.instance.get() as any).render(element, renderTarget);
|
|
61
62
|
//super.render(element, renderTarget);
|
|
62
63
|
//this.shadowRoot!.appendChild(renderTarget);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Designed to replace legacy Page and Component base classes.
|
|
7
7
|
*/
|
|
8
8
|
import IElementHolder from "../api/ElementHolder.js";
|
|
9
|
+
import { TemplateInstance } from "../engine/TemplateEngine.js";
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -13,7 +14,6 @@ import IElementHolder from "../api/ElementHolder.js";
|
|
|
13
14
|
* Use static factory methods for instantiation.
|
|
14
15
|
*/
|
|
15
16
|
export default class UniHtml {
|
|
16
|
-
|
|
17
17
|
/**
|
|
18
18
|
* Unified component lifecycle entrypoint.
|
|
19
19
|
* Loads HTML, then calls preLoadJS, render, and postLoadJS hooks in order.
|
|
@@ -21,30 +21,27 @@ export default class UniHtml {
|
|
|
21
21
|
*/
|
|
22
22
|
public async load(element: HTMLElement | ShadowRoot): Promise<void> {;
|
|
23
23
|
await this.preInit();
|
|
24
|
-
const preHtml:
|
|
25
|
-
const html:
|
|
24
|
+
const preHtml: TemplateInstance = await this._init();
|
|
25
|
+
const html: TemplateInstance = await this._postInit(preHtml);
|
|
26
26
|
|
|
27
|
-
const localRoot = html;
|
|
28
|
-
|
|
29
|
-
const holder : IElementHolder = { element: localRoot };
|
|
30
27
|
|
|
31
28
|
// ВАЖНО: preLoad() вызывается ДО монтирования в DOM/Shadow DOM.
|
|
32
29
|
// Для компонентов (UniHtmlComponent) на этом этапе ещё нельзя полагаться на this.shadowRoot —
|
|
33
30
|
// используйте переданный localRoot для подготовки DOM, данных и навешивания обработчиков.
|
|
34
31
|
// Это предпочтительный этап инициализации для компонентов.
|
|
35
|
-
await this.preLoad(
|
|
32
|
+
await this.preLoad(html);
|
|
36
33
|
// render() отвечает за помещение содержимого из localRoot в конечную цель (renderTarget).
|
|
37
34
|
// В UniHtmlComponent.render() после вызова базового render() происходит добавление wrapper в shadowRoot.
|
|
38
|
-
await this.render(
|
|
35
|
+
await this.render(html, element);
|
|
39
36
|
// postLoad() вызывается ПОСЛЕ render(). Для компонентов к этому моменту содержимое уже добавлено
|
|
40
37
|
// внутрь shadowRoot, и можно безопасно работать с this.shadowRoot, измерениями layout и т.п.
|
|
41
|
-
await this.postLoad(
|
|
38
|
+
await this.postLoad(html);
|
|
42
39
|
}
|
|
43
40
|
|
|
44
|
-
private async _postInit(html:
|
|
41
|
+
private async _postInit(html: TemplateInstance): Promise<TemplateInstance> {
|
|
45
42
|
throw new Error("Method not implemented.");
|
|
46
43
|
}
|
|
47
|
-
private async _init(): Promise<
|
|
44
|
+
private async _init(): Promise<TemplateInstance> {
|
|
48
45
|
throw new Error("Method not implemented.");
|
|
49
46
|
}
|
|
50
47
|
|
|
@@ -55,34 +52,27 @@ export default class UniHtml {
|
|
|
55
52
|
* РЕКОМЕНДАЦИЯ: предпочитайте выполнять основную подготовку, поиск элементов, навешивание обработчиков
|
|
56
53
|
* на узлы из localRoot именно здесь; затем render() вставит их в целевой контейнер/теневой DOM.
|
|
57
54
|
*/
|
|
58
|
-
protected async preLoad(
|
|
55
|
+
protected async preLoad(template: TemplateInstance) { }
|
|
59
56
|
/**
|
|
60
57
|
* Hook after rendering (e.g., event binding).
|
|
61
58
|
* Для компонентов вызывается после того, как содержимое вставлено в shadowRoot (см. UniHtmlComponent.render()).
|
|
62
59
|
* Используйте этот этап только когда необходим доступ к реально смонтированному DOM (layout/measurements,
|
|
63
60
|
* интеграции, требующие присутствия в документе). В остальных случаях предпочитайте preLoad().
|
|
64
61
|
*/
|
|
65
|
-
protected async postLoad(
|
|
62
|
+
protected async postLoad(template: TemplateInstance) { }
|
|
66
63
|
/**
|
|
67
64
|
* Main rendering step. By default, simply inserts HTML into the container.
|
|
68
65
|
* Override in subclasses for custom rendering logic.
|
|
69
66
|
* @param element Target container
|
|
70
67
|
* @param html HTML content
|
|
71
68
|
*/
|
|
72
|
-
protected async render(
|
|
69
|
+
protected async render(template: TemplateInstance, renderTarget: HTMLElement | DocumentFragment): Promise<void> {
|
|
73
70
|
// Clear renderTarget
|
|
74
71
|
while (renderTarget.firstChild) {
|
|
75
72
|
renderTarget.removeChild(renderTarget.firstChild);
|
|
76
73
|
}
|
|
77
|
-
|
|
78
|
-
// Move all children from holder.element to renderTarget
|
|
79
|
-
const children = Array.from(holder.element.childNodes);
|
|
80
|
-
for (const child of children) {
|
|
81
|
-
renderTarget.appendChild(child);
|
|
82
|
-
}
|
|
74
|
+
template.bind(renderTarget as any);
|
|
83
75
|
|
|
84
|
-
// Update holder to point to renderTarget (now contains the content)
|
|
85
|
-
(holder as { element: HTMLElement | DocumentFragment }).element = renderTarget;
|
|
86
76
|
return Promise.resolve();
|
|
87
77
|
}
|
|
88
78
|
}
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BalancedParser - утилита для парсинга сбалансированных скобок (), {}.
|
|
3
|
+
* Корректно обрабатывает строки и комментарии.
|
|
4
|
+
*/
|
|
5
|
+
export default class BalancedParser {
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Найти все сбалансированные выражения с заданным opener.
|
|
9
|
+
* @param input - входная строка
|
|
10
|
+
* @param opener - открывающая последовательность, например '@(' или '@for('
|
|
11
|
+
* @returns массив объектов с content и позициями
|
|
12
|
+
*/
|
|
13
|
+
public static parseBalanced(
|
|
14
|
+
input: string,
|
|
15
|
+
opener: string,
|
|
16
|
+
closerChar: ')' | '}' = ')'
|
|
17
|
+
): Array<{ content: string; start: number; end: number }> {
|
|
18
|
+
const results: Array<{ content: string; start: number; end: number }> = [];
|
|
19
|
+
const openerChar = closerChar === ')' ? '(' : '{';
|
|
20
|
+
let i = 0;
|
|
21
|
+
|
|
22
|
+
while (i < input.length) {
|
|
23
|
+
const idx = input.indexOf(opener, i);
|
|
24
|
+
if (idx === -1) break;
|
|
25
|
+
|
|
26
|
+
const contentStart = idx + opener.length;
|
|
27
|
+
let pos = contentStart;
|
|
28
|
+
let depth = 1;
|
|
29
|
+
|
|
30
|
+
while (pos < input.length && depth > 0) {
|
|
31
|
+
const ch = input[pos];
|
|
32
|
+
|
|
33
|
+
// Skip string literals
|
|
34
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
35
|
+
pos = this.skipString(input, pos, ch);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Skip single-line comments
|
|
40
|
+
if (ch === '/' && input[pos + 1] === '/') {
|
|
41
|
+
pos = this.skipLineComment(input, pos);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Skip multi-line comments
|
|
46
|
+
if (ch === '/' && input[pos + 1] === '*') {
|
|
47
|
+
pos = this.skipBlockComment(input, pos);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ch === openerChar) depth++;
|
|
52
|
+
else if (ch === closerChar) depth--;
|
|
53
|
+
|
|
54
|
+
pos++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (depth === 0) {
|
|
58
|
+
results.push({
|
|
59
|
+
content: input.slice(contentStart, pos - 1),
|
|
60
|
+
start: idx,
|
|
61
|
+
end: pos
|
|
62
|
+
});
|
|
63
|
+
i = pos;
|
|
64
|
+
} else {
|
|
65
|
+
// Unbalanced, skip this opener and continue
|
|
66
|
+
i = idx + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return results;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Парсить блочные Rule типа @for(...) { ... }
|
|
75
|
+
* @returns объект с condition (содержимое скобок) и block (содержимое фигурных скобок)
|
|
76
|
+
*/
|
|
77
|
+
public static parseBlockRule(
|
|
78
|
+
input: string,
|
|
79
|
+
opener: string // например '@for', '@if'
|
|
80
|
+
): Array<{ condition: string; block: string; start: number; end: number }> {
|
|
81
|
+
const results: Array<{ condition: string; block: string; start: number; end: number }> = [];
|
|
82
|
+
const openerLower = opener.toLowerCase();
|
|
83
|
+
let i = 0;
|
|
84
|
+
|
|
85
|
+
while (i < input.length) {
|
|
86
|
+
// Case-insensitive search
|
|
87
|
+
const lowerInput = input.toLowerCase();
|
|
88
|
+
let idx = lowerInput.indexOf(openerLower, i);
|
|
89
|
+
if (idx === -1) break;
|
|
90
|
+
|
|
91
|
+
// Check for @@ escape
|
|
92
|
+
if (idx > 0 && input[idx - 1] === '@') {
|
|
93
|
+
i = idx + 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find opening parenthesis
|
|
98
|
+
let parenStart = idx + opener.length;
|
|
99
|
+
while (parenStart < input.length && /\s/.test(input[parenStart])) {
|
|
100
|
+
parenStart++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (input[parenStart] !== '(') {
|
|
104
|
+
i = idx + 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parse balanced parentheses for condition
|
|
109
|
+
let pos = parenStart + 1;
|
|
110
|
+
let depth = 1;
|
|
111
|
+
|
|
112
|
+
while (pos < input.length && depth > 0) {
|
|
113
|
+
const ch = input[pos];
|
|
114
|
+
|
|
115
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
116
|
+
pos = this.skipString(input, pos, ch);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (ch === '/' && input[pos + 1] === '/') {
|
|
120
|
+
pos = this.skipLineComment(input, pos);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === '/' && input[pos + 1] === '*') {
|
|
124
|
+
pos = this.skipBlockComment(input, pos);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (ch === '(') depth++;
|
|
129
|
+
else if (ch === ')') depth--;
|
|
130
|
+
pos++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (depth !== 0) {
|
|
134
|
+
i = idx + 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const condition = input.slice(parenStart + 1, pos - 1);
|
|
139
|
+
const conditionEnd = pos;
|
|
140
|
+
|
|
141
|
+
// Find opening brace for block
|
|
142
|
+
let braceStart = conditionEnd;
|
|
143
|
+
while (braceStart < input.length && /\s/.test(input[braceStart])) {
|
|
144
|
+
braceStart++;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (input[braceStart] !== '{') {
|
|
148
|
+
i = idx + 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Parse balanced braces for block
|
|
153
|
+
pos = braceStart + 1;
|
|
154
|
+
depth = 1;
|
|
155
|
+
|
|
156
|
+
while (pos < input.length && depth > 0) {
|
|
157
|
+
const ch = input[pos];
|
|
158
|
+
|
|
159
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
160
|
+
pos = this.skipString(input, pos, ch);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (ch === '/' && input[pos + 1] === '/') {
|
|
164
|
+
pos = this.skipLineComment(input, pos);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (ch === '/' && input[pos + 1] === '*') {
|
|
168
|
+
pos = this.skipBlockComment(input, pos);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (ch === '{') depth++;
|
|
173
|
+
else if (ch === '}') depth--;
|
|
174
|
+
pos++;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (depth !== 0) {
|
|
178
|
+
i = idx + 1;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const block = input.slice(braceStart + 1, pos - 1);
|
|
183
|
+
|
|
184
|
+
results.push({
|
|
185
|
+
condition,
|
|
186
|
+
block,
|
|
187
|
+
start: idx,
|
|
188
|
+
end: pos
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
i = pos;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return results;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Парсить @if/@elseif/@else цепочки
|
|
199
|
+
*/
|
|
200
|
+
public static parseIfChain(input: string): Array<{
|
|
201
|
+
type: 'if' | 'elseif' | 'else';
|
|
202
|
+
condition?: string;
|
|
203
|
+
block: string;
|
|
204
|
+
start: number;
|
|
205
|
+
end: number;
|
|
206
|
+
}> {
|
|
207
|
+
const results: Array<{
|
|
208
|
+
type: 'if' | 'elseif' | 'else';
|
|
209
|
+
condition?: string;
|
|
210
|
+
block: string;
|
|
211
|
+
start: number;
|
|
212
|
+
end: number;
|
|
213
|
+
}> = [];
|
|
214
|
+
|
|
215
|
+
// Find @if first
|
|
216
|
+
const ifMatches = this.parseBlockRule(input, '@if');
|
|
217
|
+
|
|
218
|
+
for (const ifMatch of ifMatches) {
|
|
219
|
+
results.push({
|
|
220
|
+
type: 'if',
|
|
221
|
+
condition: ifMatch.condition,
|
|
222
|
+
block: ifMatch.block,
|
|
223
|
+
start: ifMatch.start,
|
|
224
|
+
end: ifMatch.end
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Look for @elseif/@else after this @if
|
|
228
|
+
let searchPos = ifMatch.end;
|
|
229
|
+
|
|
230
|
+
while (searchPos < input.length) {
|
|
231
|
+
// Skip whitespace
|
|
232
|
+
while (searchPos < input.length && /\s/.test(input[searchPos])) {
|
|
233
|
+
searchPos++;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const remaining = input.slice(searchPos).toLowerCase();
|
|
237
|
+
|
|
238
|
+
if (remaining.startsWith('@elseif')) {
|
|
239
|
+
const elseifMatches = this.parseBlockRule(input.slice(searchPos), '@elseif');
|
|
240
|
+
if (elseifMatches.length > 0) {
|
|
241
|
+
const m = elseifMatches[0];
|
|
242
|
+
results.push({
|
|
243
|
+
type: 'elseif',
|
|
244
|
+
condition: m.condition,
|
|
245
|
+
block: m.block,
|
|
246
|
+
start: searchPos + m.start,
|
|
247
|
+
end: searchPos + m.end
|
|
248
|
+
});
|
|
249
|
+
searchPos = searchPos + m.end;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
} else if (remaining.startsWith('@else')) {
|
|
253
|
+
// @else without condition
|
|
254
|
+
let pos = searchPos + 5; // length of '@else'
|
|
255
|
+
while (pos < input.length && /\s/.test(input[pos])) {
|
|
256
|
+
pos++;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (input[pos] === '{') {
|
|
260
|
+
let bracePos = pos + 1;
|
|
261
|
+
let depth = 1;
|
|
262
|
+
|
|
263
|
+
while (bracePos < input.length && depth > 0) {
|
|
264
|
+
const ch = input[bracePos];
|
|
265
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
266
|
+
bracePos = this.skipString(input, bracePos, ch);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (ch === '{') depth++;
|
|
270
|
+
else if (ch === '}') depth--;
|
|
271
|
+
bracePos++;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (depth === 0) {
|
|
275
|
+
results.push({
|
|
276
|
+
type: 'else',
|
|
277
|
+
block: input.slice(pos + 1, bracePos - 1),
|
|
278
|
+
start: searchPos,
|
|
279
|
+
end: bracePos
|
|
280
|
+
});
|
|
281
|
+
searchPos = bracePos;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
break; // No more @elseif/@else found
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Skip over a string literal (handles escape sequences)
|
|
296
|
+
*/
|
|
297
|
+
private static skipString(input: string, pos: number, quote: string): number {
|
|
298
|
+
pos++; // skip opening quote
|
|
299
|
+
while (pos < input.length) {
|
|
300
|
+
if (input[pos] === '\\') {
|
|
301
|
+
pos += 2; // skip escape sequence
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (input[pos] === quote) {
|
|
305
|
+
return pos + 1;
|
|
306
|
+
}
|
|
307
|
+
// Handle template literal ${...}
|
|
308
|
+
if (quote === '`' && input[pos] === '$' && input[pos + 1] === '{') {
|
|
309
|
+
pos += 2;
|
|
310
|
+
let depth = 1;
|
|
311
|
+
while (pos < input.length && depth > 0) {
|
|
312
|
+
if (input[pos] === '{') depth++;
|
|
313
|
+
else if (input[pos] === '}') depth--;
|
|
314
|
+
pos++;
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
pos++;
|
|
319
|
+
}
|
|
320
|
+
return pos;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Skip single-line comment
|
|
325
|
+
*/
|
|
326
|
+
private static skipLineComment(input: string, pos: number): number {
|
|
327
|
+
while (pos < input.length && input[pos] !== '\n') {
|
|
328
|
+
pos++;
|
|
329
|
+
}
|
|
330
|
+
return pos + 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Skip block comment
|
|
335
|
+
*/
|
|
336
|
+
private static skipBlockComment(input: string, pos: number): number {
|
|
337
|
+
pos += 2; // skip /*
|
|
338
|
+
while (pos < input.length - 1) {
|
|
339
|
+
if (input[pos] === '*' && input[pos + 1] === '/') {
|
|
340
|
+
return pos + 2;
|
|
341
|
+
}
|
|
342
|
+
pos++;
|
|
343
|
+
}
|
|
344
|
+
return pos;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Извлечь простые @(expression) без блоков
|
|
349
|
+
*/
|
|
350
|
+
public static parseExpressions(input: string): Array<{ content: string; start: number; end: number }> {
|
|
351
|
+
return this.parseBalanced(input, '@(', ')');
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EscapeHandler - обработка escape-последовательностей.
|
|
3
|
+
* @@ -> @
|
|
4
|
+
* @@@@ -> @@
|
|
5
|
+
*/
|
|
6
|
+
export default class EscapeHandler {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Заменить @@ на специальный placeholder перед парсингом
|
|
10
|
+
*/
|
|
11
|
+
public static escapeDoubleAt(input: string): { result: string; placeholder: string } {
|
|
12
|
+
// Use a unique placeholder that won't appear in normal code
|
|
13
|
+
const placeholder = '\x00AT_ESCAPE\x00';
|
|
14
|
+
const result = input.replace(/@@/g, placeholder);
|
|
15
|
+
return { result, placeholder };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Восстановить @ из placeholder после парсинга
|
|
20
|
+
*/
|
|
21
|
+
public static restoreEscapes(input: string, placeholder: string): string {
|
|
22
|
+
return input.replace(new RegExp(placeholder.replace(/\x00/g, '\\x00'), 'g'), '@');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Полный цикл: escape -> process -> restore
|
|
27
|
+
*/
|
|
28
|
+
public static process(
|
|
29
|
+
input: string,
|
|
30
|
+
processor: (escaped: string) => string
|
|
31
|
+
): string {
|
|
32
|
+
const { result: escaped, placeholder } = this.escapeDoubleAt(input);
|
|
33
|
+
const processed = processor(escaped);
|
|
34
|
+
return this.restoreEscapes(processed, placeholder);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Проверить, является ли позиция escaped (предшествует @@)
|
|
39
|
+
*/
|
|
40
|
+
public static isEscaped(input: string, position: number): boolean {
|
|
41
|
+
if (position === 0) return false;
|
|
42
|
+
|
|
43
|
+
// Count consecutive @ before this position
|
|
44
|
+
let count = 0;
|
|
45
|
+
let i = position - 1;
|
|
46
|
+
while (i >= 0 && input[i] === '@') {
|
|
47
|
+
count++;
|
|
48
|
+
i--;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// If odd number of @ before, this @ is escaped
|
|
52
|
+
return count % 2 === 1;
|
|
53
|
+
}
|
|
54
|
+
}
|