renderer.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. const mjax = require('mathjax')
  2. /* global WIKI */
  3. // ------------------------------------
  4. // Markdown - MathJax Renderer
  5. // ------------------------------------
  6. const extensions = [
  7. 'bbox',
  8. 'boldsymbol',
  9. 'braket',
  10. 'color',
  11. 'extpfeil',
  12. 'mhchem',
  13. 'newcommand',
  14. 'unicode',
  15. 'verb'
  16. ]
  17. module.exports = {
  18. async init (mdinst, conf) {
  19. const MathJax = await mjax.init({
  20. loader: {
  21. require: require,
  22. paths: { mathjax: 'mathjax/es5' },
  23. load: [
  24. 'input/tex',
  25. 'output/svg',
  26. ...extensions.map(e => `[tex]/${e}`)
  27. ]
  28. },
  29. tex: {
  30. packages: {'[+]': extensions}
  31. }
  32. })
  33. if (conf.useInline) {
  34. mdinst.inline.ruler.after('escape', 'mathjax_inline', mathjaxInline)
  35. mdinst.renderer.rules.mathjax_inline = (tokens, idx) => {
  36. try {
  37. const result = MathJax.tex2svg(tokens[idx].content, {
  38. display: false
  39. })
  40. return MathJax.startup.adaptor.innerHTML(result)
  41. } catch (err) {
  42. WIKI.logger.warn(err)
  43. return tokens[idx].content
  44. }
  45. }
  46. }
  47. if (conf.useBlocks) {
  48. mdinst.block.ruler.after('blockquote', 'mathjax_block', mathjaxBlock, {
  49. alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
  50. })
  51. mdinst.renderer.rules.mathjax_block = (tokens, idx) => {
  52. try {
  53. const result = MathJax.tex2svg(tokens[idx].content, {
  54. display: true
  55. })
  56. return `<p>` + MathJax.startup.adaptor.innerHTML(result) + `</p>`
  57. } catch (err) {
  58. WIKI.logger.warn(err)
  59. return tokens[idx].content
  60. }
  61. }
  62. }
  63. }
  64. }
  65. // Test if potential opening or closing delimieter
  66. // Assumes that there is a "$" at state.src[pos]
  67. function isValidDelim (state, pos) {
  68. let prevChar
  69. let nextChar
  70. let max = state.posMax
  71. let canOpen = true
  72. let canClose = true
  73. prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
  74. nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
  75. // Check non-whitespace conditions for opening and closing, and
  76. // check that closing delimeter isn't followed by a number
  77. if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
  78. (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
  79. canClose = false
  80. }
  81. if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
  82. canOpen = false
  83. }
  84. return {
  85. canOpen: canOpen,
  86. canClose: canClose
  87. }
  88. }
  89. function mathjaxInline (state, silent) {
  90. let start, match, token, res, pos
  91. if (state.src[state.pos] !== '$') { return false }
  92. res = isValidDelim(state, state.pos)
  93. if (!res.canOpen) {
  94. if (!silent) { state.pending += '$' }
  95. state.pos += 1
  96. return true
  97. }
  98. // First check for and bypass all properly escaped delimieters
  99. // This loop will assume that the first leading backtick can not
  100. // be the first character in state.src, which is known since
  101. // we have found an opening delimieter already.
  102. start = state.pos + 1
  103. match = start
  104. while ((match = state.src.indexOf('$', match)) !== -1) {
  105. // Found potential $, look for escapes, pos will point to
  106. // first non escape when complete
  107. pos = match - 1
  108. while (state.src[pos] === '\\') { pos -= 1 }
  109. // Even number of escapes, potential closing delimiter found
  110. if (((match - pos) % 2) === 1) { break }
  111. match += 1
  112. }
  113. // No closing delimter found. Consume $ and continue.
  114. if (match === -1) {
  115. if (!silent) { state.pending += '$' }
  116. state.pos = start
  117. return true
  118. }
  119. // Check if we have empty content, ie: $$. Do not parse.
  120. if (match - start === 0) {
  121. if (!silent) { state.pending += '$$' }
  122. state.pos = start + 1
  123. return true
  124. }
  125. // Check for valid closing delimiter
  126. res = isValidDelim(state, match)
  127. if (!res.canClose) {
  128. if (!silent) { state.pending += '$' }
  129. state.pos = start
  130. return true
  131. }
  132. if (!silent) {
  133. token = state.push('mathjax_inline', 'math', 0)
  134. token.markup = '$'
  135. token.content = state.src.slice(start, match)
  136. }
  137. state.pos = match + 1
  138. return true
  139. }
  140. function mathjaxBlock (state, start, end, silent) {
  141. let firstLine; let lastLine; let next; let lastPos; let found = false; let token
  142. let pos = state.bMarks[start] + state.tShift[start]
  143. let max = state.eMarks[start]
  144. if (pos + 2 > max) { return false }
  145. if (state.src.slice(pos, pos + 2) !== '$$') { return false }
  146. pos += 2
  147. firstLine = state.src.slice(pos, max)
  148. if (silent) { return true }
  149. if (firstLine.trim().slice(-2) === '$$') {
  150. // Single line expression
  151. firstLine = firstLine.trim().slice(0, -2)
  152. found = true
  153. }
  154. for (next = start; !found;) {
  155. next++
  156. if (next >= end) { break }
  157. pos = state.bMarks[next] + state.tShift[next]
  158. max = state.eMarks[next]
  159. if (pos < max && state.tShift[next] < state.blkIndent) {
  160. // non-empty line with negative indent should stop the list:
  161. break
  162. }
  163. if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
  164. lastPos = state.src.slice(0, max).lastIndexOf('$$')
  165. lastLine = state.src.slice(pos, lastPos)
  166. found = true
  167. }
  168. }
  169. state.line = next + 1
  170. token = state.push('mathjax_block', 'math', 0)
  171. token.block = true
  172. token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
  173. state.getLines(start + 1, next, state.tShift[start], true) +
  174. (lastLine && lastLine.trim() ? lastLine : '')
  175. token.map = [ start, state.line ]
  176. token.markup = '$$'
  177. return true
  178. }