diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2021-04-02 10:36:11 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-04-02 10:36:11 +0900 |
| commit | 1f4ae2f63a609d51942daa23772439379496064e (patch) | |
| tree | 55e67e216bd30b8a5c95f769e8696c4b885d1e61 /src/client/components | |
| parent | tweak avatar generation (diff) | |
| download | misskey-1f4ae2f63a609d51942daa23772439379496064e.tar.gz misskey-1f4ae2f63a609d51942daa23772439379496064e.tar.bz2 misskey-1f4ae2f63a609d51942daa23772439379496064e.zip | |
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
Diffstat (limited to 'src/client/components')
| -rw-r--r-- | src/client/components/mfm.ts | 89 | ||||
| -rw-r--r-- | src/client/components/note-detailed.vue | 23 | ||||
| -rw-r--r-- | src/client/components/note.vue | 23 | ||||
| -rw-r--r-- | src/client/components/page/page.text.vue | 10 | ||||
| -rw-r--r-- | src/client/components/post-form.vue | 6 |
5 files changed, 64 insertions, 87 deletions
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts index 28ac9b8942..b8e948a188 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 fb4f9502b3..5124b2a88c 100644 --- a/src/client/components/note-detailed.vue +++ b/src/client/components/note-detailed.vue @@ -120,11 +120,11 @@ </template> <script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; +import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { parse } from '../../mfm/parse'; -import { sum, unique } from '../../prelude/array'; +import * as mfm from 'mfm-js'; +import { sum } from '../../prelude/array'; import XSub from './note.sub.vue'; import XNoteHeader from './note-header.vue'; import XNotePreview from './note-preview.vue'; @@ -141,6 +141,7 @@ import { userPage } from '@client/filters/user'; import * as os from '@client/os'; import { noteActions, noteViewInterruptors } from '@client/store'; import { reactionPicker } from '@client/scripts/reaction-picker'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; function markRawAll(...xs) { for (const x of xs) { @@ -252,21 +253,7 @@ export default defineComponent({ urls(): string[] { if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: 再帰的にURL要素がないか調べる - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); + return extractUrlFromMfm(mfm.parse(this.appearNote.text)); } else { return null; } diff --git a/src/client/components/note.vue b/src/client/components/note.vue index b54cadfc80..a656ffc356 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -102,11 +102,11 @@ </template> <script lang="ts"> -import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; +import { defineAsyncComponent, defineComponent, markRaw } from 'vue'; import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; -import { parse } from '../../mfm/parse'; -import { sum, unique } from '../../prelude/array'; +import * as mfm from 'mfm-js'; +import { sum } from '../../prelude/array'; import XSub from './note.sub.vue'; import XNoteHeader from './note-header.vue'; import XNotePreview from './note-preview.vue'; @@ -123,6 +123,7 @@ import { userPage } from '@client/filters/user'; import * as os from '@client/os'; import { noteActions, noteViewInterruptors } from '@client/store'; import { reactionPicker } from '@client/scripts/reaction-picker'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; function markRawAll(...xs) { for (const x of xs) { @@ -238,21 +239,7 @@ export default defineComponent({ urls(): string[] { if (this.appearNote.text) { - const ast = parse(this.appearNote.text); - // TODO: 再帰的にURL要素がないか調べる - const urls = unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); - - // unique without hash - // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ] - const removeHash = x => x.replace(/#[^#]*$/, ''); - - return urls.reduce((array, url) => { - const removed = removeHash(url); - if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); - return array; - }, []); + return extractUrlFromMfm(mfm.parse(this.appearNote.text)); } else { return null; } diff --git a/src/client/components/page/page.text.vue b/src/client/components/page/page.text.vue index 491c62be26..580c5a93bf 100644 --- a/src/client/components/page/page.text.vue +++ b/src/client/components/page/page.text.vue @@ -9,8 +9,8 @@ import { TextBlock } from '@client/scripts/hpml/block'; import { Hpml } from '@client/scripts/hpml/evaluator'; import { defineAsyncComponent, defineComponent, PropType } from 'vue'; -import { parse } from '../../../mfm/parse'; -import { unique } from '../../../prelude/array'; +import * as mfm from 'mfm-js'; +import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm'; export default defineComponent({ components: { @@ -34,11 +34,7 @@ export default defineComponent({ computed: { urls(): string[] { if (this.text) { - const ast = parse(this.text); - // TODO: 再帰的にURL要素がないか調べる - return unique(ast - .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent)) - .map(t => t.node.props.url)); + return extractUrlFromMfm(mfm.parse(this.text)); } else { return []; } diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue index 7d2355c190..13e5c0f433 100644 --- a/src/client/components/post-form.vue +++ b/src/client/components/post-form.vue @@ -58,7 +58,7 @@ import insertTextAtCursor from 'insert-text-at-cursor'; import { length } from 'stringz'; import { toASCII } from 'punycode'; import XNotePreview from './note-preview.vue'; -import { parse } from '../../mfm/parse'; +import * as mfm from 'mfm-js'; import { host, url } from '@client/config'; import { erase, unique } from '../../prelude/array'; import extractMentions from '@/misc/extract-mentions'; @@ -229,7 +229,7 @@ export default defineComponent({ } if (this.reply && this.reply.text != null) { - const ast = parse(this.reply.text); + const ast = mfm.parse(this.reply.text); for (const x of extractMentions(ast)) { const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`; @@ -580,7 +580,7 @@ export default defineComponent({ this.deleteDraft(); this.$emit('posted'); if (this.text && this.text != '') { - const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag); + const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag); const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[]; localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history)))); } |