Use Marked and marked-mfm for MFM rendering

This commit is contained in:
Sol Fisher Romanoff 2022-07-09 14:34:59 +03:00
parent 5f9e3709b3
commit 83a3b37f1f
No known key found for this signature in database
GPG key ID: 9D3F2B64F2341B62
3 changed files with 3697 additions and 2789 deletions

View file

@ -33,6 +33,8 @@
"escape-html": "1.0.3",
"js-cookie": "^3.0.1",
"localforage": "1.10.0",
"marked": "^4.0.17",
"marked-mfm": "^0.2.1",
"mfm-js": "^0.22.1",
"parse-link-header": "1.0.1",
"phoenix": "1.6.2",

View file

@ -1,286 +1,29 @@
import { defineComponent, h } from 'vue'
import * as mfm from 'mfm-js'
import MentionLink from '../mention_link/mention_link.vue'
function concat (xss) {
return ([]).concat(...xss)
}
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'font', 'blur', 'rainbow', 'rotate']
import { defineComponent } from 'vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import { marked } from 'marked'
import markedMfm from 'marked-mfm'
export default defineComponent({
components: {
RichContent
},
props: {
status: {
type: Object,
required: true
}
},
render () {
if (!this.status) return null
const ast = mfm.parse(this.status.mfm_content, { fnNameList: MFM_TAGS })
const validTime = (t) => {
if (t == null) return null
return t.match(/^[0-9.]+s$/) ? t : null
}
const genEl = (ast) => concat(ast.map((token) => {
switch (token.type) {
case 'text': {
const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n')
const res = []
for (const t of text.split('\n')) {
res.push(h('br'))
res.push(t)
}
res.shift()
return res
}
case 'bold': {
return [h('b', genEl(token.children))]
}
case 'strike': {
return [h('del', genEl(token.children))]
}
case 'italic': {
return h('i', {
style: 'font-style: oblique;'
}, genEl(token.children))
}
case 'fn': {
// TODO: CSS token.props.args.~~~ CSS
let style
switch (token.props.name) {
case 'tada': {
style = `font-size: 150%;` + 'animation: tada 1s linear infinite both;'
break
}
case 'jelly': {
const speed = validTime(token.props.args.speed) || '1s'
style = `animation: mfm-rubberBand ${speed} linear infinite both;`
break
}
case 'twitch': {
const speed = validTime(token.props.args.speed) || '0.5s'
style = `animation: mfm-twitch ${speed} ease infinite;`
break
}
case 'shake': {
const speed = validTime(token.props.args.speed) || '0.5s'
style = `animation: mfm-shake ${speed} ease infinite;`
break
}
case 'spin': {
const direction =
token.props.args.left ? 'reverse'
: token.props.args.alternate ? 'alternate'
: 'normal'
const anime =
token.props.args.x ? 'mfm-spinX'
: token.props.args.y ? 'mfm-spinY'
: 'mfm-spin'
const speed = validTime(token.props.args.speed) || '1.5s'
style = `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};`
break
}
case 'jump': {
style = 'animation: mfm-jump 0.75s linear infinite;'
break
}
case 'bounce': {
style = 'animation: mfm-bounce 0.75s linear infinite; transform-origin: center bottom;'
break
}
case 'flip': {
const transform =
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)'
: token.props.args.v ? 'scaleY(-1)'
: 'scaleX(-1)'
style = `transform: ${transform};`
break
}
case 'x2': {
style = `font-size: 200%;`
break
}
case 'x3': {
style = `font-size: 400%;`
break
}
case 'x4': {
style = `font-size: 600%;`
break
}
case 'font': {
const family =
token.props.args.serif ? 'serif'
: token.props.args.monospace ? 'monospace'
: token.props.args.cursive ? 'cursive'
: token.props.args.fantasy ? 'fantasy'
: token.props.args.emoji ? 'emoji'
: token.props.args.math ? 'math'
: null
if (family) style = `font-family: ${family};`
break
}
case 'blur': {
return h('span', {
class: '_mfm_blur_'
}, genEl(token.children))
}
case 'rainbow': {
style = 'animation: mfm-rainbow 1s linear infinite;'
break
}
case 'rotate': {
const degrees = parseInt(token.props.args.deg) || '90'
style = `transform: rotate(${degrees}deg); transform-origin: center center;`
break
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children), ']'])
} else {
return h('span', {
style: 'display: inline-block;' + style
}, genEl(token.children))
}
}
case 'small': {
return [h('small', {
style: 'opacity: 0.7;'
}, genEl(token.children))]
}
case 'center': {
return [h('div', {
style: 'text-align:center;'
}, genEl(token.children))]
}
case 'url': {
return [h('a', {
key: Math.random(),
href: token.props.url,
rel: 'nofollow noopener'
}, token.props.url)]
}
case 'link': {
console.log(token.props)
return [h('a', {
key: Math.random(),
href: token.props.url,
rel: 'nofollow noopener'
}, genEl(token.children))]
}
case 'mention': {
const user = this.status.attentions.find((mention) => `@${mention.screen_name}` === token.props.acct || mention.screen_name === token.props.username)
if (user) {
return [h(MentionLink, {
url: user.statusnet_profile_url,
content: token.props.acct,
userScreenName: token.props.acct
})]
}
return null
}
case 'hashtag': {
return [h('a', {
rel: 'noopener noreferrer',
target: '_blank',
key: token.props.hashtag,
href: this.status.tags.find((hash) => hash.name === token.props.hashtag).url
}, `#${token.props.hashtag}`)]
}
case 'blockCode': {
return [h('pre', {
key: Math.random(),
lang: token.props.lang
}, token.props.code)]
}
case 'inlineCode': {
return [h('code', {
key: Math.random(),
display: 'inline'
}, token.props.code)]
}
case 'quote': {
if (!this.nowrap) {
return [h('div', {
class: 'quote'
}, genEl(token.children))]
} else {
return [h('span', {
class: 'quote'
}, genEl(token.children))]
}
}
case 'emojiCode': {
const emoj = this.status.emojis.find((emoji) => emoji.shortcode === token.props.name)
if (emoj) {
return [h('div', {
class: 'still-image emoji img'
},
[h('img', {
key: Math.random(),
title: token.props.name,
alt: token.props.name,
src: this.status.emojis.find((emoji) => emoji.shortcode === token.props.name).static_url
})]
)]
} else {
return `:${token.props.name}:`
}
}
case 'unicodeEmoji': {
return token.props.emoji
}
case 'math': {
return [h('pre', {
key: Math.random(),
code: token.props.code
})]
}
case 'mathInline': {
return [h('pre', {
key: Math.random(),
code: token.props.code,
inline: true
})]
}
case 'search': {
return [h('a', {
href: `https://www.google.com/search?q=${token.props.query}`
}, token.props.content)
]
}
default: {
console.error('unrecognized ast type:', token.type)
return []
}
}
}))
// Parse ast to DOM
return h('span', genEl(ast))
marked.use(markedMfm, {
mangle: false,
gfm: false,
breaks: true
})
return <RichContent
handle-links="true"
html={marked.parse(this.status.mfm_content)}
emoji={this.status.emojis}
class="text media-body"
/>
}
})

6191
yarn.lock

File diff suppressed because it is too large Load diff