Files
klz-cables.com/lib/html-compat.ts
2025-12-29 18:18:48 +01:00

888 lines
27 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* HTML Compatibility Layer
* 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 '';
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
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": "'",
'¢': '¢',
'£': '£',
'¥': '¥',
'€': '€',
'©': '©',
'®': '®',
'™': '™',
'°': '°',
'±': '±',
'×': '×',
'÷': '÷',
'µ': 'µ',
'¶': '¶',
'§': '§',
'á': 'á',
'é': 'é',
'í': 'í',
'ó': 'ó',
'ú': 'ú',
'Á': 'Á',
'É': 'É',
'Í': 'Í',
'Ó': 'Ó',
'Ú': 'Ú',
'ñ': 'ñ',
'Ñ': 'Ñ',
'ü': 'ü',
'Ü': 'Ü',
'ö': 'ö',
'Ö': 'Ö',
'ä': 'ä',
'Ä': 'Ä',
'ß': 'ß',
'—': '—',
'': '',
'…': '…',
'«': '«',
'»': '»',
'': "'",
'': "'",
'“': '"',
'”': '"',
'•': '•',
'·': '·'
};
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, '');
// Remove style tags
processed = processed.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/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, '');
// Remove any remaining WordPress shortcode-like content (e.g., [vc_row...])
processed = processed.replace(/\[[^\]]*\]/g, '');
// 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;
}
/**
* 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).replace(/<[^>]*>/g, '');
}
/**
* Get dictionary for translations
* This is a compatibility function for the i18n system
*/
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();
}