/** * Rich text content helpers for editor max-length and validation. */ export function stripRichTextToPlainText(html: string): string { if (!html) { return '' } if (typeof document !== 'undefined') { const template = document.createElement('template') template.innerHTML = html return (template.content.textContent || '').trim() } return html .replace(/<[^>]*>/g, ' ') .replace(/\s+/g, ' ') .trim() } export function getRichTextPlainTextLength(html: string): number { if (!html) { return 0 } return Array.from(stripRichTextToPlainText(html)).length } /** * Trim HTML to a maximum plain-text character count (images are kept, not counted). */ export function truncateRichTextToMaxPlainTextLength(html: string, maxLength: number): string { if (!html || maxLength <= 0) { return '' } if (getRichTextPlainTextLength(html) <= maxLength) { return html } if (typeof document === 'undefined') { const truncated = Array.from(stripRichTextToPlainText(html)).slice(0, maxLength).join('') return truncated ? `
${truncated}
` : '' } const template = document.createElement('template') template.innerHTML = html let remaining = maxLength const truncateNode = (node: Node): void => { const children = Array.from(node.childNodes) for (const child of children) { if (remaining <= 0) { if ( child.nodeType === Node.ELEMENT_NODE && (child as Element).tagName.toLowerCase() === 'img' ) { continue } child.remove() continue } if (child.nodeType === Node.TEXT_NODE) { const chars = Array.from(child.textContent || '') if (chars.length <= remaining) { remaining -= chars.length continue } child.textContent = chars.slice(0, remaining).join('') remaining = 0 continue } if (child.nodeType === Node.ELEMENT_NODE) { const element = child as Element if (element.tagName.toLowerCase() === 'img') { continue } truncateNode(child) if (!element.textContent?.trim() && !element.querySelector('img')) { element.remove() } } } } truncateNode(template.content) return template.innerHTML }