import fs from "fs"; import path from "path"; export default class { #views; #globals; #cache; #templates; #debug; constructor() { this.#views = "./views"; this.#globals = {}; this.#debug = false; this.#cache = true; this.#templates = new Map(); }; set debug(d) { this.#debug = !!d; }; set views(v) { this.#views = v; this.readdir(v); }; set globals(g) { this.#globals = g; }; set cache(c) { this.#cache = !!c; }; readdir(dir, root = dir) { for(const d of fs.readdirSync(`${path.resolve()}/${dir}`)) { if(d.endsWith(".html")) { // template 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 { // directory 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 = template.cached; } 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 = { ...data, ...locals, ...this.#globals }; try { const code = 'with(_data){const __html = [];' + '__html.push(\`' + this.getTemplate(file) .replace(/[\t]/g, ' ') .split('\`').join('\\\`') .replace(/{{--(.*?)--}}/g, "") // comments .replace(/{{(.+?)}}/g, '\`,$1,\`') // yield variable .replace(/{!!(.+?)!!}/g, '\`,this.escape($1),\`') // yield escaped variable .replace(/@js/g, "`);") // inject bare javascript .replace(/@endjs/g, ";__html.push(`") .replace(/@mtime\((.*?)\)/g, `\`);__html.push(this.getMtime('$1'));__html.push(\``) .replace(/@include\((.*?)\)/g, (_, inc) => this.render(inc, data)) // include template .replace(/@for\((.*?)\)$/gm, `\`);for($1){__html.push(\``) .replace(/@endfor/g, `\`);}__html.push(\``) .replace(/@each\((.*?) as (.*?)\)/g, `\`);this.forEach($1,($2,key)=>{__html.push(\``) // foreach for the poor .replace(/@endeach/g, "\`);});__html.push(`") .replace(/@elseif\((.*?)\)(\)?)/g, `\`);}else if($1$2){__html.push(\``) // if lol .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 a iterable object`); }; getMtime(file) { try { return +(fs.statSync(path.normalize(process.cwd() + file)).mtimeMs + '').split(".")[0]; } catch(err) { console.log(err); return 0; } } };