summaryrefslogtreecommitdiff
path: root/src/client/app
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2018-11-21 05:11:00 +0900
committerGitHub <noreply@github.com>2018-11-21 05:11:00 +0900
commit79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae (patch)
tree884a18e6735f8557eb392929fcfa2dc1ae09cb6d /src/client/app
parentRefactor checkMongoDb (#3339) (diff)
downloadsharkey-79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae.tar.gz
sharkey-79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae.tar.bz2
sharkey-79ffbf95db9d0cc019d06ab93b1bfa6ba0d4f9ae.zip
Improve MFM parser (#3337)
* wip * wip * Refactor * Refactor * wip * wip * wip * wip * Refactor * Refactor * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Clean up * Update misskey-flavored-markdown.ts * wip * wip * wip * wip * Update parser.ts * wip * Add new test * wip * Add new test * Add new test * wip * Refactor * Update parse.ts * Refactor * Update parser.ts * wip
Diffstat (limited to 'src/client/app')
-rw-r--r--src/client/app/boot.js1
-rw-r--r--src/client/app/common/views/components/index.ts2
-rw-r--r--src/client/app/common/views/components/mfm.ts (renamed from src/client/app/common/views/components/misskey-flavored-markdown.ts)120
-rw-r--r--src/client/app/common/views/components/misskey-flavored-markdown.vue57
-rw-r--r--src/client/app/common/views/components/welcome-timeline.vue2
-rw-r--r--src/client/app/common/views/pages/follow.vue2
-rw-r--r--src/client/app/desktop/views/components/note-detail.vue2
-rw-r--r--src/client/app/desktop/views/components/note.vue45
-rw-r--r--src/client/app/desktop/views/components/sub-note-content.vue2
-rw-r--r--src/client/app/desktop/views/components/user-card.vue2
-rw-r--r--src/client/app/desktop/views/pages/deck/deck.user-column.vue2
-rw-r--r--src/client/app/desktop/views/pages/user/user.header.vue2
-rw-r--r--src/client/app/mobile/views/components/note-detail.vue2
-rw-r--r--src/client/app/mobile/views/components/note.vue44
-rw-r--r--src/client/app/mobile/views/components/sub-note-content.vue2
-rw-r--r--src/client/app/mobile/views/pages/user.vue2
-rw-r--r--src/client/app/test/script.ts23
-rw-r--r--src/client/app/test/style.styl6
-rw-r--r--src/client/app/test/views/index.vue34
19 files changed, 212 insertions, 140 deletions
diff --git a/src/client/app/boot.js b/src/client/app/boot.js
index 76ea41c649..5e894a18d7 100644
--- a/src/client/app/boot.js
+++ b/src/client/app/boot.js
@@ -41,6 +41,7 @@
if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
+ if (`${url.pathname}/`.startsWith('/test/')) app = 'test';
//#endregion
// Script version
diff --git a/src/client/app/common/views/components/index.ts b/src/client/app/common/views/components/index.ts
index 8569e2cf10..b8fc7c4096 100644
--- a/src/client/app/common/views/components/index.ts
+++ b/src/client/app/common/views/components/index.ts
@@ -17,7 +17,7 @@ import forkit from './forkit.vue';
import acct from './acct.vue';
import avatar from './avatar.vue';
import nav from './nav.vue';
-import misskeyFlavoredMarkdown from './misskey-flavored-markdown';
+import misskeyFlavoredMarkdown from './misskey-flavored-markdown.vue';
import poll from './poll.vue';
import pollEditor from './poll-editor.vue';
import reactionIcon from './reaction-icon.vue';
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/mfm.ts
index 1eb738813e..b7ff5bd487 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/mfm.ts
@@ -1,11 +1,39 @@
import Vue, { VNode } from 'vue';
import { length } from 'stringz';
+import { Node } from '../../../../../mfm/parser';
import parse from '../../../../../mfm/parse';
-import getAcct from '../../../../../misc/acct/render';
import MkUrl from './url.vue';
import { concat } from '../../../../../prelude/array';
import MkFormula from './formula.vue';
import MkGoogle from './google.vue';
+import { toUnicode } from 'punycode';
+import syntaxHighlight from '../../../../../mfm/syntax-highlight';
+
+function getText(tokens: Node[]): string {
+ let text = '';
+ const extract = (tokens: Node[]) => {
+ tokens.filter(x => x.name === 'text').forEach(x => {
+ text += x.props.text;
+ });
+ tokens.filter(x => x.children).forEach(x => {
+ extract(x.children);
+ });
+ };
+ extract(tokens);
+ return text;
+}
+
+function getChildrenCount(tokens: Node[]): number {
+ let count = 0;
+ const extract = (tokens: Node[]) => {
+ tokens.filter(x => x.children).forEach(x => {
+ count++;
+ extract(x.children);
+ });
+ };
+ extract(tokens);
+ return count;
+}
export default Vue.component('misskey-flavored-markdown', {
props: {
@@ -21,6 +49,10 @@ export default Vue.component('misskey-flavored-markdown', {
type: Boolean,
default: true
},
+ author: {
+ type: Object,
+ default: null
+ },
i: {
type: Object,
default: null
@@ -31,23 +63,24 @@ export default Vue.component('misskey-flavored-markdown', {
},
render(createElement) {
- let ast: any[];
+ if (this.text == null || this.text == '') return;
+
+ let ast: Node[];
if (this.ast == null) {
// Parse text to ast
ast = parse(this.text);
} else {
- ast = this.ast as any[];
+ ast = this.ast as Node[];
}
let bigCount = 0;
let motionCount = 0;
- // Parse ast to DOM
- const els = concat(ast.map((token): VNode[] => {
- switch (token.type) {
+ const genEl = (ast: Node[]) => concat(ast.map((token): VNode[] => {
+ switch (token.name) {
case 'text': {
- const text = token.content.replace(/(\r\n|\n|\r)/g, '\n');
+ const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (this.shouldBreak) {
const x = text.split('\n')
@@ -60,12 +93,12 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'bold': {
- return [createElement('b', token.bold)];
+ return [createElement('b', genEl(token.children))];
}
case 'big': {
bigCount++;
- const isLong = length(token.big) > 10;
+ const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5;
const isMany = bigCount > 3;
return (createElement as any)('strong', {
attrs: {
@@ -75,12 +108,12 @@ export default Vue.component('misskey-flavored-markdown', {
name: 'animate-css',
value: { classes: 'tada', iteration: 'infinite' }
}]
- }, token.big);
+ }, genEl(token.children));
}
case 'motion': {
motionCount++;
- const isLong = length(token.motion) > 10;
+ const isLong = length(getText(token.children)) > 10 || getChildrenCount(token.children) > 5;
const isMany = motionCount > 3;
return (createElement as any)('span', {
attrs: {
@@ -90,13 +123,14 @@ export default Vue.component('misskey-flavored-markdown', {
name: 'animate-css',
value: { classes: 'rubberBand', iteration: 'infinite' }
}]
- }, token.motion);
+ }, genEl(token.children));
}
case 'url': {
return [createElement(MkUrl, {
+ key: Math.random(),
props: {
- url: token.content,
+ url: token.props.url,
target: '_blank',
style: 'color:var(--mfmLink);'
}
@@ -107,75 +141,75 @@ export default Vue.component('misskey-flavored-markdown', {
return [createElement('a', {
attrs: {
class: 'link',
- href: token.url,
+ href: token.props.url,
target: '_blank',
- title: token.url,
+ title: token.props.url,
style: 'color:var(--mfmLink);'
}
- }, token.title)];
+ }, genEl(token.children))];
}
case 'mention': {
+ const host = token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host;
+ const canonical = host != null ? `@${token.props.username}@${toUnicode(host)}` : `@${token.props.username}`;
return (createElement as any)('router-link', {
+ key: Math.random(),
attrs: {
- to: `/${token.canonical}`,
- dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
+ to: `/${canonical}`,
+ // TODO
+ //dataIsMe: (this as any).i && getAcct((this as any).i) == getAcct(token),
style: 'color:var(--mfmMention);'
},
directives: [{
name: 'user-preview',
- value: token.canonical
+ value: canonical
}]
- }, token.canonical);
+ }, canonical);
}
case 'hashtag': {
return [createElement('router-link', {
+ key: Math.random(),
attrs: {
- to: `/tags/${encodeURIComponent(token.hashtag)}`,
+ to: `/tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--mfmHashtag);'
}
- }, token.content)];
+ }, `#${token.props.hashtag}`)];
}
- case 'code': {
+ case 'blockCode': {
return [createElement('pre', {
class: 'code'
}, [
createElement('code', {
domProps: {
- innerHTML: token.html
+ innerHTML: syntaxHighlight(token.props.code)
}
})
])];
}
- case 'inline-code': {
+ case 'inlineCode': {
return [createElement('code', {
domProps: {
- innerHTML: token.html
+ innerHTML: syntaxHighlight(token.props.code)
}
})];
}
case 'quote': {
- const text2 = token.quote.replace(/(\r\n|\n|\r)/g, '\n');
-
if (this.shouldBreak) {
- const x = text2.split('\n')
- .map(t => [createElement('span', t), createElement('br')]);
- x[x.length - 1].pop();
return [createElement('div', {
attrs: {
class: 'quote'
}
- }, x)];
+ }, genEl(token.children))];
} else {
return [createElement('span', {
attrs: {
class: 'quote'
}
- }, text2.replace(/\n/g, ' '))];
+ }, genEl(token.children))];
}
}
@@ -184,15 +218,16 @@ export default Vue.component('misskey-flavored-markdown', {
attrs: {
class: 'title'
}
- }, token.title)];
+ }, genEl(token.children))];
}
case 'emoji': {
const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
return [createElement('mk-emoji', {
+ key: Math.random(),
attrs: {
- emoji: token.emoji,
- name: token.name
+ emoji: token.props.emoji,
+ name: token.props.name
},
props: {
customEmojis: this.customEmojis || customEmojis
@@ -203,8 +238,9 @@ export default Vue.component('misskey-flavored-markdown', {
case 'math': {
//const MkFormula = () => import('./formula.vue').then(m => m.default);
return [createElement(MkFormula, {
+ key: Math.random(),
props: {
- formula: token.formula
+ formula: token.props.formula
}
})];
}
@@ -212,22 +248,22 @@ export default Vue.component('misskey-flavored-markdown', {
case 'search': {
//const MkGoogle = () => import('./google.vue').then(m => m.default);
return [createElement(MkGoogle, {
+ key: Math.random(),
props: {
- q: token.query
+ q: token.props.query
}
})];
}
default: {
- console.log('unknown ast type:', token.type);
+ console.log('unknown ast type:', token.name);
return [];
}
}
}));
- // el.tag === 'br' のとき i !== 0 が保証されるため、短絡評価により els[i - 1] は配列外参照しない
- const _els = els.filter((el, i) => !(el.tag === 'br' && ['div', 'pre'].includes(els[i - 1].tag)));
- return createElement('span', _els);
+ // Parse ast to DOM
+ return createElement('span', genEl(ast));
}
});
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.vue b/src/client/app/common/views/components/misskey-flavored-markdown.vue
new file mode 100644
index 0000000000..b54f376935
--- /dev/null
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.vue
@@ -0,0 +1,57 @@
+<template>
+<mfm v-bind="$attrs" class="havbbuyv"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Mfm from './mfm';
+
+export default Vue.extend({
+ components: {
+ Mfm
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+.havbbuyv
+ >>> .title
+ display block
+ margin-bottom 4px
+ padding 4px
+ font-size 90%
+ text-align center
+ background var(--mfmTitleBg)
+ border-radius 4px
+
+ >>> .code
+ margin 8px 0
+
+ >>> .quote
+ margin 8px
+ padding 6px 12px
+ color var(--mfmQuote)
+ border-left solid 3px var(--mfmQuoteLine)
+
+ >>> code
+ padding 4px 8px
+ margin 0 0.5em
+ font-size 80%
+ color #525252
+ background #f8f8f8
+ border-radius 2px
+
+ >>> pre > code
+ padding 16px
+ margin 0
+
+ >>> [data-is-me]:after
+ content "you"
+ padding 0 4px
+ margin-left 4px
+ font-size 80%
+ color var(--primaryForeground)
+ background var(--primary)
+ border-radius 4px
+
+</style>
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index cad09a11a6..d075f06934 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -14,7 +14,7 @@
</div>
</header>
<div class="text">
- <misskey-flavored-markdown v-if="note.text" :text="note.text" :customEmojis="note.emojis"/>
+ <misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :custom-emojis="note.emojis"/>
</div>
</div>
</div>
diff --git a/src/client/app/common/views/pages/follow.vue b/src/client/app/common/views/pages/follow.vue
index 9db53fdf8a..72b0b73e01 100644
--- a/src/client/app/common/views/pages/follow.vue
+++ b/src/client/app/common/views/pages/follow.vue
@@ -9,7 +9,7 @@
<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
<span class="username">@{{ user | acct }}</span>
<div class="description">
- <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
</div>
</div>
</main>
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index 88108d961f..37c4093355 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -46,7 +46,7 @@
<div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
<span v-if="appearNote.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
- <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :customEmojis="appearNote.emojis" />
+ <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis" />
</div>
<div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="appearNote.files" :raw="true"/>
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index 26615b16a6..025d489c09 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -27,7 +27,7 @@
<div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
- <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :customEmojis="appearNote.emojis"/>
+ <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<a class="rp" v-if="appearNote.renote">RN:</a>
</div>
<div class="files" v-if="appearNote.files.length > 0">
@@ -223,24 +223,6 @@ export default Vue.extend({
overflow-wrap break-word
color var(--noteText)
- >>> .title
- display block
- margin-bottom 4px
- padding 4px
- font-size 90%
- text-align center
- background var(--mfmTitleBg)
- border-radius 4px
-
- >>> .code
- margin 8px 0
-
- >>> .quote
- margin 8px
- padding 6px 12px
- color var(--mfmQuote)
- border-left solid 3px var(--mfmQuoteLine)
-
> .reply
margin-right 8px
color var(--text)
@@ -322,28 +304,3 @@ export default Vue.extend({
opacity 0.7
</style>
-
-<style lang="stylus" module>
-.text
-
- code
- padding 4px 8px
- margin 0 0.5em
- font-size 80%
- color #525252
- background #f8f8f8
- border-radius 2px
-
- pre > code
- padding 16px
- margin 0
-
- [data-is-me]:after
- content "you"
- padding 0 4px
- margin-left 4px
- font-size 80%
- color var(--primaryForeground)
- background var(--primary)
- border-radius 4px
-</style>
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index 0007520e99..2a407bdcab 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -4,7 +4,7 @@
<span v-if="note.isHidden" style="opacity: 0.5">{{ $t('private') }}</span>
<span v-if="note.deletedAt" style="opacity: 0.5">{{ $t('deleted') }}</span>
<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
- <misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :custom-emojis="note.emojis"/>
+ <misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a>
</div>
<details v-if="note.files.length > 0">
diff --git a/src/client/app/desktop/views/components/user-card.vue b/src/client/app/desktop/views/components/user-card.vue
index 54fa15a190..c5d925fe6d 100644
--- a/src/client/app/desktop/views/components/user-card.vue
+++ b/src/client/app/desktop/views/components/user-card.vue
@@ -7,7 +7,7 @@
<router-link :to="user | userPage" class="name">{{ user | userName }}</router-link>
<span class="username">@{{ user | acct }}</span>
<div class="description">
- <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
</div>
</div>
</div>
diff --git a/src/client/app/desktop/views/pages/deck/deck.user-column.vue b/src/client/app/desktop/views/pages/deck/deck.user-column.vue
index 90f7e2aaaa..937166cec1 100644
--- a/src/client/app/desktop/views/pages/deck/deck.user-column.vue
+++ b/src/client/app/desktop/views/pages/deck/deck.user-column.vue
@@ -22,7 +22,7 @@
</header>
<div class="info">
<div class="description">
- <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
</div>
<div class="counts">
<div>
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index 48b5a487f4..9eacbe3914 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -14,7 +14,7 @@
<mk-avatar class="avatar" :user="user" :disable-preview="true"/>
<div class="body">
<div class="description">
- <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
</div>
<div class="info">
<span class="location" v-if="user.host === null && user.profile.location"><fa icon="map-marker"/> {{ user.profile.location }}</span>
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index f24cc0916f..61968a64d1 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -33,7 +33,7 @@
<div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
<span v-if="appearNote.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
- <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :customEmojis="appearNote.emojis"/>
+ <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
</div>
<div class="files" v-if="appearNote.files.length > 0">
<mk-media-list :media-list="appearNote.files" :raw="true"/>
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 42fb7118f8..5cfcdc0f3b 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -23,7 +23,7 @@
<div class="text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
<a class="reply" v-if="appearNote.reply"><fa icon="reply"/></a>
- <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :custom-emojis="appearNote.emojis"/>
+ <misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$store.state.i" :custom-emojis="appearNote.emojis"/>
<a class="rp" v-if="appearNote.renote != null">RN:</a>
</div>
<div class="files" v-if="appearNote.files.length > 0">
@@ -188,24 +188,6 @@ export default Vue.extend({
overflow-wrap break-word
color var(--noteText)
- >>> .title
- display block
- margin-bottom 4px
- padding 4px
- font-size 90%
- text-align center
- background var(--mfmTitleBg)
- border-radius 4px
-
- >>> .code
- margin 8px 0
-
- >>> .quote
- margin 8px
- padding 6px 12px
- color var(--mfmQuote)
- border-left solid 3px var(--mfmQuoteLine)
-
> .reply
margin-right 8px
color var(--noteText)
@@ -215,15 +197,6 @@ export default Vue.extend({
font-style oblique
color var(--renoteText)
- [data-is-me]:after
- content "you"
- padding 0 4px
- margin-left 4px
- font-size 80%
- color var(--primaryForeground)
- background var(--primary)
- border-radius 4px
-
.mk-url-preview
margin-top 8px
@@ -289,18 +262,3 @@ export default Vue.extend({
opacity 0.7
</style>
-
-<style lang="stylus" module>
-.text
- code
- padding 4px 8px
- margin 0 0.5em
- font-size 80%
- color #525252
- background #f8f8f8
- border-radius 2px
-
- pre > code
- padding 16px
- margin 0
-</style>
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index f4c86f19d2..715ddd6527 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -4,7 +4,7 @@
<span v-if="note.isHidden" style="opacity: 0.5">({{ $t('private') }})</span>
<span v-if="note.deletedAt" style="opacity: 0.5">({{ $t('deleted') }})</span>
<a class="reply" v-if="note.replyId"><fa icon="reply"/></a>
- <misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :custom-emojis="note.emojis"/>
+ <misskey-flavored-markdown v-if="note.text" :text="note.text" :author="note.user" :i="$store.state.i" :custom-emojis="note.emojis"/>
<a class="rp" v-if="note.renoteId">RN: ...</a>
</div>
<details v-if="note.files.length > 0">
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index b7f0db6eb9..1f0551680e 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -20,7 +20,7 @@
<span class="followed" v-if="user.isFollowed">{{ $t('follows-you') }}</span>
</div>
<div class="description">
- <misskey-flavored-markdown v-if="user.description" :text="user.description" :i="$store.state.i"/>
+ <misskey-flavored-markdown v-if="user.description" :text="user.description" :author="user" :i="$store.state.i"/>
</div>
<div class="info">
<p class="location" v-if="user.host === null && user.profile.location">
diff --git a/src/client/app/test/script.ts b/src/client/app/test/script.ts
new file mode 100644
index 0000000000..5818cf2913
--- /dev/null
+++ b/src/client/app/test/script.ts
@@ -0,0 +1,23 @@
+import VueRouter from 'vue-router';
+
+// Style
+import './style.styl';
+
+import init from '../init';
+import Index from './views/index.vue';
+
+init(launch => {
+ document.title = 'Misskey';
+
+ // Init router
+ const router = new VueRouter({
+ mode: 'history',
+ base: '/test/',
+ routes: [
+ { path: '/', component: Index },
+ ]
+ });
+
+ // Launch the app
+ launch(router);
+});
diff --git a/src/client/app/test/style.styl b/src/client/app/test/style.styl
new file mode 100644
index 0000000000..ae1a28226a
--- /dev/null
+++ b/src/client/app/test/style.styl
@@ -0,0 +1,6 @@
+@import "../app"
+@import "../reset"
+
+html
+ height 100%
+ background var(--bg)
diff --git a/src/client/app/test/views/index.vue b/src/client/app/test/views/index.vue
new file mode 100644
index 0000000000..b1947ffa4a
--- /dev/null
+++ b/src/client/app/test/views/index.vue
@@ -0,0 +1,34 @@
+<template>
+<main>
+ <ui-card>
+ <div slot="title">MFM Playground</div>
+ <section class="fit-top">
+ <ui-textarea v-model="mfm">
+ <span>MFM</span>
+ </ui-textarea>
+ <div>
+ <misskey-flavored-markdown :text="mfm" :i="$store.state.i"/>
+ </div>
+ </section>
+ </ui-card>
+</main>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+ data() {
+ return {
+ mfm: '',
+ };
+ }
+});
+</script>
+
+<style lang="stylus" scoped>
+main
+ max-width 700px
+ margin 0 auto
+
+</style>