Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
m1k1o
GitHub Repository: m1k1o/neko
Path: blob/master/client/src/components/markdown.ts
1301 views
1
import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown'
2
import { Component, Vue, Prop } from 'vue-property-decorator'
3
4
const { blockQuote, inlineCode, codeBlock, autolink, newline, escape, strong, text, link, url, em, u, br } =
5
defaultRules
6
7
type Rule = SingleNodeParserRule & HtmlOutputRule
8
9
interface MarkdownRules extends Rules<HtmlOutputRule> {
10
inlineCode: Rule
11
newline: Rule
12
escape: Rule
13
strong: Rule
14
em: Rule
15
u: Rule
16
blockQuote: Rule
17
codeBlock: Rule
18
autolink: Rule
19
url: Rule
20
strike: Rule
21
text: Rule
22
br: Rule
23
emoticon: Rule
24
spoiler: Rule
25
user: Rule
26
channel: Rule
27
role: Rule
28
emoji: Rule
29
everyone: Rule
30
here: Rule
31
link?: Rule
32
}
33
34
interface HTMLAttributes {
35
[key: string]: string
36
}
37
38
interface MarkdownState extends State {}
39
40
function htmlTag(
41
tagName: string,
42
content: string,
43
attributes: HTMLAttributes,
44
state: State = {},
45
isClosed: boolean = true,
46
) {
47
if (!attributes) {
48
attributes = {}
49
}
50
51
if (attributes.class && state.cssModuleNames) {
52
attributes.class = attributes.class
53
.split(' ')
54
.map((cl) => state.cssModuleNames[cl] || cl)
55
.join(' ')
56
}
57
58
let attributeString = ''
59
for (const attr in attributes) {
60
if (Object.prototype.hasOwnProperty.call(attributes, attr) && attributes[attr]) {
61
attributeString += ` ${attr}="${attributes[attr]}"` // md.sanitizeText(attr)
62
}
63
}
64
65
const unclosedTag = `<${tagName}${attributeString}>`
66
if (isClosed) {
67
return `${unclosedTag}${content}</${tagName}>`
68
}
69
70
return unclosedTag
71
}
72
73
// @ts-ignore
74
const rules: MarkdownRules = {
75
inlineCode,
76
newline,
77
escape,
78
strong,
79
em,
80
u,
81
link,
82
codeBlock: {
83
...codeBlock,
84
match: md.inlineRegex(/^```(([a-z0-9-]+?)\n+)?\n*([^]+?)\n*```/i),
85
parse(capture, parse, state) {
86
return {
87
lang: (capture[2] || '').trim(),
88
content: capture[3] || '',
89
inQuote: state.inQuote || false,
90
}
91
},
92
html(node, output, state) {
93
return htmlTag('pre', htmlTag('code', md.sanitizeText(node.content), {}, state), {}, state)
94
},
95
},
96
blockQuote: {
97
...blockQuote,
98
match(source, state, prevSource) {
99
return !/^$|\n *$/.test(prevSource) || state.inQuote
100
? null
101
: /^( *>>> ([\s\S]*))|^( *> [^\n]+(\n *> [^\n]+)*\n?)/.exec(source)
102
},
103
parse(capture, parse, state) {
104
const all = capture[0]
105
const isBlock = Boolean(/^ *>>> ?/.exec(all))
106
const removeSyntaxRegex = isBlock ? /^ *>>> ?/ : /^ *> ?/gm
107
const content = all.replace(removeSyntaxRegex, '')
108
109
state.inQuote = true
110
if (!isBlock) {
111
state.inline = true
112
}
113
114
const parsed = parse(content, state)
115
116
state.inQuote = state.inQuote || false
117
state.inline = state.inline || false
118
119
return {
120
content: parsed,
121
type: 'blockQuote',
122
}
123
},
124
},
125
autolink: {
126
...autolink,
127
parse(capture) {
128
return {
129
content: [
130
{
131
type: 'text',
132
content: capture[1],
133
},
134
],
135
target: capture[1],
136
}
137
},
138
html(node, output, state) {
139
return htmlTag(
140
'a',
141
output(node.content, state),
142
{ href: md.sanitizeUrl(node.target) as string, target: '_blank' },
143
state,
144
)
145
},
146
},
147
url: {
148
...url,
149
parse(capture) {
150
return {
151
content: [
152
{
153
type: 'text',
154
content: capture[1],
155
},
156
],
157
target: capture[1],
158
}
159
},
160
html(node, output, state) {
161
return htmlTag(
162
'a',
163
output(node.content, state),
164
{ href: md.sanitizeUrl(node.target) as string, target: '_blank' },
165
state,
166
)
167
},
168
},
169
strike: {
170
order: md.defaultRules.text.order,
171
match: md.inlineRegex(/^~~([\s\S]+?)~~(?!_)/),
172
parse(capture) {
173
return {
174
content: [
175
{
176
type: 'text',
177
content: capture[1],
178
},
179
],
180
target: capture[1],
181
}
182
},
183
html(node, output, state) {
184
return htmlTag('s', output(node.content, state), {}, state)
185
},
186
},
187
text: {
188
...text,
189
match: (source) => /^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff-]|\n\n|\n|\w+:\S|$)/.exec(source),
190
html(node, output, state) {
191
if (state.escapeHTML) {
192
return md.sanitizeText(node.content)
193
}
194
195
return node.content
196
},
197
},
198
br: {
199
...br,
200
match: md.anyScopeRegex(/^\n/),
201
},
202
emoji: {
203
order: md.defaultRules.strong.order,
204
match: (source) => /^:([^:\s]+):/.exec(source),
205
parse(capture) {
206
return {
207
id: capture[1],
208
}
209
},
210
html(node, output, state) {
211
return htmlTag(
212
'span',
213
'',
214
{
215
class: `emoji`,
216
'data-emoji': node.id,
217
'v-tooltip.top-center': `{ content:':${node.id}:', offset: 2, delay: { show: 1000, hide: 100 } }`,
218
},
219
state,
220
)
221
},
222
},
223
emoticon: {
224
order: md.defaultRules.text.order,
225
match: (source) => /^(¯\\_\(ツ\)_\/¯)/.exec(source),
226
parse(capture) {
227
return {
228
type: 'text',
229
content: capture[1],
230
}
231
},
232
html(node, output, state) {
233
return output(node.content, state)
234
},
235
},
236
spoiler: {
237
order: 0,
238
match: (source) => /^\|\|([\s\S]+?)\|\|/.exec(source),
239
parse(capture, parse, state) {
240
return {
241
content: parse(capture[1], state),
242
}
243
},
244
html(node, output, state) {
245
return htmlTag('span', htmlTag('span', output(node.content, state), {}, state), { class: 'spoiler' }, state)
246
},
247
},
248
}
249
250
const parser = md.parserFor(rules)
251
const htmlOutput = md.outputFor<HtmlOutputRule, 'html'>(rules, 'html')
252
253
@Component({
254
name: 'neko-markdown',
255
})
256
export default class extends Vue {
257
@Prop({ required: true })
258
source!: string
259
260
render(h: any) {
261
const state: MarkdownState = {
262
inline: true,
263
inQuote: false,
264
escapeHTML: true,
265
cssModuleNames: null,
266
}
267
return h({ template: `<div>${htmlOutput(parser(this.source, state), state)}</div>` })
268
}
269
}
270
271