katex.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. // Test if potential opening or closing delimieter
  2. // Assumes that there is a "$" at state.src[pos]
  3. function isValidDelim (state, pos) {
  4. const max = state.posMax
  5. let canOpen = true
  6. let canClose = true
  7. const prevChar = pos > 0 ? state.src.charCodeAt(pos - 1) : -1
  8. const nextChar = pos + 1 <= max ? state.src.charCodeAt(pos + 1) : -1
  9. // Check non-whitespace conditions for opening and closing, and
  10. // check that closing delimeter isn't followed by a number
  11. if (prevChar === 0x20/* " " */ || prevChar === 0x09/* \t */ ||
  12. (nextChar >= 0x30/* "0" */ && nextChar <= 0x39/* "9" */)) {
  13. canClose = false
  14. }
  15. if (nextChar === 0x20/* " " */ || nextChar === 0x09/* \t */) {
  16. canOpen = false
  17. }
  18. return {
  19. canOpen,
  20. canClose
  21. }
  22. }
  23. export default {
  24. katexInline (state, silent) {
  25. let match, token, res, pos
  26. if (state.src[state.pos] !== '$') { return false }
  27. res = isValidDelim(state, state.pos)
  28. if (!res.canOpen) {
  29. if (!silent) { state.pending += '$' }
  30. state.pos += 1
  31. return true
  32. }
  33. // First check for and bypass all properly escaped delimieters
  34. // This loop will assume that the first leading backtick can not
  35. // be the first character in state.src, which is known since
  36. // we have found an opening delimieter already.
  37. const start = state.pos + 1
  38. match = start
  39. while ((match = state.src.indexOf('$', match)) !== -1) {
  40. // Found potential $, look for escapes, pos will point to
  41. // first non escape when complete
  42. pos = match - 1
  43. while (state.src[pos] === '\\') { pos -= 1 }
  44. // Even number of escapes, potential closing delimiter found
  45. if (((match - pos) % 2) === 1) { break }
  46. match += 1
  47. }
  48. // No closing delimter found. Consume $ and continue.
  49. if (match === -1) {
  50. if (!silent) { state.pending += '$' }
  51. state.pos = start
  52. return true
  53. }
  54. // Check if we have empty content, ie: $$. Do not parse.
  55. if (match - start === 0) {
  56. if (!silent) { state.pending += '$$' }
  57. state.pos = start + 1
  58. return true
  59. }
  60. // Check for valid closing delimiter
  61. res = isValidDelim(state, match)
  62. if (!res.canClose) {
  63. if (!silent) { state.pending += '$' }
  64. state.pos = start
  65. return true
  66. }
  67. if (!silent) {
  68. token = state.push('katex_inline', 'math', 0)
  69. token.markup = '$'
  70. token.content = state.src
  71. // Extract the math part without the $
  72. .slice(start, match)
  73. // Escape the curly braces since they will be interpreted as
  74. // attributes by markdown-it-attrs (the "curly_attributes"
  75. // core rule)
  76. .replaceAll('{', '{{')
  77. .replaceAll('}', '}}')
  78. }
  79. state.pos = match + 1
  80. return true
  81. },
  82. katexBlock (state, start, end, silent) {
  83. let firstLine; let lastLine; let next; let lastPos; let found = false
  84. let pos = state.bMarks[start] + state.tShift[start]
  85. let max = state.eMarks[start]
  86. if (pos + 2 > max) { return false }
  87. if (state.src.slice(pos, pos + 2) !== '$$') { return false }
  88. pos += 2
  89. firstLine = state.src.slice(pos, max)
  90. if (silent) { return true }
  91. if (firstLine.trim().slice(-2) === '$$') {
  92. // Single line expression
  93. firstLine = firstLine.trim().slice(0, -2)
  94. found = true
  95. }
  96. for (next = start; !found;) {
  97. next++
  98. if (next >= end) { break }
  99. pos = state.bMarks[next] + state.tShift[next]
  100. max = state.eMarks[next]
  101. if (pos < max && state.tShift[next] < state.blkIndent) {
  102. // non-empty line with negative indent should stop the list:
  103. break
  104. }
  105. if (state.src.slice(pos, max).trim().slice(-2) === '$$') {
  106. lastPos = state.src.slice(0, max).lastIndexOf('$$')
  107. lastLine = state.src.slice(pos, lastPos)
  108. found = true
  109. }
  110. }
  111. state.line = next + 1
  112. const token = state.push('katex_block', 'math', 0)
  113. token.block = true
  114. token.content = (firstLine && firstLine.trim() ? firstLine + '\n' : '') +
  115. state.getLines(start + 1, next, state.tShift[start], true) +
  116. (lastLine && lastLine.trim() ? lastLine : '')
  117. token.map = [start, state.line]
  118. token.markup = '$$'
  119. return true
  120. }
  121. }