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