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
This commit is contained in:
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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));
|
||||
}
|
||||
});
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"/>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
23
src/client/app/test/script.ts
Normal file
23
src/client/app/test/script.ts
Normal file
@ -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);
|
||||
});
|
6
src/client/app/test/style.styl
Normal file
6
src/client/app/test/style.styl
Normal file
@ -0,0 +1,6 @@
|
||||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
height 100%
|
||||
background var(--bg)
|
34
src/client/app/test/views/index.vue
Normal file
34
src/client/app/test/views/index.vue
Normal file
@ -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>
|
Reference in New Issue
Block a user