Compare commits
	
		
			18 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					d8620187ec | ||
| 
						 | 
					520849d070 | ||
| 
						 | 
					b6a028a8ed | ||
| 
						 | 
					af5839bb59 | ||
| 
						 | 
					a53e0d9f73 | ||
| 
						 | 
					49921f2dcf | ||
| 
						 | 
					6b947c2139 | ||
| 
						 | 
					98acf919f1 | ||
| 
						 | 
					c9c2853150 | ||
| 
						 | 
					2bc708f8e6 | ||
| 
						 | 
					874b8fc3c2 | ||
| 
						 | 
					7d6aac3431 | ||
| 
						 | 
					e2fc7decad | ||
| 
						 | 
					21bed71f5e | ||
| 
						 | 
					3b974428fc | ||
| 
						 | 
					580191fb17 | ||
| 
						 | 
					be0cb88b6c | ||
| 
						 | 
					95c4e4497e | 
							
								
								
									
										16
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,6 +1,22 @@
 | 
			
		||||
ChangeLog
 | 
			
		||||
=========
 | 
			
		||||
 | 
			
		||||
10.66.2
 | 
			
		||||
-------
 | 
			
		||||
* i18nの修正
 | 
			
		||||
* ドライブのファイル一覧取得APIでファイルサイズによるソートが機能していなかった問題を修正
 | 
			
		||||
* リモートユーザーの更新時に、各ピン留め投稿の取得失敗は無視するように
 | 
			
		||||
* リモートMisskeyユーザーの情報が登録/更新出来なくなっていたのを修正
 | 
			
		||||
* メンションのリンク先URLに余計な@がプリフィクスされていたのを修正
 | 
			
		||||
* ダイレクトでリプライする際、リプライ先のユーザーは自動的に公開先として追加するように
 | 
			
		||||
* ダイレクトでメンションでもユーザーを指定できるように
 | 
			
		||||
 | 
			
		||||
10.66.1
 | 
			
		||||
-------
 | 
			
		||||
* ActivityPubのsharedInboxに関して修正
 | 
			
		||||
* MFMでのカッコの判定を改善
 | 
			
		||||
* バグ修正
 | 
			
		||||
 | 
			
		||||
10.66.0
 | 
			
		||||
-------
 | 
			
		||||
* ユーザーごとのRSSフィードを提供するように
 | 
			
		||||
 
 | 
			
		||||
@@ -805,6 +805,7 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  update-settings: "Advanced settings"
 | 
			
		||||
  prevent-update: "Postpone updates (not recommended)"
 | 
			
		||||
  prevent-update-desc: "Even if you turn this setting on, updates may apply. This setting is enabled only for this device."
 | 
			
		||||
  mark-as-read-all-unread-notes: "Mark all posts as read"
 | 
			
		||||
  no-updates: "No updates available"
 | 
			
		||||
  no-updates-desc: "Your Misskey is up to date."
 | 
			
		||||
  update-available: "A new version is available"
 | 
			
		||||
@@ -1430,7 +1431,6 @@ mobile/views/pages/settings.vue:
 | 
			
		||||
  signout: "Sign out"
 | 
			
		||||
  sound: "Sounds"
 | 
			
		||||
  enable-sounds: "Enable sounds"
 | 
			
		||||
  mark-as-read-all-unread-notes: "Mark all posts as read"
 | 
			
		||||
  password: "Password"
 | 
			
		||||
mobile/views/pages/user.vue:
 | 
			
		||||
  follows-you: "Follows you"
 | 
			
		||||
 
 | 
			
		||||
@@ -838,6 +838,7 @@ desktop/views/components/settings.vue:
 | 
			
		||||
  2fa: "二段階認証"
 | 
			
		||||
  other: "その他"
 | 
			
		||||
  license: "ライセンス"
 | 
			
		||||
  mark-as-read-all-unread-notes: "すべての投稿を既読にする"
 | 
			
		||||
  theme: "テーマ"
 | 
			
		||||
 | 
			
		||||
  behaviour: "動作"
 | 
			
		||||
@@ -1641,7 +1642,6 @@ mobile/views/pages/settings.vue:
 | 
			
		||||
  signout: "サインアウト"
 | 
			
		||||
  sound: "サウンド"
 | 
			
		||||
  enable-sounds: "サウンドを有効にする"
 | 
			
		||||
  mark-as-read-all-unread-notes: "すべての投稿を既読にする"
 | 
			
		||||
  password: "パスワード"
 | 
			
		||||
 | 
			
		||||
