mikser
Version:
Real-time static site generator
546 lines (506 loc) • 16.8 kB
JavaScript
var Promise = require('bluebird');
var path = require('path');
var cluster = require('cluster');
var extend = require('node.extend');
var S = require('string');
var fs = require("fs-extra-promise");
var shortcode = require('shortcode-parser');
var minimatch = require("minimatch");
var _ = require('lodash');
var using = Promise.using;
var constants = require('./constants.js');
var yaml = require('js-yaml');
var uuid = require('uuid');
Promise.config({cancellation: true});
module.exports = function(mikser) {
var generator = {
engines: []
};
var debug = mikser.debug('generator');
function replaceMap(content, map){
if (!map) return content;
var re = new RegExp(Object.keys(map).join("|"),"gi");
return content.replace(re, function(matched){
return map[matched.toLowerCase()];
});
}
generator.findEngine = function(context) {
let source;
if (typeof context == 'string') {
source = context;
} else {
if (context.document) {
source = context.document.source;
}
if (context.layout) {
source = context.layout.source;
}
}
if (!source) return;
for (let engine of mikser.generator.engines) {
if (minimatch(source, engine.pattern)) {
return engine;
}
}
}
generator.render = function(context) {
try {
let engine = generator.findEngine(context);
if (engine) {
let functions = _.functions(context);
for (let functionName of functions) {
context._internal = {};
context._internal['_' + functionName] = context[functionName];
context._internal[functionName] = function() {
let args = Array.from(arguments);
try {
return context._internal['_' + functionName].apply(context, args);
}
catch(err) {
args = [].slice.apply(arguments).concat([err.stack[1].trim()]);
args.pop();
if (!err.origin || mikser.options.debug){
mikser.diagnostics.log(context, 'error', functionName + '(' + args.join(', ') + ')\n ' + err.stack.toString());
} else {
mikser.diagnostics.log(context, 'error', functionName + '(' + args.join(', ') + ')');
}
throw err;
}
}
}
context.mikser = mikser;
context.content = engine.render(context);
delete context._internal;
}
mikser.diagnostics.leave(context);
}
catch (err) {
if (err.origin) {
let message = err.message;
if (err.diagnose) {
message = err.diagnose.message + '\n' + err.diagnose.details + '\n';
}
mikser.diagnostics.log(context, 'error', '[' + err.origin + '] ' + message);
}
else {
mikser.diagnostics.log(context, 'error', err.stack.toString());
}
mikser.diagnostics.break(context);
throw err;
}
return context;
};
generator.renderShortcode = function (context) {
let shortcodeDocumentContext = mikser.loader.extend(context);
delete shortcodeDocumentContext.layout;
shortcodeDocumentContext._id = context._id + '.0';
mikser.diagnostics.splice(shortcodeDocumentContext);
generator.render(shortcodeDocumentContext);
let order = 1;
for (let layout of shortcodeDocumentContext.layouts) {
shortcodeDocumentContext.layout = layout;
shortcodeDocumentContext._id = context._id + '.' + (order++).toString();
context.layoutLink(layout);
mikser.diagnostics.splice(shortcodeDocumentContext);
generator.render(shortcodeDocumentContext);
}
context.content = shortcodeDocumentContext.content;
return context;
}
generator.renderContext = function(context) {
mikser.diagnostics.splice(context);
return mikser.loader.loadSelf(context).then((context) => {
let renderPipe = Promise.resolve(context);
if (context.document) {
let shortcodes = mikser.require('shortcode-parser');
renderPipe = renderPipe.then((context) => {
let preparations = Promise.resolve();
for (let shortcode in context.shortcodes) {
//console.log('1.', shortcode, context.shortcodes[shortcode]._id);
preparations = preparations.then(() => {
let shortcodeContext = mikser.loader.extend(context);
shortcodeContext.document = extend(true, {}, context.document);
delete shortcodeContext.document.meta.layout;
delete shortcodeContext.layouts;
shortcodeContext.layout = context.shortcodes[shortcode];
return mikser.loader.loadSelf(shortcodeContext).then(() => {
//console.log('2.', shortcodeContext.layouts.map((layout) => layout._id));
let preload = Promise.resolve();
for (let shortcodeLayout of shortcodeContext.layouts) {
let shortcodeRenderContext = mikser.loader.extend(shortcodeContext);
shortcodeRenderContext.layout = shortcodeLayout;
preload = preload.then(() => {
return mikser.loader.loadPartials(shortcodeRenderContext).then(() => {
return mikser.loader.loadBlocks(shortcodeRenderContext);
}).then(() => {
// console.log('3.', shortcode, renderContext.layouts.map((layout) => layout._id));
shortcodes.add(shortcode, (content, options) => {
shortcodeRenderContext.content = S(content).trim().s;
shortcodeRenderContext.options = options;
return generator.renderShortcode(shortcodeRenderContext).content;
});
});
});
}
return preload;
});
});
}
return preparations;
});
renderPipe = renderPipe.then(() => {
context.shortcode = shortcodes.parse;
if (context.document) {
context.content = shortcodes.parse(context.document.content);
return generator.render(context);
}
});
}
let order = 1;
for (let layout of context.layouts) {
renderPipe = renderPipe.then((context) => {
let renderContext = mikser.loader.extend(context);
renderContext._id = (order++).toString();
renderContext.layout = layout;
mikser.diagnostics.splice(renderContext);
return mikser.loader.loadPartials(renderContext).then(() => {
return mikser.loader.loadBlocks(renderContext);
}).then(() => {
if (context.prerenders[layout._id]) {
context.processPrerenders[layout._id]();
return context.prerenders[layout._id];
}
}).then(() => {
generator.render(renderContext);
context.content = renderContext.content;
mikser.loader.dispose(renderContext);
return Promise.resolve(context);
});
});
}
return renderPipe;
});
}
generator.renderView = function(viewId, options) {
mikser.diagnostics.snapshot();
if (cluster.isWorker) debug('Render view[' + mikser.workerId + ']:', viewId);
else debug('Render view:', viewId);
let view = mikser.runtime.findEntity('views', viewId);
if (!view) return Promise.reject('View not found: ' + viewId);
let context = {
_id: '0',
view: view,
entity: view,
data: {},
blocks: {},
partials: {},
options: options,
config: mikser.config,
environment: mikser.options.environment
}
context.pending = Promise.resolve();
context.href = function (href, lang) {
if (_.isBoolean(lang)) {
lang = undefined;
}
return mikser.runtime.findHref(context.entity, href, lang);
}
context.hrefEntity = function (href, lang) {
let found = context.href(href, lang);
if (!found._id) return;
return found;
}
context.hrefPage = function (href, page, lang, link) {
let found = context.href(href, lang, link);
if (found && found._id && page > 0) {
let href = found.toString();
found.toString = () => {
return href.replace(path.extname(found.url), '.' + page + '.html');
}
}
return found;
}
context.hrefLang = function (href) {
href = href || context.entity.meta.href;
return mikser.runtime.findHrefLang(href) || {};
}
context.process = function(action) {
if (_.isFunction(action) && action.length) {
let capturedContext = extend({}, context);
action = action.bind(capturedContext, [context]);
}
context.pending = context.pending.then(action);
return context.pending;
}
context.prerenders = {}
context.processPrerenders = {}
context.prerender = function(action) {
if (_.isFunction(action) && action.length) {
let capturedContext = extend({}, context);
action = action.bind(capturedContext, [context]);
}
if (!context.prerenders[context.layout._id]) {
context.prerenders[context.layout._id] = new Promise((resolve, reject) => {
context.processPrerenders[context.layout._id] = resolve;
});
}
context.prerenders[context.layout._id] = context.prerenders[context.layout._id].then(action);
return context.prerenders[context.layout._id];
}
let asyncMap;
let buildAsyncMap = Promise.resolve();
context.async = function (action) {
let asyncId = uuid.v1();
let buildMap = action.then((result) => {
asyncMap = asyncMap || {};
asyncMap[asyncId] = result;
});
buildAsyncMap = buildAsyncMap.then(() => buildMap);
return asyncId;
}
context.processAsync = function (action) {
let process = context.process(action);
return context.async(process);
}
return generator.renderContext(context).then((context) => {
return context.pending.then(() => {
return buildAsyncMap.then(() => {
let content = replaceMap(context.content || '', asyncMap);
mikser.loader.dispose(context);
return Promise.resolve(content);
});
});
}).catch((err) => {
if (typeof err == 'string') err = new Error(err);
err.diagnose = context.renderPipeline;
throw err;
});
}
generator.renderDocument = function(documentId, strategy) {
mikser.diagnostics.snapshot();
let document = mikser.runtime.findEntity('documents', documentId);
if (cluster.isWorker) debug('Render[' + mikser.workerId + ']:', strategy, documentId);
else debug('Render document:', strategy, documentId);
if (!document) {
debug('Document missing: ' + documentId);
return Promise.resolve();
}
let context = {
_id: '0',
document: document,
entity: document,
strategy: strategy,
data: {},
blocks: {},
partials: {},
config: mikser.config,
environment: mikser.options.environment
}
let documentLinks = [];
context.documentLink = function(document) {
if (typeof document == 'string') {
document = {
_id: document
}
}
if (documentLinks.indexOf(document._id) == -1) {
documentLinks.push(document._id);
}
}
let documentUnlinks = [];
context.documentUnlink = function(document) {
if (typeof document == 'string') {
document = {
_id: document
}
}
if (documentUnlinks.indexOf(document._id) == -1) {
documentUnlinks.push(document._id);
}
}
let layoutLinks = [];
context.layoutLink = function(layout) {
if (typeof layout == 'string') {
layout = {
_id: layout
}
}
if (layoutLinks.indexOf(layout._id) == -1) {
layoutLinks.push(layout._id);
}
}
let layoutUnlinks = [];
context.layoutUnlink = function(layout) {
if (typeof layout == 'string') {
layout = {
_id: layout
}
}
if (layoutUnlinks.indexOf(layout._id) == -1) {
layoutUnlinks.push(layout._id);
}
}
let liveLinks = [];
context.liveLink = function(link, lang) {
lang = lang || context.document.meta.lang
if (typeof link == 'string') {
liveLinks.push({
link: link,
lang: lang
});
}
}
context.href = function (href, lang, link) {
if (_.isBoolean(lang) && link == undefined) {
link = lang;
lang = undefined;
}
if (link == undefined) {
link = true;
} else {
var liveLink = true;
}
let found = mikser.runtime.findHref(context.entity, href, lang);
if (found &&
found._id &&
found._id != context.document._id &&
found.meta.lang == context.document.meta.lang) {
if (link) {
context.documentLink(found);
}
else {
context.documentUnlink(found);
}
} else if (liveLink && (
!found || !found._id_)) {
context.liveLink(href, lang);
}
return found;
}
context.hrefEntity = function (href, lang) {
let found = context.href(href, lang, true);
if (!found._id) return;
return found;
}
context.hrefPage = function (href, page, lang, link) {
let found = context.href(href, lang, link);
if (found && found._id && page > 0) {
let href = found.toString();
found.toString = () => {
return href.replace(path.extname(found.url), '.' + page + '.html');
}
}
return found;
}
context.hrefLang = function (href) {
href = href || context.entity.meta.href;
return mikser.runtime.findHrefLang(href) || {};
}
let processPending = () => {};
context.pending = new Promise((resolve, reject) => {
processPending = resolve;
});
context.process = function(action) {
if (_.isFunction(action) && action.length) {
let capturedContext = extend({}, context);
action = action.bind(capturedContext, [context]);
}
context.pending = context.pending.then(action);
return context.pending;
}
context.prerenders = {}
context.processPrerenders = {}
context.prerender = function(action) {
if (_.isFunction(action) && action.length) {
let capturedContext = extend({}, context);
action = action.bind(capturedContext, [context]);
}
if (!context.prerenders[context.layout._id]) {
context.prerenders[context.layout._id] = new Promise((resolve, reject) => {
context.processPrerenders[context.layout._id] = resolve;
});
}
context.prerenders[context.layout._id] = context.prerenders[context.layout._id].then(action);
return context.prerenders[context.layout._id];
}
let asyncMap;
let buildAsyncMap = Promise.resolve();
context.async = function (action) {
let asyncId = uuid.v1();
let buildMap = action.then((result) => {
asyncMap = asyncMap || {};
asyncMap[asyncId] = result;
});
buildAsyncMap = buildAsyncMap.then(() => buildMap);
return asyncId;
}
context.processAsync = function (action) {
let process = context.process(action);
return context.async(process);
}
if (strategy == constants.RENDER_STRATEGY_PREVIEW) {
return generator.renderContext(context).then((context) => {
context.pending.cancel();
return buildAsyncMap.then(() => {
context.content = replaceMap(context.content || '', asyncMap);
return Promise.resolve(context.content);
});
});
}
let originalLinks;
return mikser.observer.close(document).then(() => {
return mikser.runtime.cleanLinks(document)
}).then((links) => {
originalLinks = links;
if (!document.destination) {
if (strategy > constants.RENDER_STRATEGY_STANDALONE) {
return mikser.runtime.followLinks(document, false);
}
return Promise.resolve();
}
return generator.renderContext(context).then((context) => {
processPending();
return buildAsyncMap.then(() => {
context.content = replaceMap(context.content || '', asyncMap);
return fs.createFileAsync(context.document.destination).then(() => {
return fs.writeFileAsync(context.document.destination, context.content);
});
});
}).then(() => {
documentLinks = _.difference(documentLinks, documentUnlinks);
layoutLinks = _.difference(layoutLinks, layoutUnlinks);
return mikser.runtime.addLinks(document, documentLinks, layoutLinks).then(() => {
if (strategy != constants.RENDER_STRATEGY_STANDALONE) {
return mikser.runtime.followLinks(document, false);
}
return Promise.resolve();
});
}).then(() => {
return context.pending.then(() => {
mikser.loader.dispose(context);
});
}).then(() => {
return mikser.observer.observeEntities(document, liveLinks).then(() => {
return Promise.resolve(context.content);
});
}).catch((err) => {
if (fs.existsSync(document.destination)) {
fs.removeSync(document.destination);
}
throw err;
});
}).catch((err) => {
if (typeof err == 'string') err = new Error(err);
err.diagnose = context.renderPipeline;
if (originalLinks) {
return mikser.runtime.restoreLinks(originalLinks).then(() => {
throw err;
});
}
throw err;
});
};
mikser.generator = generator;
return Promise.resolve(mikser);
}