devlien
Version:
Devlien is a lightweight, zero-dependency Node.js framework with clean MVC structure, built-in ORM, and intuitive routing for rapid backend development.
136 lines (120 loc) • 4.43 kB
JavaScript
import fs from 'fs';
import path from 'path';
export default class Template {
//
constructor(baseDir = 'components', fileExtension = '.dl') {
this.baseDir = baseDir;
this.fileExtension = fileExtension;
}
isFileExists(file) {
return fs.existsSync(path.join(this.baseDir, file));
}
async readFile(file) {
return await fs.promises.readFile(path.join(this.baseDir, file), 'utf-8');
}
/**
* Build and process the template file content, handling extends or includes.
*
* @param {string} file The template file path to read and process.
* @param {Object} [data={}] Data context to pass during rendering (optional).
* @param {boolean} [isExtendable=false] Whether to process @extends directives.
* @return {Promise<string>} The processed template content as a string.
*/
async build(file, isExtendable = false) {
if (!this.isFileExists(file)) {
return `Template "${file}" not found`;
}
let comtent = await this.readFile(file);
if (isExtendable)
return await this.processExtends(comtent, file.replace(this.fileExtension, ''));
else
return await this.processIncludes(comtent, file);
}
/**
* Process @include directives within the template by replacing them
* with unique placeholders and recursively building included templates.
*
* @param {string} template The raw template string containing @include directives.
* @param {string} file The current template file name (used for context).
* @return {Promise<Object>} An object containing processed content, script, and includes:
* {
* content: string,
* script: string,
* includes: Object<string, Object>
* }
*/
async processIncludes(template, file) {
var params = {
file: file,
content: '',
script: '',
includes: {},
};
//
const includeRegex = / \s*\(\s*['"]([^'"]+)['"]\s*\)/g;
const matches = [...template.matchAll(includeRegex)];
template = template.replace(/ \s*\(\s*['"]([^'"]+)['"]\s*\)/g, '');
var index = 0;
for (const match of matches)
{
index++;
const file = `${match[1]}${this.fileExtension}`;
if (this.isFileExists(file)) {
template = template.replace(match[0], `@${index}component`);
params.includes[`@${index}component`] = await this.build(file);
}
else {
template = template.replace(match[0],`<!-- include "${match[1]}" not found -->`);
}
}
const { content, script } = this.parseComponent(template);
params.content = content;
params.script = script;
return params;
}
/**
* Extract the content inside the <template> tag and the script block
* marked by @script ... @endscript from the component string.
*
* @param {string} tamplate The raw component string containing template and script.
* @return {Object} An object with `content` and `script` properties.
*/
parseComponent(tamplate) {
const templateMatch = tamplate.match(/<template>([\s\S]*?)<\/template>/);
const scriptMatch = tamplate.match(/ /);
return {
content: templateMatch ? templateMatch[1] : '',
script: scriptMatch ? scriptMatch[1] : '',
};
}
/**
* Process the @extend directive in the template by loading the specified parent layout,
* replacing the @yield directive in the layout with an @include of the current template,
* then processing includes recursively.
*
* @param {string} template The template content containing the @extend directive.
* @param {string} parant The current template name (used for @include replacement).
* @return {Promise<Object>} The processed template object with content, script, and includes.
*/
async processExtends(template, parant) {
//
const includeRegex = / \s*\(\s*['"]([^'"]+)['"]\s*\)/g;
const matches = [...template.matchAll(includeRegex)];
if(matches.length)
{
const [replaceable, repath] = matches[0];
if (this.isFileExists(repath + this.fileExtension)) {
const layout = await this.readFile(repath + this.fileExtension);
template = template.replace(replaceable, '');
template = layout.replace(/ /g, (xpr) => {
return ` `;
});
}
else {
template = template.replace(replaceable, `<!-- extend "${repath}" not found -->`);
}
return await this.processIncludes(template, repath + this.fileExtension);
}
return await this.processIncludes(template, parant + this.fileExtension);
}
}