@tsomaiatech/moxite 1.0.0 → 1.0.1

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 (27) hide show
  1. package/README.md +12 -12
  2. package/dist/__tests__/template-engine.test.d.ts +1 -0
  3. package/dist/__tests__/template-engine.test.js +389 -0
  4. package/dist/template-engine.d.ts +80 -0
  5. package/dist/template-engine.js +500 -0
  6. package/dist/template-manager.d.ts +21 -0
  7. package/dist/template-manager.js +100 -0
  8. package/idea-plugin/.gradle/8.13/checksums/checksums.lock +0 -0
  9. package/idea-plugin/.gradle/8.13/checksums/md5-checksums.bin +0 -0
  10. package/idea-plugin/.gradle/8.13/checksums/sha1-checksums.bin +0 -0
  11. package/idea-plugin/.gradle/8.13/executionHistory/executionHistory.bin +0 -0
  12. package/idea-plugin/.gradle/8.13/executionHistory/executionHistory.lock +0 -0
  13. package/idea-plugin/.gradle/8.13/fileChanges/last-build.bin +0 -0
  14. package/idea-plugin/.gradle/8.13/fileHashes/fileHashes.bin +0 -0
  15. package/idea-plugin/.gradle/8.13/fileHashes/fileHashes.lock +0 -0
  16. package/idea-plugin/.gradle/8.13/gc.properties +0 -0
  17. package/idea-plugin/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  18. package/idea-plugin/.gradle/buildOutputCleanup/cache.properties +2 -0
  19. package/idea-plugin/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  20. package/idea-plugin/.gradle/file-system.probe +0 -0
  21. package/idea-plugin/.gradle/vcs-1/gc.properties +0 -0
  22. package/idea-plugin/gradle/wrapper/gradle-wrapper.jar +0 -0
  23. package/idea-plugin/gradle/wrapper/gradle-wrapper.properties +7 -0
  24. package/idea-plugin/gradlew +251 -0
  25. package/idea-plugin/gradlew.bat +94 -0
  26. package/package.json +2 -1
  27. package/tsconfig.json +21 -0
package/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  # Moxite Template Engine
2
2
 
3
- Moxite is an extremely fast, structural, and strictly evaluated template engine designed to be completely **indestructible**.
3
+ Moxite is a fast, structural, and strictly evaluated template engine designed to enforce a clear separation between application logic and the presentation layer.
4
4
 
5
- Drawing inspiration from modern control flows like Angular 19, Moxite enforces a separation of logic and presentation through a pristine, character-by-character parsed Abstract Syntax Tree (AST). It completely eliminates the use of fuzzy Regular Expressions for parsing, meaning it is impossible to crash the engine with malformed overlapping templates, escaped quotes, or structural injection attacks.
5
+ Moxite approaches template parsing through a dedicated, character-by-character Lexer and a recursive descent Abstract Syntax Tree (AST) Parser, moving away from string-replacement or Regex-heavy architectures.
6
6
 
7
- ## Why Moxite?
7
+ By evaluating an isolated AST, Moxite cleanly scopes template logic and ensures high reliability when parsing complex, overlapping data structures.
8
8
 
9
- Modern template engines (like Jinja, EJS, and Handlebars) rely heavily on Regular Expressions and have slowly mutated into sprawling, poorly-implemented programming languages that execute inline logic, define JSON objects inline, and succumb to context leaks.
9
+ ## Design Philosophy
10
10
 
