renderer.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. const katex = require('katex')
  2. const chemParse = require('./mhchem')
  3. /* global WIKI */
  4. // ------------------------------------
  5. // Markdown - KaTeX Renderer
  6. // ------------------------------------
  7. //
  8. // Includes code from https://github.com/liradb2000/markdown-it-katex
  9. // Add \ce, \pu, and \tripledash to the KaTeX macros.
  10. katex.__defineMacro('\\ce', function(context) {
  11. return chemParse(context.consumeArgs(1)[0], 'ce')
  12. })
  13. katex.__defineMacro('\\pu', function(context) {
  14. return chemParse(context.consumeArgs(1)[0], 'pu')
  15. })
  16. // Needed for \bond for the ~ forms
  17. // Raise by 2.56mu, not 2mu. We're raising a hyphen-minus, U+002D, not
  18. // a mathematical minus, U+2212. So we need that extra 0.56.
  19. katex.__defineMacro('\\tripledash', '{\\vphantom{-}\\raisebox{2.56mu}{$\\mkern2mu' + '\\tiny\\text{-}\\mkern1mu\\text{-}\\mkern1mu\\text{-}\\mkern2mu$}}')
  20. module.exports = {
  21. init (mdinst, conf) {
  22. const macros = {}
  23. if (conf.useInline) {
  24. mdinst.inline.ruler.after('escape', 'katex_inline', katexInline)
  25. mdinst.renderer.rules.katex_inline = (tokens, idx) => {
  26. try {
  27. return katex.renderToString(tokens[idx].content, {
  28. displayMode: false, macros
  29. })
  30. } catch (err) {
  31. WIKI.logger.warn(err)
  32. return tokens[idx].content
  33. }
  34. }
  35. }
  36. if (conf.useBlocks) {
  37. mdinst.block.ruler.after('blockquote', 'katex_block', katexBlock, {
  38. alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
  39. })
  40. mdinst.renderer.rules.katex_block = (tokens, idx) => {
  41. try {
  42. return `<p>` + katex.renderToString(tokens[idx].content, {
  43. displayMode: true, macros
  44. }) + `</p>`
  45. } catch (err) {
  46. WIKI.logger.warn(err)
  47. return tokens[idx].content
  48. }
  49. }
  50. }
  51. }
  52. }
  53. // Test if potential opening or closing delimieter
  54. // Assumes that there is a "$" at state.src[pos]
  55. function isValidDelim (state, pos) {
  56. let prevChar
  57. let nextChar
  58. let max = state.posMax
  59. let canOpen = true
  60. let canClose = true
  61. prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
  62. nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
  63. // Check non-whitespace conditions for opening and closing, and
  64. // check that closing delimeter isn't followed by a number
  65. if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
  66. (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
  67. canClose = false
  68. }
  69. if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
  70. canOpen = false
  71. }
  72. return {
  73. canOpen: canOpen,
  74. canClose: canClose
  75. }
  76. }
  77. function katexInline (state, silent) {
  78. let start, match, token, res, pos
  79. if (state.src[state.pos] !== '$') { return false }
  80. res = isValidDelim(state, state.pos)
  81. if (!res.canOpen) {
  82. if (!silent) { state.pending += '$' }
  83. state.pos += 1
  84. return true
  85. }
  86. // First check for and bypass all properly escaped delimieters
  87. // This loop will assume that the first leading backtick can not
  88. // be the first character in state.src, which is known since
  89. // we have found an opening delimieter already.
  90. start = state.pos + 1
  91. match = start
  92. while ((match = state.src.indexOf('$', match)) !== -1) {
  93. // Found potential $, look for escapes, pos will point to
  94. // first non escape when complete
  95. pos = match - 1
  96. while (state.src[pos] === '\\') { pos -= 1 }
  97. // Even number of escapes, potential closing delimiter found
  98. if (((match - pos) % 2) === 1) { break }
  99. match += 1
  100. }
  101. // No closing delimter found. Consume $ and continue.
  102. if (match === -1) {
  103. if (!silent) { state.pending += '$' }
  104. state.pos = start
  105. return true
  106. }
  107. // Check if we have empty content, ie: $$. Do not parse.
  108. if (match - start === 0) {
  109. if (!silent) { state.pending += '$$' }
  110. state.pos = start + 1
  111. return true
  112. }
  113. // Check for valid closing delimiter
  114. res = isValidDelim(state, match)
  115. if (!res.canClose) {
  116. if (!silent) { state.pending += '$' }
  117. state.pos = start
  118. return true
  119. }
  120. if (!silent) {
  121. token = state.push('katex_inline', 'math', 0)
  122. token.markup = '$'
  123. token.content = state.src.slice(start, match)
  124. }
  125. state.pos = match + 1
  126. return true
  127. }
  128. function katexBlock (state, start, end, silent) {
  129. let firstLine; let lastLine; let next; let lastPos; let found = false; let token
  130. let pos = state.bMarks[start] + state.tShift[start]
  131. let max = state.eMarks[start]
  132. if (pos + 2 > max) { return false }
  133. if (state.src.slice(pos, pos + 2) !== '$$') { return false }
  134. pos += 2
  135. firstLine = state.src.slice(pos, max)
  136. if (silent) { return true }
  137. if (firstLine.trim().slice(-2) === '$$') {
  138. // Single line expression
  139. firstLine = firstLine.trim().slice(0, -2)
  140. found = true
  141. }
  142. for (next = start; !found;) {
  143. next++
  144. if (next >= end) { break }
  145. pos = state.bMarks[next] + state.tShift[next]
  146. max = state.eMarks[next]
  147. if (pos < max && state.tShift[next] < state.blkIndent) {
  148. // non-empty line with negative indent should stop the list:
  149. break
  150. }
  151. if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
  152. lastPos = state.src.slice(0, max).lastIndexOf('$$')
  153. lastLine = state.src.slice(pos, lastPos)
  154. found = true
  155. }
  156. }
  157. state.line = next + 1
  158. token = state.push('katex_block', 'math', 0)
  159. token.block = true
  160. token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
  161. state.getLines(start + 1, next, state.tShift[start], true) +
  162. (lastLine && lastLine.trim() ? lastLine : '')
  163. token.map = [ start, state.line ]
  164. token.markup = '$$'
  165. return true
  166. }