Created
March 13, 2025 21:22
-
-
Save mgrahamjo/2811ffd7f9831357762741ed270625f9 to your computer and use it in GitHub Desktop.
World's smallest template engine for node.js
This file contains hidden or 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 fs = require('fs').promises, | |
path = require('path'), | |
escapeMap = { | |
'<': '<', | |
'>': '>', | |
'"': '"', | |
'\'': ''' | |
}; | |
const htmlEscape = val => typeof val === 'string' | |
? val.replace(/[<>'"]/g, c => escapeMap[c]) | |
: val; | |
function parse(template) { | |
const render = new Function('ctx', ` | |
var p=[]; | |
with (ctx || {}) { | |
p.push(\`${template | |
// escape backslashes and backticks | |
.replace(/(\\|`)/g, '\\$1') | |
// capture expressions that should not be HTML escaped | |
.replace(/\s*<!--=(?!\s*}.*?-->)(?!.*{\s*-->)(.*?)-->\s*/g, '`);try{p.push($1)}catch(e){}p.push(`') | |
// capture expressions that should be HTML escaped | |
.replace(/\s*<!--(?!\s*}.*?-->)(?!.*{\s*-->)(.*?)-->\s*/g, '`);try{p.push(this._e($1))}catch(e){}p.push(`') | |
// capture logical (control flow) tags | |
.replace(/\s*<!---?(.*?)-->\s*/g, '`);$1\np.push(`') | |
}\`); | |
} | |
return p.join(""); | |
`); | |
return render.bind({ _e: htmlEscape }); | |
}; | |
async function parseIncludes(template, opts) { | |
const match = /<!--\s*?include\s(\S*?)\s*?-->/i.exec(template); | |
if (match !== null) { | |
const raw = match[0]; | |
let include = opts.partials + match[1]; | |
if (!include.endsWith(opts.extension)) { | |
include += opts.extension; | |
} | |
const html = await fs.readFile(include, { encoding: 'utf8' }); | |
return parseIncludes(template.replace(raw, html), opts); | |
} | |
return template; | |
} | |
async function getTemplate(filepath, opts) { | |
// Support paths relative to views directory | |
if (!filepath.startsWith(opts.root)) { | |
filepath = path.join(opts.views, filepath); | |
} | |
if (!filepath.endsWith(opts.extension)) { | |
filepath += opts.extension; | |
} | |
const template = await fs.readFile(filepath, { encoding: 'utf8' }); | |
const fullTemplate = await parseIncludes(template, opts); | |
return fullTemplate; | |
} | |
module.exports = function(opts = {}) { | |
opts.root = opts.root || path.dirname(require.main.filename); | |
opts.partials = path.join(opts.root, opts.partials || 'blocks', '/'); | |
opts.views = path.join(opts.root, opts.views || 'pages', '/'); | |
opts.extension = ('.' + (opts.extension || 'html')).replace('..', '.'); | |
const compiled = {}; | |
return async function(filepath, context) { | |
if (compiled[filepath]) { | |
return compiled[filepath](context); | |
} | |
const template = await getTemplate(filepath, opts); | |
compiled[filepath] = parse(template); | |
return compiled[filepath](context); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This script provides a template engine similar to the likes of Liquid, Jinja, and Mustache templates. It uses HTML comment syntax for the tags, which is kind of nice for syntax highlighting. The template "language" is javascript, so you can use any logic and reference any data and functions that are passed to the template as context.
By default tag output is HTML-escaped. To prevent HTML escaping, add an equals sign to the opening tag
<!--= like this -->
.Example usage:
./index.js:
./pages/index.html:
./blocks/head.html:
./blocks/foot.html:
Run
node index.js
, openhttp://localhost:3000
, and you'll be served the following HTML: