2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
const path = require('path');
|
|
|
|
|
|
|
|
const fs = require('fs-extra');
|
|
|
|
const log = require('fancy-log');
|
|
|
|
const { minify } = require('html-minifier-terser');
|
2020-02-27 18:57:39 -08:00
|
|
|
const { resolve, readFile, ENGINE } = require('./resolve');
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
const handlebars = require('handlebars');
|
|
|
|
const HandlebarsKit = require('hbs-kit');
|
|
|
|
HandlebarsKit.load(handlebars);
|
|
|
|
|
|
|
|
const slugs = require('slugify');
|
|
|
|
const slugify = (s) => slugs(s, { remove: /[*+~.,()'"!?:@/\\]/g }).toLowerCase();
|
|
|
|
const striptags = require('string-strip-html');
|
|
|
|
|
|
|
|
const markdownIt = require('markdown-it');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const markdownEngines = {
|
|
|
|
full: markdownIt({
|
|
|
|
html: true,
|
|
|
|
linkify: true,
|
|
|
|
typographer: true,
|
|
|
|
})
|
|
|
|
.enable('image')
|
|
|
|
.use(require('markdown-it-anchor'), {
|
|
|
|
permalink: true,
|
|
|
|
permalinkClass: 'header-link',
|
|
|
|
permalinkSymbol: '<img src="/images/svg/paragraph.svg">',
|
|
|
|
slugify,
|
|
|
|
})
|
2020-03-06 19:30:48 -08:00
|
|
|
.use(require('./lib/markdown-raw-html'), { debug: false }),
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
preview: markdownIt({
|
|
|
|
html: false,
|
|
|
|
linkify: false,
|
|
|
|
typographer: true,
|
|
|
|
})
|
2020-02-28 10:31:13 -08:00
|
|
|
.use(require('./lib/markdown-token-filter')),
|
2020-02-21 20:05:52 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
function markdown (mode, input, env) {
|
|
|
|
input = input.replace(/\{!\{([\s\S]*?)\}!\}/mg, (match, contents) => {
|
|
|
|
try {
|
|
|
|
const result = handlebars.compile(contents)(env);
|
2020-03-06 19:30:48 -08:00
|
|
|
return 'æææ' + result + 'æææ';
|
2020-02-21 20:05:52 -08:00
|
|
|
} catch (e) {
|
|
|
|
log.error(e);
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (mode === 'preview') {
|
|
|
|
input = striptags(input
|
|
|
|
.replace(/<!--\[[\s\S]*?\]-->/g, '')
|
2020-03-06 19:30:48 -08:00
|
|
|
.replace(/æææ[\s\S]*?æææ/gi, ''),
|
2020-02-21 20:05:52 -08:00
|
|
|
).trim();
|
|
|
|
if (input.length > 1000) input = input.slice(0, 1000) + '…';
|
|
|
|
input = input ? markdownEngines[mode].render(input) : '';
|
|
|
|
} else {
|
|
|
|
input = input.replace(/<!--[[\]]-->/g, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
return input ? markdownEngines[mode].render(input, env) : '';
|
|
|
|
}
|
|
|
|
|
|
|
|
function stripIndent (input) {
|
|
|
|
const match = input.match(/^[^\S\n]*(?=\S)/gm);
|
|
|
|
const indent = match && Math.min(...match.map((el) => el.length));
|
|
|
|
|
|
|
|
if (indent) {
|
|
|
|
const regexp = new RegExp(`^.{${indent}}`, 'gm');
|
|
|
|
input = input.replace(regexp, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
|
|
|
const MINIFY_CONFIG = {
|
|
|
|
conservativeCollapse: true,
|
|
|
|
collapseWhitespace: true,
|
|
|
|
minifyCSS: true,
|
|
|
|
removeComments: true,
|
|
|
|
removeRedundantAttributes: true,
|
|
|
|
};
|
|
|
|
|
|
|
|
const HANDLEBARS_PARTIALS = {
|
|
|
|
layout: 'templates/layout.hbs',
|
|
|
|
};
|
|
|
|
|
|
|
|
module.exports = exports = async function (prod) {
|
|
|
|
for (const [ name, file ] of Object.entries(HANDLEBARS_PARTIALS)) {
|
|
|
|
try {
|
2020-02-25 19:37:10 -08:00
|
|
|
const contents = await readFile(file);
|
2020-02-21 20:05:52 -08:00
|
|
|
const template = handlebars.compile(contents.toString('utf8'));
|
|
|
|
handlebars.registerPartial(name, template);
|
|
|
|
} catch (e) {
|
2020-02-25 19:37:10 -08:00
|
|
|
log.error('Could not execute load partial ' + file, e);
|
2020-02-21 20:05:52 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-29 18:33:31 -08:00
|
|
|
const pageTemplateRaw = await readFile('templates/page.hbs');
|
2020-02-21 20:05:52 -08:00
|
|
|
if (!pageTemplateRaw) throw new Error('Post template was empty?');
|
|
|
|
try {
|
|
|
|
var pageTemplate = handlebars.compile(pageTemplateRaw.toString('utf8'));
|
|
|
|
} catch (e) {
|
2020-02-29 18:33:31 -08:00
|
|
|
log.error('Crash while loading page template', e);
|
2020-02-21 20:05:52 -08:00
|
|
|
}
|
|
|
|
|
2020-02-25 19:37:10 -08:00
|
|
|
const revManifest = prod && await fs.readJson(resolve('rev-manifest.json')).catch(() => {}).then((r) => r || {});
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
const helpers = new Injectables(prod, revManifest);
|
|
|
|
handlebars.registerHelper('import', helpers.import());
|
|
|
|
handlebars.registerHelper('markdown', helpers.markdown());
|
|
|
|
handlebars.registerHelper('icon', helpers.icon());
|
|
|
|
handlebars.registerHelper('prod', helpers.production());
|
|
|
|
handlebars.registerHelper('rev', helpers.rev());
|
|
|
|
|
|
|
|
const shrink = (input) => (prod ? minify(input, MINIFY_CONFIG) : input);
|
|
|
|
|
|
|
|
const result = {
|
2020-02-27 18:57:39 -08:00
|
|
|
[ENGINE.HANDLEBARS]: (source, env) => {
|
2020-02-21 20:05:52 -08:00
|
|
|
const template = handlebars.compile(source);
|
|
|
|
return shrink(template(env));
|
|
|
|
},
|
2020-02-27 18:57:39 -08:00
|
|
|
[ENGINE.MARKDOWN]: (source, env) => shrink(pageTemplate({ ...env, contents: markdown('full', source, env) })),
|
|
|
|
[ENGINE.OTHER]: (source) => shrink(source),
|
2020-02-29 18:30:05 -08:00
|
|
|
MARKDOWN_CONTENT: (source, env) => markdown('full', source, env),
|
|
|
|
MARKDOWN_PREVIEW: (source, env) => markdown('preview', source, env),
|
2020-02-21 20:05:52 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
class Injectables {
|
|
|
|
|
|
|
|
constructor (prod, revManifest) {
|
|
|
|
this.prod = prod;
|
|
|
|
this.revManifest = revManifest;
|
|
|
|
this.injections = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
_parsePath (tpath, local, type) {
|
2020-02-25 19:37:10 -08:00
|
|
|
if (tpath[0] === '/') tpath = resolve(tpath.slice(1));
|
|
|
|
else if (tpath[0] === '~') tpath = resolve('templates', tpath.slice(2));
|
2020-02-21 20:05:52 -08:00
|
|
|
else tpath = path.resolve(local.cwd, tpath);
|
|
|
|
if (type && !tpath.endsWith(type)) tpath += '.' + type;
|
|
|
|
return tpath;
|
|
|
|
}
|
|
|
|
|
|
|
|
_template (tpath, make) {
|
2020-02-27 18:57:39 -08:00
|
|
|
if (!tpath) throw new Error('Received an empty template path: ' + tpath);
|
2020-02-21 20:05:52 -08:00
|
|
|
if (this.injections[tpath]) return this.injections[tpath];
|
|
|
|
|
|
|
|
if (!fs.existsSync(tpath)) {
|
2020-02-27 18:57:39 -08:00
|
|
|
throw new Error('Injectable does not exist: ' + tpath);
|
2020-02-21 20:05:52 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
let contents;
|
|
|
|
try {
|
|
|
|
contents = fs.readFileSync(tpath).toString('utf8');
|
|
|
|
if (make) contents = make(contents);
|
|
|
|
this.injections[tpath] = contents;
|
|
|
|
return contents;
|
|
|
|
} catch (e) {
|
2020-02-25 19:37:10 -08:00
|
|
|
log.error(e, 'An error occured while loading the injectable: ' + tpath);
|
2020-02-21 20:05:52 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
rev () {
|
|
|
|
const self = this;
|
|
|
|
return function (url) {
|
|
|
|
if (!url) return '';
|
|
|
|
if (url[0] === '/') url = url.substr(1);
|
|
|
|
if (self.prod && self.revManifest[url]) return '/' + self.revManifest[url];
|
|
|
|
return '/' + url;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
production () {
|
|
|
|
const self = this;
|
|
|
|
return function (options) {
|
|
|
|
if (!options.fn) return self.prod;
|
|
|
|
return self.prod ? options.fn(this) : options.inverse(this);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
markdown () {
|
|
|
|
const self = this;
|
|
|
|
return function (...args) {
|
2020-03-02 20:28:25 -08:00
|
|
|
const { fn, data } = args.pop();
|
2020-02-21 20:05:52 -08:00
|
|
|
let contents;
|
|
|
|
|
|
|
|
if (fn) {
|
2020-03-05 19:40:18 -08:00
|
|
|
contents = stripIndent(fn(data.root));
|
2020-02-21 20:05:52 -08:00
|
|
|
} else {
|
|
|
|
let tpath = args.shift();
|
2020-03-02 20:28:25 -08:00
|
|
|
tpath = self._parsePath(tpath, data.root.local, 'md');
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
contents = self._template(tpath);
|
|
|
|
}
|
|
|
|
|
2020-03-02 20:28:25 -08:00
|
|
|
contents = markdown('full', contents, data);
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
return new handlebars.SafeString(contents);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
import () {
|
|
|
|
const self = this;
|
|
|
|
return function (tpath, ...args) {
|
2020-03-02 20:28:25 -08:00
|
|
|
const { hash, data } = args.pop();
|
2020-02-21 20:05:52 -08:00
|
|
|
const value = args.shift();
|
2020-03-05 19:40:18 -08:00
|
|
|
const context = handlebars.createFrame(value || data.root);
|
2020-02-21 20:05:52 -08:00
|
|
|
Object.assign(context, hash || {});
|
|
|
|
|
2020-03-02 20:28:25 -08:00
|
|
|
tpath = self._parsePath(tpath, data.root.local, 'hbs');
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
try {
|
|
|
|
const contents = self._template(tpath, handlebars.compile)(context);
|
|
|
|
return new handlebars.SafeString(contents);
|
|
|
|
} catch (e) {
|
2020-02-27 18:57:39 -08:00
|
|
|
log.error('Could not execute import template ' + tpath, e);
|
2020-02-21 20:05:52 -08:00
|
|
|
return '';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
icon () {
|
|
|
|
const self = this;
|
|
|
|
return function (name, ...args) {
|
2020-03-02 20:28:25 -08:00
|
|
|
const { hash, data } = args.pop();
|
|
|
|
const tpath = path.join(data.root.local.root, 'svg', name + '.svg');
|
2020-02-21 20:05:52 -08:00
|
|
|
|
|
|
|
try {
|
|
|
|
const contents = self._template(tpath, (s) =>
|
|
|
|
handlebars.compile(`<span class="svg-icon" {{#if size}}style="width:{{size}}px;height:{{size}}px"{{/if}}>${s}</span>`),
|
|
|
|
)({ size: hash && hash.size });
|
|
|
|
|
|
|
|
return new handlebars.SafeString(contents);
|
|
|
|
} catch (e) {
|
2020-02-27 18:57:39 -08:00
|
|
|
log.error('Could not execute import template ' + tpath, e);
|
2020-02-21 20:05:52 -08:00
|
|
|
return '';
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|