@reidelsaltres/pureper 0.2.3 → 0.2.10

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