|
|
|
@ -1,27 +1,26 @@
|
|
|
|
|
/* |
|
|
|
|
* 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'); |
|
|
|
|
const { markdown } = require('syzoj-renderer'); |
|
|
|
|
const XSS = require('xss'); |
|
|
|
|
const CSSFilter = require('cssfilter'); |
|
|
|
|
const xssWhiteList = Object.assign({}, require('xss/lib/default').whiteList); |
|
|
|
|
|
|
|
|
|
delete xssWhiteList.audio; |
|
|
|
|
delete xssWhiteList.video; |
|
|
|
|
|
|
|
|
|
for (const tag in xssWhiteList) { |
|
|
|
|
xssWhiteList[tag] = xssWhiteList[tag].concat(['style', 'class']); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const xss = new XSS.FilterXSS({ |
|
|
|
|
whiteList: xssWhiteList, |
|
|
|
|
stripIgnoreTag: true, |
|
|
|
|
onTagAttr: (tag, name, value, isWhiteAttr) => { |
|
|
|
|
if (tag.toLowerCase() === 'img' && name.toLowerCase() === 'src' && value.startsWith('data:image/')) { |
|
|
|
|
return name + '="' + XSS.escapeAttrValue(value) + '"'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
let defaultCache = { |
|
|
|
|
const defaultCache = { |
|
|
|
|
data: {}, |
|
|
|
|
get(key) { |
|
|
|
|
return this.data[key]; |
|
|
|
@ -31,124 +30,14 @@ let defaultCache = {
|
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
} |
|
|
|
|
function filter(html) { |
|
|
|
|
html = xss.process(html); |
|
|
|
|
if (html) { |
|
|
|
|
html = `<div style="position: relative; overflow: hidden; ">${html}</div>`; |
|
|
|
|
} |
|
|
|
|
return html; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
render.moemark = MoeMark; |
|
|
|
|
render.cache = defaultCache; |
|
|
|
|
render.cacheOption = { |
|
|
|
|
highlight: true, |
|
|
|
|
math: true, |
|
|
|
|
result: false |
|
|
|
|
module.exports = (markdownCode, callback) => { |
|
|
|
|
markdown(markdownCode, defaultCache, filter).then(callback); |
|
|
|
|
}; |
|
|
|
|
render.config = config; |
|
|
|
|
|
|
|
|
|
module.exports = render; |
|
|
|
|