handlebars-helper-compose
Version:
{{compose}} handlebars helper. Inlines content from multiple files optionally using wildcard (globbing/minimatch) patterns, extracts YAML front matter to pass to context for each file. Accepts compare function as 3rd parameter for sorting inlined files. Good for blog posts, chapters, etc.
186 lines (150 loc) • 6.55 kB
JavaScript
/**
* Handlebars Helper: {{compose}}
* https://github.com/helpers/handlebars-helper-compose
*
* Copyright (c) 2014 Jon Schlinkert, contributors.
* https://github.com/jonschlinkert
* Licensed under the MIT license.
*/
;
// Node.js modules
var path = require('path');
// node_modules
var file = require('fs-utils');
var glob = require('globule');
var marked = require('marked');
var extras = require('marked-extras');
var matter = require('gray-matter');
var _str = require('underscore.string');
var _ = require('lodash');
module.exports.register = function (Handlebars, options, params) {
var grunt = params.grunt;
var opts = options || {};
opts.compose = opts.compose || {};
var customMarkedOpts = _.extend(options.marked || {});
extras.init(customMarkedOpts);
var markedOpts = _.defaults(customMarkedOpts, extras.markedDefaults);
// The {{compose}} helper
Handlebars.registerHelper('compose', function(context, options) {
options = _.extend(context, options);
var hash = options.hash || {};
// Default options
options = _.extend({glob: {}, sep: '\n'}, options, opts.compose, hash);
var i = 0;
var result = '';
var data;
var ctx = _.extend(grunt.config.data, opts, this);
// Define marked.js options
marked.setOptions(markedOpts);
// Add some src variables to the context
ctx.dir = path.dirname(ctx.page.src || '');
ctx.base = file.basename(ctx.page.src);
var patterns = grunt.config.process(options.src);
if(!Array.isArray(patterns)) {
patterns = [patterns];
}
options.cwd = grunt.task.current.files[0].orig.cwd || options.cwd;
var cwd = path.join.bind(null, options.cwd || '');
// Recalculate cwd after ctx.dir has been added to context
if(options.hash.cwd) {
cwd = path.join.bind(null, grunt.config.process(options.hash.cwd));
}
/**
* Accepts two objects (a, b),
* @param {Object} a
* @param {Object} b
* @return {Number} returns 1 if (a >= b), otherwise -1
*/
var compareFn = function (a, b) {
var opts = _.extend({sortOrder: 'asc', sortBy: 'index'}, options);
a = a.data[opts.sortBy];
b = b.data[opts.sortBy];
var result = 0;
result = a > b ? 1 : a < b ? -1 : 0;
if(opts.sortOrder.toLowerCase() === 'desc') {
return result * -1;
}
return result;
};
patterns.forEach(function (pattern) {
var files = glob.find(cwd(pattern), options.glob);
if(options.filter) {
files = options.filter(files);
}
var src = files.map(function (filepath) {
i += 1;
var content = matter.read(filepath).content || '';
var metadata = matter.read(filepath).context || {};
/**
* Process context from last to first, with #1 winning over other contexts.
* 1. `context` : The given context (second parameter)
* 2. `metadata` : YAML front matter of the partial
* 3. `opts.data[name]` : JSON/YAML data file defined in Assemble options.data
* with a basename matching the name of the partial, e.g
* {{include 'foo'}} => foo.json
* 4. `this` : Typically either YAML front matter of the "inheriting" page,
* layout, block expression, or "parent" helper wrapping this helper
* 5. `opts` : Custom properties defined in Assemble options
* 6. `grunt.config.data`: Data from grunt.config.data
* (e.g. pkg: grunt.file.readJSON('package.json'))
*/
var basename = file.basename(filepath);
ctx = _.extend(ctx, opts.data[ctx.base], opts.data[basename], metadata, context);
// Process config (Lo-Dash) templates
ctx = processContext(grunt, ctx);
// Also process metadata separately so we can use it to extend the `data` object
metadata = processContext(grunt, metadata);
// Inject private variables into block, so that variables on `data`
// are only available in the block helper's child template and
// not in the original context
data = Handlebars.createFrame(ctx.data);
data = _.extend(data, opts.compose, _.omit(metadata, ['assemble', 'pages', 'plugins', '_plugins']));
// Best guess at some useful properties to add to the data context
data.filepath = filepath;
data.basename = basename;
data.filename = file.filename(filepath);
data.pagename = file.filename(filepath);
data.slug = data.slug || _str.slugify(data.basename);
data.id = data.slug;
data.title = data.title || ctx.title || _str.titleize(data.basename);
// Some of these might come in handy for ordering/sorting
// or identifying posts, chapters, etc.
data.index = i;
data.number = i + 1;
data.first = (i === 0);
data.prev = i - 1;
data.next = i + 1;
data.last = (i === (files.length - 1));
// Content from src files
var glob_fn = Handlebars.compile(content);
data.content = glob_fn(ctx).replace(/^\s+/, '');
if((opts.marked && opts.marked.process === true) || hash.markdown) {
data.content = marked(data.content);
}
// Content from inside the block
var output = options.fn(ctx, {data: data});
// Prepend output with the filepath to the original partial
if (options.origin && options.origin === true) {
output = '<!-- ' + filepath + ' -->\n' + output;
}
return {
data: data,
content: output
};
}).sort(options.compare || compareFn).map(function (obj) {
if(options.debug) {file.writeDataSync(options.debug, obj);}
// Return content from src files
return obj.content;
}).join(options.sep);
result += src;
});
return new Handlebars.SafeString(result);
});
/**
* Process templates using grunt.config.data and context
*/
var processContext = function (grunt, context) {
grunt.config.data = _.defaults(context || {}, _.cloneDeep(grunt.config.data));
return grunt.config.process(grunt.config.data);
};
};