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
This commit is contained in:
		
							parent
							
								
									b378066ebf
								
							
						
					
					
						commit
						1f4ae2f63a
					
				
					 31 changed files with 262 additions and 1771 deletions
				
			
		|  | @ -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", | ||||
|  |  | |||
|  | @ -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 []; | ||||
| 				} | ||||
|  |  | |||
|  | @ -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; | ||||
| 			} | ||||
|  |  | |||
|  | @ -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; | ||||
| 			} | ||||
|  |  | |||
|  | @ -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 []; | ||||
| 			} | ||||
|  |  | |||
|  | @ -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)))); | ||||
| 					} | ||||
|  |  | |||
|  | @ -40,6 +40,7 @@ | |||
| 			<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink> | ||||
| 			<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink> | ||||
| 			<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink> | ||||
| 			<FormLink to="https://github.com/marihachi" external>@marihachi</FormLink> | ||||
| 			<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
|  |  | |||
|  | @ -37,8 +37,8 @@ | |||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } 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'; | ||||
| import MkUrlPreview from '@client/components/url-preview.vue'; | ||||
| import * as os from '@client/os'; | ||||
| 
 | ||||
|  | @ -60,10 +60,7 @@ export default defineComponent({ | |||
| 		}, | ||||
| 		urls(): string[] { | ||||
| 			if (this.message.text) { | ||||
| 				const ast = parse(this.message.text); | ||||
| 				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.message.text)); | ||||
| 			} else { | ||||
| 				return []; | ||||
| 			} | ||||
|  |  | |||
|  | @ -101,11 +101,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'; | ||||
|  | @ -122,6 +122,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; | ||||
| 			} | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons'; | |||
| import insertTextAtCursor from 'insert-text-at-cursor'; | ||||
| import { length } from 'stringz'; | ||||
| import { toASCII } from 'punycode'; | ||||
| 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'; | ||||
|  | @ -216,7 +216,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}`; | ||||
|  | @ -567,7 +567,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)))); | ||||
| 					} | ||||
|  |  | |||
|  | @ -1,7 +1,9 @@ | |||
| import * as parse5 from 'parse5'; | ||||
| import treeAdapter = require('parse5/lib/tree-adapters/default'); | ||||
| import { URL } from 'url'; | ||||
| import { urlRegex, urlRegexFull } from './prelude'; | ||||
| 
 | ||||
| const urlRegex     = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; | ||||
| const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; | ||||
| 
 | ||||
| export function fromHtml(html: string, hashtagNames?: string[]): string { | ||||
| 	const dom = parse5.parseFragment(html); | ||||
|  |  | |||
|  | @ -1,191 +0,0 @@ | |||
| import * as P from 'parsimmon'; | ||||
| import { createLeaf, createTree, urlRegex } from './prelude'; | ||||
| import { takeWhile, cumulativeSum } from '../prelude/array'; | ||||
| import parseAcct from '@/misc/acct/parse'; | ||||
| import { toUnicode } from 'punycode'; | ||||
| import { emojiRegex } from '@/misc/emoji-regex'; | ||||
| 
 | ||||
| export function removeOrphanedBrackets(s: string): string { | ||||
| 	const openBrackets = ['(', '「', '[']; | ||||
| 	const closeBrackets = [')', '」', ']']; | ||||
| 	const xs = cumulativeSum(s.split('').map(c => { | ||||
| 		if (openBrackets.includes(c)) return 1; | ||||
| 		if (closeBrackets.includes(c)) return -1; | ||||
| 		return 0; | ||||
| 	})); | ||||
| 	const firstOrphanedCloseBracket = xs.findIndex(x => x < 0); | ||||
| 	if (firstOrphanedCloseBracket !== -1) return s.substr(0, firstOrphanedCloseBracket); | ||||
| 	const lastMatched = xs.lastIndexOf(0); | ||||
| 	return s.substr(0, lastMatched + 1); | ||||
| } | ||||
| 
 | ||||
| export const mfmLanguage = P.createLanguage({ | ||||
| 	root: r => P.alt(r.block, r.inline).atLeast(1), | ||||
| 	plain: r => P.alt(r.emoji, r.text).atLeast(1), | ||||
| 	block: r => P.alt( | ||||
| 		r.quote, | ||||
| 		r.search, | ||||
| 		r.blockCode, | ||||
| 		r.mathBlock, | ||||
| 		r.center, | ||||
| 	), | ||||
| 	startOfLine: () => P((input, i) => { | ||||
| 		if (i === 0 || input[i] === '\n' || input[i - 1] === '\n') { | ||||
| 			return P.makeSuccess(i, null); | ||||
| 		} else { | ||||
| 			return P.makeFailure(i, 'not newline'); | ||||
| 		} | ||||
| 	}), | ||||
| 	quote: r => r.startOfLine.then(P((input, i) => { | ||||
| 		const text = input.substr(i); | ||||
| 		if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote'); | ||||
| 		const quote = takeWhile(line => line.startsWith('>'), text.split('\n')); | ||||
| 		const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, ''); | ||||
| 		if (qInner === '') return P.makeFailure(i, 'not a quote'); | ||||
| 		const contents = r.root.tryParse(qInner); | ||||
| 		return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {})); | ||||
| 	})), | ||||
| 	search: r => r.startOfLine.then(P((input, i) => { | ||||
| 		const text = input.substr(i); | ||||
| 		const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i); | ||||
| 		if (!match) return P.makeFailure(i, 'not a search'); | ||||
| 		return P.makeSuccess(i + match[0].length, createLeaf('search', { query: match[1], content: match[0].trim() })); | ||||
| 	})), | ||||
| 	blockCode: r => r.startOfLine.then(P((input, i) => { | ||||
| 		const text = input.substr(i); | ||||
| 		const match = text.match(/^```(.+?)?\n([\s\S]+?)\n```(\n|$)/i); | ||||
| 		if (!match) return P.makeFailure(i, 'not a blockCode'); | ||||
| 		return P.makeSuccess(i + match[0].length, createLeaf('blockCode', { code: match[2], lang: match[1] ? match[1].trim() : null })); | ||||
| 	})), | ||||
| 	inline: r => P.alt( | ||||
| 		r.big, | ||||
| 		r.bold, | ||||
| 		r.small, | ||||
| 		r.italic, | ||||
| 		r.strike, | ||||
| 		r.inlineCode, | ||||
| 		r.mathInline, | ||||
| 		r.mention, | ||||
| 		r.hashtag, | ||||
| 		r.url, | ||||
| 		r.link, | ||||
| 		r.emoji, | ||||
| 		r.fn, | ||||
| 		r.text | ||||
| 	), | ||||
| 	// TODO: そのうち消す
 | ||||
| 	big: r => P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1).map(x => createTree('fn', r.inline.atLeast(1).tryParse(x), { | ||||
| 		name: 'tada', | ||||
| 		args: {} | ||||
| 	})), | ||||
| 	bold: r => { | ||||
| 		const asterisk = P.regexp(/\*\*([\s\S]+?)\*\*/, 1); | ||||
| 		const underscore = P.regexp(/__([a-zA-Z0-9\s]+?)__/, 1); | ||||
| 		return P.alt(asterisk, underscore).map(x => createTree('bold', r.inline.atLeast(1).tryParse(x), {})); | ||||
| 	}, | ||||
| 	small: r => P.regexp(/<small>([\s\S]+?)<\/small>/, 1).map(x => createTree('small', r.inline.atLeast(1).tryParse(x), {})), | ||||
| 	italic: r => { | ||||
| 		const xml = P.regexp(/<i>([\s\S]+?)<\/i>/, 1); | ||||
| 		const underscore = P((input, i) => { | ||||
| 			const text = input.substr(i); | ||||
| 			const match = text.match(/^(\*|_)([a-zA-Z0-9]+?[\s\S]*?)\1/); | ||||
| 			if (!match) return P.makeFailure(i, 'not a italic'); | ||||
| 			if (input[i - 1] != null && input[i - 1] != ' ' && input[i - 1] != '\n') return P.makeFailure(i, 'not a italic'); | ||||
| 			return P.makeSuccess(i + match[0].length, match[2]); | ||||
| 		}); | ||||
| 
 | ||||
| 		return P.alt(xml, underscore).map(x => createTree('italic', r.inline.atLeast(1).tryParse(x), {})); | ||||
| 	}, | ||||
| 	strike: r => P.regexp(/~~([^\n~]+?)~~/, 1).map(x => createTree('strike', r.inline.atLeast(1).tryParse(x), {})), | ||||
| 	center: r => r.startOfLine.then(P.regexp(/<center>([\s\S]+?)<\/center>/, 1).map(x => createTree('center', r.inline.atLeast(1).tryParse(x), {}))), | ||||
| 	inlineCode: () => P.regexp(/`([^´\n]+?)`/, 1).map(x => createLeaf('inlineCode', { code: x })), | ||||
| 	mathBlock: r => r.startOfLine.then(P.regexp(/\\\[([\s\S]+?)\\\]/, 1).map(x => createLeaf('mathBlock', { formula: x.trim() }))), | ||||
| 	mathInline: () => P.regexp(/\\\((.+?)\\\)/, 1).map(x => createLeaf('mathInline', { formula: x })), | ||||
| 	mention: () => { | ||||
| 		return P((input, i) => { | ||||
| 			const text = input.substr(i); | ||||
| 			const match = text.match(/^@\w([\w-]*\w)?(?:@[\w.\-]+\w)?/); | ||||
| 			if (!match) return P.makeFailure(i, 'not a mention'); | ||||
| 			if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a mention'); | ||||
| 			return P.makeSuccess(i + match[0].length, match[0]); | ||||
| 		}).map(x => { | ||||
| 			const { username, host } = parseAcct(x.substr(1)); | ||||
| 			const canonical = host != null ? `@${username}@${toUnicode(host)}` : x; | ||||
| 			return createLeaf('mention', { canonical, username, host, acct: x }); | ||||
| 		}); | ||||
| 	}, | ||||
| 	hashtag: () => P((input, i) => { | ||||
| 		const text = input.substr(i); | ||||
| 		const match = text.match(/^#([^\s.,!?'"#:\/\[\]【】]+)/i); | ||||
| 		if (!match) return P.makeFailure(i, 'not a hashtag'); | ||||
| 		let hashtag = match[1]; | ||||
| 		hashtag = removeOrphanedBrackets(hashtag); | ||||
| 		if (hashtag.match(/^(\u20e3|\ufe0f)/)) return P.makeFailure(i, 'not a hashtag'); | ||||
| 		if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag'); | ||||
| 		if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a hashtag'); | ||||
| 		if (Array.from(hashtag || '').length > 128) return P.makeFailure(i, 'not a hashtag'); | ||||
| 		return P.makeSuccess(i + ('#' + hashtag).length, createLeaf('hashtag', { hashtag: hashtag })); | ||||
| 	}), | ||||
| 	url: () => { | ||||
| 		return P((input, i) => { | ||||
| 			const text = input.substr(i); | ||||
| 			const match = text.match(urlRegex); | ||||
| 			let url: string; | ||||
| 			if (!match) { | ||||
| 				const match = text.match(/^<(https?:\/\/.*?)>/); | ||||
| 				if (!match) { | ||||
| 					return P.makeFailure(i, 'not a url'); | ||||
| 				} | ||||
| 				url = match[1]; | ||||
| 				i += 2; | ||||
| 			} else { | ||||
| 				url = match[0]; | ||||
| 			} | ||||
| 			url = removeOrphanedBrackets(url); | ||||
| 			url = url.replace(/[.,]*$/, ''); | ||||
| 			return P.makeSuccess(i + url.length, url); | ||||
| 		}).map(x => createLeaf('url', { url: x })); | ||||
| 	}, | ||||
| 	link: r => { | ||||
| 		return P.seqObj( | ||||
| 			['silent', P.string('?').fallback(null).map(x => x != null)] as any, | ||||
| 			P.string('['), ['text', P.regexp(/[^\n\[\]]+/)] as any, P.string(']'), | ||||
| 			P.string('('), ['url', r.url] as any, P.string(')'), | ||||
| 		).map((x: any) => { | ||||
| 			return createTree('link', r.inline.atLeast(1).tryParse(x.text), { | ||||
| 				silent: x.silent, | ||||
| 				url: x.url.node.props.url | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| 	emoji: () => { | ||||
| 		const name = P.regexp(/:([a-z0-9_+-]+):/i, 1).map(x => createLeaf('emoji', { name: x })); | ||||
| 		const code = P.regexp(emojiRegex).map(x => createLeaf('emoji', { emoji: x })); | ||||
| 		return P.alt(name, code); | ||||
| 	}, | ||||
| 	fn: r => { | ||||
| 		return P.seqObj( | ||||
| 			P.string('['), ['fn', P.regexp(/[^\s\n\[\]]+/)] as any, P.string(' '), P.optWhitespace, ['text', P.regexp(/[^\n\[\]]+/)] as any, P.string(']'), | ||||
| 		).map((x: any) => { | ||||
| 			let name = x.fn; | ||||
| 			const args = {}; | ||||
| 			const separator = x.fn.indexOf('.'); | ||||
| 			if (separator > -1) { | ||||
| 				name = x.fn.substr(0, separator); | ||||
| 				for (const arg of x.fn.substr(separator + 1).split(',')) { | ||||
| 					const kv = arg.split('='); | ||||
| 					if (kv.length === 1) { | ||||
| 						args[kv[0]] = true; | ||||
| 					} else { | ||||
| 						args[kv[0]] = kv[1]; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 			return createTree('fn', r.inline.atLeast(1).tryParse(x.text), { | ||||
| 				name, | ||||
| 				args | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| 	text: () => P.any.map(x => createLeaf('text', { text: x })) | ||||
| }); | ||||
|  | @ -1,31 +0,0 @@ | |||
| import * as A from '../prelude/array'; | ||||
| import * as S from '../prelude/string'; | ||||
| import { MfmForest, MfmTree } from './prelude'; | ||||
| import { createTree, createLeaf } from '../prelude/tree'; | ||||
| 
 | ||||
| function isEmptyTextTree(t: MfmTree): boolean { | ||||
| 	return t.node.type === 'text' && t.node.props.text === ''; | ||||
| } | ||||
| 
 | ||||
| function concatTextTrees(ts: MfmForest): MfmTree { | ||||
| 	return createLeaf({ type: 'text', props: { text: S.concat(ts.map(x => x.node.props.text)) } }); | ||||
| } | ||||
| 
 | ||||
| function concatIfTextTrees(ts: MfmForest): MfmForest { | ||||
| 	return ts[0].node.type === 'text' ? [concatTextTrees(ts)] : ts; | ||||
| } | ||||
| 
 | ||||
| function concatConsecutiveTextTrees(ts: MfmForest): MfmForest { | ||||
| 	const us = A.concat(A.groupOn(t => t.node.type, ts).map(concatIfTextTrees)); | ||||
| 	return us.map(t => createTree(t.node, concatConsecutiveTextTrees(t.children))); | ||||
| } | ||||
| 
 | ||||
| function removeEmptyTextNodes(ts: MfmForest): MfmForest { | ||||
| 	return ts | ||||
| 		.filter(t => !isEmptyTextTree(t)) | ||||
| 		.map(t => createTree(t.node, removeEmptyTextNodes(t.children))); | ||||
| } | ||||
| 
 | ||||
| export function normalize(ts: MfmForest): MfmForest { | ||||
| 	return removeEmptyTextNodes(concatConsecutiveTextTrees(ts)); | ||||
| } | ||||
|  | @ -1,19 +0,0 @@ | |||
| import { mfmLanguage } from './language'; | ||||
| import { MfmForest } from './prelude'; | ||||
| import { normalize } from './normalize'; | ||||
| 
 | ||||
| export function parse(source: string | null): MfmForest | null { | ||||
| 	if (source == null || source === '') { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	return normalize(mfmLanguage.root.tryParse(source)); | ||||
| } | ||||
| 
 | ||||
| export function parsePlain(source: string | null): MfmForest | null { | ||||
| 	if (source == null || source === '') { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
| 	return normalize(mfmLanguage.plain.tryParse(source)); | ||||
| } | ||||
|  | @ -1,40 +0,0 @@ | |||
| import { Tree } from '../prelude/tree'; | ||||
| import * as T from '../prelude/tree'; | ||||
| 
 | ||||
| type Node<T, P> = { type: T, props: P }; | ||||
| 
 | ||||
| export type MentionNode = Node<'mention', { | ||||
| 	canonical: string, | ||||
| 	username: string, | ||||
| 	host: string, | ||||
| 	acct: string | ||||
| }>; | ||||
| 
 | ||||
| export type HashtagNode = Node<'hashtag', { | ||||
| 	hashtag: string | ||||
| }>; | ||||
| 
 | ||||
| export type EmojiNode = Node<'emoji', { | ||||
| 	name: string | ||||
| }>; | ||||
| 
 | ||||
| export type MfmNode = | ||||
| 	MentionNode | | ||||
| 	HashtagNode | | ||||
| 	EmojiNode | | ||||
| 	Node<string, any>; | ||||
| 
 | ||||
| export type MfmTree = Tree<MfmNode>; | ||||
| 
 | ||||
| export type MfmForest = MfmTree[]; | ||||
| 
 | ||||
| export function createLeaf(type: string, props: any): MfmTree { | ||||
| 	return T.createLeaf({ type, props }); | ||||
| } | ||||
| 
 | ||||
| export function createTree(type: string, children: MfmForest, props: any): MfmTree { | ||||
| 	return T.createTree({ type, props }, children); | ||||
| } | ||||
| 
 | ||||
| export const urlRegex     = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/; | ||||
| export const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/; | ||||
|  | @ -1,12 +1,12 @@ | |||
| import { JSDOM } from 'jsdom'; | ||||
| import * as mfm from 'mfm-js'; | ||||
| import config from '@/config'; | ||||
| import { intersperse } from '../prelude/array'; | ||||
| import { MfmForest, MfmTree } from './prelude'; | ||||
| import { IMentionedRemoteUsers } from '../models/entities/note'; | ||||
| import { wellKnownServices } from '../well-known-services'; | ||||
| 
 | ||||
| export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { | ||||
| 	if (tokens == null) { | ||||
| export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { | ||||
| 	if (nodes == null) { | ||||
| 		return null; | ||||
| 	} | ||||
| 
 | ||||
|  | @ -14,95 +14,101 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione | |||
| 
 | ||||
| 	const doc = window.document; | ||||
| 
 | ||||
| 	function appendChildren(children: MfmForest, targetElement: any): void { | ||||
| 		for (const child of children.map(t => handlers[t.node.type](t))) targetElement.appendChild(child); | ||||
| 	function appendChildren(children: mfm.MfmNode[], targetElement: any): void { | ||||
| 		if (children) { | ||||
| 			for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	const handlers: { [key: string]: (token: MfmTree) => any } = { | ||||
| 		bold(token) { | ||||
| 	const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = { | ||||
| 		bold(node) { | ||||
| 			const el = doc.createElement('b'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		small(token) { | ||||
| 		small(node) { | ||||
| 			const el = doc.createElement('small'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		strike(token) { | ||||
| 		strike(node) { | ||||
| 			const el = doc.createElement('del'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		italic(token) { | ||||
| 		italic(node) { | ||||
| 			const el = doc.createElement('i'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		fn(token) { | ||||
| 		fn(node) { | ||||
| 			const el = doc.createElement('i'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		blockCode(token) { | ||||
| 		blockCode(node) { | ||||
| 			const pre = doc.createElement('pre'); | ||||
| 			const inner = doc.createElement('code'); | ||||
| 			inner.textContent = token.node.props.code; | ||||
| 			inner.textContent = node.props.code; | ||||
| 			pre.appendChild(inner); | ||||
| 			return pre; | ||||
| 		}, | ||||
| 
 | ||||
| 		center(token) { | ||||
| 		center(node) { | ||||
| 			const el = doc.createElement('div'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		emoji(token) { | ||||
| 			return doc.createTextNode(token.node.props.emoji ? token.node.props.emoji : `\u200B:${token.node.props.name}:\u200B`); | ||||
| 		emojiCode(node) { | ||||
| 			return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); | ||||
| 		}, | ||||
| 
 | ||||
| 		hashtag(token) { | ||||
| 		unicodeEmoji(node) { | ||||
| 			return doc.createTextNode(node.props.emoji); | ||||
| 		}, | ||||
| 
 | ||||
| 		hashtag(node) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = `${config.url}/tags/${token.node.props.hashtag}`; | ||||
| 			a.textContent = `#${token.node.props.hashtag}`; | ||||
| 			a.href = `${config.url}/tags/${node.props.hashtag}`; | ||||
| 			a.textContent = `#${node.props.hashtag}`; | ||||
| 			a.setAttribute('rel', 'tag'); | ||||
| 			return a; | ||||
| 		}, | ||||
| 
 | ||||
