katex-extension.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import katex from 'katex';
  2. const DELIMITER_LIST = [
  3. { left: '$$', right: '$$', display: true },
  4. { left: '$', right: '$', display: false },
  5. { left: '\\pu{', right: '}', display: false },
  6. { left: '\\ce{', right: '}', display: false },
  7. { left: '\\(', right: '\\)', display: false },
  8. { left: '\\[', right: '\\]', display: true },
  9. { left: '\\begin{equation}', right: '\\end{equation}', display: true }
  10. ];
  11. // const DELIMITER_LIST = [
  12. // { left: '$$', right: '$$', display: false },
  13. // { left: '$', right: '$', display: false },
  14. // ];
  15. // const inlineRule = /^(\${1,2})(?!\$)((?:\\.|[^\\\n])*?(?:\\.|[^\\\n\$]))\1(?=[\s?!\.,:?!。,:]|$)/;
  16. // const blockRule = /^(\${1,2})\n((?:\\[^]|[^\\])+?)\n\1(?:\n|$)/;
  17. let inlinePatterns = [];
  18. let blockPatterns = [];
  19. function escapeRegex(string) {
  20. return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
  21. }
  22. function generateRegexRules(delimiters) {
  23. delimiters.forEach((delimiter) => {
  24. const { left, right, display } = delimiter;
  25. // Ensure regex-safe delimiters
  26. const escapedLeft = escapeRegex(left);
  27. const escapedRight = escapeRegex(right);
  28. if (!display) {
  29. // For inline delimiters, we match everything
  30. inlinePatterns.push(`${escapedLeft}((?:\\\\[^]|[^\\\\])+?)${escapedRight}`);
  31. } else {
  32. // Block delimiters doubles as inline delimiters when not followed by a newline
  33. inlinePatterns.push(`${escapedLeft}(?!\\n)((?:\\\\[^]|[^\\\\])+?)(?!\\n)${escapedRight}`);
  34. blockPatterns.push(`${escapedLeft}\\n((?:\\\\[^]|[^\\\\])+?)\\n${escapedRight}`);
  35. }
  36. });
  37. // Math formulas can end in special characters
  38. const inlineRule = new RegExp(
  39. `^(${inlinePatterns.join('|')})(?=[\\s?。,!-\/:-@[-\`{-~]|$)`,
  40. 'u'
  41. );
  42. const blockRule = new RegExp(`^(${blockPatterns.join('|')})(?=[\\s?。,!-\/:-@[-\`{-~]|$)`, 'u');
  43. return { inlineRule, blockRule };
  44. }
  45. const { inlineRule, blockRule } = generateRegexRules(DELIMITER_LIST);
  46. export default function (options = {}) {
  47. return {
  48. extensions: [inlineKatex(options), blockKatex(options)]
  49. };
  50. }
  51. function katexStart(src, displayMode: boolean) {
  52. let ruleReg = displayMode ? blockRule : inlineRule;
  53. let indexSrc = src;
  54. while (indexSrc) {
  55. let index = -1;
  56. let startIndex = -1;
  57. let startDelimiter = '';
  58. let endDelimiter = '';
  59. for (let delimiter of DELIMITER_LIST) {
  60. if (delimiter.display !== displayMode) {
  61. continue;
  62. }
  63. startIndex = indexSrc.indexOf(delimiter.left);
  64. if (startIndex === -1) {
  65. continue;
  66. }
  67. index = startIndex;
  68. startDelimiter = delimiter.left;
  69. endDelimiter = delimiter.right;
  70. }
  71. if (index === -1) {
  72. return;
  73. }
  74. // Check if the delimiter is preceded by a special character.
  75. // If it does, then it's potentially a math formula.
  76. const f = index === 0 || indexSrc.charAt(index - 1).match(/[\s?。,!-\/:-@[-`{-~]/);
  77. if (f) {
  78. const possibleKatex = indexSrc.substring(index);
  79. if (possibleKatex.match(ruleReg)) {
  80. return index;
  81. }
  82. }
  83. indexSrc = indexSrc.substring(index + startDelimiter.length).replace(endDelimiter, '');
  84. }
  85. }
  86. function katexTokenizer(src, tokens, displayMode: boolean) {
  87. let ruleReg = displayMode ? blockRule : inlineRule;
  88. let type = displayMode ? 'blockKatex' : 'inlineKatex';
  89. const match = src.match(ruleReg);
  90. if (match) {
  91. const text = match
  92. .slice(2)
  93. .filter((item) => item)
  94. .find((item) => item.trim());
  95. return {
  96. type,
  97. raw: match[0],
  98. text: text,
  99. displayMode
  100. };
  101. }
  102. }
  103. function inlineKatex(options) {
  104. return {
  105. name: 'inlineKatex',
  106. level: 'inline',
  107. start(src) {
  108. return katexStart(src, false);
  109. },
  110. tokenizer(src, tokens) {
  111. return katexTokenizer(src, tokens, false);
  112. }
  113. };
  114. }
  115. function blockKatex(options) {
  116. return {
  117. name: 'blockKatex',
  118. level: 'block',
  119. start(src) {
  120. return katexStart(src, true);
  121. },
  122. tokenizer(src, tokens) {
  123. return katexTokenizer(src, tokens, true);
  124. }
  125. };
  126. }