-
-
+
+
+
@@ -36,79 +38,19 @@ export default Vue.extend({
});
-
diff --git a/src/client/app/desktop/views/components/index.ts b/src/client/app/desktop/views/components/index.ts
index 8ea2b602d..2edc8117a 100644
--- a/src/client/app/desktop/views/components/index.ts
+++ b/src/client/app/desktop/views/components/index.ts
@@ -9,7 +9,6 @@ import subNoteContent from './sub-note-content.vue';
import window from './window.vue';
import noteFormWindow from './post-form-window.vue';
import renoteFormWindow from './renote-form-window.vue';
-import mediaImage from './media-image.vue';
import mediaVideo from './media-video.vue';
import notifications from './notifications.vue';
import noteForm from './post-form.vue';
@@ -32,7 +31,6 @@ Vue.component('mk-sub-note-content', subNoteContent);
Vue.component('mk-window', window);
Vue.component('mk-post-form-window', noteFormWindow);
Vue.component('mk-renote-form-window', renoteFormWindow);
-Vue.component('mk-media-image', mediaImage);
Vue.component('mk-media-video', mediaVideo);
Vue.component('mk-notifications', notifications);
Vue.component('mk-post-form', noteForm);
diff --git a/src/client/app/desktop/views/components/media-image.vue b/src/client/app/desktop/views/components/media-image.vue
deleted file mode 100644
index 446e50093..000000000
--- a/src/client/app/desktop/views/components/media-image.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-
-
-
- {{ $t('sensitive') }}
- {{ $t('click-to-show') }}
-
-
-
-
-
-
-
-
diff --git a/src/client/app/mobile/views/components/index.ts b/src/client/app/mobile/views/components/index.ts
index 9a410e827..351aaea9f 100644
--- a/src/client/app/mobile/views/components/index.ts
+++ b/src/client/app/mobile/views/components/index.ts
@@ -3,7 +3,6 @@ import Vue from 'vue';
import ui from './ui.vue';
import note from './note.vue';
import notes from './notes.vue';
-import mediaImage from './media-image.vue';
import mediaVideo from './media-video.vue';
import notePreview from './note-preview.vue';
import subNoteContent from './sub-note-content.vue';
@@ -24,7 +23,6 @@ import postForm from './post-form.vue';
Vue.component('mk-ui', ui);
Vue.component('mk-note', note);
Vue.component('mk-notes', notes);
-Vue.component('mk-media-image', mediaImage);
Vue.component('mk-media-video', mediaVideo);
Vue.component('mk-note-preview', notePreview);
Vue.component('mk-sub-note-content', subNoteContent);
diff --git a/src/docs/style.styl b/src/docs/style.styl
index 4af0f288b..96d14c2b9 100644
--- a/src/docs/style.styl
+++ b/src/docs/style.styl
@@ -3,6 +3,8 @@
html
--primary #fb4e4e
+ --link #fb4e4e
+ --linkTapHighlight #fb4e4eb3
body
margin 0
diff --git a/src/games/reversi/core.ts b/src/games/reversi/core.ts
index a198e8dd2..bb27d6f80 100644
--- a/src/games/reversi/core.ts
+++ b/src/games/reversi/core.ts
@@ -100,20 +100,6 @@ export default class Reversi {
return count(WHITE, this.board);
}
- /**
- * 黒石の比率
- */
- public get blackP() {
- return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.blackCount / (this.blackCount + this.whiteCount);
- }
-
- /**
- * 白石の比率
- */
- public get whiteP() {
- return this.blackCount == 0 && this.whiteCount == 0 ? 0 : this.whiteCount / (this.blackCount + this.whiteCount);
- }
-
public transformPosToXy(pos: number): number[] {
const x = pos % this.mapWidth;
const y = Math.floor(pos / this.mapWidth);
diff --git a/src/mfm/html.ts b/src/mfm/html.ts
index b86d33a39..acd6891ab 100644
--- a/src/mfm/html.ts
+++ b/src/mfm/html.ts
@@ -55,6 +55,18 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
return el;
},
+ spin(token) {
+ const el = doc.createElement('i');
+ appendChildren(token.children, el);
+ return el;
+ },
+
+ flip(token) {
+ const el = doc.createElement('span');
+ appendChildren(token.children, el);
+ return el;
+ },
+
blockCode(token) {
const pre = doc.createElement('pre');
const inner = doc.createElement('code');
diff --git a/src/mfm/parser.ts b/src/mfm/parser.ts
index 01b69c969..7bddea2ac 100644
--- a/src/mfm/parser.ts
+++ b/src/mfm/parser.ts
@@ -91,6 +91,7 @@ const mfm = P.createLanguage({
root: r => P.alt(
r.big,
r.small,
+ r.spin,
r.bold,
r.strike,
r.italic,
@@ -101,6 +102,7 @@ const mfm = P.createLanguage({
r.hashtag,
r.emoji,
r.blockCode,
+ r.flip,
r.inlineCode,
r.quote,
r.mathInline,
@@ -123,6 +125,7 @@ const mfm = P.createLanguage({
r.hashtag,
r.emoji,
r.mathInline,
+ r.spin,
r.text
).atLeast(1).tryParse(x), {})),
//#endregion
@@ -141,6 +144,15 @@ const mfm = P.createLanguage({
).atLeast(1).tryParse(x), {})),
//#endregion
+ //#region Spin
+ spin: r =>
+ P.regexp(/
(.+?)<\/spin>/, 1)
+ .map(x => createTree('spin', P.alt(
+ r.emoji,
+ r.text
+ ).atLeast(1).tryParse(x), {})),
+ //#endregion
+
//#region Block code
blockCode: r =>
newline.then(
@@ -163,6 +175,7 @@ const mfm = P.createLanguage({
r.hashtag,
r.url,
r.link,
+ r.flip,
r.emoji,
r.text
).atLeast(1).tryParse(x), {})),
@@ -174,6 +187,7 @@ const mfm = P.createLanguage({
.map(x => createTree('center', P.alt(
r.big,
r.small,
+ r.spin,
r.bold,
r.strike,
r.italic,
@@ -184,6 +198,7 @@ const mfm = P.createLanguage({
r.mathInline,
r.url,
r.link,
+ r.flip,
r.text
).atLeast(1).tryParse(x), {})),
//#endregion
@@ -217,6 +232,23 @@ const mfm = P.createLanguage({
}),
//#endregion
+ //#region Flip
+ flip: r =>
+ P.regexp(/(.+?)<\/flip>/, 1)
+ .map(x => createTree('flip', P.alt(
+ r.big,
+ r.small,
+ r.spin,
+ r.bold,
+ r.strike,
+ r.link,
+ r.italic,
+ r.motion,
+ r.emoji,
+ r.text
+ ).atLeast(1).tryParse(x), {})),
+ //#endregion
+
//#region Inline code
inlineCode: r =>
P.regexp(/`([^´\n]+?)`/, 1)
@@ -242,6 +274,7 @@ const mfm = P.createLanguage({
r.hashtag,
r.url,
r.link,
+ r.flip,
r.emoji,
r.text
).atLeast(1).tryParse(x), {})),
@@ -262,6 +295,7 @@ const mfm = P.createLanguage({
return createTree('link', P.alt(
r.big,
r.small,
+ r.spin,
r.bold,
r.strike,
r.italic,
@@ -311,6 +345,7 @@ const mfm = P.createLanguage({
.map(x => createTree('motion', P.alt(
r.bold,
r.small,
+ r.spin,
r.strike,
r.italic,
r.mention,
@@ -318,6 +353,7 @@ const mfm = P.createLanguage({
r.emoji,
r.url,
r.link,
+ r.flip,
r.mathInline,
r.text
).atLeast(1).tryParse(x), {})),
@@ -356,6 +392,7 @@ const mfm = P.createLanguage({
r.hashtag,
r.url,
r.link,
+ r.flip,
r.emoji,
r.text
).atLeast(1).tryParse(x), {})),
@@ -365,18 +402,20 @@ const mfm = P.createLanguage({
title: r =>
newline.then(P((input, i) => {
const text = input.substr(i);
- const match = text.match(/^((【|\[)(.+?)(】|]))(\n|$)/);
+ const match = text.match(/^([【\[]([^【\[】\]\n]+?)[】\]])(\n|$)/);
if (!match) return P.makeFailure(i, 'not a title');
- const q = match[1].trim().substring(1, match[1].length - 1);
+ const q = match[2].trim();
const contents = P.alt(
r.big,
r.small,
+ r.spin,
r.bold,
r.strike,
r.italic,
r.motion,
r.url,
r.link,
+ r.flip,
r.mention,
r.hashtag,
r.emoji,
diff --git a/src/mfm/syntax-highlight.ts b/src/mfm/syntax-highlight.ts
deleted file mode 100644
index 109923fb7..000000000
--- a/src/mfm/syntax-highlight.ts
+++ /dev/null
@@ -1,343 +0,0 @@
-import { capitalize, toUpperCase } from '../prelude/string';
-
-function escape(text: string) {
- return text
- .replace(/>/g, '>')
- .replace(/ b.length - a.length);
-
-const symbols = [
- '=',
- '+',
- '-',
- '*',
- '/',
- '%',
- '~',
- '^',
- '&',
- '|',
- '>',
- '<',
- '!',
- '?'
-];
-
-type Token = {
- html: string
- next: number
-};
-
-type Element = (code: string, i: number, source: string) => (Token | null);
-
-const elements: Element[] = [
- // comment
- code => {
- if (code.substr(0, 2) != '//') return null;
- const match = code.match(/^\/\/(.+?)(\n|$)/);
- if (!match) return null;
- const comment = match[0];
- return {
- html: ``,
- next: comment.length
- };
- },
-
- // block comment
- code => {
- const match = code.match(/^\/\*([\s\S]+?)\*\//);
- if (!match) return null;
- return {
- html: ``,
- next: match[0].length
- };
- },
-
- // string
- code => {
- if (!/^['"`]/.test(code)) return null;
- const begin = code[0];
- let str = begin;
- let thisIsNotAString = false;
- for (let i = 1; i < code.length; i++) {
- const char = code[i];
- if (char == '\\') {
- str += char;
- str += code[i + 1] || '';
- i++;
- continue;
- } else if (char == begin) {
- str += char;
- break;
- } else if (char == '\n' || i == (code.length - 1)) {
- thisIsNotAString = true;
- break;
- } else {
- str += char;
- }
- }
- if (thisIsNotAString) {
- return null;
- } else {
- return {
- html: `${escape(str)}`,
- next: str.length
- };
- }
- },
-
- // regexp
- code => {
- if (code[0] != '/') return null;
- let regexp = '';
- let thisIsNotARegexp = false;
- for (let i = 1; i < code.length; i++) {
- const char = code[i];
- if (char == '\\') {
- regexp += char;
- regexp += code[i + 1] || '';
- i++;
- continue;
- } else if (char == '/') {
- break;
- } else if (char == '\n' || i == (code.length - 1)) {
- thisIsNotARegexp = true;
- break;
- } else {
- regexp += char;
- }
- }
-
- if (thisIsNotARegexp) return null;
- if (regexp == '') return null;
- if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null;
-
- return {
- html: `/${escape(regexp)}/`,
- next: regexp.length + 2
- };
- },
-
- // label
- code => {
- if (code[0] != '@') return null;
- const match = code.match(/^@([a-zA-Z_-]+?)\n/);
- if (!match) return null;
- const label = match[0];
- return {
- html: `${label}`,
- next: label.length
- };
- },
-
- // number
- (code, i, source) => {
- const prev = source[i - 1];
- if (prev && /[a-zA-Z]/.test(prev)) return null;
- if (!/^[\-\+]?[0-9\.]+/.test(code)) return null;
- const match = code.match(/^[\-\+]?[0-9\.]+/)[0];
- if (match) {
- return {
- html: `${match}`,
- next: match.length
- };
- } else {
- return null;
- }
- },
-
- // nan
- (code, i, source) => {
- const prev = source[i - 1];
- if (prev && /[a-zA-Z]/.test(prev)) return null;
- if (code.substr(0, 3) == 'NaN') {
- return {
- html: `NaN`,
- next: 3
- };
- } else {
- return null;
- }
- },
-
- // method
- code => {
- const match = code.match(/^([a-zA-Z_-]+?)\(/);
- if (!match) return null;
-
- if (match[1] == '-') return null;
-
- return {
- html: `${match[1]}`,
- next: match[1].length
- };
- },
-
- // property
- (code, i, source) => {
- const prev = source[i - 1];
- if (prev != '.') return null;
-
- const match = code.match(/^[a-zA-Z0-9_-]+/);
- if (!match) return null;
-
- return {
- html: `${match[0]}`,
- next: match[0].length
- };
- },
-
- // keyword
- (code, i, source) => {
- const prev = source[i - 1];
- if (prev && /[a-zA-Z]/.test(prev)) return null;
-
- const match = keywords.filter(k => code.substr(0, k.length) == k)[0];
- if (match) {
- if (/^[a-zA-Z]/.test(code.substr(match.length))) return null;
- return {
- html: `${match}`,
- next: match.length
- };
- } else {
- return null;
- }
- },
-
- // symbol
- code => {
- const match = symbols.filter(s => code[0] == s)[0];
- if (match) {
- return {
- html: `${match}`,
- next: 1
- };
- } else {
- return null;
- }
- }
-];
-
-// TODO: specify lang
-export default (source: string, lang?: string): string => {
- let code = source;
- let html = '';
-
- let i = 0;
-
- function push(token: Token) {
- html += token.html;
- code = code.substr(token.next);
- i += token.next;
- }
-
- while (code != '') {
- const parsed = elements.some(el => {
- const e = el(code, i, source);
- if (e) {
- push(e);
- return true;
- } else {
- return false;
- }
- });
-
- if (!parsed) {
- push({
- html: escape(code[0]),
- next: 1
- });
- }
- }
-
- return html;
-};
diff --git a/src/server/api/common/getters.ts b/src/server/api/common/getters.ts
index 1fce58b20..1cd054cab 100644
--- a/src/server/api/common/getters.ts
+++ b/src/server/api/common/getters.ts
@@ -1,5 +1,6 @@
import * as mongo from 'mongodb';
import Note from "../../../models/note";
+import User, { isRemoteUser, isLocalUser } from "../../../models/user";
/**
* Get valied note for API processing
@@ -16,3 +17,44 @@ export async function getValiedNote(noteId: mongo.ObjectID) {
return note;
}
+
+/**
+ * Get user for API processing
+ */
+export async function getUser(userId: mongo.ObjectID) {
+ const user = await User.findOne({
+ _id: userId
+ });
+
+ if (user == null) {
+ throw 'user not found';
+ }
+
+ return user;
+}
+
+/**
+ * Get remote user for API processing
+ */
+export async function getRemoteUser(userId: mongo.ObjectID) {
+ const user = await getUser(userId);
+
+ if (!isRemoteUser(user)) {
+ throw 'user is not a remote user';
+ }
+
+ return user;
+}
+
+/**
+ * Get local user for API processing
+ */
+export async function getLocalUser(userId: mongo.ObjectID) {
+ const user = await getUser(userId);
+
+ if (!isLocalUser(user)) {
+ throw 'user is not a local user';
+ }
+
+ return user;
+}
diff --git a/src/server/api/endpoints/admin/update-remote-user.ts b/src/server/api/endpoints/admin/update-remote-user.ts
new file mode 100644
index 000000000..9288ce1fb
--- /dev/null
+++ b/src/server/api/endpoints/admin/update-remote-user.ts
@@ -0,0 +1,36 @@
+import * as mongo from 'mongodb';
+import $ from 'cafy';
+import ID, { transform } from '../../../../misc/cafy-id';
+import define from '../../define';
+import { getRemoteUser } from '../../common/getters';
+import { updatePerson } from '../../../../remote/activitypub/models/person';
+
+export const meta = {
+ desc: {
+ 'ja-JP': '指定されたリモートユーザーの情報を更新します。',
+ 'en-US': 'Update specified remote user information.'
+ },
+
+ requireCredential: true,
+ requireModerator: true,
+
+ params: {
+ userId: {
+ validator: $.type(ID),
+ transform: transform,
+ desc: {
+ 'ja-JP': '対象のユーザーID',
+ 'en-US': 'The user ID which you want to update'
+ }
+ },
+ }
+};
+
+export default define(meta, (ps) => new Promise((res, rej) => {
+ updatePersonById(ps.userId).then(() => res(), e => rej(e));
+}));
+
+async function updatePersonById(userId: mongo.ObjectID) {
+ const user = await getRemoteUser(userId);
+ await updatePerson(user.uri);
+}
diff --git a/src/server/api/endpoints/users/report-abuse.ts b/src/server/api/endpoints/users/report-abuse.ts
index b520b29e2..19beee433 100644
--- a/src/server/api/endpoints/users/report-abuse.ts
+++ b/src/server/api/endpoints/users/report-abuse.ts
@@ -2,6 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import User from '../../../../models/user';
import AbuseUserReport from '../../../../models/abuse-user-report';
+import { publishAdminStream } from '../../../../stream';
export const meta = {
desc: {
@@ -47,12 +48,31 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
return rej('cannot report admin');
}
- await AbuseUserReport.insert({
+ const report = await AbuseUserReport.insert({
createdAt: new Date(),
userId: user._id,
reporterId: me._id,
comment: ps.comment
});
+ // Publish event to moderators
+ setTimeout(async () => {
+ const moderators = await User.find({
+ $or: [{
+ isAdmin: true
+ }, {
+ isModerator: true
+ }]
+ });
+ for (const moderator of moderators) {
+ publishAdminStream(moderator._id, 'newAbuseUserReport', {
+ id: report._id,
+ userId: report.userId,
+ reporterId: report.reporterId,
+ comment: report.comment
+ });
+ }
+ }, 1);
+
res();
}));
diff --git a/src/server/api/stream/channels/admin.ts b/src/server/api/stream/channels/admin.ts
new file mode 100644
index 000000000..6bcd1a7e0
--- /dev/null
+++ b/src/server/api/stream/channels/admin.ts
@@ -0,0 +1,16 @@
+import autobind from 'autobind-decorator';
+import Channel from '../channel';
+
+export default class extends Channel {
+ public readonly chName = 'admin';
+ public static shouldShare = true;
+ public static requireCredential = true;
+
+ @autobind
+ public async init(params: any) {
+ // Subscribe admin stream
+ this.subscriber.on(`adminStream:${this.user._id}`, data => {
+ this.send(data);
+ });
+ }
+}
diff --git a/src/server/api/stream/channels/index.ts b/src/server/api/stream/channels/index.ts
index 7248579ab..02f71b585 100644
--- a/src/server/api/stream/channels/index.ts
+++ b/src/server/api/stream/channels/index.ts
@@ -11,6 +11,7 @@ import messagingIndex from './messaging-index';
import drive from './drive';
import hashtag from './hashtag';
import apLog from './ap-log';
+import admin from './admin';
import gamesReversi from './games/reversi';
import gamesReversiGame from './games/reversi-game';
@@ -28,6 +29,7 @@ export default {
drive,
hashtag,
apLog,
+ admin,
gamesReversi,
gamesReversiGame
};
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index d3c8699b2..3b5aac8f8 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -377,8 +377,10 @@ async function publish(user: IUser, note: INote, noteObj: any, reply: INote, ren
if (note.visibility == 'specified') {
for (const u of visibleUsers) {
- publishHomeTimelineStream(u._id, detailPackedNote);
- publishHybridTimelineStream(u._id, detailPackedNote);
+ if (!u._id.equals(user._id)) {
+ publishHomeTimelineStream(u._id, detailPackedNote);
+ publishHybridTimelineStream(u._id, detailPackedNote);
+ }
}
}
} else {
diff --git a/src/services/note/delete.ts b/src/services/note/delete.ts
index 9709eeaf5..e8ce181d5 100644
--- a/src/services/note/delete.ts
+++ b/src/services/note/delete.ts
@@ -30,12 +30,25 @@ export default async function(user: IUser, note: INote) {
text: null,
tags: [],
fileIds: [],
+ renoteId: null,
poll: null,
geo: null,
cw: null
}
});
+ if (note.renoteId) {
+ Note.update({ _id: note.renoteId }, {
+ $inc: {
+ renoteCount: -1,
+ score: -1
+ },
+ $pull: {
+ _quoteIds: note._id
+ }
+ });
+ }
+
publishNoteStream(note._id, 'deleted', {
deletedAt: deletedAt
});
diff --git a/src/stream.ts b/src/stream.ts
index 596cb98e7..098d49ecd 100644
--- a/src/stream.ts
+++ b/src/stream.ts
@@ -87,6 +87,10 @@ class Publisher {
public publishApLogStream = (log: any): void => {
this.publish('apLog', null, log);
}
+
+ public publishAdminStream = (userId: ID, type: string, value?: any): void => {
+ this.publish(`adminStream:${userId}`, type, typeof value === 'undefined' ? null : value);
+ }
}
const publisher = new Publisher();
@@ -107,3 +111,4 @@ export const publishHybridTimelineStream = publisher.publishHybridTimelineStream
export const publishGlobalTimelineStream = publisher.publishGlobalTimelineStream;
export const publishHashtagStream = publisher.publishHashtagStream;
export const publishApLogStream = publisher.publishApLogStream;
+export const publishAdminStream = publisher.publishAdminStream;
diff --git a/src/tools/resync-remote-user.ts b/src/tools/resync-remote-user.ts
index c013de723..4850c768a 100644
--- a/src/tools/resync-remote-user.ts
+++ b/src/tools/resync-remote-user.ts
@@ -24,9 +24,7 @@ if (!acct.match(/^\w+@\w/)) {
console.log(`resync ${acct}`);
main(acct).then(() => {
- console.log('success');
- process.exit(0);
+ console.log('Done');
}).catch(e => {
console.warn(e);
- process.exit(1);
});
diff --git a/test/mfm.ts b/test/mfm.ts
index f850e649a..7070329f3 100644
--- a/test/mfm.ts
+++ b/test/mfm.ts
@@ -152,9 +152,19 @@ describe('MFM', () => {
it('can be analyzed', () => {
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
assert.deepStrictEqual(tokens, [
- leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }),
+ leaf('mention', {
+ acct: '@himawari',
+ canonical: '@himawari',
+ username: 'himawari',
+ host: null
+ }),
text(' '),
- leaf('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }),
+ leaf('mention', {
+ acct: '@hima_sub@namori.net',
+ canonical: '@hima_sub@namori.net',
+ username: 'hima_sub',
+ host: 'namori.net'
+ }),
text(' お腹ペコい '),
leaf('emoji', { name: 'cat' }),
text(' '),
@@ -234,6 +244,24 @@ describe('MFM', () => {
]);
});
+ it('flip', () => {
+ const tokens = analyze('foo');
+ assert.deepStrictEqual(tokens, [
+ tree('flip', [
+ text('foo')
+ ], {}),
+ ]);
+ });
+
+ it('spin', () => {
+ const tokens = analyze(':foo:');
+ assert.deepStrictEqual(tokens, [
+ tree('spin', [
+ leaf('emoji', { name: 'foo' })
+ ], {}),
+ ]);
+ });
+
describe('motion', () => {
it('by triple brackets', () => {
const tokens = analyze('(((foo)))');
@@ -280,7 +308,12 @@ describe('MFM', () => {
it('local', () => {
const tokens = analyze('@himawari foo');
assert.deepStrictEqual(tokens, [
- leaf('mention', { acct: '@himawari', canonical: '@himawari', username: 'himawari', host: null }),
+ leaf('mention', {
+ acct: '@himawari',
+ canonical: '@himawari',
+ username: 'himawari',
+ host: null
+ }),
text(' foo')
]);
});
@@ -288,7 +321,12 @@ describe('MFM', () => {
it('remote', () => {
const tokens = analyze('@hima_sub@namori.net foo');
assert.deepStrictEqual(tokens, [
- leaf('mention', { acct: '@hima_sub@namori.net', canonical: '@hima_sub@namori.net', username: 'hima_sub', host: 'namori.net' }),
+ leaf('mention', {
+ acct: '@hima_sub@namori.net',
+ canonical: '@hima_sub@namori.net',
+ username: 'hima_sub',
+ host: 'namori.net'
+ }),
text(' foo')
]);
});
@@ -296,7 +334,12 @@ describe('MFM', () => {
it('remote punycode', () => {
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo');
assert.deepStrictEqual(tokens, [
- leaf('mention', { acct: '@hima_sub@xn--q9j5bya.xn--zckzah', canonical: '@hima_sub@なもり.テスト', username: 'hima_sub', host: 'xn--q9j5bya.xn--zckzah' }),
+ leaf('mention', {
+ acct: '@hima_sub@xn--q9j5bya.xn--zckzah',
+ canonical: '@hima_sub@なもり.テスト',
+ username: 'hima_sub',
+ host: 'xn--q9j5bya.xn--zckzah'
+ }),
text(' foo')
]);
});
@@ -309,11 +352,26 @@ describe('MFM', () => {
const tokens2 = analyze('@a\n@b\n@c');
assert.deepStrictEqual(tokens2, [
- leaf('mention', { acct: '@a', canonical: '@a', username: 'a', host: null }),
+ leaf('mention', {
+ acct: '@a',
+ canonical: '@a',
+ username: 'a',
+ host: null
+ }),
text('\n'),
- leaf('mention', { acct: '@b', canonical: '@b', username: 'b', host: null }),
+ leaf('mention', {
+ acct: '@b',
+ canonical: '@b',
+ username: 'b',
+ host: null
+ }),
text('\n'),
- leaf('mention', { acct: '@c', canonical: '@c', username: 'c', host: null })
+ leaf('mention', {
+ acct: '@c',
+ canonical: '@c',
+ username: 'c',
+ host: null
+ })
]);
const tokens3 = analyze('**x**@a');
@@ -321,24 +379,31 @@ describe('MFM', () => {
tree('bold', [
text('x')
], {}),
- leaf('mention', { acct: '@a', canonical: '@a', username: 'a', host: null })
+ leaf('mention', {
+ acct: '@a',
+ canonical: '@a',
+ username: 'a',
+ host: null
+ })
]);
- const tokens4 = analyze('@\n@v\n@veryverylongusername' /* \n@toolongtobeasamention */);
+ const tokens4 = analyze('@\n@v\n@veryverylongusername');
assert.deepStrictEqual(tokens4, [
text('@\n'),
- leaf('mention', { acct: '@v', canonical: '@v', username: 'v', host: null }),
+ leaf('mention', {
+ acct: '@v',
+ canonical: '@v',
+ username: 'v',
+ host: null
+ }),
text('\n'),
- leaf('mention', { acct: '@veryverylongusername', canonical: '@veryverylongusername', username: 'veryverylongusername', host: null }),
- // text('\n@toolongtobeasamention')
+ leaf('mention', {
+ acct: '@veryverylongusername',
+ canonical: '@veryverylongusername',
+ username: 'veryverylongusername',
+ host: null
+ }),
]);
- /*
- const tokens5 = analyze('@domain_is@valid.example.com\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com');
- assert.deepStrictEqual([
- leaf('mention', { acct: '@domain_is@valid.example.com', canonical: '@domain_is@valid.example.com', username: 'domain_is', host: 'valid.example.com' }),
- text('\n@domain_is@.invalid\n@domain_is@invali.d\n@domain_is@invali.d\n@domain_is@-invalid.com\n@domain_is@invalid-.com')
- ], tokens5);
- */
});
});
@@ -905,6 +970,20 @@ describe('MFM', () => {
text('after')
]);
});
+
+ it('ignore multiple title blocks', () => {
+ const tokens = analyze('【foo】bar【baz】');
+ assert.deepStrictEqual(tokens, [
+ text('【foo】bar【baz】')
+ ]);
+ });
+
+ it('disallow linebreak in title', () => {
+ const tokens = analyze('【foo\nbar】');
+ assert.deepStrictEqual(tokens, [
+ text('【foo\nbar】')
+ ]);
+ });
});
describe('center', () => {