From 1f4ae2f63a609d51942daa23772439379496064e Mon Sep 17 00:00:00 2001 From: syuilo Date: Fri, 2 Apr 2021 10:36:11 +0900 Subject: [PATCH] Use mfm-js for MFM parsing (#7415) * wip * Update mfm.ts * wip * update mfmjs * refactor * nanka * Update mfm.ts * Update to-html.ts * Update to-html.ts * wip * fix test * fix test --- package.json | 2 +- src/client/components/mfm.ts | 89 +- src/client/components/note-detailed.vue | 23 +- src/client/components/note.vue | 23 +- src/client/components/page/page.text.vue | 10 +- src/client/components/post-form.vue | 6 +- src/client/pages/about-misskey.vue | 1 + .../messaging/messaging-room.message.vue | 9 +- src/client/ui/chat/note.vue | 23 +- src/client/ui/chat/post-form.vue | 6 +- src/mfm/from-html.ts | 4 +- src/mfm/language.ts | 191 --- src/mfm/normalize.ts | 31 - src/mfm/parse.ts | 19 - src/mfm/prelude.ts | 40 - src/mfm/to-html.ts | 102 +- src/mfm/to-string.ts | 99 -- src/misc/extract-custom-emojis-from-mfm.ts | 18 + src/misc/extract-emojis.ts | 9 - src/misc/extract-hashtags.ts | 19 +- src/misc/extract-mentions.ts | 17 +- src/misc/extract-url-from-mfm.ts | 34 + src/models/repositories/note.ts | 13 +- src/prelude/tree.ts | 36 - src/remote/activitypub/misc/get-note-html.ts | 4 +- src/remote/activitypub/renderer/person.ts | 4 +- src/server/api/endpoints/i/update.ts | 12 +- src/services/note/create.ts | 14 +- test/extract-mentions.ts | 8 +- test/mfm.ts | 1150 +---------------- yarn.lock | 17 +- 31 files changed, 262 insertions(+), 1771 deletions(-) delete mode 100644 src/mfm/language.ts delete mode 100644 src/mfm/normalize.ts delete mode 100644 src/mfm/parse.ts delete mode 100644 src/mfm/prelude.ts delete mode 100644 src/mfm/to-string.ts create mode 100644 src/misc/extract-custom-emojis-from-mfm.ts delete mode 100644 src/misc/extract-emojis.ts create mode 100644 src/misc/extract-url-from-mfm.ts delete mode 100644 src/prelude/tree.ts diff --git a/package.json b/package.json index 90a80cdad..b604c75f9 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,7 @@ "markdown-it": "12.0.4", "markdown-it-anchor": "7.1.0", "matter-js": "0.16.1", + "mfm-js": "0.12.0", "mocha": "8.3.2", "moji": "0.5.1", "ms": "2.1.3", @@ -190,7 +191,6 @@ "object-assign-deep": "0.4.0", "os-utils": "0.0.14", "parse5": "6.0.1", - "parsimmon": "1.16.0", "pg": "8.5.1", "portscanner": "2.2.0", "postcss": "8.2.8", diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts index 28ac9b894..b8e948a18 100644 --- a/src/client/components/mfm.ts +++ b/src/client/components/mfm.ts @@ -1,6 +1,5 @@ import { VNode, defineComponent, h } from 'vue'; -import { MfmForest } from '@client/../mfm/prelude'; -import { parse, parsePlain } from '@client/../mfm/parse'; +import * as mfm from 'mfm-js'; import MkUrl from '@client/components/global/url.vue'; import MkLink from '@client/components/link.vue'; import MkMention from '@client/components/mention.vue'; @@ -46,17 +45,17 @@ export default defineComponent({ render() { if (this.text == null || this.text == '') return; - const ast = (this.plain ? parsePlain : parse)(this.text); + const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text); const validTime = (t: string | null | undefined) => { if (t == null) return null; return t.match(/^[0-9.]+s$/) ? t : null; }; - const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => { - switch (token.node.type) { + const genEl = (ast: mfm.MfmNode[]) => concat(ast.map((token): VNode[] => { + switch (token.type) { case 'text': { - const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (!this.plain) { const x = text.split('\n') @@ -83,38 +82,38 @@ export default defineComponent({ } case 'fn': { - // TODO: CSSを文字列で組み立てていくと token.node.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる + // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる let style; - switch (token.node.props.name) { + switch (token.props.name) { case 'tada': { style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : ''); break; } case 'jelly': { - const speed = validTime(token.node.props.args.speed) || '1s'; + const speed = validTime(token.props.args.speed) || '1s'; style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); break; } case 'twitch': { - const speed = validTime(token.node.props.args.speed) || '0.5s'; + const speed = validTime(token.props.args.speed) || '0.5s'; style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : ''; break; } case 'shake': { - const speed = validTime(token.node.props.args.speed) || '0.5s'; + const speed = validTime(token.props.args.speed) || '0.5s'; style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : ''; break; } case 'spin': { const direction = - token.node.props.args.left ? 'reverse' : - token.node.props.args.alternate ? 'alternate' : + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : 'normal'; const anime = - token.node.props.args.x ? 'mfm-spinX' : - token.node.props.args.y ? 'mfm-spinY' : + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : 'mfm-spin'; - const speed = validTime(token.node.props.args.speed) || '1.5s'; + const speed = validTime(token.props.args.speed) || '1.5s'; style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; break; } @@ -128,8 +127,8 @@ export default defineComponent({ } case 'flip': { const transform = - (token.node.props.args.h && token.node.props.args.v) ? 'scale(-1, -1)' : - token.node.props.args.v ? 'scaleY(-1)' : + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : 'scaleX(-1)'; style = `transform: ${transform};`; break; @@ -148,12 +147,12 @@ export default defineComponent({ } case 'font': { const family = - token.node.props.args.serif ? 'serif' : - token.node.props.args.monospace ? 'monospace' : - token.node.props.args.cursive ? 'cursive' : - token.node.props.args.fantasy ? 'fantasy' : - token.node.props.args.emoji ? 'emoji' : - token.node.props.args.math ? 'math' : + 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; @@ -165,7 +164,7 @@ export default defineComponent({ } } if (style == null) { - return h('span', {}, ['[', token.node.props.name, ...genEl(token.children), ']']); + return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']); } else { return h('span', { style: 'display: inline-block;' + style, @@ -188,7 +187,7 @@ export default defineComponent({ case 'url': { return [h(MkUrl, { key: Math.random(), - url: token.node.props.url, + url: token.props.url, rel: 'nofollow noopener', })]; } @@ -196,7 +195,7 @@ export default defineComponent({ case 'link': { return [h(MkLink, { key: Math.random(), - url: token.node.props.url, + url: token.props.url, rel: 'nofollow noopener', }, genEl(token.children))]; } @@ -204,32 +203,31 @@ export default defineComponent({ case 'mention': { return [h(MkMention, { key: Math.random(), - host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host, - username: token.node.props.username + host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host, + username: token.props.username })]; } case 'hashtag': { return [h(MkA, { key: Math.random(), - to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`, + to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`, style: 'color:var(--hashtag);' - }, `#${token.node.props.hashtag}`)]; + }, `#${token.props.hashtag}`)]; } case 'blockCode': { return [h(MkCode, { key: Math.random(), - code: token.node.props.code, - lang: token.node.props.lang, + code: token.props.code, + lang: token.props.lang, })]; } case 'inlineCode': { return [h(MkCode, { key: Math.random(), - code: token.node.props.code, - lang: token.node.props.lang, + code: token.props.code, inline: true })]; } @@ -246,10 +244,19 @@ export default defineComponent({ } } - case 'emoji': { + case 'emojiCode': { return [h(MkEmoji, { key: Math.random(), - emoji: token.node.props.name ? `:${token.node.props.name}:` : token.node.props.emoji, + emoji: `:${token.props.name}:`, + customEmojis: this.customEmojis, + normal: this.plain + })]; + } + + case 'unicodeEmoji': { + return [h(MkEmoji, { + key: Math.random(), + emoji: token.props.emoji, customEmojis: this.customEmojis, normal: this.plain })]; @@ -258,7 +265,7 @@ export default defineComponent({ case 'mathInline': { return [h(MkFormula, { key: Math.random(), - formula: token.node.props.formula, + formula: token.props.formula, block: false })]; } @@ -266,7 +273,7 @@ export default defineComponent({ case 'mathBlock': { return [h(MkFormula, { key: Math.random(), - formula: token.node.props.formula, + formula: token.props.formula, block: true })]; } @@ -274,12 +281,12 @@ export default defineComponent({ case 'search': { return [h(MkGoogle, { key: Math.random(), - q: token.node.props.query + q: token.props.query })]; } default: { - console.error('unrecognized ast type:', token.node.type); + console.error('unrecognized ast type:', token.type); return []; } diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue index fb4f9502b..5124b2a88 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -120,11 +120,11 @@