@aetherframework/template-engine 1.0.1 → 1.0.5
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 +430 -0
- package/index.js +17 -1
- package/package.json +1 -1
- package/src/core/ModeManager.js +0 -3
- package/src/core/TemplateEngineFactory.js +0 -10
- package/src/engines/AetherEngine.js +118 -48
- package/src/engines/CompressionEngine.js +642 -0
- package/src/engines/SSRModeEngine.js +29 -4
- package/src/engines/TemplateModeEngine.js +29 -7
- package/src/examples/basic-usage.js +217 -0
- package/src/examples/layout-example.js +404 -0
- package/src/examples/ssr-example.js +180 -0
- package/src/utils/ConfigLoader.js +2 -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,11 +13,12 @@
|
|
|
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
|
constructor(options = {}) {
|
|
19
20
|
this.name = 'aether';
|
|
20
|
-
this.version = '1.3.0';
|
|
21
|
+
this.version = '1.3.0';
|
|
21
22
|
this.initialized = false;
|
|
22
23
|
|
|
23
24
|
this.options = {
|
|
@@ -27,14 +28,27 @@ class AetherEngine {
|
|
|
27
28
|
compileCache: new Map(),
|
|
28
29
|
templateCache: new Map(),
|
|
29
30
|
debug: options.debug || false,
|
|
30
|
-
csrfToken: options.csrfToken || '',
|
|
31
|
+
csrfToken: options.csrfToken || '',
|
|
32
|
+
|
|
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,
|
|
31
42
|
...options
|
|
32
43
|
};
|
|
33
44
|
|
|
45
|
+
// Initialize compression engine
|
|
46
|
+
this.compressionEngine = new CompressionEngine(this.options);
|
|
47
|
+
|
|
34
48
|
this.filters = new Map();
|
|
35
49
|
this.functions = new Map();
|
|
36
50
|
this.layouts = new Map();
|
|
37
|
-
this.routes = {};
|
|
51
|
+
this.routes = {};
|
|
38
52
|
|
|
39
53
|
this.ensureTemplateDir();
|
|
40
54
|
this.registerDefaultFilters();
|
|
@@ -43,10 +57,37 @@ class AetherEngine {
|
|
|
43
57
|
|
|
44
58
|
initialize() {
|
|
45
59
|
if (this.initialized) return;
|
|
46
|
-
console.log(`Aether Engine initialized (v${this.version})`);
|
|
47
60
|
this.initialized = true;
|
|
48
61
|
}
|
|
49
|
-
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process content with compression based on options
|
|
65
|
+
*/
|
|
66
|
+
processWithCompression(content, options = {}) {
|
|
67
|
+
if (!this.options.compressionEnabled) return content;
|
|
68
|
+
|
|
69
|
+
const processOptions = {
|
|
70
|
+
minifyHTML: this.options.minifyHTML,
|
|
71
|
+
minifyCSS: this.options.minifyCSS,
|
|
72
|
+
minifyJS: this.options.minifyJS,
|
|
73
|
+
mangleJS: this.options.mangleJS,
|
|
74
|
+
removeComments: this.options.removeComments,
|
|
75
|
+
collapseWhitespace: this.options.collapseWhitespace,
|
|
76
|
+
cacheCompressed: this.options.cacheCompressed,
|
|
77
|
+
...options
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return this.compressionEngine.processHTML(content, processOptions);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
clearCompressionCache() {
|
|
84
|
+
if (this.compressionEngine) this.compressionEngine.clearCompressionCache();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getCompressionStats() {
|
|
88
|
+
return this.compressionEngine ? this.compressionEngine.getStats() : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
50
91
|
ensureTemplateDir() {
|
|
51
92
|
const dirs = [
|
|
52
93
|
this.options.templateDir,
|
|
@@ -89,9 +130,7 @@ class AetherEngine {
|
|
|
89
130
|
});
|
|
90
131
|
|
|
91
132
|
this.function('class', (classes) => {
|
|
92
|
-
if (Array.isArray(classes))
|
|
93
|
-
return classes.filter(c => typeof c === 'string').join(' ');
|
|
94
|
-
}
|
|
133
|
+
if (Array.isArray(classes)) return classes.filter(c => typeof c === 'string').join(' ');
|
|
95
134
|
return '';
|
|
96
135
|
});
|
|
97
136
|
|
|
@@ -313,12 +352,10 @@ class AetherEngine {
|
|
|
313
352
|
inSectionBlock = false; currentSectionName = ''; sectionContent = '';
|
|
314
353
|
}
|
|
315
354
|
}
|
|
316
|
-
// FIX: Enhance @include syntax to support passing data parameters
|
|
317
355
|
else if (token.startsWith('@include')) {
|
|
318
356
|
const includeMatch = token.match(/@include\s*\(\s*'([^']+)'\s*(?:,\s*([\s\S]*?))?\s*\)/);
|
|
319
357
|
if (includeMatch) {
|
|
320
358
|
const viewName = includeMatch[1];
|
|
321
|
-
// Default to empty object if no params; otherwise evaluate as JS expression
|
|
322
359
|
const dataExpr = includeMatch[2] ? includeMatch[2].trim() : '{}';
|
|
323
360
|
|
|
324
361
|
if (inSectionBlock) sectionContent += token;
|
|
@@ -367,17 +404,22 @@ class AetherEngine {
|
|
|
367
404
|
return value;
|
|
368
405
|
}
|
|
369
406
|
|
|
370
|
-
// FIX
|
|
407
|
+
// [CRITICAL FIX] Preserve prototype chain to prevent losing root data variables (like langUrls, t, etc.)
|
|
408
|
+
// Object.assign only copies own enumerable properties, ignoring the prototype chain where
|
|
409
|
+
// the original 'data' object properties reside.
|
|
371
410
|
function __include(name, includeData = {}) {
|
|
372
|
-
// 1.
|
|
373
|
-
const mergedScope = Object.
|
|
411
|
+
// 1. Create a new scope that inherits from the current scope's prototype (the original data)
|
|
412
|
+
const mergedScope = Object.create(Object.getPrototypeOf(__scope));
|
|
374
413
|
|
|
375
|
-
// 2.
|
|
414
|
+
// 2. Copy own properties from current scope (e.g., loop variables) and includeData
|
|
415
|
+
Object.assign(mergedScope, __scope, includeData);
|
|
416
|
+
|
|
417
|
+
// 3. Prioritize pre-compiled includes (passed via render options)
|
|
376
418
|
if (helpers.includes && helpers.includes[name]) {
|
|
377
419
|
return helpers.includes[name](mergedScope, helpers);
|
|
378
420
|
}
|
|
379
421
|
|
|
380
|
-
//
|
|
422
|
+
// 4. Synchronously load and compile template from disk
|
|
381
423
|
if (helpers.engine) {
|
|
382
424
|
try {
|
|
383
425
|
const content = helpers.engine.loadTemplateSync(name);
|
|
@@ -489,8 +531,6 @@ class AetherEngine {
|
|
|
489
531
|
let jsCode = '';
|
|
490
532
|
try {
|
|
491
533
|
jsCode = this.enhancedConvertToJsCode(templateContent);
|
|
492
|
-
if (this.options.debug) console.log('\n--- Generated JS Code ---\n' + jsCode + '\n-------------------------\n');
|
|
493
|
-
|
|
494
534
|
const renderFunc = new Function('data', 'helpers', `
|
|
495
535
|
const __output = [];
|
|
496
536
|
const __sections = {};
|
|
@@ -519,36 +559,74 @@ class AetherEngine {
|
|
|
519
559
|
const isFilePath = typeof templateName === 'string' && !templateName.includes('\n') && !templateName.includes('<') && (templateName.endsWith('.aether') || templateName.endsWith('.html') || templateName.includes('/') || templateName.includes('\\'));
|
|
520
560
|
if (isFilePath) templateContent = await this.loadTemplate(templateName);
|
|
521
561
|
|
|
562
|
+
// [FIX] Correct regex and match groups for layout inheritance
|
|
522
563
|
const extendsMatch = templateContent.match(/@extends\s*\(\s*'([^']+)'\s*\)/);
|
|
523
564
|
if (extendsMatch) {
|
|
524
|
-
const layoutName = extendsMatch[1];
|
|
565
|
+
const layoutName = extendsMatch[1];
|
|
525
566
|
try {
|
|
526
567
|
const layoutContent = await this.loadTemplate(layoutName);
|
|
527
568
|
const sections = {};
|
|
569
|
+
|
|
528
570
|
const sectionRegex = /@section\s*\(\s*'([^']+)'\s*\)([\s\S]*?)@endsection/g;
|
|
529
571
|
let sectionMatch;
|
|
530
|
-
while ((sectionMatch = sectionRegex.exec(templateContent)) !== null)
|
|
572
|
+
while ((sectionMatch = sectionRegex.exec(templateContent)) !== null) {
|
|
573
|
+
sections[sectionMatch[1]] = sectionMatch[2].trim();
|
|
574
|
+
}
|
|
531
575
|
|
|
532
576
|
const inlineSectionRegex = /@section\s*\(\s*'([^']+)'\s*,\s*'([^']*)'\s*\)/g;
|
|
533
577
|
let inlineMatch;
|
|
534
|
-
while ((inlineMatch = inlineSectionRegex.exec(templateContent)) !== null)
|
|
578
|
+
while ((inlineMatch = inlineSectionRegex.exec(templateContent)) !== null) {
|
|
579
|
+
sections[inlineMatch[1]] = inlineMatch[2];
|
|
580
|
+
}
|
|
535
581
|
|
|
536
582
|
const yieldRegex = /@yield\s*\(\s*'([^']+)'(?:\s*,\s*'([^']*)')?\s*\)/g;
|
|
537
|
-
templateContent = layoutContent.replace(yieldRegex, (match, name, defaultValue) =>
|
|
538
|
-
|
|
583
|
+
templateContent = layoutContent.replace(yieldRegex, (match, name, defaultValue) =>
|
|
584
|
+
sections[name] !== undefined ? sections[name] : (defaultValue || '')
|
|
585
|
+
);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
console.warn(`Warning: Could not load layout '${layoutName}':`, error.message);
|
|
588
|
+
}
|
|
539
589
|
}
|
|
540
590
|
|
|
541
591
|
const renderFunc = this.compile(templateContent);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
592
|
+
|
|
593
|
+
const renderedContent = renderFunc(data, {
|
|
594
|
+
filters: this.filters,
|
|
595
|
+
functions: this.functions,
|
|
596
|
+
includes: options.includes || {},
|
|
597
|
+
engine: this
|
|
548
598
|
});
|
|
599
|
+
|
|
600
|
+
if (this.options.compressionEnabled) {
|
|
601
|
+
const compressionOptions = {
|
|
602
|
+
minifyHTML: this.options.minifyHTML,
|
|
603
|
+
minifyCSS: this.options.minifyCSS,
|
|
604
|
+
minifyJS: this.options.minifyJS,
|
|
605
|
+
mangleJS: this.options.mangleJS,
|
|
606
|
+
removeComments: this.options.removeComments,
|
|
607
|
+
collapseWhitespace: this.options.collapseWhitespace,
|
|
608
|
+
removeAttributeQuotes: this.options.removeAttributeQuotes,
|
|
609
|
+
removeEmptyAttributes: this.options.removeEmptyAttributes,
|
|
610
|
+
cacheCompressed: this.options.cacheCompressed,
|
|
611
|
+
...options.compression
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
if (this.compressionEngine) {
|
|
615
|
+
const compressedContent = this.compressionEngine.processHTML(renderedContent, compressionOptions);
|
|
616
|
+
|
|
617
|
+
if (this.options.debug) {
|
|
618
|
+
const originalSize = renderedContent.length;
|
|
619
|
+
const compressedSize = compressedContent.length;
|
|
620
|
+
const reduction = ((originalSize - compressedSize) / originalSize * 100).toFixed(2);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return compressedContent;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return renderedContent;
|
|
549
628
|
}
|
|
550
629
|
|
|
551
|
-
// FIX: Extract unified path resolution logic, supporting dot notation (e.g., 'partials.header')
|
|
552
630
|
_getTemplatePaths(templateName) {
|
|
553
631
|
const paths = [
|
|
554
632
|
path.join(this.options.templateDir, 'partials', `${templateName}.aether`),
|
|
@@ -559,7 +637,6 @@ class AetherEngine {
|
|
|
559
637
|
templateName
|
|
560
638
|
];
|
|
561
639
|
|
|
562
|
-
// Support dot notation resolution: 'partials.header' -> 'partials/header.aether'
|
|
563
640
|
if (templateName.includes('.')) {
|
|
564
641
|
const dotPath = templateName.replace(/\./g, path.sep) + '.aether';
|
|
565
642
|
paths.unshift(path.join(this.options.templateDir, dotPath));
|
|
@@ -582,34 +659,27 @@ class AetherEngine {
|
|
|
582
659
|
throw new Error(`Template not found: ${templateName}`);
|
|
583
660
|
}
|
|
584
661
|
|
|
585
|
-
|
|
662
|
+
/**
|
|
663
|
+
* [FIX] Synchronously load template from disk for runtime @include usage
|
|
664
|
+
*/
|
|
586
665
|
loadTemplateSync(templateName) {
|
|
587
666
|
if (this.options.cacheEnabled && this.options.templateCache.has(templateName)) {
|
|
588
|
-
|
|
667
|
+
return this.options.templateCache.get(templateName);
|
|
589
668
|
}
|
|
590
669
|
|
|
591
670
|
const possiblePaths = this._getTemplatePaths(templateName);
|
|
592
671
|
for (const templatePath of possiblePaths) {
|
|
593
672
|
try {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
673
|
+
// Use readFileSync for synchronous loading required by __include
|
|
674
|
+
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
675
|
+
if (this.options.cacheEnabled) this.options.templateCache.set(templateName, content);
|
|
676
|
+
return content;
|
|
677
|
+
} catch (error) {
|
|
678
|
+
// Ignore and try next path
|
|
679
|
+
}
|
|
600
680
|
}
|
|
601
681
|
throw new Error(`Template not found: ${templateName}`);
|
|
602
682
|
}
|
|
603
|
-
|
|
604
|
-
clearCache() { this.options.compileCache.clear(); this.options.templateCache.clear(); }
|
|
605
|
-
|
|
606
|
-
getMetadata() {
|
|
607
|
-
return {
|
|
608
|
-
name: this.name, version: this.version, initialized: this.initialized,
|
|
609
|
-
filters: Array.from(this.filters.keys()), functions: Array.from(this.functions.keys()),
|
|
610
|
-
layouts: Array.from(this.layouts.keys()), cacheEnabled: this.options.cacheEnabled, templateDir: this.options.templateDir
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
683
|
}
|
|
614
684
|
|
|
615
685
|
export default AetherEngine;
|