import { Window as HappyDom } from "happy-dom"; import config from "../../../../config/index.js"; import { intersperse } from "../../../../prelude/array.js"; import { resolveMentionFromCache } from "../../../../remote/resolve-user.js"; export class MfmHelpers { static async toHtml(nodes, mentionedRemoteUsers = [], objectHost, inline = false, quoteUri = null) { if (nodes == null) { return null; } const window = new HappyDom(); const doc = window.document; function appendTextWithGlyphs(text, targetElement) { const regexp = /;([^:;\s]{1,100});/g; let last = 0; for (const match of text.matchAll(regexp)){ if (match.index > last) { targetElement.appendChild(doc.createTextNode(text.slice(last, match.index))); } targetElement.appendChild(doc.createTextNode(`\u200B:${match[1]}:\u200B`)); last = match.index + match[0].length; } if (last < text.length) { targetElement.appendChild(doc.createTextNode(text.slice(last))); } } async function appendChildren(children, targetElement) { if (children) { for (const child of (await Promise.all(children.map(async (x)=>await handlers[x.type](x)))))targetElement.appendChild(child); } } const handlers = { async bold (node) { const el = doc.createElement("span"); el.textContent = '**'; await appendChildren(node.children, el); el.textContent += '**'; return el; }, async small (node) { const el = doc.createElement("small"); await appendChildren(node.children, el); return el; }, async strike (node) { const el = doc.createElement("span"); el.textContent = '~~'; await appendChildren(node.children, el); el.textContent += '~~'; return el; }, async italic (node) { const el = doc.createElement("span"); el.textContent = '*'; await appendChildren(node.children, el); el.textContent += '*'; return el; }, async fn (node) { const el = doc.createElement("span"); el.textContent = '*'; await appendChildren(node.children, el); el.textContent += '*'; return el; }, blockCode (node) { const pre = doc.createElement("pre"); const inner = doc.createElement("code"); const nodes = node.props.code.split(/\r\n|\r|\n/).map((x)=>doc.createTextNode(x)); for (const x of intersperse("br", nodes)){ inner.appendChild(x === "br" ? doc.createElement("br") : x); } pre.appendChild(inner); return pre; }, async center (node) { const el = doc.createElement("div"); await appendChildren(node.children, el); return el; }, emojiCode (node) { return doc.createTextNode(`\u200B:${node.props.name}:\u200B`); }, unicodeEmoji (node) { return doc.createTextNode(node.props.emoji); }, hashtag (node) { const a = doc.createElement("a"); a.setAttribute('href', `${config.url}/tags/${node.props.hashtag}`); a.textContent = `#${node.props.hashtag}`; a.setAttribute("rel", "tag"); a.setAttribute("class", "hashtag"); return a; }, inlineCode (node) { const el = doc.createElement("code"); el.textContent = node.props.code; return el; }, mathInline (node) { const el = doc.createElement("code"); el.textContent = node.props.formula; return el; }, mathBlock (node) { const el = doc.createElement("code"); el.textContent = node.props.formula; return el; }, async link (node) { const a = doc.createElement("a"); a.setAttribute("rel", "nofollow noopener noreferrer"); a.setAttribute("target", "_blank"); a.setAttribute('href', node.props.url); await appendChildren(node.children, a); return a; }, async mention (node) { const { username, host, acct } = node.props; const resolved = await resolveMentionFromCache(username, host, objectHost, mentionedRemoteUsers); const el = doc.createElement("span"); if (resolved === null) { el.textContent = acct; } else { el.setAttribute("class", "h-card"); el.setAttribute("translate", "no"); const a = doc.createElement("a"); a.setAttribute('href', resolved.href); a.className = "u-url mention"; const span = doc.createElement("span"); span.textContent = resolved.username; a.textContent = '@'; a.appendChild(span); el.appendChild(a); } return el; }, async quote (node) { const el = doc.createElement("blockquote"); await appendChildren(node.children, el); return el; }, text (node) { const el = doc.createElement("span"); const lines = node.props.text.split(/\r\n|\r|\n/); for (const x of intersperse("br", lines)){ if (x === "br") { el.appendChild(doc.createElement("br")); continue; } appendTextWithGlyphs(x, el); } return el; }, url (node) { const a = doc.createElement("a"); a.setAttribute("rel", "nofollow noopener noreferrer"); a.setAttribute("target", "_blank"); a.setAttribute('href', node.props.url); a.textContent = node.props.url.replace(/^https?:\/\//, ''); return a; }, search (node) { const a = doc.createElement("a"); a.setAttribute('href', `${config.searchEngine}${node.props.query}`); a.textContent = node.props.content; return a; }, async plain (node) { const el = doc.createElement("span"); await appendChildren(node.children, el); return el; } }; await appendChildren(nodes, doc.body); if (quoteUri !== null) { const a = doc.createElement("a"); a.setAttribute('href', quoteUri); a.textContent = quoteUri.replace(/^https?:\/\//, ''); const quote = doc.createElement("span"); quote.setAttribute("class", "quote-inline"); quote.appendChild(doc.createElement("br")); quote.appendChild(doc.createElement("br")); quote.innerHTML += 'RE: '; quote.appendChild(a); doc.body.appendChild(quote); } const html = inline ? doc.body.innerHTML : `
${doc.body.innerHTML}
`; await window.happyDOM.close(); return html; } }