Archived
2
0
This repository has been archived on 2024-06-24. You can view files and clone it, but cannot push or open issues or pull requests.
neko-custom/client/src/components/markdown.ts

283 lines
6.0 KiB
TypeScript
Raw Normal View History

2020-01-23 06:16:40 +13:00
import md, { SingleNodeParserRule, HtmlOutputRule, defaultRules, State, Rules } from 'simple-markdown'
import { Component, Watch, Vue, Prop } from 'vue-property-decorator'
const {
blockQuote,
inlineCode,
codeBlock,
autolink,
newline,
escape,
strong,
text,
link,
url,
em,
u,
br,
} = defaultRules
type Rule = SingleNodeParserRule & HtmlOutputRule
interface MarkdownRules extends Rules<HtmlOutputRule> {
inlineCode: Rule
newline: Rule
escape: Rule
strong: Rule
em: Rule
u: Rule
blockQuote: Rule
codeBlock: Rule
autolink: Rule
url: Rule
strike: Rule
text: Rule
br: Rule
emoticon: Rule
spoiler: Rule
user: Rule
channel: Rule
role: Rule
emoji: Rule
everyone: Rule
here: Rule
link?: Rule
}
interface HTMLAttributes {
[key: string]: string
}
interface MarkdownState extends State {}
function htmlTag(
tagName: string,
content: string,
attributes: HTMLAttributes,
state: State = {},
isClosed: boolean = true,
) {
if (!attributes) {
attributes = {}
}
if (attributes.class && state.cssModuleNames) {
attributes.class = attributes.class
.split(' ')
.map(cl => state.cssModuleNames[cl] || cl)
.join(' ')
}
let attributeString = ''
for (const attr in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attr) && attributes[attr]) {
2020-02-03 11:52:07 +13:00
attributeString += ` ${attr}="${attributes[attr]}"` // md.sanitizeText(attr)
2020-01-23 06:16:40 +13:00
}
}
const unclosedTag = `<${tagName}${attributeString}>`
if (isClosed) {
return `${unclosedTag}${content}</${tagName}>`
}
return unclosedTag
}
// @ts-ignore
const rules: MarkdownRules = {
inlineCode,
newline,
escape,
strong,
em,
u,
link,
codeBlock: {
...codeBlock,
match: md.inlineRegex(/^```(([a-z0-9-]+?)\n+)?\n*([^]+?)\n*```/i),
parse(capture, parse, state) {
return {
lang: (capture[2] || '').trim(),
content: capture[3] || '',
inQuote: state.inQuote || false,
}
},
html(node, output, state) {
return htmlTag('pre', htmlTag('code', node.content, state), {}, state)
},
},
blockQuote: {
...blockQuote,
match(source, state, prevSource) {
return !/^$|\n *$/.test(prevSource) || state.inQuote
? null
: /^( *>>> ([\s\S]*))|^( *> [^\n]+(\n *> [^\n]+)*\n?)/.exec(source)
},
parse(capture, parse, state) {
const all = capture[0]
const isBlock = Boolean(/^ *>>> ?/.exec(all))
const removeSyntaxRegex = isBlock ? /^ *>>> ?/ : /^ *> ?/gm
const content = all.replace(removeSyntaxRegex, '')
state.inQuote = true
if (!isBlock) {
state.inline = true
}
const parsed = parse(content, state)
state.inQuote = state.inQuote || false
state.inline = state.inline || false
return {
content: parsed,
type: 'blockQuote',
}
},
},
autolink: {
...autolink,
parse(capture) {
return {
content: [
{
type: 'text',
content: capture[1],
},
],
target: capture[1],
}
},
html(node, output, state) {
return htmlTag(
'a',
output(node.content, state),
{ href: md.sanitizeUrl(node.target) as string, target: '_blank' },
state,
)
},
},
url: {
...url,
parse(capture) {
return {
content: [
{
type: 'text',
content: capture[1],
},
],
target: capture[1],
}
},
html(node, output, state) {
return htmlTag(
'a',
output(node.content, state),
{ href: md.sanitizeUrl(node.target) as string, target: '_blank' },
state,
)
},
},
strike: {
order: md.defaultRules.text.order,
match: md.inlineRegex(/^~~([\s\S]+?)~~(?!_)/),
parse(capture) {
return {
content: [
{
type: 'text',
content: capture[1],
},
],
target: capture[1],
}
},
html(node, output, state) {
return htmlTag('s', output(node.content, state), {}, state)
},
},
text: {
...text,
match: source => /^[\s\S]+?(?=[^0-9A-Za-z\s\u00c0-\uffff-]|\n\n|\n|\w+:\S|$)/.exec(source),
html(node, output, state) {
if (state.escapeHTML) {
2020-01-31 12:59:22 +13:00
return md.sanitizeText(node.content)
2020-01-23 06:16:40 +13:00
}
2020-01-31 12:59:22 +13:00
return node.content
2020-01-23 06:16:40 +13:00
},
},
br: {
...br,
match: md.anyScopeRegex(/^\n/),
},
2020-02-02 09:35:48 +13:00
emoji: {
order: md.defaultRules.strong.order,
match: source => /^:([a-zA-z_-]*):/.exec(source),
parse(capture) {
return {
id: capture[1],
}
},
html(node, output, state) {
2020-02-03 11:52:07 +13:00
return htmlTag(
'span',
'',
{
class: `emoji`,
'data-emoji': node.id,
'v-tooltip.top-center': `{ content:':${node.id}:', offset: 2, delay: { show: 1000, hide: 100 } }`,
},
state,
)
2020-02-02 09:35:48 +13:00
},
},
2020-01-23 06:16:40 +13:00
emoticon: {
order: md.defaultRules.text.order,
match: source => /^(¯\\_\(ツ\)_\/¯)/.exec(source),
parse(capture) {
return {
type: 'text',
content: capture[1],
}
},
html(node, output, state) {
return output(node.content, state)
},
},
spoiler: {
order: 0,
match: source => /^\|\|([\s\S]+?)\|\|/.exec(source),
parse(capture, parse, state) {
return {
content: parse(capture[1], state),
}
},
html(node, output, state) {
2020-02-03 11:52:07 +13:00
return htmlTag('span', htmlTag('span', output(node.content, state), {}, state), { class: 'spoiler' }, state)
2020-01-23 06:16:40 +13:00
},
},
}
const parser = md.parserFor(rules)
const htmlOutput = md.outputFor<HtmlOutputRule, 'html'>(rules, 'html')
@Component({
name: 'neko-markdown',
})
export default class extends Vue {
@Prop({ required: true })
source!: string
2020-02-03 11:52:07 +13:00
render(h: any) {
2020-01-23 06:16:40 +13:00
const state: MarkdownState = {
inline: true,
inQuote: false,
escapeHTML: true,
cssModuleNames: null,
}
2020-02-03 11:52:07 +13:00
return h({ template: `<div>${htmlOutput(parser(this.source, state), state)}</div>` })
2020-01-23 06:16:40 +13:00
}
}