mobile/views/pages/user.vue:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"author": "syuilo <i@syuilo.com>",
 | 
			
		||||
	"version": "10.66.0",
 | 
			
		||||
	"clientVersion": "2.0.12855",
 | 
			
		||||
	"version": "10.66.2",
 | 
			
		||||
	"clientVersion": "2.0.12873",
 | 
			
		||||
	"codename": "nighthike",
 | 
			
		||||
	"main": "./built/index.js",
 | 
			
		||||
	"private": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
<router-link class="ldlomzub" :to="`/@${ canonical }`" v-user-preview="canonical">
 | 
			
		||||
<router-link class="ldlomzub" :to="`/${ canonical }`" v-user-preview="canonical">
 | 
			
		||||
	<span class="me" v-if="isMe">{{ $t('@.you') }}</span>
 | 
			
		||||
	<span class="main">
 | 
			
		||||
		<span class="username">@{{ username }}</span>
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ export default Vue.extend({
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				if (items[0].kind == 'file') {
 | 
			
		||||
					alert('%i18n:only-one-file-attached%');
 | 
			
		||||
					alert(this.$t('only-one-file-attached'));
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
@@ -107,7 +107,7 @@ export default Vue.extend({
 | 
			
		||||
				return;
 | 
			
		||||
			} else if (e.dataTransfer.files.length > 1) {
 | 
			
		||||
				e.preventDefault();
 | 
			
		||||
				alert('%i18n:only-one-file-attached%');
 | 
			
		||||
				alert(this.$t('only-one-file-attached'));
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -207,9 +207,8 @@ export default Vue.extend({
 | 
			
		||||
			this.visibility = this.reply.visibility;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ダイレクトへのリプライはリプライ先ユーザーを初期設定
 | 
			
		||||
		if (this.reply && this.reply.visibility === 'specified') {
 | 
			
		||||
			this.$root.api('users/show', {	userId: this.reply.userId }).then(user => {
 | 
			
		||||
		if (this.reply) {
 | 
			
		||||
			this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
 | 
			
		||||
				this.visibleUsers.push(user);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
@@ -236,7 +235,7 @@ export default Vue.extend({
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
	  trimmedLength(text: string) {
 | 
			
		||||
		trimmedLength(text: string) {
 | 
			
		||||
			return length(text.trim());
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
	<header :class="$style.header">
 | 
			
		||||
		<h1>#{{ $route.params.tag }}</h1>
 | 
			
		||||
	</header>
 | 
			
		||||
	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
 | 
			
		||||
	<p :class="$style.empty" v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
 | 
			
		||||
	<mk-notes ref="timeline" :class="$style.notes" :more="existMore ? more : null"/>
 | 
			
		||||
</mk-ui>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -197,9 +197,8 @@ export default Vue.extend({
 | 
			
		||||
			this.visibility = this.reply.visibility;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ダイレクトへのリプライはリプライ先ユーザーを初期設定
 | 
			
		||||
		if (this.reply && this.reply.visibility === 'specified') {
 | 
			
		||||
			this.$root.api('users/show', {	userId: this.reply.userId }).then(user => {
 | 
			
		||||
		if (this.reply) {
 | 
			
		||||
			this.$root.api('users/show', { userId: this.reply.userId }).then(user => {
 | 
			
		||||
				this.visibleUsers.push(user);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
	<span slot="header"><span style="margin-right:4px;"><fa icon="hashtag"/></span>{{ $route.params.tag }}</span>
 | 
			
		||||
 | 
			
		||||
	<main>
 | 
			
		||||
		<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q }) }}</p>
 | 
			
		||||
		<p v-if="!fetching && empty"><fa icon="search"/> {{ $t('no-posts-found', { q: $route.params.tag }) }}</p>
 | 
			
		||||
		<mk-notes ref="timeline" :more="existMore ? more : null"/>
 | 
			
		||||
	</main>
 | 
			
		||||
</mk-ui>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import * as P from 'parsimmon';
 | 
			
		||||
import parseAcct from '../misc/acct/parse';
 | 
			
		||||
import { toUnicode } from 'punycode';
 | 
			
		||||
import { takeWhile } from '../prelude/array';
 | 
			
		||||
import { takeWhile, cumulativeSum } from '../prelude/array';
 | 
			
		||||
import { Tree } from '../prelude/tree';
 | 
			
		||||
import * as T from '../prelude/tree';
 | 
			
		||||
 | 
			
		||||
@@ -42,30 +42,18 @@ export function createTree(type: string, children: MfmForest, props: any): MfmTr
 | 
			
		||||
	return T.createTree({ type, props }, children);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getTrailingPosition(x: string): number {
 | 
			
		||||
	const brackets = [
 | 
			
		||||
		['(', ')'],
 | 
			
		||||
		['「', '」'],
 | 
			
		||||
	];
 | 
			
		||||
	const pendingBrackets = [] as any;
 | 
			
		||||
	const end = x.split('').findIndex(char => {
 | 
			
		||||
		const closeMatch = brackets.map(x => x[1]).indexOf(char);
 | 
			
		||||
		const openMatch = brackets.map(x => x[0]).indexOf(char);
 | 
			
		||||
		if (closeMatch != -1) {
 | 
			
		||||
			if (pendingBrackets[closeMatch] > 0) {
 | 
			
		||||
				pendingBrackets[closeMatch]--;
 | 
			
		||||
				return false;
 | 
			
		||||
			} else {
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		} else if (openMatch != -1) {
 | 
			
		||||
			pendingBrackets[openMatch] = (pendingBrackets[openMatch] || 0) + 1;
 | 
			
		||||
			return false;
 | 
			
		||||
		} else {
 | 
			
		||||
			return false;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	return end > 0 ? end : x.length;
 | 
			
		||||
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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const newline = P((input, i) => {
 | 
			
		||||
@@ -220,7 +208,7 @@ const mfm = P.createLanguage({
 | 
			
		||||
			const match = text.match(/^#([^\s\.,!\?#]+)/i);
 | 
			
		||||
			if (!match) return P.makeFailure(i, 'not a hashtag');
 | 
			
		||||
			let hashtag = match[1];
 | 
			
		||||
			hashtag = hashtag.substr(0, getTrailingPosition(hashtag));
 | 
			
		||||
			hashtag = removeOrphanedBrackets(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 (hashtag.length > 50) return P.makeFailure(i, 'not a hashtag');
 | 
			
		||||
@@ -390,7 +378,7 @@ const mfm = P.createLanguage({
 | 
			
		||||
			const match = text.match(/^https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.,=\+\-]+/);
 | 
			
		||||
			if (!match) return P.makeFailure(i, 'not a url');
 | 
			
		||||
			let url = match[0];
 | 
			
		||||
			url = url.substr(0, getTrailingPosition(url));
 | 
			
		||||
			url = removeOrphanedBrackets(url);
 | 
			
		||||
			if (url.endsWith('.')) url = url.substr(0, url.lastIndexOf('.'));
 | 
			
		||||
			if (url.endsWith(',')) url = url.substr(0, url.lastIndexOf(','));
 | 
			
		||||
			return P.makeSuccess(i + url.length, url);
 | 
			
		||||
 
 | 
			
		||||
@@ -109,3 +109,9 @@ export function takeWhile<T>(f: Predicate<T>, xs: T[]): T[] {
 | 
			
		||||
	}
 | 
			
		||||
	return ys;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function cumulativeSum(xs: number[]): number[] {
 | 
			
		||||
	const ys = Array.from(xs); // deep copy
 | 
			
		||||
	for (let i = 1; i < ys.length; i++) ys[i] += ys[i - 1];
 | 
			
		||||
	return ys;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ import Instance from '../../../models/instance';
 | 
			
		||||
import getDriveFileUrl from '../../../misc/get-drive-file-url';
 | 
			
		||||
import { IEmoji } from '../../../models/emoji';
 | 
			
		||||
import { ITag } from './tag';
 | 
			
		||||
import Following from '../../../models/following';
 | 
			
		||||
 | 
			
		||||
const log = debug('misskey:activitypub');
 | 
			
		||||
 | 
			
		||||
@@ -164,7 +165,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
 | 
			
		||||
				publicKeyPem: person.publicKey.publicKeyPem
 | 
			
		||||
			},
 | 
			
		||||
			inbox: person.inbox,
 | 
			
		||||
			sharedInbox: person.sharedInbox,
 | 
			
		||||
			sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
 | 
			
		||||
			featured: person.featured,
 | 
			
		||||
			endpoints: person.endpoints,
 | 
			
		||||
			uri: person.id,
 | 
			
		||||
@@ -340,7 +341,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 | 
			
		||||
		$set: {
 | 
			
		||||
			lastFetchedAt: new Date(),
 | 
			
		||||
			inbox: person.inbox,
 | 
			
		||||
			sharedInbox: person.sharedInbox,
 | 
			
		||||
			sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
 | 
			
		||||
			featured: person.featured,
 | 
			
		||||
			avatarId: avatar ? avatar._id : null,
 | 
			
		||||
			bannerId: banner ? banner._id : null,
 | 
			
		||||
@@ -368,6 +369,15 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
 | 
			
		||||
	await Following.update({
 | 
			
		||||
		followerId: exist._id
 | 
			
		||||
	}, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			'_follower.sharedInbox': person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined)
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await updateFeatured(exist._id).catch(err => console.log(err));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -431,7 +441,7 @@ export async function updateFeatured(userId: mongo.ObjectID) {
 | 
			
		||||
 | 
			
		||||
	await User.update({ _id: user._id }, {
 | 
			
		||||
		$set: {
 | 
			
		||||
			pinnedNoteIds: featuredNotes.map(note => note._id)
 | 
			
		||||
			pinnedNoteIds: featuredNotes.filter(note => note != null).map(note => note._id)
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +63,7 @@ export default async (user: ILocalUser) => {
 | 
			
		||||
		following: `${id}/following`,
 | 
			
		||||
		featured: `${id}/collections/featured`,
 | 
			
		||||
		sharedInbox: `${config.url}/inbox`,
 | 
			
		||||
		endpoints: { sharedInbox: `${config.url}/inbox` },
 | 
			
		||||
		url: `${config.url}/@${user.username}`,
 | 
			
		||||
		preferredUsername: user.username,
 | 
			
		||||
		name: user.name,
 | 
			
		||||
 
 | 
			
		||||
@@ -56,7 +56,7 @@ export interface IPerson extends IObject {
 | 
			
		||||
	following: any;
 | 
			
		||||
	featured?: any;
 | 
			
		||||
	outbox: any;
 | 
			
		||||
	endpoints: string[];
 | 
			
		||||
	endpoints: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const isCollection = (object: IObject): object is ICollection =>
 | 
			
		||||
 
 | 
			
		||||
@@ -52,9 +52,9 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
 | 
			
		||||
			_sort = {
 | 
			
		||||
				length: -1
 | 
			
		||||
			};
 | 
			
		||||
		} else if (ps.sort == '+size') {
 | 
			
		||||
		} else if (ps.sort == '-size') {
 | 
			
		||||
			_sort = {
 | 
			
		||||
				length: -1
 | 
			
		||||
				length: 1
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ export const meta = {
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		visibleUserIds: {
 | 
			
		||||
			validator: $.arr($.type(ID)).optional.unique().min(1),
 | 
			
		||||
			validator: $.arr($.type(ID)).optional.unique().min(0),
 | 
			
		||||
			transform: transformMany,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '(投稿の公開範囲が specified の場合)投稿を閲覧できるユーザー'
 | 
			
		||||
@@ -82,6 +82,30 @@ export const meta = {
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noExtractMentions: {
 | 
			
		||||
			validator: $.bool.optional,
 | 
			
		||||
			default: false,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '本文からメンションを展開しないか否か。'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noExtractHashtags: {
 | 
			
		||||
			validator: $.bool.optional,
 | 
			
		||||
			default: false,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '本文からハッシュタグを展開しないか否か。'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		noExtractEmojis: {
 | 
			
		||||
			validator: $.bool.optional,
 | 
			
		||||
			default: false,
 | 
			
		||||
			desc: {
 | 
			
		||||
				'ja-JP': '本文からカスタム絵文字を展開しないか否か。'
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		geo: {
 | 
			
		||||
			validator: $.obj({
 | 
			
		||||
				coordinates: $.arr().length(2)
 | 
			
		||||
@@ -237,6 +261,9 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
 | 
			
		||||
		localOnly: ps.localOnly,
 | 
			
		||||
		visibility: ps.visibility,
 | 
			
		||||
		visibleUsers,
 | 
			
		||||
		apMentions: ps.noExtractMentions ? [] : undefined,
 | 
			
		||||
		apHashtags: ps.noExtractHashtags ? [] : undefined,
 | 
			
		||||
		apEmojis: ps.noExtractEmojis ? [] : undefined,
 | 
			
		||||
		geo: ps.geo
 | 
			
		||||
	})
 | 
			
		||||
	.then(note => pack(note, user))
 | 
			
		||||
 
 | 
			
		||||
@@ -182,6 +182,17 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
 | 
			
		||||
				mentionedUsers.push(u);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for (const u of mentionedUsers) {
 | 
			
		||||
			if (!data.visibleUsers.some(x => x._id.equals(u._id))) {
 | 
			
		||||
				data.visibleUsers.push(u);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// ダイレクト投稿でユーザーが指定されていなかったらreject
 | 
			
		||||
		if (data.visibleUsers.length === 0) {
 | 
			
		||||
			return rej('Target user is not specified');
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const note = await insertNote(user, data, tags, emojis, mentionedUsers);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										95
									
								
								test/mfm.ts
									
									
									
									
									
								
							
							
						
						
									
										95
									
								
								test/mfm.ts
									
									
									
									
									
								
							@@ -6,7 +6,7 @@ import * as assert from 'assert';
 | 
			
		||||
 | 
			
		||||
import analyze from '../src/mfm/parse';
 | 
			
		||||
import toHtml from '../src/mfm/html';
 | 
			
		||||
import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/parser';
 | 
			
		||||
import { createTree as tree, createLeaf as leaf, MfmTree, removeOrphanedBrackets } from '../src/mfm/parser';
 | 
			
		||||
 | 
			
		||||
function text(text: string): MfmTree {
 | 
			
		||||
	return leaf('text', { text });
 | 
			
		||||
@@ -49,6 +49,99 @@ describe('createTree', () => {
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('removeOrphanedBrackets', () => {
 | 
			
		||||
	it('single (contained)', () => {
 | 
			
		||||
		const input = '(foo)';
 | 
			
		||||
		const expected = '(foo)';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('single (head)', () => {
 | 
			
		||||
		const input = '(foo)bar';
 | 
			
		||||
		const expected = '(foo)bar';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('single (tail)', () => {
 | 
			
		||||
		const input = 'foo(bar)';
 | 
			
		||||
		const expected = 'foo(bar)';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('a', () => {
 | 
			
		||||
		const input = '(foo';
 | 
			
		||||
		const expected = '';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('b', () => {
 | 
			
		||||
		const input = ')foo';
 | 
			
		||||
		const expected = '';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('nested', () => {
 | 
			
		||||
		const input = 'foo(「(bar)」)';
 | 
			
		||||
		const expected = 'foo(「(bar)」)';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('no brackets', () => {
 | 
			
		||||
		const input = 'foo';
 | 
			
		||||
		const expected = 'foo';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('with foreign bracket (single)', () => {
 | 
			
		||||
		const input = 'foo(bar))';
 | 
			
		||||
		const expected = 'foo(bar)';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('with foreign bracket (open)', () => {
 | 
			
		||||
		const input = 'foo(bar';
 | 
			
		||||
		const expected = 'foo';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('with foreign bracket (close)', () => {
 | 
			
		||||
		const input = 'foo)bar';
 | 
			
		||||
		const expected = 'foo';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('with foreign bracket (close and open)', () => {
 | 
			
		||||
		const input = 'foo)(bar';
 | 
			
		||||
		const expected = 'foo';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('various bracket type', () => {
 | 
			
		||||
		const input = 'foo「(bar)」(';
 | 
			
		||||
		const expected = 'foo「(bar)」';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	it('intersected', () => {
 | 
			
		||||
		const input = 'foo(「)」';
 | 
			
		||||
		const expected = 'foo(「)」';
 | 
			
		||||
		const actual = removeOrphanedBrackets(input);
 | 
			
		||||
		assert.deepStrictEqual(actual, expected);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe('MFM', () => {
 | 
			
		||||
	it('can be analyzed', () => {
 | 
			
		||||
		const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user