@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.
@@ -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'; // Version upgraded
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: Fully implement __include to support synchronous loading, compiling, and rendering of sub-templates from disk
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. Merge current scope with passed include data
373
- const mergedScope = Object.assign({}, __scope, includeData);
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. Prioritize pre-compiled includes (passed via render options)
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
- // 3. Synchronously load and compile template from disk
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) sections[sectionMatch[1]] = sectionMatch[2].trim();
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) sections[inlineMatch[1]] = inlineMatch[2];
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) => sections[name] !== undefined ? sections[name] : (defaultValue || ''));
538
- } catch (error) { console.warn(`Warning: Could not load layout '${layoutName}':`, error.message); }
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
- // 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
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
- // FIX: Add synchronous loading method specifically for runtime @include usage
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
- return this.options.templateCache.get(templateName);
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
- 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) {}
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;