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/hashtag-plugin.ts
Views: 894
1
/*
2
* LICENSE: MIT (same as upstream)
3
*/
4
5
/*
6
This is a rewrite of https://github.com/svbergerem/markdown-it-hashtag
7
8
LICENSE: MIT (same as the original upstream).
9
10
CHANGES:
11
- typescript
12
- only one token instead of three, which makes more sense to
13
me (better for conversion to slate)
14
*/
15
16
function renderHashtag(tokens, idx): string {
17
// obviously pretty specific to cocalc...
18
// Looks like antd tag, but scales.
19
return `<span style="color:#1b95e0;background-color:#fafafa;border:1px solid #d9d9d9;padding:0 7px;border-radius:5px">#${tokens[idx].content}</span>`;
20
}
21
22
function isLinkOpen(str) {
23
return /^<a[>\s]/i.test(str);
24
}
25
function isLinkClose(str) {
26
return /^<\/a\s*>/i.test(str);
27
}
28
29
export function hashtagPlugin(md): void {
30
const { arrayReplaceAt, escapeHtml } = md.utils;
31
32
const regex = /(^|\s)#(\w+)/g;
33
34
function hashtag(state) {
35
const { Token, tokens: blockTokens } = state;
36
37
for (let j = 0, l = blockTokens.length; j < l; j++) {
38
if (blockTokens[j].type !== "inline") {
39
continue;
40
}
41
42
let tokens = blockTokens[j].children;
43
44
let htmlLinkLevel = 0;
45
46
for (let i = tokens.length - 1; i >= 0; i--) {
47
const currentToken = tokens[i];
48
49
// skip content of markdown links
50
if (currentToken.type === "link_close") {
51
i--;
52
while (
53
tokens[i].level !== currentToken.level &&
54
tokens[i].type !== "link_open"
55
) {
56
i--;
57
}
58
continue;
59
}
60
61
// skip content of html links
62
if (currentToken.type === "html_inline") {
63
// we are going backwards, so isLinkOpen shows end of link
64
if (isLinkOpen(currentToken.content) && htmlLinkLevel > 0) {
65
htmlLinkLevel--;
66
}
67
if (isLinkClose(currentToken.content)) {
68
htmlLinkLevel++;
69
}
70
}
71
if (htmlLinkLevel > 0) {
72
continue;
73
}
74
75
if (currentToken.type !== "text") {
76
continue;
77
}
78
79
// find hashtags
80
let text = currentToken.content;
81
const matches = text.match(regex);
82
83
if (matches === null) {
84
continue;
85
}
86
87
const nodes: any[] = [];
88
const { level } = currentToken;
89
90
for (let m = 0; m < matches.length; m++) {
91
const tagName = matches[m].split("#", 2)[1];
92
93
// find the beginning of the matched text
94
let pos = text.indexOf(matches[m]);
95
// find the beginning of the hashtag
96
pos = text.indexOf("#" + tagName, pos);
97
98
if (pos > 0) {
99
const token = new Token("text", "", 0);
100
token.content = text.slice(0, pos);
101
token.level = level;
102
nodes.push(token);
103
}
104
105
const token = new Token("hashtag", "", 0);
106
token.content = escapeHtml(tagName);
107
token.level = level;
108
nodes.push(token);
109
110
text = text.slice(pos + 1 + tagName.length);
111
}
112
113
if (text.length > 0) {
114
const token = new Token("text", "", 0);
115
token.content = text;
116
token.level = level;
117
nodes.push(token);
118
}
119
120
// replace current node
121
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
122
}
123
}
124
}
125
126
md.core.ruler.after("inline", "hashtag", hashtag);
127
md.renderer.rules.hashtag = renderHashtag;
128
}
129
130