]*>)(https?:\/\/[^<]+)(<\/a>)/gi,
(match, openTag, urlText, closeTag) => {
if (urlText.length <= maxLength) return match;
// Strip protocol for display
const display = urlText.replace(/^https?:\/\//, "");
const truncated = display.slice(0, maxLength - 1) + "\u2026";
// Add title attribute with full URL for hover tooltip (if not already present)
let tag = openTag;
if (!tag.includes("title=")) {
tag = tag.replace(/>$/, ` title="${urlText}">`);
}
return `${tag}${truncated}${closeTag}`;
},
);
}
/**
* Collapse paragraphs that are mostly hashtag links (hashtag stuffing).
* Detects blocks where 80%+ of the text content is hashtag links
* and wraps them in a element.
*
* @param {string} html - Sanitized HTML content
* @param {number} [minTags=3] - Minimum number of hashtag links to trigger collapse
* @returns {string} HTML with hashtag-heavy paragraphs collapsed
*/
export function collapseHashtagStuffing(html, minTags = 3) {
if (!html) return html;
// Match blocks
return html.replace(/
([^]*?)<\/p>/gi, (match, inner) => {
// Count hashtag links: #something or plain #word
const hashtagLinks = inner.match(/]*>#[^<]+<\/a>/gi) || [];
if (hashtagLinks.length < minTags) return match;
// Calculate what fraction of text content is hashtags
const textOnly = inner.replace(/<[^>]*>/g, "").trim();
const hashtagText = hashtagLinks
.map((link) => link.replace(/<[^>]*>/g, "").trim())
.join(" ");
// If hashtags make up 80%+ of the text content, collapse
if (hashtagText.length / Math.max(textOnly.length, 1) >= 0.8) {
return `Show ${hashtagLinks.length} tags
${inner}
`;
}
return match;
});
}