@aetherframework/template-engine 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/engines/AetherEngine.js +87 -54
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ A modern, lightweight template engine for Node.js with Blade-like syntax, suppor
|
|
|
5
5
|
Features
|
|
6
6
|
|
|
7
7
|
- Blade-like Syntax: Familiar syntax similar to Laravel Blade
|
|
8
|
-
- Template Inheritance: Support for `@extends`, `@section`, `@yield`
|
|
8
|
+
- Template Inheritance: Support for `@extends`, `@section`, `@yield`, `@include`
|
|
9
9
|
- Conditionals & Loops: `@if`, `@else`, `@endif`, `@foreach`, `@endforeach`
|
|
10
10
|
- Custom Functions: `{{ route('home') }}`, `{{ asset('images/logo.png') }}`
|
|
11
11
|
- Chained Properties: `{{ auth().user.name }}`
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ import crypto from 'crypto';
|
|
|
17
17
|
class AetherEngine {
|
|
18
18
|
constructor(options = {}) {
|
|
19
19
|
this.name = 'aether';
|
|
20
|
-
this.version = '1.
|
|
20
|
+
this.version = '1.3.0'; // Version upgraded
|
|
21
21
|
this.initialized = false;
|
|
22
22
|
|
|
23
23
|
this.options = {
|
|
@@ -27,7 +27,7 @@ class AetherEngine {
|
|
|
27
27
|
compileCache: new Map(),
|
|
28
28
|
templateCache: new Map(),
|
|
29
29
|
debug: options.debug || false,
|
|
30
|
-
csrfToken: options.csrfToken || '',
|
|
30
|
+
csrfToken: options.csrfToken || '',
|
|
31
31
|
...options
|
|
32
32
|
};
|
|
33
33
|
|
|
@@ -64,12 +64,10 @@ class AetherEngine {
|
|
|
64
64
|
this.filter('escape', (value) => this.escapeHtml(value));
|
|
65
65
|
this.filter('json', (value) => JSON.stringify(value));
|
|
66
66
|
this.filter('date', (value) => new Date(value).toISOString().split('T')[0]);
|
|
67
|
-
// NEW: raw filter to bypass escaping
|
|
68
67
|
this.filter('raw', (value) => value);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
registerDefaultFunctions() {
|
|
72
|
-
// Dynamic route function
|
|
73
71
|
this.function('route', (name, params = {}) => {
|
|
74
72
|
let url = this.routes[name] || `/${name}`;
|
|
75
73
|
if (params && typeof params === 'object') {
|
|
@@ -84,60 +82,38 @@ class AetherEngine {
|
|
|
84
82
|
this.function('auth', () => ({ check: () => true, user: { name: 'John Doe', email: 'john@example.com', id: 1 } }));
|
|
85
83
|
this.function('env', (key, defaultValue = '') => process.env[key] || defaultValue);
|
|
86
84
|
this.function('url', (urlPath) => `${process.env.BASE_URL || 'http://localhost:3000'}${urlPath.startsWith('/') ? urlPath : '/' + urlPath}`);
|
|
87
|
-
|
|
88
|
-
// NEW: CSRF Token generator
|
|
89
85
|
this.function('csrf', () => `<input type="hidden" name="_csrf" value="${this.options.csrfToken}">`);
|
|
90
86
|
|
|
91
|
-
// NEW: Old input retrieval (for form validation fallbacks)
|
|
92
87
|
this.function('old', (field, defaultValue = '') => {
|
|
93
|
-
return `__get('old.${field}') || ${JSON.stringify(defaultValue)}`;
|
|
88
|
+
return `__get('old.${field}') || ${JSON.stringify(defaultValue)}`;
|
|
94
89
|
});
|
|
95
90
|
|
|
96
|
-
// NEW: Dynamic Class builder (e.g., @class(['btn', 'active' => isActive]))
|
|
97
91
|
this.function('class', (classes) => {
|
|
98
92
|
if (Array.isArray(classes)) {
|
|
99
|
-
return classes.filter(c =>
|
|
100
|
-
if (typeof c === 'string') return true;
|
|
101
|
-
return false; // Object conditions are handled at runtime via JS
|
|
102
|
-
}).join(' ');
|
|
93
|
+
return classes.filter(c => typeof c === 'string').join(' ');
|
|
103
94
|
}
|
|
104
95
|
return '';
|
|
105
96
|
});
|
|
106
97
|
|
|
107
|
-
// NEW: Pagination HTML Generator
|
|
108
98
|
this.function('pagination', (pager) => {
|
|
109
99
|
if (!pager || !pager.total || pager.total <= 1) return '';
|
|
110
100
|
let html = '<nav class="aether-pagination" aria-label="Pagination">';
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (pager.current > 1) {
|
|
114
|
-
html += `<a href="${pager.baseUrl}?page=${pager.current - 1}" class="page-link prev">« Prev</a>`;
|
|
115
|
-
} else {
|
|
116
|
-
html += `<span class="page-link disabled">« Prev</span>`;
|
|
117
|
-
}
|
|
101
|
+
if (pager.current > 1) html += `<a href="${pager.baseUrl}?page=${pager.current - 1}" class="page-link prev">« Prev</a>`;
|
|
102
|
+
else html += `<span class="page-link disabled">« Prev</span>`;
|
|
118
103
|
|
|
119
|
-
// Pages (Simple windowing logic)
|
|
120
104
|
const start = Math.max(1, pager.current - 2);
|
|
121
105
|
const end = Math.min(pager.total, pager.current + 2);
|
|
122
|
-
|
|
123
106
|
if (start > 1) html += `<a href="${pager.baseUrl}?page=1" class="page-link">1</a><span class="dots">...</span>`;
|
|
124
107
|
|
|
125
108
|
for (let i = start; i <= end; i++) {
|
|
126
|
-
if (i === pager.current) {
|
|
127
|
-
|
|
128
|
-
} else {
|
|
129
|
-
html += `<a href="${pager.baseUrl}?page=${i}" class="page-link">${i}</a>`;
|
|
130
|
-
}
|
|
109
|
+
if (i === pager.current) html += `<span class="page-link active">${i}</span>`;
|
|
110
|
+
else html += `<a href="${pager.baseUrl}?page=${i}" class="page-link">${i}</a>`;
|
|
131
111
|
}
|
|
132
112
|
|
|
133
113
|
if (end < pager.total) html += `<span class="dots">...</span><a href="${pager.baseUrl}?page=${pager.total}" class="page-link">${pager.total}</a>`;
|
|
134
114
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
html += `<a href="${pager.baseUrl}?page=${pager.current + 1}" class="page-link next">Next »</a>`;
|
|
138
|
-
} else {
|
|
139
|
-
html += `<span class="page-link disabled">Next »</span>`;
|
|
140
|
-
}
|
|
115
|
+
if (pager.current < pager.total) html += `<a href="${pager.baseUrl}?page=${pager.current + 1}" class="page-link next">Next »</a>`;
|
|
116
|
+
else html += `<span class="page-link disabled">Next »</span>`;
|
|
141
117
|
|
|
142
118
|
html += '</nav>';
|
|
143
119
|
return html;
|
|
@@ -161,7 +137,6 @@ class AetherEngine {
|
|
|
161
137
|
let cursor = 0;
|
|
162
138
|
|
|
163
139
|
const parenRegex = '\\(((?:[^()]+|\\([^()]*\\))*)\\)';
|
|
164
|
-
// Added {!! !!}, @push, @endpush, @stack, @csrf
|
|
165
140
|
const tokenRegex = new RegExp(
|
|
166
141
|
`(\\{!!.*?!!\\}|\\{\\{.*?\\}\\}|@if\\s*${parenRegex}|@elseif\\s*${parenRegex}|@else\\b|@endif\\b|@foreach\\s*${parenRegex}|@endforeach\\b|@forelse\\s*${parenRegex}|@empty\\b|@endforelse\\b|@yield\\s*${parenRegex}|@section\\s*${parenRegex}|@endsection\\b|@extends\\s*${parenRegex}|@include\\s*${parenRegex}|@push\\s*${parenRegex}|@endpush\\b|@stack\\s*${parenRegex}|@csrf\\b)`,
|
|
167
142
|
'gs'
|
|
@@ -187,14 +162,12 @@ class AetherEngine {
|
|
|
187
162
|
cursor = match.index + match[0].length;
|
|
188
163
|
const token = match[0].trim();
|
|
189
164
|
|
|
190
|
-
// 1. Raw Output {!! ... !!}
|
|
191
165
|
if (token.startsWith('{!!') && token.endsWith('!!}')) {
|
|
192
166
|
const expr = token.slice(3, -3).trim();
|
|
193
167
|
if (inSectionBlock) sectionContent += token;
|
|
194
168
|
else if (inStackBlock) stackContent += token;
|
|
195
|
-
else jsCode += `__output.push(${this.parseExpression(expr, true)});\n`;
|
|
196
|
-
}
|
|
197
|
-
// 2. Escaped Output {{ ... }}
|
|
169
|
+
else jsCode += `__output.push(${this.parseExpression(expr, true)});\n`;
|
|
170
|
+
}
|
|
198
171
|
else if (token.startsWith('{{') && token.endsWith('}}')) {
|
|
199
172
|
const expr = token.slice(2, -2).trim();
|
|
200
173
|
if (inSectionBlock) sectionContent += token;
|
|
@@ -273,7 +246,6 @@ class AetherEngine {
|
|
|
273
246
|
else if (inStackBlock) stackContent += token;
|
|
274
247
|
else jsCode += `}\n__scope = __parentScope;\n}\n`;
|
|
275
248
|
}
|
|
276
|
-
// 3. Stacks (@push / @endpush / @stack)
|
|
277
249
|
else if (token.startsWith('@push')) {
|
|
278
250
|
const pushMatch = token.match(/@push\s*\(\s*'([^']+)'\s*\)/);
|
|
279
251
|
if (pushMatch) {
|
|
@@ -305,13 +277,11 @@ class AetherEngine {
|
|
|
305
277
|
else jsCode += `if(__stacks['${stackMatch[1]}']) { __stacks['${stackMatch[1]}'].forEach(fn => __output.push(fn(__scope, helpers))); }\n`;
|
|
306
278
|
}
|
|
307
279
|
}
|
|
308
|
-
// 4. CSRF
|
|
309
280
|
else if (token === '@csrf') {
|
|
310
281
|
if (inSectionBlock) sectionContent += token;
|
|
311
282
|
else if (inStackBlock) stackContent += token;
|
|
312
283
|
else jsCode += `__output.push(__functions.get('csrf')());\n`;
|
|
313
284
|
}
|
|
314
|
-
// 5. Yields & Sections
|
|
315
285
|
else if (token.startsWith('@yield')) {
|
|
316
286
|
const yieldMatch = token.match(/@yield\s*\(\s*'([^']+)'(?:\s*,\s*'([^']*)')?\s*\)/);
|
|
317
287
|
if (yieldMatch) {
|
|
@@ -343,12 +313,17 @@ class AetherEngine {
|
|
|
343
313
|
inSectionBlock = false; currentSectionName = ''; sectionContent = '';
|
|
344
314
|
}
|
|
345
315
|
}
|
|
316
|
+
// FIX: Enhance @include syntax to support passing data parameters
|
|
346
317
|
else if (token.startsWith('@include')) {
|
|
347
|
-
const includeMatch = token.match(/@include\s*\(\s*'([^']+)'\s*\)/);
|
|
318
|
+
const includeMatch = token.match(/@include\s*\(\s*'([^']+)'\s*(?:,\s*([\s\S]*?))?\s*\)/);
|
|
348
319
|
if (includeMatch) {
|
|
320
|
+
const viewName = includeMatch[1];
|
|
321
|
+
// Default to empty object if no params; otherwise evaluate as JS expression
|
|
322
|
+
const dataExpr = includeMatch[2] ? includeMatch[2].trim() : '{}';
|
|
323
|
+
|
|
349
324
|
if (inSectionBlock) sectionContent += token;
|
|
350
325
|
else if (inStackBlock) stackContent += token;
|
|
351
|
-
else jsCode += `__output.push(__include('${
|
|
326
|
+
else jsCode += `__output.push(__include('${viewName}', ${dataExpr}));\n`;
|
|
352
327
|
}
|
|
353
328
|
}
|
|
354
329
|
else if (token.startsWith('@extends')) {
|
|
@@ -371,7 +346,6 @@ class AetherEngine {
|
|
|
371
346
|
return __output.join('');
|
|
372
347
|
};\n`;
|
|
373
348
|
} else if (inStackBlock) {
|
|
374
|
-
// Handle unclosed push block gracefully
|
|
375
349
|
stackContent += remainingText;
|
|
376
350
|
} else {
|
|
377
351
|
jsCode += `__output.push(${JSON.stringify(remainingText)});\n`;
|
|
@@ -392,8 +366,28 @@ class AetherEngine {
|
|
|
392
366
|
}
|
|
393
367
|
return value;
|
|
394
368
|
}
|
|
395
|
-
|
|
396
|
-
|
|
369
|
+
|
|
370
|
+
// FIX: Fully implement __include to support synchronous loading, compiling, and rendering of sub-templates from disk
|
|
371
|
+
function __include(name, includeData = {}) {
|
|
372
|
+
// 1. Merge current scope with passed include data
|
|
373
|
+
const mergedScope = Object.assign({}, __scope, includeData);
|
|
374
|
+
|
|
375
|
+
// 2. Prioritize pre-compiled includes (passed via render options)
|
|
376
|
+
if (helpers.includes && helpers.includes[name]) {
|
|
377
|
+
return helpers.includes[name](mergedScope, helpers);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// 3. Synchronously load and compile template from disk
|
|
381
|
+
if (helpers.engine) {
|
|
382
|
+
try {
|
|
383
|
+
const content = helpers.engine.loadTemplateSync(name);
|
|
384
|
+
const compiled = helpers.engine.compile(content);
|
|
385
|
+
return compiled(mergedScope, helpers);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
if (helpers.engine.options.debug) console.error('[AetherEngine] Include error:', e.message);
|
|
388
|
+
return ''; // Fail silently or return empty in production
|
|
389
|
+
}
|
|
390
|
+
}
|
|
397
391
|
return '';
|
|
398
392
|
}
|
|
399
393
|
`;
|
|
@@ -431,7 +425,6 @@ class AetherEngine {
|
|
|
431
425
|
return `__get('${identifier}')`;
|
|
432
426
|
});
|
|
433
427
|
|
|
434
|
-
// Apply escaping only if not raw
|
|
435
428
|
if (!isRaw) {
|
|
436
429
|
if (/^__get\('[^']+'\)$/.test(parsed)) return `__escape(${parsed})`;
|
|
437
430
|
if (!parsed.includes('__functions') && !parsed.includes('__filters') && !parsed.includes('__get(')) return `__escape(${parsed})`;
|
|
@@ -440,7 +433,6 @@ class AetherEngine {
|
|
|
440
433
|
return parsed;
|
|
441
434
|
}
|
|
442
435
|
|
|
443
|
-
// ... (parseFunctionArguments, parseCondition, parseForeachExpression remain exactly the same) ...
|
|
444
436
|
parseFunctionArguments(argsString) {
|
|
445
437
|
const args = []; let currentArg = '', inString = false, stringChar = '';
|
|
446
438
|
for (let i = 0; i < argsString.length; i++) {
|
|
@@ -502,7 +494,7 @@ class AetherEngine {
|
|
|
502
494
|
const renderFunc = new Function('data', 'helpers', `
|
|
503
495
|
const __output = [];
|
|
504
496
|
const __sections = {};
|
|
505
|
-
const __stacks = {};
|
|
497
|
+
const __stacks = {};
|
|
506
498
|
const __escape = helpers.filters.get('escape') || function(s){ return s; };
|
|
507
499
|
const __filters = helpers.filters;
|
|
508
500
|
const __functions = helpers.functions;
|
|
@@ -547,17 +539,39 @@ class AetherEngine {
|
|
|
547
539
|
}
|
|
548
540
|
|
|
549
541
|
const renderFunc = this.compile(templateContent);
|
|
550
|
-
|
|
542
|
+
// FIX: Pass engine instance to helpers so __include can synchronously call loadTemplateSync
|
|
543
|
+
return renderFunc(data, {
|
|
544
|
+
filters: this.filters,
|
|
545
|
+
functions: this.functions,
|
|
546
|
+
includes: options.includes || {},
|
|
547
|
+
engine: this
|
|
548
|
+
});
|
|
551
549
|
}
|
|
552
550
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
551
|
+
// FIX: Extract unified path resolution logic, supporting dot notation (e.g., 'partials.header')
|
|
552
|
+
_getTemplatePaths(templateName) {
|
|
553
|
+
const paths = [
|
|
554
|
+
path.join(this.options.templateDir, 'partials', `${templateName}.aether`),
|
|
555
|
+
path.join(this.options.templateDir, 'components', `${templateName}.aether`),
|
|
556
556
|
path.join(this.options.templateDir, 'pages', `${templateName}.aether`),
|
|
557
557
|
path.join(this.options.templateDir, `${templateName}.aether`),
|
|
558
558
|
path.join(this.options.templateDir, templateName),
|
|
559
559
|
templateName
|
|
560
560
|
];
|
|
561
|
+
|
|
562
|
+
// Support dot notation resolution: 'partials.header' -> 'partials/header.aether'
|
|
563
|
+
if (templateName.includes('.')) {
|
|
564
|
+
const dotPath = templateName.replace(/\./g, path.sep) + '.aether';
|
|
565
|
+
paths.unshift(path.join(this.options.templateDir, dotPath));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return paths;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async loadTemplate(templateName) {
|
|
572
|
+
if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) return this.options.templateCache.get(templateName);
|
|
573
|
+
|
|
574
|
+
const possiblePaths = this._getTemplatePaths(templateName);
|
|
561
575
|
for (const templatePath of possiblePaths) {
|
|
562
576
|
try {
|
|
563
577
|
const content = await fs.readFile(templatePath, 'utf-8');
|
|
@@ -568,6 +582,25 @@ class AetherEngine {
|
|
|
568
582
|
throw new Error(`Template not found: ${templateName}`);
|
|
569
583
|
}
|
|
570
584
|
|
|
585
|
+
// FIX: Add synchronous loading method specifically for runtime @include usage
|
|
586
|
+
loadTemplateSync(templateName) {
|
|
587
|
+
if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) {
|
|
588
|
+
return this.options.templateCache.get(templateName);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const possiblePaths = this._getTemplatePaths(templateName);
|
|
592
|
+
for (const templatePath of possiblePaths) {
|
|
593
|
+
try {
|
|
594
|
+
if (fs.existsSync(templatePath)) {
|
|
595
|
+
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
596
|
+
if (this.options.cacheEnabled) this.options.templateCache.set(templateName, content);
|
|
597
|
+
return content;
|
|
598
|
+
}
|
|
599
|
+
} catch (error) {}
|
|
600
|
+
}
|
|
601
|
+
throw new Error(`Template not found: ${templateName}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
571
604
|
clearCache() { this.options.compileCache.clear(); this.options.templateCache.clear(); }
|
|
572
605
|
|
|
573
606
|
getMetadata() {
|