@aetherframework/template-engine 1.0.0 → 1.0.2

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.
@@ -13,40 +13,95 @@
13
13
  import fs from 'fs-extra';
14
14
  import path from 'path';
15
15
  import crypto from 'crypto';
16
+ import CompressionEngine from './CompressionEngine.js';
16
17
 
17
18
  class AetherEngine {
18
- constructor(options = {}) {
19
- this.name = 'aether';
20
- this.version = '1.2.0';
21
- this.initialized = false;
22
-
23
- this.options = {
24
- templateDir: options.templateDir || './templates',
25
- cacheEnabled: options.cacheEnabled !== false,
26
- cacheTTL: options.cacheTTL || 3600,
27
- compileCache: new Map(),
28
- templateCache: new Map(),
29
- debug: options.debug || false,
30
- csrfToken: options.csrfToken || '', // CSRF token support
31
- ...options
32
- };
33
-
34
- this.filters = new Map();
35
- this.functions = new Map();
36
- this.layouts = new Map();
37
- this.routes = {};
19
+ constructor(options = {}) {
20
+ this.name = 'aether';
21
+ this.version = '1.3.0';
22
+ this.initialized = false;
23
+
24
+ this.options = {
25
+ templateDir: options.templateDir || './templates',
26
+ cacheEnabled: options.cacheEnabled !== false,
27
+ cacheTTL: options.cacheTTL || 3600,
28
+ compileCache: new Map(),
29
+ templateCache: new Map(),
30
+ debug: options.debug || false,
31
+ csrfToken: options.csrfToken || '',
38
32
 
39
- this.ensureTemplateDir();
40
- this.registerDefaultFilters();
41
- this.registerDefaultFunctions();
42
- }
33
+ // Compression options
34
+ compressionEnabled: options.compressionEnabled !== false,
35
+ minifyHTML: options.minifyHTML !== false,
36
+ minifyCSS: options.minifyCSS !== false,
37
+ minifyJS: options.minifyJS !== false,
38
+ mangleJS: options.mangleJS || false,
39
+ removeComments: options.removeComments || false,
40
+ collapseWhitespace: options.collapseWhitespace || true,
41
+ cacheCompressed: options.cacheCompressed !== false,
42
+ ...options
43
+ };
44
+
45
+ // Initialize compression engine
46
+ this.compressionEngine = new CompressionEngine(this.options);
47
+
48
+ this.filters = new Map();
49
+ this.functions = new Map();
50
+ this.layouts = new Map();
51
+ this.routes = {};
52
+
53
+ this.ensureTemplateDir();
54
+ this.registerDefaultFilters();
55
+ this.registerDefaultFunctions();
56
+ }
43
57
 
44
58
  initialize() {
45
59
  if (this.initialized) return;
46
60
  console.log(`Aether Engine initialized (v${this.version})`);
47
61
  this.initialized = true;
48
62
  }
63
+ /**
64
+ * Process content with compression based on options
65
+ * @param {string} content - Content to process
66
+ * @param {Object} options - Processing options
67
+ * @returns {string} Processed content
68
+ */
69
+ processWithCompression(content, options = {}) {
70
+ if (!this.options.compressionEnabled) {
71
+ return content;
72
+ }
49
73
 
74
+ const processOptions = {
75
+ minifyHTML: this.options.minifyHTML,
76
+ minifyCSS: this.options.minifyCSS,
77
+ minifyJS: this.options.minifyJS,
78
+ mangleJS: this.options.mangleJS,
79
+ removeComments: this.options.removeComments,
80
+ collapseWhitespace: this.options.collapseWhitespace,
81
+ cacheCompressed: this.options.cacheCompressed,
82
+ ...options
83
+ };
84
+
85
+ return this.compressionEngine.processHTML(content, processOptions);
86
+ }
87
+
88
+ /**
89
+ * Clear compression cache
90
+ */
91
+ clearCompressionCache() {
92
+ if (this.compressionEngine) {
93
+ this.compressionEngine.clearCompressionCache();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get compression statistics
99
+ * @returns {Object} Compression statistics
100
+ */
101
+ getCompressionStats() {
102
+ return this.compressionEngine ? this.compressionEngine.getStats() : null;
103
+ }
104
+
50
105
  ensureTemplateDir() {
51
106
  const dirs = [
52
107
  this.options.templateDir,
@@ -64,12 +119,10 @@ class AetherEngine {
64
119
  this.filter('escape', (value) => this.escapeHtml(value));
65
120
  this.filter('json', (value) => JSON.stringify(value));
66
121
  this.filter('date', (value) => new Date(value).toISOString().split('T')[0]);
67
- // NEW: raw filter to bypass escaping
68
122
  this.filter('raw', (value) => value);
69
123
  }
70
124
 
71
125
  registerDefaultFunctions() {
72
- // Dynamic route function
73
126
  this.function('route', (name, params = {}) => {
74
127
  let url = this.routes[name] || `/${name}`;
75
128
  if (params && typeof params === 'object') {
@@ -84,60 +137,38 @@ class AetherEngine {
84
137
  this.function('auth', () => ({ check: () => true, user: { name: 'John Doe', email: 'john@example.com', id: 1 } }));
85
138
  this.function('env', (key, defaultValue = '') => process.env[key] || defaultValue);
86
139
  this.function('url', (urlPath) => `${process.env.BASE_URL || 'http://localhost:3000'}${urlPath.startsWith('/') ? urlPath : '/' + urlPath}`);
87
-
88
- // NEW: CSRF Token generator
89
140
  this.function('csrf', () => `<input type="hidden" name="_csrf" value="${this.options.csrfToken}">`);
90
141
 
91
- // NEW: Old input retrieval (for form validation fallbacks)
92
142
  this.function('old', (field, defaultValue = '') => {
93
- return `__get('old.${field}') || ${JSON.stringify(defaultValue)}`; // Handled dynamically in parser
143
+ return `__get('old.${field}') || ${JSON.stringify(defaultValue)}`;
94
144
  });
95
145
 
96
- // NEW: Dynamic Class builder (e.g., @class(['btn', 'active' => isActive]))
97
146
  this.function('class', (classes) => {
98
147
  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(' ');
148
+ return classes.filter(c => typeof c === 'string').join(' ');
103
149
  }
104
150
  return '';
105
151
  });
106
152
 
107
- // NEW: Pagination HTML Generator
108
153
  this.function('pagination', (pager) => {
109
154
  if (!pager || !pager.total || pager.total <= 1) return '';
110
155
  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
- }
156
+ if (pager.current > 1) html += `<a href="${pager.baseUrl}?page=${pager.current - 1}" class="page-link prev">&laquo; Prev</a>`;
157
+ else html += `<span class="page-link disabled">&laquo; Prev</span>`;
118
158
 
119
- // Pages (Simple windowing logic)
120
159
  const start = Math.max(1, pager.current - 2);
121
160
  const end = Math.min(pager.total, pager.current + 2);
122
-
123
161
  if (start > 1) html += `<a href="${pager.baseUrl}?page=1" class="page-link">1</a><span class="dots">...</span>`;
124
162
 
125
163
  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
- }
164
+ if (i === pager.current) html += `<span class="page-link active">${i}</span>`;
165
+ else html += `<a href="${pager.baseUrl}?page=${i}" class="page-link">${i}</a>`;
131
166
  }
132
167
 
133
168
  if (end < pager.total) html += `<span class="dots">...</span><a href="${pager.baseUrl}?page=${pager.total}" class="page-link">${pager.total}</a>`;
134
169
 
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
- }
170
+ if (pager.current < pager.total) html += `<a href="${pager.baseUrl}?page=${pager.current + 1}" class="page-link next">Next &raquo;</a>`;
171
+ else html += `<span class="page-link disabled">Next &raquo;</span>`;
141
172
 
142
173
  html += '</nav>';
143
174
  return html;
@@ -161,7 +192,6 @@ class AetherEngine {
161
192
  let cursor = 0;
162
193
 
163
194
  const parenRegex = '\\(((?:[^()]+|\\([^()]*\\))*)\\)';
164
- // Added {!! !!}, @push, @endpush, @stack, @csrf
165
195
  const tokenRegex = new RegExp(
166
196
  `(\\{!!.*?!!\\}|\\{\\{.*?\\}\\}|@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
197
  'gs'
@@ -187,14 +217,12 @@ class AetherEngine {
187
217
  cursor = match.index + match[0].length;
188
218
  const token = match[0].trim();
189
219
 
190
- // 1. Raw Output {!! ... !!}
191
220
  if (token.startsWith('{!!') && token.endsWith('!!}')) {
192
221
  const expr = token.slice(3, -3).trim();
193
222
  if (inSectionBlock) sectionContent += token;
194
223
  else if (inStackBlock) stackContent += token;
195
- else jsCode += `__output.push(${this.parseExpression(expr, true)});\n`; // true = isRaw
196
- }
197
- // 2. Escaped Output {{ ... }}
224
+ else jsCode += `__output.push(${this.parseExpression(expr, true)});\n`;
225
+ }
198
226
  else if (token.startsWith('{{') && token.endsWith('}}')) {
199
227
  const expr = token.slice(2, -2).trim();
200
228
  if (inSectionBlock) sectionContent += token;
@@ -273,7 +301,6 @@ class AetherEngine {
273
301
  else if (inStackBlock) stackContent += token;
274
302
  else jsCode += `}\n__scope = __parentScope;\n}\n`;
275
303
  }
276
- // 3. Stacks (@push / @endpush / @stack)
277
304
  else if (token.startsWith('@push')) {
278
305
  const pushMatch = token.match(/@push\s*\(\s*'([^']+)'\s*\)/);
279
306
  if (pushMatch) {
@@ -305,13 +332,11 @@ class AetherEngine {
305
332
  else jsCode += `if(__stacks['${stackMatch[1]}']) { __stacks['${stackMatch[1]}'].forEach(fn => __output.push(fn(__scope, helpers))); }\n`;
306
333
  }
307
334
  }
308
- // 4. CSRF
309
335
  else if (token === '@csrf') {
310
336
  if (inSectionBlock) sectionContent += token;
311
337
  else if (inStackBlock) stackContent += token;
312
338
  else jsCode += `__output.push(__functions.get('csrf')());\n`;
313
339
  }
314
- // 5. Yields & Sections
315
340
  else if (token.startsWith('@yield')) {
316
341
  const yieldMatch = token.match(/@yield\s*\(\s*'([^']+)'(?:\s*,\s*'([^']*)')?\s*\)/);
317
342
  if (yieldMatch) {
@@ -343,12 +368,17 @@ class AetherEngine {
343
368
  inSectionBlock = false; currentSectionName = ''; sectionContent = '';
344
369
  }
345
370
  }
371
+ // FIX: Enhance @include syntax to support passing data parameters
346
372
  else if (token.startsWith('@include')) {
347
- const includeMatch = token.match(/@include\s*\(\s*'([^']+)'\s*\)/);
373
+ const includeMatch = token.match(/@include\s*\(\s*'([^']+)'\s*(?:,\s*([\s\S]*?))?\s*\)/);
348
374
  if (includeMatch) {
375
+ const viewName = includeMatch[1];
376
+ // Default to empty object if no params; otherwise evaluate as JS expression
377
+ const dataExpr = includeMatch[2] ? includeMatch[2].trim() : '{}';
378
+
349
379
  if (inSectionBlock) sectionContent += token;
350
380
  else if (inStackBlock) stackContent += token;
351
- else jsCode += `__output.push(__include('${includeMatch[1]}'));\n`;
381
+ else jsCode += `__output.push(__include('${viewName}', ${dataExpr}));\n`;
352
382
  }
353
383
  }
354
384
  else if (token.startsWith('@extends')) {
@@ -371,7 +401,6 @@ class AetherEngine {
371
401
  return __output.join('');
372
402
  };\n`;
373
403
  } else if (inStackBlock) {
374
- // Handle unclosed push block gracefully
375
404
  stackContent += remainingText;
376
405
  } else {
377
406
  jsCode += `__output.push(${JSON.stringify(remainingText)});\n`;
@@ -392,8 +421,28 @@ class AetherEngine {
392
421
  }
393
422
  return value;
394
423
  }
395
- function __include(name) {
396
- if (helpers.includes && helpers.includes[name]) return helpers.includes[name](__scope, helpers);
424
+
425
+ // FIX: Fully implement __include to support synchronous loading, compiling, and rendering of sub-templates from disk
426
+ function __include(name, includeData = {}) {
427
+ // 1. Merge current scope with passed include data
428
+ const mergedScope = Object.assign({}, __scope, includeData);
429
+
430
+ // 2. Prioritize pre-compiled includes (passed via render options)
431
+ if (helpers.includes && helpers.includes[name]) {
432
+ return helpers.includes[name](mergedScope, helpers);
433
+ }
434
+
435
+ // 3. Synchronously load and compile template from disk
436
+ if (helpers.engine) {
437
+ try {
438
+ const content = helpers.engine.loadTemplateSync(name);
439
+ const compiled = helpers.engine.compile(content);
440
+ return compiled(mergedScope, helpers);
441
+ } catch (e) {
442
+ if (helpers.engine.options.debug) console.error('[AetherEngine] Include error:', e.message);
443
+ return ''; // Fail silently or return empty in production
444
+ }
445
+ }
397
446
  return '';
398
447
  }
399
448
  `;
@@ -431,7 +480,6 @@ class AetherEngine {
431
480
  return `__get('${identifier}')`;
432
481
  });
433
482
 
434
- // Apply escaping only if not raw
435
483
  if (!isRaw) {
436
484
  if (/^__get\('[^']+'\)$/.test(parsed)) return `__escape(${parsed})`;
437
485
  if (!parsed.includes('__functions') && !parsed.includes('__filters') && !parsed.includes('__get(')) return `__escape(${parsed})`;
@@ -440,7 +488,6 @@ class AetherEngine {
440
488
  return parsed;
441
489
  }
442
490
 
443
- // ... (parseFunctionArguments, parseCondition, parseForeachExpression remain exactly the same) ...
444
491
  parseFunctionArguments(argsString) {
445
492
  const args = []; let currentArg = '', inString = false, stringChar = '';
446
493
  for (let i = 0; i < argsString.length; i++) {
@@ -502,7 +549,7 @@ class AetherEngine {
502
549
  const renderFunc = new Function('data', 'helpers', `
503
550
  const __output = [];
504
551
  const __sections = {};
505
- const __stacks = {}; // NEW: Stack storage for @push/@stack
552
+ const __stacks = {};
506
553
  const __escape = helpers.filters.get('escape') || function(s){ return s; };
507
554
  const __filters = helpers.filters;
508
555
  const __functions = helpers.functions;
@@ -520,44 +567,117 @@ class AetherEngine {
520
567
  }
521
568
  }
522
569
 
523
- async render(templateName, data = {}, options = {}) {
524
- if (!this.initialized) this.initialize();
525
- let templateContent = templateName;
526
-
527
- const isFilePath = typeof templateName === 'string' && !templateName.includes('\n') && !templateName.includes('<') && (templateName.endsWith('.aether') || templateName.endsWith('.html') || templateName.includes('/') || templateName.includes('\\'));
528
- if (isFilePath) templateContent = await this.loadTemplate(templateName);
529
-
530
- const extendsMatch = templateContent.match(/@extends\s*\(\s*'([^']+)'\s*\)/);
531
- if (extendsMatch) {
532
- const layoutName = extendsMatch[1];
533
- try {
534
- const layoutContent = await this.loadTemplate(layoutName);
535
- const sections = {};
536
- const sectionRegex = /@section\s*\(\s*'([^']+)'\s*\)([\s\S]*?)@endsection/g;
537
- let sectionMatch;
538
- while ((sectionMatch = sectionRegex.exec(templateContent)) !== null) sections[sectionMatch[1]] = sectionMatch[2].trim();
539
-
540
- const inlineSectionRegex = /@section\s*\(\s*'([^']+)'\s*,\s*'([^']*)'\s*\)/g;
541
- let inlineMatch;
542
- while ((inlineMatch = inlineSectionRegex.exec(templateContent)) !== null) sections[inlineMatch[1]] = inlineMatch[2];
543
-
544
- const yieldRegex = /@yield\s*\(\s*'([^']+)'(?:\s*,\s*'([^']*)')?\s*\)/g;
545
- templateContent = layoutContent.replace(yieldRegex, (match, name, defaultValue) => sections[name] !== undefined ? sections[name] : (defaultValue || ''));
546
- } catch (error) { console.warn(`Warning: Could not load layout '${layoutName}':`, error.message); }
570
+ async render(templateName, data = {}, options = {}) {
571
+ if (!this.initialized) this.initialize();
572
+ let templateContent = templateName;
573
+
574
+ const isFilePath = typeof templateName === 'string' && !templateName.includes('\n') && !templateName.includes('<') && (templateName.endsWith('.aether') || templateName.endsWith('.html') || templateName.includes('/') || templateName.includes('\\'));
575
+ if (isFilePath) templateContent = await this.loadTemplate(templateName);
576
+
577
+ // 【修复1】:恢复正确的正则表达式 \(\)
578
+ const extendsMatch = templateContent.match(/@extends\s*\(\s*'([^']+)'\s*\)/);
579
+ if (extendsMatch) {
580
+ // 【修复2】:恢复正确的匹配组索引 [1]
581
+ const layoutName = extendsMatch[1];
582
+ try {
583
+ const layoutContent = await this.loadTemplate(layoutName);
584
+ const sections = {};
585
+
586
+ // 【修复1】:恢复正确的正则表达式
587
+ const sectionRegex = /@section\s*\(\s*'([^']+)'\s*\)([\s\S]*?)@endsection/g;
588
+ let sectionMatch;
589
+ while ((sectionMatch = sectionRegex.exec(templateContent)) !== null) {
590
+ // 【修复2】:恢复正确的匹配组索引 [1] 和 [2]
591
+ sections[sectionMatch[1]] = sectionMatch[2].trim();
592
+ }
593
+
594
+ // 【修复1】:恢复正确的正则表达式
595
+ const inlineSectionRegex = /@section\s*\(\s*'([^']+)'\s*,\s*'([^']*)'\s*\)/g;
596
+ let inlineMatch;
597
+ while ((inlineMatch = inlineSectionRegex.exec(templateContent)) !== null) {
598
+ // 【修复2】:恢复正确的匹配组索引 [1] 和 [2]
599
+ sections[inlineMatch[1]] = inlineMatch[2];
600
+ }
601
+
602
+ // 【修复1】:恢复正确的正则表达式
603
+ const yieldRegex = /@yield\s*\(\s*'([^']+)'(?:\s*,\s*'([^']*)')?\s*\)/g;
604
+ templateContent = layoutContent.replace(yieldRegex, (match, name, defaultValue) =>
605
+ sections[name] !== undefined ? sections[name] : (defaultValue || '')
606
+ );
607
+ } catch (error) {
608
+ console.warn(`Warning: Could not load layout '${layoutName}':`, error.message);
547
609
  }
610
+ }
611
+
612
+ const renderFunc = this.compile(templateContent);
613
+
614
+ // 【关键修复】: 保存渲染结果到变量,而不是直接 return,以便后续进行压缩处理
615
+ const renderedContent = renderFunc(data, {
616
+ filters: this.filters,
617
+ functions: this.functions,
618
+ includes: options.includes || {},
619
+ engine: this
620
+ });
621
+
622
+ // Apply compression if enabled
623
+ if (this.options.compressionEnabled) {
624
+ const compressionOptions = {
625
+ minifyHTML: this.options.minifyHTML,
626
+ minifyCSS: this.options.minifyCSS,
627
+ minifyJS: this.options.minifyJS,
628
+ mangleJS: this.options.mangleJS,
629
+ removeComments: this.options.removeComments,
630
+ collapseWhitespace: this.options.collapseWhitespace,
631
+ removeAttributeQuotes: this.options.removeAttributeQuotes,
632
+ removeEmptyAttributes: this.options.removeEmptyAttributes,
633
+ cacheCompressed: this.options.cacheCompressed,
634
+ ...options.compression // Allow per-render override
635
+ };
548
636
 
549
- const renderFunc = this.compile(templateContent);
550
- return renderFunc(data, { filters: this.filters, functions: this.functions, includes: options.includes || {} });
637
+ // Use the compression engine to process the rendered content
638
+ if (this.compressionEngine) {
639
+ const compressedContent = this.compressionEngine.processHTML(renderedContent, compressionOptions);
640
+
641
+ // Log compression statistics in debug mode
642
+ if (this.options.debug) {
643
+ const originalSize = renderedContent.length;
644
+ const compressedSize = compressedContent.length;
645
+ const reduction = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
646
+ console.log(`[AetherEngine] Compression applied: ${originalSize} → ${compressedSize} bytes (${reduction}% reduction)`);
647
+ }
648
+
649
+ return compressedContent;
650
+ }
551
651
  }
652
+
653
+ return renderedContent;
654
+ }
552
655
 
553
- async loadTemplate(templateName) {
554
- if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) return this.options.templateCache.get(templateName);
555
- const possiblePaths = [
656
+
657
+ // FIX: Extract unified path resolution logic, supporting dot notation (e.g., 'partials.header')
658
+ _getTemplatePaths(templateName) {
659
+ const paths = [
660
+ path.join(this.options.templateDir, 'partials', `${templateName}.aether`),
661
+ path.join(this.options.templateDir, 'components', `${templateName}.aether`),
556
662
  path.join(this.options.templateDir, 'pages', `${templateName}.aether`),
557
663
  path.join(this.options.templateDir, `${templateName}.aether`),
558
664
  path.join(this.options.templateDir, templateName),
559
665
  templateName
560
666
  ];
667
+
668
+ // Support dot notation resolution: 'partials.header' -> 'partials/header.aether'
669
+ if (templateName.includes('.')) {
670
+ const dotPath = templateName.replace(/\./g, path.sep) + '.aether';
671
+ paths.unshift(path.join(this.options.templateDir, dotPath));
672
+ }
673
+
674
+ return paths;
675
+ }
676
+
677
+ async loadTemplate(templateName) {
678
+ if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) return this.options.templateCache.get(templateName);
679
+
680
+ const possiblePaths = this._getTemplatePaths(templateName);
561
681
  for (const templatePath of possiblePaths) {
562
682
  try {
563
683
  const content = await fs.readFile(templatePath, 'utf-8');
@@ -568,6 +688,25 @@ class AetherEngine {
568
688
  throw new Error(`Template not found: ${templateName}`);
569
689
  }
570
690
 
691
+ // FIX: Add synchronous loading method specifically for runtime @include usage
692
+ loadTemplateSync(templateName) {
693
+ if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) {
694
+ return this.options.templateCache.get(templateName);
695
+ }
696
+
697
+ const possiblePaths = this._getTemplatePaths(templateName);
698
+ for (const templatePath of possiblePaths) {
699
+ try {
700
+ if (fs.existsSync(templatePath)) {
701
+ const content = fs.readFileSync(templatePath, 'utf-8');
702
+ if (this.options.cacheEnabled) this.options.templateCache.set(templateName, content);
703
+ return content;
704
+ }
705
+ } catch (error) {}
706
+ }
707
+ throw new Error(`Template not found: ${templateName}`);
708
+ }
709
+
571
710
  clearCache() { this.options.compileCache.clear(); this.options.templateCache.clear(); }
572
711
 
573
712
  getMetadata() {