waffel
Version:
Static site generation done tasty.
693 lines (637 loc) • 23 kB
JavaScript
// Generated by CoffeeScript 1.11.1
var Backend, EventEmitter, MarkdownEngine, Server, Waffel, _, async, cheerio, chokidar, colors, exec, filters, fs, glob, helpers, i18n, markdown, matter, md5, moment, nunjucks, path, pretty, util, utils, yaml,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
filters = require('./filters');
helpers = require('./helpers');
utils = require('./utils');
MarkdownEngine = require('./markdown-engine');
Server = require('./server');
_ = require('lodash');
colors = require('colors');
exec = require('child_process').exec;
chokidar = require('chokidar');
path = require('path');
md5 = require('md5');
glob = require('globby');
EventEmitter = require('events').EventEmitter;
util = require('util');
async = require('async');
yaml = require('js-yaml');
matter = require('gray-matter');
i18n = require('i18next');
Backend = require('i18next-node-fs-backend');
moment = require('moment');
nunjucks = require('nunjucks');
markdown = require('nunjucks-markdown');
cheerio = require('cheerio');
pretty = require('pretty');
fs = require('fs-extra');
module.exports = Waffel = (function(superClass) {
extend(Waffel, superClass);
Waffel.prototype.defaults = {
silent: false,
verbose: false,
versionAssets: false,
defaultPagination: 10,
defaultSortField: 'slug',
defaultSortOrder: 'desc',
structureFile: 'site.yml',
viewFolder: 'views',
dataFolder: 'data',
destinationFolder: 'public',
staticFolder: 'assets',
localesFolder: 'locales',
domain: '',
basePath: '',
assetPath: '',
root: process.cwd(),
dataExt: '.md',
templateExt: '.html',
languages: [],
defaultLanguage: 'en',
fallbackLanguage: 'en',
localiseDefault: false,
sitemap: true,
sitemapName: 'sitemap.xml',
uglyUrls: false,
outputExt: '.html',
displayExt: true,
dateFormat: 'YYYY-MM-DD',
server: false,
watch: false,
watchInterval: 5000,
parallelLimit: 100,
config: {
env: 'dev'
},
markdownOptions: {},
prettyHTML: {
enable: true,
options: {
ocd: true
}
},
frontmatter: {
delims: ['---', '---']
},
serverConfig: {
port: 1999,
path: 'public',
indexPath: 'public/404.html',
extensions: ['html']
}
};
Waffel.prototype.log = function() {
if (!this.options.silent) {
return console.log.apply(null, arguments);
}
};
Waffel.prototype.error = function(what, e) {
if (this.options.verbose) {
console.log(util.inspect(what, false, 2, true));
}
return console.error(e.stack);
};
function Waffel(opts) {
this._launchServer = bind(this._launchServer, this);
this._getHelpers = bind(this._getHelpers, this);
this._generateSitemap = bind(this._generateSitemap, this);
this._createPage = bind(this._createPage, this);
this._createSinglePages = bind(this._createSinglePages, this);
this._createCollectionPage = bind(this._createCollectionPage, this);
this._renderPage = bind(this._renderPage, this);
this._target = bind(this._target, this);
this._formatToken = bind(this._formatToken, this);
this._getPageByName = bind(this._getPageByName, this);
this._parseFile = bind(this._parseFile, this);
this._generateForLanguage = bind(this._generateForLanguage, this);
this._generate = bind(this._generate, this);
this._getFiles = bind(this._getFiles, this);
this._registerTemplates = bind(this._registerTemplates, this);
this._loadLocales = bind(this._loadLocales, this);
this.postGenerate = bind(this.postGenerate, this);
this.generate = bind(this.generate, this);
this.init = bind(this.init, this);
this.getRevision = bind(this.getRevision, this);
this.error = bind(this.error, this);
this.log = bind(this.log, this);
var e, site, structureFileContents;
this.options = _.extend({}, this.defaults, opts);
['dataFolder', 'viewFolder', 'staticFolder', 'localesFolder', 'destinationFolder', 'structureFile'].forEach((function(_this) {
return function(f) {
return _this.options[f] = path.join(_this.options.root, _this.options[f]);
};
})(this));
this.helpers = _.extend({}, helpers, this.options.helpers);
this.filters = _.extend({}, filters, this.options.filters);
this.filters.loc = _.memoize(this.filters.loc, function(data, language) {
return data[0]._collection + "_" + language;
});
this.filters.excerpt = _.memoize(this.filters.excerpt, function(text, size) {
return (md5(text)) + "." + size;
});
this.filters.top = _.memoize(this.filters.top, function(data, size) {
return (_.flattenDeep(data.join(''))) + "." + size;
});
this.data = {};
try {
structureFileContents = fs.readFileSync(this.options.structureFile, 'utf8');
} catch (error1) {
e = error1;
this.error("Could not locate structureFile: " + this.options.structureFile, e);
}
site = yaml.safeLoad(structureFileContents);
this.config = _.extend(this.options.config, site.config);
this.structure = site.structure;
this.markdownEngine = this.options.markdownEngine || new MarkdownEngine(this.options.markdownOptions);
}
Waffel.prototype.getRevision = function() {
return new Promise((function(_this) {
return function(resolve, reject) {
return exec("git rev-parse HEAD", {
cwd: _this.options.root
}, function(err, stdout, stderr) {
if (err) {
return reject(err);
} else {
return resolve(stdout.split('\n').join(''));
}
});
};
})(this));
};
Waffel.prototype.init = function() {
return this.getRevision().then((function(_this) {
return function(commit) {
return _this.config.rev = commit;
};
})(this))["catch"]((function(_this) {
return function(e) {
return _this.log("--> " + "Could not get commit reference, perhaps not a repo".red + "?\n---");
};
})(this));
};
Waffel.prototype.generate = function(options) {
var dataPaths, debounced_generate, localesPath, viewPath;
if (options == null) {
options = {};
}
this.log("--> Start generation process...\n---");
dataPaths = [path.join(this.options.dataFolder, "**/*" + this.options.dataExt), path.join(this.options.dataFolder, "**/*.json")];
if (this.options.watch) {
localesPath = path.join(this.options.localesFolder, "**/*.json");
viewPath = path.join(this.options.viewFolder, "**/*");
debounced_generate = _.debounce((function(_this) {
return function() {
return _this._generate(dataPaths, options, _this.options.watchInterval);
};
})(this));
this.watcher = chokidar.watch(dataPaths.concat([localesPath, viewPath]));
this.watcher.on('change', debounced_generate);
}
return this._generate(dataPaths, options);
};
Waffel.prototype.postGenerate = function(err, pages) {
var elapsed, millis;
elapsed = process.hrtime(this.start);
millis = elapsed[1] / 1000000;
this.log("--> Generated " + (pages.length + '').cyan + " pages in " + elapsed[0] + "." + (millis.toFixed(0)) + "s.");
if (this.options.sitemap) {
this._generateSitemap(pages).then((function(_this) {
return function() {
return _this.emit('generation:complete');
};
})(this));
} else {
this.emit('generation:complete');
}
return this._launchServer();
};
Waffel.prototype._loadLocales = function() {
return new Promise((function(_this) {
return function(resolve, reject) {
var _i18n;
return _i18n = i18n.createInstance().use(Backend).init({
debug: _this.options.verbose,
preload: _this.options.languages.concat(['dev']),
lng: _this.options.defaultLanguage,
backend: {
loadPath: path.join(_this.options.localesFolder, '{{lng}}.json')
}
}, function(err, t) {
if (err) {
return reject(err);
} else {
return resolve(t);
}
});
};
})(this)).then((function(_this) {
return function(t) {
_this.i18n = t;
return _this;
};
})(this));
};
Waffel.prototype._registerTemplates = function() {
var filter, name, ref;
this.env = nunjucks.configure(this.options.viewFolder, {
watch: false,
express: null,
autoescape: false
});
ref = this.filters;
for (name in ref) {
filter = ref[name];
this.env.addFilter(name, filter.bind(this));
}
markdown.register(this.env, this.markdownEngine.getRenderer());
return nunjucks.precompile(this.options.viewFolder, {
env: this.env
});
};
Waffel.prototype._getFiles = function(dataPaths) {
if (dataPaths == null) {
dataPaths = [];
}
return Promise.all(dataPaths.map((function(_this) {
return function(_path) {
_this.log("--> Globbing " + _path.cyan + ":");
return glob(_path).then(function(files) {
var _data;
_data = {};
files.forEach(function(file) {
var collection;
if (path.extname(file) === '.json') {
collection = path.basename(file, '.json');
return _data[collection] = fs.readJsonSync(file);
} else {
return _this._parseFile(file, _data);
}
});
return _data;
});
};
})(this))).then((function(_this) {
return function(data) {
var mergedData;
mergedData = {};
data.forEach(function(x) {
return _.merge(mergedData, x);
});
return mergedData;
};
})(this));
};
Waffel.prototype._generate = function(dataPaths, options) {
return this._loadLocales().then((function(_this) {
return function() {
_this._registerTemplates();
_this.start = process.hrtime();
_this.emit('generation:start');
return _this._getFiles(dataPaths).then(function(data) {
_this.data = data;
if (options.data) {
_.merge(_this.data, options.data);
}
return fs.ensureDir(_this.options.destinationFolder).then(function() {
var i, language, languages, len, tasks;
tasks = [];
languages = _this.options.localiseDefault ? _this.options.languages : _this.options.languages.filter(function(l) {
return l !== _this.options.defaultLanguage;
});
for (i = 0, len = languages.length; i < len; i++) {
language = languages[i];
tasks = tasks.concat(_this._generateForLanguage(language, true));
}
tasks = tasks.concat(_this._generateForLanguage(_this.options.defaultLanguage, false));
return async.parallelLimit(tasks, _this.options.parallelLimit, _this.postGenerate);
});
});
};
})(this));
};
Waffel.prototype._generateForLanguage = function(language, localised) {
var _name, _page, name, page, ref, ref1, tasks, url;
tasks = [];
ref = this.structure;
for (name in ref) {
page = ref[name];
if (page.languages && indexOf.call(page.languages, language) < 0) {
if (this.options.verbose) {
this.log("Notice:".magenta + " " + name.green + " won't be rendered in " + language.yellow);
}
} else if (page.template) {
page.name = name;
url = this._url(page, {}, {
language: language,
localised: localised
});
tasks.push(this._createPage(page, name, url, {}, language, localised));
} else if (page.collection) {
ref1 = page.pages;
for (_name in ref1) {
_page = ref1[_name];
_page.name = name + "." + _name;
if (_name === 'single') {
tasks = tasks.concat(_.compact(this._createSinglePages(_page, name + ".single", this.data[page.collection], language, localised)));
} else {
tasks = tasks.concat(_.compact(this._createCollectionPage(_page, name + "." + _name, this.data[page.collection], language, localised)));
}
}
}
}
return tasks;
};
Waffel.prototype._parseFile = function(file, _data) {
var base, collection, data, language, loadedData, name1, ref, relativePath, tokens;
relativePath = utils.relativisePath(file, this.options.dataFolder);
tokens = relativePath.split(path.sep).slice(1);
collection = tokens[0];
_data[collection] || (_data[collection] = {});
loadedData = matter.read(file, {
delims: this.options.frontmatter.delims
});
data = loadedData.data;
data.__content = loadedData.content;
data.slug = data.slug || path.basename(relativePath, this.options.dataExt);
if (ref = tokens[1], indexOf.call(this.options.languages, ref) >= 0) {
language = tokens[1];
(base = _data[collection])[name1 = data.slug] || (base[name1] = {
_localised: true,
_collection: collection
});
return _data[collection][data.slug][language] = data;
} else {
return _data[collection][data.slug] = data;
}
};
Waffel.prototype._getPageByName = function(name) {
var tokens;
tokens = name.split('.');
if (tokens.length === 1) {
return this.structure[name];
} else {
return this.structure[tokens[0]].pages[tokens[1]];
}
};
Waffel.prototype._slugify = function(value) {
if (value == null) {
value = '';
}
return value.toLowerCase().replace(/\s+/g, '-').replace(/[^-\w]/g, '');
};
Waffel.prototype._formatToken = function(value) {
if (value == null) {
value = '';
}
if (value instanceof Date) {
value = moment(value).format(this.options.dateFormat);
}
return this._slugify(value);
};
Waffel.prototype._url = function(page, data, opts) {
var tokens;
if (opts == null) {
opts = {};
}
tokens = page.url.split('/');
if (opts.localised) {
tokens.unshift(opts.language);
}
tokens = tokens.map((function(_this) {
return function(token) {
if (token[0] === ':') {
return _this._formatToken(data.group || data[token.slice(1)]);
} else {
return token;
}
};
})(this));
if (page.pagination && page.pagination.page > 1) {
tokens.push('page');
tokens.push(page.pagination.page);
}
return _.compact(tokens).join('/');
};
Waffel.prototype._target = function(url) {
var ext;
ext = path.extname(url);
if (ext) {
return path.join(this.options.destinationFolder, url);
} else if (this.options.uglyUrls && url.length > 0) {
return path.join(this.options.destinationFolder, "" + url + this.options.outputExt);
} else {
return path.join(this.options.destinationFolder, url, "index" + this.options.outputExt);
}
};
Waffel.prototype._renderPage = function(page, _data) {
var error, tmpData;
tmpData = {};
tmpData[page["export"] || 'item'] = _data;
try {
return nunjucks.render("" + page.template + this.options.templateExt, _.extend({}, this._getHelpers(page), tmpData, {
options: this.options,
config: this.config,
data: this.data,
page: page
}));
} catch (error1) {
error = error1;
switch (error.name) {
case 'Template render error':
return this._printTemplateError(error, page, _data);
default:
return this.error(page, error);
}
}
};
Waffel.prototype._printTemplateError = function(error, page, data) {
var errorInfo, info, message;
if (errorInfo = error.message.match(/template not found: (.*)/i)) {
message = "Template not found: ".red + " " + errorInfo[1].green;
info = error.message.split("\n")[0];
} else if (errorInfo = error.message.match(/Template render error: (.*) \[Line (\d+), Column (\d+)\]/)) {
message = "Syntax Error:".red + " " + (error.message.split("\n").pop().trim().green);
info = errorInfo[1] + " [Line " + errorInfo[2] + ", Column " + errorInfo[3] + "]";
} else {
errorInfo = error.message.split("\n").map(function(x) {
return x.trim();
});
info = errorInfo[0];
message = "Syntax Error:".red + " " + errorInfo[1].green;
}
return console.log(message + "\n" + info.yellow + "\n");
};
Waffel.prototype._createCollectionPage = function(page, name, set, language, localised) {
var order, sets, sort, tasks;
sort = page.sort && page.sort.field ? page.sort.field : this.options.defaultSortField;
order = page.sort && page.sort.order ? page.sort.order : this.options.defaultSortOrder;
set = _.reduce(set, function(memo, item, key) {
if (item._localised && item[language]) {
memo[key] = item[language];
} else {
memo[key] = item;
}
return memo;
}, {});
if (page['filter']) {
set = _.where(set, page['filter']);
}
if (page.groupBy) {
sets = {};
_(set).pluck(page.groupBy).flatten().unique().sort().value().forEach((function(_this) {
return function(group) {
return sets[group] = _.toArray(set).filter(function(x) {
if (_(x[page.groupBy]).isArray()) {
return _.contains(x[page.groupBy], group);
} else {
return x[page.groupBy] === group;
}
});
};
})(this));
} else {
sets = [set];
}
tasks = _.map(sets, (function(_this) {
return function(set, group) {
var pages;
pages = _(set).sortBy(sort).tap(function(x) {
if (order === 'desc') {
return _(x.reverse());
} else {
return x;
}
}).chunk(page.paginate || _this.options.defaultPagination).value();
if (page.pageLimit) {
pages = pages.slice(0, Math.abs(+page.pageLimit));
}
return pages.map(function(p, index) {
var _page, url;
_page = _.clone(page);
_page.pagination = {
page0: index,
page: index + 1,
total: pages.length
};
_page.group = group;
url = _this._url(_page, {
group: group
}, {
language: language,
localised: localised
});
return _this._createPage(_page, name, url, p, language, localised);
});
};
})(this));
return _.flatten(tasks);
};
Waffel.prototype._createSinglePages = function(page, name, set, language, localised) {
return _.map(set, (function(_this) {
return function(item, slug) {
var data, url;
data = item._localised ? item[language] || item[_this.options.fallbackLanguage] : item;
if (page.filter && !_.where([data], page.filter).length) {
return false;
}
url = _this._url(page, data, {
language: language,
localised: localised
});
return _this._createPage(page, name, url, data, language, localised);
};
})(this));
};
Waffel.prototype._createPage = function(page, name, url, data, language, localised) {
if (data == null) {
data = {};
}
return (function(_this) {
return function(callback) {
var _page, languageInfo, output, pageInfo, paginationInfo, target;
target = _this._target(url);
_page = _.clone(page);
_page.path = url;
_page.language = language;
_page.localised = localised;
output = _this._renderPage(_page, data);
languageInfo = localised ? "[" + language + "] " : '[--] ';
paginationInfo = page.pagination ? " " + page.pagination.page + "/" + page.pagination.total : '';
pageInfo = data.slug || data.group || page.group || '';
pageInfo = pageInfo ? " [" + pageInfo + "]" : '';
if (_this.options.verbose) {
_this.log(languageInfo.red + "Generating " + name.green + pageInfo.yellow + paginationInfo.magenta + " at: " + target.cyan);
}
if (_this.options.prettyHTML.enable && path.extname(target) === '.html') {
output = pretty(output, _this.options.prettyHTML.options);
}
return fs.outputFile(target, output, function(err) {
return callback(err, {
page: _page,
data: data,
url: url
});
});
};
})(this);
};
Waffel.prototype._generateSitemap = function(pages) {
var output, target;
target = path.join(this.options.destinationFolder, this.options.sitemapName);
output = nunjucks.render(this.options.sitemapName, _.extend({}, this._getHelpers(), {
options: this.options,
config: this.config,
data: this.data,
pages: pages.filter(function(p) {
return !_.isBoolean(p.page.sitemap && p.page.sitemap === !false);
}),
now: new Date
}));
return fs.outputFile(target, output).then((function(_this) {
return function() {
_this.log("--> Created " + _this.options.sitemapName.cyan);
return true;
};
})(this));
};
Waffel.prototype._getHelpers = function(context) {
if (context == null) {
context = {};
}
return _.transform(this.helpers, (function(_this) {
return function(help, func, key) {
return help[key] = function() {
[].push.call(arguments, context);
[].push.call(arguments, _this);
return func.apply(null, arguments);
};
};
})(this));
};
Waffel.prototype._launchServer = function() {
var extensions, opts;
if (!this.options.server || (this.server && this.server.started)) {
return;
}
extensions = {};
if (this.options.uglyUrls && !this.options.displayExt) {
extensions = {
extensions: [this.options.outputExt.slice(1)]
};
}
opts = _.extend(extensions, this.options.serverConfig, this.options.server);
this.server = new Server(opts);
return this.server.start().then((function(_this) {
return function() {
_this.log("--> waffel server waiting for you at " + ("http://localhost:" + opts.port).green);
return _this.emit('server:start', _this.server);
};
})(this));
};
return Waffel;
})(EventEmitter);