CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/markdown/math-plugin.ts
Views: 851
1
/*
2
This is a revamp of https://github.com/goessner/markdown-it-texmath for our purposes.
3
The original license with MIT, and we consider our modified version of it to also
4
be MIT licensed.
5
6
TODO: move this to a separate package hosted on npmjs.
7
8
Original copyright:
9
* Copyright (c) Stefan Goessner - 2017-21. All rights reserved.
10
* Licensed under the MIT License. See License.txt in the project root for license information.
11
12
CHANGES we made:
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
20
The markdown-it-texmath plugin is very impressive, but it doesn't parse
21
things like \begin{equation}x^3$\end{equation} without dollar signs.
22
However, that is a basic requirement for cocalc in order to preserve
23
Jupyter classic compatibility. So we define our own rules, inspired
24
by the dollars rules from the the plugin,
25
and extend the regexps to also recognize these. We do this with a new
26
object "cocalc", to avoid potential conflicts.
27
IMPORTANT: We remove the math_block_eqno from upstream, since it
28
leads to very disturbing behavior and loss of information, e.g.,
29
$$x$$
30
31
(a) xyz
32
Gets rendered with the xyz gone. Very confusing. Equation numbers
33
when we do them, should be done as in latex, not with some weird notation that
34
is surprising. See https://github.com/sagemathinc/cocalc/issues/5879
35
*/
36
37
const 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[1];
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[1] = match[1].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[1];
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 **USED TO** 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
// We no longer support MathJax, so maybe this can be simplified?
120
return `<script type="math/tex${
121
displayMode ? "; mode=display" : ""
122
}">${tex}</script>`;
123
},
124
125
// used for enable/disable math rendering by `markdown-it`
126
inlineRuleNames: ["math_inline", "math_inline_double"],
127
blockRuleNames: ["math_block"],
128
129
rules: {
130
cocalc: {
131
inline: [
132
{
133
name: "math_inline_double",
134
rex: /\${2}([^$]*?[^\\])\${2}/gy,
135
tag: "$$",
136
displayMode: true,
137
pre,
138
post,
139
},
140
{
141
// We modify this from what's included in markdown-it-texmath to allow for
142
// multiple line inline formulas, e.g., "$2+\n3$" should work, but doesn't in upstream.
143
name: "math_inline",
144
rex: /\$((?:[^\$\s\\])|(?:[\S\s]*?[^\\]))\$/gmy,
145
tag: "$",
146
outerSpace: false,
147
pre,
148
post,
149
},
150
{
151
// using \begin/\end as part of inline markdown...
152
name: "math_inline",
153
rex: /(\\(?:begin)(\{math\})[\s\S]*?\\(?:end)\2)/gmy,
154
tag: "\\",
155
displayMode: false,
156
pre,
157
post,
158
},
159
{
160
// using \begin/\end as part of inline markdown...
161
name: "math_inline_double",
162
rex: /(\\(?:begin)(\{[a-z]*\*?\})[\s\S]*?\\(?:end)\2)/gmy,
163
tag: "\\",
164
displayMode: true,
165
pre,
166
post,
167
},
168
],
169
block: [
170
{
171
name: "math_block",
172
rex: /\${2}([^$]*?[^\\])\${2}/gmy,
173
tag: "$$",
174
},
175
{
176
name: "math_block",
177
rex: /(\\(?:begin)(\{[a-z]*\*?\})[\s\S]*?\\(?:end)\2)/gmy, // regexp to match \begin{...}...\end{...} environment.
178
tag: "\\",
179
},
180
],
181
},
182
},
183
};
184
185
export default function mathPlugin(md) {
186
for (const rule of texmath.rules["cocalc"].inline) {
187
md.inline.ruler.before("escape", rule.name, texmath.inline(rule)); // ! important
188
md.renderer.rules[rule.name] = (tokens, idx) =>
189
texmath.render(tokens[idx].content, !!rule.displayMode);
190
}
191
192
for (const rule of texmath.rules["cocalc"].block) {
193
md.block.ruler.before("fence", rule.name, texmath.block(rule)); // ! important for ```math delimiters
194
md.renderer.rules[rule.name] = (tokens, idx) =>
195
texmath.render(tokens[idx].content, true);
196
}
197
}
198
199
function pre(str, beg) {
200
const prv = beg > 0 ? str[beg - 1].charCodeAt(0) : false;
201
return (
202
!prv ||
203
(prv !== 0x5c && // no backslash,
204
(prv < 0x30 || prv > 0x39))
205
); // no decimal digit .. before opening '$'
206
}
207
208
function post(str, end) {
209
const nxt = str[end + 1] && str[end + 1].charCodeAt(0);
210
return !nxt || nxt < 0x30 || nxt > 0x39; // no decimal digit .. after closing '$'
211
}
212
213