@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.
- package/README.md +431 -1
- package/index.js +17 -1
- package/package.json +1 -1
- package/src/engines/AetherEngine.js +240 -101
- package/src/engines/CompressionEngine.js +642 -0
- package/src/engines/SSRModeEngine.js +29 -4
- package/src/engines/TemplateModeEngine.js +29 -3
- package/examples/dist/basic-usage-result.html +0 -31
- package/examples/dist/layout-example-result.html +0 -210
- package/examples/dist/templates/layouts/main.aether +0 -58
- package/examples/dist/templates/pages/home.aether +0 -116
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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)}`;
|
|
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
|
-
|
|
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
|
-
}
|
|
156
|
+
if (pager.current > 1) html += `<a href="${pager.baseUrl}?page=${pager.current - 1}" class="page-link prev">« Prev</a>`;
|
|
157
|
+
else html += `<span class="page-link disabled">« 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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
170
|
+
if (pager.current < pager.total) html += `<a href="${pager.baseUrl}?page=${pager.current + 1}" class="page-link next">Next »</a>`;
|
|
171
|
+
else html += `<span class="page-link disabled">Next »</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`;
|
|
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('${
|
|
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
|
-
|
|
396
|
-
|
|
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 = {};
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
550
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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() {
|