Skip to content

Instantly share code, notes, and snippets.

@wthit56
Last active August 29, 2015 14:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wthit56/6771af528caa30c83404 to your computer and use it in GitHub Desktop.
Save wthit56/6771af528caa30c83404 to your computer and use it in GitHub Desktop.
Using Your Own Non-Templating Engine

Please read Writing Your Own Non-Templating Engine before reading this article.

Using Your Own Non-Templating Engine

In the previous article (link at the top), I showed how you can write a very simple function that will turn a given template string into something JavaScript can use. In this article, I'll go into how you can use this in practise.

The Basics

First, let's just make sure we're on the same page as to what the code is and what it does. And while we're at it, let's wrap it up in a function we can reference in later code examples:

function rewrite(code) { // just rewrites the code
	return code.replace(/\{\{([\W\w]*?)}}/g, function(match, HTML) {
		return "\"" +
			HTML.replace(/[\\"]/g, "\\$&") // fix back-slash escaping and double-quotes
			.replace(/\n/g, "\\n") // fix new lines
		"\"";
	});
}
function compile(code) { // compiles it into a function
	return new Function("return function() { return " + rewrite(code) + "};");
}

NOTE: All code examples are just that. Examples. They likely won't be memory-efficient, or standard practice, or even that pretty. When you write your own version, use the concepts described here and make it as super-efficient or super-sloppy as you like. ;P

So now we've got a function that compiles templates into templating functions, we can make use of it:

var template = ...

{{<h1>}} + this.title + {{</h1>
<ul>}} +
	this.items.map(function(item) {
		return {{<li>
			<a href="}} + item.href + {{">}} + item.title + {{</a>
			(}} + item.progress + {{ \ }} + item.total + {{)
		</li>}};
}).join("")
+ {{</ul>}}
```"f
```js
template = compile(template);

var page = template.call({
	title: "Search Engines by Awesomeness",
	items: [
		{ title: "Google", href: "http://www.google.com/", progress: 10, total: 10 },
		{ title: "Ask", href: "http://www.ask.com/", progress: 5, total: 10 }
	]
});

page = ...

<h1>Search Engines by Awesomeness</h1>
<ul>
<li>
			<a href="http://www.google.com/">Google</a>
			(10 \ 10)
		</li><li>
			<a href="http://www.ask.com/">Ask</a>
			(5 \ 10)
		</li>
</ul>

As I've mentioned before, don't worry about the odd, lop-sided nature of rendered code. That way lies madness. But the HTML itself looks just as expected, right? You can call this compiled function whenever you want--in response to an Ajax call, on a timer, in a node http server, whatever you like.

Speaking of which...

Node

A common way of using templates is to serve HTML pages. And what do we use for server-side JavaScript? Node. (Or your preferred equivalent. And remember, these principles can be used in any language which can compile itself.)

As Node uses the V8 JavaScript engine, all the same features you'd need are present on the server. It also allows you to read and write files, which will be useful later. Best of all, it has a great require function that allows a .js file to load in another .js file (or a completely different module). This makes it really easy to separate different parts of your application into different files, and allow them to all talk to each other. This is also presents a minor hurdle we need to pass to get our templating working for any loaded-in .js files. We need to monkey-patch require.

Let's quickly re-write our template and page variables as files our require can read:

template..js = ...

module.exports = {{<h1>}} + this.title + {{</h1>
<ul>}} +
    this.items.map(function(item) {
        return {{<li>
            <a href="}} + item.href + {{">}} + item.title + {{</a>
            (}} + item.progress + {{ \ }} + item.total + {{)
        </li>}};
    }).join("")
+ {{</ul>}};

page.js = ...

module.exports = require("./template..js").call({
	title: "Search Engines by Awesomeness",
	items: [
		{ title: "Google", href: "http://www.google.com/", item.progress: 10, item.total: 10 },
		{ title: "Ask", href: "http://www.ask.com/", item.progress: 5, item.total: 10 }
	]
});

Now to write our own require function. Let's just go over a few main features of require before we start:

  • It accepts a path argument.
  • There are 3 global variables that exist when running a module:
    • require allows the module to require other files / modules.
    • module is an object that doesn't do much; it just holds a single property, .exports. The module can add properties to module.exports, or set it to something else. module.exports is what is returned by the require function.
    • And finally, exports is just a shortcut for module.exports.
  • If the path has already been required, the cached object is returned instead of reading the file and creating a new one. This means it doesn't waste time in reading the same file more than once in the same node process, but also any variables set on the exported object (by the module itself, or by other .js files), or even variables within the module, will stay persistent between requires, allowing for more complex behaviours.

This may all sound a bit like hard work, but it's easier than you think! But first, lets create a stub function we can work on.

function _require(path) {
	if (/* we need to compile it */) {
		// compile and return it here
	}
	else {
		return require.apply(this, arugments);
	}
}

So how should we decide if we need to compile the requested file? I decided on adding a little marker to the filename itself. This means that any time I look at that file (in node, in a file explorer, wherever), it's clear to me that it's a compiled file, rather than a straight-up .js file. For our example, I'm just adding a second "." before te "js" extension, but you can use any kind of pattern, marker, anything you like.

Also, remember that other, regular .js files could require our ..js files, so we'll have to pass through our patched require function to those, too.

The only case where we don't have to worry about compiling ..js files is if we're requiring an outside module.

if (/^(?:\w:|\.\.?\/)/) { // starts with "C:" (or similar), or "./" or "../"; we need to compile it
	// compile and return module
	var code = require("fs").readFileSync(path, "utf-8"); // returns the text of the file as a string
	
	if (/\.\.js$/.test(path)) { // is a compiled ..js file, so re-write the code
		code = rewrite(code);
	}

	var compiled = new Function("require", "module", "exports", code);
	//                          ^ these arguments take precedence over Node's globals
	var module = { exports: {} };
	compiled = compiled(_require, module, module.exports); // run it to get the template rendering function

	return module.exports;
}

And as simple as that, we have our very own require function that compiles templated ..js files as needed. If you wanted, that could be all you do, and you'd have a fully-functional system that allows requiring and runs perfectly fine on server-side Node.

One last feature we can implement would be to add a caching system similar to that of Node's require. This will mean that a .js file will be loaded and run only once within the lifetime of the process. This saves time, but more than that, it allows a single instance to be used by all files that require the same file/module.

All it takes is a couple of lines of code:

var cache = {};
function _require(path) {
	if (/^(?:\w:|\.\.?\/)/) {
		if (path in cache) { return cache[path]; }
		else {
			// ...
		
			cache[path] = module.exports;
			return module.exports;
		}
	}
	// ...
}

Perfect. Now all you need to do is call your require your first file with your new _require function and the rest is taken care of.

As a bit of homework, think about how you might add more functionality:

  • Pass in the path of the ..js file when it is compiled.
  • Get freshly-rendered HTML every time you use the string somewhere (as in a http response, etc.).
  • Have a list of ..js pages so that you can render links to the them.
  • Write a http server using your templating engine.
  • Write a build script that writes flat versions of the ..js files as html or css files (goodbye css pre-processing ;P).
module.exports = require("./template..js").call({
title: "Search Engines by Awesomeness",
items: [
{ title: "Google", href: "http://www.google.com/", item.progress: 10, item.total: 10 },
{ title: "Ask", href: "http://www.ask.com/", item.progress: 5, item.total: 10 }
]
});
function rewrite(code) { // just rewrites the code
return code.replace(/\{\{([\W\w]*?)}}/g, function(match, HTML) {
return "\"" +
HTML.replace(/[\\"]/g, "\\$&") // fix back-slash escaping and double-quotes
.replace(/\n/g, "\\n") // fix new lines
"\"";
});
}
var cache = {};
function _require(path) {
if (/^(?:\w:|\.\.?\/)/) {
if (path in cache) { return cache[path]; }
else {
// compile and return module
var code = require("fs").readFileSync(path, "utf-8"); // returns the text of the file as a string
if (/\.\.js$/.test(path)) { // is a compiled ..js file, so re-write the code
code = rewrite(code);
}
var compiled = new Function("require", "module", "exports", code);
// ^ these arguments take precedence over Node's globals
var module = { exports: {} };
compiled = compiled(_require, module, module.exports); // run it to get the template rendering function
cache[path] = module.exports;
return module.exports;
}
}
// ...
}
console.log(_require("./node-demo-page.js"));
module.exports = {{<h1>}} + this.title + {{</h1>
<ul>}} +
this.items.map(function(item) {
return {{<li>
<a href="}} + item.href + {{">}} + item.title + {{</a>
(}} + item.progress + {{ \ }} + item.total + {{)
</li>}};
}).join("")
+ {{</ul>}};
<script type="text/javascript">
function rewrite(code) { // just rewrites the code
return code.replace(/\{\{([\W\w]*?)}}/g, function(match, HTML) {
return "\"" +
HTML.replace(/[\\"]/g, "\\$&") // fix back-slash escaping and double-quotes
.replace(/\n/g, "\\n") // fix new lines
"\"";
});
}
function compile(code) { // compiles it into a function
return new Function("return function() { return " + rewrite(code) + "};");
}
</script>
<script id="template" type="text/js-html">
{{<h1>}} + this.title + {{</h1>
<ul>}} +
this.items.map(function(item) {
return {{<li>
<a href="}} + item.href + {{">}} + item.title + {{</a>
(}} + item.progress + {{ \ }} + item.total + {{)
</li>}};
}).join("")
+ {{</ul>}}
</script>
<script type="text/javascript">
var template = compile(document.getElementById("template").innerHTML);
var page = template.call({
title: "Search Engines by Awesomeness",
items: [
{ title: "Google", href: "http://www.google.com/", progress: 10, total: 10 },
{ title: "Ask", href: "http://www.ask.com/", progress: 5, total: 10 }
]
});
document.write(page);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment