|
|
|
/*
|
|
|
|
* This file is part of moemark-renderer.
|
|
|
|
*
|
|
|
|
* Copyright (c) 2016 Menci <huanghaorui301@gmail.com>
|
|
|
|
*
|
|
|
|
* moemark-renderer is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* moemark-renderer is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with moemark-renderer. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
const MoeMark = require('moemark');
|
|
|
|
const katex = require('katex');
|
|
|
|
const mj = require('mathjax-node');
|
|
|
|
|
|
|
|
let defaultCache = {
|
|
|
|
data: {},
|
|
|
|
get(key) {
|
|
|
|
return this.data[key];
|
|
|
|
},
|
|
|
|
set(key, val) {
|
|
|
|
this.data[key] = val;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
let config = {
|
|
|
|
highlight: require('./highlight')
|
|
|
|
};
|
|
|
|
|
|
|
|
let uuid = require('uuid');
|
|
|
|
|
|
|
|
function render(s, cb) {
|
|
|
|
if (!s.trim()) return cb('');
|
|
|
|
|
|
|
|
let mathCnt = 0, mathPending = 0, maths = new Array(), mathID = new Array(), hlCnt = 0, hlPending = 0, hls = new Array(), hlID = new Array(), res, callback, ss, cache = render.cache, cacheOption = render.cacheOption, finished = false;
|
|
|
|
if (cacheOption.result) {
|
|
|
|
let x = cache.get('RES_' + s);
|
|
|
|
if (x !== undefined) return cb(x);
|
|
|
|
}
|
|
|
|
|
|
|
|
MoeMark.setOptions({
|
|
|
|
lineNumber: false,
|
|
|
|
math: true,
|
|
|
|
highlight: function(code, lang) {
|
|
|
|
if (cacheOption.highlight) {
|
|
|
|
let x = cache.get('H_' + lang + '_' + code);
|
|
|
|
if (x !== undefined) return x;
|
|
|
|
}
|
|
|
|
let id = hlCnt;
|
|
|
|
hlCnt++, hlPending++;
|
|
|
|
config.highlight(code, lang, res => {
|
|
|
|
hls[id] = res;
|
|
|
|
if (cacheOption.highlight) cache.set('H_' + lang + '_' + code, res);
|
|
|
|
if (!--hlPending) finish();
|
|
|
|
});
|
|
|
|
return hlID[id] = uuid();
|
|
|
|
},
|
|
|
|
mathRenderer: function(str, display) {
|
|
|
|
let mathFinish = (error, result) => {
|
|
|
|
if (error) maths[id] = '<p><div style="display: inline-block; border: 1px solid #000; "><strong>' + error.toString() + '</strong></div></p>';
|
|
|
|
else if (display) maths[id] = '<p style="text-align: center; ">' + result + '</p>';
|
|
|
|
else maths[id] = result;
|
|
|
|
if (cacheOption.math) cache.set('M_' + display + '_' + str, maths[id]);
|
|
|
|
if (!--mathPending) finish();
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = mathCnt;
|
|
|
|
mathCnt++, mathPending++;
|
|
|
|
|
|
|
|
try {
|
|
|
|
let x = cache.get('M_' + display + '_' + str);
|
|
|
|
if (x !== undefined) process.nextTick(() => mathFinish(null, x));
|
|
|
|
else {
|
|
|
|
let res = katex.renderToString(str, { displayMode: display });
|
|
|
|
process.nextTick(() => mathFinish(null, '<span style="zoom: 1.01; ">' + res + '</span>'));
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
mj.typeset({
|
|
|
|
math: str,
|
|
|
|
format: display ? 'TeX' : 'inline-TeX',
|
|
|
|
svg: true,
|
|
|
|
width: 0
|
|
|
|
}, data => {
|
|
|
|
mathFinish(data.errors, data.svg);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return mathID[id] = uuid();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
function finish() {
|
|
|
|
if (finished || !res || mathPending || hlPending) return;
|
|
|
|
finished = true;
|
|
|
|
if (maths.length || hls.length) {
|
|
|
|
for (let i = 0; i < maths.length; i++) {
|
|
|
|
res = res.replace(mathID[i], maths[i]);
|
|
|
|
}
|
|
|
|
for (let i = 0; i < hls.length; i++) {
|
|
|
|
res = res.replace(hlID[i], hls[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (cacheOption.result) cache.set('RES_' + s, res);
|
|
|
|
cb(res);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
let XSS = require('xss');
|
|
|
|
let CSSFilter = require('cssfilter');
|
|
|
|
let whiteList = Object.assign({}, require('xss/lib/default').whiteList);
|
|
|
|
delete whiteList.audio;
|
|
|
|
delete whiteList.video;
|
|
|
|
for (let tag in whiteList) whiteList[tag] = whiteList[tag].concat(['style', 'class']);
|
|
|
|
let xss = new XSS.FilterXSS({
|
|
|
|
whiteList: whiteList,
|
|
|
|
stripIgnoreTag: true,
|
|
|
|
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
|
|
|
if (tag.toLowerCase() === 'img' && name.toLowerCase() === 'src' && value.startsWith('data:image/')) return name + '="' + XSS.escapeAttrValue(value) + '"';
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let replaceXSS = s => {
|
|
|
|
s = xss.process(s);
|
|
|
|
if (s) {
|
|
|
|
s = `<div style="position: relative; overflow: hidden; ">${s}</div>`;
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
};
|
|
|
|
|
|
|
|
res = replaceXSS(MoeMark(s));
|
|
|
|
if (mathPending == 0 && hlPending == 0) {
|
|
|
|
finish();
|
|
|
|
}
|
|
|
|
} catch(e) {
|
|
|
|
cb(e);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
render.moemark = MoeMark;
|
|
|
|
render.cache = defaultCache;
|
|
|
|
render.cacheOption = {
|
|
|
|
highlight: true,
|
|
|
|
math: true,
|
|
|
|
result: false
|
|
|
|
};
|
|
|
|
render.config = config;
|
|
|
|
|
|
|
|
module.exports = render;
|