UNPKG

waffel

Version:

Static site generation done tasty.

693 lines (637 loc) 23 kB
// 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);