Compare commits

..

49 Commits

Author SHA1 Message Date
syuilo
fdcc994291 10.80.0 2019-01-31 12:27:44 +09:00
syuilo
f54363076c Update CHANGELOG.md 2019-01-31 12:26:56 +09:00
syuilo
ec016e5a95 🎨 2019-01-31 12:24:21 +09:00
syuilo
bbdb2496a4 [Client] MFMの制限を緩和 2019-01-31 12:24:14 +09:00
syuilo
b515cc90e9 [MFM] Better syntax parsing
Allow nesting by same tag
2019-01-31 12:23:45 +09:00
syuilo
bb92158dff [MFM] Make some syntax block
Resolve #3508
2019-01-31 12:10:48 +09:00
Aya Morisawa
c652add16a Simplify MFM (#4046) 2019-01-31 12:06:13 +09:00
MeiMei
b8a7468c4a Do not import as pack from AP renderer (#4048)
* Do not import as pack from AP renderer

* rename
2019-01-31 02:29:35 +09:00
Acid Chicken (硫酸鶏)
e220ef3e75 Re-fix path
refs: 4bb4903ee5, 7e3a8d56e6
2019-01-31 01:38:52 +09:00
Acid Chicken (硫酸鶏)
4bb4903ee5 Fix path
refs: 7e3a8d56e6
2019-01-31 01:26:11 +09:00
Acid Chicken (硫酸鶏)
9487840ae3 Create type definition for 'is-root' (#4001)
* Update @types/sharp requirement from 0.21.0 to 0.21.1

Updates the requirements on [@types/sharp](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Add multiline math syntax

Co-authored-by: syuilo <syuilotan@yahoo.co.jp>

* New translations ja-JP.yml (Catalan)

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (English)

* New translations ja-JP.yml (French)

* New translations ja-JP.yml (German)

* New translations ja-JP.yml (Italian)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Polish)

* New translations ja-JP.yml (Portuguese)

* New translations ja-JP.yml (Russian)

* New translations ja-JP.yml (Spanish)

* New translations ja-JP.yml (Japanese, Kansai)

* New translations ja-JP.yml (Dutch)

* New translations ja-JP.yml (Norwegian)

* New translations ja-JP.yml (English)

* Create type definition for 'is-root'

* [MFM] Add spin syntax

Resolve #4003

* [MFM] Add flip syntax

Resolve #4002

* Fix test

* Update CHANGELOG.md

* 10.79.0

* Update @fortawesome/free-regular-svg-icons requirement (#3963)

Updates the requirements on [@fortawesome/free-regular-svg-icons](https://github.com/FortAwesome/Font-Awesome) to permit the latest version.
- [Release notes](https://github.com/FortAwesome/Font-Awesome/releases)
- [Changelog](https://github.com/FortAwesome/Font-Awesome/blob/master/CHANGELOG.md)
- [Commits](https://github.com/FortAwesome/Font-Awesome/commits/5.6.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Update @types/webpack requirement from 4.4.21 to 4.4.24 (#3976)

Updates the requirements on [@types/webpack](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Update @types/js-yaml requirement from 3.11.4 to 3.12.0 (#3977)

Updates the requirements on [@types/js-yaml](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Update debug requirement from 4.1.0 to 4.1.1 (#3964)

Updates the requirements on [debug](https://github.com/visionmedia/debug) to permit the latest version.
- [Release notes](https://github.com/visionmedia/debug/releases)
- [Commits](https://github.com/visionmedia/debug/commits/4.1.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (Korean)

* New translations ja-JP.yml (French)

* [MFM] spinの中でflipを使えるように

* Add jump syntax (#4007)

* Add jump syntax

* Fix typo: spin -> jump

* Fix typo

* [MFM] Resolve #4009

* Module 'nprogress' as import syntax (#4012)

* 🎨

* [Client] Fix #4008

* Use yarn instead of npm on CircleCI

* touch yarn.lock

* [Client] Resolve #3638

* 10.79.1

* New translations ja-JP.yml (Korean)

* Add missing semicolon

* Remove file-loader from dependencies (#4025)

* Update README.md [AUTOGEN] (#4028)

* Update README.md [AUTOGEN] (#4030)

* Add visibility test (#4029)

* Update ws requirement from 6.1.2 to 6.1.3 (#4027)

Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits/6.1.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Module 'web-push' as import syntax (#4017)

* Fix visibility test (#4031)

* Upgrade gulp version to 4.0.0

* Prevent typescript errors from crashing

* Remove duplicated dependencies

* Use parallel and task to specify dependencies

* Sort tasks by topological ordering

* リプライ/メンションされていれば非フォロワーへのフォロワー限定でも参照可能に (#4033)

* 非メンション/リプライ投稿がmentionsにあるかどうかはvisibilityと関係ないので削除

* リプライ/メンションされていれば非フォロワーでも参照可能に

* New translations ja-JP.yml (Chinese Simplified)

* New translations ja-JP.yml (Chinese Simplified)

* Fix #4034 (#4037)

* Fix #4034

* improve

* Module 'crypto' as import syntax (#4011)

* Extract MFM normalize function

* Extract MFM types

* Rename html to toHtml

* Rename html-to-mfm to fromHtml

* Merge plainParser into mfm

* Extract parsePlain function

* Rename analyze to parse in MFM tests

* Update @types/mongodb requirement from 3.1.18 to 3.1.19 (#4041)

Updates the requirements on [@types/mongodb](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Update vue-svg-inline-loader requirement from 1.2.7 to 1.2.10 (#4040)

Updates the requirements on [vue-svg-inline-loader](https://github.com/oliverfindl/vue-svg-inline-loader) to permit the latest version.
- [Release notes](https://github.com/oliverfindl/vue-svg-inline-loader/releases)
- [Commits](https://github.com/oliverfindl/vue-svg-inline-loader/commits/v1.2.10)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* Avoid export default

* Rename parser to language

* Fix import

* Introduce silence (#4043)

* Introduce silence

* Fix icon

* Update is-root.d.ts

* Update index.ts

* Create type definition for 'is-root'

* Update is-root.d.ts

* Update index.ts
2019-01-31 01:09:52 +09:00
Acid Chicken (硫酸鶏)
7e3a8d56e6 Update index.ts 2019-01-31 01:09:36 +09:00
Acid Chicken (硫酸鶏)
e909eac296 Create type definition for '*/package.json' (#4014)
* Create type definition for '*/package.json'

* Update tsconfig.json
2019-01-31 01:08:43 +09:00
syuilo
8dc7f28744 [ActivityPub] Use microformats on mentions
To avoid pointless link previews.
see: https://misskey.xyz/notes/5c51ab5c2d85f2003248eddc
2019-01-30 22:57:32 +09:00
syuilo
a4b1e8ca26 Update CHANGELOG.md 2019-01-30 22:44:36 +09:00
MeiMei
79b0cc6785 delete unnecessary key (#4045)
* delete unnecessary key

* Add note
2019-01-30 21:31:45 +09:00
syuilo
00b134ce1e Introduce silence (#4043)
* Introduce silence

* Fix icon
2019-01-30 17:25:56 +09:00
Aya Morisawa
b3fc4dc00f Fix import 2019-01-30 17:15:12 +09:00
Aya Morisawa
d06fbbe3ea Rename parser to language 2019-01-30 17:04:49 +09:00
Aya Morisawa
28bfb45426 Avoid export default 2019-01-30 16:56:27 +09:00
dependabot[bot]
1c60a49c96 Update vue-svg-inline-loader requirement from 1.2.7 to 1.2.10 (#4040)
Updates the requirements on [vue-svg-inline-loader](https://github.com/oliverfindl/vue-svg-inline-loader) to permit the latest version.
- [Release notes](https://github.com/oliverfindl/vue-svg-inline-loader/releases)
- [Commits](https://github.com/oliverfindl/vue-svg-inline-loader/commits/v1.2.10)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-01-30 16:06:45 +09:00
dependabot[bot]
3ae8ff083b Update @types/mongodb requirement from 3.1.18 to 3.1.19 (#4041)
Updates the requirements on [@types/mongodb](https://github.com/DefinitelyTyped/DefinitelyTyped) to permit the latest version.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-01-30 16:06:28 +09:00
Aya Morisawa
c12ccb2a15 Rename analyze to parse in MFM tests 2019-01-30 15:30:05 +09:00
Aya Morisawa
e3b1d00e4c Extract parsePlain function 2019-01-30 15:27:54 +09:00
Aya Morisawa
98795aad9a Merge plainParser into mfm 2019-01-30 15:12:48 +09:00
Aya Morisawa
ca26edbfce Rename html-to-mfm to fromHtml 2019-01-30 15:00:05 +09:00
Aya Morisawa
3058e8f354 Rename html to toHtml 2019-01-30 14:57:13 +09:00
Aya Morisawa
4c9b66b0f0 Extract MFM types 2019-01-30 14:51:30 +09:00
Aya Morisawa
6eb9ba31bf Extract MFM normalize function 2019-01-30 14:21:36 +09:00
Acid Chicken (硫酸鶏)
5bbf4187e6 Module 'crypto' as import syntax (#4011) 2019-01-30 11:51:29 +09:00
syuilo
f2425f71c2 Merge pull request #4020 from syuilo/l10n_develop
New Crowdin translations
2019-01-30 11:50:55 +09:00
MeiMei
b0e00da2f7 Fix #4034 (#4037)
* Fix #4034

* improve
2019-01-29 20:33:28 +09:00
syuilo
215472cd17 New translations ja-JP.yml (Chinese Simplified) 2019-01-29 18:33:13 +09:00
syuilo
072fd4455e New translations ja-JP.yml (Chinese Simplified) 2019-01-29 18:22:34 +09:00
MeiMei
2ed9e26a4f リプライ/メンションされていれば非フォロワーへのフォロワー限定でも参照可能に (#4033)
* 非メンション/リプライ投稿がmentionsにあるかどうかはvisibilityと関係ないので削除

* リプライ/メンションされていれば非フォロワーでも参照可能に
2019-01-29 17:34:43 +09:00
Aya Morisawa
8a3e26cdb8 Sort tasks by topological ordering 2019-01-29 17:10:16 +09:00
Aya Morisawa
7301671962 Use parallel and task to specify dependencies 2019-01-29 17:10:16 +09:00
Aya Morisawa
0d0f25818e Remove duplicated dependencies 2019-01-29 17:10:16 +09:00
Aya Morisawa
7850d68dc2 Prevent typescript errors from crashing 2019-01-29 17:10:16 +09:00
Aya Morisawa
f0f5b32300 Upgrade gulp version to 4.0.0 2019-01-29 17:10:16 +09:00
MeiMei
1ca0976e85 Fix visibility test (#4031) 2019-01-29 15:16:11 +09:00
Acid Chicken (硫酸鶏)
7fbfd17896 Module 'web-push' as import syntax (#4017) 2019-01-29 14:36:24 +09:00
dependabot[bot]
3d04f7db62 Update ws requirement from 6.1.2 to 6.1.3 (#4027)
Updates the requirements on [ws](https://github.com/websockets/ws) to permit the latest version.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/commits/6.1.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-01-29 14:35:34 +09:00
MeiMei
e301630c9d Add visibility test (#4029) 2019-01-29 13:46:24 +09:00
Acid Chicken (硫酸鶏)
afbccaae41 Update README.md [AUTOGEN] (#4030) 2019-01-29 13:42:28 +09:00
Acid Chicken (硫酸鶏)
893c01c207 Update README.md [AUTOGEN] (#4028) 2019-01-29 05:55:21 +09:00
Aya Morisawa
41c80097ce Remove file-loader from dependencies (#4025) 2019-01-28 18:04:30 +09:00
Aya Morisawa
250933fff3 Add missing semicolon 2019-01-28 17:49:20 +09:00
syuilo
bc9454f67c New translations ja-JP.yml (Korean) 2019-01-28 00:51:55 +09:00
72 changed files with 1304 additions and 718 deletions

View File

@@ -1,6 +1,14 @@
ChangeLog
=========
10.80.0
----------
* サイレンス機能の追加
* リプライ/メンションされていれば非フォロワーへのフォロワー限定でも参照可能に
* MFMの解析を強化
* Misskey以外のインスタンスからMisskeyの投稿を見たときに改行が多い問題を修正
* Misskey以外のインスタンスからMisskeyの投稿を見たときにメンションのURLが展開されるのを修正
10.79.1
----------
* jump構文の追加

View File

@@ -93,6 +93,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c8.patreon.com/2/200/12059069" alt="naga_rus" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12731202/0995c46cdcb54153ab5f073f5869b70a/1?token-time=2145916800&token-hash=prtYqPOiSHBulhM7NU0VzMaWx39-9ntdq25b6kafDNA%3D" alt="negao" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12913507/f7181eacafe8469a93033d85f5969c29/3?token-time=2145916800&token-hash=c8HeVqLtmdgH-gSBJg8i10gmOcwllM87MDHeznl3el0%3D" alt="Melilot" width="100"></td>
<td><img src="https://c8.patreon.com/2/200/16869916" alt="見当かなみ" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12999811/5f349fafcce44dd1824a8b1ebbec4564/3?token-time=2145916800&token-hash=LtV2lRi3L2jOWMLwccr9qWYfPrFlzIo2jYZHKzHEb6k%3D" alt="Xeltica" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/3384329/8b713330cb27404ea6e9fac50ff96efe/1?token-time=2145916800&token-hash=Ch3iF81ZGP0LMo894Y9ajpLisgtE91SnxtZE7fxsgrM%3D" alt="べすれい" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/12021162/963128bb8d14476dbd8407943db8f31a/1?token-time=2145916800&token-hash=1FlxS9MEgmNGH_RHUVHbO5hIXB5I1z0lvA33CTvYvjA%3D" alt="gutfuckllc" width="100"></td>
@@ -101,6 +102,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=12059069">naga_rus</a></td>
<td><a href="https://www.patreon.com/negao">negao</a></td>
<td><a href="https://www.patreon.com/user?u=12913507">Melilot</a></td>
<td><a href="https://www.patreon.com/user?u=16869916">見当かなみ</a></td>
<td><a href="https://www.patreon.com/Xeltica">Xeltica</a></td>
<td><a href="https://www.patreon.com/user?u=3384329">べすれい</a></td>
<td><a href="https://www.patreon.com/gutfuckllc">gutfuckllc</a></td>
@@ -114,7 +116,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/8241184/39e18850e87a449e9c9a71acb3310ebd/3?token-time=2145916800&token-hash=gMq30aylxu5v3G8pRhWR5jeRBbYWEoRKjGbNeiCQz5g%3D" alt="Acid Chicken" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4389829/9f709180ac714651a70f74a82f3ffdb9/2?token-time=2145916800&token-hash=zcwFxb2zopzWwksKVU1YpfAEjsl4yKT02aQ6yiAFRiQ%3D" alt="natalie" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/13034746/c711c7f58e204ecfbc2fd646bc8a4eee/1?token-time=2145916800&token-hash=5T8XcaAf9Zyzfg3QubR06s_kJZkArVEM2dwObrBVAU4%3D" alt="Hiratake" width="100"></td>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/10789744/97175095d8f04c0f86225ff47cb98d40/1?token-time=2145916800&token-hash=ubVARikVOg3v7NW6LDhtG-ClE1LTU3I2TJ3js2-5xDs%3D" alt="Naoki Hirayama" width="100"></td>
</tr><tr>
<td><a href="https://www.patreon.com/mydarkstar">mydarkstar</a></td>
<td><a href="https://www.patreon.com/user?u=12718187">Peter G.</a></td>
@@ -124,7 +125,6 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/acid_chicken">Acid Chicken</a></td>
<td><a href="https://www.patreon.com/user?u=4389829">natalie</a></td>
<td><a href="https://www.patreon.com/hiratake">Hiratake</a></td>
<td><a href="https://www.patreon.com/spinlock">Naoki Hirayama</a></td>
</tr></table>
<table><tr>
<td><img src="https://c10.patreonusercontent.com/3/eyJ3IjoyMDB9/patreon-media/p/user/4503830/ccf2cc867ea64de0b524bb2e24b9a1cb/1?token-time=2145916800&token-hash=Ksk_2l3gjPDbnzMUOCSW1E-hdPJsNs2tSR4_RAakRK8%3D" alt="dansup" width="100"></td>
@@ -138,7 +138,7 @@ Please see the [Contribution Guide](./CONTRIBUTING.md).
<td><a href="https://www.patreon.com/user?u=12531784">Takashi Shibuya</a></td>
</tr></table>
**Last updated:** Mon, 21 Jan 2019 06:45:06 UTC
**Last updated:** Tue, 29 Jan 2019 04:42:06 UTC
<!-- PATREON_END -->
:four_leaf_clover: Copyright

View File

@@ -32,14 +32,6 @@ if (isDebug) {
console.warn(chalk.yellow.bold(' built script will not be compressed.'));
}
gulp.task('build', [
'build:ts',
'build:copy',
'build:client',
'locales',
'doc'
]);
gulp.task('build:ts', () => {
const tsProject = ts.createProject('./tsconfig.json');
@@ -47,6 +39,7 @@ gulp.task('build:ts', () => {
.src()
.pipe(sourcemaps.init())
.pipe(tsProject())
.on('error', () => {})
.pipe(sourcemaps.write('.', { includeContent: false, sourceRoot: '../built' }))
.pipe(gulp.dest('./built/'));
});
@@ -55,7 +48,7 @@ gulp.task('build:copy:views', () =>
gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
);
gulp.task('build:copy', ['build:copy:views'], () =>
gulp.task('build:copy', gulp.parallel('build:copy:views', () =>
gulp.src([
'./build/Release/crypto_key.node',
'./src/const.json',
@@ -63,9 +56,7 @@ gulp.task('build:copy', ['build:copy:views'], () =>
'./src/**/assets/**/*',
'!./src/client/app/**/assets/**/*'
]).pipe(gulp.dest('./built/'))
);
gulp.task('test', ['mocha']);
));
gulp.task('lint', () =>
gulp.src('./src/**/*.ts')
@@ -92,22 +83,15 @@ gulp.task('mocha', () =>
} as any))
);
gulp.task('test', gulp.task('mocha'));
gulp.task('clean', cb =>
rimraf('./built', cb)
);
gulp.task('cleanall', ['clean'], cb =>
gulp.task('cleanall', gulp.parallel('clean', cb =>
rimraf('./node_modules', cb)
);
gulp.task('default', ['build']);
gulp.task('build:client', [
'build:ts',
'build:client:script',
'build:client:styles',
'copy:client'
]);
));
gulp.task('build:client:script', () => {
const client = require('./built/client/meta.json');
@@ -129,9 +113,7 @@ gulp.task('build:client:styles', () =>
.pipe(gulp.dest('./built/client/assets/'))
);
gulp.task('copy:client', [
'build:client:script'
], () =>
gulp.task('copy:client', () =>
gulp.src([
'./assets/**/*',
'./src/client/assets/**/*',
@@ -156,3 +138,19 @@ gulp.task('doc', () =>
.pipe((cssnano as any)())
.pipe(gulp.dest('./built/docs/assets/'))
);
gulp.task('build:client', gulp.parallel(
'build:client:script',
'build:client:styles',
'copy:client'
));
gulp.task('build', gulp.parallel(
'build:ts',
'build:copy',
'build:client',
'locales',
'doc'
));
gulp.task('default', gulp.task('build'));

View File

@@ -1274,6 +1274,8 @@ admin/views/users.vue:
unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました"
make-silence: "サイレンス"
unmake-silence: "サイレンスの解除"
verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました"

View File

@@ -593,7 +593,7 @@ desktop/views/components/calendar.vue:
title: "{year}년 {month}월"
prev: "이전 달"
next: "다음 달"
go: "클릭여 시간 회귀"
go: "클릭여 시간역행"
desktop/views/components/choose-file-from-drive-window.vue:
chosen-files: "{count} 파일 선택중"
upload: "PC에서 드라이브에 파일을 업로드"

View File

@@ -103,14 +103,14 @@ common:
f: "等待你的书写..."
search: "搜索"
delete: "删除"
loading: "正在加载, 等着就好啦"
loading: "正在加载"
ok: "没问题"
update-available-title: "有可用更新"
update-available: "新的 Misskey 版本现已发布({newer}。目前版本{current}). 刷新页面以应用更新。"
my-token-regenerated: "您的 Token 已被重置, 您将自动登出。"
i-like-sushi: "相比于布丁来说, 我更喜欢寿司。"
show-reversi-board-labels: "在 Reversi 中显示行和列表签"
use-avatar-reversi-stones: "用头像作为 Reversi 中的 “石头”"
use-avatar-reversi-stones: "用头像作为黑白棋的棋子"
verified-user: "认证用户"
disable-animated-mfm: "在帖子中禁用动画文本"
suggest-recent-hashtags: "在帖子表单上显示最近流行的主题标签"
@@ -873,8 +873,8 @@ desktop/views/components/settings.2fa.vue:
failed: "设置失败, 请确保您的密钥是正确的。"
info: "从下次登录Misskey时您的设备上显示的令牌以及密码也是必需的。"
common/views/components/media-image.vue:
sensitive: "閲覧注意"
click-to-show: "クリックして表示"
sensitive: "阅读注意"
click-to-show: "点击查看"
common/views/components/api-settings.vue:
intro: "要访问API请将此标记设置为请求参数的关键字“i”。"
caution: "请勿将此令牌输入任何应用,也不要将此令牌告诉其他人,否则您的账户可能会受到损害。"
@@ -1126,30 +1126,30 @@ admin/views/drive.vue:
deleted: "已删除"
mark-as-sensitive: "标记为“敏感”"
unmark-as-sensitive: "取消标记为“敏感”"
marked-as-sensitive: "标记为关注"
unmarked-as-sensitive: "解除关注标记"
marked-as-sensitive: "标记为“敏感”"
unmarked-as-sensitive: "取消标记为“敏感”"
admin/views/users.vue:
operation: "操作"
username-or-userid: "用户名或用户ID"
user-not-found: "用户不存在"
lookup: "订阅"
reset-password: "密码重置"
reset-password-confirm: "パスワードをリセットしますか"
reset-password-confirm: "是否重置密码"
password-updated: "密码为「{password}」"
suspend: "被冻结"
suspend-confirm: "凍結しますか"
suspend-confirm: "是否冻结"
suspended: "成功冻结用户"
unsuspend: "已解除冻结"
unsuspend-confirm: "凍結を解除しますか"
unsuspend-confirm: "是否解除冻结"
unsuspended: "已成功解除用户冻结"
verify: "认证用户"
verify-confirm: "公式アカウントにしますか"
verify-confirm: "是否官方账号"
verified: "此账户已被认证"
unverify: "解除账户认证"
unverify-confirm: "公式アカウントを解除しますか"
unverify-confirm: "是否解除官方账号认证"
unverified: "该帐户未经认证"
update-remote-user: "リモートユーザー情報の更新"
remote-user-updated: "リモートユーザー情報を更新しました"
update-remote-user: "更新远程用户信息"
remote-user-updated: "远程用户信息已更新"
users:
title: "用户"
sort:

View File

@@ -1,8 +1,8 @@
{
"name": "misskey",
"author": "syuilo <i@syuilo.com>",
"version": "10.79.1",
"clientVersion": "2.0.13871",
"version": "10.80.0",
"clientVersion": "2.0.13920",
"codename": "nighthike",
"main": "./built/index.js",
"private": true,
@@ -36,7 +36,7 @@
"@types/double-ended-queue": "2.1.0",
"@types/elasticsearch": "5.0.30",
"@types/file-type": "10.6.0",
"@types/gulp": "3.8.36",
"@types/gulp": "4.0.5",
"@types/gulp-mocha": "0.0.32",
"@types/gulp-rename": "0.0.33",
"@types/gulp-replace": "0.0.31",
@@ -61,7 +61,7 @@
"@types/minio": "7.0.1",
"@types/mkdirp": "0.5.2",
"@types/mocha": "5.2.5",
"@types/mongodb": "3.1.18",
"@types/mongodb": "3.1.19",
"@types/ms": "0.7.30",
"@types/node": "10.12.18",
"@types/nodemailer": "4.6.5",
@@ -84,6 +84,7 @@
"@types/tinycolor2": "1.4.1",
"@types/tmp": "0.0.33",
"@types/uuid": "3.4.4",
"@types/web-push": "3.3.0",
"@types/webpack": "4.4.24",
"@types/webpack-stream": "3.2.10",
"@types/websocket": "0.0.40",
@@ -117,10 +118,9 @@
"eslint-plugin-vue": "5.1.0",
"eventemitter3": "3.1.0",
"feed": "2.0.2",
"file-loader": "2.0.0",
"file-type": "10.7.0",
"fuckadblock": "3.2.1",
"gulp": "3.9.1",
"gulp": "4.0.0",
"gulp-cssnano": "2.1.3",
"gulp-imagemin": "4.1.0",
"gulp-mocha": "6.0.0",
@@ -236,7 +236,7 @@
"vue-router": "3.0.2",
"vue-sequential-entrance": "1.1.3",
"vue-style-loader": "4.1.2",
"vue-svg-inline-loader": "1.2.7",
"vue-svg-inline-loader": "1.2.10",
"vue-template-compiler": "2.5.17",
"vuedraggable": "2.17.0",
"vuewordcloud": "18.7.11",
@@ -247,7 +247,7 @@
"webpack": "4.28.4",
"webpack-cli": "3.2.1",
"websocket": "1.0.28",
"ws": "6.1.2",
"ws": "6.1.3",
"xev": "2.0.1"
}
}

5
src/@types/is-root.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module 'is-root';
declare namespace isRoot {
export function isRoot(): boolean;
}

3
src/@types/package.json.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
declare module '*/package.json' {
const version: string;
}

View File

@@ -12,6 +12,7 @@
<span class="is-admin" v-if="user.isAdmin">admin</span>
<span class="is-moderator" v-if="user.isModerator">moderator</span>
<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
<span class="is-silenced" v-if="user.isSilenced" :title="$t('@.silenced-user')"><fa :icon="faMicrophoneSlash"/></span>
<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
</header>
<div>
@@ -27,6 +28,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
@@ -34,7 +36,7 @@ export default Vue.extend({
props: ['user'],
data() {
return {
faSnowflake
faSnowflake, faMicrophoneSlash
};
},
});
@@ -76,6 +78,7 @@ export default Vue.extend({
color var(--noteHeaderAdminFg)
> .is-verified
> .is-silenced
> .is-suspended
margin 0 0 0 .5em
color #4dabf7

View File

@@ -16,6 +16,10 @@
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
</ui-horizon-group>
<ui-horizon-group>
<ui-button @click="silenceUser"><fa :icon="faMicrophoneSlash"/> {{ $t('make-silence') }}</ui-button>
<ui-button @click="unsilenceUser">{{ $t('unmake-silence') }}</ui-button>
</ui-horizon-group>
<ui-horizon-group>
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
@@ -66,7 +70,7 @@
import Vue from 'vue';
import i18n from '../../i18n';
import parseAcct from "../../../../misc/acct/parse";
import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync } from '@fortawesome/free-solid-svg-icons';
import { faCertificate, faUsers, faTerminal, faSearch, faKey, faSync, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
import XUser from './users.user.vue';
@@ -90,7 +94,7 @@ export default Vue.extend({
offset: 0,
users: [],
existMore: false,
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey, faSync, faMicrophoneSlash
};
},
@@ -216,6 +220,44 @@ export default Vue.extend({
this.refreshUser();
},
async silenceUser() {
const process = async () => {
await this.$root.api('admin/silence-user', { userId: this.user._id });
this.$root.dialog({
type: 'success',
splash: true
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
this.refreshUser();
},
async unsilenceUser() {
const process = async () => {
await this.$root.api('admin/unsilence-user', { userId: this.user._id });
this.$root.dialog({
type: 'success',
splash: true
});
};
await process().catch(e => {
this.$root.dialog({
type: 'error',
text: e.toString()
});
});
this.refreshUser();
},
async suspendUser() {
if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;

View File

@@ -1,5 +1,5 @@
// スクリプトサイズがデカい
//const crypto = require('crypto');
//import * as crypto from 'crypto';
export default (data: ArrayBuffer) => {
//const buf = new Buffer(data);

View File

@@ -1,4 +1,4 @@
import parse from '../../../../mfm/parse';
import { parse } from '../../../../mfm/parse';
import { sum, unique } from '../../../../prelude/array';
import shouldMuteNote from './should-mute-note';
import MkNoteMenu from '../views/components/note-menu.vue';

View File

@@ -34,7 +34,7 @@
<script lang="ts">
import Vue from 'vue';
import i18n from '../../../i18n';
import parse from '../../../../../mfm/parse';
import { parse } from '../../../../../mfm/parse';
import { unique } from '../../../../../prelude/array';
export default Vue.extend({

View File

@@ -1,7 +1,7 @@
import Vue, { VNode } from 'vue';
import { length } from 'stringz';
import { MfmForest } from '../../../../../mfm/parser';
import parse from '../../../../../mfm/parse';
import { MfmForest } from '../../../../../mfm/types';
import { parse, parsePlain } from '../../../../../mfm/parse';
import MkUrl from './url.vue';
import MkMention from './mention.vue';
import { concat, sum } from '../../../../../prelude/array';
@@ -46,7 +46,7 @@ export default Vue.component('misskey-flavored-markdown', {
render(createElement) {
if (this.text == null || this.text == '') return;
const ast = parse(this.text, this.plainText);
const ast = (this.plainText ? parsePlain : parse)(this.text);
let bigCount = 0;
let motionCount = 0;
@@ -98,7 +98,11 @@ export default Vue.component('misskey-flavored-markdown', {
}
case 'small': {
return [createElement('small', genEl(token.children))];
return [createElement('small', {
attrs: {
style: 'opacity: 0.7;'
},
}, genEl(token.children))];
}
case 'center': {
@@ -126,7 +130,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'spin': {
motionCount++;
const isLong = sumTextsLength(token.children) > 5 || countNodesF(token.children) > 3;
const isLong = sumTextsLength(token.children) > 10 || countNodesF(token.children) > 5;
const isMany = motionCount > 5;
const direction =
token.node.props.attr == 'left' ? 'reverse' :
@@ -144,7 +148,7 @@ export default Vue.component('misskey-flavored-markdown', {
case 'jump': {
motionCount++;
const isLong = sumTextsLength(token.children) > 5 || countNodesF(token.children) > 3;
const isLong = sumTextsLength(token.children) > 30 || countNodesF(token.children) > 5;
const isMany = motionCount > 5;
return (createElement as any)('span', {
attrs: {

View File

@@ -68,7 +68,7 @@ import insertTextAtCursor from 'insert-text-at-cursor';
import * as XDraggable from 'vuedraggable';
import getFace from '../../../common/scripts/get-face';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../mfm/parse';
import { parse } from '../../../../../mfm/parse';
import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz';

View File

@@ -60,7 +60,7 @@ import insertTextAtCursor from 'insert-text-at-cursor';
import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getFace from '../../../common/scripts/get-face';
import parse from '../../../../../mfm/parse';
import { parse } from '../../../../../mfm/parse';
import { host } from '../../../config';
import { erase, unique } from '../../../../../prelude/array';
import { length } from 'stringz';

View File

@@ -41,7 +41,7 @@
<script lang="ts">
import Vue from 'vue';
import parse from '../../../../mfm/parse';
import { parse } from '../../../../mfm/parse';
import * as JSON5 from 'json5';
export default Vue.extend({

View File

@@ -7,7 +7,7 @@ import { URL } from 'url';
import * as yaml from 'js-yaml';
import { Source, Mixin } from './types';
import isUrl = require('is-url');
const pkg = require('../../package.json');
import * as pkg from '../../package.json';
/**
* Path of configuration directory

View File

@@ -11,7 +11,7 @@ import * as cluster from 'cluster';
import * as debug from 'debug';
import chalk from 'chalk';
import * as portscanner from 'portscanner';
import isRoot = require('is-root');
import * as isRoot from 'is-root';
import Xev from 'xev';
import * as program from 'commander';
import * as sysUtils from 'systeminformation';
@@ -23,6 +23,7 @@ import notesStats from './daemons/notes-stats';
import loadConfig from './config/load';
import { Config } from './config/types';
import { lessThan } from './prelude/array';
import * as pkg from '../package.json';
const clusterLog = debug('misskey:cluster');
const ev = new Xev();
@@ -31,8 +32,6 @@ if (process.env.NODE_ENV != 'production' && process.env.DEBUG == null) {
debug.enable('misskey');
}
const pkg = require('../package.json');
//#region Command line argument definitions
program
.version(pkg.version)

View File

@@ -1,7 +1,7 @@
const parse5 = require('parse5');
import { URL } from 'url';
export default function(html: string): string {
export function fromHtml(html: string): string {
if (html == null) return null;
const dom = parse5.parseFragment(html);

File diff suppressed because one or more lines are too long

31
src/mfm/normalize.ts Normal file
View File

@@ -0,0 +1,31 @@
import * as A from '../prelude/array';
import * as S from '../prelude/string';
import { MfmForest, MfmTree } from './types';
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));
}

View File

@@ -1,36 +1,19 @@
import parser, { plainParser, MfmForest, MfmTree } from './parser';
import * as A from '../prelude/array';
import * as S from '../prelude/string';
import { createTree, createLeaf } from '../prelude/tree';
import { mfmLanguage } from './language';
import { MfmForest } from './types';
import { normalize } from './normalize';
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 isEmptyTextTree(t: MfmTree): boolean {
return t.node.type == 'text' && t.node.props.text === '';
}
function removeEmptyTextNodes(ts: MfmForest): MfmForest {
return ts
.filter(t => !isEmptyTextTree(t))
.map(t => createTree(t.node, removeEmptyTextNodes(t.children)));
}
export default (source: string, plainText = false): MfmForest => {
export function parse(source: string): MfmForest {
if (source == null || source == '') {
return null;
}
const raw = plainText ? plainParser.root.tryParse(source) : parser.root.tryParse(source) as MfmForest;
return removeEmptyTextNodes(concatConsecutiveTextTrees(raw));
};
return normalize(mfmLanguage.root.tryParse(source));
}
export function parsePlain(source: string): MfmForest {
if (source == null || source == '') {
return null;
}
return normalize(mfmLanguage.plain.tryParse(source));
}

View File

@@ -3,9 +3,9 @@ const { JSDOM } = jsdom;
import config from '../config';
import { INote } from '../models/note';
import { intersperse } from '../prelude/array';
import { MfmForest, MfmTree } from './parser';
import { MfmForest, MfmTree } from './types';
export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) => {
export function toHtml(tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteUsers'] = []) {
if (tokens == null) {
return null;
}
@@ -137,6 +137,7 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
default:
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host);
a.href = remoteUserInfo ? remoteUserInfo.uri : `${config.url}/${acct}`;
a.className = 'mention';
break;
}
a.textContent = acct;
@@ -157,7 +158,7 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
text(token) {
const el = doc.createElement('span');
const nodes = (token.node.props.text as string).split('\n').map(x => doc.createTextNode(x));
const nodes = (token.node.props.text as string).split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
for (const x of intersperse('br', nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x);
@@ -184,4 +185,4 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
appendChildren(tokens, doc.body);
return `<p>${doc.body.innerHTML}</p>`;
};
}

37
src/mfm/types.ts Normal file
View File

@@ -0,0 +1,37 @@
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);
}

View File

@@ -1,4 +1,4 @@
import { EmojiNode, MfmForest } from '../mfm/parser';
import { EmojiNode, MfmForest } from '../mfm/types';
import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array';

View File

@@ -1,4 +1,4 @@
import { HashtagNode, MfmForest } from '../mfm/parser';
import { HashtagNode, MfmForest } from '../mfm/types';
import { preorderF } from '../prelude/tree';
import { unique } from '../prelude/array';

View File

@@ -1,6 +1,6 @@
// test is located in test/extract-mentions
import { MentionNode, MfmForest } from '../mfm/parser';
import { MentionNode, MfmForest } from '../mfm/types';
import { preorderF } from '../prelude/tree';
export default function(mfmForest: MfmForest): MentionNode['props'][] {

View File

@@ -140,6 +140,12 @@ export const hideNote = async (packedNote: any, meId: mongo.ObjectID) => {
hide = true;
} else if (meId.equals(packedNote.userId)) {
hide = false;
} else if (packedNote.reply && meId.equals(packedNote.reply.userId)) {
// 自分の投稿に対するリプライ
hide = false;
} else if (packedNote.mentions && packedNote.mentions.some((id: any) => meId.equals(id))) {
// 自分へのメンション
hide = false;
} else {
// フォロワーかどうか
const following = await Following.findOne({

View File

@@ -54,6 +54,11 @@ type IUserBase = {
*/
isSuspended: boolean;
/**
* サイレンスされているか否か
*/
isSilenced: boolean;
/**
* 鍵アカウントか否か
*/
@@ -306,6 +311,7 @@ export const pack = (
delete _user.password;
delete _user.token;
delete _user.twoFactorTempSecret;
delete _user.two_factor_temp_secret; // 後方互換性のため
delete _user.twoFactorSecret;
if (_user.twitter) {
delete _user.twitter.accessToken;

View File

@@ -1,4 +1,4 @@
const push = require('web-push');
import * as push from 'web-push';
import * as mongo from 'mongodb';
import Subscription from './models/sw-subscription';
import config from './config';

View File

@@ -1,6 +1,6 @@
import { INote } from '../../../models/note';
import toHtml from '../../../mfm/html';
import parse from '../../../mfm/parse';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
export default function(note: INote) {
let html = toHtml(parse(note.text), note.mentionedRemoteUsers);

View File

@@ -9,7 +9,7 @@ import { INote as INoteActivityStreamsObject, IObject } from '../type';
import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
import { IRemoteUser, IUser } from '../../../models/user';
import htmlToMFM from '../../../mfm/html-to-mfm';
import { fromHtml } from '../../../mfm/fromHtml';
import Emoji, { IEmoji } from '../../../models/emoji';
import { ITag } from './tag';
import { toUnicode } from 'punycode';
@@ -110,7 +110,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
const cw = note.summary === '' ? null : note.summary;
// テキストのパース
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
const text = note._misskey_content ? note._misskey_content : fromHtml(note.content);
// vote
if (reply && reply.poll && text != null) {

View File

@@ -9,7 +9,7 @@ import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, isCollection, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm';
import { fromHtml } from '../../../mfm/fromHtml';
import usersChart from '../../../chart/users';
import { URL } from 'url';
import { resolveNote, extractEmojis } from './note';
@@ -150,7 +150,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
bannerId: null,
createdAt: Date.parse(person.published) || null,
lastFetchedAt: new Date(),
description: htmlToMFM(person.summary),
description: fromHtml(person.summary),
followersCount,
followingCount,
notesCount,
@@ -340,7 +340,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
sharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
featured: person.featured,
emojis: emojiNames,
description: htmlToMFM(person.summary),
description: fromHtml(person.summary),
followersCount,
followingCount,
notesCount,
@@ -463,7 +463,7 @@ export function analyzeAttachments(attachments: ITag[]) {
else
fields.push({
name: attachment.name,
value: htmlToMFM(attachment.value)
value: fromHtml(attachment.value)
});
return { fields, services };

View File

@@ -1,7 +1,7 @@
import config from '../../../config';
import * as uuid from 'uuid';
export default (x: any) => {
export const renderActivity = (x: any) => {
if (x == null) return null;
if (x !== null && typeof x === 'object' && x.id == null) {

View File

@@ -2,8 +2,8 @@ import renderImage from './image';
import renderKey from './key';
import config from '../../../config';
import { ILocalUser } from '../../../models/user';
import toHtml from '../../../mfm/html';
import parse from '../../../mfm/parse';
import { toHtml } from '../../../mfm/toHtml';
import { parse } from '../../../mfm/parse';
import DriveFile from '../../../models/drive-file';
import { getEmojis } from './note';
import renderEmoji from './emoji';

View File

@@ -2,7 +2,7 @@ import { request } from 'https';
const { sign } = require('http-signature');
import { URL } from 'url';
import * as debug from 'debug';
const crypto = require('crypto');
import * as crypto from 'crypto';
const { lookup } = require('lookup-dns-cache');
const promiseAny = require('promise-any');

View File

@@ -4,7 +4,7 @@ const json = require('koa-json-body');
const httpSignature = require('http-signature');
import { createHttpJob } from '../queue';
import pack from '../remote/activitypub/renderer';
import { renderActivity } from '../remote/activitypub/renderer';
import Note from '../models/note';
import User, { isLocalUser, ILocalUser, IUser } from '../models/user';
import Emoji from '../models/emoji';
@@ -83,7 +83,7 @@ router.get('/notes/:note', async (ctx, next) => {
return;
}
ctx.body = pack(await renderNote(note, false));
ctx.body = renderActivity(await renderNote(note, false));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
@@ -106,7 +106,7 @@ router.get('/notes/:note/activity', async ctx => {
return;
}
ctx.body = pack(await packActivity(note));
ctx.body = renderActivity(await packActivity(note));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});
@@ -137,7 +137,7 @@ router.get('/questions/:question', async (ctx, next) => {
_id: poll.userId
});
ctx.body = pack(await renderQuestion(user as ILocalUser, poll));
ctx.body = renderActivity(await renderQuestion(user as ILocalUser, poll));
setResponseType(ctx);
});
@@ -173,7 +173,7 @@ router.get('/users/:user/publickey', async ctx => {
}
if (isLocalUser(user)) {
ctx.body = pack(renderKey(user));
ctx.body = renderActivity(renderKey(user));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
} else {
@@ -188,7 +188,7 @@ async function userInfo(ctx: Router.IRouterContext, user: IUser) {
return;
}
ctx.body = pack(await renderPerson(user as ILocalUser));
ctx.body = renderActivity(await renderPerson(user as ILocalUser));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
}
@@ -235,7 +235,7 @@ router.get('/emojis/:emoji', async ctx => {
return;
}
ctx.body = pack(await renderEmoji(emoji));
ctx.body = renderActivity(await renderEmoji(emoji));
ctx.set('Cache-Control', 'public, max-age=180');
setResponseType(ctx);
});

View File

@@ -2,7 +2,7 @@ import { ObjectID } from 'mongodb';
import * as Router from 'koa-router';
import config from '../../config';
import User from '../../models/user';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import { setResponseType } from '../activitypub';
import Note from '../../models/note';
@@ -38,7 +38,7 @@ export default async (ctx: Router.IRouterContext) => {
renderedNotes.length, null, null, renderedNotes
);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
};

View File

@@ -4,7 +4,7 @@ import config from '../../config';
import $ from 'cafy'; import ID, { transform } from '../../misc/cafy-id';
import User from '../../models/user';
import Following from '../../models/following';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
@@ -77,12 +77,12 @@ export default async (ctx: Router.IRouterContext) => {
inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null
);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
setResponseType(ctx);
} else {
// index page
const rendered = renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`, null);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
}

View File

@@ -5,7 +5,7 @@ import $ from 'cafy';
import ID, { transform } from '../../misc/cafy-id';
import User from '../../models/user';
import Following from '../../models/following';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import renderFollowUser from '../../remote/activitypub/renderer/follow-user';
@@ -78,12 +78,12 @@ export default async (ctx: Router.IRouterContext) => {
inStock ? `${partOf}?page=true&cursor=${followings[followings.length - 1]._id}` : null
);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
setResponseType(ctx);
} else {
// index page
const rendered = renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`, null);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
}

View File

@@ -4,7 +4,7 @@ import config from '../../config';
import $ from 'cafy';
import ID, { transform } from '../../misc/cafy-id';
import User from '../../models/user';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderOrderedCollection from '../../remote/activitypub/renderer/ordered-collection';
import renderOrderedCollectionPage from '../../remote/activitypub/renderer/ordered-collection-page';
import { setResponseType } from '../activitypub';
@@ -94,7 +94,7 @@ export default async (ctx: Router.IRouterContext) => {
notes.length > 0 ? `${partOf}?page=true&until_id=${notes[notes.length - 1]._id}` : null
);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
} else {
@@ -103,7 +103,7 @@ export default async (ctx: Router.IRouterContext) => {
`${partOf}?page=true`,
`${partOf}?page=true&since_id=000000000000000000000000`
);
ctx.body = pack(rendered);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
setResponseType(ctx);
}

View File

@@ -0,0 +1,49 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import User from '../../../../models/user';
export const meta = {
desc: {
'ja-JP': '指定したユーザーをサイレンスにします。',
'en-US': 'Make silence a user.'
},
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to make silence'
}
},
}
};
export default define(meta, (ps) => new Promise(async (res, rej) => {
const user = await User.findOne({
_id: ps.userId
});
if (user == null) {
return rej('user not found');
}
if (user.isAdmin) {
return rej('cannot silence admin');
}
await User.findOneAndUpdate({
_id: user._id
}, {
$set: {
isSilenced: true
}
});
res();
}));

View File

@@ -0,0 +1,45 @@
import $ from 'cafy';
import ID, { transform } from '../../../../misc/cafy-id';
import define from '../../define';
import User from '../../../../models/user';
export const meta = {
desc: {
'ja-JP': '指定したユーザーのサイレンスを解除します。',
'en-US': 'Unsilence a user.'
},
requireCredential: true,
requireModerator: true,
params: {
userId: {
validator: $.type(ID),
transform: transform,
desc: {
'ja-JP': '対象のユーザーID',
'en-US': 'The user ID which you want to unsilence'
}
},
}
};
export default define(meta, (ps) => new Promise(async (res, rej) => {
const user = await User.findOne({
_id: ps.userId
});
if (user == null) {
return rej('user not found');
}
await User.findOneAndUpdate({
_id: user._id
}, {
$set: {
isSilenced: false
}
});
res();
}));

View File

@@ -1,5 +1,5 @@
import rndstr from 'rndstr';
const crypto = require('crypto');
import * as crypto from 'crypto';
import $ from 'cafy';
import App from '../../../../models/app';
import AuthSess from '../../../../models/auth-session';

View File

@@ -6,7 +6,7 @@ import acceptAllFollowRequests from '../../../../services/following/requests/acc
import { publishToFollowers } from '../../../../services/i/update';
import define from '../../define';
import getDriveFileUrl from '../../../../misc/get-drive-file-url';
import parse from '../../../../mfm/parse';
import { parse, parsePlain } from '../../../../mfm/parse';
import extractEmojis from '../../../../misc/extract-emojis';
const langmap = require('langmap');
@@ -206,7 +206,7 @@ export default define(meta, (ps, user, app) => new Promise(async (res, rej) => {
let emojis = [] as string[];
if (updates.name != null) {
const tokens = parse(updates.name, true);
const tokens = parsePlain(updates.name);
emojis = emojis.concat(extractEmojis(tokens));
}

View File

@@ -4,8 +4,8 @@ import config from '../../../config';
import Emoji from '../../../models/emoji';
import define from '../define';
import fetchMeta from '../../../misc/fetch-meta';
import * as pkg from '../../../../package.json';
const pkg = require('../../../../package.json');
const client = require('../../../../built/client/meta.json');
export const meta = {

View File

@@ -53,14 +53,23 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
const visibleQuery = [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for specified/private)
// myself (for followers/specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}, {
visibility: 'followers',
userId: { $in: followings.map(f => f.id) }
$or: [{
// フォロワーの投稿
userId: { $in: followings.map(f => f.id) },
}, {
// 自分の投稿へのリプライ
'_reply.userId': user._id,
}, {
// 自分へのメンションが含まれている
mentions: { $in: [ user._id ] }
}]
}];
const query = {

View File

@@ -51,14 +51,23 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
}] : [{
visibility: { $in: [ 'public', 'home' ] }
}, {
// myself (for specified/private)
// myself (for followers/specified/private)
userId: user._id
}, {
// to me (for specified)
visibleUserIds: { $in: [ user._id ] }
}, {
visibility: 'followers',
userId: { $in: followings.map(f => f.id) }
$or: [{
// フォロワーの投稿
userId: { $in: followings.map(f => f.id) },
}, {
// 自分の投稿へのリプライ
'_reply.userId': user._id,
}, {
// 自分へのメンションが含まれている
mentions: { $in: [ user._id ] }
}]
}];
const q = {

View File

@@ -2,7 +2,7 @@ import $ from 'cafy'; import ID, { transform } from '../../../../../misc/cafy-id
import UserList from '../../../../../models/user-list';
import User, { pack as packUser, isRemoteUser, fetchProxyAccount } from '../../../../../models/user';
import { publishUserListStream } from '../../../../../stream';
import ap from '../../../../../remote/activitypub/renderer';
import { renderActivity } from '../../../../../remote/activitypub/renderer';
import renderFollow from '../../../../../remote/activitypub/renderer/follow';
import { deliver } from '../../../../../queue';
import define from '../../../define';
@@ -72,7 +72,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
// このインスタンス内にこのリモートユーザーをフォローしているユーザーがいなくても投稿を受け取るためにダミーのユーザーがフォローしたということにする
if (isRemoteUser(user)) {
const proxy = await fetchProxyAccount();
const content = ap(renderFollow(proxy, user));
const content = renderActivity(renderFollow(proxy, user));
deliver(proxy, content, user.inbox);
}
}));

View File

@@ -6,6 +6,7 @@ import { ObjectID } from 'bson';
import Emoji from '../../../models/emoji';
import { toMastodonEmojis } from './emoji';
import fetchMeta from '../../../misc/fetch-meta';
import * as pkg from '../../../../package.json';
// Init router
const router = new Router();
@@ -48,7 +49,7 @@ router.get('/v1/instance', async ctx => { // TODO: This is a temporary implement
title: meta.name || 'Misskey',
description: meta.description || '',
email: meta.maintainer.email,
version: `0.0.0 (compatible; Misskey)`, // TODO: commit hash
version: `0.0.0 (compatible; Misskey ${pkg.version})`, // TODO: commit hash
thumbnail: meta.bannerUrl,
/*
urls: {

View File

@@ -20,7 +20,7 @@ import Note, { pack as packNote } from '../../models/note';
import getNoteSummary from '../../misc/get-note-summary';
import fetchMeta from '../../misc/fetch-meta';
import Emoji from '../../models/emoji';
const pkg = require('../../../package.json');
import * as pkg from '../../../package.json';
const client = `${__dirname}/../../client/`;

View File

@@ -2,7 +2,7 @@ import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../
import Following from '../../models/following';
import FollowRequest from '../../models/follow-request';
import { publishMainStream } from '../../stream';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
import renderBlock from '../../remote/activitypub/renderer/block';
@@ -27,7 +27,7 @@ export default async function(blocker: IUser, blockee: IUser) {
});
if (isLocalUser(blocker) && isRemoteUser(blockee)) {
const content = pack(renderBlock(blocker, blockee));
const content = renderActivity(renderBlock(blocker, blockee));
deliver(blocker, content, blockee.inbox);
}
}
@@ -67,13 +67,13 @@ async function cancelRequest(follower: IUser, followee: IUser) {
// リモートにフォローリクエストをしていたらUndoFollow送信
if (isLocalUser(follower) && isRemoteUser(followee)) {
const content = pack(renderUndo(renderFollow(follower, followee), follower));
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
// リモートからフォローリクエストを受けていたらReject送信
if (isRemoteUser(follower) && isLocalUser(followee)) {
const content = pack(renderReject(renderFollow(follower, followee, request.requestId), followee));
const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee));
deliver(followee, content, follower.inbox);
}
}
@@ -119,7 +119,7 @@ async function unFollow(follower: IUser, followee: IUser) {
// リモートにフォローをしていたらUndoFollow送信
if (isLocalUser(follower) && isRemoteUser(followee)) {
const content = pack(renderUndo(renderFollow(follower, followee), follower));
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
}

View File

@@ -1,6 +1,6 @@
import { isLocalUser, isRemoteUser, IUser } from '../../models/user';
import Blocking from '../../models/blocking';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderBlock from '../../remote/activitypub/renderer/block';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
@@ -22,7 +22,7 @@ export default async function(blocker: IUser, blockee: IUser) {
// deliver if remote bloking
if (isLocalUser(blocker) && isRemoteUser(blockee)) {
const content = pack(renderUndo(renderBlock(blocker, blockee), blocker));
const content = renderActivity(renderUndo(renderBlock(blocker, blockee), blocker));
deliver(blocker, content, blockee.inbox);
}
}

View File

@@ -322,7 +322,7 @@ export default async function(
if (type) {
res([type.mime, type.ext]);
} else if (isSvg(buffer)) {
res(['image/svg+xml', 'svg'])
res(['image/svg+xml', 'svg']);
} else {
// 種類が同定できなかったら application/octet-stream にする
res(['application/octet-stream', null]);

View File

@@ -3,7 +3,7 @@ import Following from '../../models/following';
import Blocking from '../../models/blocking';
import { publishMainStream } from '../../stream';
import notify from '../../notify';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderAccept from '../../remote/activitypub/renderer/accept';
import renderReject from '../../remote/activitypub/renderer/reject';
@@ -26,7 +26,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
if (isRemoteUser(follower) && isLocalUser(followee) && blocked) {
// リモートフォローを受けてブロックしていた場合は、エラーにするのではなくRejectを送り返しておしまい。
const content = pack(renderReject(renderFollow(follower, followee, requestId), followee));
const content = renderActivity(renderReject(renderFollow(follower, followee, requestId), followee));
deliver(followee , content, follower.inbox);
return;
} else if (isRemoteUser(follower) && isLocalUser(followee) && blocking) {
@@ -115,7 +115,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
}
if (isRemoteUser(follower) && isLocalUser(followee)) {
const content = pack(renderAccept(renderFollow(follower, followee, requestId), followee));
const content = renderActivity(renderAccept(renderFollow(follower, followee, requestId), followee));
deliver(followee, content, follower.inbox);
}
}

View File

@@ -1,7 +1,7 @@
import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../models/user';
import Following from '../../models/following';
import { publishMainStream } from '../../stream';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import renderFollow from '../../remote/activitypub/renderer/follow';
import renderUndo from '../../remote/activitypub/renderer/undo';
import { deliver } from '../../queue';
@@ -48,7 +48,7 @@ export default async function(follower: IUser, followee: IUser) {
}
if (isLocalUser(follower) && isRemoteUser(followee)) {
const content = pack(renderUndo(renderFollow(follower, followee), follower));
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower, content, followee.inbox);
}
}

View File

@@ -1,6 +1,6 @@
import User, { IUser, isRemoteUser, ILocalUser, pack as packUser, isLocalUser } from '../../../models/user';
import FollowRequest from '../../../models/follow-request';
import pack from '../../../remote/activitypub/renderer';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderAccept from '../../../remote/activitypub/renderer/accept';
import { deliver } from '../../../queue';
@@ -42,7 +42,7 @@ export default async function(followee: IUser, follower: IUser) {
followerId: follower._id
});
const content = pack(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
const content = renderActivity(renderAccept(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
deliver(followee as ILocalUser, content, follower.inbox);
}

View File

@@ -1,6 +1,6 @@
import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
import FollowRequest from '../../../models/follow-request';
import pack from '../../../remote/activitypub/renderer';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderUndo from '../../../remote/activitypub/renderer/undo';
import { deliver } from '../../../queue';
@@ -8,7 +8,7 @@ import { publishMainStream } from '../../../stream';
export default async function(followee: IUser, follower: IUser) {
if (isRemoteUser(followee)) {
const content = pack(renderUndo(renderFollow(follower, followee), follower));
const content = renderActivity(renderUndo(renderFollow(follower, followee), follower));
deliver(follower as ILocalUser, content, followee.inbox);
}

View File

@@ -1,7 +1,7 @@
import User, { isLocalUser, isRemoteUser, pack as packUser, IUser } from '../../../models/user';
import { publishMainStream } from '../../../stream';
import notify from '../../../notify';
import pack from '../../../remote/activitypub/renderer';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import { deliver } from '../../../queue';
import FollowRequest from '../../../models/follow-request';
@@ -61,7 +61,7 @@ export default async function(follower: IUser, followee: IUser, requestId?: stri
}
if (isLocalUser(follower) && isRemoteUser(followee)) {
const content = pack(renderFollow(follower, followee));
const content = renderActivity(renderFollow(follower, followee));
deliver(follower, content, followee.inbox);
}
}

View File

@@ -1,6 +1,6 @@
import User, { IUser, isRemoteUser, ILocalUser, pack as packUser } from '../../../models/user';
import FollowRequest from '../../../models/follow-request';
import pack from '../../../remote/activitypub/renderer';
import { renderActivity } from '../../../remote/activitypub/renderer';
import renderFollow from '../../../remote/activitypub/renderer/follow';
import renderReject from '../../../remote/activitypub/renderer/reject';
import { deliver } from '../../../queue';
@@ -13,7 +13,7 @@ export default async function(followee: IUser, follower: IUser) {
followerId: follower._id
});
const content = pack(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
const content = renderActivity(renderReject(renderFollow(follower, followee, request.requestId), followee as ILocalUser));
deliver(followee as ILocalUser, content, follower.inbox);
}

View File

@@ -5,7 +5,7 @@ import Note, { packMany } from '../../models/note';
import Following from '../../models/following';
import renderAdd from '../../remote/activitypub/renderer/add';
import renderRemove from '../../remote/activitypub/renderer/remove';
import packAp from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
/**
@@ -100,7 +100,7 @@ export async function deliverPinnedChange(userId: mongo.ObjectID, noteId: mongo.
const target = `${config.url}/users/${user._id}/collections/featured`;
const item = `${config.url}/notes/${noteId}`;
const content = packAp(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
const content = renderActivity(isAddition ? renderAdd(user, target, item) : renderRemove(user, target, item));
for (const inbox of queue) {
deliver(user, content, inbox);
}

View File

@@ -3,7 +3,7 @@ import User, { isLocalUser, isRemoteUser } from '../../models/user';
import Following from '../../models/following';
import renderPerson from '../../remote/activitypub/renderer/person';
import renderUpdate from '../../remote/activitypub/renderer/update';
import packAp from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
export async function publishToFollowers(userId: mongo.ObjectID) {
@@ -29,7 +29,7 @@ export async function publishToFollowers(userId: mongo.ObjectID) {
}
if (queue.length > 0) {
const content = packAp(renderUpdate(await renderPerson(user), user));
const content = renderActivity(renderUpdate(await renderPerson(user), user));
for (const inbox of queue) {
deliver(user, content, inbox);
}

View File

@@ -7,13 +7,13 @@ import { deliver } from '../../queue';
import renderNote from '../../remote/activitypub/renderer/note';
import renderCreate from '../../remote/activitypub/renderer/create';
import renderAnnounce from '../../remote/activitypub/renderer/announce';
import packAp from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import DriveFile, { IDriveFile } from '../../models/drive-file';
import notify from '../../notify';
import NoteWatching from '../../models/note-watching';
import watch from './watch';
import Mute from '../../models/mute';
import parse from '../../mfm/parse';
import { parse } from '../../mfm/parse';
import { IApp } from '../../models/app';
import UserList from '../../models/user-list';
import resolveUser from '../../remote/resolve-user';
@@ -116,6 +116,11 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
if (data.viaMobile == null) data.viaMobile = false;
if (data.localOnly == null) data.localOnly = false;
// サイレンス
if (user.isSilenced && data.visibility == 'public') {
data.visibility = 'home';
}
if (data.visibleUsers) {
data.visibleUsers = erase(null, data.visibleUsers);
}
@@ -278,7 +283,7 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
createMentionedEvents(mentionedUsers, note, nm);
const noteActivity = await renderActivity(data, note);
const noteActivity = await renderNoteOrRenoteActivity(data, note);
if (isLocalUser(user)) {
deliverNoteToMentionedRemoteUsers(mentionedUsers, user, noteActivity);
@@ -336,14 +341,14 @@ export default async (user: IUser, data: Option, silent = false) => new Promise<
index(note);
});
async function renderActivity(data: Option, note: INote) {
async function renderNoteOrRenoteActivity(data: Option, note: INote) {
if (data.localOnly) return null;
const content = data.renote && data.text == null && data.poll == null && (data.files == null || data.files.length == 0)
? renderAnnounce(data.renote.uri ? data.renote.uri : `${config.url}/notes/${data.renote._id}`, note)
: renderCreate(await renderNote(note, false), note);
return packAp(content);
return renderActivity(content);
}
function incRenoteCount(renote: INote) {

View File

@@ -2,7 +2,7 @@ import Note, { INote } from '../../models/note';
import { IUser, isLocalUser } from '../../models/user';
import { publishNoteStream } from '../../stream';
import renderDelete from '../../remote/activitypub/renderer/delete';
import pack from '../../remote/activitypub/renderer';
import { renderActivity } from '../../remote/activitypub/renderer';
import { deliver } from '../../queue';
import Following from '../../models/following';
import renderTombstone from '../../remote/activitypub/renderer/tombstone';
@@ -75,7 +75,7 @@ export default async function(user: IUser, note: INote) {
//#region ローカルの投稿なら削除アクティビティを配送
if (isLocalUser(user)) {
const content = pack(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user));
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user));
const followings = await Following.find({
followeeId: user._id,

View File

@@ -7,7 +7,7 @@ import NoteWatching from '../../../models/note-watching';
import watch from '../watch';
import renderLike from '../../../remote/activitypub/renderer/like';
import { deliver } from '../../../queue';
import pack from '../../../remote/activitypub/renderer';
import { renderActivity } from '../../../remote/activitypub/renderer';
import perUserReactionsChart from '../../../chart/per-user-reactions';
export default async (user: IUser, note: INote, reaction: string) => new Promise(async (res, rej) => {
@@ -86,7 +86,7 @@ export default async (user: IUser, note: INote, reaction: string) => new Promise
//#region 配信
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
if (isLocalUser(user) && isRemoteUser(note._user)) {
const content = pack(renderLike(user, note, reaction));
const content = renderActivity(renderLike(user, note, reaction));
deliver(user, content, note._user.inbox);
}
//#endregion

View File

@@ -4,7 +4,7 @@ import Reaction from '../../../models/note-reaction';
import { publishNoteStream } from '../../../stream';
import renderLike from '../../../remote/activitypub/renderer/like';
import renderUndo from '../../../remote/activitypub/renderer/undo';
import pack from '../../../remote/activitypub/renderer';
import { renderActivity } from '../../../remote/activitypub/renderer';
import { deliver } from '../../../queue';
export default async (user: IUser, note: INote) => new Promise(async (res, rej) => {
@@ -42,7 +42,7 @@ export default async (user: IUser, note: INote) => new Promise(async (res, rej)
//#region 配信
// リアクターがローカルユーザーかつリアクション対象がリモートユーザーの投稿なら配送
if (isLocalUser(user) && isRemoteUser(note._user)) {
const content = pack(renderUndo(renderLike(user, note, exist.reaction), user));
const content = renderActivity(renderUndo(renderLike(user, note, exist.reaction), user));
deliver(user, content, note._user.inbox);
}
//#endregion

572
test/api-visibility.ts Normal file
View File

@@ -0,0 +1,572 @@
/*
* Tests of API (visibility)
*
* How to run the tests:
* > mocha test/api-visibility.ts --require ts-node/register
*
* To specify test:
* > mocha test/api-visibility.ts --require ts-node/register -g 'test name'
*/
import * as http from 'http';
import * as assert from 'chai';
import { async, _signup, _request, _uploadFile, _post, _react, resetDb } from './utils';
const expect = assert.expect;
//#region process
Error.stackTraceLimit = Infinity;
// During the test the env variable is set to test
process.env.NODE_ENV = 'test';
// Display detail of unhandled promise rejection
process.on('unhandledRejection', console.dir);
//#endregion
const app = require('../built/server/api').default;
const db = require('../built/db/mongodb').default;
const server = http.createServer(app.callback());
//#region Utilities
const request = _request(server);
const signup = _signup(request);
const post = _post(request);
//#endregion
describe('API visibility', () => {
// Reset database each test
before(resetDb(db));
after(() => {
server.close();
});
describe('Note visibility', async () => {
//#region vars
/** ヒロイン */
let alice: any;
/** フォロワー */
let follower: any;
/** 非フォロワー */
let other: any;
/** 非フォロワーでもリプライやメンションをされた人 */
let target: any;
/** public-post */
let pub: any;
/** home-post */
let home: any;
/** followers-post */
let fol: any;
/** specified-post */
let spe: any;
/** private-post */
let pri: any;
/** public-reply to target's post */
let pubR: any;
/** home-reply to target's post */
let homeR: any;
/** followers-reply to target's post */
let folR: any;
/** specified-reply to target's post */
let speR: any;
/** private-reply to target's post */
let priR: any;
/** public-mention to target */
let pubM: any;
/** home-mention to target */
let homeM: any;
/** followers-mention to target */
let folM: any;
/** specified-mention to target */
let speM: any;
/** private-mention to target */
let priM: any;
/** reply target post */
let tgt: any;
//#endregion
const show = async (noteId: any, by: any) => {
return await request('/notes/show', {
noteId
}, by);
};
before(async () => {
//#region prepare
// signup
alice = await signup({ username: 'alice' });
follower = await signup({ username: 'follower' });
other = await signup({ username: 'other' });
target = await signup({ username: 'target' });
// follow alice <= follower
await request('/following/create', { userId: alice.id }, follower);
// normal posts
pub = await post(alice, { text: 'x', visibility: 'public' });
home = await post(alice, { text: 'x', visibility: 'home' });
fol = await post(alice, { text: 'x', visibility: 'followers' });
spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
pri = await post(alice, { text: 'x', visibility: 'private' });
// replies
tgt = await post(target, { text: 'y', visibility: 'public' });
pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
priR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'private' });
// mentions
pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
speM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'specified' });
priM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'private' });
//#endregion
});
//#region show post
// public
it('[show] public-postを自分が見れる', async(async () => {
const res = await show(pub.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-postをフォロワーが見れる', async(async () => {
const res = await show(pub.id, follower);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-postを非フォロワーが見れる', async(async () => {
const res = await show(pub.id, other);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-postを未認証が見れる', async(async () => {
const res = await show(pub.id, null);
expect(res.body).have.property('text').eql('x');
}));
// home
it('[show] home-postを自分が見れる', async(async () => {
const res = await show(home.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-postをフォロワーが見れる', async(async () => {
const res = await show(home.id, follower);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-postを非フォロワーが見れる', async(async () => {
const res = await show(home.id, other);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-postを未認証が見れる', async(async () => {
const res = await show(home.id, null);
expect(res.body).have.property('text').eql('x');
}));
// followers
it('[show] followers-postを自分が見れる', async(async () => {
const res = await show(fol.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] followers-postをフォロワーが見れる', async(async () => {
const res = await show(fol.id, follower);
expect(res.body).have.property('text').eql('x');
}));
it('[show] followers-postを非フォロワーが見れない', async(async () => {
const res = await show(fol.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] followers-postを未認証が見れない', async(async () => {
const res = await show(fol.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
// specified
it('[show] specified-postを自分が見れる', async(async () => {
const res = await show(spe.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] specified-postを指定ユーザーが見れる', async(async () => {
const res = await show(spe.id, target);
expect(res.body).have.property('text').eql('x');
}));
it('[show] specified-postをフォロワーが見れない', async(async () => {
const res = await show(spe.id, follower);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] specified-postを非フォロワーが見れない', async(async () => {
const res = await show(spe.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] specified-postを未認証が見れない', async(async () => {
const res = await show(spe.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
// private
it('[show] private-postを自分が見れる', async(async () => {
const res = await show(pri.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] private-postをフォロワーが見れない', async(async () => {
const res = await show(pri.id, follower);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] private-postを非フォロワーが見れない', async(async () => {
const res = await show(pri.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] private-postを未認証が見れない', async(async () => {
const res = await show(pri.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
//#endregion
//#region show reply
// public
it('[show] public-replyを自分が見れる', async(async () => {
const res = await show(pubR.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-replyをされた人が見れる', async(async () => {
const res = await show(pubR.id, target);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-replyをフォロワーが見れる', async(async () => {
const res = await show(pubR.id, follower);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-replyを非フォロワーが見れる', async(async () => {
const res = await show(pubR.id, other);
expect(res.body).have.property('text').eql('x');
}));
it('[show] public-replyを未認証が見れる', async(async () => {
const res = await show(pubR.id, null);
expect(res.body).have.property('text').eql('x');
}));
// home
it('[show] home-replyを自分が見れる', async(async () => {
const res = await show(homeR.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-replyをされた人が見れる', async(async () => {
const res = await show(homeR.id, target);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-replyをフォロワーが見れる', async(async () => {
const res = await show(homeR.id, follower);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-replyを非フォロワーが見れる', async(async () => {
const res = await show(homeR.id, other);
expect(res.body).have.property('text').eql('x');
}));
it('[show] home-replyを未認証が見れる', async(async () => {
const res = await show(homeR.id, null);
expect(res.body).have.property('text').eql('x');
}));
// followers
it('[show] followers-replyを自分が見れる', async(async () => {
const res = await show(folR.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] followers-replyを非フォロワーでもリプライされていれば見れる', async(async () => {
const res = await show(folR.id, target);
expect(res.body).have.property('text').eql('x');
}));
it('[show] followers-replyをフォロワーが見れる', async(async () => {
const res = await show(folR.id, follower);
expect(res.body).have.property('text').eql('x');
}));
it('[show] followers-replyを非フォロワーが見れない', async(async () => {
const res = await show(folR.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] followers-replyを未認証が見れない', async(async () => {
const res = await show(folR.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
// specified
it('[show] specified-replyを自分が見れる', async(async () => {
const res = await show(speR.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] specified-replyを指定ユーザーが見れる', async(async () => {
const res = await show(speR.id, target);
expect(res.body).have.property('text').eql('x');
}));
it('[show] specified-replyをされた人が指定されてなくても見れる', async(async () => {
const res = await show(speR.id, target);
expect(res.body).have.property('text').eql('x');
}));
it('[show] specified-replyをフォロワーが見れない', async(async () => {
const res = await show(speR.id, follower);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] specified-replyを非フォロワーが見れない', async(async () => {
const res = await show(speR.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] specified-replyを未認証が見れない', async(async () => {
const res = await show(speR.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
// private
it('[show] private-replyを自分が見れる', async(async () => {
const res = await show(priR.id, alice);
expect(res.body).have.property('text').eql('x');
}));
it('[show] private-replyをフォロワーが見れない', async(async () => {
const res = await show(priR.id, follower);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] private-replyを非フォロワーが見れない', async(async () => {
const res = await show(priR.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] private-replyを未認証が見れない', async(async () => {
const res = await show(priR.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
//#endregion
//#region show mention
// public
it('[show] public-mentionを自分が見れる', async(async () => {
const res = await show(pubM.id, alice);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] public-mentionをされた人が見れる', async(async () => {
const res = await show(pubM.id, target);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] public-mentionをフォロワーが見れる', async(async () => {
const res = await show(pubM.id, follower);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] public-mentionを非フォロワーが見れる', async(async () => {
const res = await show(pubM.id, other);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] public-mentionを未認証が見れる', async(async () => {
const res = await show(pubM.id, null);
expect(res.body).have.property('text').eql('@target x');
}));
// home
it('[show] home-mentionを自分が見れる', async(async () => {
const res = await show(homeM.id, alice);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] home-mentionをされた人が見れる', async(async () => {
const res = await show(homeM.id, target);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] home-mentionをフォロワーが見れる', async(async () => {
const res = await show(homeM.id, follower);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] home-mentionを非フォロワーが見れる', async(async () => {
const res = await show(homeM.id, other);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] home-mentionを未認証が見れる', async(async () => {
const res = await show(homeM.id, null);
expect(res.body).have.property('text').eql('@target x');
}));
// followers
it('[show] followers-mentionを自分が見れる', async(async () => {
const res = await show(folM.id, alice);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] followers-mentionを非フォロワーでもメンションされていれば見れる', async(async () => {
const res = await show(folM.id, target);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] followers-mentionをフォロワーが見れる', async(async () => {
const res = await show(folM.id, follower);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] followers-mentionを非フォロワーが見れない', async(async () => {
const res = await show(folM.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] followers-mentionを未認証が見れない', async(async () => {
const res = await show(folM.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
// specified
it('[show] specified-mentionを自分が見れる', async(async () => {
const res = await show(speM.id, alice);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] specified-mentionを指定ユーザーが見れる', async(async () => {
const res = await show(speM.id, target);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] specified-mentionをされた人が指定されてなくても見れる', async(async () => {
const res = await show(speM.id, target);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] specified-mentionをフォロワーが見れない', async(async () => {
const res = await show(speM.id, follower);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] specified-mentionを非フォロワーが見れない', async(async () => {
const res = await show(speM.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] specified-mentionを未認証が見れない', async(async () => {
const res = await show(speM.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
// private
it('[show] private-mentionを自分が見れる', async(async () => {
const res = await show(priM.id, alice);
expect(res.body).have.property('text').eql('@target x');
}));
it('[show] private-mentionをフォロワーが見れない', async(async () => {
const res = await show(priM.id, follower);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] private-mentionを非フォロワーが見れない', async(async () => {
const res = await show(priM.id, other);
expect(res.body).have.property('isHidden').eql(true);
}));
it('[show] private-mentionを未認証が見れない', async(async () => {
const res = await show(priM.id, null);
expect(res.body).have.property('isHidden').eql(true);
}));
//#endregion
//#region HTL
it('[HTL] public-post が 自分が見れる', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, alice);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == pub.id);
expect(notes[0]).have.property('text').eql('x');
}));
it('[HTL] public-post が 非フォロワーから見れない', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, other);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == pub.id);
expect(notes).length(0);
}));
it('[HTL] followers-post が フォロワーから見れる', async(async () => {
const res = await request('/notes/timeline', { limit: 100 }, follower);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == fol.id);
expect(notes[0]).have.property('text').eql('x');
}));
//#endregion
//#region RTL
it('[replies] followers-reply が フォロワーから見れる', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, follower);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == folR.id);
expect(notes[0]).have.property('text').eql('x');
}));
it('[replies] followers-reply が 非フォロワー (リプライ先ではない) から見れない', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, other);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == folR.id);
expect(notes).length(0);
}));
it('[replies] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
const res = await request('/notes/replies', { noteId: tgt.id, limit: 100 }, target);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == folR.id);
expect(notes[0]).have.property('text').eql('x');
}));
//#endregion
//#region MTL
it('[mentions] followers-reply が 非フォロワー (リプライ先である) から見れる', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == folR.id);
expect(notes[0]).have.property('text').eql('x');
}));
it('[mentions] followers-mention が 非フォロワー (メンション先である) から見れる', async(async () => {
const res = await request('/notes/mentions', { limit: 100 }, target);
expect(res).have.status(200);
const notes = res.body.filter((n: any) => n.id == folM.id);
expect(notes[0]).have.property('text').eql('@target x');
}));
//#endregion
});
});

View File

@@ -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 '../src/mfm/parse';
describe('Extract mentions', () => {
it('simple', () => {

View File

@@ -10,9 +10,10 @@
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, removeOrphanedBrackets } from '../src/mfm/parser';
import { parse, parsePlain } from '../src/mfm/parse';
import { toHtml } from '../src/mfm/toHtml';
import { createTree as tree, createLeaf as leaf, MfmTree } from '../src/mfm/types';
import { removeOrphanedBrackets } from '../src/mfm/language';
function text(text: string): MfmTree {
return leaf('text', { text });
@@ -150,7 +151,7 @@ describe('removeOrphanedBrackets', () => {
describe('MFM', () => {
it('can be analyzed', () => {
const tokens = analyze('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
const tokens = parse('@himawari @hima_sub@namori.net お腹ペコい :cat: #yryr');
assert.deepStrictEqual(tokens, [
leaf('mention', {
acct: '@himawari',
@@ -175,7 +176,7 @@ describe('MFM', () => {
describe('elements', () => {
describe('bold', () => {
it('simple', () => {
const tokens = analyze('**foo**');
const tokens = parse('**foo**');
assert.deepStrictEqual(tokens, [
tree('bold', [
text('foo')
@@ -184,7 +185,7 @@ describe('MFM', () => {
});
it('with other texts', () => {
const tokens = analyze('bar**foo**bar');
const tokens = parse('bar**foo**bar');
assert.deepStrictEqual(tokens, [
text('bar'),
tree('bold', [
@@ -195,7 +196,7 @@ describe('MFM', () => {
});
it('with underscores', () => {
const tokens = analyze('__foo__');
const tokens = parse('__foo__');
assert.deepStrictEqual(tokens, [
tree('bold', [
text('foo')
@@ -204,21 +205,21 @@ describe('MFM', () => {
});
it('with underscores (ensure it allows alphabet only)', () => {
const tokens = analyze('(=^・__________・^=)');
const tokens = parse('(=^・__________・^=)');
assert.deepStrictEqual(tokens, [
text('(=^・__________・^=)')
]);
});
it('mixed syntax', () => {
const tokens = analyze('**foo__');
const tokens = parse('**foo__');
assert.deepStrictEqual(tokens, [
text('**foo__'),
]);
});
it('mixed syntax', () => {
const tokens = analyze('__foo**');
const tokens = parse('__foo**');
assert.deepStrictEqual(tokens, [
text('__foo**'),
]);
@@ -226,7 +227,7 @@ describe('MFM', () => {
});
it('big', () => {
const tokens = analyze('***Strawberry*** Pasta');
const tokens = parse('***Strawberry*** Pasta');
assert.deepStrictEqual(tokens, [
tree('big', [
text('Strawberry')
@@ -236,7 +237,7 @@ describe('MFM', () => {
});
it('small', () => {
const tokens = analyze('<small>smaller</small>');
const tokens = parse('<small>smaller</small>');
assert.deepStrictEqual(tokens, [
tree('small', [
text('smaller')
@@ -245,7 +246,7 @@ describe('MFM', () => {
});
it('flip', () => {
const tokens = analyze('<flip>foo</flip>');
const tokens = parse('<flip>foo</flip>');
assert.deepStrictEqual(tokens, [
tree('flip', [
text('foo')
@@ -255,7 +256,7 @@ describe('MFM', () => {
describe('spin', () => {
it('simple', () => {
const tokens = analyze('<spin>:foo:</spin>');
const tokens = parse('<spin>:foo:</spin>');
assert.deepStrictEqual(tokens, [
tree('spin', [
leaf('emoji', { name: 'foo' })
@@ -266,7 +267,7 @@ describe('MFM', () => {
});
it('with attr', () => {
const tokens = analyze('<spin left>:foo:</spin>');
const tokens = parse('<spin left>:foo:</spin>');
assert.deepStrictEqual(tokens, [
tree('spin', [
leaf('emoji', { name: 'foo' })
@@ -275,10 +276,25 @@ describe('MFM', () => {
}),
]);
});
it('nested', () => {
const tokens = parse('<spin><spin>:foo:</spin></spin>');
assert.deepStrictEqual(tokens, [
tree('spin', [
tree('spin', [
leaf('emoji', { name: 'foo' })
], {
attr: null
}),
], {
attr: null
}),
]);
});
});
it('jump', () => {
const tokens = analyze('<jump>:foo:</jump>');
const tokens = parse('<jump>:foo:</jump>');
assert.deepStrictEqual(tokens, [
tree('jump', [
leaf('emoji', { name: 'foo' })
@@ -288,7 +304,7 @@ describe('MFM', () => {
describe('motion', () => {
it('by triple brackets', () => {
const tokens = analyze('(((foo)))');
const tokens = parse('(((foo)))');
assert.deepStrictEqual(tokens, [
tree('motion', [
text('foo')
@@ -297,7 +313,7 @@ describe('MFM', () => {
});
it('by triple brackets (with other texts)', () => {
const tokens = analyze('bar(((foo)))bar');
const tokens = parse('bar(((foo)))bar');
assert.deepStrictEqual(tokens, [
text('bar'),
tree('motion', [
@@ -308,7 +324,7 @@ describe('MFM', () => {
});
it('by <motion> tag', () => {
const tokens = analyze('<motion>foo</motion>');
const tokens = parse('<motion>foo</motion>');
assert.deepStrictEqual(tokens, [
tree('motion', [
text('foo')
@@ -317,7 +333,7 @@ describe('MFM', () => {
});
it('by <motion> tag (with other texts)', () => {
const tokens = analyze('bar<motion>foo</motion>bar');
const tokens = parse('bar<motion>foo</motion>bar');
assert.deepStrictEqual(tokens, [
text('bar'),
tree('motion', [
@@ -330,7 +346,7 @@ describe('MFM', () => {
describe('mention', () => {
it('local', () => {
const tokens = analyze('@himawari foo');
const tokens = parse('@himawari foo');
assert.deepStrictEqual(tokens, [
leaf('mention', {
acct: '@himawari',
@@ -343,7 +359,7 @@ describe('MFM', () => {
});
it('remote', () => {
const tokens = analyze('@hima_sub@namori.net foo');
const tokens = parse('@hima_sub@namori.net foo');
assert.deepStrictEqual(tokens, [
leaf('mention', {
acct: '@hima_sub@namori.net',
@@ -356,7 +372,7 @@ describe('MFM', () => {
});
it('remote punycode', () => {
const tokens = analyze('@hima_sub@xn--q9j5bya.xn--zckzah foo');
const tokens = parse('@hima_sub@xn--q9j5bya.xn--zckzah foo');
assert.deepStrictEqual(tokens, [
leaf('mention', {
acct: '@hima_sub@xn--q9j5bya.xn--zckzah',
@@ -369,12 +385,12 @@ describe('MFM', () => {
});
it('ignore', () => {
const tokens = analyze('idolm@ster');
const tokens = parse('idolm@ster');
assert.deepStrictEqual(tokens, [
text('idolm@ster')
]);
const tokens2 = analyze('@a\n@b\n@c');
const tokens2 = parse('@a\n@b\n@c');
assert.deepStrictEqual(tokens2, [
leaf('mention', {
acct: '@a',
@@ -398,7 +414,7 @@ describe('MFM', () => {
})
]);
const tokens3 = analyze('**x**@a');
const tokens3 = parse('**x**@a');
assert.deepStrictEqual(tokens3, [
tree('bold', [
text('x')
@@ -411,7 +427,7 @@ describe('MFM', () => {
})
]);
const tokens4 = analyze('@\n@v\n@veryverylongusername');
const tokens4 = parse('@\n@v\n@veryverylongusername');
assert.deepStrictEqual(tokens4, [
text('@\n'),
leaf('mention', {
@@ -433,14 +449,14 @@ describe('MFM', () => {
describe('hashtag', () => {
it('simple', () => {
const tokens = analyze('#alice');
const tokens = parse('#alice');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'alice' })
]);
});
it('after line break', () => {
const tokens = analyze('foo\n#alice');
const tokens = parse('foo\n#alice');
assert.deepStrictEqual(tokens, [
text('foo\n'),
leaf('hashtag', { hashtag: 'alice' })
@@ -448,7 +464,7 @@ describe('MFM', () => {
});
it('with text', () => {
const tokens = analyze('Strawberry Pasta #alice');
const tokens = parse('Strawberry Pasta #alice');
assert.deepStrictEqual(tokens, [
text('Strawberry Pasta '),
leaf('hashtag', { hashtag: 'alice' })
@@ -456,7 +472,7 @@ describe('MFM', () => {
});
it('with text (zenkaku)', () => {
const tokens = analyze('こんにちは#世界');
const tokens = parse('こんにちは#世界');
assert.deepStrictEqual(tokens, [
text('こんにちは'),
leaf('hashtag', { hashtag: '世界' })
@@ -464,7 +480,7 @@ describe('MFM', () => {
});
it('ignore comma and period', () => {
const tokens = analyze('Foo #bar, baz #piyo.');
const tokens = parse('Foo #bar, baz #piyo.');
assert.deepStrictEqual(tokens, [
text('Foo '),
leaf('hashtag', { hashtag: 'bar' }),
@@ -475,7 +491,7 @@ describe('MFM', () => {
});
it('ignore exclamation mark', () => {
const tokens = analyze('#Foo!');
const tokens = parse('#Foo!');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'Foo' }),
text('!'),
@@ -483,7 +499,7 @@ describe('MFM', () => {
});
it('ignore colon', () => {
const tokens = analyze('#Foo:');
const tokens = parse('#Foo:');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'Foo' }),
text(':'),
@@ -491,7 +507,7 @@ describe('MFM', () => {
});
it('ignore single quote', () => {
const tokens = analyze('#foo\'');
const tokens = parse('#foo\'');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'foo' }),
text('\''),
@@ -499,7 +515,7 @@ describe('MFM', () => {
});
it('ignore double quote', () => {
const tokens = analyze('#foo"');
const tokens = parse('#foo"');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'foo' }),
text('"'),
@@ -507,21 +523,21 @@ describe('MFM', () => {
});
it('allow including number', () => {
const tokens = analyze('#foo123');
const tokens = parse('#foo123');
assert.deepStrictEqual(tokens, [
leaf('hashtag', { hashtag: 'foo123' }),
]);
});
it('with brackets', () => {
const tokens1 = analyze('(#foo)');
const tokens1 = parse('(#foo)');
assert.deepStrictEqual(tokens1, [
text('('),
leaf('hashtag', { hashtag: 'foo' }),
text(')'),
]);
const tokens2 = analyze('「#foo」');
const tokens2 = parse('「#foo」');
assert.deepStrictEqual(tokens2, [
text('「'),
leaf('hashtag', { hashtag: 'foo' }),
@@ -530,7 +546,7 @@ describe('MFM', () => {
});
it('with mixed brackets', () => {
const tokens = analyze('「#foo(bar)」');
const tokens = parse('「#foo(bar)」');
assert.deepStrictEqual(tokens, [
text('「'),
leaf('hashtag', { hashtag: 'foo(bar)' }),
@@ -539,14 +555,14 @@ describe('MFM', () => {
});
it('with brackets (space before)', () => {
const tokens1 = analyze('(bar #foo)');
const tokens1 = parse('(bar #foo)');
assert.deepStrictEqual(tokens1, [
text('(bar '),
leaf('hashtag', { hashtag: 'foo' }),
text(')'),
]);
const tokens2 = analyze('「bar #foo」');
const tokens2 = parse('「bar #foo」');
assert.deepStrictEqual(tokens2, [
text('「bar '),
leaf('hashtag', { hashtag: 'foo' }),
@@ -555,14 +571,14 @@ describe('MFM', () => {
});
it('disallow number only', () => {
const tokens = analyze('#123');
const tokens = parse('#123');
assert.deepStrictEqual(tokens, [
text('#123'),
]);
});
it('disallow number only (with brackets)', () => {
const tokens = analyze('(#123)');
const tokens = parse('(#123)');
assert.deepStrictEqual(tokens, [
text('(#123)'),
]);
@@ -571,14 +587,14 @@ describe('MFM', () => {
describe('quote', () => {
it('basic', () => {
const tokens1 = analyze('> foo');
const tokens1 = parse('> foo');
assert.deepStrictEqual(tokens1, [
tree('quote', [
text('foo')
], {})
]);
const tokens2 = analyze('>foo');
const tokens2 = parse('>foo');
assert.deepStrictEqual(tokens2, [
tree('quote', [
text('foo')
@@ -587,7 +603,7 @@ describe('MFM', () => {
});
it('series', () => {
const tokens = analyze('> foo\n\n> bar');
const tokens = parse('> foo\n\n> bar');
assert.deepStrictEqual(tokens, [
tree('quote', [
text('foo')
@@ -600,14 +616,14 @@ describe('MFM', () => {
});
it('trailing line break', () => {
const tokens1 = analyze('> foo\n');
const tokens1 = parse('> foo\n');
assert.deepStrictEqual(tokens1, [
tree('quote', [
text('foo')
], {}),
]);
const tokens2 = analyze('> foo\n\n');
const tokens2 = parse('> foo\n\n');
assert.deepStrictEqual(tokens2, [
tree('quote', [
text('foo')
@@ -617,14 +633,14 @@ describe('MFM', () => {
});
it('multiline', () => {
const tokens1 = analyze('>foo\n>bar');
const tokens1 = parse('>foo\n>bar');
assert.deepStrictEqual(tokens1, [
tree('quote', [
text('foo\nbar')
], {})
]);
const tokens2 = analyze('> foo\n> bar');
const tokens2 = parse('> foo\n> bar');
assert.deepStrictEqual(tokens2, [
tree('quote', [
text('foo\nbar')
@@ -633,14 +649,14 @@ describe('MFM', () => {
});
it('multiline with trailing line break', () => {
const tokens1 = analyze('> foo\n> bar\n');
const tokens1 = parse('> foo\n> bar\n');
assert.deepStrictEqual(tokens1, [
tree('quote', [
text('foo\nbar')
], {}),
]);
const tokens2 = analyze('> foo\n> bar\n\n');
const tokens2 = parse('> foo\n> bar\n\n');
assert.deepStrictEqual(tokens2, [
tree('quote', [
text('foo\nbar')
@@ -650,7 +666,7 @@ describe('MFM', () => {
});
it('with before and after texts', () => {
const tokens = analyze('before\n> foo\nafter');
const tokens = parse('before\n> foo\nafter');
assert.deepStrictEqual(tokens, [
text('before\n'),
tree('quote', [
@@ -661,7 +677,7 @@ describe('MFM', () => {
});
it('multiple quotes', () => {
const tokens = analyze('> foo\nbar\n\n> foo\nbar\n\n> foo\nbar');
const tokens = parse('> foo\nbar\n\n> foo\nbar\n\n> foo\nbar');
assert.deepStrictEqual(tokens, [
tree('quote', [
text('foo')
@@ -679,14 +695,14 @@ describe('MFM', () => {
});
it('require line break before ">"', () => {
const tokens = analyze('foo>bar');
const tokens = parse('foo>bar');
assert.deepStrictEqual(tokens, [
text('foo>bar'),
]);
});
it('nested', () => {
const tokens = analyze('>> foo\n> bar');
const tokens = parse('>> foo\n> bar');
assert.deepStrictEqual(tokens, [
tree('quote', [
tree('quote', [
@@ -698,7 +714,7 @@ describe('MFM', () => {
});
it('trim line breaks', () => {
const tokens = analyze('foo\n\n>a\n>>b\n>>\n>>>\n>>>c\n>>>\n>d\n\n');
const tokens = parse('foo\n\n>a\n>>b\n>>\n>>>\n>>>c\n>>>\n>d\n\n');
assert.deepStrictEqual(tokens, [
text('foo\n\n'),
tree('quote', [
@@ -718,14 +734,14 @@ describe('MFM', () => {
describe('url', () => {
it('simple', () => {
const tokens = analyze('https://example.com');
const tokens = parse('https://example.com');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://example.com' })
]);
});
it('ignore trailing period', () => {
const tokens = analyze('https://example.com.');
const tokens = parse('https://example.com.');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://example.com' }),
text('.')
@@ -733,14 +749,14 @@ describe('MFM', () => {
});
it('with comma', () => {
const tokens = analyze('https://example.com/foo?bar=a,b');
const tokens = parse('https://example.com/foo?bar=a,b');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://example.com/foo?bar=a,b' })
]);
});
it('ignore trailing comma', () => {
const tokens = analyze('https://example.com/foo, bar');
const tokens = parse('https://example.com/foo, bar');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://example.com/foo' }),
text(', bar')
@@ -748,14 +764,14 @@ describe('MFM', () => {
});
it('with brackets', () => {
const tokens = analyze('https://example.com/foo(bar)');
const tokens = parse('https://example.com/foo(bar)');
assert.deepStrictEqual(tokens, [
leaf('url', { url: 'https://example.com/foo(bar)' })
]);
});
it('ignore parent brackets', () => {
const tokens = analyze('(https://example.com/foo)');
const tokens = parse('(https://example.com/foo)');
assert.deepStrictEqual(tokens, [
text('('),
leaf('url', { url: 'https://example.com/foo' }),
@@ -764,7 +780,7 @@ describe('MFM', () => {
});
it('ignore parent brackets 2', () => {
const tokens = analyze('(foo https://example.com/foo)');
const tokens = parse('(foo https://example.com/foo)');
assert.deepStrictEqual(tokens, [
text('(foo '),
leaf('url', { url: 'https://example.com/foo' }),
@@ -773,7 +789,7 @@ describe('MFM', () => {
});
it('ignore parent brackets with internal brackets', () => {
const tokens = analyze('(https://example.com/foo(bar))');
const tokens = parse('(https://example.com/foo(bar))');
assert.deepStrictEqual(tokens, [
text('('),
leaf('url', { url: 'https://example.com/foo(bar)' }),
@@ -784,7 +800,7 @@ describe('MFM', () => {
describe('link', () => {
it('simple', () => {
const tokens = analyze('[foo](https://example.com)');
const tokens = parse('[foo](https://example.com)');
assert.deepStrictEqual(tokens, [
tree('link', [
text('foo')
@@ -793,7 +809,7 @@ describe('MFM', () => {
});
it('simple (with silent flag)', () => {
const tokens = analyze('?[foo](https://example.com)');
const tokens = parse('?[foo](https://example.com)');
assert.deepStrictEqual(tokens, [
tree('link', [
text('foo')
@@ -802,7 +818,7 @@ describe('MFM', () => {
});
it('in text', () => {
const tokens = analyze('before[foo](https://example.com)after');
const tokens = parse('before[foo](https://example.com)after');
assert.deepStrictEqual(tokens, [
text('before'),
tree('link', [
@@ -813,7 +829,7 @@ describe('MFM', () => {
});
it('with brackets', () => {
const tokens = analyze('[foo](https://example.com/foo(bar))');
const tokens = parse('[foo](https://example.com/foo(bar))');
assert.deepStrictEqual(tokens, [
tree('link', [
text('foo')
@@ -822,7 +838,7 @@ describe('MFM', () => {
});
it('with parent brackets', () => {
const tokens = analyze('([foo](https://example.com/foo(bar)))');
const tokens = parse('([foo](https://example.com/foo(bar)))');
assert.deepStrictEqual(tokens, [
text('('),
tree('link', [
@@ -834,19 +850,19 @@ describe('MFM', () => {
});
it('emoji', () => {
const tokens1 = analyze(':cat:');
const tokens1 = parse(':cat:');
assert.deepStrictEqual(tokens1, [
leaf('emoji', { name: 'cat' })
]);
const tokens2 = analyze(':cat::cat::cat:');
const tokens2 = parse(':cat::cat::cat:');
assert.deepStrictEqual(tokens2, [
leaf('emoji', { name: 'cat' }),
leaf('emoji', { name: 'cat' }),
leaf('emoji', { name: 'cat' })
]);
const tokens3 = analyze('🍎');
const tokens3 = parse('🍎');
assert.deepStrictEqual(tokens3, [
leaf('emoji', { emoji: '🍎' })
]);
@@ -854,21 +870,21 @@ describe('MFM', () => {
describe('block code', () => {
it('simple', () => {
const tokens = analyze('```\nvar x = "Strawberry Pasta";\n```');
const tokens = parse('```\nvar x = "Strawberry Pasta";\n```');
assert.deepStrictEqual(tokens, [
leaf('blockCode', { code: 'var x = "Strawberry Pasta";', lang: null })
]);
});
it('can specify language', () => {
const tokens = analyze('``` json\n{ "x": 42 }\n```');
const tokens = parse('``` json\n{ "x": 42 }\n```');
assert.deepStrictEqual(tokens, [
leaf('blockCode', { code: '{ "x": 42 }', lang: 'json' })
]);
});
it('require line break before "```"', () => {
const tokens = analyze('before```\nfoo\n```');
const tokens = parse('before```\nfoo\n```');
assert.deepStrictEqual(tokens, [
text('before'),
leaf('inlineCode', { code: '`' }),
@@ -878,7 +894,7 @@ describe('MFM', () => {
});
it('series', () => {
const tokens = analyze('```\nfoo\n```\n```\nbar\n```\n```\nbaz\n```');
const tokens = parse('```\nfoo\n```\n```\nbar\n```\n```\nbaz\n```');
assert.deepStrictEqual(tokens, [
leaf('blockCode', { code: 'foo', lang: null }),
leaf('blockCode', { code: 'bar', lang: null }),
@@ -887,14 +903,14 @@ describe('MFM', () => {
});
it('ignore internal marker', () => {
const tokens = analyze('```\naaa```bbb\n```');
const tokens = parse('```\naaa```bbb\n```');
assert.deepStrictEqual(tokens, [
leaf('blockCode', { code: 'aaa```bbb', lang: null })
]);
});
it('trim after line break', () => {
const tokens = analyze('```\nfoo\n```\nbar');
const tokens = parse('```\nfoo\n```\nbar');
assert.deepStrictEqual(tokens, [
leaf('blockCode', { code: 'foo', lang: null }),
text('bar')
@@ -904,21 +920,21 @@ describe('MFM', () => {
describe('inline code', () => {
it('simple', () => {
const tokens = analyze('`var x = "Strawberry Pasta";`');
const tokens = parse('`var x = "Strawberry Pasta";`');
assert.deepStrictEqual(tokens, [
leaf('inlineCode', { code: 'var x = "Strawberry Pasta";' })
]);
});
it('disallow line break', () => {
const tokens = analyze('`foo\nbar`');
const tokens = parse('`foo\nbar`');
assert.deepStrictEqual(tokens, [
text('`foo\nbar`')
]);
});
it('disallow ´', () => {
const tokens = analyze('`foo´bar`');
const tokens = parse('`foo´bar`');
assert.deepStrictEqual(tokens, [
text('`foo´bar`')
]);
@@ -928,7 +944,7 @@ describe('MFM', () => {
it('mathInline', () => {
const fomula = 'x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}';
const content = `\\(${fomula}\\)`;
const tokens = analyze(content);
const tokens = parse(content);
assert.deepStrictEqual(tokens, [
leaf('mathInline', { formula: fomula })
]);
@@ -938,7 +954,7 @@ describe('MFM', () => {
it('simple', () => {
const fomula = 'x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}';
const content = `\\[\n${fomula}\n\\]`;
const tokens = analyze(content);
const tokens = parse(content);
assert.deepStrictEqual(tokens, [
leaf('mathBlock', { formula: fomula })
]);
@@ -946,22 +962,22 @@ describe('MFM', () => {
});
it('search', () => {
const tokens1 = analyze('a b c 検索');
const tokens1 = parse('a b c 検索');
assert.deepStrictEqual(tokens1, [
leaf('search', { content: 'a b c 検索', query: 'a b c' })
]);
const tokens2 = analyze('a b c Search');
const tokens2 = parse('a b c Search');
assert.deepStrictEqual(tokens2, [
leaf('search', { content: 'a b c Search', query: 'a b c' })
]);
const tokens3 = analyze('a b c search');
const tokens3 = parse('a b c search');
assert.deepStrictEqual(tokens3, [
leaf('search', { content: 'a b c search', query: 'a b c' })
]);
const tokens4 = analyze('a b c SEARCH');
const tokens4 = parse('a b c SEARCH');
assert.deepStrictEqual(tokens4, [
leaf('search', { content: 'a b c SEARCH', query: 'a b c' })
]);
@@ -969,7 +985,7 @@ describe('MFM', () => {
describe('title', () => {
it('simple', () => {
const tokens = analyze('【foo】');
const tokens = parse('【foo】');
assert.deepStrictEqual(tokens, [
tree('title', [
text('foo')
@@ -978,14 +994,14 @@ describe('MFM', () => {
});
it('require line break', () => {
const tokens = analyze('a【foo】');
const tokens = parse('a【foo】');
assert.deepStrictEqual(tokens, [
text('a【foo】')
]);
});
it('with before and after texts', () => {
const tokens = analyze('before\n【foo】\nafter');
const tokens = parse('before\n【foo】\nafter');
assert.deepStrictEqual(tokens, [
text('before\n'),
tree('title', [
@@ -996,14 +1012,14 @@ describe('MFM', () => {
});
it('ignore multiple title blocks', () => {
const tokens = analyze('【foo】bar【baz】');
const tokens = parse('【foo】bar【baz】');
assert.deepStrictEqual(tokens, [
text('【foo】bar【baz】')
]);
});
it('disallow linebreak in title', () => {
const tokens = analyze('【foo\nbar】');
const tokens = parse('【foo\nbar】');
assert.deepStrictEqual(tokens, [
text('【foo\nbar】')
]);
@@ -1012,7 +1028,7 @@ describe('MFM', () => {
describe('center', () => {
it('simple', () => {
const tokens = analyze('<center>foo</center>');
const tokens = parse('<center>foo</center>');
assert.deepStrictEqual(tokens, [
tree('center', [
text('foo')
@@ -1023,7 +1039,7 @@ describe('MFM', () => {
describe('strike', () => {
it('simple', () => {
const tokens = analyze('~~foo~~');
const tokens = parse('~~foo~~');
assert.deepStrictEqual(tokens, [
tree('strike', [
text('foo')
@@ -1034,7 +1050,7 @@ describe('MFM', () => {
describe('italic', () => {
it('<i>', () => {
const tokens = analyze('<i>foo</i>');
const tokens = parse('<i>foo</i>');
assert.deepStrictEqual(tokens, [
tree('italic', [
text('foo')
@@ -1043,7 +1059,7 @@ describe('MFM', () => {
});
it('underscore', () => {
const tokens = analyze('_foo_');
const tokens = parse('_foo_');
assert.deepStrictEqual(tokens, [
tree('italic', [
text('foo')
@@ -1052,7 +1068,7 @@ describe('MFM', () => {
});
it('simple with asterix', () => {
const tokens = analyze('*foo*');
const tokens = parse('*foo*');
assert.deepStrictEqual(tokens, [
tree('italic', [
text('foo')
@@ -1061,28 +1077,28 @@ describe('MFM', () => {
});
it('exlude emotes', () => {
const tokens = analyze('*.*');
const tokens = parse('*.*');
assert.deepStrictEqual(tokens, [
text("*.*"),
]);
});
it('mixed', () => {
const tokens = analyze('_foo*');
const tokens = parse('_foo*');
assert.deepStrictEqual(tokens, [
text('_foo*'),
]);
});
it('mixed', () => {
const tokens = analyze('*foo_');
const tokens = parse('*foo_');
assert.deepStrictEqual(tokens, [
text('*foo_'),
]);
});
it('ignore snake_case string', () => {
const tokens = analyze('foo_bar_baz');
const tokens = parse('foo_bar_baz');
assert.deepStrictEqual(tokens, [
text('foo_bar_baz'),
]);
@@ -1090,16 +1106,54 @@ describe('MFM', () => {
});
});
describe('plainText', () => {
it('text', () => {
const tokens = parsePlain('foo');
assert.deepStrictEqual(tokens, [
text('foo'),
]);
});
it('emoji', () => {
const tokens = parsePlain(':foo:');
assert.deepStrictEqual(tokens, [
leaf('emoji', { name: 'foo' })
]);
});
it('emoji in text', () => {
const tokens = parsePlain('foo:bar:baz');
assert.deepStrictEqual(tokens, [
text('foo'),
leaf('emoji', { name: 'bar' }),
text('baz'),
]);
});
it('disallow other syntax', () => {
const tokens = parsePlain('foo **bar** baz');
assert.deepStrictEqual(tokens, [
text('foo **bar** baz'),
]);
});
});
describe('toHtml', () => {
it('br', () => {
const input = 'foo\nbar\nbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(analyze(input)), output);
assert.equal(toHtml(parse(input)), output);
});
it('br alt', () => {
const input = 'foo\r\nbar\rbaz';
const output = '<p><span>foo<br>bar<br>baz</span></p>';
assert.equal(toHtml(parse(input)), output);
});
});
it('code block with quote', () => {
const tokens = analyze('> foo\n```\nbar\n```');
const tokens = parse('> foo\n```\nbar\n```');
assert.deepStrictEqual(tokens, [
tree('quote', [
text('foo')
@@ -1109,7 +1163,7 @@ describe('MFM', () => {
});
it('quote between two code blocks', () => {
const tokens = analyze('```\nbefore\n```\n> foo\n```\nafter\n```');
const tokens = parse('```\nbefore\n```\n> foo\n```\nafter\n```');
assert.deepStrictEqual(tokens, [
leaf('blockCode', { code: 'before', lang: null }),
tree('quote', [

View File

@@ -15,7 +15,8 @@
"noLib": false,
"strict": true,
"strictNullChecks": false,
"experimentalDecorators": true
"experimentalDecorators": true,
"resolveJsonModule": true
},
"compileOnSave": false,
"include": [