/** * HTML Compatibility Layer * Handles HTML entities, formatting, and class conversions from WordPress exports */ import { getMediaById } from './data'; /** * Process HTML content from WordPress * - Sanitizes dangerous content * - Converts HTML entities * - Removes scripts and styles * - Processes shortcodes */ export function processHTML(html: string | null | undefined): string { if (!html) return ''; let processed = html; // Step 1: Replace HTML entities processed = replaceHTMLEntities(processed); // Step 2: Remove dangerous content processed = sanitizeHTML(processed); // Step 3: Process WordPress shortcodes processed = processShortcodes(processed); // Step 4: Clean up whitespace processed = cleanWhitespace(processed); return processed; } /** * Replace common HTML entities with their actual characters */ function replaceHTMLEntities(html: string): string { const entities: Record = { '\u00A0': ' ', // Non-breaking space '&': '&', '<': '<', '>': '>', '"': '"', "'": "'", '¢': '¢', '£': '£', '¥': '¥', '€': '€', '©': '©', '®': '®', '™': '™', '°': '°', '±': '±', '×': '×', '÷': '÷', 'µ': 'µ', '¶': '¶', '§': '§', 'á': 'á', 'é': 'é', 'í': 'í', 'ó': 'ó', 'ú': 'ú', 'Á': 'Á', 'É': 'É', 'Í': 'Í', 'Ó': 'Ó', 'Ú': 'Ú', 'ñ': 'ñ', 'Ñ': 'Ñ', 'ü': 'ü', 'Ü': 'Ü', 'ö': 'ö', 'Ö': 'Ö', 'ä': 'ä', 'Ä': 'Ä', 'ß': 'ß', '—': '—', '–': '–', '…': '…', '«': '«', '»': '»', '‘': "'", '’': "'", '“': '"', '”': '"', '•': '•', '·': '·', // Additional common entities that might appear in WordPress exports '„': '"', // Double low-reversed-9 quote '‟': '"', // Double high-reversed-9 quote '′': "'", // Prime '″': '"', // Double prime '‹': '<', // Single left-pointing angle quotation mark '›': '>', // Single right-pointing angle quotation mark '†': '†', // Dagger '‡': '‡', // Double dagger '‰': '‰', // Per mille }; let processed = html; for (const [entity, char] of Object.entries(entities)) { processed = processed.replace(new RegExp(entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), char); } return processed; } /** * Sanitize HTML by removing dangerous tags and attributes */ function sanitizeHTML(html: string): string { let processed = html; // Remove script tags processed = processed.replace(/)<[^<]*)*<\/script>/gi, ''); // Remove style tags processed = processed.replace(/)<[^<]*)*<\/style>/gi, ''); // Remove inline event handlers processed = processed.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, ''); // Remove dangerous attributes processed = processed.replace(/\s+(href|src)\s*=\s*["']\s*javascript:/gi, ''); // Note: Shortcode removal is handled in processShortcodes function // Don't remove shortcodes here as they need to be processed first // Allow safe HTML tags const allowedTags = [ 'p', 'br', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'b', 'em', 'i', 'u', 'small', 'ul', 'ol', 'li', 'a', 'div', 'span', 'img', 'section', 'article', 'figure', 'figcaption', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'blockquote', 'code', 'pre', 'hr' ]; const tagPattern = allowedTags.join('|'); processed = processed.replace( new RegExp(`<\/?(?!\\/?(?:${tagPattern})(\\s|>))[^>]*>`, 'gi'), '' ); return processed; } /** * Strip ChatGPT artifacts from HTML content * Removes react-scroll-to-bottom--css-* classes and data-message-* attributes */ function stripChatGPTArtifacts(html: string): string { let processed = html; // Remove react-scroll-to-bottom CSS classes processed = processed.replace(/class=["'][^"']*react-scroll-to-bottom--css[^"']*["']/gi, ''); // Remove data-message-* attributes processed = processed.replace(/\s+data-message-[^\s=]*=["'][^"']*["']/gi, ''); // Clean up multiple spaces that might result from removal processed = processed.replace(/\s+/g, ' '); // Clean up empty class attributes processed = processed.replace(/\s+class=["']\s*["']/gi, ''); return processed; } /** * Process [split_line_heading] shortcode * Converts to split h2 with line styling */ function processSplitLineHeading(html: string): string { return html.replace(/\[split_line_heading([^\]]*)\]([\s\S]*?)\[\/split_line_heading\]/g, (match, attrs, content) => { // Extract alignment from attributes const alignMatch = attrs.match(/align=["']([^"']*)["']/i); const align = alignMatch ? alignMatch[1] : 'left'; // Parse the content - it might contain HTML or just text // Format: "Line 1|Line 2" or "Line 1|Line 2|Line 3" const lines = content.split('|').map((line: string) => line.trim()); // Build the HTML structure const classes = ['split-line-heading', 'text-center']; if (align === 'center') classes.push('text-center'); if (align === 'right') classes.push('text-right'); if (align === 'left') classes.push('text-left'); let html = `
`; html += '

'; lines.forEach((line: string, index: number) => { if (index > 0) { html += ''; } html += `${line}`; }); html += '

'; html += '
'; return html; }); } /** * Process WordPress shortcodes by converting them to HTML with proper styling * Also handles mixed scenarios where some content is already HTML with WordPress classes */ export function processShortcodes(html: string): string { let processed = html; try { // Step 0: Decode HTML entities in shortcode attributes processed = replaceHTMLEntities(processed); // Step 0.5: Strip ChatGPT artifacts (react-scroll-*, data-message-*) processed = stripChatGPTArtifacts(processed); // Step 0.6: Process split_line_heading shortcode processed = processSplitLineHeading(processed); // Check if input has divs that need conversion const hasDivs = /]*class=["'][^"']*(?:vc-row|vc_row|vc_row_inner|vc-column|vc_column|vc_column_inner|vc-column-text|vc_column_text)[^"']*["'][^>]*>/i.test(processed); const hasShortcodes = /\[(vc_row|vc_column|vc_column_text|vc_single_image|vc_btn|vc_separator|vc_video)/i.test(processed); // Only convert divs to shortcodes if there are divs but no existing shortcodes if (hasDivs && !hasShortcodes) { // Use a stack-based approach to handle nested divs const stack: string[] = []; let result = ''; let i = 0; while (i < processed.length) { // Check for opening div tags const openDivMatch = processed.slice(i).match(/^]*class=["'][^"']*(?:vc-row|vc_row|vc_row_inner|vc-column|vc_column|vc_column_inner|vc-column-text|vc_column_text)[^"']*["'][^>]*>/i); if (openDivMatch) { const attrs = extractAttributesFromHTML(openDivMatch[0]); let tag: string; if (openDivMatch[0].includes('vc_row_inner') || openDivMatch[0].includes('vc-row-inner')) { tag = 'vc_row_inner'; } else if (openDivMatch[0].includes('vc-row') || openDivMatch[0].includes('vc_row')) { tag = 'vc_row'; } else if (openDivMatch[0].includes('vc_column_inner') || openDivMatch[0].includes('vc-column-inner')) { tag = 'vc_column_inner'; } else if (openDivMatch[0].includes('vc-column') || openDivMatch[0].includes('vc_column')) { tag = 'vc_column'; } else if (openDivMatch[0].includes('vc-column-text') || openDivMatch[0].includes('vc_column_text')) { tag = 'vc_column_text'; } else { // Unknown tag, skip result += openDivMatch[0]; i += openDivMatch[0].length; continue; } stack.push(tag); result += `[${tag} ${attrs}]`; i += openDivMatch[0].length; continue; } // Check for closing div if (processed.slice(i, i+6) === '') { if (stack.length > 0) { const tag = stack.pop(); result += `[/${tag}]`; i += 6; continue; } } // Check for img tags (vc_single_image) const imgMatch = processed.slice(i).match(/^]*class=["'][^"']*(?:vc-single-image|vc_single_image)[^"']*["'][^>]*>/i); if (imgMatch) { const attrs = extractAttributesFromHTML(imgMatch[0]); const imageId = extractAttribute(attrs, 'data-wp-image-id') || extractAttribute(attrs, 'src'); const width = extractAttribute(attrs, 'data-width') || ''; result += `[vc_single_image src="${imageId}" width="${width}"]`; i += imgMatch[0].length; continue; } // Check for anchor tags (vc_btn) const anchorMatch = processed.slice(i).match(/^]*class=["'][^"']*(?:vc-btn|vc_btn)[^"']*["'][^>]*>(.*?)<\/a>/i); if (anchorMatch) { const attrs = extractAttributesFromHTML(anchorMatch[0]); const href = extractAttribute(attrs, 'href'); const title = anchorMatch[1]; // Content between tags result += `[vc_btn href="${href}" title="${title}"]`; i += anchorMatch[0].length; continue; } // Check for hr tags (vc_separator) const hrMatch = processed.slice(i).match(/^]*class=["'][^"']*(?:vc-separator|vc_separator)[^"']*["'][^>]*>/i); if (hrMatch) { const attrs = extractAttributesFromHTML(hrMatch[0]); result += `[vc_separator ${attrs}]`; i += hrMatch[0].length; continue; } // Regular character result += processed[i]; i++; } processed = result; } // Step 2: Process shortcode blocks into HTML processed = processVcRowShortcodes(processed); processed = processVcColumnShortcodes(processed); processed = processVcColumnTextShortcodes(processed); processed = processVcImageShortcodes(processed); processed = processVcButtonShortcodes(processed); processed = processVcSeparatorShortcodes(processed); processed = processVcVideoShortcodes(processed); processed = processBackgroundShortcodes(processed); // Step 3: Check for unprocessed shortcodes and log them const unprocessedShortcodes = processed.match(/\[[^\]]*\]/g); if (unprocessedShortcodes && unprocessedShortcodes.length > 0) { console.warn('Unprocessed shortcodes found and will be removed:', unprocessedShortcodes); } // Clean up any remaining shortcode artifacts processed = processed.replace(/\[[^\]]*\]/g, ''); // Step 4: Clean up any remaining empty div tags processed = processed.replace(/
\s*<\/div>/g, ''); return processed; } catch (error) { console.error('Error processing shortcodes:', error); return html; } } /** * Extract attributes from HTML tag */ function extractAttributesFromHTML(html: string): string { // Extract all key="value" pairs from HTML tag const attrMatches = html.matchAll(/([a-zA-Z-]+)=["']([^"']*)["']/g); const attrs: string[] = []; for (const match of attrMatches) { const key = match[1]; const value = match[2]; // Map HTML data attributes back to shortcode attributes if (key.startsWith('data-')) { const shortcodeKey = key.replace('data-', '').replace(/-([a-z])/g, (g) => g[1].toUpperCase()); attrs.push(`${shortcodeKey}="${value}"`); } else if (key === 'class') { // Skip class attribute for shortcode conversion continue; } else { attrs.push(`${key}="${value}"`); } } return attrs.join(' '); } /** * Process [vc_row] shortcodes and convert to flex containers * Also handles underscored versions: vc_row, vc_row_inner */ function processVcRowShortcodes(html: string): string { return html.replace(/\[vc_row(?:_inner)?([^\]]*)\]([\s\S]*?)\[\/vc_row(?:_inner)?\]/g, (match, attrs, content) => { const classes = ['vc-row', 'flex', 'flex-wrap', '-mx-4']; // Parse attributes for background colors, images, etc. // Support both snake_case (shortcode) and camelCase (from data attributes) const bgImage = extractAttribute(attrs, 'bg_image') || extractAttribute(attrs, 'bgImage'); const bgColor = extractAttribute(attrs, 'bg_color') || extractAttribute(attrs, 'bgColor'); const colorOverlay = extractAttribute(attrs, 'color_overlay') || extractAttribute(attrs, 'colorOverlay'); const colorOverlay2 = extractAttribute(attrs, 'color_overlay_2') || extractAttribute(attrs, 'colorOverlay2'); const overlayStrength = extractAttribute(attrs, 'overlay_strength') || extractAttribute(attrs, 'overlayStrength'); const enableGradient = extractAttribute(attrs, 'enable_gradient') || extractAttribute(attrs, 'enableGradient'); const gradientDirection = extractAttribute(attrs, 'gradient_direction') || extractAttribute(attrs, 'gradientDirection'); const topPadding = extractAttribute(attrs, 'top_padding') || extractAttribute(attrs, 'topPadding'); const bottomPadding = extractAttribute(attrs, 'bottom_padding') || extractAttribute(attrs, 'bottomPadding'); const fullScreen = extractAttribute(attrs, 'full_screen_row_position') || extractAttribute(attrs, 'fullScreenRowPosition'); const videoBg = extractAttribute(attrs, 'video_bg') || extractAttribute(attrs, 'videoBg'); const videoMp4 = extractAttribute(attrs, 'video_mp4') || extractAttribute(attrs, 'videoMp4'); const videoWebm = extractAttribute(attrs, 'video_webm') || extractAttribute(attrs, 'videoWebm'); const textAlign = extractAttribute(attrs, 'text_align') || extractAttribute(attrs, 'textAlign'); const textColor = extractAttribute(attrs, 'text_color') || extractAttribute(attrs, 'textColor'); const overflow = extractAttribute(attrs, 'overflow'); const equalHeight = extractAttribute(attrs, 'equal_height') || extractAttribute(attrs, 'equalHeight'); const contentPlacement = extractAttribute(attrs, 'content_placement') || extractAttribute(attrs, 'contentPlacement'); const columnDirection = extractAttribute(attrs, 'column_direction') || extractAttribute(attrs, 'columnDirection'); const rowBorderRadius = extractAttribute(attrs, 'row_border_radius') || extractAttribute(attrs, 'rowBorderRadius'); const rowBorderRadiusApplies = extractAttribute(attrs, 'row_border_radius_applies') || extractAttribute(attrs, 'rowBorderRadiusApplies'); // Build style string let style = ''; let wrapperClasses = [...classes]; // Handle text alignment if (textAlign === 'center') wrapperClasses.push('text-center'); if (textAlign === 'right') wrapperClasses.push('text-right'); if (textAlign === 'left') wrapperClasses.push('text-left'); // Handle text color if (textColor === 'light') wrapperClasses.push('text-white'); // Handle overflow if (overflow === 'visible') wrapperClasses.push('overflow-visible'); // Handle equal height if (equalHeight === 'yes') { wrapperClasses.push('items-stretch'); wrapperClasses.push('flex'); } // Handle content placement if (contentPlacement === 'bottom') wrapperClasses.push('justify-end'); if (contentPlacement === 'middle') wrapperClasses.push('justify-center'); // Handle column direction if (columnDirection === 'column') wrapperClasses.push('flex-col'); // Handle border radius if (rowBorderRadius === 'none' && rowBorderRadiusApplies === 'bg') { wrapperClasses.push('rounded-none'); } // Handle background image - FIXED: Support both numeric IDs and local paths if (bgImage) { // Check if it's already a local path (from processed data) if (bgImage.startsWith('/media/')) { wrapperClasses.push('bg-cover', 'bg-center'); style += `background-image: url(${bgImage}); `; } else { // Try to parse as numeric ID const mediaId = parseInt(bgImage); if (!isNaN(mediaId)) { // Use getMediaById to get the actual file path const media = getMediaById(mediaId); if (media) { wrapperClasses.push('bg-cover', 'bg-center'); style += `background-image: url(${media.localPath}); `; } else { // Fallback if media not found wrapperClasses.push('bg-cover', 'bg-center'); style += `background-image: url(/media/${bgImage}.webp); `; } } else { // Assume it's a direct URL style += `background-image: url(${bgImage}); `; } } style += `background-size: cover; `; style += `background-position: center; `; } // Handle background color if (bgColor) { style += `background-color: ${bgColor}; `; } // Handle video background if (videoBg === 'use_video' && (videoMp4 || videoWebm)) { // Mark for ContentRenderer to handle wrapperClasses.push('relative', 'overflow-hidden'); style += `position: relative; `; // Create video background structure with data attributes const videoAttrs = []; if (videoMp4) videoAttrs.push(`data-video-mp4="${videoMp4}"`); if (videoWebm) videoAttrs.push(`data-video-webm="${videoWebm}"`); videoAttrs.push('data-video-bg="true"'); return `
${content}
`; } // Handle video attributes even if not using video_bg flag (enhanced preservation) if (videoMp4 || videoWebm) { // Add video attributes to wrapper for preservation if (videoMp4) wrapperClasses.push(`has-video-mp4`); if (videoWebm) wrapperClasses.push(`has-video-webm`); // Store video URLs in data attributes for later use const videoDataAttrs = []; if (videoMp4) videoDataAttrs.push(`data-video-mp4="${videoMp4}"`); if (videoWebm) videoDataAttrs.push(`data-video-webm="${videoWebm}"`); // If there's no other special handling, just add the attributes to the div if (!bgImage && !bgColor && !colorOverlay && !enableGradient) { return `
${content}
`; } // For complex backgrounds with video, add video attrs to existing structure // We'll handle this in the ContentRenderer const existingHtml = `
${content}
`; // Add video attributes as data attributes on the wrapper return existingHtml.replace('
${content}
`; return resultHtml; } // Handle gradient (without overlay) if (enableGradient === 'true' || enableGradient === '1') { const gradientClass = getGradientClass(gradientDirection); wrapperClasses.push(gradientClass); } // Handle padding if (topPadding || bottomPadding) { // Convert percentage values to Tailwind arbitrary values const pt = topPadding ? `pt-[${topPadding}]` : ''; const pb = bottomPadding ? `pb-[${bottomPadding}]` : ''; if (pt) wrapperClasses.push(pt); if (pb) wrapperClasses.push(pb); } // Handle full screen if (fullScreen === 'middle') { wrapperClasses.push('min-h-screen', 'flex', 'items-center'); } // Don't return empty divs if (!content || content.trim() === '') { return ''; } return `
${content}
`; }); } /** * Process [vc_column] shortcodes */ function processVcColumnShortcodes(html: string): string { return html.replace(/\[vc_column([^\]]*)\]([\s\S]*?)\[\/vc_column\]/g, (match, attrs, content) => { const width = extractAttribute(attrs, 'width') || '12'; const classes = ['vc-column', 'px-4']; // Convert width to Tailwind classes if (width === '12' || width === 'full') { classes.push('w-full'); } else if (width === '6') { classes.push('w-full', 'md:w-1/2'); } else if (width === '4') { classes.push('w-full', 'md:w-1/3'); } else if (width === '3') { classes.push('w-full', 'md:w-1/4'); } return `
${content}
`; }); } /** * Process [vc_column_text] shortcodes */ function processVcColumnTextShortcodes(html: string): string { return html.replace(/\[vc_column_text([^\]]*)\]([\s\S]*?)\[\/vc_column_text\]/g, (match, attrs, content) => { const classes = ['vc-column-text', 'prose', 'max-w-none']; // Handle text alignment const align = extractAttribute(attrs, 'text_align'); if (align === 'center') classes.push('text-center'); if (align === 'right') classes.push('text-right'); return `
${content}
`; }); } /** * Process [vc_single_image] shortcodes */ function processVcImageShortcodes(html: string): string { return html.replace(/\[vc_single_image([^\]]*)\]/g, (match, attrs) => { const imageId = extractAttribute(attrs, 'src') || extractAttribute(attrs, 'image'); const align = extractAttribute(attrs, 'align') || 'none'; const width = extractAttribute(attrs, 'width'); const classes = ['vc-single-image', 'my-4']; // Handle alignment if (align === 'center') classes.push('mx-auto'); if (align === 'left') classes.push('float-left', 'mr-4', 'mb-4'); if (align === 'right') classes.push('float-right', 'ml-4', 'mb-4'); // Use data attribute for image ID to be processed by ContentRenderer return ``; }); } /** * Process [vc_btn] and [vc_button] shortcodes */ function processVcButtonShortcodes(html: string): string { return html.replace(/\[vc_btn([^\]]*)\]/g, (match, attrs) => { const title = extractAttribute(attrs, 'title') || 'Click Here'; const href = extractAttribute(attrs, 'href') || extractAttribute(attrs, 'link'); const color = extractAttribute(attrs, 'color') || 'primary'; const size = extractAttribute(attrs, 'size') || 'md'; const classes = ['vc-btn', 'inline-flex', 'items-center', 'justify-center', 'px-4', 'py-2', 'rounded-lg', 'font-semibold', 'transition-colors', 'duration-200']; // Color mapping if (color === 'primary' || color === 'skype') classes.push('bg-primary', 'text-white', 'hover:bg-primary-dark'); else if (color === 'secondary') classes.push('bg-secondary', 'text-white', 'hover:bg-secondary-light'); else if (color === 'ghost' || color === 'outline') classes.push('border-2', 'border-primary', 'text-primary', 'hover:bg-primary', 'hover:text-white'); else if (color === 'white') classes.push('bg-white', 'text-gray-900', 'hover:bg-gray-100'); // Size mapping if (size === 'lg' || size === 'large') classes.push('px-6', 'py-3', 'text-lg'); if (size === 'sm' || size === 'small') classes.push('px-3', 'py-1', 'text-sm'); if (href) { return `${title}`; } return ``; }); } /** * Process [vc_separator] and [vc_text_separator] shortcodes */ function processVcSeparatorShortcodes(html: string): string { return html.replace(/\[vc_separator([^\]]*)\]/g, (match, attrs) => { const color = extractAttribute(attrs, 'color') || 'default'; const width = extractAttribute(attrs, 'width') || '100'; const thickness = extractAttribute(attrs, 'thickness') || '1'; const classes = ['vc-separator', 'my-6']; // Color mapping if (color === 'primary') classes.push('border-primary'); else if (color === 'secondary') classes.push('border-secondary'); else if (color === 'white') classes.push('border-white'); else classes.push('border-gray-300'); // Width and thickness const style = `width: ${width}%; border-top-width: ${thickness}px;`; return `
`; }); } /** * Process [vc_video] shortcodes */ function processVcVideoShortcodes(html: string): string { return html.replace(/\[vc_video([^\]]*)\]/g, (match, attrs) => { const link = extractAttribute(attrs, 'link'); const mp4 = extractAttribute(attrs, 'mp4'); const webm = extractAttribute(attrs, 'webm'); if (mp4 || webm) { // Video background const poster = extractAttribute(attrs, 'poster'); return `
`; } if (link) { // Embedded video (YouTube, Vimeo, etc.) return `
`; } return ''; }); } /** * Process background-related shortcodes and attributes */ function processBackgroundShortcodes(html: string): string { // Handle background image attributes in divs // Support both numeric IDs (from raw data) and local paths (from processed data) html = html.replace(/bg_image="([^"]+)"/g, (match, imageValue) => { // If it's a numeric ID, keep it as-is for now (will be handled by ContentRenderer) // If it's already a local path, convert to data-bg-image if (/^\d+$/.test(imageValue)) { // Numeric ID - keep as data attribute for ContentRenderer to resolve return `data-bg-image="${imageValue}"`; } else if (imageValue.startsWith('/media/')) { // Already a local path - use directly return `data-bg-image="${imageValue}"`; } else { // Unknown format - keep as-is return `data-bg-image="${imageValue}"`; } }); // Handle video background attributes - enhanced to preserve all video data html = html.replace(/video_bg="use_video"/g, 'data-video-bg="true"'); html = html.replace(/video_mp4="([^"]+)"/g, (match, url) => { // Ensure URL is properly formatted const cleanUrl = url.trim().replace(/^["']|["']$/g, ''); return `data-video-mp4="${cleanUrl}"`; }); html = html.replace(/video_webm="([^"]+)"/g, (match, url) => { // Ensure URL is properly formatted const cleanUrl = url.trim().replace(/^["']|["']$/g, ''); return `data-video-webm="${cleanUrl}"`; }); // Handle parallax html = html.replace(/parallax_bg="true"/g, 'data-parallax="true"'); // Also handle video attributes that might appear without the video_bg flag // This ensures video data is preserved even if the flag is missing html = html.replace(/\s+mp4="([^"]+)"/g, (match, url) => ` data-video-mp4="${url}"`); html = html.replace(/\s+webm="([^"]+)"/g, (match, url) => ` data-video-webm="${url}"`); return html; } /** * Extract attribute value from shortcode attributes * Handles complex patterns with quotes, special characters, and spaces */ function extractAttribute(attrs: string, key: string): string | null { // First try: key="value" or key='value' const quotedPattern = new RegExp(`${key}=["']([^"']*)["']`, 'i'); const quotedMatch = attrs.match(quotedPattern); if (quotedMatch) return quotedMatch[1]; // Second try: key=value (without quotes, until space or end) const unquotedPattern = new RegExp(`${key}=([^\\s\\]]+)`, 'i'); const unquotedMatch = attrs.match(unquotedPattern); if (unquotedMatch) return unquotedMatch[1]; return null; } /** * Get Tailwind gradient class from gradient direction */ function getGradientClass(direction: string): string { const gradientMap: Record = { 'left_to_right': 'bg-gradient-to-r from-primary to-secondary', 'right_to_left': 'bg-gradient-to-l from-primary to-secondary', 'top_to_bottom': 'bg-gradient-to-b from-primary to-secondary', 'bottom_to_top': 'bg-gradient-to-t from-primary to-secondary', 'left_t_to_right_b': 'bg-gradient-to-br from-primary to-secondary', 'default': 'bg-gradient-to-r from-primary to-secondary', }; return gradientMap[direction] || gradientMap['default']; } /** * Clean up whitespace and normalize spacing */ function cleanWhitespace(html: string): string { let processed = html; // Remove empty paragraphs processed = processed.replace(/

\s*<\/p>/g, ''); processed = processed.replace(/

\s* \s*<\/p>/g, ''); // Remove multiple spaces processed = processed.replace(/\s+/g, ' '); // Remove spaces around block elements processed = processed.replace(/\s*(<\/?(h[1-6]|div|section|article|p|ul|ol|li|table|tr|td|th|blockquote|figure|figcaption|br|hr)\s*>)\s*/g, '$1'); // Trim processed = processed.trim(); return processed; } /** * Convert WordPress/Salient classes to Tailwind equivalents */ export function convertWordPressClasses(html: string): string { if (!html) return ''; const classMap: Record = { // Layout classes 'vc_row': 'flex flex-wrap -mx-4', 'vc_row-fluid': 'w-full', 'vc_row-full-width': 'w-full -mx-4', 'vc_row-o-content-top': 'items-start', 'vc_row-o-content-middle': 'items-center', 'vc_row-o-content-bottom': 'items-end', // Column classes 'vc_col-sm-12': 'w-full px-4', 'vc_col-md-6': 'w-full md:w-1/2 px-4', 'vc_col-md-4': 'w-full md:w-1/3 px-4', 'vc_col-md-3': 'w-full md:w-1/4 px-4', 'vc_col-lg-6': 'w-full lg:w-1/2 px-4', 'vc_col-lg-4': 'w-full lg:w-1/3 px-4', 'vc_col-lg-3': 'w-full lg:w-1/4 px-4', 'vc_col-xs-12': 'w-full px-4', // Wrapper classes 'wpb_wrapper': 'space-y-4', 'wpb_text_column': 'prose max-w-none', 'wpb_content_element': 'mb-8', 'wpb_single_image': 'my-4', // Typography 'wpb_heading': 'text-2xl font-bold mb-2', 'wpb_button': 'inline-block px-4 py-2 rounded-lg font-semibold', // Alignment 'text-left': 'text-left', 'text-center': 'text-center', 'text-right': 'text-right', 'alignleft': 'float-left mr-4 mb-4', 'alignright': 'float-right ml-4 mb-4', 'aligncenter': 'mx-auto', // Colors 'accent-color': 'text-primary', 'primary-color': 'text-primary', 'secondary-color': 'text-secondary', 'text-color': 'text-gray-800', 'light-text': 'text-gray-300', 'dark-text': 'text-gray-900', // Backgrounds 'bg-light': 'bg-gray-50', 'bg-light-gray': 'bg-gray-100', 'bg-dark': 'bg-gray-900', 'bg-dark-gray': 'bg-gray-800', 'bg-primary': 'bg-primary', 'bg-secondary': 'bg-secondary', 'bg-white': 'bg-white', 'bg-transparent': 'bg-transparent', // Buttons 'btn': 'inline-flex items-center justify-center px-4 py-2 rounded-lg font-semibold transition-colors duration-200', 'btn-primary': 'bg-primary text-white hover:bg-primary-dark', 'btn-secondary': 'bg-secondary text-white hover:bg-secondary-light', 'btn-outline': 'border-2 border-primary text-primary hover:bg-primary hover:text-white', 'btn-large': 'px-6 py-3 text-lg', 'btn-small': 'px-3 py-1 text-sm', // Spacing 'mt-0': 'mt-0', 'mb-0': 'mb-0', 'mt-2': 'mt-2', 'mb-2': 'mb-2', 'mt-4': 'mt-4', 'mb-4': 'mb-4', 'mt-6': 'mt-6', 'mb-6': 'mb-6', 'mt-8': 'mt-8', 'mb-8': 'mb-8', 'mt-12': 'mt-12', 'mb-12': 'mb-12', // Containers 'container': 'container mx-auto px-4', 'container-fluid': 'w-full px-4', // Visibility 'hidden': 'hidden', 'visible': 'visible', 'block': 'block', 'inline': 'inline', 'inline-block': 'inline-block', // Borders 'border': 'border', 'border-0': 'border-0', 'border-t': 'border-t', 'border-b': 'border-b', 'border-l': 'border-l', 'border-r': 'border-r', // Shadows 'shadow': 'shadow', 'shadow-sm': 'shadow-sm', 'shadow-md': 'shadow-md', 'shadow-lg': 'shadow-lg', 'shadow-xl': 'shadow-xl', // Rounded 'rounded': 'rounded', 'rounded-none': 'rounded-none', 'rounded-sm': 'rounded-sm', 'rounded-lg': 'rounded-lg', 'rounded-full': 'rounded-full', // Overflow 'overflow-hidden': 'overflow-hidden', 'overflow-auto': 'overflow-auto', 'overflow-scroll': 'overflow-scroll', // Position 'relative': 'relative', 'absolute': 'absolute', 'fixed': 'fixed', 'sticky': 'sticky', // Z-index 'z-0': 'z-0', 'z-10': 'z-10', 'z-20': 'z-20', 'z-30': 'z-30', 'z-40': 'z-40', 'z-50': 'z-50', // Width 'w-full': 'w-full', 'w-1/2': 'w-1/2', 'w-1/3': 'w-1/3', 'w-2/3': 'w-2/3', 'w-1/4': 'w-1/4', 'w-3/4': 'w-3/4', // Height 'h-full': 'h-full', 'h-screen': 'h-screen', 'h-32': 'h-32', 'h-48': 'h-48', 'h-64': 'h-64', // Flexbox 'flex': 'flex', 'flex-col': 'flex-col', 'flex-row': 'flex-row', 'flex-wrap': 'flex-wrap', 'flex-nowrap': 'flex-nowrap', 'justify-start': 'justify-start', 'justify-center': 'justify-center', 'justify-end': 'justify-end', 'justify-between': 'justify-between', 'justify-around': 'justify-around', 'items-start': 'items-start', 'items-center': 'items-center', 'items-end': 'items-end', 'items-stretch': 'items-stretch', // Grid (if used) 'grid': 'grid', 'grid-cols-1': 'grid-cols-1', 'grid-cols-2': 'grid-cols-2', 'grid-cols-3': 'grid-cols-3', 'grid-cols-4': 'grid-cols-4', 'gap-2': 'gap-2', 'gap-4': 'gap-4', 'gap-6': 'gap-6', 'gap-8': 'gap-8', // Padding 'p-0': 'p-0', 'p-2': 'p-2', 'p-4': 'p-4', 'p-6': 'p-6', 'p-8': 'p-8', 'p-12': 'p-12', 'px-4': 'px-4', 'py-4': 'py-4', 'pt-4': 'pt-4', 'pb-4': 'pb-4', // Margin 'm-0': 'm-0', 'm-2': 'm-2', 'm-4': 'm-4', 'm-6': 'm-6', 'm-8': 'm-8', 'mx-auto': 'mx-auto', // Text transform 'uppercase': 'uppercase', 'lowercase': 'lowercase', 'capitalize': 'capitalize', 'normal-case': 'normal-case', // Font weight 'font-light': 'font-light', 'font-normal': 'font-normal', 'font-medium': 'font-medium', 'font-semibold': 'font-semibold', 'font-bold': 'font-bold', // Text size 'text-xs': 'text-xs', 'text-sm': 'text-sm', 'text-base': 'text-base', 'text-lg': 'text-lg', 'text-xl': 'text-xl', 'text-2xl': 'text-2xl', 'text-3xl': 'text-3xl', 'text-4xl': 'text-4xl', // Text color 'text-white': 'text-white', 'text-black': 'text-black', 'text-gray-100': 'text-gray-100', 'text-gray-200': 'text-gray-200', 'text-gray-300': 'text-gray-300', 'text-gray-400': 'text-gray-400', 'text-gray-500': 'text-gray-500', 'text-gray-600': 'text-gray-600', 'text-gray-700': 'text-gray-700', 'text-gray-800': 'text-gray-800', 'text-gray-900': 'text-gray-900', // Background color (continued) 'bg-gray-100': 'bg-gray-100', 'bg-gray-200': 'bg-gray-200', 'bg-gray-300': 'bg-gray-300', 'bg-gray-400': 'bg-gray-400', 'bg-gray-500': 'bg-gray-500', 'bg-gray-600': 'bg-gray-600', 'bg-gray-700': 'bg-gray-700', 'bg-gray-800': 'bg-gray-800', 'bg-gray-900': 'bg-gray-900', // Opacity 'opacity-0': 'opacity-0', 'opacity-25': 'opacity-25', 'opacity-50': 'opacity-50', 'opacity-75': 'opacity-75', 'opacity-100': 'opacity-100', // Display (continued) 'inline-flex': 'inline-flex', 'contents': 'contents', // Object fit 'object-cover': 'object-cover', 'object-contain': 'object-contain', 'object-fill': 'object-fill', 'object-none': 'object-none', 'object-scale-down': 'object-scale-down', // Aspect ratio 'aspect-square': 'aspect-square', 'aspect-video': 'aspect-video', 'aspect-[4/3]': 'aspect-[4/3]', 'aspect-[16/9]': 'aspect-[16/9]', // Transforms 'transform': 'transform', 'scale-95': 'scale-95', 'scale-100': 'scale-100', 'scale-105': 'scale-105', 'rotate-0': 'rotate-0', 'rotate-45': 'rotate-45', 'rotate-90': 'rotate-90', // Transitions 'transition': 'transition', 'transition-all': 'transition-all', 'transition-colors': 'transition-colors', 'transition-opacity': 'transition-opacity', 'transition-transform': 'transition-transform', 'duration-150': 'duration-150', 'duration-200': 'duration-200', 'duration-300': 'duration-300', 'duration-500': 'duration-500', // Hover states (these will be handled differently) 'hover:bg-primary': 'hover:bg-primary', 'hover:text-white': 'hover:text-white', 'hover:scale-105': 'hover:scale-105', // Focus states 'focus:outline-none': 'focus:outline-none', 'focus:ring-2': 'focus:ring-2', 'focus:ring-primary': 'focus:ring-primary', // Responsive prefixes 'sm:block': 'sm:block', 'sm:hidden': 'sm:hidden', 'md:block': 'md:block', 'md:hidden': 'md:hidden', 'lg:block': 'lg:block', 'lg:hidden': 'lg:hidden', 'xl:block': 'xl:block', 'xl:hidden': 'xl:hidden', // Custom utility classes for the project 'section-padding': 'py-12 md:py-16', 'container-narrow': 'max-w-4xl mx-auto', 'container-wide': 'max-w-6xl mx-auto', 'text-gradient': 'bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent', // Animation classes 'animate-fade-in': 'animate-fade-in', 'animate-fade-in-up': 'animate-fade-in-up', 'animate-slide-in': 'animate-slide-in', 'animate-bounce': 'animate-bounce', 'animate-pulse': 'animate-pulse', 'animate-spin': 'animate-spin', // Custom classes for WordPress compatibility 'wp-caption': 'figure', 'wp-caption-text': 'figcaption text-sm text-gray-600 mt-2', 'alignnone': 'block', 'size-full': 'w-full', 'size-large': 'w-full max-w-3xl', 'size-medium': 'w-full max-w-xl', 'size-thumbnail': 'w-32 h-32', }; let processed = html; // Replace classes in HTML attributes Object.entries(classMap).forEach(([wpClass, twClass]) => { // Handle class="..." with the class at the beginning const classRegex1 = new RegExp(`class=["']${wpClass}\\s+([^"']*)["']`, 'g'); processed = processed.replace(classRegex1, (match, rest) => { const newClasses = `${twClass} ${rest}`.trim().replace(/\s+/g, ' '); return `class="${newClasses}"`; }); // Handle class="..." with the class in the middle const classRegex2 = new RegExp(`class=["']([^"']*)\\s+${wpClass}\\s+([^"']*)["']`, 'g'); processed = processed.replace(classRegex2, (match, before, after) => { const newClasses = `${before} ${twClass} ${after}`.trim().replace(/\s+/g, ' '); return `class="${newClasses}"`; }); // Handle class="..." with the class at the end const classRegex3 = new RegExp(`class=["']([^"']*)\\s+${wpClass}["']`, 'g'); processed = processed.replace(classRegex3, (match, before) => { const newClasses = `${before} ${twClass}`.trim().replace(/\s+/g, ' '); return `class="${newClasses}"`; }); // Handle class="..." with only the class const classRegex4 = new RegExp(`class=["']${wpClass}["']`, 'g'); processed = processed.replace(classRegex4, `class="${twClass}"`); }); return processed; } /** * Extract text from HTML (strip all tags) */ export function stripHTML(html: string | null | undefined): string { if (!html) return ''; return html.replace(/<[^>]*>/g, ''); } /** * Extract text from HTML and process it */ export function extractTextFromHTML(html: string | null | undefined): string { if (!html) return ''; return processHTML(html).replace(/<[^>]*>/g, ''); } /** * Get dictionary for translations * This is a compatibility function for the i18n system */ export function getDictionary(locale: string): Record { // For now, return empty dictionary // In a real implementation, this would load translation files return {}; } /** * Process HTML for preview (shorter, sanitized) */ export function processHTMLForPreview(html: string | null | undefined, maxLength: number = 200): string { if (!html) return ''; const processed = processHTML(html); const stripped = stripHTML(processed); if (stripped.length <= maxLength) { return stripped; } return stripped.substring(0, maxLength) + '...'; } /** * Check if HTML contains dangerous content */ export function hasDangerousContent(html: string | null | undefined): boolean { if (!html) return false; const dangerousPatterns = [ / pattern.test(html)); } /** * Normalize HTML for comparison */ export function normalizeHTML(html: string): string { return processHTML(html) .replace(/\s+/g, ' ') .replace(/> <') .trim(); }