renderer.js 5.4 KB

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