eslint integration

This commit is contained in:
Klemek
2021-03-30 14:53:26 +02:00
parent f6d6f04d59
commit e9d67985a6
17 changed files with 4176 additions and 1538 deletions
+248 -240
View File
@@ -21,19 +21,19 @@ app.use(bodyParser.json());
* @type {{warn: string, ok: string, error: string}}
*/
const cons = {
ok: '\x1b[32m✔\x1b[0m %s',
warn: '\x1b[33m⚠\x1b[0m %s',
error: '\x1b[31m✘\x1b[0m %s',
ok: '\x1b[32m✔\x1b[0m %s',
warn: '\x1b[33m⚠\x1b[0m %s',
error: '\x1b[31m✘\x1b[0m %s',
};
module.exports = (config) => {
/**
/**
* Fetch articles from the data folder and send success as a response
* @param success
* @param error
*/
let reload;
/**
let reload;
/**
* Render the page with the view engine and catch errors
* @param req
* @param res
@@ -41,256 +41,264 @@ module.exports = (config) => {
* @param data - data to pass to the view
* @param code - code to send along the page
*/
let render;
/**
let render;
/**
* Show an error with the correct page
* @param req
* @param res
* @param code - error code
*/
let showError;
const fw = require('./file_walker')(config);
const renderer = require('./renderer')(config);
let showError;
const fw = require('./file_walker')(config);
const renderer = require('./renderer')(config);
// set view engine from configuration
app.set('view engine', config['view_engine']);
// reroute the views folder to the root folder
app.set('views', path.join(__dirname, '..'));
// set view engine from configuration
app.set('view engine', config['view_engine']);
// reroute the views folder to the root folder
app.set('views', path.join(__dirname, '..'));
const articles = {};
let lastRSS = '';
let host = config['host'];
const articles = {};
let lastRSS = '';
let host = config['host'];
reload = (success, error) => {
fw.fetchArticles((err, dict) => {
if (err) {
console.error(cons.error, 'error loading articles : ' + err);
return error ? error() : null;
}
Object.keys(articles).forEach((key) => delete articles[key]);
Object.keys(dict).forEach((key) => articles[key] = dict[key]);
const nb = Object.keys(articles).length;
const dnb = Object.values(articles).filter(a => a.draft).length;
if (nb > 0)
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
else
console.log(cons.warn, `no articles loaded, check your configuration`);
lastRSS = '';
success();
});
};
if (config['test'])
app.reload = reload;
render = (req, res, vPath, data, code = 200) => {
data.info = {
title: config['home']['title'],
description: config['home']['description'],
host: host,
version: pjson.version,
request: req,
config: config
};
res.render(vPath, data, (err, html) => {
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
showError(req, res, 500);
} else if (err) {
res.sendStatus(500);
console.log(cons.error, `failed to render error page : ${err}`);
} else
res.status(code).send(html);
});
};
showError = (req, res, code) => {
const errorPath = path.join(config['data_dir'], config['home']['error']);
fs.access(errorPath, fs.constants.R_OK, (err) => {
if (err)
res.sendStatus(code);
else
render(req, res, errorPath, {error: code}, code);
});
};
app.use((req, res, next) => {
if (!host) {
host = 'http://' + req.headers.host;
console.log(cons.ok, 'Currently hosted on ' + host);
}
next();
});
//rate limit for safer server
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: config['rate_limit']
});
app.use(limiter);
//log request at result end
app.use((req, res, next) => {
if (config['access_log']) {
const end = res.end;
res.end = (chunk, encoding) => {
fs.appendFile(config['access_log'],
`${res.statusCode} ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n`,
{encoding: 'UTF-8'}, () => {
res.end = end;
res.end(chunk, encoding);
});
};
}
next();
});
// home endpoint : send the correct index page or error if not existing
app.get('/', (req, res) => {
const homePath = path.join(config['data_dir'], config['home']['index']);
fs.access(homePath, fs.constants.R_OK, (err) => {
if (err)
showError(req, res, 404);
else
render(req, res, homePath,
{
articles: Object.values(articles)
.filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path))
});
});
});
//RSS endpoint
app.get(config['rss']['endpoint'], (req, res) => {
if (config['modules']['rss']) {
if (!lastRSS) {
const feed = new Rss({
'title': config['rss']['title'],
'description': config['rss']['description'],
'feed_url': host + req.url,
'site_url': host
});
Object.values(articles)
.slice(0, config['rss']['length'])
.forEach((article) => {
feed.item({
title: article.title,
url: host + article.url,
date: article.date
});
});
lastRSS = feed.xml();
}
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
} else {
showError(req, res, 404);
}
});
//webhook endpoint
app.post(config['webhook']['endpoint'], (req, res) => {
if (config['modules']['webhook']) {
if (config['webhook']['signature_header'] && config['webhook']['secret']) {
const payload = JSON.stringify(req.body) || '';
const hmac = crypto.createHmac('sha1', config['webhook']['secret']);
const digest = 'sha1=' + hmac.update(payload).digest('hex');
const checksum = req.headers[config['webhook']['signature_header']];
if (!checksum || !digest || checksum !== digest) {
return res.sendStatus(403);
}
}
cp.exec(config['webhook']['pull_command'], {cwd: path.join(__dirname, '..', config['data_dir'])}, (err) => {
if (err) {
console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`);
return res.sendStatus(500);
}
reload(() => {
res.sendStatus(200);
});
});
} else {
res.sendStatus(400);
}
});
//rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png
app.use((req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url))
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
next();
});
// catch all article urls and render them
app.get('*', (req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) {
const articlePath = req.path.substr(1, 10);
const article = articles[articlePath];
if (!article)
showError(req, res, 404);
else {
renderer.render(article.realPath, (err, html) => {
if (err) {
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
return showError(req, res, 500);
}
article.content = html;
const templatePath = path.join(config['data_dir'], config['article']['template']);
fs.access(templatePath, fs.constants.R_OK, (err) => {
reload = (success, error) => {
fw.fetchArticles((err, dict) => {
if (err) {
console.log(cons.error, `no template found at ${templatePath}`);
showError(req, res, 500);
} else
render(req, res, templatePath, {article: article});
});
console.error(cons.error, 'error loading articles : ' + err);
return error ? error() : null;
}
Object.keys(articles).forEach((key) => delete articles[key]);
Object.keys(dict).forEach((key) => articles[key] = dict[key]);
const nb = Object.keys(articles).length;
const dnb = Object.values(articles).filter(a => a.draft).length;
if (nb > 0) {
console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''} (${dnb} drafted)`);
} else {
console.log(cons.warn, 'no articles loaded, check your configuration');
}
lastRSS = '';
success();
});
}
} else {
next();
};
if (config['test']) {
app.reload = reload;
}
});
// catch all hidden file type and return 404
config['home']['hidden'].forEach(pathMatcher => {
app.get(pathMatcher, (req, res) => {
showError(req, res, 404);
render = (req, res, vPath, data, code = 200) => {
data.info = {
title: config['home']['title'],
description: config['home']['description'],
host: host,
version: pjson.version,
request: req,
config: config
};
res.render(vPath, data, (err, html) => {
if (err && vPath !== path.join(config['data_dir'], config['home']['error'])) {
console.log(cons.error, `failed to render page ${vPath} : ${err}`);
showError(req, res, 500);
} else if (err) {
res.sendStatus(500);
console.log(cons.error, `failed to render error page : ${err}`);
} else {
res.status(code).send(html);
}
});
};
showError = (req, res, code) => {
const errorPath = path.join(config['data_dir'], config['home']['error']);
fs.access(errorPath, fs.constants.R_OK, (err) => {
if (err) {
res.sendStatus(code);
} else {
render(req, res, errorPath, {error: code}, code);
}
});
};
app.use((req, res, next) => {
if (!host) {
host = 'http://' + req.headers.host;
console.log(cons.ok, 'Currently hosted on ' + host);
}
next();
});
});
// serve all static files via get
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
// catch express.static errors (mostly not found) by displaying 404
app.get('*', (req, res) => {
showError(req, res, 404);
});
// catch all other methods and return 400
app.all('*', (req, res) => {
res.status(400).send('bad request');
});
//log all server errors
app.use((err, req, res, next) => {
console.log(cons.error, `error when handling ${req.method} ${req.path} request : ${err}`);
if (!config['error_log'])
next(err);
fs.appendFile(config['error_log'],
`500 ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n${err.stack}\n`,
{encoding: 'UTF-8'}, () => {
next(err);
});
});
// must be use in a server.js to start the server
app.start = () => {
reload(() => {
app.listen(config['node_port'], () => {
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
});
//rate limit for safer server
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: config['rate_limit']
});
};
app.use(limiter);
return app;
//log request at result end
app.use((req, res, next) => {
if (config['access_log']) {
const end = res.end;
res.end = (chunk, encoding) => {
fs.appendFile(config['access_log'],
`${res.statusCode} ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n`,
{encoding: 'UTF-8'}, () => {
res.end = end;
res.end(chunk, encoding);
});
};
}
next();
});
// home endpoint : send the correct index page or error if not existing
app.get('/', (req, res) => {
const homePath = path.join(config['data_dir'], config['home']['index']);
fs.access(homePath, fs.constants.R_OK, (err) => {
if (err) {
showError(req, res, 404);
} else {
render(req, res, homePath,
{
articles: Object.values(articles)
.filter(d => !d.draft).sort((a, b) => ('' + b.path).localeCompare(a.path))
});
}
});
});
//RSS endpoint
app.get(config['rss']['endpoint'], (req, res) => {
if (config['modules']['rss']) {
if (!lastRSS) {
const feed = new Rss({
'title': config['rss']['title'],
'description': config['rss']['description'],
'feed_url': host + req.url,
'site_url': host
});
Object.values(articles)
.slice(0, config['rss']['length'])
.forEach((article) => {
feed.item({
title: article.title,
url: host + article.url,
date: article.date
});
});
lastRSS = feed.xml();
}
res.type(req.headers['user-agent'].match(/Mozilla/) ? 'text/xml' : 'rss').send(lastRSS);
} else {
showError(req, res, 404);
}
});
//webhook endpoint
app.post(config['webhook']['endpoint'], (req, res) => {
if (config['modules']['webhook']) {
if (config['webhook']['signature_header'] && config['webhook']['secret']) {
const payload = JSON.stringify(req.body) || '';
const hmac = crypto.createHmac('sha1', config['webhook']['secret']);
const digest = 'sha1=' + hmac.update(payload).digest('hex');
const checksum = req.headers[config['webhook']['signature_header']];
if (!checksum || !digest || checksum !== digest) {
return res.sendStatus(403);
}
}
cp.exec(config['webhook']['pull_command'], {cwd: path.join(__dirname, '..', config['data_dir'])}, (err) => {
if (err) {
console.log(cons.error, `command '${config['webhook']['pull_command']}' failed : ${err}`);
return res.sendStatus(500);
}
reload(() => {
res.sendStatus(200);
});
});
} else {
res.sendStatus(400);
}
});
//rewrite urls to hide articles titles : /2019/05/05/sometitle/img.png => /2019/05/05/img.png
app.use((req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\//.test(req.url)) {
req.url = req.url.slice(0, 11) + req.url.slice(req.url.lastIndexOf('/'));
}
next();
});
// catch all article urls and render them
app.get('*', (req, res, next) => {
if (/^\/\d{4}\/\d{2}\/\d{2}\/$/.test(req.path)) {
const articlePath = req.path.substr(1, 10);
const article = articles[articlePath];
if (!article) {
showError(req, res, 404);
} else {
renderer.render(article.realPath, (err, html) => {
if (err) {
console.log(cons.error, `failed to render article ${req.path} : ${err}`);
return showError(req, res, 500);
}
article.content = html;
const templatePath = path.join(config['data_dir'], config['article']['template']);
fs.access(templatePath, fs.constants.R_OK, (err) => {
if (err) {
console.log(cons.error, `no template found at ${templatePath}`);
showError(req, res, 500);
} else {
render(req, res, templatePath, {article: article});
}
});
});
}
} else {
next();
}
});
// catch all hidden file type and return 404
config['home']['hidden'].forEach(pathMatcher => {
app.get(pathMatcher, (req, res) => {
showError(req, res, 404);
});
});
// serve all static files via get
app.get('*', express.static(path.join(__dirname, '..', config['data_dir'])));
// catch express.static errors (mostly not found) by displaying 404
app.get('*', (req, res) => {
showError(req, res, 404);
});
// catch all other methods and return 400
app.all('*', (req, res) => {
res.status(400).send('bad request');
});
//log all server errors
app.use((err, req, res, next) => {
console.log(cons.error, `error when handling ${req.method} ${req.path} request : ${err}`);
if (!config['error_log']) {
next(err);
}
fs.appendFile(config['error_log'],
`500 ${req.method} ${req.url} ${new Date().toUTCString()} ${req.ips.join(' ') || req.ip}\n${err.stack}\n`,
{encoding: 'UTF-8'}, () => {
next(err);
});
});
// must be use in a server.js to start the server
app.start = () => {
reload(() => {
app.listen(config['node_port'], () => {
console.log(cons.ok, `gitblog.md server listening on port ${config['node_port']}`);
});
});
};
return app;
};
+21 -21
View File
@@ -8,28 +8,28 @@ const fs = require('fs');
* @returns {*}
*/
const merge = (ref, src) => {
if (typeof ref !== typeof src) {
return ref;
} else if (ref.length && !src.length) {
return ref;
} else if (ref.length && src.length) {
return src;
} else if (typeof ref === 'object') {
const out = {};
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
return out;
} else {
return src;
}
if (typeof ref !== typeof src) {
return ref;
} else if (ref.length && !src.length) {
return ref;
} else if (ref.length && src.length) {
return src;
} else if (typeof ref === 'object') {
const out = {};
Object.keys(ref).forEach((key) => out[key] = merge(ref[key], src[key]));
return out;
} else {
return src;
}
};
module.exports = () => {
try {
let configData = fs.readFileSync('config.json', {encoding: 'UTF-8'});
let config = JSON.parse(configData);
return merge(refConfig, config);
} catch (error) {
console.log('\x1b[33m⚠\x1b[0m %s', 'Failed to load config.json : ' + error);
return refConfig;
}
try {
let configData = fs.readFileSync('config.json', {encoding: 'UTF-8'});
let config = JSON.parse(configData);
return merge(refConfig, config);
} catch (error) {
console.log('\x1b[33m⚠\x1b[0m %s', 'Failed to load config.json : ' + error);
return refConfig;
}
};
+85 -75
View File
@@ -9,29 +9,33 @@ const joinUrl = (...paths) => path.join(...paths).replace(/\\/g, '/');
* @param cb
*/
const getFileTree = (dir, cb) => {
let list = [];
let remaining = 0;
fs.readdir(dir, {withFileTypes: true}, (err, items) => {
if (err)
return cb(err);
items.forEach((item) => {
if (item.isDirectory()) {
remaining++;
getFileTree(path.join(dir, item.name), (err, out) => {
if (err)
let list = [];
let remaining = 0;
fs.readdir(dir, {withFileTypes: true}, (err, items) => {
if (err) {
return cb(err);
list.push(...out);
remaining--;
if (remaining === 0)
cb(null, list);
}
items.forEach((item) => {
if (item.isDirectory()) {
remaining++;
getFileTree(path.join(dir, item.name), (err, out) => {
if (err) {
return cb(err);
}
list.push(...out);
remaining--;
if (remaining === 0) {
cb(null, list);
}
});
} else {
list.push(path.join(dir, item.name));
}
});
} else {
list.push(path.join(dir, item.name));
}
if (remaining === 0) {
cb(null, list);
}
});
if (remaining === 0)
cb(null, list);
});
};
/**
@@ -41,71 +45,77 @@ const getFileTree = (dir, cb) => {
* @param cb
*/
const readIndexFile = (path, thumbnailTag, cb) => {
fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => {
if (err)
return cb(err);
fs.readFile(path, {encoding: 'UTF-8'}, (err, data) => {
if (err) {
return cb(err);
}
let info = {};
let info = {};
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
info.title = regRes1 ? regRes1[2].trim() : undefined;
const regRes1 = data.match(/(^|[^#])#([^#\r\n]*)\r?\n?$/m);
info.title = regRes1 ? regRes1[2].trim() : undefined;
const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i');
const regRes2 = data.match(thumbnailRegEx);
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
const thumbnailRegEx = new RegExp(`!\\[${thumbnailTag}]\\(([^)]*)\\)`, 'i');
const regRes2 = data.match(thumbnailRegEx);
info.thumbnail = regRes2 ? regRes2[1].trim() : undefined;
cb(null, info);
});
cb(null, info);
});
};
module.exports = (config) => {
return {
fileTree: config['test'] ? getFileTree : undefined,
readIndexFile: config['test'] ? readIndexFile : undefined,
/**
return {
fileTree: config['test'] ? getFileTree : undefined,
readIndexFile: config['test'] ? readIndexFile : undefined,
/**
* find and read all articles inside the data directory
* @param cb
*/
fetchArticles: (cb) => {
getFileTree(config['data_dir'], (err, fileList) => {
if (err)
return cb(err);
const paths = fileList
.map((p) => p.substr(config['data_dir'].length + 1).split(path.sep))
.filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) &&
fetchArticles: (cb) => {
getFileTree(config['data_dir'], (err, fileList) => {
if (err) {
return cb(err);
}
const paths = fileList
.map((p) => p.substr(config['data_dir'].length + 1).split(path.sep))
.filter((p) => p.length === 4 && (p[3] === config['article']['index'] || p[3] === config['article']['draft']) &&
/^\d{4}$/.test(p[0]) && /^\d{2}$/.test(p[1]) && /^\d{2}$/.test(p[2]));
if (paths.length === 0)
cb(null, {});
const articles = {};
let remaining = 0;
paths.forEach((p) => {
const article = {
path: joinUrl(p[0], p[1], p[2]),
draft: p[3] === config['article']['draft'],
realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]),
year: parseInt(p[0]),
month: parseInt(p[1]),
day: parseInt(p[2])
};
article.date = new Date(article.year, article.month, article.day);
article.date.setUTCHours(0);
remaining++;
readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => {
if (err)
return cb(err);
article.title = info.title || config['article']['default_title'];
article.thumbnail = info.thumbnail ? joinUrl(article.path, info.thumbnail) : config['article']['default_thumbnail'];
article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_');
article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
if (!articles[article.path] || !article.draft)
articles[article.path] = article;
remaining--;
if (remaining === 0)
cb(null, articles);
});
});
if (paths.length === 0) {
cb(null, {});
}
const articles = {};
let remaining = 0;
paths.forEach((p) => {
const article = {
path: joinUrl(p[0], p[1], p[2]),
draft: p[3] === config['article']['draft'],
realPath: path.join(config['data_dir'], p[0], p[1], p[2], p[3]),
year: parseInt(p[0]),
month: parseInt(p[1]),
day: parseInt(p[2])
};
article.date = new Date(article.year, article.month, article.day);
article.date.setUTCHours(0);
remaining++;
readIndexFile(article.realPath, config['article']['thumbnail_tag'], (err, info) => {
if (err) {
return cb(err);
}
article.title = info.title || config['article']['default_title'];
article.thumbnail = info.thumbnail ? joinUrl(article.path, info.thumbnail) : config['article']['default_thumbnail'];
article.escapedTitle = article.title.toLowerCase().replace(/[^\w]/gm, ' ').trim().replace(/ /gm, '_');
article.url = '/' + joinUrl(article.path, article.escapedTitle) + '/';
if (!articles[article.path] || !article.draft) {
articles[article.path] = article;
}
remaining--;
if (remaining === 0) {
cb(null, articles);
}
});
});
});
}
};
});
}
};
};
+16 -14
View File
@@ -3,28 +3,30 @@ const path = require('path');
const ncp = require('ncp').ncp;
const copy = (src, dest) => {
ncp(src, dest, function (err) {
if (err)
console.error(err);
else
console.log(`copied ${src} to ${dest}`);
});
ncp(src, dest, function (err) {
if (err) {
console.error(err);
} else {
console.log(`copied ${src} to ${dest}`);
}
});
};
copy(path.join('src', 'config.default.json'), 'config.example.json');
if (!fs.existsSync('data')) {
fs.mkdirSync('data');
fs.mkdirSync('data');
copy(path.join('sample_data', 'home'), 'data');
copy(path.join('sample_data', 'home'), 'data');
const pad0 = (n) => ('0' + n).substr(-2);
const pad0 = (n) => ('0' + n).substr(-2);
const datetime = new Date();
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
const datetime = new Date();
const dir = path.join('data', datetime.getFullYear().toString(), pad0(datetime.getMonth() + 1), pad0(datetime.getDate()));
if (!fs.existsSync(dir))
fs.mkdirSync(dir, {recursive: true});
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, {recursive: true});
}
copy(path.join('sample_data', 'article'), dir);
copy(path.join('sample_data', 'article'), dir);
}
+204 -194
View File
@@ -3,216 +3,226 @@ const path = require('path');
const showdown = require('showdown');
module.exports = (config) => {
const converter = new showdown.Converter(config['showdown']);
const converter = new showdown.Converter(config['showdown']);
/**
/**
* get parts outside of codes/scripts
* @param {string} data
* @returns {{index:number, end:number, text:string}[]} parts
*/
const getParts = (data) => {
let parts = [];
let match;
let i = 0;
while ((match = /```/m.exec(data.slice(i)))) {
parts.push({
index: i,
text: data.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < data.length)
parts.push({
index: i,
text: data.slice(i, data.length),
});
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
// detect scripts outside of code
parts.forEach((p, pi) => {
let i = 0;
const subParts = [];
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < p.text.length)
subParts.push({
index: p.index + i,
text: p.text.slice(i, p.text.length),
});
parts.splice(pi, 1, ...subParts);
});
parts.forEach(part => part.end = part.index + part.text.length);
return parts;
};
const renderShowDown = (data, cb) => {
const html = converter.makeHtml(data);
cb(html);
};
let Prism;
if (config['modules']['prism'])
Prism = require('node-prismjs');
const renderPrism = (data, cb) => {
if (!config['modules']['prism'])
return cb(data);
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
let match;
while ((match = codeRegex.exec(data))) {
const lang = match[1].trim();
const code = match[2].trim();
const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang);
data = data.slice(0, match.index) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
}
cb(data);
};
if (config['modules']['plantuml']) {
require('./script_loader')(path.join(__dirname, 'lib', 'plantuml_synchro.js'));
}
const renderPlantUML = (data, cb) => {
if (!config['modules']['plantuml'])
return cb(data);
const parts = getParts(data);
const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m;
let match;
parts.forEach(part => {
while ((match = umlRegex.exec(part.text))) {
const code = match[1].trim();
const s = unescape(encodeURIComponent(code)); // jshint ignore:line
const compressed = global['zip_deflate'](s);
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;// jshint ignore:line
part.text = part.text.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
};
let mjAPI;
if (config['modules']['mathjax']) {
mjAPI = require('mathjax-node');
mjAPI.config({
MathJax: {
tex2jax: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
const getParts = (data) => {
let parts = [];
let match;
let i = 0;
while ((match = /```/m.exec(data.slice(i)))) {
parts.push({
index: i,
text: data.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < data.length) {
parts.push({
index: i,
text: data.slice(i, data.length),
});
}
}
});
}
const renderMathJax = (data, cb) => {
if (!config['modules']['mathjax'])
return cb(data);
parts = parts.filter((p, i) => i % 2 === 0); //filter out code parts
const parts = getParts(data);
const doMJ = (match, format, i) => {
const eq = match[1].trim();
const output = config['mathjax']['output_format'];
const mjConf = {
math: eq,
format: format,
speakText: config['mathjax']['speak_text']
};
mjConf[output] = true;
mjAPI.typeset(mjConf, (res) => {
data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length);
renderMathJax(data, (data2) => {
cb(data2);
// detect scripts outside of code
parts.forEach((p, pi) => {
let i = 0;
const subParts = [];
while ((match = /(<script>((?:(?!<\/script>)[\s\S])*)<\/script>)/gm.exec(p.text.slice(i)))) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, i + match.index),
});
i += match.index + match[0].length;
}
if (i < p.text.length) {
subParts.push({
index: p.index + i,
text: p.text.slice(i, p.text.length),
});
}
parts.splice(pi, 1, ...subParts);
});
});
parts.forEach(part => part.end = part.index + part.text.length);
return parts;
};
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const inlineEqRegex = /\$([^$\n]*)\$/;
const renderShowDown = (data, cb) => {
const html = converter.makeHtml(data);
cb(html);
};
for (let i = 0; i < parts.length; i++) {
let match;
if ((match = eqRegex.exec(parts[i].text))) {
return doMJ(match, 'TeX', i);
} else if ((match = inlineEqRegex.exec(parts[i].text))) {
return doMJ(match, 'inline-TeX', i);
}
let Prism;
if (config['modules']['prism']) {
Prism = require('node-prismjs');
}
cb(data);
};
let faDiagrams;
let toml;
if (config['modules']['fa-diagrams']) {
faDiagrams = require('fa-diagrams');
toml = require('@iarna/toml');
}
const renderFaDiagrams = (data, cb) => {
if (!config['modules']['fa-diagrams'])
return cb(data);
const parts = getParts(data);
const diagramsRegex = /@startfad\r?\n((?:(?!@endfad)[\s\S])*)\r?\n@endfad/m;
let match;
parts.forEach(part => {
while ((match = diagramsRegex.exec(part.text))) {
const code = match[1].trim();
let output;
try {
const diagData = toml.parse(code);
const findLineBreaks = (data) => {
Object.keys(data).forEach(key => {
if (typeof data[key] === 'object')
findLineBreaks(data[key]);
else if (typeof data[key] === 'string')
data[key] = data[key].replace(/\\n/gm, '\n');
});
};
findLineBreaks(diagData);
output = faDiagrams.compute(diagData);
} catch (err) {
output = `<b style="color:red">${err.toString()}</b>`;
const renderPrism = (data, cb) => {
if (!config['modules']['prism']) {
return cb(data);
}
part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
};
const codeRegex = /```([\w-]+)\r?\n((?:(?!```)[\s\S])*)\r?\n```/m;
let match;
while ((match = codeRegex.exec(data))) {
const lang = match[1].trim();
const code = match[2].trim();
const block = Prism.highlight(code, Prism.languages[lang] || Prism.languages.autoit, lang);
data = data.slice(0, match.index) + `<pre><code class="${lang} language-${lang}">` + block + '</code></pre>' + data.slice(match.index + match[0].length);
}
cb(data);
};
return {
getParts: config['test'] ? getParts : undefined,
renderShowDown: config['test'] ? renderShowDown : undefined,
renderPrism: config['test'] ? renderPrism : undefined,
renderPlantUML: config['test'] ? renderPlantUML : undefined,
renderMathJax: config['test'] ? renderMathJax : undefined,
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
render: (file, cb) => {
fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => {
if (err)
return cb(err);
renderPlantUML(data, (data) => {
renderFaDiagrams(data, (data) => {
renderMathJax(data, (data) => {
renderPrism(data, (data) => {
renderShowDown(data, (html) => {
cb(null, html);
});
});
});
});
});
});
if (config['modules']['plantuml']) {
require('./script_loader')(path.join(__dirname, 'lib', 'plantuml_synchro.js'));
}
};
const renderPlantUML = (data, cb) => {
/* global encode64 */
if (!config['modules']['plantuml']) {
return cb(data);
}
const parts = getParts(data);
const umlRegex = /@startuml\r?\n((?:(?!@enduml)[\s\S])*)\r?\n@enduml/m;
let match;
parts.forEach(part => {
while ((match = umlRegex.exec(part.text))) {
const code = match[1].trim();
const s = unescape(encodeURIComponent(code));
const compressed = global['zip_deflate'](s);
const url = `http://www.plantuml.com/plantuml/${config['plantuml']['output_format']}/${encode64(compressed)}`;
part.text = part.text.slice(0, match.index) + `<img alt="generated PlantUML diagram" src="${url}">` + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
};
let mjAPI;
if (config['modules']['mathjax']) {
mjAPI = require('mathjax-node');
mjAPI.config({
MathJax: {
tex2jax: {
inlineMath: [['$', '$']],
displayMath: [['$$', '$$']]
}
}
});
}
const renderMathJax = (data, cb) => {
if (!config['modules']['mathjax']) {
return cb(data);
}
const parts = getParts(data);
const doMJ = (match, format, i) => {
const eq = match[1].trim();
const output = config['mathjax']['output_format'];
const mjConf = {
math: eq,
format: format,
speakText: config['mathjax']['speak_text']
};
mjConf[output] = true;
mjAPI.typeset(mjConf, (res) => {
data = data.slice(0, parts[i].index + match.index) + res[output] + data.slice(parts[i].index + match.index + match[0].length);
renderMathJax(data, (data2) => {
cb(data2);
});
});
};
const eqRegex = /\$\$((?:(?!\$\$)[\s\S])*)\$\$/m;
const inlineEqRegex = /\$([^$\n]*)\$/;
for (let i = 0; i < parts.length; i++) {
let match;
if ((match = eqRegex.exec(parts[i].text))) {
return doMJ(match, 'TeX', i);
} else if ((match = inlineEqRegex.exec(parts[i].text))) {
return doMJ(match, 'inline-TeX', i);
}
}
cb(data);
};
let faDiagrams;
let toml;
if (config['modules']['fa-diagrams']) {
faDiagrams = require('fa-diagrams');
toml = require('@iarna/toml');
}
const renderFaDiagrams = (data, cb) => {
if (!config['modules']['fa-diagrams']) {
return cb(data);
}
const parts = getParts(data);
const diagramsRegex = /@startfad\r?\n((?:(?!@endfad)[\s\S])*)\r?\n@endfad/m;
let match;
parts.forEach(part => {
while ((match = diagramsRegex.exec(part.text))) {
const code = match[1].trim();
let output;
try {
const diagData = toml.parse(code);
const findLineBreaks = (data) => {
Object.keys(data).forEach(key => {
if (typeof data[key] === 'object') {
findLineBreaks(data[key]);
} else if (typeof data[key] === 'string') {
data[key] = data[key].replace(/\\n/gm, '\n');
}
});
};
findLineBreaks(diagData);
output = faDiagrams.compute(diagData);
} catch (err) {
output = `<b style="color:red">${err.toString()}</b>`;
}
part.text = part.text.slice(0, match.index) + output + part.text.slice(match.index + match[0].length);
}
data = data.slice(0, part.index) + part.text + data.slice(part.end);
});
cb(data);
};
return {
getParts: config['test'] ? getParts : undefined,
renderShowDown: config['test'] ? renderShowDown : undefined,
renderPrism: config['test'] ? renderPrism : undefined,
renderPlantUML: config['test'] ? renderPlantUML : undefined,
renderMathJax: config['test'] ? renderMathJax : undefined,
renderFaDiagrams: config['test'] ? renderFaDiagrams : undefined,
render: (file, cb) => {
fs.readFile(file, {encoding: 'UTF-8'}, (err, data) => {
if (err) {
return cb(err);
}
renderPlantUML(data, (data) => {
renderFaDiagrams(data, (data) => {
renderMathJax(data, (data) => {
renderPrism(data, (data) => {
renderShowDown(data, (html) => {
cb(null, html);
});
});
});
});
});
});
}
};
};
+1 -1
View File
@@ -5,6 +5,6 @@ const fs = require('fs');
* @param scriptPath
*/
module.exports = (scriptPath) => {
eval.call(global, fs.readFileSync(scriptPath, {encoding: 'UTF-8'}));
eval.call(global, fs.readFileSync(scriptPath, {encoding: 'UTF-8'}));
};