adjust MFM to HTML conversion

Removed the misc/get-note-html module which was only used in one place.
Instead of it, the general MFM to HTML functionality has been improved to
take care of the use cases of that module as well.

Co-authored-by: Francis Dinh <normandy@biribiri.dev>
This commit is contained in:
Johann150 2022-08-24 23:16:53 +02:00
parent f94ed5e8a6
commit 3bdbbcadd9
Signed by untrusted user: Johann150
GPG key ID: 9EE6577A2A06F8F1
4 changed files with 35 additions and 29 deletions

View file

@ -1,17 +1,29 @@
import { JSDOM } from 'jsdom'; import { JSDOM } from 'jsdom';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { UserProfiles } from '@/models/index.js';
import { extractMentions } from '@/misc/extract-mentions.js';
import { intersperse } from '@/prelude/array.js'; import { intersperse } from '@/prelude/array.js';
import { IMentionedRemoteUsers } from '@/models/entities/note.js';
export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMentionedRemoteUsers = []) { // Transforms MFM to HTML, given the MFM text and a list of user IDs that are
// mentioned in the text. If the list of mentions is not given, all mentions
// from the text will be extracted.
export async function toHtml(mfmText: string, mentions?: string[]): string | null {
const nodes = mfm.parse(mfmText);
if (nodes == null) { if (nodes == null) {
return null; return null;
} }
const { window } = new JSDOM(''); const mentionedUsers = await UserProfiles.createQueryBuilder("user_profiles")
.leftJoin('user_profile.user', 'user')
.select('user.username')
.addSelect('user.host')
// links should preferably use user friendly urls, only fall back to AP ids
.addSelect('COALESCE(user_profile.url, user.uri)', 'url')
.where('userId IN (:...ids)', { ids: mentions ?? extractMentions(nodes) })
.getManyRaw();
const doc = window.document; const doc = new JSDOM('').window.document;
function appendChildren(children: mfm.MfmNode[], targetElement: any): void { function appendChildren(children: mfm.MfmNode[], targetElement: any): void {
if (children) { if (children) {
@ -106,18 +118,23 @@ export function toHtml(nodes: mfm.MfmNode[] | null, mentionedRemoteUsers: IMenti
}, },
mention(node) { mention(node) {
// Mastodon microformat: span.h-card > a.u-url.mention
const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); const userInfo = mentionedUsers.find(user => user.username === username && user.host === host);
a.href = remoteUserInfo?.url ?? remoteUserInfo?.uri ?? `${config.url}/${acct}`; if (userInfo != null) {
a.className = 'u-url mention'; // Mastodon microformat: span.h-card > a.u-url.mention
a.textContent = acct; const a = doc.createElement('a');
a.href = userInfo.url ?? `${config.url}/${acct}`;
a.className = 'u-url mention';
a.textContent = acct;
const card = doc.createElement('span'); const card = doc.createElement('span');
card.className = 'h-card'; card.className = 'h-card';
card.appendChild(a); card.appendChild(a);
return card; return card;
} else {
// this user does not actually exist
return doc.createTextNode(acct);
}
}, },
quote(node) { quote(node) {

View file

@ -1,8 +0,0 @@
import * as mfm from 'mfm-js';
import { Note } from '@/models/entities/note.js';
import { toHtml } from '../../../mfm/to-html.js';
export default function(note: Note) {
if (!note.text) return '';
return toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
}

View file

@ -5,7 +5,7 @@ import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js'; import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js';
import { Emoji } from '@/models/entities/emoji.js'; import { Emoji } from '@/models/entities/emoji.js';
import { Poll } from '@/models/entities/poll.js'; import { Poll } from '@/models/entities/poll.js';
import toHtml from '../misc/get-note-html.js'; import { toHtml } from '@/mfm/to-html.js';
import renderEmoji from './emoji.js'; import renderEmoji from './emoji.js';
import renderMention from './mention.js'; import renderMention from './mention.js';
import renderHashtag from './hashtag.js'; import renderHashtag from './hashtag.js';
@ -97,9 +97,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw; const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
const content = toHtml(Object.assign({}, note, { const content = await toHtml(apText, note.mentions);
text: apText,
}));
const emojis = await getEmojis(note.emojis); const emojis = await getEmojis(note.emojis);
const apemojis = emojis.map(emoji => renderEmoji(emoji)); const apemojis = emojis.map(emoji => renderEmoji(emoji));
@ -112,7 +110,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const asPoll = poll ? { const asPoll = poll ? {
type: 'Question', type: 'Question',
content: toHtml(Object.assign({}, note, { text })), content: await toHtml(text, note.mentions),
[poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt, [poll.expiresAt && poll.expiresAt < new Date() ? 'closed' : 'endTime']: poll.expiresAt,
[poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({ [poll.multiple ? 'anyOf' : 'oneOf']: poll.choices.map((text, i) => ({
type: 'Note', type: 'Note',

View file

@ -1,5 +1,4 @@
import { URL } from 'node:url'; import { URL } from 'node:url';
import * as mfm from 'mfm-js';
import config from '@/config/index.js'; import config from '@/config/index.js';
import { ILocalUser } from '@/models/entities/user.js'; import { ILocalUser } from '@/models/entities/user.js';
import { toHtml } from '@/mfm/to-html.js'; import { toHtml } from '@/mfm/to-html.js';
@ -66,7 +65,7 @@ export async function renderPerson(user: ILocalUser) {
url: `${config.url}/@${user.username}`, url: `${config.url}/@${user.username}`,
preferredUsername: user.username, preferredUsername: user.username,
name: user.name, name: user.name,
summary: profile.description ? toHtml(mfm.parse(profile.description)) : null, summary: profile.description ? await toHtml(profile.description) : null,
icon: avatar ? renderImage(avatar) : null, icon: avatar ? renderImage(avatar) : null,
image: banner ? renderImage(banner) : null, image: banner ? renderImage(banner) : null,
tag, tag,