@marvalt/wparser 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1390,7 +1390,7 @@ function renderBlock(block, registry, key, options, page) {
1390
1390
  const children = block.innerBlocks && block.innerBlocks.length
1391
1391
  ? block.innerBlocks.map((child, i) => renderBlock(child, registry, `${key}-${i}`, options, page))
1392
1392
  : undefined;
1393
- const node = Renderer({ block, children, context: { registry, page } });
1393
+ const node = Renderer({ block, children, context: { registry, page, colorMapper: registry.colorMapper } });
1394
1394
  if (options?.debugWrappers) {
1395
1395
  return (jsxRuntimeExports.jsx("div", { "data-block": block.name, className: "wp-block", children: node }, key));
1396
1396
  }
@@ -1662,98 +1662,6 @@ function extractImageUrlWithFallback(block) {
1662
1662
  return getImageUrl(block);
1663
1663
  }
1664
1664
 
1665
- /**
1666
- * Style mapping utilities
1667
- * Maps WordPress block attributes to Tailwind CSS classes
1668
- */
1669
- /**
1670
- * Map WordPress alignment to Tailwind classes
1671
- */
1672
- function getAlignmentClasses(align) {
1673
- if (!align)
1674
- return '';
1675
- switch (align) {
1676
- case 'full':
1677
- return 'w-full';
1678
- case 'wide':
1679
- return 'max-w-7xl mx-auto';
1680
- case 'center':
1681
- return 'mx-auto';
1682
- case 'left':
1683
- return 'mr-auto';
1684
- case 'right':
1685
- return 'ml-auto';
1686
- default:
1687
- return '';
1688
- }
1689
- }
1690
- /**
1691
- * Map WordPress text alignment to Tailwind classes
1692
- */
1693
- function getTextAlignClasses(textAlign) {
1694
- if (!textAlign)
1695
- return '';
1696
- switch (textAlign) {
1697
- case 'center':
1698
- return 'text-center';
1699
- case 'left':
1700
- return 'text-left';
1701
- case 'right':
1702
- return 'text-right';
1703
- default:
1704
- return '';
1705
- }
1706
- }
1707
- /**
1708
- * Map WordPress font size to Tailwind classes
1709
- */
1710
- function getFontSizeClasses(fontSize) {
1711
- if (!fontSize)
1712
- return '';
1713
- // Map WordPress font sizes to Tailwind
1714
- const sizeMap = {
1715
- 'small': 'text-sm',
1716
- 'medium': 'text-base',
1717
- 'large': 'text-lg',
1718
- 'x-large': 'text-xl',
1719
- 'xx-large': 'text-3xl',
1720
- 'xxx-large': 'text-4xl',
1721
- };
1722
- return sizeMap[fontSize] || '';
1723
- }
1724
- /**
1725
- * Get container classes based on layout and alignment
1726
- */
1727
- function getContainerClasses(align, layout) {
1728
- const alignClass = getAlignmentClasses(align);
1729
- // If layout is constrained, use container
1730
- if (layout?.type === 'constrained') {
1731
- return align === 'full' ? 'w-full' : 'container';
1732
- }
1733
- return alignClass || 'container';
1734
- }
1735
- /**
1736
- * Get spacing classes for sections
1737
- */
1738
- function getSectionSpacingClasses() {
1739
- return 'py-16 md:py-24';
1740
- }
1741
- /**
1742
- * Get content spacing classes
1743
- */
1744
- function getContentSpacingClasses() {
1745
- return 'space-y-6';
1746
- }
1747
- /**
1748
- * Build className string from multiple class sources
1749
- */
1750
- function buildClassName(...classes) {
1751
- return classes
1752
- .filter((cls) => Boolean(cls && cls.trim()))
1753
- .join(' ')
1754
- .trim();
1755
- }
1756
-
1757
1665
  /**
1758
1666
  * Cloudflare Images URL helpers for wparser package
1759
1667
  * Formats Cloudflare image URLs with variants for optimal performance
@@ -1790,834 +1698,968 @@ const getCloudflareVariantUrl = (url, options) => {
1790
1698
  return `${base}/${variant}`;
1791
1699
  };
1792
1700
 
1793
- const Paragraph = ({ block, context }) => {
1794
- const content = getBlockTextContent(block);
1701
+ /**
1702
+ * Extract background image URL from a block
1703
+ * Checks various possible sources: cloudflareUrl, url, backgroundImage, innerHTML, featured image
1704
+ */
1705
+ function extractBackgroundImage(block, page) {
1795
1706
  const attrs = block.attributes || {};
1796
- const textAlign = getTextAlignClasses(attrs['align']);
1797
- // Check if content contains shortcodes
1798
- const hasShortcodes = /\[(\w+)/.test(content);
1799
- if (hasShortcodes && context.registry.shortcodes) {
1800
- const parts = renderTextWithShortcodes(content, context.registry);
1801
- // Check if any part is a block-level element (section, div, etc.)
1802
- // If so, render without wrapping in <p> to avoid DOM nesting violations
1803
- const isBlockLevelElement = (element) => {
1804
- if (!React.isValidElement(element)) {
1805
- return false;
1806
- }
1807
- const type = element.type;
1808
- // Check for block-level HTML elements
1809
- if (typeof type === 'string' && ['section', 'div', 'article', 'header', 'footer', 'aside', 'nav'].includes(type)) {
1810
- return true;
1811
- }
1812
- // Check if it's React.Fragment - recursively check its children
1813
- if (type === React.Fragment) {
1814
- const fragmentProps = element.props;
1815
- const children = React.Children.toArray(fragmentProps.children);
1816
- return children.some((child) => isBlockLevelElement(child));
1817
- }
1818
- // Check if it's a React component (likely block-level)
1819
- // Most custom components render block-level content
1820
- if (typeof type === 'function' || (typeof type === 'object' && type !== null && type !== React.Fragment)) {
1821
- return true;
1822
- }
1823
- return false;
1824
- };
1825
- const hasBlockLevelContent = React.Children.toArray(parts).some((part) => isBlockLevelElement(part));
1826
- if (hasBlockLevelContent) {
1827
- // Render block-level content without <p> wrapper
1828
- return jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: parts });
1829
- }
1830
- return jsxRuntimeExports.jsx("p", { className: buildClassName('text-gray-700', textAlign), children: parts });
1707
+ // Check if block uses featured image
1708
+ if (attrs['useFeaturedImage'] === true && page?._embedded?.['wp:featuredmedia']?.[0]?.source_url) {
1709
+ return page._embedded['wp:featuredmedia'][0].source_url;
1831
1710
  }
1832
- return jsxRuntimeExports.jsx("p", { className: buildClassName('text-gray-700', textAlign), children: content });
1833
- };
1834
- const Heading = ({ block, children }) => {
1711
+ // Use the improved extraction function that handles incomplete cloudflareUrl
1712
+ // This will check cloudflareUrl first, then innerHTML, then regular attributes
1713
+ return extractImageUrlWithFallback(block);
1714
+ }
1715
+ /**
1716
+ * Extract image URL from a block
1717
+ * Returns Cloudflare URL if available, otherwise WordPress URL
1718
+ */
1719
+ function extractImageUrl(block) {
1720
+ // Use the improved extraction function that handles incomplete cloudflareUrl
1721
+ return extractImageUrlWithFallback(block);
1722
+ }
1723
+ /**
1724
+ * Extract image attributes (url, alt, width, height)
1725
+ */
1726
+ function extractImageAttributes(block) {
1727
+ return getImageAttributes(block);
1728
+ }
1729
+ /**
1730
+ * Extract title/heading text from a block
1731
+ */
1732
+ function extractTitle(block) {
1835
1733
  const attrs = block.attributes || {};
1836
- const { level = 2 } = attrs;
1837
- const content = getBlockTextContent(block);
1838
- const textAlign = getTextAlignClasses(attrs['textAlign']);
1839
- const fontSize = getFontSizeClasses(attrs['fontSize']);
1840
- const Tag = `h${Math.min(Math.max(Number(level) || 2, 1), 6)}`;
1841
- // Default heading sizes if fontSize not specified
1842
- const sizeClass = fontSize || (level === 1 ? 'text-4xl' : level === 2 ? 'text-3xl' : level === 3 ? 'text-2xl' : 'text-xl');
1843
- return (jsxRuntimeExports.jsx(Tag, { className: buildClassName('font-bold text-gray-900', sizeClass, textAlign), children: children ?? content }));
1844
- };
1845
- const Image = ({ block }) => {
1846
- const imageAttrs = getImageAttributes(block);
1847
- if (!imageAttrs.url)
1848
- return null;
1849
- // Use Cloudflare variant URL if it's a Cloudflare image
1850
- let imageUrl = imageAttrs.url;
1851
- if (isCloudflareImageUrl(imageUrl)) {
1852
- const width = imageAttrs.width || 1024;
1853
- const height = imageAttrs.height;
1854
- imageUrl = getCloudflareVariantUrl(imageUrl, { width, height });
1855
- }
1856
- return (jsxRuntimeExports.jsx("img", { src: imageUrl, alt: imageAttrs.alt, width: imageAttrs.width, height: imageAttrs.height, className: "w-full h-auto rounded-lg object-cover", style: { maxWidth: '100%', height: 'auto' }, loading: "lazy" }));
1857
- };
1858
- const List = ({ block, children }) => {
1734
+ const title = attrs['title'] || attrs['content'] || getBlockTextContent(block);
1735
+ return typeof title === 'string' ? title.trim() : null;
1736
+ }
1737
+ /**
1738
+ * Extract content/text from a block
1739
+ * Returns React node for rendering
1740
+ */
1741
+ function extractContent(block, context) {
1742
+ const text = getBlockTextContent(block);
1743
+ return text || null;
1744
+ }
1745
+ /**
1746
+ * Extract media position from media-text block
1747
+ */
1748
+ function extractMediaPosition(block) {
1859
1749
  const attrs = block.attributes || {};
1860
- const { ordered } = attrs;
1861
- const Tag = ordered ? 'ol' : 'ul';
1862
- return React.createElement(Tag, { className: 'list-disc pl-6 space-y-2 text-gray-700' }, children);
1863
- };
1864
- const ListItem = ({ children }) => {
1865
- return jsxRuntimeExports.jsx("li", { className: "text-gray-700", children: children });
1866
- };
1867
- const Group = ({ block, children }) => {
1750
+ const position = attrs['mediaPosition'] || 'left';
1751
+ return position === 'right' ? 'right' : 'left';
1752
+ }
1753
+ /**
1754
+ * Extract vertical alignment from block
1755
+ */
1756
+ function extractVerticalAlignment(block) {
1868
1757
  const attrs = block.attributes || {};
1869
- const align = attrs['align'];
1870
- // Layout can be an object with type property, or nested structure
1871
- const layout = attrs['layout'];
1872
- // Determine if this is a section-level group (has alignment) or content-level
1873
- const isSection = align === 'full' || align === 'wide';
1874
- const containerClass = getContainerClasses(align, layout);
1875
- const spacingClass = isSection ? getSectionSpacingClasses() : getContentSpacingClasses();
1876
- // Ensure container class is always applied for constrained groups
1877
- const finalContainerClass = layout?.type === 'constrained' && align === 'wide'
1878
- ? 'container'
1879
- : containerClass;
1880
- return (jsxRuntimeExports.jsx("div", { className: buildClassName(finalContainerClass, spacingClass), children: children }));
1881
- };
1882
- const Columns = ({ block, children }) => {
1758
+ const alignment = attrs['verticalAlignment'] || 'center';
1759
+ if (alignment === 'top' || alignment === 'bottom') {
1760
+ return alignment;
1761
+ }
1762
+ return 'center';
1763
+ }
1764
+ /**
1765
+ * Extract alignment (full, wide, contained) from block
1766
+ */
1767
+ function extractAlignment(block) {
1883
1768
  const attrs = block.attributes || {};
1884
1769
  const align = attrs['align'];
1885
- const alignClass = getAlignmentClasses(align);
1886
- return (jsxRuntimeExports.jsx("div", { className: buildClassName('grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-12', alignClass), children: children }));
1887
- };
1888
- const Column = ({ block, children }) => {
1889
- const attrs = block.attributes || {};
1890
- const width = attrs['width'];
1891
- // Handle column width (e.g., "50%" becomes flex-basis)
1892
- const style = width ? { flexBasis: width } : undefined;
1893
- return (jsxRuntimeExports.jsx("div", { className: "space-y-4", style: style, children: children }));
1894
- };
1895
- const Separator = () => jsxRuntimeExports.jsx("hr", { className: "border-gray-200 my-8" });
1896
- const ButtonBlock = ({ block }) => {
1770
+ if (align === 'full' || align === 'wide') {
1771
+ return align;
1772
+ }
1773
+ return 'contained';
1774
+ }
1775
+ /**
1776
+ * Extract overlay color from cover block
1777
+ */
1778
+ function extractOverlayColor(block) {
1897
1779
  const attrs = block.attributes || {};
1898
- let url = attrs['url'];
1899
- let text = attrs['text'];
1900
- attrs['linkDestination'];
1901
- // Extract from innerHTML if not in attributes (buttons often store data in innerHTML)
1902
- if (!url && block.innerHTML) {
1903
- const linkMatch = block.innerHTML.match(/<a[^>]+href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/i);
1904
- if (linkMatch) {
1905
- url = linkMatch[1];
1906
- text = linkMatch[2] || text;
1907
- }
1908
- }
1909
- // Get text from block content if still missing
1910
- if (!text) {
1911
- text = getBlockTextContent(block);
1780
+ const overlayColor = attrs['overlayColor'];
1781
+ if (typeof overlayColor === 'string') {
1782
+ return overlayColor;
1912
1783
  }
1913
- if (!url && !text)
1914
- return null;
1915
- const buttonText = text || 'Learn more';
1916
- // Handle internal vs external links
1917
- const isExternal = url && (url.startsWith('http://') || url.startsWith('https://'));
1918
- const linkProps = isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {};
1919
- return (jsxRuntimeExports.jsx("a", { href: url || '#', className: "inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-primary-foreground font-medium hover:bg-primary/90 transition-colors", ...linkProps, children: buttonText }));
1920
- };
1921
- const Cover = ({ block, children }) => {
1784
+ return null;
1785
+ }
1786
+ /**
1787
+ * Extract dim ratio (overlay opacity) from cover block
1788
+ */
1789
+ function extractDimRatio(block) {
1922
1790
  const attrs = block.attributes || {};
1923
- const { url, id, backgroundImage, cloudflareUrl, overlayColor, dimRatio = 0, align = 'full', minHeight, minHeightUnit = 'vh', hasParallax, } = attrs;
1924
- // Get background image URL from various possible sources
1925
- // Use the improved extraction function that handles incomplete cloudflareUrl
1926
- let bgImageUrl = null;
1927
- // First, try cloudflareUrl if it's valid
1928
- if (cloudflareUrl && isValidCloudflareUrl(cloudflareUrl)) {
1929
- bgImageUrl = cloudflareUrl;
1791
+ const dimRatio = attrs['dimRatio'];
1792
+ if (typeof dimRatio === 'number') {
1793
+ return dimRatio;
1930
1794
  }
1931
- // If not valid or not found, try regular attributes
1932
- if (!bgImageUrl) {
1933
- bgImageUrl = url || backgroundImage || (typeof backgroundImage === 'object' && backgroundImage?.url) || null;
1795
+ return 0;
1796
+ }
1797
+ /**
1798
+ * Extract min height from block
1799
+ */
1800
+ function extractMinHeight(block) {
1801
+ const attrs = block.attributes || {};
1802
+ const minHeight = attrs['minHeight'];
1803
+ const minHeightUnit = attrs['minHeightUnit'] || 'vh';
1804
+ if (typeof minHeight === 'number') {
1805
+ return { value: minHeight, unit: minHeightUnit };
1934
1806
  }
1935
- // If still not found, use the fallback extraction (from innerHTML)
1936
- if (!bgImageUrl) {
1937
- bgImageUrl = extractImageUrlWithFallback(block);
1807
+ return null;
1808
+ }
1809
+ /**
1810
+ * Extract heading level from heading block
1811
+ */
1812
+ function extractHeadingLevel(block) {
1813
+ const attrs = block.attributes || {};
1814
+ const level = attrs['level'];
1815
+ if (typeof level === 'number' && level >= 1 && level <= 6) {
1816
+ return level;
1938
1817
  }
1939
- // Convert to Cloudflare URL variant if it's a Cloudflare image
1940
- if (bgImageUrl) {
1941
- if (isCloudflareImageUrl(bgImageUrl)) {
1942
- // Use full width for cover images
1943
- bgImageUrl = getCloudflareVariantUrl(bgImageUrl, { width: 1920 });
1944
- }
1818
+ return 2; // Default to h2
1819
+ }
1820
+ /**
1821
+ * Extract text alignment from block
1822
+ */
1823
+ function extractTextAlign(block) {
1824
+ const attrs = block.attributes || {};
1825
+ const align = attrs['align'] || attrs['textAlign'];
1826
+ if (align === 'left' || align === 'center' || align === 'right') {
1827
+ return align;
1945
1828
  }
1946
- // Build alignment classes
1947
- const alignClass = getAlignmentClasses(align);
1948
- // Build style object
1949
- const style = {};
1950
- if (minHeight) {
1951
- const minHeightValue = typeof minHeight === 'number' ? minHeight : parseFloat(String(minHeight));
1952
- style.minHeight = minHeightUnit === 'vh' ? `${minHeightValue}vh` : `${minHeightValue}px`;
1829
+ return null;
1830
+ }
1831
+ /**
1832
+ * Extract font size from block
1833
+ */
1834
+ function extractFontSize(block) {
1835
+ const attrs = block.attributes || {};
1836
+ const fontSize = attrs['fontSize'];
1837
+ return typeof fontSize === 'string' ? fontSize : null;
1838
+ }
1839
+ /**
1840
+ * Convert image URL to Cloudflare variant if it's a Cloudflare URL
1841
+ */
1842
+ function convertImageToCloudflareVariant(url, options = {}) {
1843
+ if (!url)
1844
+ return null;
1845
+ if (isCloudflareImageUrl(url)) {
1846
+ const width = options.width || 1024;
1847
+ const height = options.height;
1848
+ return getCloudflareVariantUrl(url, { width, height });
1953
1849
  }
1954
- if (bgImageUrl) {
1955
- style.backgroundImage = `url(${bgImageUrl})`;
1956
- style.backgroundSize = 'cover';
1957
- style.backgroundPosition = 'center';
1958
- if (hasParallax) {
1959
- style.backgroundAttachment = 'fixed';
1850
+ return url;
1851
+ }
1852
+ /**
1853
+ * Extract title from innerBlocks (finds first heading block)
1854
+ */
1855
+ function extractTitleFromInnerBlocks(block) {
1856
+ const innerBlocks = block.innerBlocks || [];
1857
+ // Recursively search for heading blocks
1858
+ for (const innerBlock of innerBlocks) {
1859
+ if (innerBlock.name === 'core/heading') {
1860
+ return getBlockTextContent(innerBlock);
1960
1861
  }
1862
+ // Recursively search nested blocks
1863
+ const nestedTitle = extractTitleFromInnerBlocks(innerBlock);
1864
+ if (nestedTitle)
1865
+ return nestedTitle;
1961
1866
  }
1962
- // Calculate overlay opacity
1963
- const overlayOpacity = typeof dimRatio === 'number' ? dimRatio / 100 : 0;
1964
- return (jsxRuntimeExports.jsxs("div", { className: buildClassName('relative w-full', alignClass), style: style, children: [overlayOpacity > 0 && (jsxRuntimeExports.jsx("span", { className: "absolute inset-0 z-0", style: {
1965
- backgroundColor: overlayColor === 'contrast' ? '#000000' : (overlayColor || '#000000'),
1966
- opacity: overlayOpacity,
1967
- }, "aria-hidden": "true" })), jsxRuntimeExports.jsx("div", { className: buildClassName('relative z-10', align === 'full' ? 'w-full' : 'container mx-auto px-4'), children: children })] }));
1968
- };
1969
- const MediaText = ({ block, children, context }) => {
1970
- const attrs = block.attributes || {};
1971
- const { mediaPosition = 'left', verticalAlignment = 'center', imageFill = false, align, } = attrs;
1972
- // Access innerBlocks to identify media vs content
1867
+ return null;
1868
+ }
1869
+ /**
1870
+ * Extract subtitle/description from innerBlocks (finds first paragraph block)
1871
+ */
1872
+ function extractSubtitleFromInnerBlocks(block) {
1973
1873
  const innerBlocks = block.innerBlocks || [];
1974
- // Find media block (image or video)
1975
- let mediaBlockIndex = innerBlocks.findIndex((b) => b.name === 'core/image' || b.name === 'core/video');
1976
- // Render children - media-text typically has media as first child, then content
1977
- const childrenArray = React.Children.toArray(children);
1978
- let mediaElement = mediaBlockIndex >= 0 && childrenArray[mediaBlockIndex]
1979
- ? childrenArray[mediaBlockIndex]
1980
- : null;
1981
- // If no media element from innerBlocks, try to extract image URL
1982
- if (!mediaElement) {
1983
- const imageUrl = extractImageUrlWithFallback(block);
1984
- if (imageUrl) {
1985
- // Convert to Cloudflare variant if it's a Cloudflare URL
1986
- // extractImageUrlWithFallback already handles incomplete cloudflareUrl by falling back to innerHTML
1987
- const finalImageUrl = isCloudflareImageUrl(imageUrl)
1988
- ? getCloudflareVariantUrl(imageUrl, { width: 1024 })
1989
- : imageUrl;
1990
- mediaElement = (jsxRuntimeExports.jsx("img", { src: finalImageUrl, alt: "", className: "w-full h-auto rounded-lg shadow-lg object-cover", style: { maxWidth: '100%', height: 'auto' }, loading: "lazy" }));
1874
+ // Recursively search for paragraph blocks
1875
+ for (const innerBlock of innerBlocks) {
1876
+ if (innerBlock.name === 'core/paragraph') {
1877
+ const text = getBlockTextContent(innerBlock);
1878
+ if (text && text.trim()) {
1879
+ return text;
1880
+ }
1991
1881
  }
1882
+ // Recursively search nested blocks
1883
+ const nestedSubtitle = extractSubtitleFromInnerBlocks(innerBlock);
1884
+ if (nestedSubtitle)
1885
+ return nestedSubtitle;
1992
1886
  }
1993
- // Content is all other children
1994
- const contentElements = childrenArray.filter((_, index) => index !== mediaBlockIndex);
1995
- // Build alignment classes - ensure proper container width
1996
- // For 'wide', media-text blocks are typically inside constrained groups (which use 'container' class)
1997
- // So we should use 'w-full' to fill the parent container, not apply another max-width
1998
- // Only use 'max-w-7xl' for truly standalone wide blocks (rare case)
1999
- let alignClass;
2000
- let spacingClass;
2001
- if (align === 'full') {
2002
- alignClass = 'w-full';
2003
- // Full-width blocks are typically top-level sections, so add section spacing
2004
- spacingClass = getSectionSpacingClasses();
2005
- }
2006
- else if (align === 'wide') {
2007
- // Wide blocks are usually inside constrained groups (which already have container and spacing)
2008
- // So just fill the parent container without adding section spacing
2009
- alignClass = 'w-full';
2010
- spacingClass = ''; // No section spacing - parent group handles it
1887
+ return null;
1888
+ }
1889
+ /**
1890
+ * Extract buttons from innerBlocks (finds buttons block and extracts button data)
1891
+ */
1892
+ function extractButtonsFromInnerBlocks(block) {
1893
+ const buttons = [];
1894
+ const innerBlocks = block.innerBlocks || [];
1895
+ // Find buttons block
1896
+ const findButtonsBlock = (blocks) => {
1897
+ for (const innerBlock of blocks) {
1898
+ if (innerBlock.name === 'core/buttons') {
1899
+ return innerBlock;
1900
+ }
1901
+ if (innerBlock.innerBlocks) {
1902
+ const found = findButtonsBlock(innerBlock.innerBlocks);
1903
+ if (found)
1904
+ return found;
1905
+ }
1906
+ }
1907
+ return null;
1908
+ };
1909
+ const buttonsBlock = findButtonsBlock(innerBlocks);
1910
+ if (!buttonsBlock || !buttonsBlock.innerBlocks) {
1911
+ return buttons;
2011
1912
  }
2012
- else {
2013
- // Default to contained width (not full width)
2014
- alignClass = 'container mx-auto';
2015
- // Contained blocks might be standalone, so add section spacing
2016
- spacingClass = getSectionSpacingClasses();
1913
+ // Extract button data from button blocks
1914
+ for (const buttonBlock of buttonsBlock.innerBlocks) {
1915
+ if (buttonBlock.name === 'core/button') {
1916
+ const attrs = buttonBlock.attributes || {};
1917
+ const url = attrs['url'];
1918
+ const text = attrs['text'] || getBlockTextContent(buttonBlock);
1919
+ // Try to extract from innerHTML if not in attributes
1920
+ if (!url && buttonBlock.innerHTML) {
1921
+ const linkMatch = buttonBlock.innerHTML.match(/<a[^>]+href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/i);
1922
+ if (linkMatch) {
1923
+ const extractedUrl = linkMatch[1];
1924
+ const extractedText = linkMatch[2] || text;
1925
+ if (extractedUrl) {
1926
+ buttons.push({
1927
+ text: extractedText || 'Learn More',
1928
+ url: extractedUrl,
1929
+ isExternal: extractedUrl.startsWith('http://') || extractedUrl.startsWith('https://'),
1930
+ });
1931
+ }
1932
+ }
1933
+ }
1934
+ else if (url && text) {
1935
+ buttons.push({
1936
+ text,
1937
+ url,
1938
+ isExternal: url.startsWith('http://') || url.startsWith('https://'),
1939
+ });
1940
+ }
1941
+ }
2017
1942
  }
2018
- // Vertical alignment classes
2019
- const verticalAlignClass = verticalAlignment === 'top' ? 'items-start' :
2020
- verticalAlignment === 'bottom' ? 'items-end' :
2021
- 'items-center';
2022
- // Stack on mobile
2023
- const stackClass = 'flex-col md:flex-row';
2024
- // Media position determines order
2025
- const isMediaRight = mediaPosition === 'right';
2026
- return (jsxRuntimeExports.jsx("div", { className: buildClassName(alignClass, spacingClass), children: jsxRuntimeExports.jsxs("div", { className: buildClassName('flex', stackClass, verticalAlignClass, 'gap-6 lg:gap-12', 'w-full'), children: [jsxRuntimeExports.jsx("div", { className: buildClassName(isMediaRight ? 'order-2' : 'order-1', imageFill ? 'w-full md:w-1/2' : 'w-full md:w-1/2', 'min-w-0', // Allow flex item to shrink below content size
2027
- 'overflow-hidden' // Ensure images don't overflow
2028
- ), children: mediaElement || jsxRuntimeExports.jsx("div", { className: "bg-gray-200 h-64 rounded-lg" }) }), jsxRuntimeExports.jsx("div", { className: buildClassName(isMediaRight ? 'order-1' : 'order-2', 'w-full md:w-1/2', // Explicit width to ensure proper sizing
2029
- 'min-w-0', // Allow flex item to shrink below content size
2030
- getContentSpacingClasses()), children: contentElements.length > 0 ? contentElements : children })] }) }));
2031
- };
2032
- const Fallback = ({ block, children }) => {
2033
- // Minimal fallback; do not render innerHTML directly in v1 for safety
2034
- return jsxRuntimeExports.jsx("div", { "data-unknown-block": block.name, children: children });
2035
- };
2036
- function createDefaultRegistry() {
2037
- const renderers = {
2038
- 'core/paragraph': Paragraph,
2039
- 'core/heading': Heading,
2040
- 'core/image': Image,
2041
- 'core/list': List,
2042
- 'core/list-item': ListItem,
2043
- 'core/group': Group,
2044
- 'core/columns': Columns,
2045
- 'core/column': Column,
2046
- 'core/separator': Separator,
2047
- 'core/button': ButtonBlock,
2048
- 'core/buttons': ({ block, children }) => {
2049
- const attrs = block.attributes || {};
2050
- const layout = attrs['layout'];
2051
- const justifyContent = layout?.justifyContent || 'left';
2052
- const justifyClass = justifyContent === 'center' ? 'justify-center' :
2053
- justifyContent === 'right' ? 'justify-end' :
2054
- 'justify-start';
2055
- return jsxRuntimeExports.jsx("div", { className: buildClassName('flex flex-wrap gap-3', justifyClass), children: children });
2056
- },
2057
- 'core/quote': ({ children }) => jsxRuntimeExports.jsx("blockquote", { className: "border-l-4 pl-4 italic", children: children }),
2058
- 'core/code': ({ block }) => (jsxRuntimeExports.jsx("pre", { className: "bg-gray-100 p-3 rounded text-sm overflow-auto", children: jsxRuntimeExports.jsx("code", { children: getString(block) }) })),
2059
- 'core/preformatted': ({ block }) => jsxRuntimeExports.jsx("pre", { children: getString(block) }),
2060
- 'core/table': ({ children }) => jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: jsxRuntimeExports.jsx("table", { className: "table-auto w-full", children: children }) }),
2061
- 'core/table-row': ({ children }) => jsxRuntimeExports.jsx("tr", { children: children }),
2062
- 'core/table-cell': ({ children }) => jsxRuntimeExports.jsx("td", { className: "border px-3 py-2", children: children }),
2063
- // Cover block - hero sections with background images
2064
- 'core/cover': Cover,
2065
- // Media & Text block - side-by-side media and content
2066
- 'core/media-text': MediaText,
2067
- // HTML block - render innerHTML as-is
2068
- // Note: Shortcodes in HTML blocks are not parsed (they would need to be in text content)
2069
- 'core/html': ({ block }) => {
2070
- const html = block.innerHTML || '';
2071
- return jsxRuntimeExports.jsx("div", { dangerouslySetInnerHTML: { __html: html } });
2072
- },
2073
- };
2074
- return {
2075
- renderers,
2076
- shortcodes: {}, // Empty by default - apps extend this
2077
- fallback: Fallback,
2078
- };
2079
- }
2080
- // Legacy function for backward compatibility - use getBlockTextContent instead
2081
- function getString(block) {
2082
- return getBlockTextContent(block);
1943
+ return buttons;
2083
1944
  }
2084
-
2085
1945
  /**
2086
- * Check if a block matches a pattern
1946
+ * Extract text alignment from inner blocks
1947
+ * Recursively searches for heading or paragraph blocks with textAlign attribute
1948
+ * Also checks group blocks for justifyContent in layout
1949
+ * Priority: heading/paragraph textAlign takes precedence over group justifyContent
2087
1950
  */
2088
- function matchesPattern(block, pattern) {
2089
- // Check block name
2090
- if (block.name !== pattern.name) {
2091
- return false;
2092
- }
2093
- // Check attributes if specified
2094
- if (pattern.attributes) {
2095
- const blockAttrs = block.attributes || {};
2096
- for (const [key, value] of Object.entries(pattern.attributes)) {
2097
- if (blockAttrs[key] !== value) {
2098
- return false;
1951
+ function extractTextAlignFromInnerBlocks(block) {
1952
+ const innerBlocks = block.innerBlocks || [];
1953
+ // First, recursively search for heading or paragraph blocks with textAlign
1954
+ // (These take priority over group justifyContent)
1955
+ for (const innerBlock of innerBlocks) {
1956
+ if (innerBlock.name === 'core/heading' || innerBlock.name === 'core/paragraph') {
1957
+ const attrs = innerBlock.attributes || {};
1958
+ const textAlign = attrs['textAlign'];
1959
+ if (textAlign === 'left' || textAlign === 'center' || textAlign === 'right') {
1960
+ return textAlign;
2099
1961
  }
2100
1962
  }
1963
+ // Recursively search nested blocks for headings/paragraphs first
1964
+ const nestedAlign = extractTextAlignFromInnerBlocks(innerBlock);
1965
+ if (nestedAlign)
1966
+ return nestedAlign;
2101
1967
  }
2102
- // Check innerBlocks patterns if specified
2103
- if (pattern.innerBlocks && pattern.innerBlocks.length > 0) {
2104
- const blockInnerBlocks = block.innerBlocks || [];
2105
- // If pattern specifies innerBlocks, check if block has matching innerBlocks
2106
- for (const innerPattern of pattern.innerBlocks) {
2107
- // Find at least one matching innerBlock
2108
- const hasMatch = blockInnerBlocks.some(innerBlock => matchesPattern(innerBlock, innerPattern));
2109
- if (!hasMatch) {
2110
- return false;
2111
- }
1968
+ // Only check group blocks if no heading/paragraph alignment found
1969
+ for (const innerBlock of innerBlocks) {
1970
+ if (innerBlock.name === 'core/group') {
1971
+ const attrs = innerBlock.attributes || {};
1972
+ const layout = attrs['layout'];
1973
+ const justifyContent = layout?.justifyContent;
1974
+ if (justifyContent === 'left')
1975
+ return 'left';
1976
+ if (justifyContent === 'center')
1977
+ return 'center';
1978
+ if (justifyContent === 'right')
1979
+ return 'right';
2112
1980
  }
2113
1981
  }
2114
- return true;
1982
+ return null;
2115
1983
  }
2116
1984
  /**
2117
- * Find the best matching component mapping for a block
2118
- * Returns the mapping with highest priority that matches, or null
1985
+ * Parse contentPosition string into horizontal and vertical alignment
1986
+ * Format: "horizontal vertical" (e.g., "center center", "left top", "right bottom")
2119
1987
  */
2120
- function findMatchingMapping(block, mappings) {
2121
- // Sort by priority (higher first), then by order in array
2122
- const sortedMappings = [...mappings].sort((a, b) => {
2123
- const priorityA = a.priority ?? 0;
2124
- const priorityB = b.priority ?? 0;
2125
- if (priorityA !== priorityB) {
2126
- return priorityB - priorityA; // Higher priority first
1988
+ function parseContentPosition(contentPosition) {
1989
+ if (!contentPosition) {
1990
+ return { horizontal: 'left', vertical: 'center' };
1991
+ }
1992
+ const parts = contentPosition.trim().split(/\s+/);
1993
+ const horizontal = parts[0] || 'left';
1994
+ const vertical = parts[1] || 'center';
1995
+ return {
1996
+ horizontal: (horizontal === 'center' || horizontal === 'right' ? horizontal : 'left'),
1997
+ vertical: (vertical === 'top' || vertical === 'bottom' ? vertical : 'center'),
1998
+ };
1999
+ }
2000
+ /**
2001
+ * Extract video iframe HTML from innerBlocks (finds HTML block with iframe)
2002
+ */
2003
+ function extractVideoIframeFromInnerBlocks(block) {
2004
+ const innerBlocks = block.innerBlocks || [];
2005
+ // Recursively search for HTML blocks with iframe
2006
+ for (const innerBlock of innerBlocks) {
2007
+ if (innerBlock.name === 'core/html' && innerBlock.innerHTML) {
2008
+ // Check if innerHTML contains an iframe
2009
+ if (innerBlock.innerHTML.includes('<iframe')) {
2010
+ return innerBlock.innerHTML;
2011
+ }
2127
2012
  }
2128
- return 0; // Keep original order for same priority
2129
- });
2130
- // Find first matching mapping
2131
- for (const mapping of sortedMappings) {
2132
- if (matchesPattern(block, mapping.pattern)) {
2133
- return mapping;
2013
+ // Recursively search nested blocks
2014
+ if (innerBlock.innerBlocks) {
2015
+ const nestedVideo = extractVideoIframeFromInnerBlocks(innerBlock);
2016
+ if (nestedVideo)
2017
+ return nestedVideo;
2134
2018
  }
2135
2019
  }
2136
2020
  return null;
2137
2021
  }
2138
-
2139
2022
  /**
2140
- * Create an enhanced registry that supports pattern-based component mapping
2023
+ * Extract and map background color from block attributes
2024
+ * Uses colorMapper from context to convert WordPress theme colors to app CSS classes
2141
2025
  *
2142
- * This combines the default registry (for fallback) with app-specific component mappings.
2143
- * When a block matches a pattern, it uses the mapped component. Otherwise, it falls back
2144
- * to the default renderer.
2026
+ * WordPress controls which color is applied (via backgroundColor attribute),
2027
+ * app controls what it means (via colorMapper function)
2145
2028
  *
2146
- * @param mappings - Array of component mappings with patterns
2147
- * @param baseRegistry - Optional base registry (defaults to createDefaultRegistry())
2148
- * @returns Enhanced registry with pattern matching capabilities
2029
+ * @param block - WordPress block to extract background color from
2030
+ * @param context - Render context containing optional colorMapper
2031
+ * @returns CSS class string (e.g., 'bg-gray-100') or null if no mapping
2149
2032
  *
2150
2033
  * @example
2151
2034
  * ```ts
2152
- * const mappings: ComponentMapping[] = [
2153
- * {
2154
- * pattern: { name: 'core/cover' },
2155
- * component: HomeHeroSection,
2156
- * extractProps: (block) => ({
2157
- * backgroundImage: extractBackgroundImage(block),
2158
- * title: extractTitle(block),
2159
- * }),
2160
- * wrapper: SectionWrapper,
2161
- * },
2162
- * ];
2163
- *
2164
- * const registry = createEnhancedRegistry(mappings);
2035
+ * // In WordPress, group block has: backgroundColor: "accent-5"
2036
+ * // In app, colorMapper maps: "accent-5" → "bg-gray-100"
2037
+ * // Result: extractBackgroundColor returns "bg-gray-100"
2165
2038
  * ```
2166
2039
  */
2167
- function createEnhancedRegistry(mappings = [], baseRegistry) {
2168
- const base = baseRegistry || createDefaultRegistry();
2169
- // Create enhanced renderers that check patterns first
2170
- const enhancedRenderers = {
2171
- ...base.renderers,
2172
- };
2173
- // Override renderers for blocks that have mappings
2174
- // We need to check patterns at render time, so we create a wrapper renderer
2175
- const createPatternRenderer = (blockName) => {
2176
- return (props) => {
2177
- const { block, context } = props;
2178
- // Find matching mapping
2179
- const mapping = findMatchingMapping(block, mappings);
2180
- if (mapping) {
2181
- // Extract props from block
2182
- const componentProps = mapping.extractProps(block, context);
2183
- // Render component
2184
- const Component = mapping.component;
2185
- const content = jsxRuntimeExports.jsx(Component, { ...componentProps });
2186
- // Wrap with wrapper if provided
2187
- if (mapping.wrapper) {
2188
- const Wrapper = mapping.wrapper;
2189
- return jsxRuntimeExports.jsx(Wrapper, { block: block, children: content });
2190
- }
2191
- return content;
2192
- }
2193
- // Fall back to default renderer
2194
- const defaultRenderer = base.renderers[blockName] || base.fallback;
2195
- return defaultRenderer(props);
2196
- };
2197
- };
2198
- // For each mapping, override the renderer for that block name
2199
- for (const mapping of mappings) {
2200
- const blockName = mapping.pattern.name;
2201
- if (blockName) {
2202
- enhancedRenderers[blockName] = createPatternRenderer(blockName);
2203
- }
2204
- }
2205
- // Create matchBlock function
2206
- const matchBlock = (block) => {
2207
- return findMatchingMapping(block, mappings);
2208
- };
2209
- return {
2210
- ...base,
2211
- renderers: enhancedRenderers,
2212
- mappings,
2213
- matchBlock,
2214
- };
2215
- }
2216
-
2217
- const WPContent = ({ blocks, registry, className, page }) => {
2218
- if (!Array.isArray(blocks)) {
2219
- if (process.env.NODE_ENV !== 'production') {
2220
- // eslint-disable-next-line no-console
2221
- console.warn('WPContent: invalid blocks prop');
2222
- }
2040
+ function extractBackgroundColor(block, context) {
2041
+ const attrs = block.attributes || {};
2042
+ const wpColorName = attrs['backgroundColor'] || attrs['background'];
2043
+ if (!wpColorName || typeof wpColorName !== 'string') {
2223
2044
  return null;
2224
2045
  }
2225
- if (!registry || !registry.renderers) {
2226
- throw new Error('WPContent: registry is required');
2046
+ // Use colorMapper from context if available
2047
+ if (context.colorMapper) {
2048
+ return context.colorMapper(wpColorName);
2227
2049
  }
2228
- const ast = parseGutenbergBlocks(blocks);
2229
- return (jsxRuntimeExports.jsx("div", { className: className, children: renderNodes(ast, registry, undefined, page) }));
2230
- };
2050
+ // Fallback: return null (no background applied)
2051
+ return null;
2052
+ }
2231
2053
 
2232
2054
  /**
2233
- * Hero logic:
2234
- * - If the content contains a [HEROSECTION] shortcode (case-insensitive), do NOT auto-hero.
2235
- * - Else, if the page has featured media, render a hero section using the image as background.
2055
+ * Style mapping utilities
2056
+ * Maps WordPress block attributes to Tailwind CSS classes
2236
2057
  */
2237
- const WPPage = ({ page, registry, className }) => {
2238
- if (!page || !Array.isArray(page.blocks)) {
2239
- if (process.env.NODE_ENV !== 'production') {
2240
- // eslint-disable-next-line no-console
2241
- console.warn('WPPage: invalid page prop');
2242
- }
2243
- return null;
2244
- }
2245
- if (!registry || !registry.renderers) {
2246
- throw new Error('WPPage: registry is required');
2247
- }
2248
- const hasHeroShortcode = React.useMemo(() => detectHeroShortcode(page.blocks), [page.blocks]);
2249
- const featured = getFeaturedImage(page);
2250
- return (jsxRuntimeExports.jsxs("article", { className: className, children: [!hasHeroShortcode && featured && (jsxRuntimeExports.jsx(HeroFromFeatured, { featured: featured, title: page.title?.rendered })), jsxRuntimeExports.jsx(WPContent, { blocks: page.blocks, registry: registry, page: page })] }));
2251
- };
2252
- function detectHeroShortcode(blocks) {
2253
- for (const block of blocks) {
2254
- const html = (block.innerHTML || '').toLowerCase();
2255
- if (html.includes('[herosection]'))
2256
- return true;
2257
- // Check if this is a cover block (which is a hero section)
2258
- if (block.name === 'core/cover')
2259
- return true;
2260
- if (block.innerBlocks?.length && detectHeroShortcode(block.innerBlocks))
2261
- return true;
2262
- }
2263
- return false;
2264
- }
2265
- function getFeaturedImage(page) {
2266
- const fm = page._embedded?.['wp:featuredmedia'];
2267
- if (Array.isArray(fm) && fm.length > 0)
2268
- return fm[0];
2269
- return null;
2270
- }
2271
- const HeroFromFeatured = ({ featured, title }) => {
2272
- const url = featured.source_url;
2273
- if (!url)
2274
- return null;
2275
- return (jsxRuntimeExports.jsx("section", { className: "w-full h-[40vh] min-h-[280px] flex items-end bg-cover bg-center", style: { backgroundImage: `url(${url})` }, "aria-label": featured.alt_text || title || 'Hero', children: jsxRuntimeExports.jsx("div", { className: "container mx-auto px-4 py-8 bg-gradient-to-t from-black/50 to-transparent w-full", children: title && jsxRuntimeExports.jsx("h1", { className: "text-white text-4xl font-bold drop-shadow", children: title }) }) }));
2276
- };
2277
-
2278
- class WPErrorBoundary extends React.Component {
2279
- constructor(props) {
2280
- super(props);
2281
- this.state = { hasError: false };
2282
- }
2283
- static getDerivedStateFromError() {
2284
- return { hasError: true };
2285
- }
2286
- componentDidCatch(error) {
2287
- if (process.env.NODE_ENV !== 'production') {
2288
- // eslint-disable-next-line no-console
2289
- console.error('WPErrorBoundary caught error:', error);
2290
- }
2291
- }
2292
- render() {
2293
- if (this.state.hasError) {
2294
- return this.props.fallback ?? null;
2295
- }
2296
- return this.props.children;
2297
- }
2298
- }
2299
-
2300
2058
  /**
2301
- * Extract background image URL from a block
2302
- * Checks various possible sources: cloudflareUrl, url, backgroundImage, innerHTML, featured image
2059
+ * Map WordPress alignment to Tailwind classes
2303
2060
  */
2304
- function extractBackgroundImage(block, page) {
2305
- const attrs = block.attributes || {};
2306
- // Check if block uses featured image
2307
- if (attrs['useFeaturedImage'] === true && page?._embedded?.['wp:featuredmedia']?.[0]?.source_url) {
2308
- return page._embedded['wp:featuredmedia'][0].source_url;
2061
+ function getAlignmentClasses(align) {
2062
+ if (!align)
2063
+ return '';
2064
+ switch (align) {
2065
+ case 'full':
2066
+ return 'w-full';
2067
+ case 'wide':
2068
+ return 'max-w-7xl mx-auto';
2069
+ case 'center':
2070
+ return 'mx-auto';
2071
+ case 'left':
2072
+ return 'mr-auto';
2073
+ case 'right':
2074
+ return 'ml-auto';
2075
+ default:
2076
+ return '';
2309
2077
  }
2310
- // Use the improved extraction function that handles incomplete cloudflareUrl
2311
- // This will check cloudflareUrl first, then innerHTML, then regular attributes
2312
- return extractImageUrlWithFallback(block);
2313
- }
2314
- /**
2315
- * Extract image URL from a block
2316
- * Returns Cloudflare URL if available, otherwise WordPress URL
2317
- */
2318
- function extractImageUrl(block) {
2319
- // Use the improved extraction function that handles incomplete cloudflareUrl
2320
- return extractImageUrlWithFallback(block);
2321
2078
  }
2322
2079
  /**
2323
- * Extract image attributes (url, alt, width, height)
2080
+ * Map WordPress text alignment to Tailwind classes
2324
2081
  */
2325
- function extractImageAttributes(block) {
2326
- return getImageAttributes(block);
2082
+ function getTextAlignClasses(textAlign) {
2083
+ if (!textAlign)
2084
+ return '';
2085
+ switch (textAlign) {
2086
+ case 'center':
2087
+ return 'text-center';
2088
+ case 'left':
2089
+ return 'text-left';
2090
+ case 'right':
2091
+ return 'text-right';
2092
+ default:
2093
+ return '';
2094
+ }
2327
2095
  }
2328
2096
  /**
2329
- * Extract title/heading text from a block
2097
+ * Map WordPress font size to Tailwind classes
2330
2098
  */
2331
- function extractTitle(block) {
2332
- const attrs = block.attributes || {};
2333
- const title = attrs['title'] || attrs['content'] || getBlockTextContent(block);
2334
- return typeof title === 'string' ? title.trim() : null;
2099
+ function getFontSizeClasses(fontSize) {
2100
+ if (!fontSize)
2101
+ return '';
2102
+ // Map WordPress font sizes to Tailwind
2103
+ const sizeMap = {
2104
+ 'small': 'text-sm',
2105
+ 'medium': 'text-base',
2106
+ 'large': 'text-lg',
2107
+ 'x-large': 'text-xl',
2108
+ 'xx-large': 'text-3xl',
2109
+ 'xxx-large': 'text-4xl',
2110
+ };
2111
+ return sizeMap[fontSize] || '';
2335
2112
  }
2336
2113
  /**
2337
- * Extract content/text from a block
2338
- * Returns React node for rendering
2114
+ * Get container classes based on layout and alignment
2339
2115
  */
2340
- function extractContent(block, context) {
2341
- const text = getBlockTextContent(block);
2342
- return text || null;
2116
+ function getContainerClasses(align, layout) {
2117
+ const alignClass = getAlignmentClasses(align);
2118
+ // If layout is constrained, use container
2119
+ if (layout?.type === 'constrained') {
2120
+ return align === 'full' ? 'w-full' : 'container';
2121
+ }
2122
+ return alignClass || 'container';
2343
2123
  }
2344
2124
  /**
2345
- * Extract media position from media-text block
2125
+ * Get spacing classes for sections
2346
2126
  */
2347
- function extractMediaPosition(block) {
2348
- const attrs = block.attributes || {};
2349
- const position = attrs['mediaPosition'] || 'left';
2350
- return position === 'right' ? 'right' : 'left';
2127
+ function getSectionSpacingClasses() {
2128
+ return 'py-16 md:py-24';
2351
2129
  }
2352
2130
  /**
2353
- * Extract vertical alignment from block
2131
+ * Get content spacing classes
2354
2132
  */
2355
- function extractVerticalAlignment(block) {
2356
- const attrs = block.attributes || {};
2357
- const alignment = attrs['verticalAlignment'] || 'center';
2358
- if (alignment === 'top' || alignment === 'bottom') {
2359
- return alignment;
2360
- }
2361
- return 'center';
2133
+ function getContentSpacingClasses() {
2134
+ return 'space-y-6';
2362
2135
  }
2363
2136
  /**
2364
- * Extract alignment (full, wide, contained) from block
2137
+ * Build className string from multiple class sources
2365
2138
  */
2366
- function extractAlignment(block) {
2367
- const attrs = block.attributes || {};
2368
- const align = attrs['align'];
2369
- if (align === 'full' || align === 'wide') {
2370
- return align;
2371
- }
2372
- return 'contained';
2139
+ function buildClassName(...classes) {
2140
+ return classes
2141
+ .filter((cls) => Boolean(cls && cls.trim()))
2142
+ .join(' ')
2143
+ .trim();
2373
2144
  }
2374
- /**
2375
- * Extract overlay color from cover block
2376
- */
2377
- function extractOverlayColor(block) {
2145
+
2146
+ const Paragraph = ({ block, context }) => {
2147
+ const content = getBlockTextContent(block);
2378
2148
  const attrs = block.attributes || {};
2379
- const overlayColor = attrs['overlayColor'];
2380
- if (typeof overlayColor === 'string') {
2381
- return overlayColor;
2149
+ const textAlign = getTextAlignClasses(attrs['align']);
2150
+ // Check if content contains shortcodes
2151
+ const hasShortcodes = /\[(\w+)/.test(content);
2152
+ if (hasShortcodes && context.registry.shortcodes) {
2153
+ const parts = renderTextWithShortcodes(content, context.registry);
2154
+ // Check if any part is a block-level element (section, div, etc.)
2155
+ // If so, render without wrapping in <p> to avoid DOM nesting violations
2156
+ const isBlockLevelElement = (element) => {
2157
+ if (!React.isValidElement(element)) {
2158
+ return false;
2159
+ }
2160
+ const type = element.type;
2161
+ // Check for block-level HTML elements
2162
+ if (typeof type === 'string' && ['section', 'div', 'article', 'header', 'footer', 'aside', 'nav'].includes(type)) {
2163
+ return true;
2164
+ }
2165
+ // Check if it's React.Fragment - recursively check its children
2166
+ if (type === React.Fragment) {
2167
+ const fragmentProps = element.props;
2168
+ const children = React.Children.toArray(fragmentProps.children);
2169
+ return children.some((child) => isBlockLevelElement(child));
2170
+ }
2171
+ // Check if it's a React component (likely block-level)
2172
+ // Most custom components render block-level content
2173
+ if (typeof type === 'function' || (typeof type === 'object' && type !== null && type !== React.Fragment)) {
2174
+ return true;
2175
+ }
2176
+ return false;
2177
+ };
2178
+ const hasBlockLevelContent = React.Children.toArray(parts).some((part) => isBlockLevelElement(part));
2179
+ if (hasBlockLevelContent) {
2180
+ // Render block-level content without <p> wrapper
2181
+ return jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: parts });
2182
+ }
2183
+ return jsxRuntimeExports.jsx("p", { className: buildClassName('text-gray-700 my-4', textAlign), children: parts });
2382
2184
  }
2383
- return null;
2384
- }
2385
- /**
2386
- * Extract dim ratio (overlay opacity) from cover block
2387
- */
2388
- function extractDimRatio(block) {
2185
+ return jsxRuntimeExports.jsx("p", { className: buildClassName('text-gray-700 my-4', textAlign), children: content });
2186
+ };
2187
+ const Heading = ({ block, children }) => {
2389
2188
  const attrs = block.attributes || {};
2390
- const dimRatio = attrs['dimRatio'];
2391
- if (typeof dimRatio === 'number') {
2392
- return dimRatio;
2189
+ const { level = 2 } = attrs;
2190
+ const content = getBlockTextContent(block);
2191
+ const textAlign = getTextAlignClasses(attrs['textAlign']);
2192
+ const fontSize = getFontSizeClasses(attrs['fontSize']);
2193
+ const Tag = `h${Math.min(Math.max(Number(level) || 2, 1), 6)}`;
2194
+ // Default heading sizes if fontSize not specified
2195
+ const sizeClass = fontSize || (level === 1 ? 'text-4xl' : level === 2 ? 'text-3xl' : level === 3 ? 'text-2xl' : 'text-xl');
2196
+ // Add spacing: mt for top (except first heading), mb for bottom
2197
+ // Use larger bottom margin for headings to create visual separation
2198
+ const spacingClass = level === 1 ? 'mt-8 mb-6' : level === 2 ? 'mt-6 mb-4' : 'mt-4 mb-3';
2199
+ return (jsxRuntimeExports.jsx(Tag, { className: buildClassName('font-bold text-gray-900', sizeClass, textAlign, spacingClass), children: children ?? content }));
2200
+ };
2201
+ const Image = ({ block }) => {
2202
+ const imageAttrs = getImageAttributes(block);
2203
+ if (!imageAttrs.url)
2204
+ return null;
2205
+ // Use Cloudflare variant URL if it's a Cloudflare image
2206
+ let imageUrl = imageAttrs.url;
2207
+ if (isCloudflareImageUrl(imageUrl)) {
2208
+ const width = imageAttrs.width || 1024;
2209
+ const height = imageAttrs.height;
2210
+ imageUrl = getCloudflareVariantUrl(imageUrl, { width, height });
2393
2211
  }
2394
- return 0;
2395
- }
2396
- /**
2397
- * Extract min height from block
2398
- */
2399
- function extractMinHeight(block) {
2212
+ return (jsxRuntimeExports.jsx("img", { src: imageUrl, alt: imageAttrs.alt, width: imageAttrs.width, height: imageAttrs.height, className: "w-full h-auto rounded-lg object-cover my-4", style: { maxWidth: '100%', height: 'auto' }, loading: "lazy" }));
2213
+ };
2214
+ const List = ({ block, children }) => {
2400
2215
  const attrs = block.attributes || {};
2401
- const minHeight = attrs['minHeight'];
2402
- const minHeightUnit = attrs['minHeightUnit'] || 'vh';
2403
- if (typeof minHeight === 'number') {
2404
- return { value: minHeight, unit: minHeightUnit };
2405
- }
2406
- return null;
2407
- }
2408
- /**
2409
- * Extract heading level from heading block
2410
- */
2411
- function extractHeadingLevel(block) {
2216
+ const { ordered } = attrs;
2217
+ const Tag = ordered ? 'ol' : 'ul';
2218
+ return React.createElement(Tag, { className: 'list-disc pl-6 space-y-2 text-gray-700 my-4' }, children);
2219
+ };
2220
+ const ListItem = ({ children }) => {
2221
+ return jsxRuntimeExports.jsx("li", { className: "text-gray-700", children: children });
2222
+ };
2223
+ const Group = ({ block, children, context }) => {
2412
2224
  const attrs = block.attributes || {};
2413
- const level = attrs['level'];
2414
- if (typeof level === 'number' && level >= 1 && level <= 6) {
2415
- return level;
2416
- }
2417
- return 2; // Default to h2
2418
- }
2419
- /**
2420
- * Extract text alignment from block
2421
- */
2422
- function extractTextAlign(block) {
2225
+ const align = attrs['align'];
2226
+ // Layout can be an object with type property, or nested structure
2227
+ const layout = attrs['layout'];
2228
+ // Extract background color using color mapper
2229
+ const backgroundColor = extractBackgroundColor(block, context);
2230
+ // Determine if this is a section-level group (has alignment) or content-level
2231
+ const isSection = align === 'full' || align === 'wide';
2232
+ const containerClass = getContainerClasses(align, layout);
2233
+ const spacingClass = isSection ? getSectionSpacingClasses() : getContentSpacingClasses();
2234
+ // Ensure container class is always applied for constrained groups
2235
+ const finalContainerClass = layout?.type === 'constrained' && align === 'wide'
2236
+ ? 'container'
2237
+ : containerClass;
2238
+ // Build className with background color if present
2239
+ const className = buildClassName(finalContainerClass, spacingClass, backgroundColor // This will be null if no mapping, which is fine
2240
+ );
2241
+ return (jsxRuntimeExports.jsx("div", { className: className, children: children }));
2242
+ };
2243
+ const Columns = ({ block, children }) => {
2423
2244
  const attrs = block.attributes || {};
2424
- const align = attrs['align'] || attrs['textAlign'];
2425
- if (align === 'left' || align === 'center' || align === 'right') {
2426
- return align;
2427
- }
2428
- return null;
2429
- }
2430
- /**
2431
- * Extract font size from block
2432
- */
2433
- function extractFontSize(block) {
2245
+ const align = attrs['align'];
2246
+ const alignClass = getAlignmentClasses(align);
2247
+ return (jsxRuntimeExports.jsx("div", { className: buildClassName('grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-12', alignClass), children: children }));
2248
+ };
2249
+ const Column = ({ block, children }) => {
2434
2250
  const attrs = block.attributes || {};
2435
- const fontSize = attrs['fontSize'];
2436
- return typeof fontSize === 'string' ? fontSize : null;
2437
- }
2438
- /**
2439
- * Convert image URL to Cloudflare variant if it's a Cloudflare URL
2440
- */
2441
- function convertImageToCloudflareVariant(url, options = {}) {
2442
- if (!url)
2251
+ const width = attrs['width'];
2252
+ // Handle column width (e.g., "50%" becomes flex-basis)
2253
+ const style = width ? { flexBasis: width } : undefined;
2254
+ return (jsxRuntimeExports.jsx("div", { className: "space-y-4", style: style, children: children }));
2255
+ };
2256
+ const Separator = () => jsxRuntimeExports.jsx("hr", { className: "border-gray-200 my-8" });
2257
+ const ButtonBlock = ({ block }) => {
2258
+ const attrs = block.attributes || {};
2259
+ let url = attrs['url'];
2260
+ let text = attrs['text'];
2261
+ attrs['linkDestination'];
2262
+ // Extract from innerHTML if not in attributes (buttons often store data in innerHTML)
2263
+ if (!url && block.innerHTML) {
2264
+ const linkMatch = block.innerHTML.match(/<a[^>]+href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/i);
2265
+ if (linkMatch) {
2266
+ url = linkMatch[1];
2267
+ text = linkMatch[2] || text;
2268
+ }
2269
+ }
2270
+ // Get text from block content if still missing
2271
+ if (!text) {
2272
+ text = getBlockTextContent(block);
2273
+ }
2274
+ if (!url && !text)
2443
2275
  return null;
2444
- if (isCloudflareImageUrl(url)) {
2445
- const width = options.width || 1024;
2446
- const height = options.height;
2447
- return getCloudflareVariantUrl(url, { width, height });
2276
+ const buttonText = text || 'Learn more';
2277
+ // Handle internal vs external links
2278
+ const isExternal = url && (url.startsWith('http://') || url.startsWith('https://'));
2279
+ const linkProps = isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {};
2280
+ return (jsxRuntimeExports.jsx("a", { href: url || '#', className: "inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-primary-foreground font-medium hover:bg-primary/90 transition-colors", ...linkProps, children: buttonText }));
2281
+ };
2282
+ const Cover = ({ block, children }) => {
2283
+ const attrs = block.attributes || {};
2284
+ const { url, id, backgroundImage, cloudflareUrl, overlayColor, dimRatio = 0, align = 'full', minHeight, minHeightUnit = 'vh', hasParallax, } = attrs;
2285
+ // Get background image URL from various possible sources
2286
+ // Use the improved extraction function that handles incomplete cloudflareUrl
2287
+ let bgImageUrl = null;
2288
+ // First, try cloudflareUrl if it's valid
2289
+ if (cloudflareUrl && isValidCloudflareUrl(cloudflareUrl)) {
2290
+ bgImageUrl = cloudflareUrl;
2448
2291
  }
2449
- return url;
2450
- }
2451
- /**
2452
- * Extract title from innerBlocks (finds first heading block)
2453
- */
2454
- function extractTitleFromInnerBlocks(block) {
2455
- const innerBlocks = block.innerBlocks || [];
2456
- // Recursively search for heading blocks
2457
- for (const innerBlock of innerBlocks) {
2458
- if (innerBlock.name === 'core/heading') {
2459
- return getBlockTextContent(innerBlock);
2292
+ // If not valid or not found, try regular attributes
2293
+ if (!bgImageUrl) {
2294
+ bgImageUrl = url || backgroundImage || (typeof backgroundImage === 'object' && backgroundImage?.url) || null;
2295
+ }
2296
+ // If still not found, use the fallback extraction (from innerHTML)
2297
+ if (!bgImageUrl) {
2298
+ bgImageUrl = extractImageUrlWithFallback(block);
2299
+ }
2300
+ // Convert to Cloudflare URL variant if it's a Cloudflare image
2301
+ if (bgImageUrl) {
2302
+ if (isCloudflareImageUrl(bgImageUrl)) {
2303
+ // Use full width for cover images
2304
+ bgImageUrl = getCloudflareVariantUrl(bgImageUrl, { width: 1920 });
2460
2305
  }
2461
- // Recursively search nested blocks
2462
- const nestedTitle = extractTitleFromInnerBlocks(innerBlock);
2463
- if (nestedTitle)
2464
- return nestedTitle;
2465
2306
  }
2466
- return null;
2467
- }
2468
- /**
2469
- * Extract subtitle/description from innerBlocks (finds first paragraph block)
2470
- */
2471
- function extractSubtitleFromInnerBlocks(block) {
2472
- const innerBlocks = block.innerBlocks || [];
2473
- // Recursively search for paragraph blocks
2474
- for (const innerBlock of innerBlocks) {
2475
- if (innerBlock.name === 'core/paragraph') {
2476
- const text = getBlockTextContent(innerBlock);
2477
- if (text && text.trim()) {
2478
- return text;
2479
- }
2307
+ // Build alignment classes
2308
+ const alignClass = getAlignmentClasses(align);
2309
+ // Build style object
2310
+ const style = {};
2311
+ if (minHeight) {
2312
+ const minHeightValue = typeof minHeight === 'number' ? minHeight : parseFloat(String(minHeight));
2313
+ style.minHeight = minHeightUnit === 'vh' ? `${minHeightValue}vh` : `${minHeightValue}px`;
2314
+ }
2315
+ if (bgImageUrl) {
2316
+ style.backgroundImage = `url(${bgImageUrl})`;
2317
+ style.backgroundSize = 'cover';
2318
+ style.backgroundPosition = 'center';
2319
+ if (hasParallax) {
2320
+ style.backgroundAttachment = 'fixed';
2480
2321
  }
2481
- // Recursively search nested blocks
2482
- const nestedSubtitle = extractSubtitleFromInnerBlocks(innerBlock);
2483
- if (nestedSubtitle)
2484
- return nestedSubtitle;
2485
2322
  }
2486
- return null;
2487
- }
2488
- /**
2489
- * Extract buttons from innerBlocks (finds buttons block and extracts button data)
2490
- */
2491
- function extractButtonsFromInnerBlocks(block) {
2492
- const buttons = [];
2323
+ // Calculate overlay opacity
2324
+ const overlayOpacity = typeof dimRatio === 'number' ? dimRatio / 100 : 0;
2325
+ return (jsxRuntimeExports.jsxs("div", { className: buildClassName('relative w-full', alignClass), style: style, children: [overlayOpacity > 0 && (jsxRuntimeExports.jsx("span", { className: "absolute inset-0 z-0", style: {
2326
+ backgroundColor: overlayColor === 'contrast' ? '#000000' : (overlayColor || '#000000'),
2327
+ opacity: overlayOpacity,
2328
+ }, "aria-hidden": "true" })), jsxRuntimeExports.jsx("div", { className: buildClassName('relative z-10', align === 'full' ? 'w-full' : 'container mx-auto px-4'), children: children })] }));
2329
+ };
2330
+ const MediaText = ({ block, children, context }) => {
2331
+ const attrs = block.attributes || {};
2332
+ const { mediaPosition = 'left', verticalAlignment = 'center', imageFill = false, align, } = attrs;
2333
+ // Access innerBlocks to identify media vs content
2493
2334
  const innerBlocks = block.innerBlocks || [];
2494
- // Find buttons block
2495
- const findButtonsBlock = (blocks) => {
2496
- for (const innerBlock of blocks) {
2497
- if (innerBlock.name === 'core/buttons') {
2498
- return innerBlock;
2499
- }
2500
- if (innerBlock.innerBlocks) {
2501
- const found = findButtonsBlock(innerBlock.innerBlocks);
2502
- if (found)
2503
- return found;
2504
- }
2335
+ // Find media block (image or video)
2336
+ let mediaBlockIndex = innerBlocks.findIndex((b) => b.name === 'core/image' || b.name === 'core/video');
2337
+ // Render children - media-text typically has media as first child, then content
2338
+ const childrenArray = React.Children.toArray(children);
2339
+ let mediaElement = mediaBlockIndex >= 0 && childrenArray[mediaBlockIndex]
2340
+ ? childrenArray[mediaBlockIndex]
2341
+ : null;
2342
+ // If no media element from innerBlocks, try to extract image URL
2343
+ if (!mediaElement) {
2344
+ const imageUrl = extractImageUrlWithFallback(block);
2345
+ if (imageUrl) {
2346
+ // Convert to Cloudflare variant if it's a Cloudflare URL
2347
+ // extractImageUrlWithFallback already handles incomplete cloudflareUrl by falling back to innerHTML
2348
+ const finalImageUrl = isCloudflareImageUrl(imageUrl)
2349
+ ? getCloudflareVariantUrl(imageUrl, { width: 1024 })
2350
+ : imageUrl;
2351
+ mediaElement = (jsxRuntimeExports.jsx("img", { src: finalImageUrl, alt: "", className: "w-full h-auto rounded-lg shadow-lg object-cover", style: { maxWidth: '100%', height: 'auto' }, loading: "lazy" }));
2505
2352
  }
2506
- return null;
2507
- };
2508
- const buttonsBlock = findButtonsBlock(innerBlocks);
2509
- if (!buttonsBlock || !buttonsBlock.innerBlocks) {
2510
- return buttons;
2511
2353
  }
2512
- // Extract button data from button blocks
2513
- for (const buttonBlock of buttonsBlock.innerBlocks) {
2514
- if (buttonBlock.name === 'core/button') {
2515
- const attrs = buttonBlock.attributes || {};
2516
- const url = attrs['url'];
2517
- const text = attrs['text'] || getBlockTextContent(buttonBlock);
2518
- // Try to extract from innerHTML if not in attributes
2519
- if (!url && buttonBlock.innerHTML) {
2520
- const linkMatch = buttonBlock.innerHTML.match(/<a[^>]+href=["']([^"']+)["'][^>]*>([^<]+)<\/a>/i);
2521
- if (linkMatch) {
2522
- const extractedUrl = linkMatch[1];
2523
- const extractedText = linkMatch[2] || text;
2524
- if (extractedUrl) {
2525
- buttons.push({
2526
- text: extractedText || 'Learn More',
2527
- url: extractedUrl,
2528
- isExternal: extractedUrl.startsWith('http://') || extractedUrl.startsWith('https://'),
2529
- });
2530
- }
2531
- }
2354
+ // Content is all other children
2355
+ const contentElements = childrenArray.filter((_, index) => index !== mediaBlockIndex);
2356
+ // Build alignment classes - ensure proper container width
2357
+ // For 'wide', media-text blocks are typically inside constrained groups (which use 'container' class)
2358
+ // So we should use 'w-full' to fill the parent container, not apply another max-width
2359
+ // Only use 'max-w-7xl' for truly standalone wide blocks (rare case)
2360
+ let alignClass;
2361
+ let spacingClass;
2362
+ if (align === 'full') {
2363
+ alignClass = 'w-full';
2364
+ // Full-width blocks are typically top-level sections, so add section spacing
2365
+ spacingClass = getSectionSpacingClasses();
2366
+ }
2367
+ else if (align === 'wide') {
2368
+ // Wide blocks are usually inside constrained groups (which already have container and spacing)
2369
+ // So just fill the parent container without adding section spacing
2370
+ alignClass = 'w-full';
2371
+ spacingClass = ''; // No section spacing - parent group handles it
2372
+ }
2373
+ else {
2374
+ // Default to contained width (not full width)
2375
+ alignClass = 'container mx-auto';
2376
+ // Contained blocks might be standalone, so add section spacing
2377
+ spacingClass = getSectionSpacingClasses();
2378
+ }
2379
+ // Vertical alignment classes
2380
+ const verticalAlignClass = verticalAlignment === 'top' ? 'items-start' :
2381
+ verticalAlignment === 'bottom' ? 'items-end' :
2382
+ 'items-center';
2383
+ // Stack on mobile
2384
+ const stackClass = 'flex-col md:flex-row';
2385
+ // Media position determines order
2386
+ const isMediaRight = mediaPosition === 'right';
2387
+ return (jsxRuntimeExports.jsx("div", { className: buildClassName(alignClass, spacingClass), children: jsxRuntimeExports.jsxs("div", { className: buildClassName('flex', stackClass, verticalAlignClass, 'gap-6 lg:gap-12', 'w-full'), children: [jsxRuntimeExports.jsx("div", { className: buildClassName(isMediaRight ? 'order-2' : 'order-1', imageFill ? 'w-full md:w-1/2' : 'w-full md:w-1/2', 'min-w-0', // Allow flex item to shrink below content size
2388
+ 'overflow-hidden' // Ensure images don't overflow
2389
+ ), children: mediaElement || jsxRuntimeExports.jsx("div", { className: "bg-gray-200 h-64 rounded-lg" }) }), jsxRuntimeExports.jsx("div", { className: buildClassName(isMediaRight ? 'order-1' : 'order-2', 'w-full md:w-1/2', // Explicit width to ensure proper sizing
2390
+ 'min-w-0', // Allow flex item to shrink below content size
2391
+ getContentSpacingClasses()), children: contentElements.length > 0 ? contentElements : children })] }) }));
2392
+ };
2393
+ const Fallback = ({ block, children }) => {
2394
+ // Minimal fallback; do not render innerHTML directly in v1 for safety
2395
+ return jsxRuntimeExports.jsx("div", { "data-unknown-block": block.name, children: children });
2396
+ };
2397
+ function createDefaultRegistry(colorMapper) {
2398
+ const renderers = {
2399
+ 'core/paragraph': Paragraph,
2400
+ 'core/heading': Heading,
2401
+ 'core/image': Image,
2402
+ 'core/list': List,
2403
+ 'core/list-item': ListItem,
2404
+ 'core/group': Group,
2405
+ 'core/columns': Columns,
2406
+ 'core/column': Column,
2407
+ 'core/separator': Separator,
2408
+ 'core/button': ButtonBlock,
2409
+ 'core/buttons': ({ block, children }) => {
2410
+ const attrs = block.attributes || {};
2411
+ const layout = attrs['layout'];
2412
+ const justifyContent = layout?.justifyContent || 'left';
2413
+ const justifyClass = justifyContent === 'center' ? 'justify-center' :
2414
+ justifyContent === 'right' ? 'justify-end' :
2415
+ 'justify-start';
2416
+ return jsxRuntimeExports.jsx("div", { className: buildClassName('flex flex-wrap gap-3', justifyClass), children: children });
2417
+ },
2418
+ 'core/quote': ({ children }) => jsxRuntimeExports.jsx("blockquote", { className: "border-l-4 pl-4 italic", children: children }),
2419
+ 'core/code': ({ block }) => (jsxRuntimeExports.jsx("pre", { className: "bg-gray-100 p-3 rounded text-sm overflow-auto", children: jsxRuntimeExports.jsx("code", { children: getString(block) }) })),
2420
+ 'core/preformatted': ({ block }) => jsxRuntimeExports.jsx("pre", { children: getString(block) }),
2421
+ 'core/table': ({ children }) => jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: jsxRuntimeExports.jsx("table", { className: "table-auto w-full", children: children }) }),
2422
+ 'core/table-row': ({ children }) => jsxRuntimeExports.jsx("tr", { children: children }),
2423
+ 'core/table-cell': ({ children }) => jsxRuntimeExports.jsx("td", { className: "border px-3 py-2", children: children }),
2424
+ // Cover block - hero sections with background images
2425
+ 'core/cover': Cover,
2426
+ // Media & Text block - side-by-side media and content
2427
+ 'core/media-text': MediaText,
2428
+ // HTML block - render innerHTML as-is
2429
+ // Note: Shortcodes in HTML blocks are not parsed (they would need to be in text content)
2430
+ 'core/html': ({ block }) => {
2431
+ const html = block.innerHTML || '';
2432
+ return jsxRuntimeExports.jsx("div", { dangerouslySetInnerHTML: { __html: html } });
2433
+ },
2434
+ };
2435
+ return {
2436
+ renderers,
2437
+ shortcodes: {}, // Empty by default - apps extend this
2438
+ fallback: Fallback,
2439
+ colorMapper,
2440
+ };
2441
+ }
2442
+ // Legacy function for backward compatibility - use getBlockTextContent instead
2443
+ function getString(block) {
2444
+ return getBlockTextContent(block);
2445
+ }
2446
+
2447
+ /**
2448
+ * Check if a block matches a pattern
2449
+ */
2450
+ function matchesPattern(block, pattern) {
2451
+ // Check block name
2452
+ if (block.name !== pattern.name) {
2453
+ return false;
2454
+ }
2455
+ // Check attributes if specified
2456
+ if (pattern.attributes) {
2457
+ const blockAttrs = block.attributes || {};
2458
+ for (const [key, value] of Object.entries(pattern.attributes)) {
2459
+ if (blockAttrs[key] !== value) {
2460
+ return false;
2532
2461
  }
2533
- else if (url && text) {
2534
- buttons.push({
2535
- text,
2536
- url,
2537
- isExternal: url.startsWith('http://') || url.startsWith('https://'),
2538
- });
2462
+ }
2463
+ }
2464
+ // Check innerBlocks patterns if specified
2465
+ if (pattern.innerBlocks && pattern.innerBlocks.length > 0) {
2466
+ const blockInnerBlocks = block.innerBlocks || [];
2467
+ // If pattern specifies innerBlocks, check if block has matching innerBlocks
2468
+ for (const innerPattern of pattern.innerBlocks) {
2469
+ // Find at least one matching innerBlock
2470
+ const hasMatch = blockInnerBlocks.some(innerBlock => matchesPattern(innerBlock, innerPattern));
2471
+ if (!hasMatch) {
2472
+ return false;
2539
2473
  }
2540
2474
  }
2541
2475
  }
2542
- return buttons;
2476
+ return true;
2543
2477
  }
2544
2478
  /**
2545
- * Extract text alignment from inner blocks
2546
- * Recursively searches for heading or paragraph blocks with textAlign attribute
2547
- * Also checks group blocks for justifyContent in layout
2548
- * Priority: heading/paragraph textAlign takes precedence over group justifyContent
2479
+ * Find the best matching component mapping for a block
2480
+ * Returns the mapping with highest priority that matches, or null
2549
2481
  */
2550
- function extractTextAlignFromInnerBlocks(block) {
2551
- const innerBlocks = block.innerBlocks || [];
2552
- // First, recursively search for heading or paragraph blocks with textAlign
2553
- // (These take priority over group justifyContent)
2554
- for (const innerBlock of innerBlocks) {
2555
- if (innerBlock.name === 'core/heading' || innerBlock.name === 'core/paragraph') {
2556
- const attrs = innerBlock.attributes || {};
2557
- const textAlign = attrs['textAlign'];
2558
- if (textAlign === 'left' || textAlign === 'center' || textAlign === 'right') {
2559
- return textAlign;
2560
- }
2482
+ function findMatchingMapping(block, mappings) {
2483
+ // Sort by priority (higher first), then by order in array
2484
+ const sortedMappings = [...mappings].sort((a, b) => {
2485
+ const priorityA = a.priority ?? 0;
2486
+ const priorityB = b.priority ?? 0;
2487
+ if (priorityA !== priorityB) {
2488
+ return priorityB - priorityA; // Higher priority first
2561
2489
  }
2562
- // Recursively search nested blocks for headings/paragraphs first
2563
- const nestedAlign = extractTextAlignFromInnerBlocks(innerBlock);
2564
- if (nestedAlign)
2565
- return nestedAlign;
2566
- }
2567
- // Only check group blocks if no heading/paragraph alignment found
2568
- for (const innerBlock of innerBlocks) {
2569
- if (innerBlock.name === 'core/group') {
2570
- const attrs = innerBlock.attributes || {};
2571
- const layout = attrs['layout'];
2572
- const justifyContent = layout?.justifyContent;
2573
- if (justifyContent === 'left')
2574
- return 'left';
2575
- if (justifyContent === 'center')
2576
- return 'center';
2577
- if (justifyContent === 'right')
2578
- return 'right';
2490
+ return 0; // Keep original order for same priority
2491
+ });
2492
+ // Find first matching mapping
2493
+ for (const mapping of sortedMappings) {
2494
+ if (matchesPattern(block, mapping.pattern)) {
2495
+ return mapping;
2579
2496
  }
2580
2497
  }
2581
2498
  return null;
2582
2499
  }
2500
+
2583
2501
  /**
2584
- * Parse contentPosition string into horizontal and vertical alignment
2585
- * Format: "horizontal vertical" (e.g., "center center", "left top", "right bottom")
2502
+ * Create an enhanced registry that supports pattern-based component mapping
2503
+ *
2504
+ * This combines the default registry (for fallback) with app-specific component mappings.
2505
+ * When a block matches a pattern, it uses the mapped component. Otherwise, it falls back
2506
+ * to the default renderer.
2507
+ *
2508
+ * @param mappings - Array of component mappings with patterns
2509
+ * @param baseRegistry - Optional base registry (defaults to createDefaultRegistry())
2510
+ * @returns Enhanced registry with pattern matching capabilities
2511
+ *
2512
+ * @example
2513
+ * ```ts
2514
+ * const mappings: ComponentMapping[] = [
2515
+ * {
2516
+ * pattern: { name: 'core/cover' },
2517
+ * component: HomeHeroSection,
2518
+ * extractProps: (block) => ({
2519
+ * backgroundImage: extractBackgroundImage(block),
2520
+ * title: extractTitle(block),
2521
+ * }),
2522
+ * wrapper: SectionWrapper,
2523
+ * },
2524
+ * ];
2525
+ *
2526
+ * const registry = createEnhancedRegistry(mappings);
2527
+ * ```
2586
2528
  */
2587
- function parseContentPosition(contentPosition) {
2588
- if (!contentPosition) {
2589
- return { horizontal: 'left', vertical: 'center' };
2529
+ function createEnhancedRegistry(mappings = [], baseRegistry, colorMapper) {
2530
+ const base = baseRegistry || createDefaultRegistry(colorMapper);
2531
+ // Create enhanced renderers that check patterns first
2532
+ const enhancedRenderers = {
2533
+ ...base.renderers,
2534
+ };
2535
+ // Override renderers for blocks that have mappings
2536
+ // We need to check patterns at render time, so we create a wrapper renderer
2537
+ const createPatternRenderer = (blockName) => {
2538
+ return (props) => {
2539
+ const { block, context } = props;
2540
+ // Find matching mapping
2541
+ const mapping = findMatchingMapping(block, mappings);
2542
+ if (mapping) {
2543
+ // Extract props from block
2544
+ const componentProps = mapping.extractProps(block, context);
2545
+ // Render component
2546
+ const Component = mapping.component;
2547
+ const content = jsxRuntimeExports.jsx(Component, { ...componentProps });
2548
+ // Wrap with wrapper if provided
2549
+ if (mapping.wrapper) {
2550
+ const Wrapper = mapping.wrapper;
2551
+ return jsxRuntimeExports.jsx(Wrapper, { block: block, children: content });
2552
+ }
2553
+ return content;
2554
+ }
2555
+ // Fall back to default renderer
2556
+ const defaultRenderer = base.renderers[blockName] || base.fallback;
2557
+ return defaultRenderer(props);
2558
+ };
2559
+ };
2560
+ // For each mapping, override the renderer for that block name
2561
+ for (const mapping of mappings) {
2562
+ const blockName = mapping.pattern.name;
2563
+ if (blockName) {
2564
+ enhancedRenderers[blockName] = createPatternRenderer(blockName);
2565
+ }
2590
2566
  }
2591
- const parts = contentPosition.trim().split(/\s+/);
2592
- const horizontal = parts[0] || 'left';
2593
- const vertical = parts[1] || 'center';
2567
+ // Create matchBlock function
2568
+ const matchBlock = (block) => {
2569
+ return findMatchingMapping(block, mappings);
2570
+ };
2594
2571
  return {
2595
- horizontal: (horizontal === 'center' || horizontal === 'right' ? horizontal : 'left'),
2596
- vertical: (vertical === 'top' || vertical === 'bottom' ? vertical : 'center'),
2572
+ ...base,
2573
+ renderers: enhancedRenderers,
2574
+ mappings,
2575
+ matchBlock,
2576
+ // Use provided colorMapper or inherit from base registry
2577
+ colorMapper: colorMapper || base.colorMapper,
2597
2578
  };
2598
2579
  }
2580
+
2581
+ const WPContent = ({ blocks, registry, className, page }) => {
2582
+ if (!Array.isArray(blocks)) {
2583
+ if (process.env.NODE_ENV !== 'production') {
2584
+ // eslint-disable-next-line no-console
2585
+ console.warn('WPContent: invalid blocks prop');
2586
+ }
2587
+ return null;
2588
+ }
2589
+ if (!registry || !registry.renderers) {
2590
+ throw new Error('WPContent: registry is required');
2591
+ }
2592
+ const ast = parseGutenbergBlocks(blocks);
2593
+ return (jsxRuntimeExports.jsx("div", { className: className, children: renderNodes(ast, registry, undefined, page) }));
2594
+ };
2595
+
2599
2596
  /**
2600
- * Extract video iframe HTML from innerBlocks (finds HTML block with iframe)
2597
+ * Hero logic:
2598
+ * - If the content contains a [HEROSECTION] shortcode (case-insensitive), do NOT auto-hero.
2599
+ * - Else, if the page has featured media, render a hero section using the image as background.
2601
2600
  */
2602
- function extractVideoIframeFromInnerBlocks(block) {
2603
- const innerBlocks = block.innerBlocks || [];
2604
- // Recursively search for HTML blocks with iframe
2605
- for (const innerBlock of innerBlocks) {
2606
- if (innerBlock.name === 'core/html' && innerBlock.innerHTML) {
2607
- // Check if innerHTML contains an iframe
2608
- if (innerBlock.innerHTML.includes('<iframe')) {
2609
- return innerBlock.innerHTML;
2610
- }
2611
- }
2612
- // Recursively search nested blocks
2613
- if (innerBlock.innerBlocks) {
2614
- const nestedVideo = extractVideoIframeFromInnerBlocks(innerBlock);
2615
- if (nestedVideo)
2616
- return nestedVideo;
2601
+ const WPPage = ({ page, registry, className }) => {
2602
+ if (!page || !Array.isArray(page.blocks)) {
2603
+ if (process.env.NODE_ENV !== 'production') {
2604
+ // eslint-disable-next-line no-console
2605
+ console.warn('WPPage: invalid page prop');
2617
2606
  }
2607
+ return null;
2608
+ }
2609
+ if (!registry || !registry.renderers) {
2610
+ throw new Error('WPPage: registry is required');
2611
+ }
2612
+ const hasHeroShortcode = React.useMemo(() => detectHeroShortcode(page.blocks), [page.blocks]);
2613
+ const featured = getFeaturedImage(page);
2614
+ return (jsxRuntimeExports.jsxs("article", { className: className, children: [!hasHeroShortcode && featured && (jsxRuntimeExports.jsx(HeroFromFeatured, { featured: featured, title: page.title?.rendered })), jsxRuntimeExports.jsx(WPContent, { blocks: page.blocks, registry: registry, page: page })] }));
2615
+ };
2616
+ function detectHeroShortcode(blocks) {
2617
+ for (const block of blocks) {
2618
+ const html = (block.innerHTML || '').toLowerCase();
2619
+ if (html.includes('[herosection]'))
2620
+ return true;
2621
+ // Check if this is a cover block (which is a hero section)
2622
+ if (block.name === 'core/cover')
2623
+ return true;
2624
+ if (block.innerBlocks?.length && detectHeroShortcode(block.innerBlocks))
2625
+ return true;
2618
2626
  }
2627
+ return false;
2628
+ }
2629
+ function getFeaturedImage(page) {
2630
+ const fm = page._embedded?.['wp:featuredmedia'];
2631
+ if (Array.isArray(fm) && fm.length > 0)
2632
+ return fm[0];
2619
2633
  return null;
2620
2634
  }
2635
+ const HeroFromFeatured = ({ featured, title }) => {
2636
+ const url = featured.source_url;
2637
+ if (!url)
2638
+ return null;
2639
+ return (jsxRuntimeExports.jsx("section", { className: "w-full h-[40vh] min-h-[280px] flex items-end bg-cover bg-center", style: { backgroundImage: `url(${url})` }, "aria-label": featured.alt_text || title || 'Hero', children: jsxRuntimeExports.jsx("div", { className: "container mx-auto px-4 py-8 bg-gradient-to-t from-black/50 to-transparent w-full", children: title && jsxRuntimeExports.jsx("h1", { className: "text-white text-4xl font-bold drop-shadow", children: title }) }) }));
2640
+ };
2641
+
2642
+ class WPErrorBoundary extends React.Component {
2643
+ constructor(props) {
2644
+ super(props);
2645
+ this.state = { hasError: false };
2646
+ }
2647
+ static getDerivedStateFromError() {
2648
+ return { hasError: true };
2649
+ }
2650
+ componentDidCatch(error) {
2651
+ if (process.env.NODE_ENV !== 'production') {
2652
+ // eslint-disable-next-line no-console
2653
+ console.error('WPErrorBoundary caught error:', error);
2654
+ }
2655
+ }
2656
+ render() {
2657
+ if (this.state.hasError) {
2658
+ return this.props.fallback ?? null;
2659
+ }
2660
+ return this.props.children;
2661
+ }
2662
+ }
2621
2663
 
2622
2664
  /**
2623
2665
  * Convert image URL with optional Cloudflare variant transformation
@@ -2712,6 +2754,7 @@ exports.convertImageUrls = convertImageUrls;
2712
2754
  exports.createDefaultRegistry = createDefaultRegistry;
2713
2755
  exports.createEnhancedRegistry = createEnhancedRegistry;
2714
2756
  exports.extractAlignment = extractAlignment;
2757
+ exports.extractBackgroundColor = extractBackgroundColor;
2715
2758
  exports.extractBackgroundImage = extractBackgroundImage;
2716
2759
  exports.extractButtonsFromInnerBlocks = extractButtonsFromInnerBlocks;
2717
2760
  exports.extractContent = extractContent;