migration wip

This commit is contained in:
2025-12-29 18:18:48 +01:00
parent 292975299d
commit f86785bfb0
182 changed files with 30131 additions and 9321 deletions

View File

@@ -1,14 +1,39 @@
/**
* HTML Compatibility Layer
* Handles HTML entities and formatting from WordPress exports
* Handles HTML entities, formatting, and class conversions from WordPress exports
*/
/**
* 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 '';
// Replace common HTML entities
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<string, string> = {
'\u00A0': ' ', // Non-breaking space
'&': '&',
@@ -62,11 +87,20 @@ export function processHTML(html: string | null | undefined): string {
'·': '·'
};
// Replace entities
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\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
@@ -78,30 +112,724 @@ export function processHTML(html: string | null | undefined): string {
// Remove dangerous attributes
processed = processed.replace(/\s+(href|src)\s*=\s*["']\s*javascript:/gi, '');
// Remove any remaining WordPress shortcode-like content (e.g., [vc_row...])
processed = processed.replace(/\[[^\]]*\]/g, '');
// Keep HTML structure from processed data - allow divs with our classes
// Allow: <p>, <br>, <h1-6>, <strong>, <b>, <em>, <i>, <ul>, <ol>, <li>, <a>, <div>, <span>, <img>
// Also allow our vc-row/vc-column classes
processed = processed.replace(/<\/?(?!\/?(p|br|h[1-6]|strong|b|em|i|ul|ol|li|a|div|span|img|small)(\s|>))[^>]*>/gi, '');
// 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'),
''
);
// Clean up empty paragraphs and extra spaces
processed = processed.replace(/<p>\s*<\/p>/g, '');
processed = processed.replace(/\s+/g, ' ').trim();
return processed;
}
/**
* Process WordPress shortcodes by converting them to HTML with proper styling
*/
function processShortcodes(html: string): string {
let processed = html;
// Process shortcode blocks first (most complex)
processed = processVcRowShortcodes(processed);
processed = processVcColumnShortcodes(processed);
processed = processVcColumnTextShortcodes(processed);
processed = processVcImageShortcodes(processed);
processed = processVcButtonShortcodes(processed);
processed = processVcSeparatorShortcodes(processed);
processed = processVcVideoShortcodes(processed);
processed = processBackgroundShortcodes(processed);
// Remove any remaining shortcodes
processed = processed.replace(/\[[^\]]*\]/g, '');
return processed;
}
/**
* Process [vc_row] shortcodes and convert to flex containers
*/
function processVcRowShortcodes(html: string): string {
return html.replace(/\[vc_row([^\]]*)\]([\s\S]*?)\[\/vc_row\]/g, (match, attrs, content) => {
const classes = ['vc-row', 'flex', 'flex-wrap', '-mx-4'];
// Parse attributes for background colors, images, etc.
const bgImage = extractAttribute(attrs, 'bg_image');
const bgColor = extractAttribute(attrs, 'bg_color');
const colorOverlay = extractAttribute(attrs, 'color_overlay');
const overlayStrength = extractAttribute(attrs, 'overlay_strength');
const enableGradient = extractAttribute(attrs, 'enable_gradient');
const gradientDirection = extractAttribute(attrs, 'gradient_direction');
const topPadding = extractAttribute(attrs, 'top_padding');
const bottomPadding = extractAttribute(attrs, 'bottom_padding');
const fullScreen = extractAttribute(attrs, 'full_screen_row_position');
// Build style string
let style = '';
let wrapperClasses = [...classes];
// Handle background image
if (bgImage) {
style += `background-image: url(/media/${bgImage}.webp); `;
style += `background-size: cover; `;
style += `background-position: center; `;
wrapperClasses.push('bg-cover', 'bg-center');
}
// Handle background color
if (bgColor) {
style += `background-color: ${bgColor}; `;
}
// Handle color overlay
if (colorOverlay) {
const opacity = overlayStrength ? parseFloat(overlayStrength) : 0.5;
style += `position: relative; `;
wrapperClasses.push('relative');
// Create overlay div
const overlayStyle = `background-color: ${colorOverlay}; opacity: ${opacity};`;
return `<div class="${wrapperClasses.join(' ')}" style="${style}">
<div class="absolute inset-0" style="${overlayStyle}"></div>
<div class="relative flex flex-wrap -mx-4 w-full">${content}</div>
</div>`;
}
// Handle gradient
if (enableGradient === 'true' || enableGradient === '1') {
const gradientClass = getGradientClass(gradientDirection);
wrapperClasses.push(gradientClass);
}
// Handle padding
if (topPadding || bottomPadding) {
const pt = topPadding ? `pt-[${topPadding}]` : '';
const pb = bottomPadding ? `pb-[${bottomPadding}]` : '';
wrapperClasses.push(pt, pb);
}
// Handle full screen
if (fullScreen === 'middle') {
wrapperClasses.push('min-h-screen', 'flex', 'items-center');
}
return `<div class="${wrapperClasses.join(' ')}" style="${style}">${content}</div>`;
});
}
/**
* 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 `<div class="${classes.join(' ')}">${content}</div>`;
});
}
/**
* 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 `<div class="${classes.join(' ')}">${content}</div>`;
});
}
/**
* 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 `<img class="${classes.join(' ')}" data-wp-image-id="${imageId}" data-width="${width || ''}" alt="" />`;
});
}
/**
* 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 `<a href="${href}" class="${classes.join(' ')}" target="_blank" rel="noopener noreferrer">${title}</a>`;
}
return `<button class="${classes.join(' ')}">${title}</button>`;
});
}
/**
* 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 `<hr class="${classes.join(' ')}" style="${style}" />`;
});
}
/**
* 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 `<div class="vc-video bg-black relative overflow-hidden rounded-lg my-4">
<video class="w-full" ${poster ? `poster="${poster}"` : ''} autoPlay loop muted playsInline>
${mp4 ? `<source src="${mp4}" type="video/mp4">` : ''}
${webm ? `<source src="${webm}" type="video/webm">` : ''}
</video>
</div>`;
}
if (link) {
// Embedded video (YouTube, Vimeo, etc.)
return `<div class="vc-video embed-responsive aspect-video my-4">
<iframe src="${link}" frameborder="0" allowfullscreen class="w-full h-full"></iframe>
</div>`;
}
return '';
});
}
/**
* Process background-related shortcodes and attributes
*/
function processBackgroundShortcodes(html: string): string {
// Handle background image attributes in divs
html = html.replace(/bg_image="(\d+)"/g, (match, imageId) => {
return `data-bg-image="${imageId}"`;
});
// Handle video background attributes
html = html.replace(/video_bg="use_video"/g, 'data-video-bg="true"');
html = html.replace(/video_mp4="([^"]+)"/g, (match, url) => `data-video-mp4="${url}"`);
html = html.replace(/video_webm="([^"]+)"/g, (match, url) => `data-video-webm="${url}"`);
// Handle parallax
html = html.replace(/parallax_bg="true"/g, 'data-parallax="true"');
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<string, string> = {
'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(/<p>\s*<\/p>/g, '');
processed = processed.replace(/<p>\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<string, string> = {
// 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);
return processHTML(html).replace(/<[^>]*>/g, '');
}
/**
@@ -112,4 +840,49 @@ export function getDictionary(locale: string): Record<string, string> {
// 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 = [
/<script\b/i,
/javascript:/i,
/on\w+\s*=/i,
/<style\b/i,
/expression\s*\(/i,
/vbscript:/i,
/data:text\/html/i,
];
return dangerousPatterns.some(pattern => pattern.test(html));
}
/**
* Normalize HTML for comparison
*/
export function normalizeHTML(html: string): string {
return processHTML(html)
.replace(/\s+/g, ' ')
.replace(/> </g, '><')
.trim();
}