リモートユーザーのHTMLで表現されたプロフィールをMFMに変換するように

This commit is contained in:
syuilo 2018-06-21 01:21:57 +09:00
parent 3633d7ada1
commit 79d1bf30a4
29 changed files with 90 additions and 92 deletions

View file

@ -32,7 +32,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../text/parse'; import parse from '../../../../../mfm/parse';
export default Vue.extend({ export default Vue.extend({
props: { props: {

View file

@ -1,6 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import * as emojilib from 'emojilib'; import * as emojilib from 'emojilib';
import parse from '../../../../../text/parse'; import parse from '../../../../../mfm/parse';
import getAcct from '../../../../../acct/render'; import getAcct from '../../../../../acct/render';
import { url } from '../../../config'; import { url } from '../../../config';
import MkUrl from './url.vue'; import MkUrl from './url.vue';

View file

@ -83,7 +83,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify'; 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 MkPostFormWindow from './post-form-window.vue';
import MkRenoteFormWindow from './renote-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue';

View file

@ -76,7 +76,7 @@
import Vue from 'vue'; import Vue from 'vue';
import dateStringify from '../../../common/scripts/date-stringify'; import dateStringify from '../../../common/scripts/date-stringify';
import canHideText from '../../../common/scripts/can-hide-text'; 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 MkPostFormWindow from './post-form-window.vue';
import MkRenoteFormWindow from './renote-form-window.vue'; import MkRenoteFormWindow from './renote-form-window.vue';

View file

@ -49,7 +49,7 @@ import Vue from 'vue';
import * as XDraggable from 'vuedraggable'; import * as XDraggable from 'vuedraggable';
import getKao from '../../../common/scripts/get-kao'; import getKao from '../../../common/scripts/get-kao';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import parse from '../../../../../text/parse'; import parse from '../../../../../mfm/parse';
import { host } from '../../../config'; import { host } from '../../../config';
export default Vue.extend({ export default Vue.extend({

View file

@ -67,7 +67,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../../text/parse'; import parse from '../../../../../../mfm/parse';
import canHideText from '../../../../common/scripts/can-hide-text'; import canHideText from '../../../../common/scripts/can-hide-text';
import MkNoteMenu from '../../../../common/views/components/note-menu.vue'; import MkNoteMenu from '../../../../common/views/components/note-menu.vue';

View file

@ -83,7 +83,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../text/parse'; import parse from '../../../../../mfm/parse';
import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkNoteMenu from '../../../common/views/components/note-menu.vue';
import MkReactionPicker from '../../../common/views/components/reaction-picker.vue'; import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';

View file

@ -68,7 +68,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import parse from '../../../../../text/parse'; import parse from '../../../../../mfm/parse';
import canHideText from '../../../common/scripts/can-hide-text'; import canHideText from '../../../common/scripts/can-hide-text';
import MkNoteMenu from '../../../common/views/components/note-menu.vue'; import MkNoteMenu from '../../../common/views/components/note-menu.vue';

View file

@ -45,7 +45,7 @@ import Vue from 'vue';
import * as XDraggable from 'vuedraggable'; import * as XDraggable from 'vuedraggable';
import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue'; import MkVisibilityChooser from '../../../common/views/components/visibility-chooser.vue';
import getKao from '../../../common/scripts/get-kao'; import getKao from '../../../common/scripts/get-kao';
import parse from '../../../../../text/parse'; import parse from '../../../../../mfm/parse';
import { host } from '../../../config'; import { host } from '../../../config';
export default Vue.extend({ export default Vue.extend({

71
src/mfm/html-to-mfm.ts Normal file
View file

@ -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;
}
}
}

View file

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

View file

@ -1,5 +1,4 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
const parse5 = require('parse5');
import * as debug from 'debug'; import * as debug from 'debug';
import config from '../../../config'; import config from '../../../config';
@ -10,79 +9,10 @@ import { INote as INoteActivityStreamsObject, IObject } from '../type';
import { resolvePerson, updatePerson } from './person'; import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image'; import { resolveImage } from './image';
import { IRemoteUser, IUser } from '../../../models/user'; import { IRemoteUser, IUser } from '../../../models/user';
import htmlToMFM from '../../../mfm/html-to-mfm';
const log = debug('misskey:activitypub'); 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をフェッチします * 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 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) { if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {

View file

@ -1,5 +1,4 @@
import * as mongo from 'mongodb'; import * as mongo from 'mongodb';
import { JSDOM } from 'jsdom';
import { toUnicode } from 'punycode'; import { toUnicode } from 'punycode';
import * as debug from 'debug'; import * as debug from 'debug';
@ -11,6 +10,7 @@ import { resolveImage } from './image';
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type'; import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
import { IDriveFile } from '../../../models/drive-file'; import { IDriveFile } from '../../../models/drive-file';
import Meta from '../../../models/meta'; import Meta from '../../../models/meta';
import htmlToMFM from '../../../mfm/html-to-mfm';
const log = debug('misskey:activitypub'); 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 host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase();
const summaryDOM = JSDOM.fragment(person.summary);
// Create user // Create user
let user: IRemoteUser; let user: IRemoteUser;
@ -89,7 +88,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
avatarId: null, avatarId: null,
bannerId: null, bannerId: null,
createdAt: Date.parse(person.published) || null, createdAt: Date.parse(person.published) || null,
description: summaryDOM.textContent, description: htmlToMFM(person.summary),
followersCount, followersCount,
followingCount, followingCount,
notesCount, 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>([ const [avatar, banner] = (await Promise.all<IDriveFile>([
person.icon, person.icon,
@ -231,7 +228,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
bannerId: banner ? banner._id : null, bannerId: banner ? banner._id : null,
avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null, avatarUrl: avatar && avatar.metadata.isMetaOnly ? avatar.metadata.url : null,
bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null, bannerUrl: banner && banner.metadata.isMetaOnly ? banner.metadata.url : null,
description: summaryDOM.textContent, description: htmlToMFM(person.summary),
followersCount, followersCount,
followingCount, followingCount,
notesCount, notesCount,

View file

@ -14,7 +14,7 @@ import watch from './watch';
import Mute from '../../models/mute'; import Mute from '../../models/mute';
import pushSw from '../../publishers/push-sw'; import pushSw from '../../publishers/push-sw';
import event from '../../publishers/stream'; import event from '../../publishers/stream';
import parse from '../../text/parse'; import parse from '../../mfm/parse';
import { IApp } from '../../models/app'; import { IApp } from '../../models/app';
import UserList from '../../models/user-list'; import UserList from '../../models/user-list';
import resolveUser from '../../remote/resolve-user'; import resolveUser from '../../remote/resolve-user';

View file

@ -1,7 +1,7 @@
import * as assert from 'assert'; import * as assert from 'assert';
import analyze from '../src/text/parse'; import analyze from '../src/mfm/parse';
import syntaxhighlighter from '../src/text/parse/core/syntax-highlighter'; import syntaxhighlighter from '../src/mfm/parse/core/syntax-highlighter';
describe('Text', () => { describe('Text', () => {
it('can be analyzed', () => { it('can be analyzed', () => {