11
- Moxite takes a diametrically opposed approach:
12
- 1. **Zero Regex Parsing**: Every token is mapped through a strict, atomic Lexical State machine and evaluated via a recursive descent Parser.
13
- 2. **Zero Inline Objects**: The Expression Lexer intentionally forbids array closures `[1, 2]` or object structures `{k: v}` to prevent Turing-complete bloat and force developers to construct their data in the core application logic (e.g. TypeScript/Java), not in the view layer.
14
- 3. **Implicit Safe Navigation**: The engine automatically protects against `null` or `undefined` property access without throwing exceptions, turning `{{ user.profile.name }}` into an implicit `{{ user?.profile?.name }}`.
15
- 4. **Absolute Polyglot Isolation**: Moxite's domain syntax (`@` and `{{ }}`) will perfectly isolate itself even when overlapping natively with 10 other template engines or syntaxes in the exact same file (C# Razor, PHP, Bash, JSON-LD, etc.).
11
+ The core philosophy of Moxite is to keep templates simple and focused purely on rendering:
12
+
13
+ 1. **Explicit Lexing**: Every template token is mapped through an atomic Lexical State machine and evaluated via a recursive descent Parser, ensuring predictable boundaries even when templates overlap with other syntaxes like JSON, bash scripts, or nested code blocks.
14
+ 2. **Strict Expression Evaluation**: The expression parser deliberately limits complex inline operations. Features like inline array definitions `[1, 2]` or JSON closures `{key: val}` are intentionally unsupported, encouraging developers to construct their data payloads cleanly in the upstream application code.
15
+ 3. **Implicit Safe Navigation**: The engine automatically protects against `null` or `undefined` property access without throwing verbose errors. For example, `{{ user.profile.name }}` is implicitly handled as `user?.profile?.name` under the hood.
16
16
 
17
17
  ## Syntax Outline
18
18
 
@@ -40,7 +40,7 @@ Implicit Safe Output: {{ user.deep.missing.path.shouldNotCrash }}
40
40
  @endfor
41
41
  ```
42
42
 
43
- ### Immutable Block Scoping (`@const`)
43
+ ### Block Scoping (`@const`)
44
44
  ```mx
45
45
  @const greeting = "Hello"
46
46
  @const role = user.role
@@ -49,7 +49,7 @@ Implicit Safe Output: {{ user.deep.missing.path.shouldNotCrash }}
49
49
  ```
50
50
 
51
51
  ### Pipes (`|`)
52
- Pipes allow you to pass values through pure formatting functions registered in the evaluation context.
52
+ Pipes allow you to pass variables through formatting functions registered in your evaluation context.
53
53
  ```mx
54
54
  {{ user.name | upper }}
55
55
  {{ total | currency: "USD" }}
@@ -58,4 +58,4 @@ Pipes allow you to pass values through pure formatting functions registered in t
58
58
  ## Structure
59
59
 
60
60
  - `src/`: Contains the pure TypeScript reference implementation of the strict Lexer, AST Parser, and Evaluator (`template-engine.ts`).
