@reidelsaltres/pureper 0.1.154 → 0.1.156
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/HMLELoader.d.ts +9 -0
- package/out/foundation/HMLELoader.d.ts.map +1 -0
- package/out/foundation/HMLELoader.js +14 -0
- package/out/foundation/HMLELoader.js.map +1 -0
- package/out/foundation/Triplet.d.ts.map +1 -1
- package/out/foundation/Triplet.js +3 -3
- package/out/foundation/Triplet.js.map +1 -1
- package/out/foundation/TripletDecorator.d.ts.map +1 -1
- package/out/foundation/TripletDecorator.js +4 -0
- package/out/foundation/TripletDecorator.js.map +1 -1
- package/out/foundation/component_api/Attribute.d.ts +2 -0
- package/out/foundation/component_api/Attribute.d.ts.map +1 -1
- package/out/foundation/component_api/Attribute.js +6 -0
- package/out/foundation/component_api/Attribute.js.map +1 -1
- package/out/index.d.ts +1 -2
- package/out/index.d.ts.map +1 -1
- package/out/index.js +1 -2
- package/out/index.js.map +1 -1
- package/package.json +1 -1
- package/src/foundation/HMLELoader.ts +18 -0
- package/src/foundation/Triplet.ts +3 -5
- package/src/foundation/TripletDecorator.ts +8 -0
- package/src/foundation/component_api/Attribute.ts +7 -0
- package/src/index.ts +1 -4
- package/src/components/DynamicBlock.ts +0 -25
- package/src/foundation/HMLEParser.md +0 -117
- package/src/foundation/HMLEParserReborn.ts +0 -990
- package/src/foundation/PHTMLParser.ts +0 -231
- package/src/foundation/dynamic/Rule.ts +0 -24
|
@@ -1,990 +0,0 @@
|
|
|
1
|
-
import Observable from './api/Observer.js';
|
|
2
|
-
import Context from './hmle/Context.js';
|
|
3
|
-
|
|
4
|
-
// Helper: encode expression for HTML attribute
|
|
5
|
-
function encodeAttr(s: string): string {
|
|
6
|
-
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Helper: decode expression from HTML attribute
|
|
10
|
-
function decodeAttr(s: string): string {
|
|
11
|
-
return s.replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/&/g, '&');
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Helper: find all Observable variable names used in an expression
|
|
15
|
-
// Also considers dynamicVars as "Observable-like" for template generation
|
|
16
|
-
function findObservablesInExpr(expr: string, scope: Record<string, any>, dynamicVars?: Set<string>): string[] {
|
|
17
|
-
const result: string[] = [];
|
|
18
|
-
// Extract all identifier-like tokens from expression
|
|
19
|
-
const identifiers = expr.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
|
|
20
|
-
for (const id of identifiers) {
|
|
21
|
-
// Check if it's an actual Observable in scope
|
|
22
|
-
if (scope[id] instanceof Observable && !result.includes(id)) {
|
|
23
|
-
result.push(id);
|
|
24
|
-
}
|
|
25
|
-
// Check if it's a dynamic variable (from @for loop)
|
|
26
|
-
else if (dynamicVars?.has(id) && !result.includes(id)) {
|
|
27
|
-
result.push(id);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return result;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Helper: Find the index of the matching closing brace '}' ignoring braces inside quotes/comments
|
|
34
|
-
function findMatchingClosingBrace(content: string, openIndex: number): number {
|
|
35
|
-
let i = openIndex + 1;
|
|
36
|
-
let depth = 1;
|
|
37
|
-
let inSingle = false;
|
|
38
|
-
let inDouble = false;
|
|
39
|
-
let inBacktick = false;
|
|
40
|
-
let inLineComment = false;
|
|
41
|
-
let inBlockComment = false;
|
|
42
|
-
let prevChar = '';
|
|
43
|
-
|
|
44
|
-
while (i < content.length && depth > 0) {
|
|
45
|
-
const ch = content[i];
|
|
46
|
-
|
|
47
|
-
// handle comment states
|
|
48
|
-
if (inLineComment) {
|
|
49
|
-
if (ch === '\n') inLineComment = false;
|
|
50
|
-
prevChar = ch;
|
|
51
|
-
i++;
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (inBlockComment) {
|
|
55
|
-
if (prevChar === '*' && ch === '/') inBlockComment = false;
|
|
56
|
-
prevChar = ch;
|
|
57
|
-
i++;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// handle string/template states, allow escaping
|
|
62
|
-
if (inSingle) {
|
|
63
|
-
if (ch === '\\' && prevChar !== '\\') { prevChar = ch; i++; continue; }
|
|
64
|
-
if (ch === "'" && prevChar !== '\\') inSingle = false;
|
|
65
|
-
prevChar = ch;
|
|
66
|
-
i++; continue;
|
|
67
|
-
}
|
|
68
|
-
if (inDouble) {
|
|
69
|
-
if (ch === '\\' && prevChar !== '\\') { prevChar = ch; i++; continue; }
|
|
70
|
-
if (ch === '"' && prevChar !== '\\') inDouble = false;
|
|
71
|
-
prevChar = ch;
|
|
72
|
-
i++; continue;
|
|
73
|
-
}
|
|
74
|
-
if (inBacktick) {
|
|
75
|
-
if (ch === '\\' && prevChar !== '\\') { prevChar = ch; i++; continue; }
|
|
76
|
-
if (ch === '`' && prevChar !== '\\') inBacktick = false;
|
|
77
|
-
prevChar = ch;
|
|
78
|
-
i++; continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Not inside quotes or comments
|
|
82
|
-
// Start comments
|
|
83
|
-
if (prevChar === '/' && ch === '/') { inLineComment = true; prevChar = ''; i++; continue; }
|
|
84
|
-
if (prevChar === '/' && ch === '*') { inBlockComment = true; prevChar = ''; i++; continue; }
|
|
85
|
-
|
|
86
|
-
// Start quotes
|
|
87
|
-
if (ch === "'") { inSingle = true; prevChar = ch; i++; continue; }
|
|
88
|
-
if (ch === '"') { inDouble = true; prevChar = ch; i++; continue; }
|
|
89
|
-
if (ch === '`') { inBacktick = true; prevChar = ch; i++; continue; }
|
|
90
|
-
|
|
91
|
-
// handle braces
|
|
92
|
-
if (ch === '{') depth++;
|
|
93
|
-
else if (ch === '}') depth--;
|
|
94
|
-
|
|
95
|
-
prevChar = ch;
|
|
96
|
-
i++;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// i is index just after the closing brace (since we increment after reading '}' )
|
|
100
|
-
return i;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Rule — represents a single parsing rule with its own check and execution logic.
|
|
105
|
-
* Rules can apply to any of the 3 parsing stages or to specific ones.
|
|
106
|
-
*/
|
|
107
|
-
export interface Rule {
|
|
108
|
-
name: string;
|
|
109
|
-
/** Stage 1: text parsing — returns transformed string or null if rule doesn't apply */
|
|
110
|
-
parseText?: (parser: HMLEParserReborn, content: string, scope?: Record<string, any>, dynamicVars?: Set<string>) => string | null;
|
|
111
|
-
/** Stage 3: hydration — processes template elements in DOM */
|
|
112
|
-
hydrate?: (parser: HMLEParserReborn, template: HTMLTemplateElement, scope?: Record<string, any>) => void;
|
|
113
|
-
/** Optional: element-level hydration (stage 2/DOM parse) */
|
|
114
|
-
elementHydrate?: (parser: HMLEParserReborn, el: Element, scope?: Record<string, any>) => void;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export default class HMLEParserReborn {
|
|
118
|
-
private rules: Rule[] = [];
|
|
119
|
-
public variables: Record<string, any> = {};
|
|
120
|
-
|
|
121
|
-
constructor() {
|
|
122
|
-
// Register default rules — order matters!
|
|
123
|
-
// forRule must come before expRule so that @for creates local scope first
|
|
124
|
-
this.rules.push(forRule);
|
|
125
|
-
// element attribute rules
|
|
126
|
-
this.rules.push(refRule);
|
|
127
|
-
this.rules.push(onRule);
|
|
128
|
-
this.rules.push(expRule);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Add a custom rule to the parser
|
|
133
|
-
*/
|
|
134
|
-
public addRule(rule: Rule): this {
|
|
135
|
-
this.rules.push(rule);
|
|
136
|
-
return this;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Get registered rules
|
|
141
|
-
*/
|
|
142
|
-
public getRules(): Rule[] {
|
|
143
|
-
return this.rules;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
public static isIdentifier(s: string): boolean {
|
|
147
|
-
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test((s || '').trim());
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private buildContext(scope?: Record<string, any>): Record<string, any> {
|
|
151
|
-
return Context.build(this.variables, scope);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
public evaluate(expr: string, scope?: Record<string, any>): any {
|
|
155
|
-
const ctx = this.buildContext(scope);
|
|
156
|
-
try {
|
|
157
|
-
const fn = new Function('with(this){ return (' + expr + '); }');
|
|
158
|
-
return fn.call(ctx);
|
|
159
|
-
} catch (e) {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
public stringify(v: any): string {
|
|
165
|
-
if (v == null) return '';
|
|
166
|
-
if (typeof v === 'string') return v;
|
|
167
|
-
return String(v);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Stage 1: Parsing — parse HMLE as text, execute STATIC RULES.
|
|
172
|
-
* Observable values are left as <template ...> placeholders for Stage 3.
|
|
173
|
-
*/
|
|
174
|
-
public parse(content: string, scope?: Record<string, any>): string {
|
|
175
|
-
let working = content || '';
|
|
176
|
-
|
|
177
|
-
// Build dynamic var list from ref attributes in raw content so expressions referencing
|
|
178
|
-
// DOM-created variables (like @[ref]="name") are not evaluated at parse time.
|
|
179
|
-
const dynamicVars = new Set<string>();
|
|
180
|
-
// Match patterns like @[ref]="name" or @[ref]='name'
|
|
181
|
-
const refAttrRe = /@\[\s*ref\s*\]\s*=\s*"([A-Za-z_$][A-Za-z0-9_$]*)"|@\[\s*ref\s*\]\s*=\s*'([A-Za-z_$][A-Za-z0-9_$]*)'/g;
|
|
182
|
-
let rm: RegExpExecArray | null;
|
|
183
|
-
while ((rm = refAttrRe.exec(working)) !== null) {
|
|
184
|
-
const name = rm[1] || rm[2];
|
|
185
|
-
if (name) dynamicVars.add(name);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Apply parseText rules in order, passing dynamicVars so rules like expRule can
|
|
189
|
-
// treat references to these variables as dynamic
|
|
190
|
-
for (const rule of this.rules) {
|
|
191
|
-
if (rule.parseText) {
|
|
192
|
-
const result = rule.parseText(this, working, scope, dynamicVars);
|
|
193
|
-
if (result !== null) {
|
|
194
|
-
working = result;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return working;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Stage 2: DOM Parsing — parse HMLE text to DOM, create <template> for dynamic rules.
|
|
204
|
-
* Created templates are preserved for reuse when Observable updates.
|
|
205
|
-
*/
|
|
206
|
-
public parseToDOM(content: string, scope?: Record<string, any>): DocumentFragment {
|
|
207
|
-
const html = this.parse(content, scope);
|
|
208
|
-
const template = document.createElement('template');
|
|
209
|
-
template.innerHTML = html.trim();
|
|
210
|
-
return template.content;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Stage 3: Hydration — remove templates and execute dynamic rules.
|
|
215
|
-
*/
|
|
216
|
-
public hydrate(fragment: DocumentFragment | Element, scope?: Record<string, any>): void {
|
|
217
|
-
let root: any;
|
|
218
|
-
if (typeof DocumentFragment !== 'undefined' && fragment instanceof DocumentFragment) root = fragment;
|
|
219
|
-
else if ((fragment as any).nodeType === 11) root = fragment;
|
|
220
|
-
else root = fragment;
|
|
221
|
-
|
|
222
|
-
// Stage 2-ish: Process element-level rules (like @[ref] and @[on-event]) before templates are hydrated
|
|
223
|
-
const allElements = Array.from(root.querySelectorAll('*')) as Element[];
|
|
224
|
-
for (const rule of this.rules) {
|
|
225
|
-
if (rule.elementHydrate) {
|
|
226
|
-
for (const el of allElements) {
|
|
227
|
-
rule.elementHydrate(this, el, scope);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Process each rule's template hydrate method (stage 3)
|
|
233
|
-
for (const rule of this.rules) {
|
|
234
|
-
if (rule.hydrate) {
|
|
235
|
-
const selector = `template[${rule.name}]`;
|
|
236
|
-
const templates = Array.from(root.querySelectorAll(selector)) as HTMLTemplateElement[];
|
|
237
|
-
for (const t of templates) {
|
|
238
|
-
rule.hydrate(this, t, scope);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Process {{EXP:...}} placeholders in attributes
|
|
244
|
-
this.hydrateAttributeExpressions(root, scope);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Process {{EXP:expr}} placeholders in element attributes
|
|
249
|
-
*/
|
|
250
|
-
private hydrateAttributeExpressions(root: Element | DocumentFragment, scope?: Record<string, any>): void {
|
|
251
|
-
const expPattern = /\{\{EXP:([^}]+)\}\}/g;
|
|
252
|
-
|
|
253
|
-
// Get all elements
|
|
254
|
-
const elements = root.querySelectorAll('*');
|
|
255
|
-
const allElements = [root, ...Array.from(elements)] as Element[];
|
|
256
|
-
|
|
257
|
-
for (const el of allElements) {
|
|
258
|
-
if (!el.attributes) continue;
|
|
259
|
-
|
|
260
|
-
for (const attr of Array.from(el.attributes)) {
|
|
261
|
-
if (!expPattern.test(attr.value)) continue;
|
|
262
|
-
|
|
263
|
-
// Reset regex
|
|
264
|
-
expPattern.lastIndex = 0;
|
|
265
|
-
|
|
266
|
-
const originalValue = attr.value;
|
|
267
|
-
const observablesInAttr: { expr: string, obs: Observable<any>[] }[] = [];
|
|
268
|
-
|
|
269
|
-
// Find all expressions in this attribute
|
|
270
|
-
let match;
|
|
271
|
-
while ((match = expPattern.exec(originalValue)) !== null) {
|
|
272
|
-
const expr = decodeAttr(match[1]);
|
|
273
|
-
const obsNames = scope ? findObservablesInExpr(expr, scope) : [];
|
|
274
|
-
const obs = obsNames.map(name => (scope as any)[name]).filter(o => o instanceof Observable);
|
|
275
|
-
observablesInAttr.push({ expr, obs });
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Evaluate and replace
|
|
279
|
-
const evalAttr = () => {
|
|
280
|
-
let result = originalValue;
|
|
281
|
-
expPattern.lastIndex = 0;
|
|
282
|
-
result = result.replace(expPattern, (_, encodedExpr) => {
|
|
283
|
-
const expr = decodeAttr(encodedExpr);
|
|
284
|
-
// Build scope with Observable values
|
|
285
|
-
const evalScope = Object.assign({}, scope);
|
|
286
|
-
if (scope) {
|
|
287
|
-
for (const name of findObservablesInExpr(expr, scope)) {
|
|
288
|
-
const obs = (scope as any)[name];
|
|
289
|
-
if (obs instanceof Observable) {
|
|
290
|
-
evalScope[name] = obs.getObject ? obs.getObject() : undefined;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const val = this.evaluate(expr, evalScope);
|
|
295
|
-
return this.stringify(val);
|
|
296
|
-
});
|
|
297
|
-
return result;
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// Initial evaluation
|
|
301
|
-
attr.value = evalAttr();
|
|
302
|
-
|
|
303
|
-
// Subscribe to all Observables for updates
|
|
304
|
-
for (const { obs } of observablesInAttr) {
|
|
305
|
-
for (const o of obs) {
|
|
306
|
-
o.subscribe(() => {
|
|
307
|
-
attr.value = evalAttr();
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Helper: Parse content with specific variables treated as "dynamic" (Observable-like).
|
|
318
|
-
* This is used inside Observable @for loops where index and value variables
|
|
319
|
-
* should be treated as dynamic even though they're not in scope yet.
|
|
320
|
-
*/
|
|
321
|
-
function parseWithDynamicVars(parser: HMLEParserReborn, content: string, scope?: Record<string, any>, dynamicVars?: Set<string>): string {
|
|
322
|
-
let working = content;
|
|
323
|
-
|
|
324
|
-
// Apply parseText rules in order with dynamicVars
|
|
325
|
-
for (const rule of parser.getRules()) {
|
|
326
|
-
if (rule.parseText) {
|
|
327
|
-
const result = rule.parseText(parser, working, scope, dynamicVars);
|
|
328
|
-
if (result !== null) {
|
|
329
|
-
working = result;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return working;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// ==========================================
|
|
338
|
-
// Rule: exp — @(expression or variable)
|
|
339
|
-
// ==========================================
|
|
340
|
-
const expRule: Rule = {
|
|
341
|
-
name: 'exp',
|
|
342
|
-
|
|
343
|
-
parseText(parser, content, scope, dynamicVars) {
|
|
344
|
-
let working = content;
|
|
345
|
-
let out = '';
|
|
346
|
-
let pos = 0;
|
|
347
|
-
|
|
348
|
-
// Track if we're inside a <template for> tag
|
|
349
|
-
let templateDepth = 0;
|
|
350
|
-
|
|
351
|
-
while (pos < working.length) {
|
|
352
|
-
// Check for <template for opening
|
|
353
|
-
const templateForStart = working.indexOf('<template for', pos);
|
|
354
|
-
// Check for </template> closing
|
|
355
|
-
const templateEnd = working.indexOf('</template>', pos);
|
|
356
|
-
// Check for @(
|
|
357
|
-
const atIdx = working.indexOf('@(', pos);
|
|
358
|
-
|
|
359
|
-
// Find the earliest marker
|
|
360
|
-
const markers = [
|
|
361
|
-
{ type: 'start', idx: templateForStart },
|
|
362
|
-
{ type: 'end', idx: templateEnd },
|
|
363
|
-
{ type: 'expr', idx: atIdx }
|
|
364
|
-
].filter(m => m.idx !== -1).sort((a, b) => a.idx - b.idx);
|
|
365
|
-
|
|
366
|
-
if (markers.length === 0) {
|
|
367
|
-
// No more markers, append rest and break
|
|
368
|
-
out += working.slice(pos);
|
|
369
|
-
break;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const first = markers[0];
|
|
373
|
-
|
|
374
|
-
if (first.type === 'start') {
|
|
375
|
-
// Found <template for — copy up to it and increase depth
|
|
376
|
-
const endOfTag = working.indexOf('>', first.idx);
|
|
377
|
-
if (endOfTag === -1) {
|
|
378
|
-
out += working.slice(pos);
|
|
379
|
-
break;
|
|
380
|
-
}
|
|
381
|
-
out += working.slice(pos, endOfTag + 1);
|
|
382
|
-
pos = endOfTag + 1;
|
|
383
|
-
templateDepth++;
|
|
384
|
-
continue;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (first.type === 'end') {
|
|
388
|
-
// Found </template> — copy up to it (including) and decrease depth
|
|
389
|
-
const closeEnd = first.idx + '</template>'.length;
|
|
390
|
-
out += working.slice(pos, closeEnd);
|
|
391
|
-
pos = closeEnd;
|
|
392
|
-
if (templateDepth > 0) templateDepth--;
|
|
393
|
-
continue;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (first.type === 'expr') {
|
|
397
|
-
// Found @( — if inside template, skip it
|
|
398
|
-
if (templateDepth > 0) {
|
|
399
|
-
// Copy including @( and move on
|
|
400
|
-
out += working.slice(pos, first.idx + 2);
|
|
401
|
-
pos = first.idx + 2;
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Not inside template — process the expression
|
|
406
|
-
out += working.slice(pos, first.idx);
|
|
407
|
-
|
|
408
|
-
// Find balanced parentheses
|
|
409
|
-
let j = first.idx + 2;
|
|
410
|
-
let depth = 1;
|
|
411
|
-
while (j < working.length && depth > 0) {
|
|
412
|
-
if (working[j] === '(') depth++;
|
|
413
|
-
else if (working[j] === ')') depth--;
|
|
414
|
-
j++;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (depth !== 0) {
|
|
418
|
-
out += working.slice(first.idx, first.idx + 2);
|
|
419
|
-
pos = first.idx + 2;
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const innerExpr = working.slice(first.idx + 2, j - 1).trim();
|
|
424
|
-
|
|
425
|
-
// Check if expression references any Observable in scope or dynamic vars
|
|
426
|
-
const observablesUsed = scope ? findObservablesInExpr(innerExpr, scope, dynamicVars) :
|
|
427
|
-
(dynamicVars ? findObservablesInExpr(innerExpr, {}, dynamicVars) : []);
|
|
428
|
-
|
|
429
|
-
if (observablesUsed.length > 0) {
|
|
430
|
-
// Expression uses Observable or dynamic var — make it dynamic
|
|
431
|
-
// Check if we're inside an HTML attribute (look back for ="
|
|
432
|
-
const beforeMatch = out.slice(-50);
|
|
433
|
-
const inAttribute = /=["'][^"']*$/.test(beforeMatch);
|
|
434
|
-
|
|
435
|
-
if (inAttribute) {
|
|
436
|
-
out += `{{EXP:${encodeAttr(innerExpr)}}}`;
|
|
437
|
-
} else {
|
|
438
|
-
// For dynamic vars from @for, include the var name
|
|
439
|
-
const dynamicVarsUsed = dynamicVars ? observablesUsed.filter(v => dynamicVars.has(v)) : [];
|
|
440
|
-
|
|
441
|
-
if (dynamicVarsUsed.length > 0) {
|
|
442
|
-
// Expression uses dynamic loop variables
|
|
443
|
-
out += `<template exp var="${dynamicVarsUsed.join(',')}" expr="${encodeAttr(innerExpr)}"></template>`;
|
|
444
|
-
} else if (HMLEParserReborn.isIdentifier(innerExpr)) {
|
|
445
|
-
out += `<template exp var="${innerExpr}"></template>`;
|
|
446
|
-
} else {
|
|
447
|
-
out += `<template exp expr="${encodeAttr(innerExpr)}"></template>`;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
pos = j;
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Evaluate expression
|
|
455
|
-
const res = parser.evaluate(innerExpr, scope);
|
|
456
|
-
|
|
457
|
-
if (res instanceof Observable) {
|
|
458
|
-
out += `<template exp></template>`;
|
|
459
|
-
} else if (typeof res === 'undefined') {
|
|
460
|
-
// void: nothing displayed
|
|
461
|
-
} else {
|
|
462
|
-
out += parser.stringify(res);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
pos = j;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
return out;
|
|
470
|
-
},
|
|
471
|
-
|
|
472
|
-
hydrate(parser, template, scope) {
|
|
473
|
-
const varAttr = template.getAttribute('var') ?? null;
|
|
474
|
-
const exprAttr = template.getAttribute('expr');
|
|
475
|
-
const expr = exprAttr ? decodeAttr(exprAttr) : null;
|
|
476
|
-
|
|
477
|
-
// Case 1: var="varName" without expr — simple variable reference @(obs)
|
|
478
|
-
if (varAttr && !expr && scope) {
|
|
479
|
-
// Single variable that should be Observable in scope
|
|
480
|
-
if ((scope as any)[varAttr] instanceof Observable) {
|
|
481
|
-
const obs = (scope as any)[varAttr] as Observable<any>;
|
|
482
|
-
const value = obs.getObject ? obs.getObject() : undefined;
|
|
483
|
-
const textNode = document.createTextNode(parser.stringify(value));
|
|
484
|
-
|
|
485
|
-
template.parentNode?.replaceChild(textNode, template);
|
|
486
|
-
|
|
487
|
-
// Subscribe for updates
|
|
488
|
-
obs.subscribe((v: any) => {
|
|
489
|
-
textNode.textContent = parser.stringify(v);
|
|
490
|
-
});
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Case 2: var="i,v" with expr="..." — expression using dynamic variables
|
|
496
|
-
// The var attribute contains comma-separated list of dynamic vars that should be Observable in scope
|
|
497
|
-
if (varAttr && expr && scope) {
|
|
498
|
-
const dynamicVarNames = varAttr.split(',').map(s => s.trim());
|
|
499
|
-
|
|
500
|
-
// Collect all Observable variables used in the expression
|
|
501
|
-
const observablesUsed: { name: string, obs: Observable<any> }[] = [];
|
|
502
|
-
|
|
503
|
-
// Add dynamic vars from var attribute
|
|
504
|
-
for (const name of dynamicVarNames) {
|
|
505
|
-
if ((scope as any)[name] instanceof Observable) {
|
|
506
|
-
observablesUsed.push({ name, obs: (scope as any)[name] });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// Also find any other Observable variables used in expression
|
|
511
|
-
const identifiers = expr.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
|
|
512
|
-
for (const id of identifiers) {
|
|
513
|
-
if ((scope as any)[id] instanceof Observable && !observablesUsed.some(o => o.name === id)) {
|
|
514
|
-
observablesUsed.push({ name: id, obs: (scope as any)[id] });
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Evaluate with current Observable values
|
|
519
|
-
const evalWithScope = () => {
|
|
520
|
-
const evalScope = Object.assign({}, scope);
|
|
521
|
-
for (const { name, obs } of observablesUsed) {
|
|
522
|
-
evalScope[name] = obs.getObject ? obs.getObject() : undefined;
|
|
523
|
-
}
|
|
524
|
-
return parser.evaluate(expr, evalScope);
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const value = evalWithScope();
|
|
528
|
-
const textNode = document.createTextNode(parser.stringify(value));
|
|
529
|
-
template.parentNode?.replaceChild(textNode, template);
|
|
530
|
-
|
|
531
|
-
// Subscribe to all Observables
|
|
532
|
-
for (const { obs } of observablesUsed) {
|
|
533
|
-
obs.subscribe(() => {
|
|
534
|
-
textNode.textContent = parser.stringify(evalWithScope());
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// Case 3: Expression without var attribute that uses Observables @(action(greeting))
|
|
541
|
-
if (expr && scope) {
|
|
542
|
-
const observables = findObservablesInExpr(expr, scope);
|
|
543
|
-
|
|
544
|
-
// Evaluate with current Observable values
|
|
545
|
-
const evalWithScope = () => {
|
|
546
|
-
const evalScope = Object.assign({}, scope);
|
|
547
|
-
// Get current values from Observables
|
|
548
|
-
for (const name of observables) {
|
|
549
|
-
const obs = (scope as any)[name];
|
|
550
|
-
if (obs instanceof Observable) {
|
|
551
|
-
evalScope[name] = obs.getObject ? obs.getObject() : undefined;
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
return parser.evaluate(expr, evalScope);
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
const value = evalWithScope();
|
|
558
|
-
const textNode = document.createTextNode(parser.stringify(value));
|
|
559
|
-
template.parentNode?.replaceChild(textNode, template);
|
|
560
|
-
|
|
561
|
-
// Subscribe to all Observables used in expression
|
|
562
|
-
for (const name of observables) {
|
|
563
|
-
const obs = (scope as any)[name];
|
|
564
|
-
if (obs instanceof Observable) {
|
|
565
|
-
obs.subscribe(() => {
|
|
566
|
-
textNode.textContent = parser.stringify(evalWithScope());
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Fallback: replace with comment
|
|
574
|
-
const comment = document.createComment('exp');
|
|
575
|
-
template.parentNode?.replaceChild(comment, template);
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
|
|
579
|
-
// ==========================================
|
|
580
|
-
// Rule: for — @for (index, value in values) { ... }
|
|
581
|
-
// ==========================================
|
|
582
|
-
const forRule: Rule = {
|
|
583
|
-
name: 'for',
|
|
584
|
-
|
|
585
|
-
parseText(parser, content, scope, dynamicVars?: Set<string>) {
|
|
586
|
-
let working = content;
|
|
587
|
-
const forRe = /@for\s*\(\s*([A-Za-z_$][A-Za-z0-9_$]*)(?:\s*,\s*([A-Za-z_$][A-Za-z0-9_$]*))?\s+in\s+([^\)\s]+)\s*\)\s*\{/g;
|
|
588
|
-
|
|
589
|
-
let out = '';
|
|
590
|
-
let lastIndex = 0;
|
|
591
|
-
forRe.lastIndex = 0;
|
|
592
|
-
let m: RegExpExecArray | null;
|
|
593
|
-
|
|
594
|
-
while ((m = forRe.exec(working)) !== null) {
|
|
595
|
-
out += working.slice(lastIndex, m.index);
|
|
596
|
-
|
|
597
|
-
const a = m[1];
|
|
598
|
-
const b = m[2];
|
|
599
|
-
const iterable = m[3];
|
|
600
|
-
|
|
601
|
-
const blockStart = m.index + m[0].length - 1; // position of '{'
|
|
602
|
-
|
|
603
|
-
// Extract balanced brace block using robust finder that ignores braces inside strings/comments
|
|
604
|
-
const i = findMatchingClosingBrace(working, blockStart);
|
|
605
|
-
|
|
606
|
-
if (i > working.length || i <= blockStart) {
|
|
607
|
-
// Unable to find matching closing brace — fallback to leaving original match as-is
|
|
608
|
-
out += working.slice(m.index, forRe.lastIndex);
|
|
609
|
-
lastIndex = forRe.lastIndex;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const inner = working.slice(blockStart + 1, i - 1);
|
|
614
|
-
|
|
615
|
-
// Determine if this @for is dynamic (iterates over Observable or uses dynamic vars)
|
|
616
|
-
const indexName = b ? a : 'index';
|
|
617
|
-
const varName = b ? b : a;
|
|
618
|
-
|
|
619
|
-
let isObservable = false;
|
|
620
|
-
let values: any[] = [];
|
|
621
|
-
|
|
622
|
-
if (/^\d+$/.test(iterable)) {
|
|
623
|
-
// Numeric literal: @for (i in 5) { } — static
|
|
624
|
-
const n = parseInt(iterable, 10);
|
|
625
|
-
values = Array.from({ length: Math.max(0, n) }, (_, k) => k);
|
|
626
|
-
} else {
|
|
627
|
-
// Variable or dotted path
|
|
628
|
-
const rootName = iterable.split('.')[0];
|
|
629
|
-
|
|
630
|
-
// Check if iterable references Observable or dynamic variable
|
|
631
|
-
const val = scope ? (scope as any)[rootName] : undefined;
|
|
632
|
-
const isDynamicIterable = dynamicVars?.has(rootName);
|
|
633
|
-
|
|
634
|
-
if (val instanceof Observable || isDynamicIterable) {
|
|
635
|
-
isObservable = true;
|
|
636
|
-
} else {
|
|
637
|
-
// Evaluate expression for non-Observable
|
|
638
|
-
const resolved = parser.evaluate(iterable, scope);
|
|
639
|
-
if (Array.isArray(resolved)) values = resolved;
|
|
640
|
-
else if (typeof resolved === 'number' && isFinite(resolved)) {
|
|
641
|
-
values = Array.from({ length: Math.max(0, Math.floor(resolved)) }, (_, k) => k);
|
|
642
|
-
} else {
|
|
643
|
-
values = [];
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
if (isObservable) {
|
|
649
|
-
// DYNAMIC @for — create template placeholder
|
|
650
|
-
// Parse inner content with index and value marked as dynamic variables
|
|
651
|
-
const innerDynamicVars = new Set(dynamicVars ?? []);
|
|
652
|
-
innerDynamicVars.add(indexName);
|
|
653
|
-
innerDynamicVars.add(varName);
|
|
654
|
-
|
|
655
|
-
// Parse inner content — @() using dynamic vars will create <template exp>
|
|
656
|
-
const parsedInner = parseWithDynamicVars(parser, inner, scope, innerDynamicVars);
|
|
657
|
-
|
|
658
|
-
out += `<template for index="${indexName}" var="${varName}" in="${iterable}">${parsedInner}</template>`;
|
|
659
|
-
lastIndex = i;
|
|
660
|
-
forRe.lastIndex = i;
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// STATIC expansion for non-Observable values
|
|
665
|
-
for (let idx = 0; idx < values.length; idx++) {
|
|
666
|
-
const item = values[idx];
|
|
667
|
-
const localScope = Object.assign({}, scope ?? {});
|
|
668
|
-
if (b) {
|
|
669
|
-
localScope[a] = idx;
|
|
670
|
-
localScope[b] = item;
|
|
671
|
-
} else {
|
|
672
|
-
localScope[a] = item;
|
|
673
|
-
}
|
|
674
|
-
out += parser.parse(inner, localScope);
|
|
675
|
-
}
|
|
676
|
-
lastIndex = i;
|
|
677
|
-
forRe.lastIndex = i;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
out += working.slice(lastIndex);
|
|
681
|
-
return out;
|
|
682
|
-
},
|
|
683
|
-
|
|
684
|
-
hydrate(parser, template, scope) {
|
|
685
|
-
const inExpr = template.getAttribute('in') ?? '';
|
|
686
|
-
const varName = template.getAttribute('var') ?? 'item';
|
|
687
|
-
const indexName = template.getAttribute('index') ?? 'i';
|
|
688
|
-
|
|
689
|
-
// Get the root variable name and any nested path
|
|
690
|
-
const rootName = inExpr.split('.')[0];
|
|
691
|
-
const isNestedPath = inExpr.includes('.');
|
|
692
|
-
const pathAfterRoot = isNestedPath ? inExpr.slice(rootName.length + 1) : '';
|
|
693
|
-
|
|
694
|
-
const parent = template.parentNode;
|
|
695
|
-
if (!parent) return;
|
|
696
|
-
|
|
697
|
-
// Save insertion point (element after template, or null if at end)
|
|
698
|
-
const insertionPoint = template.nextSibling;
|
|
699
|
-
const innerContent = template.innerHTML;
|
|
700
|
-
|
|
701
|
-
// Track rendered nodes for cleanup
|
|
702
|
-
let rendered: { nodes: Node[], observables: { idx: Observable<number>, val: Observable<any> } }[] = [];
|
|
703
|
-
|
|
704
|
-
// Helper: get array value from expression, unwrapping Observables
|
|
705
|
-
const resolveArray = (): any[] => {
|
|
706
|
-
if (isNestedPath) {
|
|
707
|
-
// For nested paths like category.items, unwrap Observables in scope
|
|
708
|
-
const unwrappedScope = Object.assign({}, scope);
|
|
709
|
-
for (const key in unwrappedScope) {
|
|
710
|
-
if (unwrappedScope[key] instanceof Observable) {
|
|
711
|
-
unwrappedScope[key] = unwrappedScope[key].getObject();
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const resolved = parser.evaluate(inExpr, unwrappedScope);
|
|
715
|
-
return Array.isArray(resolved) ? resolved : [];
|
|
716
|
-
} else {
|
|
717
|
-
const val = scope ? (scope as any)[rootName] : undefined;
|
|
718
|
-
if (val instanceof Observable) {
|
|
719
|
-
const v = val.getObject();
|
|
720
|
-
return Array.isArray(v) ? v : [];
|
|
721
|
-
} else if (Array.isArray(val)) {
|
|
722
|
-
return val;
|
|
723
|
-
} else if (typeof val === 'number') {
|
|
724
|
-
return Array.from({ length: Math.max(0, Math.floor(val)) }, (_, k) => k);
|
|
725
|
-
}
|
|
726
|
-
return [];
|
|
727
|
-
}
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
// Check if this loop depends on any Observable
|
|
731
|
-
const rootVal = scope ? (scope as any)[rootName] : undefined;
|
|
732
|
-
const isObservableLoop = rootVal instanceof Observable;
|
|
733
|
-
|
|
734
|
-
// Render a single item and return its nodes
|
|
735
|
-
const renderItem = (idx: number, item: any, insertBefore: Node | null): { nodes: Node[], observables: { idx: Observable<number>, val: Observable<any> } } => {
|
|
736
|
-
const localScope = Object.assign({}, scope ?? {});
|
|
737
|
-
|
|
738
|
-
// Create Observables for index and value (so inner templates can subscribe)
|
|
739
|
-
const idxObs = new Observable(idx);
|
|
740
|
-
const valObs = new Observable(item);
|
|
741
|
-
|
|
742
|
-
// Always put Observables in scope for Observable loops
|
|
743
|
-
if (isObservableLoop) {
|
|
744
|
-
localScope[indexName] = idxObs;
|
|
745
|
-
localScope[varName] = valObs;
|
|
746
|
-
} else {
|
|
747
|
-
localScope[indexName] = idx;
|
|
748
|
-
localScope[varName] = item;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const tmp = document.createElement('template');
|
|
752
|
-
tmp.innerHTML = innerContent;
|
|
753
|
-
|
|
754
|
-
// Hydrate nested templates with Observable scope
|
|
755
|
-
parser.hydrate(tmp.content, localScope);
|
|
756
|
-
|
|
757
|
-
const nodes: Node[] = [];
|
|
758
|
-
while (tmp.content.firstChild) {
|
|
759
|
-
nodes.push(tmp.content.firstChild);
|
|
760
|
-
parent.insertBefore(tmp.content.firstChild, insertBefore);
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
return { nodes, observables: { idx: idxObs, val: valObs } };
|
|
764
|
-
};
|
|
765
|
-
|
|
766
|
-
// Remove all rendered nodes
|
|
767
|
-
const clearRendered = () => {
|
|
768
|
-
for (const r of rendered) {
|
|
769
|
-
for (const node of r.nodes) {
|
|
770
|
-
if (node.parentNode) {
|
|
771
|
-
node.parentNode.removeChild(node);
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
rendered = [];
|
|
776
|
-
};
|
|
777
|
-
|
|
778
|
-
// Full re-render: clear old nodes and render new array
|
|
779
|
-
const fullRerender = (insertBefore: Node | null) => {
|
|
780
|
-
clearRendered();
|
|
781
|
-
const arr = resolveArray();
|
|
782
|
-
for (let idx = 0; idx < arr.length; idx++) {
|
|
783
|
-
const result = renderItem(idx, arr[idx], insertBefore);
|
|
784
|
-
rendered.push(result);
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
// Smart update: update existing, add/remove as needed
|
|
789
|
-
const smartUpdate = (newArr: any[]) => {
|
|
790
|
-
const oldLen = rendered.length;
|
|
791
|
-
const newLen = newArr.length;
|
|
792
|
-
|
|
793
|
-
// Update existing items (just update Observable values)
|
|
794
|
-
for (let i = 0; i < Math.min(oldLen, newLen); i++) {
|
|
795
|
-
rendered[i].observables.idx.setObject(i);
|
|
796
|
-
rendered[i].observables.val.setObject(newArr[i]);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// If new array is longer, add new items
|
|
800
|
-
if (newLen > oldLen) {
|
|
801
|
-
let insertBeforeNode: Node | null = null;
|
|
802
|
-
if (rendered.length > 0) {
|
|
803
|
-
const lastNodes = rendered[rendered.length - 1].nodes;
|
|
804
|
-
if (lastNodes.length > 0) {
|
|
805
|
-
insertBeforeNode = lastNodes[lastNodes.length - 1].nextSibling;
|
|
806
|
-
}
|
|
807
|
-
} else {
|
|
808
|
-
insertBeforeNode = insertionPoint;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
for (let i = oldLen; i < newLen; i++) {
|
|
812
|
-
const result = renderItem(i, newArr[i], insertBeforeNode);
|
|
813
|
-
rendered.push(result);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
// If new array is shorter, remove extra items
|
|
818
|
-
if (newLen < oldLen) {
|
|
819
|
-
for (let i = oldLen - 1; i >= newLen; i--) {
|
|
820
|
-
for (const node of rendered[i].nodes) {
|
|
821
|
-
if (node.parentNode) {
|
|
822
|
-
node.parentNode.removeChild(node);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
rendered.splice(newLen);
|
|
827
|
-
}
|
|
828
|
-
};
|
|
829
|
-
|
|
830
|
-
// Initial render
|
|
831
|
-
const arr = resolveArray();
|
|
832
|
-
for (let idx = 0; idx < arr.length; idx++) {
|
|
833
|
-
const result = renderItem(idx, arr[idx], template);
|
|
834
|
-
rendered.push(result);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Remove the template element
|
|
838
|
-
parent.removeChild(template);
|
|
839
|
-
|
|
840
|
-
// Subscribe to Observable updates
|
|
841
|
-
if (isObservableLoop) {
|
|
842
|
-
if (isNestedPath) {
|
|
843
|
-
// For nested paths like category.items, we need to re-render when the root Observable changes
|
|
844
|
-
// because the nested structure might have changed entirely
|
|
845
|
-
(rootVal as Observable<any>).subscribe(() => {
|
|
846
|
-
// Find current insertion point (after last rendered node, or original point)
|
|
847
|
-
let insertBeforeNode: Node | null = insertionPoint;
|
|
848
|
-
if (rendered.length > 0) {
|
|
849
|
-
const lastNodes = rendered[rendered.length - 1].nodes;
|
|
850
|
-
if (lastNodes.length > 0) {
|
|
851
|
-
insertBeforeNode = lastNodes[lastNodes.length - 1].nextSibling;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
fullRerender(insertBeforeNode);
|
|
855
|
-
});
|
|
856
|
-
} else {
|
|
857
|
-
// Direct Observable - use smart update
|
|
858
|
-
(rootVal as Observable<any>).subscribe((newArr: any) => {
|
|
859
|
-
const items = Array.isArray(newArr) ? newArr : [];
|
|
860
|
-
smartUpdate(items);
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
};
|
|
866
|
-
|
|
867
|
-
// ==========================================
|
|
868
|
-
// Rule: ref — @[ref]="name"
|
|
869
|
-
// Create a variable in scope referencing the element
|
|
870
|
-
// ==========================================
|
|
871
|
-
const refRule: Rule = {
|
|
872
|
-
name: 'ref',
|
|
873
|
-
elementHydrate(parser, el, scope) {
|
|
874
|
-
// attribute name is '@[ref]'
|
|
875
|
-
if (!el.hasAttribute('@[ref]')) return;
|
|
876
|
-
const raw = el.getAttribute('@[ref]')?.trim();
|
|
877
|
-
if (!raw) return;
|
|
878
|
-
|
|
879
|
-
// Build evaluation context so prototype methods are available
|
|
880
|
-
const ctx = Context.build(parser.variables, scope);
|
|
881
|
-
|
|
882
|
-
// If attribute contains attribute-expression placeholders like {{EXP:...}},
|
|
883
|
-
// replace them by evaluating inner expressions. Otherwise try to evaluate the whole
|
|
884
|
-
// attribute as an expression. Fallback to literal if evaluation fails.
|
|
885
|
-
let refName: string | undefined;
|
|
886
|
-
const expPattern = /\{\{EXP:([^}]+)\}\}/g;
|
|
887
|
-
if (expPattern.test(raw)) {
|
|
888
|
-
// Replace each placeholder with evaluated string
|
|
889
|
-
let tmp = raw;
|
|
890
|
-
expPattern.lastIndex = 0;
|
|
891
|
-
tmp = tmp.replace(expPattern, (_m, enc) => {
|
|
892
|
-
const expr = decodeAttr(enc);
|
|
893
|
-
// Build evaluation scope with unwrapped Observable values used in expr
|
|
894
|
-
const evalScope = Object.assign({}, ctx);
|
|
895
|
-
if (scope) {
|
|
896
|
-
const obsNames = findObservablesInExpr(expr, scope);
|
|
897
|
-
for (const name of obsNames) {
|
|
898
|
-
const o = (scope as any)[name];
|
|
899
|
-
if (o instanceof Observable) evalScope[name] = o.getObject ? o.getObject() : undefined;
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
const val = parser.evaluate(expr, evalScope);
|
|
903
|
-
return val == null ? '' : String(val);
|
|
904
|
-
});
|
|
905
|
-
refName = tmp;
|
|
906
|
-
} else {
|
|
907
|
-
// Try evaluating full attribute as JS expression
|
|
908
|
-
try {
|
|
909
|
-
const evalScope = Object.assign({}, ctx);
|
|
910
|
-
if (scope) {
|
|
911
|
-
const obsNames = findObservablesInExpr(raw, scope);
|
|
912
|
-
for (const name of obsNames) {
|
|
913
|
-
const o = (scope as any)[name];
|
|
914
|
-
if (o instanceof Observable) evalScope[name] = o.getObject ? o.getObject() : undefined;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
const evaluated = parser.evaluate(raw, evalScope);
|
|
918
|
-
if (typeof evaluated === 'string') refName = evaluated;
|
|
919
|
-
else if (evaluated != null) refName = String(evaluated);
|
|
920
|
-
} catch (e) {
|
|
921
|
-
// ignore and fallback to raw
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if (!refName) refName = raw;
|
|
926
|
-
|
|
927
|
-
if (refName) {
|
|
928
|
-
if (scope) {
|
|
929
|
-
(scope as any)[refName] = el;
|
|
930
|
-
} else {
|
|
931
|
-
(parser as any).variables[refName] = el;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
// ==========================================
|
|
938
|
-
// Rule: on — @[onclick]="expression"; binds events to elements
|
|
939
|
-
// ==========================================
|
|
940
|
-
const onRule: Rule = {
|
|
941
|
-
name: 'on',
|
|
942
|
-
elementHydrate(parser, el, scope) {
|
|
943
|
-
// Ensure we don't attach duplicate handlers when hydration runs multiple times
|
|
944
|
-
const anyEl = el as any;
|
|
945
|
-
anyEl.__hmle_on_handlers = anyEl.__hmle_on_handlers || {};
|
|
946
|
-
const HMLE_ON_HANDLERS = anyEl.__hmle_on_handlers as Record<string, EventListener>;
|
|
947
|
-
for (const attr of Array.from(el.attributes)) {
|
|
948
|
-
const an = attr.name;
|
|
949
|
-
if (!an.startsWith('@[on') || !an.endsWith(']')) continue;
|
|
950
|
-
// Example: '@[onclick]' -> 'onclick' -> event 'click'
|
|
951
|
-
let eventName = an.slice(2, an.length - 1); // removes '@[' and ']'
|
|
952
|
-
if (eventName.startsWith('on')) eventName = eventName.slice(2);
|
|
953
|
-
const expr = attr.value;
|
|
954
|
-
// Skip if we already attached a handler for this event on this element
|
|
955
|
-
if (HMLE_ON_HANDLERS[eventName]) {
|
|
956
|
-
el.removeAttribute(an);
|
|
957
|
-
continue;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
// Add event listener
|
|
961
|
-
const handler = (ev: Event) => {
|
|
962
|
-
// Build evaluation scope with unwrapped Observables while preserving
|
|
963
|
-
// the prototype of the original scope so prototype methods remain bound.
|
|
964
|
-
let evalScope: Record<string, any> = Object.create(scope ?? null);
|
|
965
|
-
|
|
966
|
-
// Ensure Context.bindPrototypeMethods binds to the real scope instance,
|
|
967
|
-
// not to this wrapper object (otherwise `this` inside methods is not HTMLElement).
|
|
968
|
-
(evalScope as any).__hmle_this = scope ?? null;
|
|
969
|
-
|
|
970
|
-
// Unwrap Observables referenced in expression into own-properties
|
|
971
|
-
if (scope) {
|
|
972
|
-
const observed = findObservablesInExpr(expr, scope);
|
|
973
|
-
for (const name of observed) {
|
|
974
|
-
const o = (scope as any)[name];
|
|
975
|
-
if (o instanceof Observable) (evalScope as any)[name] = o.getObject ? o.getObject() : undefined;
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
// event and element are added to the scope as own-properties
|
|
980
|
-
(evalScope as any)['event'] = ev;
|
|
981
|
-
(evalScope as any)['element'] = el;
|
|
982
|
-
|
|
983
|
-
parser.evaluate(expr, evalScope);
|
|
984
|
-
};
|
|
985
|
-
el.addEventListener(eventName, handler);
|
|
986
|
-
HMLE_ON_HANDLERS[eventName] = handler;
|
|
987
|
-
el.removeAttribute(an);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
};
|