Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupport News AboutSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/markdown/math-plugin.ts
Views: 272
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 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
184
export 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
198
function 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
207
function 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