61
- - `idea-plugin/`: An independent Gradle project containing the IntelliJ IDEA plugin. Utilizing Grammar-Kit and JFlex, it tokenizes `.mx` files with the exact deterministic boundary rules as the TypeScript engine, providing native JetBrains syntax highlighting and completion.
61
+ - `idea-plugin/`: A Gradle project containing the IntelliJ IDEA plugin. Powered by Grammar-Kit and JFlex, this plugin provides native JetBrains syntax highlighting, completion, and semantic validation for `.mx` files using the exact same DFA token boundaries as the TypeScript engine.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,389 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const template_engine_1 = require("../template-engine");
4
+ describe('Template Engine', () => {
5
+ it('renders text only', () => {
6
+ const tpl = 'Hello world';
7
+ expect((0, template_engine_1.render)(tpl, {})).toBe('Hello world');
8
+ });
9
+ it('handles interpolation & implicit safe chaining', () => {
10
+ const tpl = 'Hello {{ user.profile.name }}!';
11
+ const ctx = { user: { profile: { name: 'Alice' } } };
12
+ expect((0, template_engine_1.render)(tpl, ctx)).toBe('Hello Alice!');
13
+ // Safe chaining
14
+ const ctxBad = {};
15
+ expect((0, template_engine_1.render)(tpl, ctxBad)).toBe('Hello !');
16
+ });
17
+ it('applies pipes correctly', () => {
18
+ const tpl = 'Hello {{ name | upper }} - {{ num | add: "5" }}';
19
+ const ctx = { name: 'bob', num: 10 };
20
+ const pipes = {
21
+ upper: (val) => val.toUpperCase(),
22
+ add: (val, amt) => val + Number(amt)
23
+ };
24
+ expect((0, template_engine_1.render)(tpl, ctx, pipes)).toBe('Hello BOB - 15');
25
+ });
26
+ it('evaluates @if / @else if / @else', () => {
27
+ const tpl = `
28
+ @if (user.isAdmin)
29
+ Admin
30
+ @else if (user.isGuest === "yes")
31
+ Guest
32
+ @else if (user.age > 18)
33
+ Adult
34
+ @else
35
+ User
36
+ @endif
37
+ `.trim();
38
+ expect((0, template_engine_1.render)(tpl, { user: { isAdmin: true } }).trim()).toBe('Admin');
39
+ expect((0, template_engine_1.render)(tpl, { user: { isGuest: "yes" } }).trim()).toBe('Guest');
40
+ expect((0, template_engine_1.render)(tpl, { user: { age: 20 } }).trim()).toBe('Adult');
41
+ expect((0, template_engine_1.render)(tpl, { user: { age: 10 } }).trim()).toBe('User');
42
+ });
43
+ it('runs @for loops', () => {
44
+ const tpl = `
45
+ @for (item of items)
46
+ - {{ item.name }}
47
+ @endfor
48
+ `.trim();
49
+ const ctx = { items: [{ name: 'A' }, { name: 'B' }] };
50
+ expect((0, template_engine_1.render)(tpl, ctx).trim()).toBe('- A\n- B');
51
+ });
52
+ it('supports @const block-level scoping', () => {
53
+ const tpl = `
54
+ @const a = 1
55
+ @if (true)
56
+ @const b = 2
57
+ {{ a }} - {{ b }}
58
+ @endif
59
+ @if (true)
60
+ @const b = 3
61
+ {{ a }} - {{ b }}
62
+ @endif
63
+ `.trim();
64
+ const ctx = {};
65
+ expect((0, template_engine_1.render)(tpl, ctx).trim()).toBe('1 - 2\n1 - 3');
66
+ });
67
+ it('executes complex battle test (Nested @for, @if, @const, Pipes)', () => {
68
+ const tpl = `
69
+ @const roleName = user.role
70
+ @if (user.isActive)
71
+ @for (project of user.projects)
72
+ @const projectStatus = project.status
73
+ @if (projectStatus === "active")
74
+ Active Project [{{ roleName | upper | prefix: ">> " }}]: {{ project.name }}
75
+ @for (task of project.tasks)
76
+ - {{ task.title }} ({{ task.priority | upper }})
77
+ @endfor
78
+ @else if (projectStatus === "archived")
79
+ Archived Project [{{ roleName | upper }}]: {{ project.name }}
80
+ @else
81
+ Unknown Project Status
82
+ @endif
83
+ @endfor
84
+ @else
85
+ Inactive User
86
+ @endif
87
+ `.trim();
88
+ const ctx = {
89
+ user: {
90
+ isActive: true,
91
+ role: 'engineer',
92
+ projects: [
93
+ {
94
+ name: 'Relay Engine',
95
+ status: 'active',
96
+ tasks: [
97
+ { title: 'Write lexer', priority: 'high' },
98
+ { title: 'Write parser', priority: 'medium' }
99
+ ]
100
+ },
101
+ {
102
+ name: 'Old Project',
103
+ status: 'archived',
104
+ tasks: []
105
+ }
106
+ ]
107
+ }
108
+ };
109
+ const pipes = {
110
+ upper: (val) => val.toUpperCase(),
111
+ prefix: (val, p) => p + val
112
+ };
113
+ const expected = `
114
+ Active Project [>> ENGINEER]: Relay Engine
115
+
116
+ - Write lexer (HIGH)
117
+
118
+ - Write parser (MEDIUM)
119
+
120
+
121
+ Archived Project [ENGINEER]: Old Project
122
+ `.replace(/^\s+/gm, '').trim();
123
+ const result = (0, template_engine_1.render)(tpl, ctx, pipes).replace(/^\s+/gm, '').trim();
124
+ expect(result).toBe(expected);
125
+ });
126
+ it('handles edge cases with empty lists and nested object paths', () => {
127
+ const tpl = `
128
+ @if (user.isNotSet)
129
+ Should not render
130
+ @else if (user.count === 0)
131
+ Zero Count: {{ user.metadata.tags[0] }}
132
+ @endif
133
+ @for (item of emptyList)
134
+ Should not render
135
+ @endfor
136
+ Done
137
+ `.trim();
138
+ const ctx = { user: { count: 0, metadata: { tags: ['admin'] } }, emptyList: [] };
139
+ expect((0, template_engine_1.render)(tpl, ctx).trim()).toBe('Zero Count: admin\nDone');
140
+ });
141
+ it('Deep nesting, parallel scopes, falsy values, pipe chains', () => {
142
+ const tpl = `
143
+ @const globalPrefix = ">>"
144
+ @if (payload.isValid === false)
145
+ Payload Invalid. Error: {{ payload.error | upper }}
146
+ @else
147
+ @for (org of payload.organizations)
148
+ ORGANIZATION: {{ org.name }}
149
+ @const orgStatus = org.isActive
150
+ @if (orgStatus)
151
+ @const memberCount = org.members.length
152
+ @if (memberCount === 0)
153
+ No members in active org.
154
+ @else
155
+ Members ({{ memberCount }}):
156
+ @for (member of org.members)
157
+ @const prefix = globalPrefix | concat: " " | concat: member.role
158
+ @if (member.isBanned)
159
+ [BANNED] {{ prefix }} - {{ member.name }}
160
+ @else if (member.age < 18)
161
+ [MINOR] {{ prefix }} - {{ member.name }}
162
+ @else
163
+ [ACTIVE] {{ prefix }} - {{ member.name }} | Data: {{ member.deep.missing.path.shouldNotCrash }}
164
+ @endif
165
+ @endfor
166
+ @endif
167
+ @else
168
+ Organization {{ org.name }} is INACTIVE.
169
+ @endif
170
+ ---
171
+ @endfor
172
+ @endif
173
+ `.trim();
174
+ const ctx = {
175
+ payload: {
176
+ isValid: true,
177
+ error: null,
178
+ organizations: [
179
+ {
180
+ name: 'Alpha Corp',
181
+ isActive: false,
182
+ members: []
183
+ },
184
+ {
185
+ name: 'Beta LLC',
186
+ isActive: true,
187
+ members: []
188
+ },
189
+ {
190
+ name: 'Gamma Inc',
191
+ isActive: true,
192
+ members: [
193
+ { name: 'Alice', role: 'ADMIN', isBanned: false, age: 30, deep: {} },
194
+ { name: 'Bob', role: 'USER', isBanned: true, age: 25 },
195
+ { name: 'Charlie', role: 'GUEST', isBanned: false, age: 16 }
196
+ ]
197
+ }
198
+ ]
199
+ }
200
+ };
201
+ const pipes = {
202
+ upper: (val) => val ? val.toUpperCase() : '',
203
+ concat: (val, extra) => val + extra
204
+ };
205
+ const expected = `
206
+ ORGANIZATION: Alpha Corp
207
+ Organization Alpha Corp is INACTIVE.
208
+ ---
209
+ ORGANIZATION: Beta LLC
210
+ No members in active org.
211
+ ---
212
+ ORGANIZATION: Gamma Inc
213
+ Members (3):
214
+ [ACTIVE] >> ADMIN - Alice | Data:
215
+ [BANNED] >> USER - Bob
216
+ [MINOR] >> GUEST - Charlie
217
+ ---
218
+ `.trim().replace(/^\s+/gm, '');
219
+ const result = (0, template_engine_1.render)(tpl, ctx, pipes).trim().replace(/^\s+/gm, '');
220
+ expect(result).toBe(expected);
221
+ });
222
+ it('Resilience against malformed tokens (throws correctly or ignores)', () => {
223
+ const unclosedTpl = 'Hello {{ user.name';
224
+ expect(() => (0, template_engine_1.render)(unclosedTpl, {})).toThrow(/Unclosed interpolation/);
225
+ const malformedConst = '@const user.name = "bob"';
226
+ expect(() => (0, template_engine_1.render)(malformedConst, {})).toThrow(/Malformed @const/);
227
+ const badPipe = '{{ user.name | unknownPipe }}';
228
+ expect(() => (0, template_engine_1.render)(badPipe, { user: { name: 'bob' } })).toThrow(/Unknown pipe: unknownPipe/);
229
+ // Shadowing constant should throw
230
+ const shadowTpl = `
231
+ @const a = 1
232
+ @const a = 2
233
+ `;
234
+ expect(() => (0, template_engine_1.render)(shadowTpl, {})).toThrow(/Cannot shadow or redefine constant 'a'/);
235
+ });
236
+ it('Extreme Edge Cases & Parser Torment', () => {
237
+ // 1. Whitespace Chaos
238
+ const tpl1 = `
239
+ @if (
240
+ user . age
241
+ ===
242
+ 20
243
+ )
244
+ Yes
245
+ @endif`.trim();
246
+ expect((0, template_engine_1.render)(tpl1, { user: { age: 20 } }).trim()).toBe('Yes');
247
+ // 2. Computed Properties with crazy names
248
+ const tpl2 = `{{ data["crazy-key-with space"] }}`;
249
+ expect((0, template_engine_1.render)(tpl2, { data: { "crazy-key-with space": "works" } })).toBe('works');
250
+ // 3. String literals containing template syntax (Lexer should not interpolate inner strings)
251
+ const tpl3 = `{{ "{{ not an interpolation }}" }}`;
252
+ expect((0, template_engine_1.render)(tpl3, {})).toBe('{{ not an interpolation }}');
253
+ // 4. Dangling blocks
254
+ expect(() => (0, template_engine_1.render)('@else', {})).toThrow(/Unexpected block tag: @else/);
255
+ expect(() => (0, template_engine_1.render)('@endif', {})).toThrow(/Unexpected block tag: @endif/);
256
+ expect(() => (0, template_engine_1.render)('@endfor', {})).toThrow(/Unexpected block tag: @endfor/);
257
+ });
258
+ it('The Unhandled JS Idioms (Breaking the engine!)', () => {
259
+ // These are scenarios I know the strict Lexer cannot handle yet because they are complex JS idioms
260
+ // 1. Escaped Quotes inside strings: The lexer will stop at the first internal quote.
261
+ const tplEscaped = `{{ "He said \\"Hello\\"" }}`;
262
+ expect(() => (0, template_engine_1.render)(tplEscaped, {})).toThrow();
263
+ // 2. Negative Numbers: Lexer only recognizes digits, not the minus operator as part of a number or unary.
264
+ const tplNegative = `@const temp = -10`;
265
+ expect(() => (0, template_engine_1.render)(tplNegative, {})).toThrow(/Unexpected character in expression.*-/);
266
+ // 3. Unary Operators (boolean NOT):
267
+ const tplUnary = `@if (!user.isActive) \n inactive \n @endif`;
268
+ expect(() => (0, template_engine_1.render)(tplUnary, { user: { isActive: false } })).toThrow(/Unexpected character in expression.*!/);
269
+ // 4. Unorthodox 'of' usage in @for loops
270
+ const tplForOf = `@for (item of ["string of doom", "other"]) \n {{ item }} \n @endfor`;
271
+ // Throws because our strict expression parser intentionally does not support Array literals `[`
272
+ expect(() => (0, template_engine_1.render)(tplForOf, {})).toThrow();
273
+ // It works because indexOf(' of ') finds the first instance, but what if the item name has "of" with spaces around it?
274
+ // User names a variable `list of things`. Invalid identifier but interesting break!
275
+ expect(() => (0, template_engine_1.render)(`@for (list of things of items)`, {})).toThrow();
276
+ });
277
+ it('False positives and overlapping data structures', () => {
278
+ // 1. JSON-LD and Emails (False positive tags)
279
+ // The engine must NOT crash when encountering `@context`, `@id`, `user@email.com`, or `@iframe`.
280
+ const tpl1 = `{
281
+ "@context": "https://json-ld.org/contexts/person.jsonld",
282
+ "@id": "http://dbpedia.org/resource/John_Lennon",
283
+ "email": "john@beatles.com",
284
+ "handle": "@johnlennon",
285
+ "tagLike": "@iframe width=100"
286
+ }`;
287
+ expect((0, template_engine_1.render)(tpl1, {})).toBe(tpl1);
288
+ // 2. Data that generates another template (Template Inception)
289
+ const tpl2 = `
290
+ @const open = "{{"
291
+ @const close = "}}"
292
+ Code: {{ open }} user.name {{ close }}
293
+ `.trim();
294
+ expect((0, template_engine_1.render)(tpl2, {})).toBe('Code: {{ user.name }}');
295
+ // 3. String literals containing structural AST tokens
296
+ const tpl3 = `
297
+ @if (user.status === "ACTIVE (ignore this)")
298
+ @const message = "Status is: @if(true) nested @endif"
299
+ {{ message }}
300
+ @endif
301
+ `.trim();
302
+ expect((0, template_engine_1.render)(tpl3, { user: { status: "ACTIVE (ignore this)" } }).trim()).toBe("Status is: @if(true) nested @endif");
303
+ });
304
+ it('Syntax Polyglot & Cross-Language Interference', () => {
305
+ // These tests mix syntaxes from other engines (Jinja, Razor, Blade, Handlebars, JSX)
306
+ // and structural languages (Python, PHP, C#, Bash) to prove our engine only executes its own domain.
307
+ // (@Model.User)
308
+ // Our Lexer must ignore `@Model` because it doesn't match `@if`, `@for`, etc.
309
+ const razorTpl = `<div>@Model.UserName - @if(user.exists) FOUND @endif</div>`;
310
+ expect((0, template_engine_1.render)(razorTpl, { user: { exists: true } })).toBe('<div>@Model.UserName - FOUND </div>');
311
+ // ({% if %}, {# comment #})
312
+ const pythonTpl = `
313
+ def print_user():
314
+ # {% if user.is_active %}
315
+ print("{{ user.name }}") # Outputs native interpolation
316
+ # {% endif %}
317
+ @const result = user.name
318
+ return "{{ result }}"
319
+ `;
320
+ const expectedPython = `def print_user(): # {% if user.is_active %} print("Alice") # Outputs native interpolation # {% endif %} return "Alice"`;
321
+ expect((0, template_engine_1.render)(pythonTpl, { user: { name: "Alice" } }).replace(/\s+/g, ' ').trim()).toBe(expectedPython);
322
+ // ({user.name} vs {{user.name}})
323
+ // Only double braces get executed, single braces get passed as text.
324
+ const jsxTpl = `<div className={styles.container}>{user.name} is actually {{ user.name }}</div>`;
325
+ expect((0, template_engine_1.render)(jsxTpl, { user: { name: "Alice" } })).toBe('<div className={styles.container}>{user.name} is actually Alice</div>');
326
+ // Engine must ignore @foreach and <?php tags.
327
+ const bladeTpl = `
328
+ <?php echo $var; ?>
329
+ @foreach($users as $u)
330
+ @for (u of users)
331
+ {{ u }}
332
+ @endfor
333
+ @endforeach
334
+ `;
335
+ const expectedBlade = `<?php echo $var; ?> @foreach($users as $u) Bob @endforeach`;
336
+ expect((0, template_engine_1.render)(bladeTpl, { users: ["Bob"] }).replace(/\s+/g, ' ').trim()).toBe(expectedBlade);
337
+ // Bash script Interference ($USER, ${USER})
338
+ const bashTpl = `
339
+ #!/bin/bash
340
+ export THE_USER="{{ sys.user }}"
341
+ echo $THE_USER
342
+ echo \${THE_USER}
343
+ @if (sys.isRoot)
344
+ sudo rm -rf /
345
+ @endif
346
+ `;
347
+ const expectedBash = `#!/bin/bash export THE_USER="admin" echo $THE_USER echo \${THE_USER} sudo rm -rf /`;
348
+ expect((0, template_engine_1.render)(bashTpl, { sys: { user: "admin", isRoot: true } }).replace(/\s+/g, ' ').trim()).toBe(expectedBash);
349
+ // Triple Curly Interference ({{{ user.name }}})
350
+ // Emulates Vue's v-html raw injection braces.
351
+ // Our lexer matches the outermost {{ and }} and passes `{ param }` to the expression parser.
352
+ // Since our strict expression parser explicitly forbids JSON objects (no Javascript `{` or `}`), this safely throws a syntax error!
353
+ const handlebarsTpl = `{{{ param }}}`;
354
+ expect(() => (0, template_engine_1.render)(handlebarsTpl, { param: "value" })).toThrow(/Unexpected character in expression at index 0: \{/);
355
+ const erbTpl = `<%= user.id %> / <%- @for (num of arr) -%>{{ num }}<%- @endfor -%>`;
356
+ // Note: The space after `)` in `@for (num of arr) -%>` is preserved as text
357
+ const expectedErb = `<%= user.id %> / <%- -%>1<%- -%>`;
358
+ expect((0, template_engine_1.render)(erbTpl, { arr: [1] })).toBe(expectedErb);
359
+ const jsTpl = `
360
+ const greeting = \`Hello \${user.name}\`;
361
+ @const name = "Bob"
362
+ const override = \`Hello {{ name }}\`;
363
+ `;
364
+ const expectedJs = `const greeting = \`Hello \${user.name}\`; const override = \`Hello Bob\`;`;
365
+ expect((0, template_engine_1.render)(jsTpl, {}).replace(/\s+/g, ' ').trim()).toBe(expectedJs);
366
+ const mdTpl = `
367
+ \`\`\`ts
368
+ @for (item of items)
369
+ console.log("{{ item }}");
370
+ @endfor
371
+ \`\`\`
372
+ `;
373
+ const expectedMd = `\`\`\`ts console.log("X"); \`\`\``;
374
+ expect((0, template_engine_1.render)(mdTpl, { items: ["X"] }).replace(/\s+/g, ' ').trim()).toBe(expectedMd);
375
+ const javaTpl = `
376
+ @RestController
377
+ @RequestMapping("/api")
378
+ public class App {
379
+ @Autowired
380
+ private Service service;
381
+ // @if (java.generateSetter)
382
+ public void setService(Service s) { this.service = s; }
383
+ // @endif
384
+ }
385
+ `;
386
+ const expectedJava = `@RestController @RequestMapping("/api") public class App { @Autowired private Service service; // public void setService(Service s) { this.service = s; } // }`;
387
+ expect((0, template_engine_1.render)(javaTpl, { java: { generateSetter: true } }).replace(/\s+/g, ' ').trim()).toBe(expectedJava);
388
+ });
389
+ });
@@ -0,0 +1,80 @@
1
+ export type Context = Record<string, any>;
2
+ export type PipeFunction = (value: any, ...args: any[]) => any;
3
+ export type PipeRegistry = Record<string, PipeFunction>;
4
+ export type ExprToken = {
5
+ type: 'Identifier';
6
+ value: string;
7
+ } | {
8
+ type: 'Number';
9
+ value: number;
10
+ } | {
11
+ type: 'String';
12
+ value: string;
13
+ } | {
14
+ type: 'Operator';
15
+ value: string;
16
+ } | {
17
+ type: 'Punctuation';
18
+ value: string;
19
+ };
20
+ export declare function tokenizeExpression(source: string): ExprToken[];
21
+ export type ExprAST = {
22
+ type: 'Literal';
23
+ value: any;
24
+ } | {
25
+ type: 'Identifier';
26
+ name: string;
27
+ } | {
28
+ type: 'Member';
29
+ object: ExprAST;
30
+ property: string | ExprAST;
31
+ computed: boolean;
32
+ } | {
33
+ type: 'Binary';
34
+ operator: string;
35
+ left: ExprAST;
36
+ right: ExprAST;
37
+ } | {
38
+ type: 'Pipe';
39
+ base: ExprAST;
40
+ name: string;
41
+ args: ExprAST[];
42
+ };
43
+ export declare function parseExpression(source: string): ExprAST;
44
+ export type TemplateToken = {
45
+ type: 'TEXT';
46
+ value: string;
47
+ } | {
48
+ type: 'INTERPOLATION';
49
+ value: string;
50
+ } | {
51
+ type: 'TAG';
52
+ name: string;
53
+ inner: string | null;
54
+ };
55
+ export declare function tokenizeTemplate(source: string): TemplateToken[];
56
+ export type ASTNode = {
57
+ type: 'TEXT';
58
+ content: string;
59
+ } | {
60
+ type: 'INTERPOLATION';
61
+ expression: ExprAST;
62
+ } | {
63
+ type: 'CONST';
64
+ name: string;
65
+ expression: ExprAST;
66
+ } | {
67
+ type: 'IF';
68
+ condition: ExprAST;
69
+ consequence: ASTNode[];
70
+ alternate: ASTNode[] | null;
71
+ } | {
72
+ type: 'FOR';
73
+ itemName: string;
74
+ listExpression: ExprAST;
75
+ body: ASTNode[];
76
+ };
77
+ export declare function parseTemplate(tokens: TemplateToken[]): ASTNode[];
78
+ export declare function evaluateExprAST(ast: ExprAST, context: Context, pipes?: PipeRegistry): any;
79
+ export declare function evaluateTemplate(nodes: ASTNode[], context: Context, pipes?: PipeRegistry): string;
80
+ export declare function render(templateStr: string, context: Context, pipes?: PipeRegistry): string;