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.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/mathjax-utils.js
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// This is taken from Jupyter, which is BSD/Apache2 licensed... -- https://github.com/jupyter/notebook/blob/master/notebook/static/notebook/js/mathjaxutils.js
7
8
// Some magic for deferring mathematical expressions to MathJax
9
// by hiding them from the Markdown parser.
10
// Some of the code here is adapted with permission from Davide Cervone
11
// under the terms of the Apache2 license governing the MathJax project.
12
// Other minor modifications are also due to StackExchange and are used with
13
// permission.
14
15
// MATHSPLIT contains the pattern for math delimiters and special symbols
16
// needed for searching for math in the text input.
17
18
const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|(?:\n\s*)+)/i;
19
20
// This would also capture \[ \] \( \), but I don't want to do that because
21
// Jupyter classic doesn't and it conflicts too much with markdown. Use $'s and e.g., \begin{equation}.
22
// const MATHSPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|(?:\n\s*)+|\\(?:\(|\)|\[|\]))/i;
23
24
// This runs under node.js and is js (not ts) so can't use import.
25
const { regex_split } = require("./regex-split");
26
27
// The math is in blocks i through j, so
28
// collect it into one block and clear the others.
29
// Clear the current math positions and store the index of the
30
// math, then push the math string onto the storage array.
31
// The preProcess function is called on all blocks if it has been passed in
32
function process_math(i, j, pre_process, math, blocks, tags) {
33
let block = blocks.slice(i, j + 1).join("");
34
while (j > i) {
35
blocks[j] = "";
36
j--;
37
}
38
// replace the current block text with a unique tag to find later
39
if (block[0] === "$" && block[1] === "$") {
40
blocks[i] = tags.display_open + math.length + tags.display_close;
41
} else {
42
blocks[i] = tags.open + math.length + tags.close;
43
}
44
if (pre_process) {
45
block = pre_process(block);
46
}
47
math.push(block);
48
return blocks;
49
}
50
51
// Break up the text into its component parts and search
52
// through them for math delimiters, braces, linebreaks, etc.
53
// Math delimiters must match and braces must balance.
54
// Don't allow math to pass through a double linebreak
55
// (which will be a paragraph).
56
//
57
58
// Do *NOT* conflict with the ones used in ./markdown-utils.ts
59
const MATH_ESCAPE = "\uFE32\uFE33"; // unused unicode -- hardcoded below too
60
exports.MATH_ESCAPE = MATH_ESCAPE;
61
62
const DEFAULT_TAGS = {
63
open: MATH_ESCAPE,
64
close: MATH_ESCAPE,
65
display_open: MATH_ESCAPE,
66
display_close: MATH_ESCAPE,
67
};
68
69
function remove_math(text, tags = DEFAULT_TAGS) {
70
let math = []; // stores math strings for later
71
let start;
72
let end;
73
let last;
74
let braces;
75
76
// Except for extreme edge cases, this should catch precisely those pieces of the markdown
77
// source that will later be turned into code spans. While MathJax will not TeXify code spans,
78
// we still have to consider them at this point; the following issue has happened several times:
79
//
80
// `$foo` and `$bar` are variables. --> <code>$foo ` and `$bar</code> are variables.
81
82
let hasCodeSpans = /`/.test(text),
83
de_tilde;
84
if (hasCodeSpans) {
85
text = text
86
.replace(/~/g, "~T")
87
.replace(/(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm, function (wholematch) {
88
return wholematch.replace(/\$/g, "~D");
89
});
90
de_tilde = function (text) {
91
return text.replace(/~([TD])/g, function (wholematch, character) {
92
return { T: "~", D: "$" }[character];
93
});
94
};
95
} else {
96
de_tilde = function (text) {
97
return text;
98
};
99
}
100
101
let blocks = regex_split(text.replace(/\r\n?/g, "\n"), MATHSPLIT);
102
103
for (let i = 1, m = blocks.length; i < m; i += 2) {
104
const block = blocks[i];
105
if (start) {
106
//
107
// If we are in math, look for the end delimiter,
108
// but don't go past double line breaks, and
109
// and balance braces within the math.
110
//
111
if (block === end) {
112
if (braces) {
113
last = i;
114
} else {
115
blocks = process_math(start, i, de_tilde, math, blocks, tags);
116
start = null;
117
end = null;
118
last = null;
119
}
120
} else if (block.match(/\n.*\n/)) {
121
if (last) {
122
i = last;
123
blocks = process_math(start, i, de_tilde, math, blocks, tags);
124
}
125
start = null;
126
end = null;
127
last = null;
128
braces = 0;
129
} else if (block === "{") {
130
braces++;
131
} else if (block === "}" && braces) {
132
braces--;
133
}
134
} else {
135
//
136
// Look for math start delimiters and when
137
// found, set up the end delimiter.
138
//
139
if (block === "$" || block === "$$") {
140
start = i;
141
end = block;
142
braces = 0;
143
} else if (block === "\\\\(" || block === "\\\\[") {
144
start = i;
145
end = block.slice(-1) === "(" ? "\\\\)" : "\\\\]";
146
braces = 0;
147
} else if (block.substr(1, 5) === "begin") {
148
start = i;
149
end = "\\end" + block.substr(6);
150
braces = 0;
151
}
152
}
153
}
154
if (last) {
155
blocks = process_math(start, last, de_tilde, math, blocks, tags);
156
start = null;
157
end = null;
158
last = null;
159
}
160
return [de_tilde(blocks.join("")), math];
161
}
162
exports.remove_math = remove_math;
163
164
//
165
// Put back the math strings that were saved.
166
//
167
function replace_math(text, math, tags) {
168
// Replace all the math group placeholders in the text
169
// with the saved strings.
170
if (tags == null) {
171
// Easy to do with a regexp
172
return text.replace(/\uFE32\uFE33(\d+)\uFE32\uFE33/g, function (match, n) {
173
return math[n];
174
});
175
} else {
176
// harder since tags could be anything.
177
// We assume that tags.display_open doesn't match tags.open and similarly with close,
178
// e.g., the display one is more complicated.
179
// .split might be faster...?
180
while (true) {
181
const i = text.indexOf(tags.display_open);
182
if (i == -1) break;
183
const j = text.indexOf(tags.display_close);
184
if (j == -1) break;
185
const n = parseInt(text.slice(i + tags.display_open.length, j));
186
text =
187
text.slice(0, i) + math[n] + text.slice(j + tags.display_close.length);
188
}
189
while (true) {
190
const i = text.indexOf(tags.open);
191
if (i == -1) break;
192
const j = text.indexOf(tags.close);
193
if (j == -1) break;
194
const n = parseInt(text.slice(i + tags.open.length, j));
195
text = text.slice(0, i) + math[n] + text.slice(j + tags.close.length);
196
}
197
return text;
198
}
199
}
200
201
exports.replace_math = replace_math;
202
203