Created
June 4, 2018 23:42
-
-
Save NoelFB/487bf8cd3a3bcbd0698f542cce8a8aa5 to your computer and use it in GitHub Desktop.
Javascript HTML template engine that just... runs JS in a vm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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