Skip to content

Instantly share code, notes, and snippets.

@dbushell
Last active December 15, 2015 02:19
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save dbushell/5186122 to your computer and use it in GitHub Desktop.
Save dbushell/5186122 to your computer and use it in GitHub Desktop.
Grunt task to build HTML templates with includes (work in progress!)
/*!
*
* Copyright (c) David Bushell | @dbushell | http://dbushell.com/
*
*/
var fs = require("fs"),
path = require("path");
module.exports = function(grunt)
{
'use strict';
/**
* remove dir and all its sub-directoies and files recursively
* https://gist.github.com/liangzan/807712
*/
var removeDir = function(dir)
{
var files;
try {
files = fs.readdirSync(dir);
} catch(e) {
return;
}
files.forEach(function(file)
{
var p = dir + '/' + file;
if (fs.statSync(p).isFile()) {
fs.unlinkSync(p);
} else {
removeDir(p);
}
});
fs.rmdirSync(dir);
};
/**
* make dir and its structure recursively
* https://github.com/substack/node-mkdirp
*/
var makeDir = function(p, r)
{
var dirs = p.split('/'),
dir = dirs.shift(),
root = (r || '') + dir + '/';
try {
fs.mkdirSync(root);
} catch (err) {
if(!fs.statSync(root).isDirectory()) {
throw new Error(err);
}
}
return !dirs.length || makeDir(dirs.join('/'), root);
};
/**
* strip leading and trailing slashes
*/
var normalizePath = function(p)
{
return (typeof p === 'string' ? p : '').trim().replace(/^\/|\/$/g, '');
};
/**
* register Grunt task
*/
// htmlizr: {
// dev: {
// buildDir: 'build',
// templateDir: 'templates',
// src: ['templates/**/*.html']
// }
// }
grunt.registerMultiTask('htmlizr', 'HTML build.', function()
{
var done = this.async(),
files = grunt.file.expand(this.data.src),
// multiple passes will replace includes within includes
passes = parseInt(this.data.passes, 10) || 3,
// build directory
buildDir = normalizePath(this.data.buildDir) || 'build',
// template root directory
templateDir = normalizePath(this.data.templateDir) || '',
// file caches
includes = [],
templates = [],
// build in progress
building = false;
/**
* callback to end the task
*/
var complete = function(err)
{
if (err) {
done(err);
} else {
done();
}
};
/**
* cache include or template file ready for build
*/
var cacheFile = function(part)
{
(part.name.charAt(0) === '_' ? includes : templates).push(part);
fs.readFile(normalizePath(templateDir + '/' + part.path), { encoding: 'utf8' }, function(err, data)
{
if (err) {
grunt.log.error('Failed to read *' + part.path + '*');
complete(err);
return;
}
part.data = data.toString();
build();
});
};
/**
* retrieve a cached file from reference (name or path)
*/
var getCachedFile = function(ref, cache)
{
var ret = null;
cache.forEach(function(item) {
if (ref === item.name || ref === item.path) {
ret = item;
}
});
return ret;
};
/**
* check if all files have been cached
*/
var cacheReady = function(cache)
{
var count = 0;
cache.forEach(function(item) {
if (item.data) {
count++;
}
});
return count === cache.length;
};
/**
* replace includes in template, e.g. <!-- @include _header.html -->
*/
var parseTemplate = function(tmp)
{
var match,
found = [],
rcomment = /(<!--[ \t]*?@include[ \t]*?(.*?)-->)/g;
// search for includes
while((match = rcomment.exec(tmp.data)) !== null)
{
found.push({
'tag': match[1],
'ref': match[2].trim(),
'offset': match.index
});
}
var inc, i, head, tail;
// insert includes backwards to maintain offsets
for (i = found.length - 1; i >= 0; i--)
{
match = found[i];
head = tmp.data.substring(0, match.offset);
tail = tmp.data.substring(match.offset + match.tag.length, tmp.data.length);
inc = getCachedFile(match.ref, includes);
if (inc) {
tmp.data = head + inc.data + tail;
} else {
tmp.data = head + tail;
grunt.log.error('Unknown *' + match.ref + '* in *' + tmp.name + '*');
}
}
return found.length;
};
var build = function()
{
if (building || !cacheReady(includes) || !cacheReady(templates)) {
return;
}
building = true;
// remove existing build
if (fs.existsSync(buildDir)) {
removeDir(buildDir);
}
fs.mkdirSync(buildDir);
var rendered = 0;
templates.forEach(function(tmp)
{
// ensure directory structure
var buildpath = buildDir + '/' + tmp.path;
makeDir(path.dirname(path.resolve(buildpath)));
fs.open(buildpath, 'w', function(err, fd)
{
if (err) {
grunt.log.error('Error writing *' + buildpath + '*');
complete(err);
return;
}
for (var i = 0; i < passes; i++) {
if (!parseTemplate(tmp)) {
break;
}
}
var buffer = new Buffer(tmp.data);
fs.writeSync(fd, buffer, 0, buffer.length);
if (++rendered >= templates.length) {
complete();
}
});
});
};
// cache all files
files.forEach(function(file)
{
cacheFile({
'name': path.basename(file),
'path': file.replace(new RegExp('^' + templateDir + '/'), ''),
'data': null
});
});
build();
});
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment