import emojifyText from './emojify-text'; const fauxDiv = document.createElement('div'); function enhanceContent(content, opts = {}) { const { emojis, postEnhanceDOM = () => {} } = opts; let enhancedContent = content; const dom = document.createElement('div'); dom.innerHTML = enhancedContent; // Add target="_blank" to all links with no target="_blank" // E.g. `note` in `account` const links = Array.from(dom.querySelectorAll('a:not([target="_blank"])')); links.forEach((link) => { link.setAttribute('target', '_blank'); }); // Spanify un-spanned mentions const mentionLinks = Array.from(dom.querySelectorAll('a[href].mention')); mentionLinks.forEach((link) => { if (link.querySelector('*')) { return; } const text = link.innerText; // If text looks like @username@domain, then it's a mention if (/^@[^@]+@[^@]+$/g.test(text)) { // Only show @username const username = text.split('@')[1]; link.innerHTML = `@${username}`; } }); // EMOJIS // ====== // Convert :shortcode: to let textNodes = extractTextNodes(dom); textNodes.forEach((node) => { let html = node.nodeValue.replace(//g, '>'); if (emojis) { html = emojifyText(html, emojis); } fauxDiv.innerHTML = html; const nodes = Array.from(fauxDiv.childNodes); node.replaceWith(...nodes); }); // CODE BLOCKS // =========== // Convert ```code``` to
code
const blocks = Array.from(dom.querySelectorAll('p')).filter((p) =>
/^```[^]+```$/g.test(p.innerText.trim()),
);
blocks.forEach((block) => {
const pre = document.createElement('pre');
// Replace ${block.innerHTML.trim()}
`;
block.replaceWith(pre);
});
// Convert multi-paragraph code blocks to code
const paragraphs = Array.from(dom.querySelectorAll('p'));
// Filter out paragraphs with ``` in beginning only
const codeBlocks = paragraphs.filter((p) => /^```/g.test(p.innerText));
// For each codeBlocks, get all paragraphs until the last paragraph with ``` at the end only
codeBlocks.forEach((block) => {
const nextParagraphs = [block];
let hasCodeBlock = false;
let currentBlock = block;
while (currentBlock.nextElementSibling) {
const next = currentBlock.nextElementSibling;
if (next && next.tagName === 'P') {
if (/```$/g.test(next.innerText)) {
nextParagraphs.push(next);
hasCodeBlock = true;
break;
} else {
nextParagraphs.push(next);
}
} else {
break;
}
currentBlock = next;
}
if (hasCodeBlock) {
const pre = document.createElement('pre');
nextParagraphs.forEach((p) => {
// Replace ${codeText}
`;
block.replaceWith(pre);
nextParagraphs.forEach((p) => p.remove());
}
});
// INLINE CODE
// ===========
// Convert `code` to code
textNodes = extractTextNodes(dom);
textNodes.forEach((node) => {
let html = node.nodeValue.replace(//g, '>');
if (/`[^`]+`/g.test(html)) {
html = html.replaceAll(/(`[^]+?`)/g, '$1
');
}
fauxDiv.innerHTML = html;
const nodes = Array.from(fauxDiv.childNodes);
node.replaceWith(...nodes);
});
// TWITTER USERNAMES
// =================
// Convert @username@twitter.com to @username@twitter.com
textNodes = extractTextNodes(dom, {
rejectFilter: ['A'],
});
textNodes.forEach((node) => {
let html = node.nodeValue.replace(//g, '>');
if (/@[a-zA-Z0-9_]+@twitter\.com/g.test(html)) {
html = html.replaceAll(
/(@([a-zA-Z0-9_]+)@twitter\.com)/g,
'$1',
);
}
fauxDiv.innerHTML = html;
const nodes = Array.from(fauxDiv.childNodes);
node.replaceWith(...nodes);
});
if (postEnhanceDOM) {
postEnhanceDOM(dom); // mutate dom
}
enhancedContent = dom.innerHTML;
return enhancedContent;
}
const defaultRejectFilter = [
// Document metadata
'STYLE',
// Image and multimedia
'IMG',
'VIDEO',
'AUDIO',
'AREA',
'MAP',
'TRACK',
// Embedded content
'EMBED',
'IFRAME',
'OBJECT',
'PICTURE',
'PORTAL',
'SOURCE',
// SVG and MathML
'SVG',
'MATH',
// Scripting
'CANVAS',
'NOSCRIPT',
'SCRIPT',
// Forms
'INPUT',
'OPTION',
'TEXTAREA',
// Web Components
'SLOT',
'TEMPLATE',
];
const defaultRejectFilterMap = Object.fromEntries(
defaultRejectFilter.map((nodeName) => [nodeName, true]),
);
function extractTextNodes(dom, opts = {}) {
const textNodes = [];
const walk = document.createTreeWalker(
dom,
NodeFilter.SHOW_TEXT,
{
acceptNode(node) {
if (defaultRejectFilterMap[node.parentNode.nodeName]) {
return NodeFilter.FILTER_REJECT;
}
if (
opts.rejectFilter &&
opts.rejectFilter.includes(node.parentNode.nodeName)
) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
},
},
false,
);
let node;
while ((node = walk.nextNode())) {
textNodes.push(node);
}
return textNodes;
}
export default enhanceContent;