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
	 syuilo
					syuilo