2
0

renderer.js 4.9 KB

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