@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aetherframework/template-engine",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A lightweight, high-performance template engine with SSR support and custom syntax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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.2.0';
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 || '', // CSRF token support
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)}`; // Handled dynamically in parser
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
- // Prev
113
- if (pager.current > 1) {
114
- html += `<a href="${pager.baseUrl}?page=${pager.current - 1}" class="page-link prev">&laquo; Prev</a>`;
115
- } else {
116
- html += `<span class="page-link disabled">&laquo; Prev</span>`;
117
- }
101
+ if (pager.current > 1) html += `<a href="${pager.baseUrl}?page=${pager.current - 1}" class="page-link prev">&laquo; Prev</a>`;
102
+ else html += `<span class="page-link disabled">&laquo; 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
- html += `<span class="page-link active">${i}</span>`;
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
- // Next
136
- if (pager.current < pager.total) {
137
- html += `<a href="${pager.baseUrl}?page=${pager.current + 1}" class="page-link next">Next &raquo;</a>`;
138
- } else {
139
- html += `<span class="page-link disabled">Next &raquo;</span>`;
140
- }
115
+ if (pager.current < pager.total) html += `<a href="${pager.baseUrl}?page=${pager.current + 1}" class="page-link next">Next &raquo;</a>`;
116
+ else html += `<span class="page-link disabled">Next &raquo;</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`; // true = isRaw
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('${includeMatch[1]}'));\n`;
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
- function __include(name) {
396
- if (helpers.includes && helpers.includes[name]) return helpers.includes[name](__scope, helpers);
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 = {}; // NEW: Stack storage for @push/@stack
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
- return renderFunc(data, { filters: this.filters, functions: this.functions, includes: options.includes || {} });
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
- async loadTemplate(templateName) {
554
- if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) return this.options.templateCache.get(templateName);
555
- const possiblePaths = [
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() {