markdown-it-katex.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /* eslint-disable eqeqeq */
  2. /* eslint-disable no-unused-vars */
  3. /* eslint-disable @typescript-eslint/no-unused-vars */
  4. /* eslint-disable prefer-const */
  5. /* eslint-disable camelcase */
  6. /* Process inline math */
  7. /*
  8. Like markdown-it-simplemath, this is a stripped down, simplified version of:
  9. https://github.com/runarberg/markdown-it-math
  10. It differs in that it takes (a subset of) LaTeX as input and relies on KaTeX
  11. for rendering output.
  12. */
  13. /* jslint node: true */
  14. 'use strict'
  15. const katex = require('katex')
  16. // Test if potential opening or closing delimieter
  17. // Assumes that there is a "$" at state.src[pos]
  18. function isValidDelim(state, pos) {
  19. let prevChar
  20. let nextChar
  21. const max = state.posMax
  22. let can_open = true
  23. let can_close = true
  24. prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
  25. nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
  26. // Check non-whitespace conditions for opening and closing, and
  27. // check that closing delimeter isn't followed by a number
  28. if (
  29. prevChar === 0x20 /* " " */ ||
  30. prevChar === 0x09 /* \t */ ||
  31. (nextChar >= 0x30 /* "0" */ && nextChar <= 0x39) /* "9" */
  32. ) {
  33. can_close = false
  34. }
  35. if (nextChar === 0x20 /* " " */ || nextChar === 0x09 /* \t */) {
  36. can_open = false
  37. }
  38. return {
  39. can_open,
  40. can_close
  41. }
  42. }
  43. function math_inline(state, silent) {
  44. let start, match, token, res, pos, esc_count
  45. if (state.src[state.pos] !== '$') {
  46. return false
  47. }
  48. res = isValidDelim(state, state.pos)
  49. if (!res.can_open) {
  50. if (!silent) {
  51. state.pending += '$'
  52. }
  53. state.pos += 1
  54. return true
  55. }
  56. // First check for and bypass all properly escaped delimieters
  57. // This loop will assume that the first leading backtick can not
  58. // be the first character in state.src, which is known since
  59. // we have found an opening delimieter already.
  60. start = state.pos + 1
  61. match = start
  62. while ((match = state.src.indexOf('$', match)) !== -1) {
  63. // Found potential $, look for escapes, pos will point to
  64. // first non escape when complete
  65. pos = match - 1
  66. while (state.src[pos] === '\\') {
  67. pos -= 1
  68. }
  69. // Even number of escapes, potential closing delimiter found
  70. if ((match - pos) % 2 == 1) {
  71. break
  72. }
  73. match += 1
  74. }
  75. // No closing delimter found. Consume $ and continue.
  76. if (match === -1) {
  77. if (!silent) {
  78. state.pending += '$'
  79. }
  80. state.pos = start
  81. return true
  82. }
  83. // Check if we have empty content, ie: $$. Do not parse.
  84. if (match - start === 0) {
  85. if (!silent) {
  86. state.pending += '$$'
  87. }
  88. state.pos = start + 1
  89. return true
  90. }
  91. // Check for valid closing delimiter
  92. res = isValidDelim(state, match)
  93. if (!res.can_close) {
  94. if (!silent) {
  95. state.pending += '$'
  96. }
  97. state.pos = start
  98. return true
  99. }
  100. if (!silent) {
  101. token = state.push('math_inline', 'math', 0)
  102. token.markup = '$'
  103. token.content = state.src.slice(start, match)
  104. }
  105. state.pos = match + 1
  106. return true
  107. }
  108. function math_block(state, start, end, silent) {
  109. let firstLine
  110. let lastLine
  111. let next
  112. let lastPos
  113. let found = false
  114. let token
  115. let pos = state.bMarks[start] + state.tShift[start]
  116. let max = state.eMarks[start]
  117. if (pos + 2 > max) {
  118. return false
  119. }
  120. if (state.src.slice(pos, pos + 2) !== '$$') {
  121. return false
  122. }
  123. pos += 2
  124. firstLine = state.src.slice(pos, max)
  125. if (silent) {
  126. return true
  127. }
  128. if (firstLine.trim().slice(-2) === '$$') {
  129. // Single line expression
  130. firstLine = firstLine.trim().slice(0, -2)
  131. found = true
  132. }
  133. for (next = start; !found;) {
  134. next++
  135. if (next >= end) {
  136. break
  137. }
  138. pos = state.bMarks[next] + state.tShift[next]
  139. max = state.eMarks[next]
  140. if (pos < max && state.tShift[next] < state.blkIndent) {
  141. // non-empty line with negative indent should stop the list:
  142. break
  143. }
  144. if (
  145. state.src
  146. .slice(pos, max)
  147. .trim()
  148. .slice(-2) === '$$'
  149. ) {
  150. lastPos = state.src.slice(0, max).lastIndexOf('$$')
  151. lastLine = state.src.slice(pos, lastPos)
  152. found = true
  153. }
  154. }
  155. state.line = next + 1
  156. token = state.push('math_block', 'math', 0)
  157. token.block = true
  158. token.content =
  159. (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
  160. state.getLines(start + 1, next, state.tShift[start], true) +
  161. (lastLine && lastLine.trim() ? lastLine : '')
  162. token.map = [start, state.line]
  163. token.markup = '$$'
  164. return true
  165. }
  166. module.exports = function math_plugin(md, options) {
  167. // Default options
  168. options = options || {}
  169. // set KaTeX as the renderer for markdown-it-simplemath
  170. const katexInline = function (latex) {
  171. options.displayMode = false
  172. try {
  173. return katex.renderToString(latex, options)
  174. } catch (error) {
  175. if (options.throwOnError) {
  176. // eslint-disable-next-line no-console
  177. console.log(error)
  178. }
  179. return latex
  180. }
  181. }
  182. const inlineRenderer = function (tokens, idx) {
  183. return katexInline(tokens[idx].content)
  184. }
  185. const katexBlock = function (latex) {
  186. options.displayMode = true
  187. try {
  188. return '<p>' + katex.renderToString(latex, options) + '</p>'
  189. } catch (error) {
  190. if (options.throwOnError) {
  191. // eslint-disable-next-line no-console
  192. console.log(error)
  193. }
  194. return latex
  195. }
  196. }
  197. const blockRenderer = function (tokens, idx) {
  198. return katexBlock(tokens[idx].content) + '\n'
  199. }
  200. md.inline.ruler.after('escape', 'math_inline', math_inline)
  201. md.block.ruler.after('blockquote', 'math_block', math_block, {
  202. alt: ['paragraph', 'reference', 'blockquote', 'list']
  203. })
  204. md.renderer.rules.math_inline = inlineRenderer
  205. md.renderer.rules.math_block = blockRenderer
  206. }