/** * Shared utilities for editor components */ /** * Convert HTML to plain text */ export const htmlToText = (html: string): string => { const div = document.createElement("div"); div.innerHTML = html; return div.textContent || div.innerText || ""; }; /** * Convert markdown to HTML (basic) */ export const markdownToHtml = (markdown: string): string => { let html = markdown; // Headers html = html.replace(/^### (.*$)/gim, "

$1

"); html = html.replace(/^## (.*$)/gim, "

$1

"); html = html.replace(/^# (.*$)/gim, "

$1

"); // Bold html = html.replace(/\*\*(.+?)\*\*/g, "$1"); // Italic html = html.replace(/\*(.+?)\*/g, "$1"); // Links html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1'); // Code html = html.replace(/`([^`]+)`/g, "$1"); // Line breaks html = html.replace(/\n/g, "
"); return html; }; /** * Serialize Tiptap JSON to HTML */ export const serializeToHtml = (json: any): string => { if (!json || !json.content) return ""; let html = ""; for (const node of json.content) { switch (node.type) { case "paragraph": html += `

${serializeContent(node.content)}

`; break; case "heading": html += `${serializeContent(node.content)}`; break; case "bulletList": html += ``; break; case "orderedList": html += `
    ${serializeContent(node.content)}
`; break; case "listItem": html += `
  • ${serializeContent(node.content)}
  • `; break; case "blockquote": html += `
    ${serializeContent(node.content)}
    `; break; case "codeBlock": html += `
    ${serializeContent(node.content)}
    `; break; default: if (node.content) { html += serializeContent(node.content); } } } return html; }; /** * Serialize content array */ const serializeContent = (content?: any[]): string => { if (!content) return ""; return content .map((node) => { switch (node.type) { case "text": { let text = node.text || ""; if (node.marks) { for (const mark of node.marks) { switch (mark.type) { case "bold": text = `${text}`; break; case "italic": text = `${text}`; break; case "code": text = `${text}`; break; case "link": text = `${text}`; break; case "strike": text = `${text}`; break; case "underline": text = `${text}`; break; } } } return text; } default: return serializeToHtml({ content: [node] }); } }) .join(""); }; /** * Get word count from text */ export const getWordCount = (text: string): number => { return text .trim() .split(/\s+/) .filter((word) => word.length > 0).length; }; /** * Get character count from text */ export const getCharacterCount = (text: string): number => { return text.length; }; /** * Estimate reading time in minutes */ export const getReadingTime = (text: string, wordsPerMinute = 200): number => { const words = getWordCount(text); return Math.ceil(words / wordsPerMinute); }; /** * Sanitize HTML to prevent XSS */ export const sanitizeHtml = (html: string): string => { const div = document.createElement("div"); div.textContent = html; return div.innerHTML; }; /** * Truncate text to specified length */ export const truncateText = ( text: string, maxLength: number, suffix = "...", ): string => { if (text.length <= maxLength) return text; return text.substring(0, maxLength - suffix.length) + suffix; }; /** * Extract plain text from Tiptap JSON */ export const extractText = (json: any): string => { if (!json || !json.content) return ""; const extractFromContent = (content: any[]): string => { return content .map((node) => { if (node.type === "text") { return node.text || ""; } if (node.content) { return extractFromContent(node.content); } return ""; }) .join(" "); }; return extractFromContent(json.content).trim(); }; /** * Insert text at cursor position in textarea */ export const insertAtCursor = ( textarea: HTMLTextAreaElement, text: string, before = "", after = "", ): void => { const start = textarea.selectionStart; const end = textarea.selectionEnd; const value = textarea.value; const newValue = value.substring(0, start) + before + text + after + value.substring(end); textarea.value = newValue; // Set cursor position after inserted text const newPosition = start + before.length + text.length; textarea.setSelectionRange(newPosition, newPosition); textarea.focus(); };