diff --git a/src/client/app/common/views/components/messaging-room.message.vue b/src/client/app/common/views/components/messaging-room.message.vue index d7e7c6dcb5..f33173da6f 100644 --- a/src/client/app/common/views/components/messaging-room.message.vue +++ b/src/client/app/common/views/components/messaging-room.message.vue @@ -32,7 +32,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; export default Vue.extend({ props: { diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts index 61dbc02d9a..30ad3ce3f2 100644 --- a/src/client/app/common/views/components/misskey-flavored-markdown.ts +++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts @@ -1,6 +1,6 @@ import Vue from 'vue'; import * as emojilib from 'emojilib'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import getAcct from '../../../../../acct/render'; import { url } from '../../../config'; import MkUrl from './url.vue'; diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue index 017f599e31..a9640ff9c4 100644 --- a/src/client/app/desktop/views/components/note-detail.vue +++ b/src/client/app/desktop/views/components/note-detail.vue @@ -83,7 +83,7 @@ <script lang="ts"> import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import MkPostFormWindow from './post-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue'; diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue index 661223ee25..b96ae2da5a 100644 --- a/src/client/app/desktop/views/components/notes.note.vue +++ b/src/client/app/desktop/views/components/notes.note.vue @@ -76,7 +76,7 @@ import Vue from 'vue'; import dateStringify from '../../../common/scripts/date-stringify'; import canHideText from '../../../common/scripts/can-hide-text'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import MkPostFormWindow from './post-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue'; diff --git a/src/client/app/desktop/views/components/post-form.vue b/src/client/app/desktop/views/components/post-form.vue index 33f2288e06..3832e5b38c 100644 --- a/src/client/app/desktop/views/components/post-form.vue +++ b/src/client/app/desktop/views/components/post-form.vue @@ -49,7 +49,7 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import getKao from '../../../common/scripts/get-kao'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; export default Vue.extend({ diff --git a/src/client/app/desktop/views/pages/deck/deck.note.vue b/src/client/app/desktop/views/pages/deck/deck.note.vue index 067297af37..d50fc3c235 100644 --- a/src/client/app/desktop/views/pages/deck/deck.note.vue +++ b/src/client/app/desktop/views/pages/deck/deck.note.vue @@ -67,7 +67,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../../text/parse'; +import parse from '../../../../../../mfm/parse'; import canHideText from '../../../../common/scripts/can-hide-text'; import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue index 6499f78f1b..76633490fd 100644 --- a/src/client/app/mobile/views/components/note-detail.vue +++ b/src/client/app/mobile/views/components/note-detail.vue @@ -83,7 +83,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue index 77513a6591..ecdfee3b44 100644 --- a/src/client/app/mobile/views/components/note.vue +++ b/src/client/app/mobile/views/components/note.vue @@ -68,7 +68,7 @@ <script lang="ts"> import Vue from 'vue'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import canHideText from '../../../common/scripts/can-hide-text'; import MkNoteMenu from '../../../common/views/components/note-menu.vue'; diff --git a/src/client/app/mobile/views/components/post-form.vue b/src/client/app/mobile/views/components/post-form.vue index 62fa185085..1015a44115 100644 --- a/src/client/app/mobile/views/components/post-form.vue +++ b/src/client/app/mobile/views/components/post-form.vue @@ -45,7 +45,7 @@ import Vue from 'vue'; import * as XDraggable from 'vuedraggable'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import getKao from '../../../common/scripts/get-kao'; -import parse from '../../../../../text/parse'; +import parse from '../../../../../mfm/parse'; import { host } from '../../../config'; export default Vue.extend({ diff --git a/src/mfm/html-to-mfm.ts b/src/mfm/html-to-mfm.ts new file mode 100644 index 0000000000..540635036a --- /dev/null +++ b/src/mfm/html-to-mfm.ts @@ -0,0 +1,71 @@ +const parse5 = require('parse5'); + +export default function(html: string): string { + const dom = parse5.parseFragment(html); + + let text = ''; + + dom.childNodes.forEach((n: any) => analyze(n)); + + return text.trim(); + + function getText(node: any) { + if (node.nodeName == '#text') return node.value; + + if (node.childNodes) { + return node.childNodes.map((n: any) => getText(n)).join(''); + } + + return ''; + } + + function analyze(node: any) { + switch (node.nodeName) { + case '#text': + text += node.value; + break; + + case 'br': + text += '\n'; + break; + + case 'a': + const txt = getText(node); + + // メンション + if (txt.startsWith('@')) { + const part = txt.split('@'); + + if (part.length == 2) { + //#region ホスト名部分が省略されているので復元する + const href = new URL(node.attrs.find((x: any) => x.name == 'href').value); + const acct = txt + '@' + href.hostname; + text += acct; + break; + //#endregion + } else if (part.length == 3) { + text += txt; + break; + } + } + + if (node.childNodes) { + node.childNodes.forEach((n: any) => analyze(n)); + } + break; + + case 'p': + text += '\n\n'; + if (node.childNodes) { + node.childNodes.forEach((n: any) => analyze(n)); + } + break; + + default: + if (node.childNodes) { + node.childNodes.forEach((n: any) => analyze(n)); + } + break; + } + } +} diff --git a/src/text/html.ts b/src/mfm/html.ts similarity index 100% rename from src/text/html.ts rename to src/mfm/html.ts diff --git a/src/text/parse/core/syntax-highlighter.ts b/src/mfm/parse/core/syntax-highlighter.ts similarity index 100% rename from src/text/parse/core/syntax-highlighter.ts rename to src/mfm/parse/core/syntax-highlighter.ts diff --git a/src/text/parse/elements/bold.ts b/src/mfm/parse/elements/bold.ts similarity index 100% rename from src/text/parse/elements/bold.ts rename to src/mfm/parse/elements/bold.ts diff --git a/src/text/parse/elements/code.ts b/src/mfm/parse/elements/code.ts similarity index 100% rename from src/text/parse/elements/code.ts rename to src/mfm/parse/elements/code.ts diff --git a/src/text/parse/elements/emoji.ts b/src/mfm/parse/elements/emoji.ts similarity index 100% rename from src/text/parse/elements/emoji.ts rename to src/mfm/parse/elements/emoji.ts diff --git a/src/text/parse/elements/hashtag.ts b/src/mfm/parse/elements/hashtag.ts similarity index 100% rename from src/text/parse/elements/hashtag.ts rename to src/mfm/parse/elements/hashtag.ts diff --git a/src/text/parse/elements/inline-code.ts b/src/mfm/parse/elements/inline-code.ts similarity index 100% rename from src/text/parse/elements/inline-code.ts rename to src/mfm/parse/elements/inline-code.ts diff --git a/src/text/parse/elements/link.ts b/src/mfm/parse/elements/link.ts similarity index 100% rename from src/text/parse/elements/link.ts rename to src/mfm/parse/elements/link.ts diff --git a/src/text/parse/elements/mention.ts b/src/mfm/parse/elements/mention.ts similarity index 100% rename from src/text/parse/elements/mention.ts rename to src/mfm/parse/elements/mention.ts diff --git a/src/text/parse/elements/quote.ts b/src/mfm/parse/elements/quote.ts similarity index 100% rename from src/text/parse/elements/quote.ts rename to src/mfm/parse/elements/quote.ts diff --git a/src/text/parse/elements/search.ts b/src/mfm/parse/elements/search.ts similarity index 100% rename from src/text/parse/elements/search.ts rename to src/mfm/parse/elements/search.ts diff --git a/src/text/parse/elements/title.ts b/src/mfm/parse/elements/title.ts similarity index 100% rename from src/text/parse/elements/title.ts rename to src/mfm/parse/elements/title.ts diff --git a/src/text/parse/elements/url.ts b/src/mfm/parse/elements/url.ts similarity index 100% rename from src/text/parse/elements/url.ts rename to src/mfm/parse/elements/url.ts diff --git a/src/text/parse/index.ts b/src/mfm/parse/index.ts similarity index 100% rename from src/text/parse/index.ts rename to src/mfm/parse/index.ts diff --git a/src/remote/activitypub/misc/get-note-html.ts b/src/remote/activitypub/misc/get-note-html.ts index 0ceecdd00b..8df440930b 100644 --- a/src/remote/activitypub/misc/get-note-html.ts +++ b/src/remote/activitypub/misc/get-note-html.ts @@ -1,6 +1,6 @@ import { INote } from '../../../models/note'; -import toHtml from '../../../text/html'; -import parse from '../../../text/parse'; +import toHtml from '../../../mfm/html'; +import parse from '../../../mfm/parse'; import config from '../../../config'; export default function(note: INote) { diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts index b0fe045e6d..85a8f89bc8 100644 --- a/src/remote/activitypub/models/note.ts +++ b/src/remote/activitypub/models/note.ts @@ -1,5 +1,4 @@ import * as mongo from 'mongodb'; -const parse5 = require('parse5'); import * as debug from 'debug'; import config from '../../../config'; @@ -10,79 +9,10 @@ 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'; const log = debug('misskey:activitypub'); -function parse(html: string): string { - const dom = parse5.parseFragment(html); - - let text = ''; - - dom.childNodes.forEach((n: any) => analyze(n)); - - return text.trim(); - - function getText(node: any) { - if (node.nodeName == '#text') return node.value; - - if (node.childNodes) { - return node.childNodes.map((n: any) => getText(n)).join(''); - } - - return ''; - } - - function analyze(node: any) { - switch (node.nodeName) { - case '#text': - text += node.value; - break; - - case 'br': - text += '\n'; - break; - - case 'a': - const txt = getText(node); - - // メンション - if (txt.startsWith('@')) { - const part = txt.split('@'); - - if (part.length == 2) { - //#region ホスト名部分が省略されているので復元する - const href = new URL(node.attrs.find((x: any) => x.name == 'href').value); - const acct = txt + '@' + href.hostname; - text += acct; - break; - //#endregion - } else if (part.length == 3) { - text += txt; - break; - } - } - - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); - } - break; - - case 'p': - text += '\n\n'; - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); - } - break; - - default: - if (node.childNodes) { - node.childNodes.forEach((n: any) => analyze(n)); - } - break; - } - } -} - /** * Noteをフェッチします。 * @@ -158,7 +88,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false const reply = note.inReplyTo ? await resolveNote(note.inReplyTo, resolver) : null; // テキストのパース - const text = parse(note.content); + const text = htmlToMFM(note.content); // ユーザーの情報が古かったらついでに更新しておく if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) { diff --git a/src/remote/activitypub/models/person.ts b/src/remote/activitypub/models/person.ts index 42ee5a27df..f233346395 100644 --- a/src/remote/activitypub/models/person.ts +++ b/src/remote/activitypub/models/person.ts @@ -1,5 +1,4 @@ import * as mongo from 'mongodb'; -import { JSDOM } from 'jsdom'; import { toUnicode } from 'punycode'; import * as debug from 'debug'; @@ -11,6 +10,7 @@ import { resolveImage } from './image'; import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; import { IDriveFile } from '../../../models/drive-file'; import Meta from '../../../models/meta'; +import htmlToMFM from '../../../mfm/html-to-mfm'; const log = debug('misskey:activitypub'); @@ -80,7 +80,6 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs ]); const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase(); - const summaryDOM = JSDOM.fragment(person.summary); // Create user let user: IRemoteUser; @@ -89,7 +88,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs avatarId: null, bannerId: null, createdAt: Date.parse(person.published) || null, - description: summaryDOM.textContent, + description: htmlToMFM(person.summary), followersCount, followingCount, notesCount, @@ -211,8 +210,6 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) ) ]); - const summaryDOM = JSDOM.fragment(person.summary); - // アイコンとヘッダー画像をフェッチ const [avatar, banner] = (await Promise.all<IDriveFile>([ person.icon, @@ -231,7 +228,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver) bannerId: banner ? banner._id : null, avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null, bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null, - description: summaryDOM.textContent, + description: htmlToMFM(person.summary), followersCount, followingCount, notesCount, diff --git a/src/services/note/create.ts b/src/services/note/create.ts index ef03c4e044..782ac5a4fe 100644 --- a/src/services/note/create.ts +++ b/src/services/note/create.ts @@ -14,7 +14,7 @@ import watch from './watch'; import Mute from '../../models/mute'; import pushSw from '../../publishers/push-sw'; import event from '../../publishers/stream'; -import parse from '../../text/parse'; +import parse from '../../mfm/parse'; import { IApp } from '../../models/app'; import UserList from '../../models/user-list'; import resolveUser from '../../remote/resolve-user'; diff --git a/test/text.ts b/test/mfm.ts similarity index 96% rename from test/text.ts rename to test/mfm.ts index a64999fc0b..de722bffb3 100644 --- a/test/text.ts +++ b/test/mfm.ts @@ -1,7 +1,7 @@ import * as assert from 'assert'; -import analyze from '../src/text/parse'; -import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter'; +import analyze from '../src/mfm/parse'; +import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter'; describe('Text', () => { it('can be analyzed', () => {