summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-04-02 10:36:11 +0900
committerGitHub <noreply@github.com>2021-04-02 10:36:11 +0900
commit1f4ae2f63a609d51942daa23772439379496064e (patch)
tree55e67e216bd30b8a5c95f769e8696c4b885d1e61 /src
parenttweak avatar generation (diff)
downloadmisskey-1f4ae2f63a609d51942daa23772439379496064e.tar.gz
misskey-1f4ae2f63a609d51942daa23772439379496064e.tar.bz2
misskey-1f4ae2f63a609d51942daa23772439379496064e.zip
Use mfm-js for MFM parsing (#7415)
* wip * Update mfm.ts * wip * update mfmjs * refactor * nanka * Update mfm.ts * Update to-html.ts * Update to-html.ts * wip * fix test * fix test
Diffstat (limited to 'src')
-rw-r--r--src/client/components/mfm.ts89
-rw-r--r--src/client/components/note-detailed.vue23
-rw-r--r--src/client/components/note.vue23
-rw-r--r--src/client/components/page/page.text.vue10
-rw-r--r--src/client/components/post-form.vue6
-rw-r--r--src/client/pages/about-misskey.vue1
-rw-r--r--src/client/pages/messaging/messaging-room.message.vue9
-rw-r--r--src/client/ui/chat/note.vue23
-rw-r--r--src/client/ui/chat/post-form.vue6
-rw-r--r--src/mfm/from-html.ts4
-rw-r--r--src/mfm/language.ts191
-rw-r--r--src/mfm/normalize.ts31
-rw-r--r--src/mfm/parse.ts19
-rw-r--r--src/mfm/prelude.ts40
-rw-r--r--src/mfm/to-html.ts102
-rw-r--r--src/mfm/to-string.ts99
-rw-r--r--src/misc/extract-custom-emojis-from-mfm.ts18
-rw-r--r--src/misc/extract-emojis.ts9
-rw-r--r--src/misc/extract-hashtags.ts19
-rw-r--r--src/misc/extract-mentions.ts17
-rw-r--r--src/misc/extract-url-from-mfm.ts34
-rw-r--r--src/models/repositories/note.ts13
-rw-r--r--src/prelude/tree.ts36
-rw-r--r--src/remote/activitypub/misc/get-note-html.ts4
-rw-r--r--src/remote/activitypub/renderer/person.ts4
-rw-r--r--src/server/api/endpoints/i/update.ts12
-rw-r--r--src/services/note/create.ts14
27 files changed, 238 insertions, 618 deletions
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index 28ac9b8942..b8e948a188 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -1,6 +1,5 @@
import { VNode, defineComponent, h } from 'vue';
-import { MfmForest } from '@client/../mfm/prelude';
-import { parse, parsePlain } from '@client/../mfm/parse';
+import * as mfm from 'mfm-js';
import MkUrl from '@client/components/global/url.vue';
import MkLink from '@client/components/link.vue';
import MkMention from '@client/components/mention.vue';
@@ -46,17 +45,17 @@ export default defineComponent({
render() {
if (this.text == null || this.text == '') return;
- const ast = (this.plain ? parsePlain : parse)(this.text);
+ const ast = (this.plain ? mfm.parsePlain : mfm.parse)(this.text);
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
- const genEl = (ast: MfmForest) => concat(ast.map((token): VNode[] => {
- switch (token.node.type) {
+ const genEl = (ast: mfm.MfmNode[]) => concat(ast.map((token): VNode[] => {
+ switch (token.type) {
case 'text': {
- const text = token.node.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+ const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!this.plain) {
const x = text.split('\n')
@@ -83,38 +82,38 @@ export default defineComponent({
}
case 'fn': {
- // TODO: CSSを文字列で組み立てていくと token.node.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+ // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
let style;
- switch (token.node.props.name) {
+ switch (token.props.name) {
case 'tada': {
style = `font-size: 150%;` + (this.$store.state.animatedMfm ? 'animation: tada 1s linear infinite both;' : '');
break;
}
case 'jelly': {
- const speed = validTime(token.node.props.args.speed) || '1s';
+ const speed = validTime(token.props.args.speed) || '1s';
style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
break;
}
case 'twitch': {
- const speed = validTime(token.node.props.args.speed) || '0.5s';
+ const speed = validTime(token.props.args.speed) || '0.5s';
style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : '';
break;
}
case 'shake': {
- const speed = validTime(token.node.props.args.speed) || '0.5s';
+ const speed = validTime(token.props.args.speed) || '0.5s';
style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : '';
break;
}
case 'spin': {
const direction =
- token.node.props.args.left ? 'reverse' :
- token.node.props.args.alternate ? 'alternate' :
+ token.props.args.left ? 'reverse' :
+ token.props.args.alternate ? 'alternate' :
'normal';
const anime =
- token.node.props.args.x ? 'mfm-spinX' :
- token.node.props.args.y ? 'mfm-spinY' :
+ token.props.args.x ? 'mfm-spinX' :
+ token.props.args.y ? 'mfm-spinY' :
'mfm-spin';
- const speed = validTime(token.node.props.args.speed) || '1.5s';
+ const speed = validTime(token.props.args.speed) || '1.5s';
style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
break;
}
@@ -128,8 +127,8 @@ export default defineComponent({
}
case 'flip': {
const transform =
- (token.node.props.args.h && token.node.props.args.v) ? 'scale(-1, -1)' :
- token.node.props.args.v ? 'scaleY(-1)' :
+ (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
+ token.props.args.v ? 'scaleY(-1)' :
'scaleX(-1)';
style = `transform: ${transform};`;
break;
@@ -148,12 +147,12 @@ export default defineComponent({
}
case 'font': {
const family =
- token.node.props.args.serif ? 'serif' :
- token.node.props.args.monospace ? 'monospace' :
- token.node.props.args.cursive ? 'cursive' :
- token.node.props.args.fantasy ? 'fantasy' :
- token.node.props.args.emoji ? 'emoji' :
- token.node.props.args.math ? 'math' :
+ token.props.args.serif ? 'serif' :
+ token.props.args.monospace ? 'monospace' :
+ token.props.args.cursive ? 'cursive' :
+ token.props.args.fantasy ? 'fantasy' :
+ token.props.args.emoji ? 'emoji' :
+ token.props.args.math ? 'math' :
null;
if (family) style = `font-family: ${family};`;
break;
@@ -165,7 +164,7 @@ export default defineComponent({
}
}
if (style == null) {
- return h('span', {}, ['[', token.node.props.name, ...genEl(token.children), ']']);
+ return h('span', {}, ['[', token.props.name, ...genEl(token.children), ']']);
} else {
return h('span', {
style: 'display: inline-block;' + style,
@@ -188,7 +187,7 @@ export default defineComponent({
case 'url': {
return [h(MkUrl, {
key: Math.random(),
- url: token.node.props.url,
+ url: token.props.url,
rel: 'nofollow noopener',
})];
}
@@ -196,7 +195,7 @@ export default defineComponent({
case 'link': {
return [h(MkLink, {
key: Math.random(),
- url: token.node.props.url,
+ url: token.props.url,
rel: 'nofollow noopener',
}, genEl(token.children))];
}
@@ -204,32 +203,31 @@ export default defineComponent({
case 'mention': {
return [h(MkMention, {
key: Math.random(),
- host: (token.node.props.host == null && this.author && this.author.host != null ? this.author.host : token.node.props.host) || host,
- username: token.node.props.username
+ host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
+ username: token.props.username
})];
}
case 'hashtag': {
return [h(MkA, {
key: Math.random(),
- to: this.isNote ? `/tags/${encodeURIComponent(token.node.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.node.props.hashtag)}`,
+ to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/explore/tags/${encodeURIComponent(token.props.hashtag)}`,
style: 'color:var(--hashtag);'
- }, `#${token.node.props.hashtag}`)];
+ }, `#${token.props.hashtag}`)];
}
case 'blockCode': {
return [h(MkCode, {
key: Math.random(),
- code: token.node.props.code,
- lang: token.node.props.lang,
+ code: token.props.code,
+ lang: token.props.lang,
})];
}
case 'inlineCode': {
return [h(MkCode, {
key: Math.random(),
- code: token.node.props.code,
- lang: token.node.props.lang,
+ code: token.props.code,
inline: true
})];
}
@@ -246,10 +244,19 @@ export default defineComponent({
}
}
- case 'emoji': {
+ case 'emojiCode': {
return [h(MkEmoji, {
key: Math.random(),
- emoji: token.node.props.name ? `:${token.node.props.name}:` : token.node.props.emoji,
+ emoji: `:${token.props.name}:`,
+ customEmojis: this.customEmojis,
+ normal: this.plain
+ })];
+ }
+
+ case 'unicodeEmoji': {
+ return [h(MkEmoji, {
+ key: Math.random(),
+ emoji: token.props.emoji,
customEmojis: this.customEmojis,
normal: this.plain
})];
@@ -258,7 +265,7 @@ export default defineComponent({
case 'mathInline': {
return [h(MkFormula, {
key: Math.random(),
- formula: token.node.props.formula,
+ formula: token.props.formula,
block: false
})];
}
@@ -266,7 +273,7 @@ export default defineComponent({
case 'mathBlock': {
return [h(MkFormula, {
key: Math.random(),
- formula: token.node.props.formula,
+ formula: token.props.formula,
block: true
})];
}
@@ -274,12 +281,12 @@ export default defineComponent({
case 'search': {
return [h(MkGoogle, {
key: Math.random(),
- q: token.node.props.query
+ q: token.props.query
})];
}
default: {
- console.error('unrecognized ast type:', token.node.type);
+ console.error('unrecognized ast type:', token.type);
return [];
}
diff --git a/src/client/components/note-detailed.vue b/src/client/components/note-detailed.vue
index fb4f9502b3..5124b2a88c 100644
--- a/src/client/components/note-detailed.vue
+++ b/src/client/components/note-detailed.vue
@@ -120,11 +120,11 @@
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
-import { parse } from '../../mfm/parse';
-import { sum, unique } from '../../prelude/array';
+import * as mfm from 'mfm-js';
+import { sum } from '../../prelude/array';
import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue';
import XNotePreview from './note-preview.vue';
@@ -141,6 +141,7 @@ import { userPage } from '@client/filters/user';
import * as os from '@client/os';
import { noteActions, noteViewInterruptors } from '@client/store';
import { reactionPicker } from '@client/scripts/reaction-picker';
+import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
function markRawAll(...xs) {
for (const x of xs) {
@@ -252,21 +253,7 @@ export default defineComponent({
urls(): string[] {
if (this.appearNote.text) {
- const ast = parse(this.appearNote.text);
- // TODO: 再帰的にURL要素がないか調べる
- const urls = unique(ast
- .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
- .map(t => t.node.props.url));
-
- // unique without hash
- // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
- const removeHash = x => x.replace(/#[^#]*$/, '');
-
- return urls.reduce((array, url) => {
- const removed = removeHash(url);
- if (!array.map(x => removeHash(x)).includes(removed)) array.push(url);
- return array;
- }, []);
+ return extractUrlFromMfm(mfm.parse(this.appearNote.text));
} else {
return null;
}
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index b54cadfc80..a656ffc356 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -102,11 +102,11 @@
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
-import { parse } from '../../mfm/parse';
-import { sum, unique } from '../../prelude/array';
+import * as mfm from 'mfm-js';
+import { sum } from '../../prelude/array';
import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue';
import XNotePreview from './note-preview.vue';
@@ -123,6 +123,7 @@ import { userPage } from '@client/filters/user';
import * as os from '@client/os';
import { noteActions, noteViewInterruptors } from '@client/store';
import { reactionPicker } from '@client/scripts/reaction-picker';
+import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
function markRawAll(...xs) {
for (const x of xs) {
@@ -238,21 +239,7 @@ export default defineComponent({
urls(): string[] {
if (this.appearNote.text) {
- const ast = parse(this.appearNote.text);
- // TODO: 再帰的にURL要素がないか調べる
- const urls = unique(ast
- .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
- .map(t => t.node.props.url));
-
- // unique without hash
- // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
- const removeHash = x => x.replace(/#[^#]*$/, '');
-
- return urls.reduce((array, url) => {
- const removed = removeHash(url);
- if (!array.map(x => removeHash(x)).includes(removed)) array.push(url);
- return array;
- }, []);
+ return extractUrlFromMfm(mfm.parse(this.appearNote.text));
} else {
return null;
}
diff --git a/src/client/components/page/page.text.vue b/src/client/components/page/page.text.vue
index 491c62be26..580c5a93bf 100644
--- a/src/client/components/page/page.text.vue
+++ b/src/client/components/page/page.text.vue
@@ -9,8 +9,8 @@
import { TextBlock } from '@client/scripts/hpml/block';
import { Hpml } from '@client/scripts/hpml/evaluator';
import { defineAsyncComponent, defineComponent, PropType } from 'vue';
-import { parse } from '../../../mfm/parse';
-import { unique } from '../../../prelude/array';
+import * as mfm from 'mfm-js';
+import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
export default defineComponent({
components: {
@@ -34,11 +34,7 @@ export default defineComponent({
computed: {
urls(): string[] {
if (this.text) {
- const ast = parse(this.text);
- // TODO: 再帰的にURL要素がないか調べる
- return unique(ast
- .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
- .map(t => t.node.props.url));
+ return extractUrlFromMfm(mfm.parse(this.text));
} else {
return [];
}
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 7d2355c190..13e5c0f433 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -58,7 +58,7 @@ import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
import { toASCII } from 'punycode';
import XNotePreview from './note-preview.vue';
-import { parse } from '../../mfm/parse';
+import * as mfm from 'mfm-js';
import { host, url } from '@client/config';
import { erase, unique } from '../../prelude/array';
import extractMentions from '@/misc/extract-mentions';
@@ -229,7 +229,7 @@ export default defineComponent({
}
if (this.reply && this.reply.text != null) {
- const ast = parse(this.reply.text);
+ const ast = mfm.parse(this.reply.text);
for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
@@ -580,7 +580,7 @@ export default defineComponent({
this.deleteDraft();
this.$emit('posted');
if (this.text && this.text != '') {
- const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
+ const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
diff --git a/src/client/pages/about-misskey.vue b/src/client/pages/about-misskey.vue
index e9e2e15573..72b94968df 100644
--- a/src/client/pages/about-misskey.vue
+++ b/src/client/pages/about-misskey.vue
@@ -40,6 +40,7 @@
<FormLink to="https://github.com/rinsuki" external>@rinsuki</FormLink>
<FormLink to="https://github.com/Xeltica" external>@Xeltica</FormLink>
<FormLink to="https://github.com/u1-liquid" external>@u1-liquid</FormLink>
+ <FormLink to="https://github.com/marihachi" external>@marihachi</FormLink>
<template #caption><MkLink url="https://github.com/misskey-dev/misskey/graphs/contributors">{{ $ts._aboutMisskey.allContributors }}</MkLink></template>
</FormGroup>
<FormGroup>
diff --git a/src/client/pages/messaging/messaging-room.message.vue b/src/client/pages/messaging/messaging-room.message.vue
index a6d142bd34..3755bc2b5c 100644
--- a/src/client/pages/messaging/messaging-room.message.vue
+++ b/src/client/pages/messaging/messaging-room.message.vue
@@ -37,8 +37,8 @@
<script lang="ts">
import { defineComponent } from 'vue';
-import { parse } from '../../../mfm/parse';
-import { unique } from '../../../prelude/array';
+import * as mfm from 'mfm-js';
+import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
import MkUrlPreview from '@client/components/url-preview.vue';
import * as os from '@client/os';
@@ -60,10 +60,7 @@ export default defineComponent({
},
urls(): string[] {
if (this.message.text) {
- const ast = parse(this.message.text);
- return unique(ast
- .filter(t => ((t.node.type === 'url' || t.node.type === 'link') && t.node.props.url && !t.node.props.silent))
- .map(t => t.node.props.url));
+ return extractUrlFromMfm(mfm.parse(this.message.text));
} else {
return [];
}
diff --git a/src/client/ui/chat/note.vue b/src/client/ui/chat/note.vue
index 4afd7989e1..f6789f214d 100644
--- a/src/client/ui/chat/note.vue
+++ b/src/client/ui/chat/note.vue
@@ -101,11 +101,11 @@
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue';
+import { defineAsyncComponent, defineComponent, markRaw } from 'vue';
import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
-import { parse } from '../../../mfm/parse';
-import { sum, unique } from '../../../prelude/array';
+import * as mfm from 'mfm-js';
+import { sum } from '../../../prelude/array';
import XSub from './note.sub.vue';
import XNoteHeader from './note-header.vue';
import XNotePreview from './note-preview.vue';
@@ -122,6 +122,7 @@ import { userPage } from '@client/filters/user';
import * as os from '@client/os';
import { noteActions, noteViewInterruptors } from '@client/store';
import { reactionPicker } from '@client/scripts/reaction-picker';
+import { extractUrlFromMfm } from '@/misc/extract-url-from-mfm';
function markRawAll(...xs) {
for (const x of xs) {
@@ -238,21 +239,7 @@ export default defineComponent({
urls(): string[] {
if (this.appearNote.text) {
- const ast = parse(this.appearNote.text);
- // TODO: 再帰的にURL要素がないか調べる
- const urls = unique(ast
- .filter(t => ((t.node.type == 'url' || t.node.type == 'link') && t.node.props.url && !t.node.props.silent))
- .map(t => t.node.props.url));
-
- // unique without hash
- // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
- const removeHash = x => x.replace(/#[^#]*$/, '');
-
- return urls.reduce((array, url) => {
- const removed = removeHash(url);
- if (!array.map(x => removeHash(x)).includes(removed)) array.push(url);
- return array;
- }, []);
+ return extractUrlFromMfm(mfm.parse(this.appearNote.text));
} else {
return null;
}
diff --git a/src/client/ui/chat/post-form.vue b/src/client/ui/chat/post-form.vue
index 5bb1a04d58..e5f4132c4b 100644
--- a/src/client/ui/chat/post-form.vue
+++ b/src/client/ui/chat/post-form.vue
@@ -53,7 +53,7 @@ import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
import { toASCII } from 'punycode';
-import { parse } from '../../../mfm/parse';
+import * as mfm from 'mfm-js';
import { host, url } from '@client/config';
import { erase, unique } from '../../../prelude/array';
import extractMentions from '@/misc/extract-mentions';
@@ -216,7 +216,7 @@ export default defineComponent({
}
if (this.reply && this.reply.text != null) {
- const ast = parse(this.reply.text);
+ const ast = mfm.parse(this.reply.text);
for (const x of extractMentions(ast)) {
const mention = x.host ? `@${x.username}@${toASCII(x.host)}` : `@${x.username}`;
@@ -567,7 +567,7 @@ export default defineComponent({
this.deleteDraft();
this.$emit('posted');
if (this.text && this.text != '') {
- const hashtags = parse(this.text).filter(x => x.node.type === 'hashtag').map(x => x.node.props.hashtag);
+ const hashtags = mfm.parse(this.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
const history = JSON.parse(localStorage.getItem('hashtags') || '[]') as string[];
localStorage.setItem('hashtags', JSON.stringify(unique(hashtags.concat(history))));
}
diff --git a/src/mfm/from-html.ts b/src/mfm/from-html.ts
index 0b4f9b8945..4c8e2dbec8 100644
--- a/src/mfm/from-html.ts
+++ b/src/mfm/from-html.ts
@@ -1,7 +1,9 @@
import * as parse5 from 'parse5';
import treeAdapter = require('parse5/lib/tree-adapters/default');
import { URL } from 'url';
-import { urlRegex, urlRegexFull } from './prelude';
+
+const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
+const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
export function fromHtml(html: string, hashtagNames?: string[]): string {
const dom = parse5.parseFragment(html);
diff --git a/src/mfm/language.ts b/src/mfm/language.ts
deleted file mode 100644
index bad7b10a0d..0000000000
--- a/src/mfm/language.ts
+++ /dev/null
@@ -1,191 +0,0 @@
-import * as P from 'parsimmon';
-import { createLeaf, createTree, urlRegex } from './prelude';
-import { takeWhile, cumulativeSum } from '../prelude/array';
-import parseAcct from '@/misc/acct/parse';
-import { toUnicode } from 'punycode';
-import { emojiRegex } from '@/misc/emoji-regex';
-
-export function removeOrphanedBrackets(s: string): string {
- const openBrackets = ['(', '「', '['];
- const closeBrackets = [')', '」', ']'];
- const xs = cumulativeSum(s.split('').map(c => {
- if (openBrackets.includes(c)) return 1;
- if (closeBrackets.includes(c)) return -1;
- return 0;
- }));
- const firstOrphanedCloseBracket = xs.findIndex(x => x < 0);
- if (firstOrphanedCloseBracket !== -1) return s.substr(0, firstOrphanedCloseBracket);
- const lastMatched = xs.lastIndexOf(0);
- return s.substr(0, lastMatched + 1);
-}
-
-export const mfmLanguage = P.createLanguage({
- root: r => P.alt(r.block, r.inline).atLeast(1),
- plain: r => P.alt(r.emoji, r.text).atLeast(1),
- block: r => P.alt(
- r.quote,
- r.search,
- r.blockCode,
- r.mathBlock,
- r.center,
- ),
- startOfLine: () => P((input, i) => {
- if (i === 0 || input[i] === '\n' || input[i - 1] === '\n') {
- return P.makeSuccess(i, null);
- } else {
- return P.makeFailure(i, 'not newline');
- }
- }),
- quote: r => r.startOfLine.then(P((input, i) => {
- const text = input.substr(i);
- if (!text.match(/^>[\s\S]+?/)) return P.makeFailure(i, 'not a quote');
- const quote = takeWhile(line => line.startsWith('>'), text.split('\n'));
- const qInner = quote.join('\n').replace(/^>/gm, '').replace(/^ /gm, '');
- if (qInner === '') return P.makeFailure(i, 'not a quote');
- const contents = r.root.tryParse(qInner);
- return P.makeSuccess(i + quote.join('\n').length + 1, createTree('quote', contents, {}));
- })),
- search: r => r.startOfLine.then(P((input, i) => {
- const text = input.substr(i);
- const match = text.match(/^(.+?)( | )(検索|\[検索\]|Search|\[Search\])(\n|$)/i);
- if (!match) return P.makeFailure(i, 'not a search');
- return P.makeSuccess(i + match[0].length, createLeaf('search', { query: match[1], content: match[0].trim() }));
- })),
- blockCode: r => r.startOfLine.then(P((input, i) => {
- const text = input.substr(i);
- const match = text.match(/^```(.+?)?\n([\s\S]+?)\n```(\n|$)/i);
- if (!match) return P.makeFailure(i, 'not a blockCode');
- return P.makeSuccess(i + match[0].length, createLeaf('blockCode', { code: match[2], lang: match[1] ? match[1].trim() : null }));
- })),
- inline: r => P.alt(
- r.big,
- r.bold,
- r.small,
- r.italic,
- r.strike,
- r.inlineCode,
- r.mathInline,
- r.mention,
- r.hashtag,
- r.url,
- r.link,
- r.emoji,
- r.fn,
- r.text
- ),
- // TODO: そのうち消す
- big: r => P.regexp(/^\*\*\*([\s\S]+?)\*\*\*/, 1).map(x => createTree('fn', r.inline.atLeast(1).tryParse(x), {
- name: 'tada',
- args: {}
- })),
- bold: r => {
- const asterisk = P.regexp(/\*\*([\s\S]+?)\*\*/, 1);
- const underscore = P.regexp(/__([a-zA-Z0-9\s]+?)__/, 1);
- return P.alt(asterisk, underscore).map(x => createTree('bold', r.inline.atLeast(1).tryParse(x), {}));
- },
- small: r => P.regexp(/<small>([\s\S]+?)<\/small>/, 1).map(x => createTree('small', r.inline.atLeast(1).tryParse(x), {})),
- italic: r => {
- const xml = P.regexp(/<i>([\s\S]+?)<\/i>/, 1);
- const underscore = P((input, i) => {
- const text = input.substr(i);
- const match = text.match(/^(\*|_)([a-zA-Z0-9]+?[\s\S]*?)\1/);
- if (!match) return P.makeFailure(i, 'not a italic');
- if (input[i - 1] != null && input[i - 1] != ' ' && input[i - 1] != '\n') return P.makeFailure(i, 'not a italic');
- return P.makeSuccess(i + match[0].length, match[2]);
- });
-
- return P.alt(xml, underscore).map(x => createTree('italic', r.inline.atLeast(1).tryParse(x), {}));
- },
- strike: r => P.regexp(/~~([^\n~]+?)~~/, 1).map(x => createTree('strike', r.inline.atLeast(1).tryParse(x), {})),
- center: r => r.startOfLine.then(P.regexp(/<center>([\s\S]+?)<\/center>/, 1).map(x => createTree('center', r.inline.atLeast(1).tryParse(x), {}))),
- inlineCode: () => P.regexp(/`([^´\n]+?)`/, 1).map(x => createLeaf('inlineCode', { code: x })),
- mathBlock: r => r.startOfLine.then(P.regexp(/\\\[([\s\S]+?)\\\]/, 1).map(x => createLeaf('mathBlock', { formula: x.trim() }))),
- mathInline: () => P.regexp(/\\\((.+?)\\\)/, 1).map(x => createLeaf('mathInline', { formula: x })),
- mention: () => {
- return P((input, i) => {
- const text = input.substr(i);
- const match = text.match(/^@\w([\w-]*\w)?(?:@[\w.\-]+\w)?/);
- if (!match) return P.makeFailure(i, 'not a mention');
- if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a mention');
- return P.makeSuccess(i + match[0].length, match[0]);
- }).map(x => {
- const { username, host } = parseAcct(x.substr(1));
- const canonical = host != null ? `@${username}@${toUnicode(host)}` : x;
- return createLeaf('mention', { canonical, username, host, acct: x });
- });
- },
- hashtag: () => P((input, i) => {
- const text = input.substr(i);
- const match = text.match(/^#([^\s.,!?'"#:\/\[\]【】]+)/i);
- if (!match) return P.makeFailure(i, 'not a hashtag');
- let hashtag = match[1];
- hashtag = removeOrphanedBrackets(hashtag);
- if (hashtag.match(/^(\u20e3|\ufe0f)/)) return P.makeFailure(i, 'not a hashtag');
- if (hashtag.match(/^[0-9]+$/)) return P.makeFailure(i, 'not a hashtag');
- if (input[i - 1] != null && input[i - 1].match(/[a-z0-9]/i)) return P.makeFailure(i, 'not a hashtag');
- if (Array.from(hashtag || '').length > 128) return P.makeFailure(i, 'not a hashtag');
- return P.makeSuccess(i + ('#' + hashtag).length, createLeaf('hashtag', { hashtag: hashtag }));
- }),
- url: () => {
- return P((input, i) => {
- const text = input.substr(i);
- const match = text.match(urlRegex);
- let url: string;
- if (!match) {
- const match = text.match(/^<(https?:\/\/.*?)>/);
- if (!match) {
- return P.makeFailure(i, 'not a url');
- }
- url = match[1];
- i += 2;
- } else {
- url = match[0];
- }
- url = removeOrphanedBrackets(url);
- url = url.replace(/[.,]*$/, '');
- return P.makeSuccess(i + url.length, url);
- }).map(x => createLeaf('url', { url: x }));
- },
- link: r => {
- return P.seqObj(
- ['silent', P.string('?').fallback(null).map(x => x != null)] as any,
- P.string('['), ['text', P.regexp(/[^\n\[\]]+/)] as any, P.string(']'),
- P.string('('), ['url', r.url] as any, P.string(')'),
- ).map((x: any) => {
- return createTree('link', r.inline.atLeast(1).tryParse(x.text), {
- silent: x.silent,
- url: x.url.node.props.url
- });
- });
- },
- emoji: () => {
- const name = P.regexp(/:([a-z0-9_+-]+):/i, 1).map(x => createLeaf('emoji', { name: x }));
- const code = P.regexp(emojiRegex).map(x => createLeaf('emoji', { emoji: x }));
- return P.alt(name, code);
- },
- fn: r => {
- return P.seqObj(
- P.string('['), ['fn', P.regexp(/[^\s\n\[\]]+/)] as any, P.string(' '), P.optWhitespace, ['text', P.regexp(/[^\n\[\]]+/)] as any, P.string(']'),
- ).map((x: any) => {
- let name = x.fn;
- const args = {};
- const separator = x.fn.indexOf('.');
- if (separator > -1) {
- name = x.fn.substr(0, separator);
- for (const arg of x.fn.substr(separator + 1).split(',')) {
- const kv = arg.split('=');
- if (kv.length === 1) {
- args[kv[0]] = true;
- } else {
- args[kv[0]] = kv[1];
- }
- }
- }
- return createTree('fn', r.inline.atLeast(1).tryParse(x.text), {
- name,
- args
- });
- });
- },
- text: () => P.any.map(x => createLeaf('text', { text: x }))
-});
diff --git a/src/mfm/normalize.ts b/src/mfm/normalize.ts
deleted file mode 100644
index a0f0702096..0000000000
--- a/src/mfm/normalize.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as A from '../prelude/array';
-import * as S from '../prelude/string';
-import { MfmForest, MfmTree } from './prelude';
-import { createTree, createLeaf } from '../prelude/tree';
-
-function isEmptyTextTree(t: MfmTree): boolean {
- return t.node.type === 'text' && t.node.props.text === '';
-}
-
-function concatTextTrees(ts: MfmForest): MfmTree {
- return createLeaf({ type: 'text', props: { text: S.concat(ts.map(x => x.node.props.text)) } });
-}
-
-function concatIfTextTrees(ts: MfmForest): MfmForest {
- return ts[0].node.type === 'text' ? [concatTextTrees(ts)] : ts;
-}
-
-function concatConsecutiveTextTrees(ts: MfmForest): MfmForest {
- const us = A.concat(A.groupOn(t => t.node.type, ts).map(concatIfTextTrees));
- return us.map(t => createTree(t.node, concatConsecutiveTextTrees(t.children)));
-}
-
-function removeEmptyTextNodes(ts: MfmForest): MfmForest {
- return ts
- .filter(t => !isEmptyTextTree(t))
- .map(t => createTree(t.node, removeEmptyTextNodes(t.children)));
-}
-
-export function normalize(ts: MfmForest): MfmForest {
- return removeEmptyTextNodes(concatConsecutiveTextTrees(ts));
-}
diff --git a/src/mfm/parse.ts b/src/mfm/parse.ts
deleted file mode 100644
index c628042f12..0000000000
--- a/src/mfm/parse.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { mfmLanguage } from './language';
-import { MfmForest } from './prelude';
-import { normalize } from './normalize';
-
-export function parse(source: string | null): MfmForest | null {
- if (source == null || source === '') {
- return null;
- }
-
- return normalize(mfmLanguage.root.tryParse(source));
-}
-
-export function parsePlain(source: string | null): MfmForest | null {
- if (source == null || source === '') {
- return null;
- }
-
- return normalize(mfmLanguage.plain.tryParse(source));
-}
diff --git a/src/mfm/prelude.ts b/src/mfm/prelude.ts
deleted file mode 100644
index a8b52eb315..0000000000
--- a/src/mfm/prelude.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { Tree } from '../prelude/tree';
-import * as T from '../prelude/tree';
-
-type Node<T, P> = { type: T, props: P };
-
-export type MentionNode = Node<'mention', {
- canonical: string,
- username: string,
- host: string,
- acct: string
-}>;
-
-export type HashtagNode = Node<'hashtag', {
- hashtag: string
-}>;
-
-export type EmojiNode = Node<'emoji', {
- name: string
-}>;
-
-export type MfmNode =
- MentionNode |
- HashtagNode |
- EmojiNode |
- Node<string, any>;
-
-export type MfmTree = Tree<MfmNode>;
-
-export type MfmForest = MfmTree[];
-
-export function createLeaf(type: string, props: any): MfmTree {
- return T.createLeaf({ type, props });
-}
-
-export function createTree(type: string, children: MfmForest, props: any): MfmTree {
- return T.createTree({ type, props }, children);
-}
-
-export const urlRegex = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+/;
-export const urlRegexFull = /^https?:\/\/[\w\/:%#@$&?!()\[\]~.,=+\-]+$/;
diff --git a/src/mfm/to-html.ts b/src/mfm/to-html.ts
index 66015d539f..aa39443c64 100644
--- a/src/mfm/to-html.ts
+++ b/src/mfm/to-html.ts
@@ -1,12 +1,12 @@
import { JSDOM } from 'jsdom';
+import * as mfm from 'mfm-js';
import config from '@/config';
import { intersperse } from '../prelude/array';
-import { MfmForest, MfmTree } from './prelude';
import { IMentionedRemoteUsers } from '../models/entities/note';
import { wellKnownServices } from '../well-known-services';
-export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
- if (tokens == null) {
+export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) {
+ if (nodes == null) {
return null;
}
@@ -14,95 +14,101 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione
const doc = window.document;
- function appendChildren(children: MfmForest, targetElement: any): void {
- for (const child of children.map(t => handlers[t.node.type](t))) targetElement.appendChild(child);
+ function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
+ if (children) {
+ for (const child of children.map(x => (handlers as any)[x.type](x))) targetElement.appendChild(child);
+ }
}
- const handlers: { [key: string]: (token: MfmTree) => any } = {
- bold(token) {
+ const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
+ bold(node) {
const el = doc.createElement('b');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- small(token) {
+ small(node) {
const el = doc.createElement('small');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- strike(token) {
+ strike(node) {
const el = doc.createElement('del');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- italic(token) {
+ italic(node) {
const el = doc.createElement('i');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- fn(token) {
+ fn(node) {
const el = doc.createElement('i');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- blockCode(token) {
+ blockCode(node) {
const pre = doc.createElement('pre');
const inner = doc.createElement('code');
- inner.textContent = token.node.props.code;
+ inner.textContent = node.props.code;
pre.appendChild(inner);
return pre;
},
- center(token) {
+ center(node) {
const el = doc.createElement('div');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- emoji(token) {
- return doc.createTextNode(token.node.props.emoji ? token.node.props.emoji : `\u200B:${token.node.props.name}:\u200B`);
+ emojiCode(node) {
+ return doc.createTextNode(`\u200B:${node.props.name}:\u200B`);
+ },
+
+ unicodeEmoji(node) {
+ return doc.createTextNode(node.props.emoji);
},
- hashtag(token) {
+ hashtag(node) {
const a = doc.createElement('a');
- a.href = `${config.url}/tags/${token.node.props.hashtag}`;
- a.textContent = `#${token.node.props.hashtag}`;
+ a.href = `${config.url}/tags/${node.props.hashtag}`;
+ a.textContent = `#${node.props.hashtag}`;
a.setAttribute('rel', 'tag');
return a;
},
- inlineCode(token) {
+ inlineCode(node) {
const el = doc.createElement('code');
- el.textContent = token.node.props.code;
+ el.textContent = node.props.code;
return el;
},
- mathInline(token) {
+ mathInline(node) {
const el = doc.createElement('code');
- el.textContent = token.node.props.formula;
+ el.textContent = node.props.formula;
return el;
},
- mathBlock(token) {
+ mathBlock(node) {
const el = doc.createElement('code');
- el.textContent = token.node.props.formula;
+ el.textContent = node.props.formula;
return el;
},
- link(token) {
+ link(node) {
const a = doc.createElement('a');
- a.href = token.node.props.url;
- appendChildren(token.children, a);
+ a.href = node.props.url;
+ appendChildren(node.children, a);
return a;
},
- mention(token) {
+ mention(node) {
const a = doc.createElement('a');
- const { username, host, acct } = token.node.props;
+ const { username, host, acct } = node.props;
const wellKnown = wellKnownServices.find(x => x[0] === host);
if (wellKnown) {
a.href = wellKnown[1](username);
@@ -115,39 +121,39 @@ export function toHtml(tokens: MfmForest | null, mentionedRemoteUsers: IMentione
return a;
},
- quote(token) {
+ quote(node) {
const el = doc.createElement('blockquote');
- appendChildren(token.children, el);
+ appendChildren(node.children, el);
return el;
},
- text(token) {
+ text(node) {
const el = doc.createElement('span');
- const nodes = (token.node.props.text as string).split(/\r\n|\r|\n/).map(x => doc.createTextNode(x) as Node);
+ const nodes = node.props.text.split(/\r\n|\r|\n/).map(x => doc.createTextNode(x));
- for (const x of intersperse<Node | 'br'>('br', nodes)) {
+ for (const x of intersperse<FIXME | 'br'>('br', nodes)) {
el.appendChild(x === 'br' ? doc.createElement('br') : x);
}
return el;
},
- url(token) {
+ url(node) {
const a = doc.createElement('a');
- a.href = token.node.props.url;
- a.textContent = token.node.props.url;
+ a.href = node.props.url;
+ a.textContent = node.props.url;
return a;
},
- search(token) {
+ search(node) {
const a = doc.createElement('a');
- a.href = `https://www.google.com/search?q=${token.node.props.query}`;
- a.textContent = token.node.props.content;
+ a.href = `https://www.google.com/search?q=${node.props.query}`;
+ a.textContent = node.props.content;
return a;
}
};
- appendChildren(tokens, doc.body);
+ appendChildren(nodes, doc.body);
return `<p>${doc.body.innerHTML}</p>`;
}
diff --git a/src/mfm/to-string.ts b/src/mfm/to-string.ts
deleted file mode 100644
index 347c94c247..0000000000
--- a/src/mfm/to-string.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { MfmForest, MfmTree } from './prelude';
-import { nyaize } from '@/misc/nyaize';
-
-export type RestoreOptions = {
- doNyaize?: boolean;
-};
-
-export function toString(tokens: MfmForest | null, opts?: RestoreOptions): string {
-
- if (tokens === null) return '';
-
- function appendChildren(children: MfmForest, opts?: RestoreOptions): string {
- return children.map(t => handlers[t.node.type](t, opts)).join('');
- }
-
- const handlers: { [key: string]: (token: MfmTree, opts?: RestoreOptions) => string } = {
- bold(token, opts) {
- return `**${appendChildren(token.children, opts)}**`;
- },
-
- small(token, opts) {
- return `<small>${appendChildren(token.children, opts)}</small>`;
- },
-
- strike(token, opts) {
- return `~~${appendChildren(token.children, opts)}~~`;
- },
-
- italic(token, opts) {
- return `<i>${appendChildren(token.children, opts)}</i>`;
- },
-
- fn(token, opts) {
- const name = token.node.props?.name;
- const args = token.node.props?.args || {};
- const argsStr = Object.entries(args).map(([k, v]) => v === true ? k : `${k}=${v}`).join(',');
- return `[${name}${argsStr !== '' ? '.' + argsStr : ''} ${appendChildren(token.children, opts)}]`;
- },
-
- blockCode(token) {
- return `\`\`\`${token.node.props.lang || ''}\n${token.node.props.code}\n\`\`\`\n`;
- },
-
- center(token, opts) {
- return `<center>${appendChildren(token.children, opts)}</center>`;
- },
-
- emoji(token) {
- return (token.node.props.emoji ? token.node.props.emoji : `:${token.node.props.name}:`);
- },
-
- hashtag(token) {
- return `#${token.node.props.hashtag}`;
- },
-
- inlineCode(token) {
- return `\`${token.node.props.code}\``;
- },
-
- mathInline(token) {
- return `\\(${token.node.props.formula}\\)`;
- },
-
- mathBlock(token) {
- return `\\[${token.node.props.formula}\\]`;
- },
-
- link(token, opts) {
- if (token.node.props.silent) {
- return `?[${appendChildren(token.children, opts)}](${token.node.props.url})`;
- } else {
- return `[${appendChildren(token.children, opts)}](${token.node.props.url})`;
- }
- },
-
- mention(token) {
- return token.node.props.canonical;
- },
-
- quote(token) {
- return `${appendChildren(token.children, {doNyaize: false}).replace(/^/gm,'>').trim()}\n`;
- },
-
- text(token, opts) {
- return (opts && opts.doNyaize) ? nyaize(token.node.props.text) : token.node.props.text;
- },
-
- url(token) {
- return `<${token.node.props.url}>`;
- },
-
- search(token, opts) {
- const query = token.node.props.query;
- return `${(opts && opts.doNyaize ? nyaize(query) : query)} [search]\n`;
- }
- };
-
- return appendChildren(tokens, { doNyaize: (opts && opts.doNyaize) || false }).trim();
-}
diff --git a/src/misc/extract-custom-emojis-from-mfm.ts b/src/misc/extract-custom-emojis-from-mfm.ts
new file mode 100644
index 0000000000..f1477a79f0
--- /dev/null
+++ b/src/misc/extract-custom-emojis-from-mfm.ts
@@ -0,0 +1,18 @@
+import * as mfm from 'mfm-js';
+import { unique } from '@/prelude/array';
+
+export function extractCustomEmojisFromMfm(nodes: mfm.MfmNode[]): string[] {
+ const emojiNodes = [] as mfm.MfmEmojiCode[];
+
+ function scan(nodes: mfm.MfmNode[]) {
+ for (const node of nodes) {
+ if (node.type === 'emojiCode') emojiNodes.push(node);
+ else if (node.children) scan(node.children);
+ }
+ }
+
+ scan(nodes);
+
+ const emojis = emojiNodes.filter(x => x.props.name.length <= 100).map(x => x.props.name!);
+ return unique(emojis);
+}
diff --git a/src/misc/extract-emojis.ts b/src/misc/extract-emojis.ts
deleted file mode 100644
index 2c57e9a8aa..0000000000
--- a/src/misc/extract-emojis.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { EmojiNode, MfmForest } from '../mfm/prelude';
-import { preorderF } from '../prelude/tree';
-import { unique } from '../prelude/array';
-
-export default function(mfmForest: MfmForest): string[] {
- const emojiNodes = preorderF(mfmForest).filter(x => x.type === 'emoji') as EmojiNode[];
- const emojis = emojiNodes.filter(x => x.props.name && x.props.name.length <= 100).map(x => x.props.name);
- return unique(emojis);
-}
diff --git a/src/misc/extract-hashtags.ts b/src/misc/extract-hashtags.ts
index 36b2296a76..9961755ccd 100644
--- a/src/misc/extract-hashtags.ts
+++ b/src/misc/extract-hashtags.ts
@@ -1,9 +1,18 @@
-import { HashtagNode, MfmForest } from '../mfm/prelude';
-import { preorderF } from '../prelude/tree';
-import { unique } from '../prelude/array';
+import * as mfm from 'mfm-js';
+import { unique } from '@/prelude/array';
+
+export default function(nodes: mfm.MfmNode[]): string[] {
+ const hashtagNodes = [] as mfm.MfmHashtag[];
+
+ function scan(nodes: mfm.MfmNode[]) {
+ for (const node of nodes) {
+ if (node.type === 'hashtag') hashtagNodes.push(node);
+ else if (node.children) scan(node.children);
+ }
+ }
+
+ scan(nodes);
-export default function(mfmForest: MfmForest): string[] {
- const hashtagNodes = preorderF(mfmForest).filter(x => x.type === 'hashtag') as HashtagNode[];
const hashtags = hashtagNodes.map(x => x.props.hashtag);
return unique(hashtags);
}
diff --git a/src/misc/extract-mentions.ts b/src/misc/extract-mentions.ts
index 72330d31e1..a9d4b378f3 100644
--- a/src/misc/extract-mentions.ts
+++ b/src/misc/extract-mentions.ts
@@ -1,10 +1,19 @@
// test is located in test/extract-mentions
-import { MentionNode, MfmForest } from '../mfm/prelude';
-import { preorderF } from '../prelude/tree';
+import * as mfm from 'mfm-js';
-export default function(mfmForest: MfmForest): MentionNode['props'][] {
+export default function(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] {
// TODO: 重複を削除
- const mentionNodes = preorderF(mfmForest).filter(x => x.type === 'mention') as MentionNode[];
+ const mentionNodes = [] as mfm.MfmMention[];
+
+ function scan(nodes: mfm.MfmNode[]) {
+ for (const node of nodes) {
+ if (node.type === 'mention') mentionNodes.push(node);
+ else if (node.children) scan(node.children);
+ }
+ }
+
+ scan(nodes);
+
return mentionNodes.map(x => x.props);
}
diff --git a/src/misc/extract-url-from-mfm.ts b/src/misc/extract-url-from-mfm.ts
new file mode 100644
index 0000000000..aa7f5f2540
--- /dev/null
+++ b/src/misc/extract-url-from-mfm.ts
@@ -0,0 +1,34 @@
+import * as mfm from 'mfm-js';
+import { unique } from '@/prelude/array';
+
+// unique without hash
+// [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
+const removeHash = (x: string) => x.replace(/#[^#]*$/, '');
+
+export function extractUrlFromMfm(nodes: mfm.MfmNode[], respectSilentFlag = true): string[] {
+ const urlNodes = [] as (mfm.MfmUrl | mfm.MfmLink)[];
+
+ function scan(nodes: mfm.MfmNode[]) {
+ for (const node of nodes) {
+ if (node.type === 'url') {
+ urlNodes.push(node);
+ } else if (node.type === 'link') {
+ if (!respectSilentFlag || !node.props.silent) {
+ urlNodes.push(node);
+ }
+ } else if (node.children) {
+ scan(node.children);
+ }
+ }
+ }
+
+ scan(nodes);
+
+ const urls = unique(urlNodes.map(x => x.props.url));
+
+ return urls.reduce((array, url) => {
+ const removed = removeHash(url);
+ if (!array.map(x => removeHash(x)).includes(removed)) array.push(url);
+ return array;
+ }, [] as string[]);
+}
diff --git a/src/models/repositories/note.ts b/src/models/repositories/note.ts
index 3642a03c2c..cdf4841918 100644
--- a/src/models/repositories/note.ts
+++ b/src/models/repositories/note.ts
@@ -1,12 +1,12 @@
import { EntityRepository, Repository, In } from 'typeorm';
+import * as mfm from 'mfm-js';
import { Note } from '../entities/note';
import { User } from '../entities/user';
import { Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls, Channels } from '..';
import { SchemaType } from '@/misc/schema';
+import { nyaize } from '@/misc/nyaize';
import { awaitAll } from '../../prelude/await-all';
import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '@/misc/reaction-lib';
-import { toString } from '../../mfm/to-string';
-import { parse } from '../../mfm/parse';
import { NoteReaction } from '../entities/note-reaction';
import { aggregateNoteEmojis, populateEmojis, prefetchEmojis } from '@/misc/populate-emojis';
@@ -223,8 +223,13 @@ export class NoteRepository extends Repository<Note> {
});
if (packed.user.isCat && packed.text) {
- const tokens = packed.text ? parse(packed.text) : [];
- packed.text = toString(tokens, { doNyaize: true });
+ const tokens = packed.text ? mfm.parse(packed.text) : [];
+ mfm.inspect(tokens, node => {
+ if (node.type === 'text') {
+ node.props.text = nyaize(node.props.text);
+ }
+ });
+ packed.text = mfm.toString(tokens);
}
if (!opts.skipHide) {
diff --git a/src/prelude/tree.ts b/src/prelude/tree.ts
deleted file mode 100644
index 519234a0b0..0000000000
--- a/src/prelude/tree.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { concat, sum } from './array';
-
-export type Tree<T> = {
- node: T,
- children: Forest<T>;
-};
-
-export type Forest<T> = Tree<T>[];
-
-export function createLeaf<T>(node: T): Tree<T> {
- return { node, children: [] };
-}
-
-export function createTree<T>(node: T, children: Forest<T>): Tree<T> {
- return { node, children };
-}
-
-export function hasChildren<T>(t: Tree<T>): boolean {
- return t.children.length !== 0;
-}
-
-export function preorder<T>(t: Tree<T>): T[] {
- return [t.node, ...preorderF(t.children)];
-}
-
-export function preorderF<T>(ts: Forest<T>): T[] {
- return concat(ts.map(preorder));
-}
-
-export function countNodes<T>(t: Tree<T>): number {
- return preorder(t).length;
-}
-
-export function countNodesF<T>(ts: Forest<T>): number {
- return sum(ts.map(countNodes));
-}
diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts
index 6990a4ae5e..683860d9cc 100644
--- a/src/remote/activitypub/misc/get-note-html.ts
+++ b/src/remote/activitypub/misc/get-note-html.ts
@@ -1,9 +1,9 @@
+import * as mfm from 'mfm-js';
import { Note } from '../../../models/entities/note';
import { toHtml } from '../../../mfm/to-html';
-import { parse } from '../../../mfm/parse';
export default function(note: Note) {
- let html = toHtml(parse(note.text), JSON.parse(note.mentionedRemoteUsers));
+ let html = note.text ? toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)) : null;
if (html == null) html = '<p>.</p>';
return html;
diff --git a/src/remote/activitypub/renderer/person.ts b/src/remote/activitypub/renderer/person.ts
index e4e8f24f10..91b91bff92 100644
--- a/src/remote/activitypub/renderer/person.ts
+++ b/src/remote/activitypub/renderer/person.ts
@@ -1,10 +1,10 @@
import { URL } from 'url';
+import * as mfm from 'mfm-js';
import renderImage from './image';
import renderKey from './key';
import config from '@/config';
import { ILocalUser } from '../../../models/entities/user';
import { toHtml } from '../../../mfm/to-html';
-import { parse } from '../../../mfm/parse';
import { getEmojis } from './note';
import renderEmoji from './emoji';
import { IIdentifier } from '../models/identifier';
@@ -66,7 +66,7 @@ export async function renderPerson(user: ILocalUser) {
url: `${config.url}/@${user.username}`,
preferredUsername: user.username,
name: user.name,
- summary: toHtml(parse(profile.description)),
+ summary: profile.description ? toHtml(mfm.parse(profile.description)) : null,
icon: avatar ? renderImage(avatar) : null,
image: banner ? renderImage(banner) : null,
tag,
diff --git a/src/server/api/endpoints/i/update.ts b/src/server/api/endpoints/i/update.ts
index 0554fe76fb..2c20da41c5 100644
--- a/src/server/api/endpoints/i/update.ts
+++ b/src/server/api/endpoints/i/update.ts
@@ -1,11 +1,11 @@
import $ from 'cafy';
+import * as mfm from 'mfm-js';
import { ID } from '@/misc/cafy-id';
import { publishMainStream, publishUserEvent } from '../../../../services/stream';
import acceptAllFollowRequests from '../../../../services/following/requests/accept-all';
import { publishToFollowers } from '../../../../services/i/update';
import define from '../../define';
-import { parse, parsePlain } from '../../../../mfm/parse';
-import extractEmojis from '@/misc/extract-emojis';
+import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
import extractHashtags from '@/misc/extract-hashtags';
import * as langmap from 'langmap';
import { updateUsertags } from '../../../../services/update-hashtag';
@@ -291,13 +291,13 @@ export default define(meta, async (ps, _user, token) => {
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
if (newName != null) {
- const tokens = parsePlain(newName);
- emojis = emojis.concat(extractEmojis(tokens!));
+ const tokens = mfm.parsePlain(newName);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
}
if (newDescription != null) {
- const tokens = parse(newDescription);
- emojis = emojis.concat(extractEmojis(tokens!));
+ const tokens = mfm.parse(newDescription);
+ emojis = emojis.concat(extractCustomEmojisFromMfm(tokens!));
tags = extractHashtags(tokens!).map(tag => normalizeForSearch(tag)).splice(0, 32);
}
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 64d5513ecc..125285f34a 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,3 +1,4 @@
+import * as mfm from 'mfm-js';
import es from '../../db/elasticsearch';
import { publishMainStream, publishNotesStream } from '../stream';
import DeliverManager from '../../remote/activitypub/deliver-manager';
@@ -5,7 +6,6 @@ import renderNote from '../../remote/activitypub/renderer/note';
import renderCreate from '../../remote/activitypub/renderer/create';
import renderAnnounce from '../../remote/activitypub/renderer/announce';
import { renderActivity } from '../../remote/activitypub/renderer';
-import { parse } from '../../mfm/parse';
import { resolveUser } from '../../remote/resolve-user';
import config from '@/config';
import { updateHashtags } from '../update-hashtag';
@@ -13,7 +13,7 @@ import { concat } from '../../prelude/array';
import insertNoteUnread from './unread';
import { registerOrFetchInstanceDoc } from '../register-or-fetch-instance-doc';
import extractMentions from '@/misc/extract-mentions';
-import extractEmojis from '@/misc/extract-emojis';
+import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm';
import extractHashtags from '@/misc/extract-hashtags';
import { Note, IMentionedRemoteUsers } from '../../models/entities/note';
import { Mutings, Users, NoteWatchings, Notes, Instances, UserProfiles, Antennas, Followings, MutedNotes, Channels, ChannelFollowings } from '../../models';
@@ -182,17 +182,17 @@ export default async (user: { id: User['id']; username: User['username']; host:
// Parse MFM if needed
if (!tags || !emojis || !mentionedUsers) {
- const tokens = data.text ? parse(data.text)! : [];
- const cwTokens = data.cw ? parse(data.cw)! : [];
+ const tokens = data.text ? mfm.parse(data.text)! : [];
+ const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
const choiceTokens = data.poll && data.poll.choices
- ? concat(data.poll.choices.map(choice => parse(choice)!))
+ ? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
: [];
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
tags = data.apHashtags || extractHashtags(combinedTokens);
- emojis = data.apEmojis || extractEmojis(combinedTokens);
+ emojis = data.apEmojis || extractCustomEmojisFromMfm(combinedTokens);
mentionedUsers = data.apMentions || await extractMentionedUsers(user, combinedTokens);
}
@@ -604,7 +604,7 @@ function incNotesCountOfUser(user: { id: User['id']; }) {
.execute();
}
-async function extractMentionedUsers(user: { host: User['host']; }, tokens: ReturnType<typeof parse>): Promise<User[]> {
+async function extractMentionedUsers(user: { host: User['host']; }, tokens: mfm.MfmNode[]): Promise<User[]> {
if (tokens == null) return [];
const mentions = extractMentions(tokens);