Skip to content

Instantly share code, notes, and snippets.

@NoelFB
Created June 4, 2018 23:42
Show Gist options
  • Save NoelFB/487bf8cd3a3bcbd0698f542cce8a8aa5 to your computer and use it in GitHub Desktop.
Save NoelFB/487bf8cd3a3bcbd0698f542cce8a8aa5 to your computer and use it in GitHub Desktop.
Javascript HTML template engine that just... runs JS in a vm
let data = {
title: "All My Posts",
posts: [
{ title: "first post", body: "nothing to see here" },
{ title: "second post", author: { name: "Somebody" }, body: "another post, wow" },
{ title: "final post", body: "this is the end" }
]
};
let fs = require("fs");
let templater = require("./templater");
// create template
let source = fs.readFileSync("template.html", "utf-8");
let template = templater.compile(source);
// custom helper methods
let helpers = { clean: (str) => { return escape(str); } };
// get output html
let result = template(posts, helpers);
<div class="posts">
<h1>[[+title]]</h1>
Latest 2 Posts:
[[for (let i = 0; i < Math.min(posts.length, 2); i ++) {]]
<h2>[[+post.title]]</h2>
[[?post.author]]<h3>[[+post.author.name]]</h3>[[/?]]
<p>[[+strip(post.body)]]</p>
[[}]]
</div>
const vm = require("vm");
const regex = new RegExp(/(\[\[(?:.*?)\]\])/s);
const outvar = "__out";
let partials = {};
/**
* Creates a new Partial template
* @param {string} name The Name of the partial
* @param {string} source The HTML Source of the partial
*/
exports.partial = (name, source) =>
{
return partials[name] = scriptify(source);
};
// default command shortcuts
let shortcuts = {
// insert value shortcut
"+": (cmd) => {
return `${outvar} += ${cmd};`;
},
// embed partial template shortcut
">": (cmd) => {
let name = cmd;
let pass = "{}";
let split = cmd.indexOf(' ');
if (split > 0)
{
name = cmd.substr(0, split);
pass = cmd.substr(split + 1);
}
return `embed("${name}", ${pass});`;
},
"??": (cmd) => { return '} else {'; },
"?": (cmd) => { return `if (typeof ${cmd} !== "undefined" && ${cmd}) {`; },
"!": (cmd) => { return `if (typeof ${cmd} === "undefined" || !${cmd}) {`; },
"/?": (cmd) => { return `}`; },
"/!": (cmd) => { return `}`; },
"each": (cmd) => {
let parts = cmd.split(' as ');
return `${parts[0]}.forEach((${parts[1]}) => {`;
},
"/each": (cmd) => { return `});`; }
};
/**
* Adds a Javascript shortcut
* @param {*} name Name of the shortcut
* @param {*} func Function to run on the script
*/
exports.shortcut = (name, func) =>
{
shortcuts[name] = func;
}
/**
* Turns a Source HTML file into a JS template script
* @param {*} source the HTML Source
*/
let scriptify = (source) =>
{
let js = `var ${outvar} = "";\n`;
// sort shortcut keys by length
let shortcutKeys = [];
for (let key in shortcuts)
shortcutKeys.push(key);
shortcutKeys.sort().reverse();
// iterate over source segments
let segments = source.split(regex);
for (let i = 0; i < segments.length; i ++)
{
// is a javascript command
if (regex.test(segments[i]))
{
let script = segments[i].substr(2, segments[i].length - 4);
// is a shortcut command
let isShortcut = false;
for(let j = 0; j < shortcutKeys.length && !isShortcut; j ++)
{
let key = shortcutKeys[j];
if (script.startsWith(key))
{
js += shortcuts[key](script.substr(key.length));
isShortcut = true;
break;
}
}
// normal js code
if (!isShortcut)
js += script;
js += '\n';
}
// is text
else
{
let lines = segments[i].split(/\n|\r\n/).join('\\n');
lines = lines.split('"').join('\\"');
if (lines.length > 0)
js += `${outvar} += "${lines}";\n`;
}
}
return new vm.Script(js);
}
/**
* Runs the given vm.Script with the given sandbox object
* @param {vm.Script} script
* @param {object} sandbox
*/
let run = (script, sandbox) =>
{
vm.createContext(sandbox);
script.runInContext(sandbox);
return sandbox[outvar];
}
/**
* Compiles source HTML into a template. Returns a method that should be called with the given context
* @param {string} source source HTML of the template
*/
exports.compile = (source) =>
{
// create compiled function
let js = scriptify(source);
// result
return (function(data, helpers)
{
let sandbox = { }
// default helper functions
helpers = helpers || {};
helpers.embed = (name, pass) =>
{
if (partials[name] != undefined)
sandbox[outvar] += run(partials[name], Object.assign({}, sandbox, pass));
};
helpers.print = (str) =>
{
sandbox[outvar] += str;
};
// run
Object.assign(sandbox, data, helpers);
return run(js, sandbox);
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment