1082 lines
35 KiB
TypeScript
1082 lines
35 KiB
TypeScript
/**
|
||
* 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();
|
||
} |