Last active
October 5, 2015 10:38
-
-
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)
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
#!/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