diff --git a/package.json b/package.json index 3ad60ce1..26650324 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "markdown-it-sub": "1.0.0", "markdown-it-sup": "1.0.0", "markdown-it-task-lists": "2.1.1", - "mathjax-node": "2.1.1", + "mathjax": "3.0.5", "mime-types": "2.1.27", "moment": "2.24.0", "moment-timezone": "0.5.28", diff --git a/server/modules/rendering/html-security/renderer.js b/server/modules/rendering/html-security/renderer.js index 4fb9966d..99d3275e 100644 --- a/server/modules/rendering/html-security/renderer.js +++ b/server/modules/rendering/html-security/renderer.js @@ -10,10 +10,12 @@ module.exports = { blockquote: ['class', 'id', 'style'], code: ['class', 'style'], details: ['class', 'style'], + defs: ['stroke', 'fill', 'stroke-width', 'transform'], div: ['class', 'id', 'style'], em: ['class', 'style'], figcaption: ['class', 'style'], figure: ['class', 'style'], + g: ['transform', 'stroke', 'stroke-width', 'fill'], h1: ['class', 'id', 'style'], h2: ['class', 'id', 'style'], h3: ['class', 'id', 'style'], @@ -29,7 +31,7 @@ module.exports = { mark: ['class', 'style'], ol: ['class', 'style', 'start'], p: ['class', 'style'], - path: ['d', 'style'], + path: ['d', 'style', 'id'], pre: ['class', 'style'], section: ['class', 'style'], span: ['class', 'style', 'aria-hidden'], @@ -44,7 +46,8 @@ module.exports = { th: ['align', 'class', 'colspan', 'rowspan', 'style', 'valign'], thead: ['class', 'style'], tr: ['class', 'rowspan', 'style', 'align', 'valign'], - ul: ['class', 'style'] + ul: ['class', 'style'], + use: ['href', 'transform'] }, css: false }) diff --git a/server/modules/rendering/markdown-core/renderer.js b/server/modules/rendering/markdown-core/renderer.js index 2ce8dcf1..40632026 100644 --- a/server/modules/rendering/markdown-core/renderer.js +++ b/server/modules/rendering/markdown-core/renderer.js @@ -36,7 +36,7 @@ module.exports = { for (let child of this.children) { const renderer = require(`../${_.kebabCase(child.key)}/renderer.js`) - renderer.init(mkdown, child.config) + await renderer.init(mkdown, child.config) } return mkdown.render(this.input) diff --git a/server/modules/rendering/markdown-katex/definition.yml b/server/modules/rendering/markdown-katex/definition.yml index cfe478d0..87b3b218 100644 --- a/server/modules/rendering/markdown-katex/definition.yml +++ b/server/modules/rendering/markdown-katex/definition.yml @@ -1,6 +1,6 @@ key: markdownKatex title: Katex -description: LaTeX Math Typesetting Renderer +description: LaTeX Math + Chemical Expression Typesetting Renderer author: requarks.io icon: mdi-math-integral enabledDefault: true diff --git a/server/modules/rendering/markdown-mathjax/definition.yml b/server/modules/rendering/markdown-mathjax/definition.yml new file mode 100644 index 00000000..bf2e6460 --- /dev/null +++ b/server/modules/rendering/markdown-mathjax/definition.yml @@ -0,0 +1,20 @@ +key: markdownMathjax +title: Mathjax +description: LaTeX Math + Chemical Expression Typesetting Renderer +author: requarks.io +icon: mdi-math-integral +enabledDefault: false +dependsOn: markdownCore +props: + useInline: + type: Boolean + default: true + title: Inline TeX + hint: Process inline TeX expressions surrounded by $ symbols. + order: 1 + useBlocks: + type: Boolean + default: true + title: TeX Blocks + hint: Process TeX blocks enclosed by $$ symbols. + order: 2 diff --git a/server/modules/rendering/markdown-mathjax/renderer.js b/server/modules/rendering/markdown-mathjax/renderer.js new file mode 100644 index 00000000..3acfebec --- /dev/null +++ b/server/modules/rendering/markdown-mathjax/renderer.js @@ -0,0 +1,207 @@ +const mjax = require('mathjax') + +/* global WIKI */ + +// ------------------------------------ +// Markdown - MathJax Renderer +// ------------------------------------ + +const extensions = [ + 'bbox', + 'boldsymbol', + 'braket', + 'color', + 'extpfeil', + 'mhchem', + 'newcommand', + 'unicode', + 'verb' +] + +module.exports = { + async init (mdinst, conf) { + const MathJax = await mjax.init({ + loader: { + require: require, + paths: { mathjax: 'mathjax/es5' }, + load: [ + 'input/tex', + 'output/svg', + ...extensions.map(e => `[tex]/${e}`) + ] + }, + tex: { + packages: {'[+]': extensions} + } + }) + if (conf.useInline) { + mdinst.inline.ruler.after('escape', 'mathjax_inline', mathjaxInline) + mdinst.renderer.rules.mathjax_inline = (tokens, idx) => { + try { + const result = MathJax.tex2svg(tokens[idx].content, { + display: false + }) + return MathJax.startup.adaptor.innerHTML(result) + } catch (err) { + WIKI.logger.warn(err) + return tokens[idx].content + } + } + } + if (conf.useBlocks) { + mdinst.block.ruler.after('blockquote', 'mathjax_block', mathjaxBlock, { + alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] + }) + mdinst.renderer.rules.mathjax_block = (tokens, idx) => { + try { + const result = MathJax.tex2svg(tokens[idx].content, { + display: true + }) + return `

` + MathJax.startup.adaptor.innerHTML(result) + `

` + } catch (err) { + WIKI.logger.warn(err) + return tokens[idx].content + } + } + } + } +} + +// Test if potential opening or closing delimieter +// Assumes that there is a "$" at state.src[pos] +function isValidDelim (state, pos) { + let prevChar + let nextChar + let max = state.posMax + let canOpen = true + let canClose = true + + prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1 + nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1 + + // Check non-whitespace conditions for opening and closing, and + // check that closing delimeter isn't followed by a number + if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ || + (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) { + canClose = false + } + if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) { + canOpen = false + } + + return { + canOpen: canOpen, + canClose: canClose + } +} + +function mathjaxInline (state, silent) { + let start, match, token, res, pos + + if (state.src[state.pos] !== '$') { return false } + + res = isValidDelim(state, state.pos) + if (!res.canOpen) { + if (!silent) { state.pending += '$' } + state.pos += 1 + return true + } + + // First check for and bypass all properly escaped delimieters + // This loop will assume that the first leading backtick can not + // be the first character in state.src, which is known since + // we have found an opening delimieter already. + start = state.pos + 1 + match = start + while ((match = state.src.indexOf('$', match)) !== -1) { + // Found potential $, look for escapes, pos will point to + // first non escape when complete + pos = match - 1 + while (state.src[pos] === '\\') { pos -= 1 } + + // Even number of escapes, potential closing delimiter found + if (((match - pos) % 2) === 1) { break } + match += 1 + } + + // No closing delimter found. Consume $ and continue. + if (match === -1) { + if (!silent) { state.pending += '$' } + state.pos = start + return true + } + + // Check if we have empty content, ie: $$. Do not parse. + if (match - start === 0) { + if (!silent) { state.pending += '$$' } + state.pos = start + 1 + return true + } + + // Check for valid closing delimiter + res = isValidDelim(state, match) + if (!res.canClose) { + if (!silent) { state.pending += '$' } + state.pos = start + return true + } + + if (!silent) { + token = state.push('mathjax_inline', 'math', 0) + token.markup = '$' + token.content = state.src.slice(start, match) + } + + state.pos = match + 1 + return true +} + +function mathjaxBlock (state, start, end, silent) { + let firstLine; let lastLine; let next; let lastPos; let found = false; let token + let pos = state.bMarks[start] + state.tShift[start] + let max = state.eMarks[start] + + if (pos + 2 > max) { return false } + if (state.src.slice(pos, pos + 2) !== '$$') { return false } + + pos += 2 + firstLine = state.src.slice(pos, max) + + if (silent) { return true } + if (firstLine.trim().slice(-2) === '$$') { + // Single line expression + firstLine = firstLine.trim().slice(0, -2) + found = true + } + + for (next = start; !found;) { + next++ + + if (next >= end) { break } + + pos = state.bMarks[next] + state.tShift[next] + max = state.eMarks[next] + + if (pos < max && state.tShift[next] < state.blkIndent) { + // non-empty line with negative indent should stop the list: + break + } + + if (state.src.slice(pos, max).trim().slice(-2) === '$$') { + lastPos = state.src.slice(0, max).lastIndexOf('$$') + lastLine = state.src.slice(pos, lastPos) + found = true + } + } + + state.line = next + 1 + + token = state.push('mathjax_block', 'math', 0) + token.block = true + token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') + + state.getLines(start + 1, next, state.tShift[start], true) + + (lastLine && lastLine.trim() ? lastLine : '') + token.map = [ start, state.line ] + token.markup = '$$' + return true +} diff --git a/yarn.lock b/yarn.lock index a401cf3f..217c8287 100644 Binary files a/yarn.lock and b/yarn.lock differ