1268 lines
43 KiB
TypeScript
1268 lines
43 KiB
TypeScript
/**
|
||
* HTML Compatibility Layer
|
||
* Handles HTML entities, formatting, and class conversions from WordPress exports
|
||
*/
|
||
|
||
import { getMediaById } from './data';
|
||
|
||
/**
|
||
* Process HTML content from WordPress
|
||
* - Sanitizes dangerous content
|
||
* - Converts HTML entities
|
||
* - Removes scripts and styles
|
||
* - Processes shortcodes
|
||
*/
|
||
export function processHTML(html: string | null | undefined): string {
|
||
if (!html) return '';
|
||
|
||
let processed = html;
|
||
|
||
// Step 1: Replace HTML entities
|
||
processed = replaceHTMLEntities(processed);
|
||
|
||
// Step 2: Remove dangerous content
|
||
processed = sanitizeHTML(processed);
|
||
|
||
// Step 3: Process WordPress shortcodes
|
||
processed = processShortcodes(processed);
|
||
|
||
// Step 4: Clean up whitespace
|
||
processed = cleanWhitespace(processed);
|
||
|
||
return processed;
|
||
}
|
||
|
||
/**
|
||
* Replace common HTML entities with their actual characters
|
||
*/
|
||
function replaceHTMLEntities(html: string): string {
|
||
const entities: Record<string, string> = {
|
||
'\u00A0': ' ', // Non-breaking space
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": "'",
|
||
'¢': '¢',
|
||
'£': '£',
|
||
'¥': '¥',
|
||
'€': '€',
|
||
'©': '©',
|
||
'®': '®',
|
||
'™': '™',
|
||
'°': '°',
|
||
'±': '±',
|
||
'×': '×',
|
||
'÷': '÷',
|
||
'µ': 'µ',
|
||
'¶': '¶',
|
||
'§': '§',
|
||
'á': 'á',
|
||
'é': 'é',
|
||
'í': 'í',
|
||
'ó': 'ó',
|
||
'ú': 'ú',
|
||
'Á': 'Á',
|
||
'É': 'É',
|
||
'Í': 'Í',
|
||
'Ó': 'Ó',
|
||
'Ú': 'Ú',
|
||
'ñ': 'ñ',
|
||
'Ñ': 'Ñ',
|
||
'ü': 'ü',
|
||
'Ü': 'Ü',
|
||
'ö': 'ö',
|
||
'Ö': 'Ö',
|
||
'ä': 'ä',
|
||
'Ä': 'Ä',
|
||
'ß': 'ß',
|
||
'—': '—',
|
||
'–': '–',
|
||
'…': '…',
|
||
'«': '«',
|
||
'»': '»',
|
||
'‘': "'",
|
||
'’': "'",
|
||
'“': '"',
|
||
'”': '"',
|
||
'•': '•',
|
||
'·': '·',
|
||
// Additional common entities that might appear in WordPress exports
|
||
'„': '"', // Double low-reversed-9 quote
|
||
'‟': '"', // Double high-reversed-9 quote
|
||
'′': "'", // Prime
|
||
'″': '"', // Double prime
|
||
'‹': '<', // Single left-pointing angle quotation mark
|
||
'›': '>', // Single right-pointing angle quotation mark
|
||
'†': '†', // Dagger
|
||
'‡': '‡', // Double dagger
|
||
'‰': '‰', // Per mille
|
||
};
|
||
|
||
let processed = html;
|
||
for (const [entity, char] of Object.entries(entities)) {
|
||
processed = processed.replace(new RegExp(entity.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), char);
|
||
}
|
||
|
||
return processed;
|
||
}
|
||
|
||
/**
|
||
* Sanitize HTML by removing dangerous tags and attributes
|
||
*/
|
||
function sanitizeHTML(html: string): string {
|
||
let processed = html;
|
||
|
||
// Remove script tags
|
||
processed = processed.replace(/<script\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;
|
||
}
|
||
|
||
/**
|
||
* Strip ChatGPT artifacts from HTML content
|
||
* Removes react-scroll-to-bottom--css-* classes and data-message-* attributes
|
||
*/
|
||
function stripChatGPTArtifacts(html: string): string {
|
||
let processed = html;
|
||
|
||
// Remove react-scroll-to-bottom CSS classes
|
||
processed = processed.replace(/class=["'][^"']*react-scroll-to-bottom--css[^"']*["']/gi, '');
|
||
|
||
// Remove data-message-* attributes
|
||
processed = processed.replace(/\s+data-message-[^\s=]*=["'][^"']*["']/gi, '');
|
||
|
||
// Clean up multiple spaces that might result from removal
|
||
processed = processed.replace(/\s+/g, ' ');
|
||
|
||
// Clean up empty class attributes
|
||
processed = processed.replace(/\s+class=["']\s*["']/gi, '');
|
||
|
||
return processed;
|
||
}
|
||
|
||
/**
|
||
* Process [split_line_heading] shortcode
|
||
* Converts to split h2 with line styling
|
||
*/
|
||
function processSplitLineHeading(html: string): string {
|
||
return html.replace(/\[split_line_heading([^\]]*)\]([\s\S]*?)\[\/split_line_heading\]/g, (match, attrs, content) => {
|
||
// Extract alignment from attributes
|
||
const alignMatch = attrs.match(/align=["']([^"']*)["']/i);
|
||
const align = alignMatch ? alignMatch[1] : 'left';
|
||
|
||
// Parse the content - it might contain HTML or just text
|
||
// Format: "Line 1|Line 2" or "Line 1|Line 2|Line 3"
|
||
const lines = content.split('|').map((line: string) => line.trim());
|
||
|
||
// Build the HTML structure
|
||
const classes = ['split-line-heading', 'text-center'];
|
||
if (align === 'center') classes.push('text-center');
|
||
if (align === 'right') classes.push('text-right');
|
||
if (align === 'left') classes.push('text-left');
|
||
|
||
let html = `<div class="${classes.join(' ')}">`;
|
||
html += '<h2 class="split-heading">';
|
||
|
||
lines.forEach((line: string, index: number) => {
|
||
if (index > 0) {
|
||
html += '<span class="line-separator"></span>';
|
||
}
|
||
html += `<span class="line">${line}</span>`;
|
||
});
|
||
|
||
html += '</h2>';
|
||
html += '</div>';
|
||
|
||
return html;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Process WordPress shortcodes by converting them to HTML with proper styling
|
||
* Also handles mixed scenarios where some content is already HTML with WordPress classes
|
||
*/
|
||
export function processShortcodes(html: string): string {
|
||
let processed = html;
|
||
|
||
try {
|
||
// Step 0: Decode HTML entities in shortcode attributes
|
||
processed = replaceHTMLEntities(processed);
|
||
|
||
// Step 0.5: Strip ChatGPT artifacts (react-scroll-*, data-message-*)
|
||
processed = stripChatGPTArtifacts(processed);
|
||
|
||
// Step 0.6: Process split_line_heading shortcode
|
||
processed = processSplitLineHeading(processed);
|
||
|
||
// Check if input has divs that need conversion
|
||
const hasDivs = /<div[^>]*class=["'][^"']*(?:vc-row|vc_row|vc_row_inner|vc-column|vc_column|vc_column_inner|vc-column-text|vc_column_text)[^"']*["'][^>]*>/i.test(processed);
|
||
const hasShortcodes = /\[(vc_row|vc_column|vc_column_text|vc_single_image|vc_btn|vc_separator|vc_video)/i.test(processed);
|
||
|
||
// Only convert divs to shortcodes if there are divs but no existing shortcodes
|
||
if (hasDivs && !hasShortcodes) {
|
||
// Use a stack-based approach to handle nested divs
|
||
const stack: string[] = [];
|
||
let result = '';
|
||
let i = 0;
|
||
|
||
while (i < processed.length) {
|
||
// Check for opening div tags
|
||
const openDivMatch = processed.slice(i).match(/^<div[^>]*class=["'][^"']*(?:vc-row|vc_row|vc_row_inner|vc-column|vc_column|vc_column_inner|vc-column-text|vc_column_text)[^"']*["'][^>]*>/i);
|
||
if (openDivMatch) {
|
||
const attrs = extractAttributesFromHTML(openDivMatch[0]);
|
||
let tag: string;
|
||
|
||
if (openDivMatch[0].includes('vc_row_inner') || openDivMatch[0].includes('vc-row-inner')) {
|
||
tag = 'vc_row_inner';
|
||
} else if (openDivMatch[0].includes('vc-row') || openDivMatch[0].includes('vc_row')) {
|
||
tag = 'vc_row';
|
||
} else if (openDivMatch[0].includes('vc_column_inner') || openDivMatch[0].includes('vc-column-inner')) {
|
||
tag = 'vc_column_inner';
|
||
} else if (openDivMatch[0].includes('vc-column') || openDivMatch[0].includes('vc_column')) {
|
||
tag = 'vc_column';
|
||
} else if (openDivMatch[0].includes('vc-column-text') || openDivMatch[0].includes('vc_column_text')) {
|
||
tag = 'vc_column_text';
|
||
} else {
|
||
// Unknown tag, skip
|
||
result += openDivMatch[0];
|
||
i += openDivMatch[0].length;
|
||
continue;
|
||
}
|
||
|
||
stack.push(tag);
|
||
result += `[${tag} ${attrs}]`;
|
||
i += openDivMatch[0].length;
|
||
continue;
|
||
}
|
||
|
||
// Check for closing div
|
||
if (processed.slice(i, i+6) === '</div>') {
|
||
if (stack.length > 0) {
|
||
const tag = stack.pop();
|
||
result += `[/${tag}]`;
|
||
i += 6;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// Check for img tags (vc_single_image)
|
||
const imgMatch = processed.slice(i).match(/^<img[^>]*class=["'][^"']*(?:vc-single-image|vc_single_image)[^"']*["'][^>]*>/i);
|
||
if (imgMatch) {
|
||
const attrs = extractAttributesFromHTML(imgMatch[0]);
|
||
const imageId = extractAttribute(attrs, 'data-wp-image-id') || extractAttribute(attrs, 'src');
|
||
const width = extractAttribute(attrs, 'data-width') || '';
|
||
result += `[vc_single_image src="${imageId}" width="${width}"]`;
|
||
i += imgMatch[0].length;
|
||
continue;
|
||
}
|
||
|
||
// Check for anchor tags (vc_btn)
|
||
const anchorMatch = processed.slice(i).match(/^<a[^>]*class=["'][^"']*(?:vc-btn|vc_btn)[^"']*["'][^>]*>(.*?)<\/a>/i);
|
||
if (anchorMatch) {
|
||
const attrs = extractAttributesFromHTML(anchorMatch[0]);
|
||
const href = extractAttribute(attrs, 'href');
|
||
const title = anchorMatch[1]; // Content between tags
|
||
result += `[vc_btn href="${href}" title="${title}"]`;
|
||
i += anchorMatch[0].length;
|
||
continue;
|
||
}
|
||
|
||
// Check for hr tags (vc_separator)
|
||
const hrMatch = processed.slice(i).match(/^<hr[^>]*class=["'][^"']*(?:vc-separator|vc_separator)[^"']*["'][^>]*>/i);
|
||
if (hrMatch) {
|
||
const attrs = extractAttributesFromHTML(hrMatch[0]);
|
||
result += `[vc_separator ${attrs}]`;
|
||
i += hrMatch[0].length;
|
||
continue;
|
||
}
|
||
|
||
// Regular character
|
||
result += processed[i];
|
||
i++;
|
||
}
|
||
|
||
processed = result;
|
||
}
|
||
|
||
// Step 2: Process shortcode blocks into HTML
|
||
processed = processVcRowShortcodes(processed);
|
||
processed = processVcColumnShortcodes(processed);
|
||
processed = processVcColumnTextShortcodes(processed);
|
||
processed = processVcImageShortcodes(processed);
|
||
processed = processVcButtonShortcodes(processed);
|
||
processed = processVcSeparatorShortcodes(processed);
|
||
processed = processVcVideoShortcodes(processed);
|
||
processed = processBackgroundShortcodes(processed);
|
||
|
||
// Step 3: Check for unprocessed shortcodes and log them
|
||
const unprocessedShortcodes = processed.match(/\[[^\]]*\]/g);
|
||
if (unprocessedShortcodes && unprocessedShortcodes.length > 0) {
|
||
console.warn('Unprocessed shortcodes found and will be removed:', unprocessedShortcodes);
|
||
}
|
||
|
||
// Clean up any remaining shortcode artifacts
|
||
processed = processed.replace(/\[[^\]]*\]/g, '');
|
||
|
||
// Step 4: Clean up any remaining empty div tags
|
||
processed = processed.replace(/<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.
|
||
// Support both snake_case (shortcode) and camelCase (from data attributes)
|
||
const bgImage = extractAttribute(attrs, 'bg_image') || extractAttribute(attrs, 'bgImage');
|
||
const bgColor = extractAttribute(attrs, 'bg_color') || extractAttribute(attrs, 'bgColor');
|
||
const colorOverlay = extractAttribute(attrs, 'color_overlay') || extractAttribute(attrs, 'colorOverlay');
|
||
const colorOverlay2 = extractAttribute(attrs, 'color_overlay_2') || extractAttribute(attrs, 'colorOverlay2');
|
||
const overlayStrength = extractAttribute(attrs, 'overlay_strength') || extractAttribute(attrs, 'overlayStrength');
|
||
const enableGradient = extractAttribute(attrs, 'enable_gradient') || extractAttribute(attrs, 'enableGradient');
|
||
const gradientDirection = extractAttribute(attrs, 'gradient_direction') || extractAttribute(attrs, 'gradientDirection');
|
||
const topPadding = extractAttribute(attrs, 'top_padding') || extractAttribute(attrs, 'topPadding');
|
||
const bottomPadding = extractAttribute(attrs, 'bottom_padding') || extractAttribute(attrs, 'bottomPadding');
|
||
const fullScreen = extractAttribute(attrs, 'full_screen_row_position') || extractAttribute(attrs, 'fullScreenRowPosition');
|
||
const videoBg = extractAttribute(attrs, 'video_bg') || extractAttribute(attrs, 'videoBg');
|
||
const videoMp4 = extractAttribute(attrs, 'video_mp4') || extractAttribute(attrs, 'videoMp4');
|
||
const videoWebm = extractAttribute(attrs, 'video_webm') || extractAttribute(attrs, 'videoWebm');
|
||
const textAlign = extractAttribute(attrs, 'text_align') || extractAttribute(attrs, 'textAlign');
|
||
const textColor = extractAttribute(attrs, 'text_color') || extractAttribute(attrs, 'textColor');
|
||
const overflow = extractAttribute(attrs, 'overflow');
|
||
const equalHeight = extractAttribute(attrs, 'equal_height') || extractAttribute(attrs, 'equalHeight');
|
||
const contentPlacement = extractAttribute(attrs, 'content_placement') || extractAttribute(attrs, 'contentPlacement');
|
||
const columnDirection = extractAttribute(attrs, 'column_direction') || extractAttribute(attrs, 'columnDirection');
|
||
const rowBorderRadius = extractAttribute(attrs, 'row_border_radius') || extractAttribute(attrs, 'rowBorderRadius');
|
||
const rowBorderRadiusApplies = extractAttribute(attrs, 'row_border_radius_applies') || extractAttribute(attrs, 'rowBorderRadiusApplies');
|
||
|
||
// Build style string
|
||
let style = '';
|
||
let wrapperClasses = [...classes];
|
||
|
||
// Handle text alignment
|
||
if (textAlign === 'center') wrapperClasses.push('text-center');
|
||
if (textAlign === 'right') wrapperClasses.push('text-right');
|
||
if (textAlign === 'left') wrapperClasses.push('text-left');
|
||
|
||
// Handle text color
|
||
if (textColor === 'light') wrapperClasses.push('text-white');
|
||
|
||
// Handle overflow
|
||
if (overflow === 'visible') wrapperClasses.push('overflow-visible');
|
||
|
||
// Handle equal height
|
||
if (equalHeight === 'yes') {
|
||
wrapperClasses.push('items-stretch');
|
||
wrapperClasses.push('flex');
|
||
}
|
||
|
||
// Handle content placement
|
||
if (contentPlacement === 'bottom') wrapperClasses.push('justify-end');
|
||
if (contentPlacement === 'middle') wrapperClasses.push('justify-center');
|
||
|
||
// Handle column direction
|
||
if (columnDirection === 'column') wrapperClasses.push('flex-col');
|
||
|
||
// Handle border radius
|
||
if (rowBorderRadius === 'none' && rowBorderRadiusApplies === 'bg') {
|
||
wrapperClasses.push('rounded-none');
|
||
}
|
||
|
||
// Handle background image - FIXED: Support both numeric IDs and local paths
|
||
if (bgImage) {
|
||
// Check if it's already a local path (from processed data)
|
||
if (bgImage.startsWith('/media/')) {
|
||
wrapperClasses.push('bg-cover', 'bg-center');
|
||
style += `background-image: url(${bgImage}); `;
|
||
} else {
|
||
// Try to parse as numeric ID
|
||
const mediaId = parseInt(bgImage);
|
||
if (!isNaN(mediaId)) {
|
||
// Use getMediaById to get the actual file path
|
||
const media = getMediaById(mediaId);
|
||
if (media) {
|
||
wrapperClasses.push('bg-cover', 'bg-center');
|
||
style += `background-image: url(${media.localPath}); `;
|
||
} else {
|
||
// Fallback if media not found
|
||
wrapperClasses.push('bg-cover', 'bg-center');
|
||
style += `background-image: url(/media/${bgImage}.webp); `;
|
||
}
|
||
} else {
|
||
// Assume it's a direct URL
|
||
style += `background-image: url(${bgImage}); `;
|
||
}
|
||
}
|
||
style += `background-size: cover; `;
|
||
style += `background-position: center; `;
|
||
}
|
||
|
||
// Handle background color
|
||
if (bgColor) {
|
||
style += `background-color: ${bgColor}; `;
|
||
}
|
||
|
||
// Handle video background
|
||
if (videoBg === 'use_video' && (videoMp4 || videoWebm)) {
|
||
// Mark for ContentRenderer to handle
|
||
wrapperClasses.push('relative', 'overflow-hidden');
|
||
style += `position: relative; `;
|
||
|
||
// Create video background structure with data attributes
|
||
const videoAttrs = [];
|
||
if (videoMp4) videoAttrs.push(`data-video-mp4="${videoMp4}"`);
|
||
if (videoWebm) videoAttrs.push(`data-video-webm="${videoWebm}"`);
|
||
videoAttrs.push('data-video-bg="true"');
|
||
|
||
return `<div class="${wrapperClasses.join(' ')}" style="${style}" ${videoAttrs.join(' ')}>
|
||
<div class="relative flex flex-wrap -mx-4 w-full h-full">${content}</div>
|
||
</div>`;
|
||
}
|
||
|
||
// Handle video attributes even if not using video_bg flag (enhanced preservation)
|
||
if (videoMp4 || videoWebm) {
|
||
// Add video attributes to wrapper for preservation
|
||
if (videoMp4) wrapperClasses.push(`has-video-mp4`);
|
||
if (videoWebm) wrapperClasses.push(`has-video-webm`);
|
||
|
||
// Store video URLs in data attributes for later use
|
||
const videoDataAttrs = [];
|
||
if (videoMp4) videoDataAttrs.push(`data-video-mp4="${videoMp4}"`);
|
||
if (videoWebm) videoDataAttrs.push(`data-video-webm="${videoWebm}"`);
|
||
|
||
// If there's no other special handling, just add the attributes to the div
|
||
if (!bgImage && !bgColor && !colorOverlay && !enableGradient) {
|
||
return `<div class="${wrapperClasses.join(' ')}" style="${style}" ${videoDataAttrs.join(' ')}>
|
||
<div class="relative flex flex-wrap -mx-4 w-full">${content}</div>
|
||
</div>`;
|
||
}
|
||
|
||
// For complex backgrounds with video, add video attrs to existing structure
|
||
// We'll handle this in the ContentRenderer
|
||
const existingHtml = `<div class="${wrapperClasses.join(' ')}" style="${style}">
|
||
<div class="relative flex flex-wrap -mx-4 w-full">${content}</div>
|
||
</div>`;
|
||
|
||
// Add video attributes as data attributes on the wrapper
|
||
return existingHtml.replace('<div class="', `<div ${videoDataAttrs.join(' ')} class="`);
|
||
}
|
||
|
||
// Handle color overlay (single or gradient)
|
||
const hasOverlay = colorOverlay || colorOverlay2 || enableGradient === 'true' || enableGradient === '1';
|
||
if (hasOverlay) {
|
||
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};`;
|
||
}
|
||
|
||
const resultHtml = `<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>`;
|
||
return resultHtml;
|
||
}
|
||
|
||
// Handle gradient (without overlay)
|
||
if (enableGradient === 'true' || enableGradient === '1') {
|
||
const gradientClass = getGradientClass(gradientDirection);
|
||
wrapperClasses.push(gradientClass);
|
||
}
|
||
|
||
// Handle padding
|
||
if (topPadding || bottomPadding) {
|
||
// Convert percentage values to Tailwind arbitrary values
|
||
const pt = topPadding ? `pt-[${topPadding}]` : '';
|
||
const pb = bottomPadding ? `pb-[${bottomPadding}]` : '';
|
||
if (pt) wrapperClasses.push(pt);
|
||
if (pb) wrapperClasses.push(pb);
|
||
}
|
||
|
||
// Handle full screen
|
||
if (fullScreen === 'middle') {
|
||
wrapperClasses.push('min-h-screen', 'flex', 'items-center');
|
||
}
|
||
|
||
// Don't return empty divs
|
||
if (!content || content.trim() === '') {
|
||
return '';
|
||
}
|
||
|
||
return `<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
|
||
// Support both numeric IDs (from raw data) and local paths (from processed data)
|
||
html = html.replace(/bg_image="([^"]+)"/g, (match, imageValue) => {
|
||
// If it's a numeric ID, keep it as-is for now (will be handled by ContentRenderer)
|
||
// If it's already a local path, convert to data-bg-image
|
||
if (/^\d+$/.test(imageValue)) {
|
||
// Numeric ID - keep as data attribute for ContentRenderer to resolve
|
||
return `data-bg-image="${imageValue}"`;
|
||
} else if (imageValue.startsWith('/media/')) {
|
||
// Already a local path - use directly
|
||
return `data-bg-image="${imageValue}"`;
|
||
} else {
|
||
// Unknown format - keep as-is
|
||
return `data-bg-image="${imageValue}"`;
|
||
}
|
||
});
|
||
|
||
// Handle video background attributes - enhanced to preserve all video data
|
||
html = html.replace(/video_bg="use_video"/g, 'data-video-bg="true"');
|
||
html = html.replace(/video_mp4="([^"]+)"/g, (match, url) => {
|
||
// Ensure URL is properly formatted
|
||
const cleanUrl = url.trim().replace(/^["']|["']$/g, '');
|
||
return `data-video-mp4="${cleanUrl}"`;
|
||
});
|
||
html = html.replace(/video_webm="([^"]+)"/g, (match, url) => {
|
||
// Ensure URL is properly formatted
|
||
const cleanUrl = url.trim().replace(/^["']|["']$/g, '');
|
||
return `data-video-webm="${cleanUrl}"`;
|
||
});
|
||
|
||
// Handle parallax
|
||
html = html.replace(/parallax_bg="true"/g, 'data-parallax="true"');
|
||
|
||
// Also handle video attributes that might appear without the video_bg flag
|
||
// This ensures video data is preserved even if the flag is missing
|
||
html = html.replace(/\s+mp4="([^"]+)"/g, (match, url) => ` data-video-mp4="${url}"`);
|
||
html = html.replace(/\s+webm="([^"]+)"/g, (match, url) => ` data-video-webm="${url}"`);
|
||
|
||
return html;
|
||
}
|
||
|
||
/**
|
||
* Extract attribute value from shortcode attributes
|
||
* Handles complex patterns with quotes, special characters, and spaces
|
||
*/
|
||
function extractAttribute(attrs: string, key: string): string | null {
|
||
// First try: key="value" or key='value'
|
||
const quotedPattern = new RegExp(`${key}=["']([^"']*)["']`, 'i');
|
||
const quotedMatch = attrs.match(quotedPattern);
|
||
if (quotedMatch) return quotedMatch[1];
|
||
|
||
// Second try: key=value (without quotes, until space or end)
|
||
const unquotedPattern = new RegExp(`${key}=([^\\s\\]]+)`, 'i');
|
||
const unquotedMatch = attrs.match(unquotedPattern);
|
||
if (unquotedMatch) return unquotedMatch[1];
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Get Tailwind gradient class from gradient direction
|
||
*/
|
||
function getGradientClass(direction: string): string {
|
||
const gradientMap: Record<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();
|
||
} |