Files
klz-cables.com/lib/html-compat.ts
2025-12-30 00:06:54 +01:00

1082 lines
35 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, '');
// 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;
}
/**
* 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 1: Convert any existing HTML with WordPress classes back to shortcode format
// This ensures we have a consistent format to work with
// Handle vc_row and vc_row_inner
processed = processed.replace(/<div[^>]*class=["'][^"']*(?:vc-row|vc_row|vc_row_inner)[^"']*["'][^>]*>/gi, (match) => {
const attrs = extractAttributesFromHTML(match);
const isInner = match.includes('vc_row_inner') || match.includes('vc-row-inner');
return `[${isInner ? 'vc_row_inner' : 'vc_row'} ${attrs}]`;
});
// Handle vc_column and vc_column_inner
processed = processed.replace(/<div[^>]*class=["'][^"']*(?:vc-column|vc_column|vc_column_inner)[^"']*["'][^>]*>/gi, (match) => {
const attrs = extractAttributesFromHTML(match);
const isInner = match.includes('vc_column_inner') || match.includes('vc-column-inner');
return `[${isInner ? 'vc_column_inner' : 'vc_column'} ${attrs}]`;
});
// Handle vc_column_text
processed = processed.replace(/<div[^>]*class=["'][^"']*(?:vc-column-text|vc_column_text)[^"']*["'][^>]*>/gi, (match) => {
const attrs = extractAttributesFromHTML(match);
return `[vc_column_text ${attrs}]`;
});
// Handle vc_single_image
processed = processed.replace(/<img[^>]*class=["'][^"']*(?:vc-single-image|vc_single_image)[^"']*["'][^>]*>/gi, (match) => {
const attrs = extractAttributesFromHTML(match);
const imageId = extractAttribute(attrs, 'data-wp-image-id') || extractAttribute(attrs, 'src');
const width = extractAttribute(attrs, 'data-width') || '';
return `[vc_single_image src="${imageId}" width="${width}"]`;
});
// Handle vc_btn
processed = processed.replace(/<a[^>]*class=["'][^"']*(?:vc-btn|vc_btn)[^"']*["'][^>]*>(.*?)<\/a>/gi, (match, content) => {
const attrs = extractAttributesFromHTML(match);
const href = extractAttribute(attrs, 'href');
const title = content;
return `[vc_btn href="${href}" title="${title}"]`;
});
// Handle vc_separator
processed = processed.replace(/<hr[^>]*class=["'][^"']*(?:vc-separator|vc_separator)[^"']*["'][^>]*>/gi, (match) => {
const attrs = extractAttributesFromHTML(match);
return `[vc_separator ${attrs}]`;
});
// Handle closing div tags by looking for matching opening shortcode tags
// This is more complex, so we'll handle it carefully
processed = processed.replace(/<\/div>/gi, (match, offset) => {
const beforeContent = processed.substring(0, offset);
const lastOpenTag = beforeContent.match(/\[(vc_row(?:_inner)?|vc_column(?:_inner)?|vc_column_text)\s*[^\]]*\]$/i);
if (lastOpenTag) {
return `[/${lastOpenTag[1]}]`;
}
// If no matching shortcode, keep the div closing tag
return match;
});
// 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
// Only remove shortcodes that weren't processed
processed = processed.replace(/\[[^\]]*\]/g, '');
// Step 4: Clean up any remaining empty div tags
processed = processed.replace(/<div[^>]*>\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.
const bgImage = extractAttribute(attrs, 'bg_image');
const bgColor = extractAttribute(attrs, 'bg_color');
const colorOverlay = extractAttribute(attrs, 'color_overlay');
const colorOverlay2 = extractAttribute(attrs, 'color_overlay_2');
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');
const videoBg = extractAttribute(attrs, 'video_bg');
const videoMp4 = extractAttribute(attrs, 'video_mp4');
const videoWebm = extractAttribute(attrs, 'video_webm');
const textAlign = extractAttribute(attrs, 'text_align');
const textColor = extractAttribute(attrs, 'text_color');
const overflow = extractAttribute(attrs, 'overflow');
const equalHeight = extractAttribute(attrs, 'equal_height');
const contentPlacement = extractAttribute(attrs, 'content_placement');
const columnDirection = extractAttribute(attrs, 'column_direction');
const rowBorderRadius = extractAttribute(attrs, 'row_border_radius');
const rowBorderRadiusApplies = extractAttribute(attrs, 'row_border_radius_applies');
// 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
if (bgImage) {
// Try to get media by ID first
const mediaId = parseInt(bgImage);
if (!isNaN(mediaId)) {
// This will be handled by ContentRenderer with data attributes
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
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 `<div class="${wrapperClasses.join(' ')}" style="${style}" ${videoAttrs.join(' ')}>
<div class="relative flex flex-wrap -mx-4 w-full h-full">${content}</div>
</div>`;
}
// Handle color overlay (single or gradient)
if (colorOverlay || colorOverlay2 || enableGradient === 'true' || enableGradient === '1') {
style += `position: relative; `;
wrapperClasses.push('relative');
let overlayStyle = '';
if (colorOverlay2 && enableGradient === 'true') {
// Gradient overlay
const gradientDir = gradientDirection || 'left_to_right';
let gradientCSS = '';
switch(gradientDir) {
case 'left_to_right':
gradientCSS = `linear-gradient(to right, ${colorOverlay}, ${colorOverlay2})`;
break;
case 'right_to_left':
gradientCSS = `linear-gradient(to left, ${colorOverlay}, ${colorOverlay2})`;
break;
case 'top_to_bottom':
gradientCSS = `linear-gradient(to bottom, ${colorOverlay}, ${colorOverlay2})`;
break;
case 'bottom_to_top':
gradientCSS = `linear-gradient(to top, ${colorOverlay}, ${colorOverlay2})`;
break;
default:
gradientCSS = `linear-gradient(to right, ${colorOverlay}, ${colorOverlay2})`;
}
overlayStyle = `background: ${gradientCSS}; opacity: 0.32;`;
} else if (colorOverlay) {
// Solid color overlay
const opacity = overlayStrength ? parseFloat(overlayStrength) : 0.5;
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 (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');
}
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();
}