Skip to content

Instantly share code, notes, and snippets.

@erickedji
Last active October 5, 2015 10:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save erickedji/e332e0586f5a61e6e039 to your computer and use it in GitHub Desktop.
Save erickedji/e332e0586f5a61e6e039 to your computer and use it in GitHub Desktop.
Statically process file includes for a set of html files, using underscore's template function (idempotent in-place update)
#!/usr/bin/env node
"use strict";
// This script will search for HTML files on the root folder and process
// static includes wrapped by the HTML comments like this:
//
// <!-- #include "foo.html" -->
// anything wrapped by these comments will be replaced
// by the content of the "partialsDir/foo.html" file
// <!-- endinclude -->
//
// You can also add some replacements to the include using
// underscore's template string syntax.
//
// <!-- #include "header.html" title="Example Title" foo='bar' -->
// the copy inside <%=title%> and <%=foo%> will be replaced.
// <!-- endinclude -->
//
// Usage:
// node --harmony_arrow_functions updatePartials.js partialsDir/ file1.html file2.html file3.html dir1/*.html ...
//
// Original Author: Miller Medeiros
// (https://gist.github.com/millermedeiros/3498183)
// Original Version: 0.2.0 (2012/08/28)
// Update by: Eric KEDJI (2015/09/06)
// ---
let fs = require('fs'),
path = require('path'),
readdirP = promisify(fs, fs.readdir),
readFileP = promisify(fs, fs.readFile),
writeFileP = promisify(fs, fs.writeFile),
partialsDir = process.argv[2],
files = process.argv.slice(3),
_ = {};
// ---
// $1 = include start
// $2 = file name
// $3 = props
// $4 = content
// $5 = end include
//
const _reInc = /(^\s*<!--\s*\#include\s*["']([^"']+)["']\s*(.+)?\s*-->\s*$)([\s\S]*?)(^\s*<!--\s*end\s*include\s*-->\s*$)/gm;
// $1 = prop name
// $2 = value
const _rePropSingle = /([-_\w]+)\s*=\s*"([^"]+)"/g;
const _rePropDouble = /([-_\w]+)\s*=\s*'([^']+)'/g;
// ---
loadPartials(partialsDir)
.then(partials => files.forEach(processFile.bind(this, partials)))
.catch(err => console.error(err.stack));
// ============================= HELPERS
function promisify(obj, func) {
return function () {
var args = Array.prototype.slice.call(arguments, 0);
return new Promise(function (resolve, reject) {
args.push(function (err, result) {
if (err) {
reject(err);
} else {
resolve(result);
}
});
func.apply(obj, args);
});
};
}
function loadPartials(dir) {
return readdirP(dir).then(fileNames => {
let filePaths = fileNames.map(fileName => path.join(dir, fileName));
return Promise
.all(filePaths.map(p => readFileP(p)))
.then(contents => {
var map = {};
fileNames.forEach((fileName, idx) => {
map[fileName] = _.template(contents[idx].toString());
console.log(' partial loaded: ' + fileName);
});
return map;
});
});
}
function processFile(partials, filePath) {
readFileP(filePath).then(data => {
data = data.toString();
console.log(' -> processing: ' + filePath);
data = data.replace(_reInc, function(match, includeStart, partialName, props, content, includeEnd){
if (!partials[partialName]) {
throw new Error('Partial not found: ' + partialName);
}
props = parseProps(props);
var newContent = '\n' + partials[partialName](props);
return [includeStart, newContent, includeEnd].join('');
});
return writeFileP(filePath, data, 'utf8').then(() => {
console.log(' <- updated: '+ filePath);
});
})
.catch(err => console.error(err.stack));
}
function parseProps(props){
var obj = {};
var match;
while ((match = _rePropSingle.exec(props) || _rePropDouble.exec(props))) {
obj[ match[1] ] = match[2];
}
return obj;
}
// ====================================== UNDERSCORE CODE SOURCE
// This one is faked, not from the underscore source
_.allKeys = obj => obj ? Object.keys(obj) : [];
// An internal function for creating assigner functions.
var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj) {
var length = arguments.length;
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};
// Fill in a given object with default properties.
_.defaults = createAssigner(_.allKeys, true);
// By default, Underscore uses ERB-style template delimiters, change the
// following template settings to use alternative delimiters.
_.templateSettings = {
evaluate : /<%([\s\S]+?)%>/g,
interpolate : /<%=([\s\S]+?)%>/g,
escape : /<%-([\s\S]+?)%>/g
};
// When customizing `templateSettings`, if you don't want to define an
// interpolation, evaluation or escaping regex, we need one that is
// guaranteed not to match.
var noMatch = /(.)^/;
// Certain characters need to be escaped so that they can be put into a
// string literal.
var escapes = {
"'": "'",
'\\': '\\',
'\r': 'r',
'\n': 'n',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
var escaper = /\\|'|\r|\n|\u2028|\u2029/g;
var escapeChar = function(match) {
return '\\' + escapes[match];
};
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
// NB: `oldSettings` only exists for backwards compatibility.
_.template = function(text, settings, oldSettings) {
if (!settings && oldSettings) settings = oldSettings;
settings = _.defaults({}, settings, _.templateSettings);
// Combine delimiters into one regular expression via alternation.
var matcher = RegExp([
(settings.escape || noMatch).source,
(settings.interpolate || noMatch).source,
(settings.evaluate || noMatch).source
].join('|') + '|$', 'g');
// Compile the template source, escaping string literals appropriately.
var index = 0;
var source = "__p+='";
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
source += text.slice(index, offset).replace(escaper, escapeChar);
index = offset + match.length;
if (escape) {
source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
} else if (interpolate) {
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
} else if (evaluate) {
source += "';\n" + evaluate + "\n__p+='";
}
// Adobe VMs need the match returned to produce the correct offest.
return match;
});
source += "';\n";
// If a variable is not specified, place data values in local scope.
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
source = "var __t,__p='',__j=Array.prototype.join," +
"print=function(){__p+=__j.call(arguments,'');};\n" +
source + 'return __p;\n';
try {
var render = new Function(settings.variable || 'obj', '_', source);
} catch (e) {
e.source = source;
throw e;
}
var template = function(data) {
return render.call(this, data, _);
};
// Provide the compiled source as a convenience for precompilation.
var argument = settings.variable || 'obj';
template.source = 'function(' + argument + '){\n' + source + '}';
return template;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment