  sagemathinc
GitHub Repository: sagemathinc/cocalc
Views: 49
1/*
2This is a revamp of https://github.com/goessner/markdown-it-texmath for our purposes.
3The original license with MIT, and we consider our modified version of it to also
5
6TODO: move this to a separate package hosted on npmjs.
7
11
13
14- We don't care about using katex for rendering, so that code is gone.
15  We only care about parsing.
16
17
18- RULES:
19
20The markdown-it-texmath plugin is very impressive, but it doesn't parse
21things like \begin{equation}x^3$\end{equation} without dollar signs. 22However, that is a basic requirement for cocalc in order to preserve 23Jupyter classic compatibility. So we define our own rules, inspired 24by the dollars rules from the the plugin, 25and extend the regexps to also recognize these. We do this with a new 26object "cocalc", to avoid potential conflicts. 27IMPORTANT: We remove the math_block_eqno from upstream, since it 28leads to very disturbing behavior and loss of information, e.g., 29 $$x$$ 30 31 (a) xyz 32Gets rendered with the xyz gone. Very confusing. Equation numbers 33when we do them, should be done as in latex, not with some weird notation that 34is surprising. See https://github.com/sagemathinc/cocalc/issues/5879 35*/ 36 37const texmath = { 38 inline: (rule) => 39 function inline(state, silent) { 40 const pos = state.pos; 41 const str = state.src; 42 const pre = 43 str.startsWith(rule.tag, (rule.rex.lastIndex = pos)) && 44 (!rule.pre || rule.pre(str, pos)); // valid pre-condition ... 45 const match = pre && rule.rex.exec(str); 46 const res = 47 !!match && 48 pos < rule.rex.lastIndex && 49 (!rule.post || rule.post(str, rule.rex.lastIndex - 1)); 50 51 if (res) { 52 if (!silent) { 53 const token = state.push(rule.name, "math", 0); 54 token.content = match; 55 token.markup = rule.tag; 56 } 57 state.pos = rule.rex.lastIndex; 58 } 59 return res; 60 }, 61 62 block: (rule) => 63 function block(state, begLine, endLine, silent) { 64 const pos = state.bMarks[begLine] + state.tShift[begLine]; 65 const str = state.src; 66 const pre = 67 str.startsWith(rule.tag, (rule.rex.lastIndex = pos)) && 68 (!rule.pre || rule.pre(str, false, pos)); // valid pre-condition .... 69 const match = pre && rule.rex.exec(str); 70 const res = 71 !!match && 72 pos < rule.rex.lastIndex && 73 (!rule.post || rule.post(str, false, rule.rex.lastIndex - 1)); 74 75 if (res && !silent) { 76 // match and valid post-condition ... 77 const endpos = rule.rex.lastIndex - 1; 78 let curline; 79 80 for (curline = begLine; curline < endLine; curline++) 81 if ( 82 endpos >= state.bMarks[curline] + state.tShift[curline] && 83 endpos <= state.eMarks[curline] 84 ) { 85 // line for end of block math found ... 86 break; 87 } 88 // "this will prevent lazy continuations from ever going past our end marker" 89 // https://github.com/markdown-it/markdown-it-container/blob/master/index.js 90 const lineMax = state.lineMax; 91 const parentType = state.parentType; 92 state.lineMax = curline; 93 state.parentType = "math"; 94 95 if (parentType === "blockquote") { 96 // remove all leading '>' inside multiline formula 97 match = match.replace(/(\n*?^(?:\s*>)+)/gm, ""); 98 } 99 // begin token 100 let token = state.push(rule.name, "math", 0); // 'math_block' 101 token.block = true; 102 token.tag = rule.tag; 103 token.markup = ""; 104 token.content = match; 105 token.map = [begLine, curline + 1]; // WARNING: this +1 is also fixes an upstream bug. Getting this right is critical for caching in slate. 106 // end token ... superfluous ... 107 108 state.parentType = parentType; 109 state.lineMax = lineMax; 110 state.line = curline + 1; 111 } 112 return res; 113 }, 114 render: (tex, displayMode) => { 115 // We need to continue to support rendering to MathJax as an option, 116 // but texmath only supports katex. Thus we output by default to 117 // html using script tags, which are then parsed later using our 118 // katex/mathjax plugin. 119 return <script type="math/tex${
120      displayMode ? "; mode=display" : ""
121    }">${tex}</script>; 122 }, 123 124 // used for enable/disable math rendering by markdown-it 125 inlineRuleNames: ["math_inline", "math_inline_double"], 126 blockRuleNames: ["math_block"], 127 128 rules: { 129 cocalc: { 130 inline: [ 131 { 132 name: "math_inline_double", 133 rex: /\${2}([^$]*?[^\\])\${2}/gy,
134          tag: "$$", 135 displayMode: true, 136 pre, 137 post, 138 }, 139 { 140 // We modify this from what's included in markdown-it-texmath to allow for 141 // multiple line inline formulas, e.g., "2+\n3" should work, but doesn't in upstream. 142 name: "math_inline", 143 rex: /\((?:[^\\s\\])|(?:[\S\s]*?[^\\]))\/gmy, 144 tag: "", 145 outerSpace: false, 146 pre, 147 post, 148 }, 149 { 150 // using \begin/\end as part of inline markdown... 151 name: "math_inline", 152 rex: /(\\(?:begin)(\{math\})[\s\S]*?\\(?:end)\2)/gmy, 153 tag: "\\", 154 displayMode: false, 155 pre, 156 post, 157 }, 158 { 159 // using \begin/\end as part of inline markdown... 160 name: "math_inline_double", 161 rex: /(\\(?:begin)(\{[a-z]*\*?\})[\s\S]*?\\(?:end)\2)/gmy, 162 tag: "\\", 163 displayMode: true, 164 pre, 165 post, 166 }, 167 ], 168 block: [ 169 { 170 name: "math_block", 171 rex: /\{2}([^]*?[^\\])\{2}/gmy, 172 tag: "$$",
173        },
174        {
175          name: "math_block",
176          rex: /(\\(?:begin)(\{[a-z]*\*?\})[\s\S]*?\\(?:end)\2)/gmy, // regexp to match \begin{...}...\end{...} environment.
177          tag: "\\",
178        },
179      ],
180    },
181  },
182};
183
184export default function mathPlugin(md) {
185  for (const rule of texmath.rules["cocalc"].inline) {
186    md.inline.ruler.before("escape", rule.name, texmath.inline(rule)); // ! important
187    md.renderer.rules[rule.name] = (tokens, idx) =>
188      texmath.render(tokens[idx].content, !!rule.displayMode);
189  }
190
191  for (const rule of texmath.rules["cocalc"].block) {
192    md.block.ruler.before("fence", rule.name, texmath.block(rule)); // ! important for math delimiters
193    md.renderer.rules[rule.name] = (tokens, idx) =>
194      texmath.render(tokens[idx].content, true);
195  }
196}
197
198function pre(str, beg) {
199  const prv = beg > 0 ? str[beg - 1].charCodeAt(0) : false;
200  return (
201    !prv ||
202    (prv !== 0x5c && // no backslash,
203      (prv < 0x30 || prv > 0x39))
204  ); // no decimal digit .. before opening '$' 205} 206 207function post(str, end) { 208 const nxt = str[end + 1] && str[end + 1].charCodeAt(0); 209 return !nxt || nxt < 0x30 || nxt > 0x39; // no decimal digit .. after closing '$'
210}
211
212`