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