| 		inlineCode(token) { | ||||
| 		inlineCode(node) { | ||||
| 			const el = doc.createElement('code'); | ||||
| 			el.textContent = token.node.props.code; | ||||
| 			el.textContent = node.props.code; | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		mathInline(token) { | ||||
| 		mathInline(node) { | ||||
| 			const el = doc.createElement('code'); | ||||
| 			el.textContent = token.node.props.formula; | ||||
| 			el.textContent = node.props.formula; | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		mathBlock(token) { | ||||
| 		mathBlock(node) { | ||||
| 			const el = doc.createElement('code'); | ||||
| 			el.textContent = token.node.props.formula; | ||||
| 			el.textContent = node.props.formula; | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		link(token) { | ||||
| 		link(node) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = token.node.props.url; | ||||
| 			appendChildren(token.children, a); | ||||
| 			a.href = node.props.url; | ||||
| 			appendChildren(node.children, a); | ||||
| 			return a; | ||||
| 		}, | ||||
| 
 | ||||
| 		mention(token) { | ||||
| 		mention(node) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			const { username, host, acct } = token.node.props; | ||||
| 			const { username, host, acct } = node.props; | ||||
| 			const wellKnown = wellKnownServices.find(x => x[0] === host); | ||||
| 			if (wellKnown) { | ||||
| 				a.href = wellKnown[1](username); | ||||
|  | @ -115,39 +121,39 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione | |||
| 			return a; | ||||
| 		}, | ||||
| 
 | ||||
| 		quote(token) { | ||||
| 		quote(node) { | ||||
| 			const el = doc.createElement('blockquote'); | ||||
| 			appendChildren(token.children, el); | ||||
| 			appendChildren(node.children, el); | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		text(token) { | ||||
| 		text(node) { | ||||
| 			const el = doc.createElement('span'); | ||||
| 			const nodes = (token.node.props.text as string).split(/\r\n|\r|\n/).map(x => doc.createTextNode(x) as Node); | ||||
| 			const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x)); | ||||
| 
 | ||||
| 			for (const x of intersperse<Node | 'br'>('br', nodes)) { | ||||
| 			for (const x of intersperse<FIXME | 'br'>('br', nodes)) { | ||||
| 				el.appendChild(x === 'br' ? doc.createElement('br') : x); | ||||
| 			} | ||||
| 
 | ||||
| 			return el; | ||||
| 		}, | ||||
| 
 | ||||
| 		url(token) { | ||||
| 		url(node) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = token.node.props.url; | ||||
| 			a.textContent = token.node.props.url; | ||||
| 			a.href = node.props.url; | ||||
| 			a.textContent = node.props.url; | ||||
| 			return a; | ||||
| 		}, | ||||
| 
 | ||||
| 		search(token) { | ||||
| 		search(node) { | ||||
| 			const a = doc.createElement('a'); | ||||
| 			a.href = `https://www.google.com/search?q=${token.node.props.query}`; | ||||
| 			a.textContent = token.node.props.content; | ||||
| 			a.href = `https://www.google.com/search?q=${node.props.query}`; | ||||
| 			a.textContent = node.props.content; | ||||
| 			return a; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	appendChildren(tokens, doc.body); | ||||
| 	appendChildren(nodes, doc.body); | ||||
| 
 | ||||
| 	return `<p>${doc.body.innerHTML}</p>`; | ||||
| } | ||||
|  |  | |||
|  | @ -1,99 +0,0 @@ | |||
| import { MfmForest, MfmTree } from './prelude'; | ||||
| import { nyaize } from '@/misc/nyaize'; | ||||
| 
 | ||||
| export type RestoreOptions = { | ||||
| 	doNyaize?: boolean; | ||||
| }; | ||||
| 
 | ||||
| export function toString(tokens: MfmForest | null, opts?: RestoreOptions): string { | ||||
| 
 | ||||
| 	if (tokens === null) return ''; | ||||
| 
 | ||||
| 	function appendChildren(children: MfmForest, opts?: RestoreOptions): string { | ||||
| 		return children.map(t => handlers[t.node.type](t, opts)).join(''); | ||||
| 	} | ||||
| 
 | ||||
| 	const handlers: { [key: string]: (token: MfmTree, opts?: RestoreOptions) => string } = { | ||||
| 		bold(token, opts) { | ||||
| 			return `**${appendChildren(token.children, opts)}**`; | ||||
| 		}, | ||||
| 
 | ||||
| 		small(token, opts) { | ||||
| 			return `<small>${appendChildren(token.children, opts)}</small>`; | ||||
| 		}, | ||||
| 
 | ||||
| 		strike(token, opts) { | ||||
| 			return `~~${appendChildren(token.children, opts)}~~`; | ||||
| 		}, | ||||
| 
 | ||||
| 		italic(token, opts) { | ||||
| 			return `<i>${appendChildren(token.children, opts)}</i>`; | ||||
| 		}, | ||||
| 
 | ||||
| 		fn(token, opts) { | ||||
| 			const name = token.node.props?.name; | ||||
| 			const args = token.node.props?.args || {}; | ||||
| 			const argsStr = Object.entries(args).map(([k, v]) => v === true ? k : `${k}=${v}`).join(','); | ||||
| 			return `[${name}${argsStr !== '' ? '.' + argsStr : ''} ${appendChildren(token.children, opts)}]`; | ||||
| 		}, | ||||
| 
 | ||||
| 		blockCode(token) { | ||||
| 			return `\`\`\`${token.node.props.lang || ''}\n${token.node.props.code}\n\`\`\`\n`; | ||||
| 		}, | ||||
| 
 | ||||
| 		center(token, opts) { | ||||
| 			return `<center>${appendChildren(token.children, opts)}</center>`; | ||||
| 		}, | ||||
| 
 | ||||
| 		emoji(token) { | ||||
| 			return (token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`); | ||||
| 		}, | ||||
| 
 | ||||
| 		hashtag(token) { | ||||
| 			return `#${token.node.props.hashtag}`; | ||||
| 		}, | ||||
| 
 | ||||
| 		inlineCode(token) { | ||||
| 			return `\`${token.node.props.code}\``; | ||||
| 		}, | ||||
| 
 | ||||
| 		mathInline(token) { | ||||
| 			return `\\(${token.node.props.formula}\\)`; | ||||
| 		}, | ||||
| 
 | ||||
| 		mathBlock(token) { | ||||
| 			return `\\[${token.node.props.formula}\\]`; | ||||
| 		}, | ||||
| 
 | ||||
| 		link(token, opts) { | ||||
| 			if (token.node.props.silent) { | ||||
| 				return `?[${appendChildren(token.children, opts)}](${token.node.props.url})`; | ||||
| 			} else { | ||||
| 				return `[${appendChildren(token.children, opts)}](${token.node.props.url})`; | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		mention(token) { | ||||
| 			return token.node.props.canonical; | ||||
| 		}, | ||||
| 
 | ||||
| 		quote(token) { | ||||
| 			return `${appendChildren(token.children, {doNyaize: false}).replace(/^/gm,'>').trim()}\n`; | ||||
| 		}, | ||||
| 
 | ||||
| 		text(token, opts) { | ||||
| 			return (opts && opts.doNyaize) ? nyaize(token.node.props.text) : token.node.props.text; | ||||
| 		}, | ||||
| 
 | ||||
| 		url(token) { | ||||
| 			return `<${token.node.props.url}>`; | ||||
| 		}, | ||||
| 
 | ||||
| 		search(token, opts) { | ||||
| 			const query = token.node.props.query; | ||||
| 			return `${(opts && opts.doNyaize ? nyaize(query) : query)} [search]\n`; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	return appendChildren(tokens, { doNyaize: (opts && opts.doNyaize) || false }).trim(); | ||||
| } | ||||
							
								
								
									
										18
									
								
								src/misc/extract-custom-emojis-from-mfm.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/misc/extract-custom-emojis-from-mfm.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import * as mfm from 'mfm-js'; | ||||
| import { unique } from '@/prelude/array'; | ||||
| 
 | ||||
| export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] { | ||||
| 	const emojiNodes = [] as mfm.MfmEmojiCode[]; | ||||
| 
 | ||||
| 	function scan(nodes: mfm.MfmNode[]) { | ||||
| 		for (const node of nodes) { | ||||
| 			if (node.type === 'emojiCode') emojiNodes.push(node); | ||||
| 			else if (node.children) scan(node.children); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	scan(nodes); | ||||
| 
 | ||||
| 	const emojis = emojiNodes.filter(x => x.props.name.length <= 100).map(x => x.props.name!); | ||||
| 	return unique(emojis); | ||||
| } | ||||
|  | @ -1,9 +0,0 @@ | |||
| import { EmojiNode, MfmForest } from '../mfm/prelude'; | ||||
| import { preorderF } from '../prelude/tree'; | ||||
| import { unique } from '../prelude/array'; | ||||
| 
 | ||||
| export default function(mfmForest: MfmForest): string[] { | ||||
| 	const emojiNodes = preorderF(mfmForest).filter(x => x.type === 'emoji') as EmojiNode[]; | ||||
| 	const emojis = emojiNodes.filter(x => x.props.name && x.props.name.length <= 100).map(x => x.props.name); | ||||
| 	return unique(emojis); | ||||
| } | ||||
|  | @ -1,9 +1,18 @@ | |||
| import { HashtagNode, MfmForest } from '../mfm/prelude'; | ||||
| import { preorderF } from '../prelude/tree'; | ||||
| import { unique } from '../prelude/array'; | ||||
| import * as mfm from 'mfm-js'; | ||||
| import { unique } from '@/prelude/array'; | ||||
| 
 | ||||
| export default function(nodes: mfm.MfmNode[]): string[] { | ||||
| 	const hashtagNodes = [] as mfm.MfmHashtag[]; | ||||
| 
 | ||||
| 	function scan(nodes: mfm.MfmNode[]) { | ||||
| 		for (const node of nodes) { | ||||
| 			if (node.type === 'hashtag') hashtagNodes.push(node); | ||||
| 			else if (node.children) scan(node.children); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	scan(nodes); | ||||
| 
 | ||||
| export default function(mfmForest: MfmForest): string[] { | ||||
| 	const hashtagNodes = preorderF(mfmForest).filter(x => x.type === 'hashtag') as HashtagNode[]; | ||||
| 	const hashtags = hashtagNodes.map(x => x.props.hashtag); | ||||
| 	return unique(hashtags); | ||||
| } | ||||
|  |  | |||
|  | @ -1,10 +1,19 @@ | |||
| // test is located in test/extract-mentions
 | ||||
| 
 | ||||
| import { MentionNode, MfmForest } from '../mfm/prelude'; | ||||
| import { preorderF } from '../prelude/tree'; | ||||
| import * as mfm from 'mfm-js'; | ||||
| 
 | ||||
| export default function(mfmForest: MfmForest): MentionNode['props'][] { | ||||
| export default function(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { | ||||
| 	// TODO: 重複を削除
 | ||||
| 	const mentionNodes = preorderF(mfmForest).filter(x => x.type === 'mention') as MentionNode[]; | ||||
| 	const mentionNodes = [] as mfm.MfmMention[]; | ||||
| 
 | ||||
| 	function scan(nodes: mfm.MfmNode[]) { | ||||
| 		for (const node of nodes) { | ||||
| 			if (node.type === 'mention') mentionNodes.push(node); | ||||
| 			else if (node.children) scan(node.children); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	scan(nodes); | ||||
| 
 | ||||
| 	return mentionNodes.map(x => x.props); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										34
									
								
								src/misc/extract-url-from-mfm.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/misc/extract-url-from-mfm.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | |||
| import * as mfm from 'mfm-js'; | ||||
| import { unique } from '@/prelude/array'; | ||||
| 
 | ||||
| // unique without hash
 | ||||
| // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
 | ||||
| const removeHash = (x: string) => x.replace(/#[^#]*$/, ''); | ||||
| 
 | ||||
| export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] { | ||||
| 	const urlNodes = [] as (mfm.MfmUrl | mfm.MfmLink)[]; | ||||
| 
 | ||||
| 	function scan(nodes: mfm.MfmNode[]) { | ||||
| 		for (const node of nodes) { | ||||
| 			if (node.type === 'url') { | ||||
| 				urlNodes.push(node); | ||||
| 			} else if (node.type === 'link') { | ||||
| 				if (!respectSilentFlag || !node.props.silent) { | ||||
| 					urlNodes.push(node); | ||||
| 				} | ||||
| 			} else if (node.children) { | ||||
| 				scan(node.children); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	scan(nodes); | ||||
| 
 | ||||
| 	const urls = unique(urlNodes.map(x => x.props.url)); | ||||
| 
 | ||||
| 	return urls.reduce((array, url) => { | ||||
| 		const removed = removeHash(url); | ||||
| 		if (!array.map(x => removeHash(x)).includes(removed)) array.push(url); | ||||
| 		return array; | ||||
| 	}, [] as string[]); | ||||
| } | ||||
|  | @ -1,12 +1,12 @@ | |||
| import { EntityRepository, Repository, In } from 'typeorm'; | ||||
| import * as mfm from 'mfm-js'; | ||||
| import { Note } from '../entities/note'; | ||||
| import { User } from '../entities/user'; | ||||
| import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..'; | ||||
| import { SchemaType } from '@/misc/schema'; | ||||
| import { nyaize } from '@/misc/nyaize'; | ||||
| import { awaitAll } from '../../prelude/await-all'; | ||||
| import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib'; | ||||
| import { toString } from '../../mfm/to-string'; | ||||
| import { parse } from '../../mfm/parse'; | ||||
| import { NoteReaction } from '../entities/note-reaction'; | ||||
| import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis'; | ||||
| 
 | ||||
|  | @ -223,8 +223,13 @@ export class NoteRepository extends Repository<Note> { | |||
| 		}); | ||||
| 
 | ||||
| 		if (packed.user.isCat && packed.text) { | ||||
| 			const tokens = packed.text ? parse(packed.text) : []; | ||||
| 			packed.text = toString(tokens, { doNyaize: true }); | ||||
| 			const tokens = packed.text ? mfm.parse(packed.text) : []; | ||||
| 			mfm.inspect(tokens, node => { | ||||
| 				if (node.type === 'text') { | ||||
| 					node.props.text = nyaize(node.props.text); | ||||
| 				} | ||||
| 			}); | ||||
| 			packed.text = mfm.toString(tokens); | ||||
| 		} | ||||
| 
 | ||||
| 		if (!opts.skipHide) { | ||||
|  |  | |||
|  | @ -1,36 +0,0 @@ | |||
| import { concat, sum } from './array'; | ||||
| 
 | ||||
| export type Tree<T> = { | ||||
| 	node: T, | ||||
| 	children: Forest<T>; | ||||
| }; | ||||
| 
 | ||||
| export type Forest<T> = Tree<T>[]; | ||||
| 
 | ||||
| export function createLeaf<T>(node: T): Tree<T> { | ||||
| 	return { node, children: [] }; | ||||
| } | ||||
| 
 | ||||
| export function createTree<T>(node: T, children: Forest<T>): Tree<T> { | ||||
| 	return { node, children }; | ||||
| } | ||||
| 
 | ||||
| export function hasChildren<T>(t: Tree<T>): boolean { | ||||
| 	return t.children.length !== 0; | ||||
| } | ||||
| 
 | ||||
| export function preorder<T>(t: Tree<T>): T[] { | ||||
| 	return [t.node, ...preorderF(t.children)]; | ||||
| } | ||||
| 
 | ||||
| export function preorderF<T>(ts: Forest<T>): T[] { | ||||
| 	return concat(ts.map(preorder)); | ||||
| } | ||||
| 
 | ||||
| export function countNodes<T>(t: Tree<T>): number { | ||||
| 	return preorder(t).length; | ||||
| } | ||||
| 
 | ||||
| export function countNodesF<T>(ts: Forest<T>): number { | ||||
| 	return sum(ts.map(countNodes)); | ||||
| } | ||||
|  | @ -1,9 +1,9 @@ | |||
| import * as mfm from 'mfm-js'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { toHtml } from '../../../mfm/to-html'; | ||||
| import { parse } from '../../../mfm/parse'; | ||||
| 
 | ||||
| export default function(note: Note) { | ||||
| 	let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers)); | ||||
| 	let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null; | ||||
| 	if (html == null) html = '<p>.</p>'; | ||||
| 
 | ||||
| 	return html; | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import { URL } from 'url'; | ||||
| import * as mfm from 'mfm-js'; | ||||
| import renderImage from './image'; | ||||
| import renderKey from './key'; | ||||
| import config from '@/config'; | ||||
| import { ILocalUser } from '../../../models/entities/user'; | ||||
| import { toHtml } from '../../../mfm/to-html'; | ||||
| import { parse } from '../../../mfm/parse'; | ||||
| import { getEmojis } from './note'; | ||||
| import renderEmoji from './emoji'; | ||||
| import { IIdentifier } from '../models/identifier'; | ||||
|  | @ -66,7 +66,7 @@ export async function renderPerson(user: ILocalUser) { | |||
| 		url: `${config.url}/@${user.username}`, | ||||
| 		preferredUsername: user.username, | ||||
| 		name: user.name, | ||||
| 		summary: toHtml(parse(profile.description)), | ||||
| 		summary: profile.description ? toHtml(mfm.parse(profile.description)) : null, | ||||
| 		icon: avatar ? renderImage(avatar) : null, | ||||
| 		image: banner ? renderImage(banner) : null, | ||||
| 		tag, | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import $ from 'cafy'; | ||||
| import * as mfm from 'mfm-js'; | ||||
| import { ID } from '@/misc/cafy-id'; | ||||
| import { publishMainStream, publishUserEvent } from '../../../../services/stream'; | ||||
| import acceptAllFollowRequests from '../../../../services/following/requests/accept-all'; | ||||
| import { publishToFollowers } from '../../../../services/i/update'; | ||||
| import define from '../../define'; | ||||
| import { parse, parsePlain } from '../../../../mfm/parse'; | ||||
| import extractEmojis from '@/misc/extract-emojis'; | ||||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; | ||||
| import extractHashtags from '@/misc/extract-hashtags'; | ||||
| import * as langmap from 'langmap'; | ||||
| import { updateUsertags } from '../../../../services/update-hashtag'; | ||||
|  | @ -291,13 +291,13 @@ export default define(meta, async (ps, _user, token) => { | |||
| 	const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; | ||||
| 
 | ||||
| 	if (newName != null) { | ||||
| 		const tokens = parsePlain(newName); | ||||
| 		emojis = emojis.concat(extractEmojis(tokens!)); | ||||
| 		const tokens = mfm.parsePlain(newName); | ||||
| 		emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); | ||||
| 	} | ||||
| 
 | ||||
| 	if (newDescription != null) { | ||||
| 		const tokens = parse(newDescription); | ||||
| 		emojis = emojis.concat(extractEmojis(tokens!)); | ||||
| 		const tokens = mfm.parse(newDescription); | ||||
| 		emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!)); | ||||
| 		tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32); | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import * as mfm from 'mfm-js'; | ||||
| import es from '../../db/elasticsearch'; | ||||
| import { publishMainStream, publishNotesStream } from '../stream'; | ||||
| import DeliverManager from '../../remote/activitypub/deliver-manager'; | ||||
|  | @ -5,7 +6,6 @@ import renderNote from '../../remote/activitypub/renderer/note'; | |||
| import renderCreate from '../../remote/activitypub/renderer/create'; | ||||
| import renderAnnounce from '../../remote/activitypub/renderer/announce'; | ||||
| import { renderActivity } from '../../remote/activitypub/renderer'; | ||||
| import { parse } from '../../mfm/parse'; | ||||
| import { resolveUser } from '../../remote/resolve-user'; | ||||
| import config from '@/config'; | ||||
| import { updateHashtags } from '../update-hashtag'; | ||||
|  | @ -13,7 +13,7 @@ import { concat } from '../../prelude/array'; | |||
| import insertNoteUnread from './unread'; | ||||
| import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc'; | ||||
| import extractMentions from '@/misc/extract-mentions'; | ||||
| import extractEmojis from '@/misc/extract-emojis'; | ||||
| import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm'; | ||||
| import extractHashtags from '@/misc/extract-hashtags'; | ||||
| import { Note, IMentionedRemoteUsers } from '../../models/entities/note'; | ||||
| import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings } from '../../models'; | ||||
|  | @ -182,17 +182,17 @@ export default async (user: { id: User['id']; username: User['username']; host: | |||
| 
 | ||||
| 	// Parse MFM if needed
 | ||||
| 	if (!tags || !emojis || !mentionedUsers) { | ||||
| 		const tokens = data.text ? parse(data.text)! : []; | ||||
| 		const cwTokens = data.cw ? parse(data.cw)! : []; | ||||
| 		const tokens = data.text ? mfm.parse(data.text)! : []; | ||||
| 		const cwTokens = data.cw ? mfm.parse(data.cw)! : []; | ||||
| 		const choiceTokens = data.poll && data.poll.choices | ||||
| 			? concat(data.poll.choices.map(choice => parse(choice)!)) | ||||
| 			? concat(data.poll.choices.map(choice => mfm.parse(choice)!)) | ||||
| 			: []; | ||||
| 
 | ||||
| 		const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens); | ||||
| 
 | ||||
| 		tags = data.apHashtags || extractHashtags(combinedTokens); | ||||
| 
 | ||||
| 		emojis = data.apEmojis || extractEmojis(combinedTokens); | ||||
| 		emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens); | ||||
| 
 | ||||
| 		mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens); | ||||
| 	} | ||||
|  | @ -604,7 +604,7 @@ function incNotesCountOfUser(user: { id: User['id']; }) { | |||
| 		.execute(); | ||||
| } | ||||
| 
 | ||||
| async function extractMentionedUsers(user: { host: User['host']; }, tokens: ReturnType<typeof parse>): Promise<User[]> { | ||||
| async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise<User[]> { | ||||
| 	if (tokens == null) return []; | ||||
| 
 | ||||
| 	const mentions = extractMentions(tokens); | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import * as assert from 'assert'; | ||||
| 
 | ||||
| import extractMentions from '../src/misc/extract-mentions'; | ||||
| import { parse } from '../src/mfm/parse'; | ||||
| import { parse } from 'mfm-js'; | ||||
| 
 | ||||
| describe('Extract mentions', () => { | ||||
| 	it('simple', () => { | ||||
|  | @ -10,17 +10,14 @@ describe('Extract mentions', () => { | |||
| 		assert.deepStrictEqual(mentions, [{ | ||||
| 			username: 'foo', | ||||
| 			acct: '@foo', | ||||
| 			canonical: '@foo', | ||||
| 			host: null | ||||
| 		}, { | ||||
| 			username: 'bar', | ||||
| 			acct: '@bar', | ||||
| 			canonical: '@bar', | ||||
| 			host: null | ||||
| 		}, { | ||||
| 			username: 'baz', | ||||
| 			acct: '@baz', | ||||
| 			canonical: '@baz', | ||||
| 			host: null | ||||
| 		}]); | ||||
| 	}); | ||||
|  | @ -31,17 +28,14 @@ describe('Extract mentions', () => { | |||
| 		assert.deepStrictEqual(mentions, [{ | ||||
| 			username: 'foo', | ||||
| 			acct: '@foo', | ||||
| 			canonical: '@foo', | ||||
| 			host: null | ||||
| 		}, { | ||||
| 			username: 'bar', | ||||
| 			acct: '@bar', | ||||
| 			canonical: '@bar', | ||||
| 			host: null | ||||
| 		}, { | ||||
| 			username: 'baz', | ||||
| 			acct: '@baz', | ||||
| 			canonical: '@baz', | ||||
| 			host: null | ||||
| 		}]); | ||||
| 	}); | ||||
|  |  | |||
							
								
								
									
										1150
									
								
								test/mfm.ts
									
										
									
									
									
								
							
							
						
						
									
										1150
									
								
								test/mfm.ts
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										17
									
								
								yarn.lock
									
										
									
									
									
								
							
							
						
						
									
										17
									
								
								yarn.lock
									
										
									
									
									
								
							|  | @ -6608,6 +6608,13 @@ methods@^1.1.2: | |||
|   resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" | ||||
|   integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= | ||||
| 
 | ||||
| mfm-js@0.12.0: | ||||
|   version "0.12.0" | ||||
|   resolved "https://registry.yarnpkg.com/mfm-js/-/mfm-js-0.12.0.tgz#47be2fdb18869b9e55576fffcc159d0417c670db" | ||||
|   integrity sha512-u0IyIMwzsGsOGmctRXcOdWYsh9LWHKHqX+XCBfPjORX+1DCBdonaO6pryOawns6z16Xvus2yZk0KMMqWt2TotQ== | ||||
|   dependencies: | ||||
|     twemoji-parser "13.0.x" | ||||
| 
 | ||||
| micromatch@^3.0.4, micromatch@^3.1.4: | ||||
|   version "3.1.10" | ||||
|   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" | ||||
|  | @ -7494,11 +7501,6 @@ parseurl@^1.3.2: | |||
|   resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" | ||||
|   integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== | ||||
| 
 | ||||
| parsimmon@1.16.0: | ||||
|   version "1.16.0" | ||||
|   resolved "https://registry.yarnpkg.com/parsimmon/-/parsimmon-1.16.0.tgz#2834e3db645b6a855ab2ea14fbaad10d82867e0f" | ||||
|   integrity sha512-tekGDz2Lny27SQ/5DzJdIK0lqsWwZ667SCLFIDCxaZM7VNgQjyKLbaL7FYPKpbjdxNAXFV/mSxkq5D2fnkW4pA== | ||||
| 
 | ||||
| pascalcase@^0.1.1: | ||||
|   version "0.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" | ||||
|  | @ -10521,6 +10523,11 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: | |||
|   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" | ||||
|   integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= | ||||
| 
 | ||||
| twemoji-parser@13.0.x: | ||||
|   version "13.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.0.0.tgz#bd9d1b98474f1651dc174696b45cabefdfa405af" | ||||
|   integrity sha512-zMaGdskpH8yKjT2RSE/HwE340R4Fm+fbie4AaqjDa4H/l07YUmAvxkSfNl6awVWNRRQ0zdzLQ8SAJZuY5MgstQ== | ||||
| 
 | ||||
| type-check@^0.4.0, type-check@~0.4.0: | ||||
|   version "0.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue