@aetherframework/template-engine 1.0.0

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.
@@ -0,0 +1,139 @@
1
+
2
+ /**
3
+ * @license MIT
4
+ * Copyright (c) 2026-present AetherFramework Contributors.
5
+ * SPDX-License-Identifier: MIT
6
+ * @module @aetherframework/template-engine/src/engines/SSrModeEngine
7
+ */
8
+
9
+ import AetherEngine from './AetherEngine.js'; // Adjust this path if AetherEngine is in a different folder
10
+
11
+ /**
12
+ * SSR Mode Engine - Server-Side Rendering engine for Aether templates
13
+ * Converts Aether templates to full HTML documents with SSR optimizations
14
+ */
15
+ class SSRModeEngine extends AetherEngine {
16
+ constructor(options = {}) {
17
+ super(options);
18
+ this.name = 'ssr-mode';
19
+ this.version = '1.0.0';
20
+
21
+ // SSR-specific options
22
+ this.ssrOptions = {
23
+ hydrate: options.hydrate !== false,
24
+ stream: options.stream || false,
25
+ ...options
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Render template with SSR optimizations
31
+ * @param {string} templateName - Template name or content
32
+ * @param {Object} data - Template data
33
+ * @param {Object} options - Render options
34
+ * @returns {Promise<string>} Rendered HTML with SSR enhancements
35
+ */
36
+ async render(templateName, data = {}, options = {}) {
37
+ // First, render the inner content using the parent Aether engine
38
+ const content = await super.render(templateName, data, options);
39
+
40
+ // Wrap and enhance for SSR
41
+ return this.enhanceForSSR(content, data, options);
42
+ }
43
+
44
+ /**
45
+ * Enhance rendered content for SSR by wrapping it in a full HTML document
46
+ * @param {string} content - Rendered HTML content
47
+ * @param {Object} data - Template data
48
+ * @param {Object} options - SSR options
49
+ * @returns {string} Enhanced HTML
50
+ * @private
51
+ */
52
+ enhanceForSSR(content, data, options) {
53
+ // Extract meta information from data with safe fallbacks
54
+ const title = data.title || 'Aether Framework';
55
+ const description = data.description || 'Next-generation lightweight full-stack solution';
56
+ const keywords = data.keywords || 'Aether,Framework,Web';
57
+
58
+ // Safely serialize data for hydration (prevent XSS in script tags)
59
+ const safeDataJson = JSON.stringify(data).replace(/</g, '\\u003c');
60
+
61
+ // Build full HTML document with SSR optimizations
62
+ return `<!DOCTYPE html>
63
+ <html lang="en">
64
+ <head>
65
+ <meta charset="UTF-8">
66
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
67
+ <title>${this.escapeHtml(title)}</title>
68
+ <meta name="description" content="${this.escapeHtml(description)}">
69
+ <meta name="keywords" content="${this.escapeHtml(keywords)}">
70
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
71
+ <script src="https://cdn.tailwindcss.com"></script>
72
+
73
+ ${data.head || ''}
74
+
75
+ <!-- SSR Hydration Data -->
76
+ <script type="application/json" id="ssr-data">
77
+ ${safeDataJson}
78
+ </script>
79
+
80
+ <!-- SSR Styles -->
81
+ <style>
82
+ body {
83
+ margin: 0;
84
+ padding: 0;
85
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
86
+ }
87
+ .ssr-hydrated { opacity: 1; transition: opacity 0.3s ease; }
88
+ </style>
89
+ </head>
90
+ <body class="antialiased bg-[#020617] text-white">
91
+ <div id="app" class="ssr-hydrated">
92
+ ${content}
93
+ </div>
94
+
95
+ ${data.scripts || ''}
96
+
97
+ <!-- SSR Hydration Script -->
98
+ <script>
99
+ (function() {
100
+ // Hydrate SSR data
101
+ const ssrData = document.getElementById('ssr-data');
102
+ if (ssrData) {
103
+ try {
104
+ window.__SSR_DATA__ = JSON.parse(ssrData.textContent);
105
+ } catch (e) {
106
+ console.error('Failed to parse SSR data', e);
107
+ }
108
+ }
109
+
110
+ // Mark as hydrated
111
+ document.addEventListener('DOMContentLoaded', function() {
112
+ const app = document.getElementById('app');
113
+ if (app) {
114
+ app.classList.add('ssr-hydrated');
115
+ }
116
+ });
117
+ })();
118
+ </script>
119
+ </body>
120
+ </html>`;
121
+ }
122
+
123
+ /**
124
+ * Get engine metadata
125
+ * @returns {Object} Engine information
126
+ */
127
+ getMetadata() {
128
+ const baseMetadata = super.getMetadata();
129
+ return {
130
+ ...baseMetadata,
131
+ mode: 'ssr',
132
+ hydrate: this.ssrOptions.hydrate,
133
+ stream: this.ssrOptions.stream
134
+ };
135
+ }
136
+ }
137
+
138
+ // Use 'export default' to match the import in your index.js
139
+ export default SSRModeEngine;
@@ -0,0 +1,320 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/template-engine/src/engines/TemplateModeEngine
6
+ */
7
+ /**
8
+ * Template Mode Engine - Template rendering engine for Aether templates
9
+ * Extends AetherEngine with template-specific functionality
10
+ */
11
+ import AetherEngine from './AetherEngine.js';
12
+
13
+ class TemplateModeEngine extends AetherEngine {
14
+ constructor(options = {}) {
15
+ super(options);
16
+ this.name = 'template-mode';
17
+ this.version = '1.0.0';
18
+
19
+ // Template mode specific options
20
+ this.templateOptions = {
21
+ layoutSupport: options.layoutSupport !== false,
22
+ includeSupport: options.includeSupport !== false,
23
+ cacheTemplates: options.cacheTemplates !== false,
24
+ ...options
25
+ };
26
+
27
+ // Initialize template directories
28
+ this.ensureTemplateDir();
29
+ }
30
+
31
+ /**
32
+ * Initialize the template engine
33
+ * @returns {Promise<TemplateModeEngine>} Initialized engine instance
34
+ */
35
+ async initialize() {
36
+ if (this.initialized) return this;
37
+
38
+ await super.initialize();
39
+
40
+ // Load default layout if exists
41
+ await this.loadDefaultLayout();
42
+
43
+ console.log(`✅ Template Mode Engine initialized (v${this.version})`);
44
+ this.initialized = true;
45
+ return this;
46
+ }
47
+
48
+ /**
49
+ * Load default layout from file
50
+ * @private
51
+ */
52
+ async loadDefaultLayout() {
53
+ try {
54
+ const defaultLayoutPath = `${this.options.templateDir}/layouts/default.aether`;
55
+ const fs = await import('fs-extra');
56
+
57
+ if (await fs.pathExists(defaultLayoutPath)) {
58
+ const layoutContent = await fs.readFile(defaultLayoutPath, 'utf-8');
59
+ this.registerLayout('default', layoutContent);
60
+ console.log('📄 Default layout loaded from file');
61
+ } else {
62
+ // Create default layout based on user's template
63
+ const defaultLayout = `<!DOCTYPE html>
64
+ <html lang="zh-CN">
65
+ <head>
66
+ <meta charset="UTF-8">
67
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
68
+ <title>@yield('title', 'Aether Framework')</title>
69
+ <meta name="description" content="@yield('description', '下一代轻量级全栈解决方案')">
70
+ <meta name="keywords" content="@yield('keywords', 'Aether,Framework,Web')">
71
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
72
+ <script src="https://cdn.tailwindcss.com"></script>
73
+ @yield('head')
74
+ </head>
75
+ <body class="antialiased bg-[#020617] text-white">
76
+ @yield('content')
77
+ @yield('scripts')
78
+ </body>
79
+ </html>`;
80
+
81
+ this.registerLayout('default', defaultLayout);
82
+
83
+ // Save to file for future use
84
+ await fs.ensureDir(`${this.options.templateDir}/layouts`);
85
+ await fs.writeFile(defaultLayoutPath, defaultLayout, 'utf-8');
86
+ console.log('📄 Default layout created and saved');
87
+ }
88
+ } catch (error) {
89
+ console.warn('⚠️ Could not load default layout:', error.message);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Render template with template mode enhancements
95
+ * @param {string} templateName - Template name or content
96
+ * @param {Object} data - Template data
97
+ * @param {Object} options - Render options
98
+ * @returns {Promise<string>} Rendered HTML
99
+ */
100
+ async render(templateName, data = {}, options = {}) {
101
+ if (!this.initialized) {
102
+ await this.initialize();
103
+ }
104
+
105
+ // Add template mode specific data
106
+ const enhancedData = {
107
+ ...data,
108
+ _mode: 'template',
109
+ _timestamp: new Date().toISOString(),
110
+ _engine: this.name
111
+ };
112
+
113
+ // Render using parent Aether engine
114
+ const content = await super.render(templateName, enhancedData, options);
115
+
116
+ // Add template mode specific enhancements
117
+ return this.enhanceForTemplateMode(content, enhancedData, options);
118
+ }
119
+
120
+ /**
121
+ * Enhance rendered content for template mode
122
+ * @param {string} content - Rendered HTML content
123
+ * @param {Object} data - Template data
124
+ * @param {Object} options - Template options
125
+ * @returns {string} Enhanced HTML
126
+ * @private
127
+ */
128
+ enhanceForTemplateMode(content, data, options) {
129
+ // Add template mode specific meta tags
130
+ const metaTags = `
131
+ <!-- Template Mode: ${this.name} -->
132
+ <!-- Rendered at: ${new Date().toISOString()} -->
133
+ <!-- Cache: ${this.options.cacheEnabled ? 'Enabled' : 'Disabled'} -->`;
134
+
135
+ // Insert meta tags before closing head tag
136
+ if (content.includes('</head>')) {
137
+ content = content.replace('</head>', `${metaTags}\n</head>`);
138
+ } else {
139
+ // If no head tag, add at the beginning
140
+ content = `${metaTags}\n${content}`;
141
+ }
142
+
143
+ // Add debug information if enabled
144
+ if (this.options.debug) {
145
+ const debugInfo = `
146
+ <!-- DEBUG INFO -->
147
+ <!-- Template: ${data._template || 'unknown'} -->
148
+ <!-- Data Keys: ${Object.keys(data).join(', ')} -->
149
+ <!-- Render Time: ${Date.now() - (data._startTime || Date.now())}ms -->`;
150
+
151
+ if (content.includes('</body>')) {
152
+ content = content.replace('</body>', `${debugInfo}\n</body>`);
153
+ } else {
154
+ content = `${content}\n${debugInfo}`;
155
+ }
156
+ }
157
+
158
+ return content;
159
+ }
160
+
161
+ /**
162
+ * Compile template for template mode
163
+ * @param {string} template - Template content
164
+ * @param {Object} options - Compile options
165
+ * @returns {Function} Compiled function
166
+ */
167
+ compile(template, options = {}) {
168
+ return super.compile(template, options);
169
+ }
170
+
171
+ /**
172
+ * Create a template file
173
+ * @param {string} name - Template name
174
+ * @param {string} type - Template type ('page', 'component', 'partial', 'layout')
175
+ * @param {string} content - Template content
176
+ * @returns {Promise<string>} File path
177
+ */
178
+ async createTemplate(name, type = 'page', content = null) {
179
+ const fs = await import('fs-extra');
180
+ const path = await import('path');
181
+
182
+ const typeDirs = {
183
+ page: 'pages',
184
+ component: 'components',
185
+ partial: 'partials',
186
+ layout: 'layouts'
187
+ };
188
+
189
+ const dir = typeDirs[type] || 'pages';
190
+ const fileName = `${name}.aether`;
191
+ const filePath = path.join(this.options.templateDir, dir, fileName);
192
+
193
+ // Default content if not provided
194
+ if (!content) {
195
+ switch (type) {
196
+ case 'page':
197
+ content = `@extends('layouts.default')
198
+
199
+ @section('title', '${name.charAt(0).toUpperCase() + name.slice(1)} Page')
200
+
201
+ @section('description', 'This is a ${name} page')
202
+
203
+ @section('keywords', '${name}, page, example')
204
+
205
+ @section('content')
206
+ <div class="container mx-auto px-4 py-8">
207
+ <h1 class="text-3xl font-bold mb-6">Welcome to ${name} Page</h1>
208
+ <p class="text-gray-300 mb-4">This is an example page created with Aether Template Engine.</p>
209
+
210
+ <div class="bg-gray-800 rounded-lg p-6 mb-6">
211
+ <h2 class="text-xl font-semibold mb-4">Example Data</h2>
212
+ <ul class="space-y-2">
213
+ {{#each items as item}}
214
+ <li class="flex items-center space-x-2">
215
+ <i class="fas fa-check text-green-500"></i>
216
+ <span>{{item}}</span>
217
+ </li>
218
+ {{/each}}
219
+ </ul>
220
+ </div>
221
+
222
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
223
+ <div class="bg-blue-900/30 rounded-lg p-6">
224
+ <h3 class="text-lg font-semibold mb-3">Template Features</h3>
225
+ <ul class="space-y-2">
226
+ <li>Layout inheritance with @extends</li>
227
+ <li>Section management with @section/@yield</li>
228
+ <li>Conditional rendering with @if/@else</li>
229
+ <li>Loop rendering with @foreach</li>
230
+ <li>Variable output with {{variable}}</li>
231
+ </ul>
232
+ </div>
233
+
234
+ <div class="bg-purple-900/30 rounded-lg p-6">
235
+ <h3 class="text-lg font-semibold mb-3">Current Data</h3>
236
+ <pre class="bg-black/50 p-4 rounded text-sm overflow-x-auto">
237
+ {{json data}}
238
+ </pre>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ @endsection
243
+
244
+ @section('scripts')
245
+ <script>
246
+ console.log('${name} page loaded');
247
+ // Add your JavaScript here
248
+ </script>
249
+ @endsection`;
250
+ break;
251
+
252
+ case 'component':
253
+ content = `<div class="{{class}}">
254
+ <h3 class="text-lg font-semibold mb-2">{{title}}</h3>
255
+ <p class="text-gray-300">{{content}}</p>
256
+ {{#if buttonText}}
257
+ <button class="mt-3 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded transition">
258
+ {{buttonText}}
259
+ </button>
260
+ {{/if}}
261
+ </div>`;
262
+ break;
263
+
264
+ case 'partial':
265
+ content = `<div class="bg-gray-800 rounded-lg p-4 mb-4">
266
+ <h4 class="font-semibold mb-2">{{title}}</h4>
267
+ <p>{{content}}</p>
268
+ </div>`;
269
+ break;
270
+
271
+ case 'layout':
272
+ content = `<!DOCTYPE html>
273
+ <html lang="zh-CN">
274
+ <head>
275
+ <meta charset="UTF-8">
276
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
277
+ <title>@yield('title', 'Aether Framework')</title>
278
+ <meta name="description" content="@yield('description', '下一代轻量级全栈解决方案')">
279
+ <meta name="keywords" content="@yield('keywords', 'Aether,Framework,Web')">
280
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
281
+ <script src="https://cdn.tailwindcss.com"></script>
282
+ @yield('head')
283
+ </head>
284
+ <body class="antialiased bg-[#020617] text-white">
285
+ @yield('content')
286
+ @yield('scripts')
287
+ </body>
288
+ </html>`;
289
+ break;
290
+ }
291
+ }
292
+
293
+ await fs.ensureDir(path.dirname(filePath));
294
+ await fs.writeFile(filePath, content, 'utf-8');
295
+
296
+ // Cache the template if caching is enabled
297
+ if (this.options.cacheEnabled) {
298
+ this.options.templateCache.set(name, content);
299
+ }
300
+
301
+ return filePath;
302
+ }
303
+
304
+ /**
305
+ * Get engine metadata
306
+ * @returns {Object} Engine information
307
+ */
308
+ getMetadata() {
309
+ const baseMetadata = super.getMetadata();
310
+ return {
311
+ ...baseMetadata,
312
+ mode: 'template',
313
+ layoutSupport: this.templateOptions.layoutSupport,
314
+ includeSupport: this.templateOptions.includeSupport,
315
+ cacheTemplates: this.templateOptions.cacheTemplates
316
+ };
317
+ }
318
+ }
319
+
320
+ export default TemplateModeEngine;