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/frontend/markdown/index.ts
Views: 686
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
/*
7
Conversion from Markdown *to* HTML, trying not to horribly mangle math.
8
9
We also define and configure our Markdown parsers below, which are used
10
in other code directly, e.g, in supporting use of the slate editor.
11
```
12
*/
13
14
export * from "./types";
15
export * from "./table-of-contents";
16
17
import * as cheerio from "cheerio";
18
import MarkdownIt from "markdown-it";
19
import emojiPlugin from "markdown-it-emoji";
20
import { checkboxPlugin } from "./checkbox-plugin";
21
import { hashtagPlugin } from "./hashtag-plugin";
22
import { mentionPlugin } from "./mentions-plugin";
23
import mathPlugin from "./math-plugin";
24
export { parseHeader } from "./header";
25
import Markdown from "./component";
26
export { Markdown };
27
28
const MarkdownItFrontMatter = require("markdown-it-front-matter");
29
30
export const OPTIONS: MarkdownIt.Options = {
31
html: true,
32
typographer: false,
33
linkify: true,
34
breaks: false, // breaks=true is NOT liked by many devs.
35
};
36
37
const PLUGINS = [
38
[
39
mathPlugin,
40
{
41
delimiters: "cocalc",
42
engine: {
43
renderToString: (tex, options) => {
44
// We **used to** need to continue to support rendering to MathJax as an option,
45
// but texmath only supports katex. Thus we output by default to
46
// html using script tags, which are then parsed later using our
47
// katex/mathjax plugin.
48
// We no longer support MathJax, so maybe this can be simplified?
49
return `<script type="math/tex${
50
options.displayMode ? "; mode=display" : ""
51
}">${tex}</script>`;
52
},
53
},
54
},
55
],
56
[emojiPlugin],
57
[checkboxPlugin],
58
[hashtagPlugin],
59
[mentionPlugin],
60
];
61
62
function usePlugins(m, plugins) {
63
for (const [plugin, options] of plugins) {
64
m.use(plugin, options);
65
}
66
}
67
68
export const markdown_it = new MarkdownIt(OPTIONS);
69
usePlugins(markdown_it, PLUGINS);
70
71
/*
72
export function markdownParser() {
73
const m = new MarkdownIt(OPTIONS);
74
usePlugins(m, PLUGINS);
75
return m;
76
}*/
77
78
/*
79
Inject line numbers for sync.
80
- We track only headings and paragraphs, at any level.
81
- TODO Footnotes content causes jumps. Level limit filters it automatically.
82
83
See https://github.com/digitalmoksha/markdown-it-inject-linenumbers/blob/master/index.js
84
*/
85
function inject_linenumbers_plugin(md) {
86
function injectLineNumbers(tokens, idx, options, env, slf) {
87
if (tokens[idx].map) {
88
const line = tokens[idx].map[0];
89
tokens[idx].attrJoin("class", "source-line");
90
tokens[idx].attrSet("data-source-line", String(line));
91
}
92
return slf.renderToken(tokens, idx, options, env, slf);
93
}
94
95
md.renderer.rules.paragraph_open = injectLineNumbers;
96
md.renderer.rules.heading_open = injectLineNumbers;
97
md.renderer.rules.list_item_open = injectLineNumbers;
98
md.renderer.rules.table_open = injectLineNumbers;
99
}
100
const markdown_it_line_numbers = new MarkdownIt(OPTIONS);
101
markdown_it_line_numbers.use(inject_linenumbers_plugin);
102
usePlugins(markdown_it_line_numbers, PLUGINS);
103
104
/*
105
Turn the given markdown *string* into an HTML *string*.
106
We heuristically try to remove and put back the math via
107
remove_math, so that markdown itself doesn't
108
mangle it too much before Mathjax/Katex finally see it.
109
Note that remove_math is NOT perfect, e.g., it messes up
110
111
<a href="http://abc" class="foo-$">test $</a>
112
113
However, at least it is based on code in Jupyter classical,
114
so agrees with them, so people are used it it as a "standard".
115
116
See https://github.com/sagemathinc/cocalc/issues/2863
117
for another example where remove_math is annoying.
118
*/
119
120
export interface MD2html {
121
html: string;
122
frontmatter: string;
123
}
124
125
interface Options {
126
line_numbers?: boolean; // if given, embed extra line number info useful for inverse/forward search.
127
processMath?: (string) => string; // if given, apply this function to all the math
128
}
129
130
function process(
131
markdown_string: string,
132
mode: "default" | "frontmatter",
133
options?: Options,
134
): MD2html {
135
let text = markdown_string;
136
if (typeof text != "string") {
137
console.warn(
138
"WARNING: called markdown process with non-string input",
139
text,
140
);
141
// this function can get used for rendering markdown errors, and it's better
142
// to show something then blow up in our face.
143
text = JSON.stringify(text);
144
}
145
146
let html: string;
147
let frontmatter = "";
148
149
// avoid instantiating a new markdown object for normal md processing
150
if (mode == "frontmatter") {
151
const md_frontmatter = new MarkdownIt(OPTIONS).use(
152
MarkdownItFrontMatter,
153
(fm) => {
154
frontmatter = fm;
155
},
156
);
157
html = md_frontmatter.render(text);
158
} else {
159
if (options?.line_numbers) {
160
html = markdown_it_line_numbers.render(text);
161
} else {
162
html = markdown_it.render(text);
163
}
164
}
165
return { html, frontmatter };
166
}
167
168
export function markdown_to_html_frontmatter(s: string): MD2html {
169
return process(s, "frontmatter");
170
}
171
172
export function markdown_to_html(s: string, options?: Options): string {
173
return process(s, "default", options).html;
174
}
175
176
export function markdown_to_cheerio(s: string, options?: Options) {
177
return cheerio.load(`<div>${markdown_to_html(s, options)}</div>`);
178
}
179
180