This commit is contained in:
2026-01-06 13:55:04 +01:00
parent 297de69928
commit f991ea6b9b
393 changed files with 41362 additions and 4811 deletions

View File

@@ -139,10 +139,39 @@ function parseWPBakery(html: string): React.ReactNode[] {
// Check for full-width background
const isFullWidth = $row.hasClass('full-width-bg') || $row.hasClass('full-width') || $row.attr('data-full-width');
// Get background image from data attributes or inline styles
const bgImage = $row.attr('data-bg-image') ||
// Get background properties from data attributes
const bgImage = $row.attr('data-bg-image') ||
$row.attr('style')?.match(/background-image:\s*url\(([^)]+)\)/)?.[1] ||
'';
const bgColor = $row.attr('bg_color') || $row.attr('data-bg-color');
const colorOverlay = $row.attr('color_overlay') || $row.attr('data-color-overlay');
const overlayStrength = $row.attr('overlay_strength') || $row.attr('data-overlay-strength');
const topPadding = $row.attr('top_padding');
const bottomPadding = $row.attr('bottom_padding');
const fullScreen = $row.attr('full_screen_row_position');
// Video background attributes - enhanced detection
const videoMp4 = $row.attr('video_mp4') || $row.attr('data-video-mp4') ||
$row.find('[data-video-mp4]').attr('data-video-mp4');
const videoWebm = $row.attr('video_webm') || $row.attr('data-video-webm') ||
$row.find('[data-video-webm]').attr('data-video-webm');
// Check if row has video background indicators
const hasVideoBg = $row.attr('data-video-bg') === 'true' ||
$row.hasClass('nectar-video-wrap') ||
!!(videoMp4?.trim()) || !!(videoWebm?.trim());
// Additional WordPress Salient props
const enableGradient = $row.attr('enable_gradient') === 'true';
const gradientDirection = $row.attr('gradient_direction') || 'left_to_right';
const colorOverlay2 = $row.attr('color_overlay_2');
const parallaxBg = $row.attr('parallax_bg') === 'true';
const parallaxBgSpeed = $row.attr('parallax_bg_speed') || 'medium';
const bgImageAnimation = $row.attr('bg_image_animation') || 'none';
const textAlignment = $row.attr('text_align') || 'left';
const textColor = $row.attr('text_color') || 'dark';
const shapeType = $row.attr('shape_type');
const scenePosition = $row.attr('scene_position') || 'center';
// Get row text for pattern detection
const rowText = $row.text();
@@ -164,14 +193,41 @@ function parseWPBakery(html: string): React.ReactNode[] {
$clone.find('p').first().remove();
const remainingContent = $clone.html()?.trim();
// Calculate overlay opacity
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : undefined;
// Determine height based on full screen position
let heroHeight: 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'screen' = isFullWidth ? 'xl' : 'md';
if (fullScreen === 'middle' || fullScreen === 'top' || fullScreen === 'bottom') {
heroHeight = 'screen';
}
elements.push(
<Hero
key={`hero-${i}`}
title={title}
subtitle={subtitle || undefined}
backgroundImage={heroBg ? replaceUrlInAttribute(heroBg) : undefined}
height={isFullWidth ? 'xl' : 'md'}
backgroundImage={heroBg && !hasVideoBg ? replaceUrlInAttribute(heroBg) : undefined}
height={heroHeight}
overlay={!!heroBg}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
/>
);
@@ -421,8 +477,33 @@ function parseWPBakery(html: string): React.ReactNode[] {
const title = $h3.text().trim();
const content = $ps.map((pIdx, pEl) => $(pEl).html() || '').get().join('<br/>');
// Calculate overlay opacity
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section key={`content-${i}`} padding="lg">
<Section
key={`content-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<h3 className="text-2xl font-bold mb-4">{title}</h3>
<ContentRenderer content={content} parsePatterns={false} />
</Section>
@@ -438,7 +519,121 @@ function parseWPBakery(html: string): React.ReactNode[] {
return;
}
// PATTERN 12: Slider/Carousel (nectar_slider, vc_row with slider class)
// PATTERN 12: Generic content row with background (no specific pattern)
// This handles rows with backgrounds that don't match other patterns
if (bgImage || bgColor || colorOverlay || videoMp4 || videoWebm) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
const innerHtml = $row.html();
if (innerHtml) {
elements.push(
<Section
key={`bg-section-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<ContentRenderer content={innerHtml} parsePatterns={true} />
</Section>
);
$row.remove();
return;
}
}
// PATTERN 13: Buttons (vc_btn, .btn classes)
const $buttons = $row.find('a[class*="btn"], a.vc_btn, button.vc_btn');
if ($buttons.length > 0) {
const buttons = $buttons.map((btnIdx, btnEl) => {
const $btn = $(btnEl);
const text = $btn.text().trim();
const href = $btn.attr('href');
const classes = $btn.attr('class') || '';
// Determine variant from classes
let variant: 'primary' | 'secondary' | 'outline' | 'ghost' = 'primary';
if (classes.includes('btn-outline') || classes.includes('vc_btn-outline')) variant = 'outline';
if (classes.includes('btn-secondary') || classes.includes('vc_btn-secondary')) variant = 'secondary';
if (classes.includes('btn-ghost') || classes.includes('vc_btn-ghost')) variant = 'ghost';
// Determine size
let size: 'sm' | 'md' | 'lg' = 'md';
if (classes.includes('btn-large') || classes.includes('vc_btn-lg')) size = 'lg';
if (classes.includes('btn-small') || classes.includes('vc_btn-sm')) size = 'sm';
return (
<Button
key={`btn-${btnIdx}`}
variant={variant}
size={size}
onClick={() => href && (window.location.href = replaceUrlInAttribute(href))}
className={classes.includes('btn-full') ? 'w-full' : ''}
>
{text || 'Click Here'}
</Button>
);
}).get();
if (buttons.length > 0) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section
key={`buttons-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<div className={cn(
'flex flex-wrap gap-3',
buttons.length > 1 ? 'justify-center' : 'justify-start'
)}>
{buttons}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 14: Slider/Carousel (nectar_slider, vc_row with slider class)
if ($row.hasClass('nectar-slider') || $row.hasClass('vc_row-slider') || $row.find('.nectar-slider').length > 0) {
const slides: Slide[] = [];
@@ -501,56 +696,6 @@ function parseWPBakery(html: string): React.ReactNode[] {
}
}
// PATTERN 13: Buttons (vc_btn, .btn classes)
const $buttons = $row.find('a[class*="btn"], a.vc_btn, button.vc_btn');
if ($buttons.length > 0) {
const buttons = $buttons.map((btnIdx, btnEl) => {
const $btn = $(btnEl);
const text = $btn.text().trim();
const href = $btn.attr('href');
const classes = $btn.attr('class') || '';
// Determine variant from classes
let variant: 'primary' | 'secondary' | 'outline' | 'ghost' = 'primary';
if (classes.includes('btn-outline') || classes.includes('vc_btn-outline')) variant = 'outline';
if (classes.includes('btn-secondary') || classes.includes('vc_btn-secondary')) variant = 'secondary';
if (classes.includes('btn-ghost') || classes.includes('vc_btn-ghost')) variant = 'ghost';
// Determine size
let size: 'sm' | 'md' | 'lg' = 'md';
if (classes.includes('btn-large') || classes.includes('vc_btn-lg')) size = 'lg';
if (classes.includes('btn-small') || classes.includes('vc_btn-sm')) size = 'sm';
return (
<Button
key={`btn-${btnIdx}`}
variant={variant}
size={size}
onClick={() => href && (window.location.href = replaceUrlInAttribute(href))}
className={classes.includes('btn-full') ? 'w-full' : ''}
>
{text || 'Click Here'}
</Button>
);
}).get();
if (buttons.length > 0) {
elements.push(
<Section key={`buttons-${i}`} padding="lg">
<div className={cn(
'flex flex-wrap gap-3',
buttons.length > 1 ? 'justify-center' : 'justify-start'
)}>
{buttons}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 14: Testimonials (quote blocks, testimonial divs)
const hasTestimonialQuotes = rowText.includes('„') || rowText.includes('“') ||
rowText.includes('"') || rowText.includes('Expertise') ||
@@ -691,6 +836,389 @@ function parseWPBakery(html: string): React.ReactNode[] {
}
}
// PATTERN 16: External Resource Links (vlp-link-container)
const $vlpLinks = $row.find('.vlp-link-container');
if ($vlpLinks.length > 0) {
const linkCards: React.ReactNode[] = [];
$vlpLinks.each((linkIdx, linkEl) => {
const $link = $(linkEl);
const href = $link.find('a').first().attr('href') || '';
const title = $link.find('.vlp-link-title').first().text().trim() || $link.find('a').first().text().trim();
const summary = $link.find('.vlp-link-summary').first().text().trim();
const imgSrc = $link.find('img').first().attr('src');
if (href && title) {
linkCards.push(
<Card key={`vlp-${linkIdx}`} variant="bordered" padding="md" hoverable>
<a
href={replaceUrlInAttribute(href)}
target="_blank"
rel="noopener noreferrer"
className="block hover:no-underline"
>
{imgSrc && (
<div className="mb-3">
<FeaturedImage
src={replaceUrlInAttribute(imgSrc)}
alt={title}
size="sm"
aspectRatio="1:1"
className="rounded-md"
/>
</div>
)}
<h4 className="text-lg font-bold mb-2 text-primary">{title}</h4>
{summary && <p className="text-sm text-gray-600">{summary}</p>}
<div className="mt-2 text-xs text-gray-500 flex items-center gap-1">
<span>🔗</span>
<span className="truncate">{new URL(href).hostname}</span>
</div>
</a>
</Card>
);
}
});
if (linkCards.length > 0) {
elements.push(
<Section key={`vlp-${i}`} padding="lg">
<h3 className="text-2xl font-bold mb-4">Related Resources</h3>
<Grid cols={Math.min(linkCards.length, 3) as 2 | 3 | 4} gap="md">
{linkCards}
</Grid>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 17: Technical Specification Tables
const $tables = $row.find('table');
if ($tables.length > 0) {
const tableElements: React.ReactNode[] = [];
$tables.each((tableIdx, tableEl) => {
const $table = $(tableEl);
const $rows = $table.find('tr');
// Check if this is a spec table (property/value format)
const isSpecTable = $rows.length > 2 && $rows.eq(0).text().includes('Property');
if (isSpecTable) {
const specs: { property: string; value: string }[] = [];
$rows.each((rowIdx, rowEl) => {
if (rowIdx === 0) return; // Skip header
const $row = $(rowEl);
const $cells = $row.find('td');
if ($cells.length >= 2) {
specs.push({
property: $cells.eq(0).text().trim(),
value: $cells.eq(1).text().trim()
});
}
});
if (specs.length > 0) {
tableElements.push(
<div key={`spec-table-${tableIdx}`} className="bg-gray-50 rounded-lg p-6 border border-gray-200">
<h4 className="text-xl font-bold mb-4">Technical Specifications</h4>
<dl className="space-y-3">
{specs.map((spec, idx) => (
<div key={idx} className="flex flex-col sm:flex-row sm:gap-4 border-b border-gray-200 pb-2 last:border-0">
<dt className="font-semibold text-gray-700 sm:w-1/3">{spec.property}</dt>
<dd className="text-gray-600 sm:w-2/3">{spec.value}</dd>
</div>
))}
</dl>
</div>
);
}
} else {
// Regular table - use default HTML rendering
const tableHtml = $table.prop('outerHTML');
if (tableHtml) {
tableElements.push(
<div key={`table-${tableIdx}`} className="my-4 overflow-x-auto">
<ContentRenderer content={tableHtml} parsePatterns={false} />
</div>
);
}
}
});
if (tableElements.length > 0) {
elements.push(
<Section key={`tables-${i}`} padding="lg">
{tableElements}
</Section>
);
$row.remove();
return;
}
}
// PATTERN 18: FAQ Sections
const $questions = $row.find('h3, h4').filter((idx, el) => {
const text = $(el).text().trim();
return text.endsWith('?') || text.toLowerCase().includes('faq') || text.toLowerCase().includes('question');
});
if ($questions.length > 0) {
const faqItems: React.ReactNode[] = [];
$questions.each((qIdx, qEl) => {
const $q = $(qEl);
const question = $q.text().trim();
const $nextP = $q.next('p');
const answer = $nextP.text().trim();
if (question && answer) {
faqItems.push(
<details key={`faq-${qIdx}`} className="bg-white border border-gray-200 rounded-lg p-4 my-2">
<summary className="font-bold cursor-pointer text-primary hover:text-primary-dark">
{question}
</summary>
<p className="mt-2 text-gray-700">{answer}</p>
</details>
);
}
});
if (faqItems.length > 0) {
elements.push(
<Section key={`faq-${i}`} padding="lg">
<h3 className="text-2xl font-bold mb-4">Frequently Asked Questions</h3>
{faqItems}
</Section>
);
$row.remove();
return;
}
}
// PATTERN 19: Call-to-Action (CTA) Sections
const $ctaText = $row.text();
const isCTA = $ctaText.includes('👉') ||
$ctaText.includes('Contact Us') ||
$ctaText.includes('Get in touch') ||
$ctaText.includes('Send your inquiry') ||
($row.find('a[href*="contact"]').length > 0 && $row.find('h2, h3').length > 0);
if (isCTA && colCount <= 2) {
const $title = $row.find('h2, h3').first();
const $desc = $row.find('p').first();
const $button = $row.find('a').first();
const title = $title.text().trim();
const description = $desc.text().trim();
const buttonText = $button.text().trim() || 'Contact Us';
const buttonHref = $button.attr('href') || '/contact';
if (title) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section
key={`cta-${i}`}
padding="xl"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor || '#1a1a1a'}
colorOverlay={colorOverlay || '#000000'}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
textAlignment="center"
textColor="light"
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<div className="max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold mb-4">{title}</h2>
{description && <p className="text-xl mb-6 opacity-90">{description}</p>}
<Button
variant="primary"
size="lg"
onClick={() => window.location.href = replaceUrlInAttribute(buttonHref)}
>
{buttonText}
</Button>
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 20: Quote/Blockquote Sections
const $blockquote = $row.find('blockquote').first();
if ($blockquote.length > 0 && colCount === 1) {
const quote = $blockquote.text().trim();
const $cite = $blockquote.find('cite');
const author = $cite.text().trim();
if (quote) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
elements.push(
<Section
key={`quote-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
textAlignment="center"
textColor={textColor as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<div className="max-w-2xl mx-auto">
<div className="text-4xl font-serif text-primary mb-4">"</div>
<blockquote className="text-2xl md:text-3xl font-serif italic mb-4">
{quote}
</blockquote>
{author && (
<cite className="text-lg font-semibold not-italic text-gray-300">
— {author}
</cite>
)}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 21: Numbered List with Icons
const $listItems = $row.find('li');
if ($listItems.length >= 3 && $row.find('i[class*="fa-"]').length > 0) {
const items: React.ReactNode[] = [];
$listItems.each((liIdx, liEl) => {
const $li = $(liEl);
const $icon = $li.find('i[class*="fa-"]').first();
const iconClass = $icon.attr('class') || '';
const text = $li.text().trim().replace(/\s+/g, ' ');
if (text) {
const iconProps = parseWpIcon(iconClass);
items.push(
<div key={`list-item-${liIdx}`} className="flex items-start gap-3">
<div className="flex-shrink-0 mt-1">
<Icon {...iconProps} className="text-primary" />
</div>
<div className="flex-1">
<p className="text-gray-700">{text}</p>
</div>
</div>
);
}
});
if (items.length > 0) {
elements.push(
<Section key={`icon-list-${i}`} padding="lg">
<div className="space-y-3">
{items}
</div>
</Section>
);
$row.remove();
return;
}
}
// PATTERN 22: Video Background Row (nectar-video-wrap or data-video-bg)
if (hasVideoBg) {
const overlayOpacityValue = overlayStrength ? parseFloat(overlayStrength) : 0.5;
const innerHtml = $row.html();
if (innerHtml) {
elements.push(
<Section
key={`video-bg-${i}`}
padding="lg"
backgroundImage={bgImage && !hasVideoBg ? replaceUrlInAttribute(bgImage) : undefined}
backgroundColor={bgColor}
colorOverlay={colorOverlay}
overlayOpacity={overlayOpacityValue}
enableGradient={enableGradient}
gradientDirection={gradientDirection as any}
colorOverlay2={colorOverlay2}
parallaxBg={parallaxBg}
parallaxBgSpeed={parallaxBgSpeed as any}
bgImageAnimation={bgImageAnimation as any}
topPadding={topPadding}
bottomPadding={bottomPadding}
textAlignment={textAlignment as any}
textColor={textColor as any}
shapeType={shapeType}
scenePosition={scenePosition as any}
fullScreenRowPosition={fullScreen as any}
videoMp4={videoMp4 ? replaceUrlInAttribute(videoMp4) : undefined}
videoWebm={videoWebm ? replaceUrlInAttribute(videoWebm) : undefined}
>
<ContentRenderer content={innerHtml} parsePatterns={true} />
</Section>
);
$row.remove();
return;
}
}
// PATTERN 23: Video Embed with Text
const $video = $row.find('video').first();
if ($video.length > 0 && colCount === 1) {
const videoSrc = $video.attr('src') || $video.find('source').first().attr('src');
const $title = $row.find('h2, h3').first();
const $desc = $row.find('p').first();
if (videoSrc) {
elements.push(
<Section key={`video-embed-${i}`} padding="lg">
<div className="grid md:grid-cols-2 gap-6 items-center">
<div>
<video
controls
className="w-full rounded-lg shadow-lg"
style={{ opacity: 1 }}
poster={replaceUrlInAttribute($video.attr('poster') || '')}
>
<source src={replaceUrlInAttribute(videoSrc)} type="video/mp4" />
</video>
</div>
<div>
{$title.length > 0 && <h3 className="text-2xl font-bold mb-3">{$title.text().trim()}</h3>}
{$desc.length > 0 && <ContentRenderer content={$desc.html() || ''} parsePatterns={false} />}
</div>
</div>
</Section>
);
$row.remove();
return;
}
}
// FALLBACK: Generic section with nested content
const innerHtml = $row.html();
if (innerHtml) {
@@ -925,7 +1453,7 @@ function parseHTMLToReact(html: string): React.ReactNode {
// Get sources
const sources: React.ReactNode[] = [];
Array.from(element.childNodes).forEach((child, i) => {
Array.from(node.childNodes).forEach((child, i) => {
if (child.nodeType === Node.ELEMENT_NODE && (child as HTMLElement).tagName.toLowerCase() === 'source') {
const sourceEl = child as HTMLSourceElement;
// Replace asset URLs in source src
@@ -950,6 +1478,10 @@ function parseHTMLToReact(html: string): React.ReactNode {
videoProps.poster = replaceUrlInAttribute(element.getAttribute('poster'));
}
// Ensure video is always fully visible
if (!videoProps.style) videoProps.style = {};
videoProps.style.opacity = 1;
return (
<video {...videoProps}>
{sources}
@@ -1004,6 +1536,7 @@ function parseHTMLToReact(html: string): React.ReactNode {
loop
muted
playsInline
style={{ opacity: 1 }}
>
{mp4 && <source src={replaceUrlInAttribute(mp4)} type="video/mp4" />}
{webm && <source src={replaceUrlInAttribute(webm)} type="video/webm" />}
@@ -1166,19 +1699,20 @@ function replaceWordPressAssets(html: string): string {
/**
* Convert WordPress/Salient classes to Tailwind equivalents
* Note: vc-row and vc-column classes are preserved for pattern parsing
*/
function convertWordPressClasses(html: string): string {
const classMap: Record<string, string> = {
// Salient/Vc_row classes
'vc_row': 'flex flex-wrap -mx-4',
'vc_row-fluid': 'w-full',
'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',
// Salient/Vc_row classes - PRESERVED for pattern parsing
// 'vc-row': 'flex flex-wrap -mx-4', // REMOVED - handled by parseWPBakery
// 'vc_row-fluid': 'w-full', // REMOVED - handled by parseWPBakery
// 'vc_col-sm-12': 'w-full px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-md-6': 'w-full md:w-1/2 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-md-4': 'w-full md:w-1/3 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-md-3': 'w-full md:w-1/4 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-lg-6': 'w-full lg:w-1/2 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-lg-4': 'w-full lg:w-1/3 px-4', // REMOVED - handled by parseWPBakery
// 'vc_col-lg-3': 'w-full lg:w-1/4 px-4', // REMOVED - handled by parseWPBakery
// Typography
'wpb_wrapper': 'space-y-4',