/* * This file is part of moemark-renderer. * * Copyright (c) 2016 Menci * * 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 . */ 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] = '

' + error.toString() + '

'; else if (display) maths[id] = '

' + result + '

'; 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, '' + res + '')); } } 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 = `
${s}
`; } 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;