import fs from "node:fs"; import path from "node:path"; export default class Template { constructor() { this.views = "./views"; this.globals = {}; this.debug = false; this.cache = true; this.templates = new Map(); } setDebug(debug) { this.debug = debug; } setViews(views) { this.views = views; this.readdir(views); } setGlobals(globals) { this.globals = globals; } setCache(cache) { this.cache = cache; } readdir(dir, root = dir) { for (const d of fs.readdirSync(`${path.resolve()}/${dir}`)) { if (d.endsWith(".html")) { const file = path.parse(`${dir.replace(this.views, "")}/${d}`); const t_dir = file.dir.substring(1); const t_file = file.name; this.getTemplate(!t_dir ? t_file : `${t_dir}/${t_file}`); } else { this.readdir(`${dir}/${d}`, root); } } } getTemplate(tpl) { let template; let cache = false; if (this.cache && this.templates.has(tpl)) { template = this.templates.get(tpl); cache = true; } else { template = { code: fs.readFileSync(`${this.views}/${tpl}.html`, "utf-8"), cached: new Date(), }; this.templates.set(tpl, template); } return [ template.code, this.debug ? `` : "", ].join(""); } render(file, data = {}, locals = {}) { data = Object.assign(Object.assign(Object.assign({}, data), locals), this.globals); try { const code = 'with(_data){const __html = [];' + '__html.push(`' + this.getTemplate(file) .replace(/[\t]/g, " ") .split("`") .join("\\`") .replace(/{{--(.*?)--}}/g, "") .replace(/{{(.+?)}}/g, "`, $1, `") .replace(/{!!(.+?)!!}/g, "`, this.escape($1), `") .replace(/@js/g, "`);") .replace(/@endjs/g, ";__html.push(`") .replace(/@mtime\((.*?)\)/g, "`);__html.push(this.getMtime('$1'));__html.push(`") .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data)) .replace(/@for\((.*?)\)$/gm, "`); for($1) { __html.push(`") .replace(/@endfor/g, "`); } __html.push(`") .replace(/@each\((.*?) as (.*?)\)/g, "`); this.forEach($1, ($2, key) => { __html.push(`") .replace(/@endeach/g, "`); }); __html.push(`") .replace(/@elseif\((.*?)\)(\)?)/g, "`); } else if ($1$2) { __html.push(`") .replace(/@if\((.*?)\)(\)?)/g, "`); if ($1$2) { __html.push(`") .replace(/@else/g, "`); } else { __html.push(`") .replace(/@endif/g, "`); } __html.push(`") + "`); return __html.join('').replace(/\\n\\s*\\n/g, '\\n'); }"; return new Function("_data", code).bind({ escape: this.escape, forEach: this.forEach, getMtime: this.getMtime, })(data); } catch (err) { console.log(file, err.message); return this.debug ? `${err.message} in ${file}` : ""; } } escape(str) { return (str + "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'") .replace(/{/g, "{") .replace(/}/g, "}"); } forEach(o, f) { if (Array.isArray(o)) { o.forEach(f); } else if (typeof o === "object") { Object.keys(o).forEach((k) => f.call(null, o[k], k)); } else { throw new Error(`${o} is not an iterable object`); } } getMtime(file) { try { return +`${fs.statSync(path.normalize(process.cwd() + file)).mtimeMs}`.split(".")[0]; } catch (err) { console.log(err); return 0; } } }