@marvalt/wparser 0.1.3 → 0.1.5

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
@@ -1504,51 +1504,354 @@ function renderTextWithShortcodes(text, registry) {
1504
1504
  return parts;
1505
1505
  }
1506
1506
 
1507
+ /**
1508
+ * Content extraction utilities for WordPress blocks
1509
+ * Extracts text content from various block formats
1510
+ */
1511
+ /**
1512
+ * Extract text content from a block's innerHTML by stripping HTML tags
1513
+ */
1514
+ function extractTextFromHTML(html) {
1515
+ if (!html)
1516
+ return '';
1517
+ // Remove HTML tags and decode entities
1518
+ const text = html
1519
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
1520
+ .replace(/&nbsp;/g, ' ') // Replace &nbsp; with space
1521
+ .replace(/&#8217;/g, "'") // Replace apostrophe entity
1522
+ .replace(/&#8220;/g, '"') // Replace left double quote
1523
+ .replace(/&#8221;/g, '"') // Replace right double quote
1524
+ .replace(/&#8230;/g, '...') // Replace ellipsis
1525
+ .replace(/&amp;/g, '&') // Replace &amp;
1526
+ .replace(/&lt;/g, '<') // Replace &lt;
1527
+ .replace(/&gt;/g, '>') // Replace &gt;
1528
+ .replace(/&quot;/g, '"') // Replace &quot;
1529
+ .trim();
1530
+ return text;
1531
+ }
1532
+ /**
1533
+ * Extract text content from block attributes or innerHTML
1534
+ */
1535
+ function getBlockTextContent(block) {
1536
+ const attrs = block.attributes || {};
1537
+ // Try various attribute keys
1538
+ const content = attrs['content'] || attrs['text'] || attrs['value'] || '';
1539
+ if (typeof content === 'string' && content.trim()) {
1540
+ return content.trim();
1541
+ }
1542
+ // Fall back to innerHTML
1543
+ if (block.innerHTML) {
1544
+ return extractTextFromHTML(block.innerHTML);
1545
+ }
1546
+ return '';
1547
+ }
1548
+ /**
1549
+ * Extract image URL from block attributes
1550
+ * Checks for Cloudflare URLs first, then falls back to regular URLs
1551
+ */
1552
+ function getImageUrl(block) {
1553
+ const attrs = block.attributes || {};
1554
+ // Check various possible URL attributes
1555
+ const url = attrs['url'] ||
1556
+ attrs['src'] ||
1557
+ attrs['imageUrl'] ||
1558
+ attrs['mediaUrl'] ||
1559
+ attrs['backgroundImage'];
1560
+ if (typeof url === 'string' && url.trim()) {
1561
+ return url.trim();
1562
+ }
1563
+ // Try to extract from innerHTML if it's an img tag
1564
+ if (block.innerHTML) {
1565
+ const imgMatch = block.innerHTML.match(/src=["']([^"']+)["']/);
1566
+ if (imgMatch && imgMatch[1]) {
1567
+ return imgMatch[1];
1568
+ }
1569
+ }
1570
+ return null;
1571
+ }
1572
+ /**
1573
+ * Extract image attributes (alt, width, height) from block
1574
+ */
1575
+ function getImageAttributes(block) {
1576
+ const attrs = block.attributes || {};
1577
+ const url = getImageUrl(block);
1578
+ return {
1579
+ url,
1580
+ alt: attrs['alt'] || '',
1581
+ width: attrs['width'] ? Number(attrs['width']) : undefined,
1582
+ height: attrs['height'] ? Number(attrs['height']) : undefined,
1583
+ };
1584
+ }
1585
+
1586
+ /**
1587
+ * Style mapping utilities
1588
+ * Maps WordPress block attributes to Tailwind CSS classes
1589
+ */
1590
+ /**
1591
+ * Map WordPress alignment to Tailwind classes
1592
+ */
1593
+ function getAlignmentClasses(align) {
1594
+ if (!align)
1595
+ return '';
1596
+ switch (align) {
1597
+ case 'full':
1598
+ return 'w-full';
1599
+ case 'wide':
1600
+ return 'max-w-7xl mx-auto';
1601
+ case 'center':
1602
+ return 'mx-auto';
1603
+ case 'left':
1604
+ return 'mr-auto';
1605
+ case 'right':
1606
+ return 'ml-auto';
1607
+ default:
1608
+ return '';
1609
+ }
1610
+ }
1611
+ /**
1612
+ * Map WordPress text alignment to Tailwind classes
1613
+ */
1614
+ function getTextAlignClasses(textAlign) {
1615
+ if (!textAlign)
1616
+ return '';
1617
+ switch (textAlign) {
1618
+ case 'center':
1619
+ return 'text-center';
1620
+ case 'left':
1621
+ return 'text-left';
1622
+ case 'right':
1623
+ return 'text-right';
1624
+ default:
1625
+ return '';
1626
+ }
1627
+ }
1628
+ /**
1629
+ * Map WordPress font size to Tailwind classes
1630
+ */
1631
+ function getFontSizeClasses(fontSize) {
1632
+ if (!fontSize)
1633
+ return '';
1634
+ // Map WordPress font sizes to Tailwind
1635
+ const sizeMap = {
1636
+ 'small': 'text-sm',
1637
+ 'medium': 'text-base',
1638
+ 'large': 'text-lg',
1639
+ 'x-large': 'text-xl',
1640
+ 'xx-large': 'text-3xl',
1641
+ 'xxx-large': 'text-4xl',
1642
+ };
1643
+ return sizeMap[fontSize] || '';
1644
+ }
1645
+ /**
1646
+ * Get container classes based on layout and alignment
1647
+ */
1648
+ function getContainerClasses(align, layout) {
1649
+ const alignClass = getAlignmentClasses(align);
1650
+ // If layout is constrained, use container
1651
+ if (layout?.type === 'constrained') {
1652
+ return align === 'full' ? 'w-full' : 'container';
1653
+ }
1654
+ return alignClass || 'container';
1655
+ }
1656
+ /**
1657
+ * Get spacing classes for sections
1658
+ */
1659
+ function getSectionSpacingClasses() {
1660
+ return 'py-16 md:py-24';
1661
+ }
1662
+ /**
1663
+ * Get content spacing classes
1664
+ */
1665
+ function getContentSpacingClasses() {
1666
+ return 'space-y-6';
1667
+ }
1668
+ /**
1669
+ * Build className string from multiple class sources
1670
+ */
1671
+ function buildClassName(...classes) {
1672
+ return classes
1673
+ .filter((cls) => Boolean(cls && cls.trim()))
1674
+ .join(' ')
1675
+ .trim();
1676
+ }
1677
+
1678
+ /**
1679
+ * Cloudflare Images URL helpers for wparser package
1680
+ * Formats Cloudflare image URLs with variants for optimal performance
1681
+ */
1682
+ /**
1683
+ * Check if a URL is a Cloudflare Images URL
1684
+ */
1685
+ const isCloudflareImageUrl = (url) => {
1686
+ if (!url)
1687
+ return false;
1688
+ try {
1689
+ const u = new URL(url);
1690
+ return u.hostname.toLowerCase().includes('imagedelivery.net');
1691
+ }
1692
+ catch {
1693
+ return false;
1694
+ }
1695
+ };
1696
+ /**
1697
+ * Append Cloudflare Images variant to the base URL if not already present.
1698
+ * Example output: https://imagedelivery.net/<account>/<id>/w=1024,h=600
1699
+ */
1700
+ const getCloudflareVariantUrl = (url, options) => {
1701
+ if (!isCloudflareImageUrl(url))
1702
+ return url;
1703
+ // If a transform already exists, return as-is
1704
+ if (/\/w=\d+/.test(url) || /\/(public|thumbnail|banner|avatar)/.test(url)) {
1705
+ return url;
1706
+ }
1707
+ const { width, height } = options;
1708
+ const hasTrailingSlash = url.endsWith('/');
1709
+ const base = hasTrailingSlash ? url.slice(0, -1) : url;
1710
+ const variant = `w=${Math.max(1, Math.floor(width))}` + (height ? `,h=${Math.max(1, Math.floor(height))}` : '');
1711
+ return `${base}/${variant}`;
1712
+ };
1713
+
1507
1714
  const Paragraph = ({ block, context }) => {
1508
- const content = getString(block);
1715
+ const content = getBlockTextContent(block);
1716
+ const attrs = block.attributes || {};
1717
+ const textAlign = getTextAlignClasses(attrs['align']);
1509
1718
  // Check if content contains shortcodes
1510
1719
  const hasShortcodes = /\[(\w+)/.test(content);
1511
1720
  if (hasShortcodes && context.registry.shortcodes) {
1512
1721
  const parts = renderTextWithShortcodes(content, context.registry);
1513
- return jsxRuntimeExports.jsx("p", { className: "prose-p", children: parts });
1722
+ return jsxRuntimeExports.jsx("p", { className: buildClassName('text-gray-700', textAlign), children: parts });
1514
1723
  }
1515
- return jsxRuntimeExports.jsx("p", { className: "prose-p", children: content });
1724
+ return jsxRuntimeExports.jsx("p", { className: buildClassName('text-gray-700', textAlign), children: content });
1516
1725
  };
1517
1726
  const Heading = ({ block, children }) => {
1518
- const { level = 2 } = block.attributes || {};
1519
- const content = getString(block);
1727
+ const attrs = block.attributes || {};
1728
+ const { level = 2 } = attrs;
1729
+ const content = getBlockTextContent(block);
1730
+ const textAlign = getTextAlignClasses(attrs['textAlign']);
1731
+ const fontSize = getFontSizeClasses(attrs['fontSize']);
1520
1732
  const Tag = `h${Math.min(Math.max(Number(level) || 2, 1), 6)}`;
1521
- return jsxRuntimeExports.jsx(Tag, { className: "prose-headings font-semibold", children: children ?? content });
1733
+ // Default heading sizes if fontSize not specified
1734
+ const sizeClass = fontSize || (level === 1 ? 'text-4xl' : level === 2 ? 'text-3xl' : level === 3 ? 'text-2xl' : 'text-xl');
1735
+ return (jsxRuntimeExports.jsx(Tag, { className: buildClassName('font-bold text-gray-900', sizeClass, textAlign), children: children ?? content }));
1522
1736
  };
1523
1737
  const Image = ({ block }) => {
1524
- const { url, alt, width, height } = block.attributes || {};
1525
- if (!url)
1738
+ const imageAttrs = getImageAttributes(block);
1739
+ if (!imageAttrs.url)
1526
1740
  return null;
1527
- return (jsxRuntimeExports.jsx("img", { src: url, alt: alt || '', width: width, height: height, className: "w-full h-auto object-cover", loading: "lazy" }));
1741
+ // Use Cloudflare variant URL if it's a Cloudflare image
1742
+ let imageUrl = imageAttrs.url;
1743
+ if (isCloudflareImageUrl(imageUrl)) {
1744
+ const width = imageAttrs.width || 1024;
1745
+ const height = imageAttrs.height;
1746
+ imageUrl = getCloudflareVariantUrl(imageUrl, { width, height });
1747
+ }
1748
+ return (jsxRuntimeExports.jsx("img", { src: imageUrl, alt: imageAttrs.alt, width: imageAttrs.width, height: imageAttrs.height, className: "w-full h-auto object-cover rounded-lg", loading: "lazy" }));
1528
1749
  };
1529
1750
  const List = ({ block, children }) => {
1530
- const { ordered } = block.attributes || {};
1751
+ const attrs = block.attributes || {};
1752
+ const { ordered } = attrs;
1531
1753
  const Tag = ordered ? 'ol' : 'ul';
1532
- return React.createElement(Tag, { className: 'list-disc pl-6 space-y-1' }, children);
1754
+ return React.createElement(Tag, { className: 'list-disc pl-6 space-y-2 text-gray-700' }, children);
1533
1755
  };
1534
1756
  const ListItem = ({ children }) => {
1535
- return jsxRuntimeExports.jsx("li", { children: children });
1757
+ return jsxRuntimeExports.jsx("li", { className: "text-gray-700", children: children });
1536
1758
  };
1537
- const Group = ({ children }) => {
1538
- return jsxRuntimeExports.jsx("div", { className: "space-y-4", children: children });
1759
+ const Group = ({ block, children }) => {
1760
+ const attrs = block.attributes || {};
1761
+ const align = attrs['align'];
1762
+ const layout = attrs['layout'];
1763
+ // Determine if this is a section-level group (has alignment) or content-level
1764
+ const isSection = align === 'full' || align === 'wide';
1765
+ const containerClass = getContainerClasses(align, layout);
1766
+ const spacingClass = isSection ? getSectionSpacingClasses() : getContentSpacingClasses();
1767
+ return (jsxRuntimeExports.jsx("div", { className: buildClassName(containerClass, spacingClass), children: children }));
1539
1768
  };
1540
- const Columns = ({ children }) => {
1541
- return jsxRuntimeExports.jsx("div", { className: "grid gap-6 md:grid-cols-2", children: children });
1769
+ const Columns = ({ block, children }) => {
1770
+ const attrs = block.attributes || {};
1771
+ const align = attrs['align'];
1772
+ const alignClass = getAlignmentClasses(align);
1773
+ return (jsxRuntimeExports.jsx("div", { className: buildClassName('grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-12', alignClass), children: children }));
1542
1774
  };
1543
- const Column = ({ children }) => {
1544
- return jsxRuntimeExports.jsx("div", { className: "space-y-4", children: children });
1775
+ const Column = ({ block, children }) => {
1776
+ const attrs = block.attributes || {};
1777
+ const width = attrs['width'];
1778
+ // Handle column width (e.g., "50%" becomes flex-basis)
1779
+ const style = width ? { flexBasis: width } : undefined;
1780
+ return (jsxRuntimeExports.jsx("div", { className: "space-y-4", style: style, children: children }));
1545
1781
  };
1546
1782
  const Separator = () => jsxRuntimeExports.jsx("hr", { className: "border-gray-200 my-8" });
1547
1783
  const ButtonBlock = ({ block }) => {
1548
- const { url, text } = block.attributes || {};
1549
- if (!url)
1784
+ const attrs = block.attributes || {};
1785
+ const url = attrs['url'];
1786
+ const text = attrs['text'];
1787
+ attrs['linkDestination'];
1788
+ if (!url && !text)
1550
1789
  return null;
1551
- return (jsxRuntimeExports.jsx("a", { href: url, className: "inline-flex items-center rounded-md bg-primary px-4 py-2 text-white", children: text || 'Learn more' }));
1790
+ const buttonText = text || getBlockTextContent(block) || 'Learn more';
1791
+ // Handle internal vs external links
1792
+ const isExternal = url && (url.startsWith('http://') || url.startsWith('https://'));
1793
+ const linkProps = isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {};
1794
+ return (jsxRuntimeExports.jsx("a", { href: url || '#', className: "inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-white font-medium hover:bg-primary/90 transition-colors", ...linkProps, children: buttonText }));
1795
+ };
1796
+ const Cover = ({ block, children }) => {
1797
+ const attrs = block.attributes || {};
1798
+ const { url, id, backgroundImage, overlayColor, dimRatio = 0, align = 'full', minHeight, minHeightUnit = 'vh', hasParallax, } = attrs;
1799
+ // Get background image URL from various possible sources
1800
+ let bgImageUrl = url || backgroundImage || (typeof backgroundImage === 'object' && backgroundImage?.url);
1801
+ // If we have an image ID, try to get Cloudflare URL from media data
1802
+ // For now, use the URL as-is (should already be Cloudflare if processed)
1803
+ if (bgImageUrl && isCloudflareImageUrl(bgImageUrl)) {
1804
+ // Use full width for cover images
1805
+ bgImageUrl = getCloudflareVariantUrl(bgImageUrl, { width: 1920 });
1806
+ }
1807
+ // Build alignment classes
1808
+ const alignClass = getAlignmentClasses(align);
1809
+ // Build style object
1810
+ const style = {};
1811
+ if (minHeight) {
1812
+ const minHeightValue = typeof minHeight === 'number' ? minHeight : parseFloat(String(minHeight));
1813
+ style.minHeight = minHeightUnit === 'vh' ? `${minHeightValue}vh` : `${minHeightValue}px`;
1814
+ }
1815
+ if (bgImageUrl) {
1816
+ style.backgroundImage = `url(${bgImageUrl})`;
1817
+ style.backgroundSize = 'cover';
1818
+ style.backgroundPosition = 'center';
1819
+ if (hasParallax) {
1820
+ style.backgroundAttachment = 'fixed';
1821
+ }
1822
+ }
1823
+ // Calculate overlay opacity
1824
+ const overlayOpacity = typeof dimRatio === 'number' ? dimRatio / 100 : 0;
1825
+ return (jsxRuntimeExports.jsxs("div", { className: buildClassName('relative', alignClass), style: style, children: [overlayOpacity > 0 && (jsxRuntimeExports.jsx("span", { className: "absolute inset-0", style: {
1826
+ backgroundColor: overlayColor || '#000000',
1827
+ opacity: overlayOpacity,
1828
+ }, "aria-hidden": "true" })), jsxRuntimeExports.jsx("div", { className: "relative z-10 container", children: children })] }));
1829
+ };
1830
+ const MediaText = ({ block, children, context }) => {
1831
+ const attrs = block.attributes || {};
1832
+ const { mediaPosition = 'left', verticalAlignment = 'center', imageFill = false, align = 'wide', } = attrs;
1833
+ // Access innerBlocks to identify media vs content
1834
+ const innerBlocks = block.innerBlocks || [];
1835
+ // Find media block (image or video)
1836
+ const mediaBlockIndex = innerBlocks.findIndex((b) => b.name === 'core/image' || b.name === 'core/video');
1837
+ // Render children - media-text typically has media as first child, then content
1838
+ const childrenArray = React.Children.toArray(children);
1839
+ const mediaElement = mediaBlockIndex >= 0 && childrenArray[mediaBlockIndex]
1840
+ ? childrenArray[mediaBlockIndex]
1841
+ : null;
1842
+ // Content is all other children
1843
+ const contentElements = childrenArray.filter((_, index) => index !== mediaBlockIndex);
1844
+ // Build alignment classes
1845
+ const alignClass = getAlignmentClasses(align) || 'max-w-7xl mx-auto';
1846
+ // Vertical alignment classes
1847
+ const verticalAlignClass = verticalAlignment === 'top' ? 'items-start' :
1848
+ verticalAlignment === 'bottom' ? 'items-end' :
1849
+ 'items-center';
1850
+ // Stack on mobile
1851
+ const stackClass = 'flex-col md:flex-row';
1852
+ // Media position determines order
1853
+ const isMediaRight = mediaPosition === 'right';
1854
+ return (jsxRuntimeExports.jsx("div", { className: buildClassName(alignClass, 'px-4'), 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'), children: mediaElement || jsxRuntimeExports.jsx("div", { className: "bg-gray-200 h-64 rounded-lg" }) }), jsxRuntimeExports.jsx("div", { className: buildClassName(isMediaRight ? 'order-1' : 'order-2', imageFill ? 'w-full md:w-1/2' : 'flex-1', 'space-y-4'), children: contentElements.length > 0 ? contentElements : children })] }) }));
1552
1855
  };
1553
1856
  const Fallback = ({ block, children }) => {
1554
1857
  // Minimal fallback; do not render innerHTML directly in v1 for safety
@@ -1566,13 +1869,25 @@ function createDefaultRegistry() {
1566
1869
  'core/column': Column,
1567
1870
  'core/separator': Separator,
1568
1871
  'core/button': ButtonBlock,
1569
- 'core/buttons': ({ children }) => jsxRuntimeExports.jsx("div", { className: "flex flex-wrap gap-3", children: children }),
1872
+ 'core/buttons': ({ block, children }) => {
1873
+ const attrs = block.attributes || {};
1874
+ const layout = attrs['layout'];
1875
+ const justifyContent = layout?.justifyContent || 'left';
1876
+ const justifyClass = justifyContent === 'center' ? 'justify-center' :
1877
+ justifyContent === 'right' ? 'justify-end' :
1878
+ 'justify-start';
1879
+ return jsxRuntimeExports.jsx("div", { className: buildClassName('flex flex-wrap gap-3', justifyClass), children: children });
1880
+ },
1570
1881
  'core/quote': ({ children }) => jsxRuntimeExports.jsx("blockquote", { className: "border-l-4 pl-4 italic", children: children }),
1571
1882
  'core/code': ({ block }) => (jsxRuntimeExports.jsx("pre", { className: "bg-gray-100 p-3 rounded text-sm overflow-auto", children: jsxRuntimeExports.jsx("code", { children: getString(block) }) })),
1572
1883
  'core/preformatted': ({ block }) => jsxRuntimeExports.jsx("pre", { children: getString(block) }),
1573
1884
  'core/table': ({ children }) => jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: jsxRuntimeExports.jsx("table", { className: "table-auto w-full", children: children }) }),
1574
1885
  'core/table-row': ({ children }) => jsxRuntimeExports.jsx("tr", { children: children }),
1575
1886
  'core/table-cell': ({ children }) => jsxRuntimeExports.jsx("td", { className: "border px-3 py-2", children: children }),
1887
+ // Cover block - hero sections with background images
1888
+ 'core/cover': Cover,
1889
+ // Media & Text block - side-by-side media and content
1890
+ 'core/media-text': MediaText,
1576
1891
  // HTML block - render innerHTML as-is
1577
1892
  // Note: Shortcodes in HTML blocks are not parsed (they would need to be in text content)
1578
1893
  'core/html': ({ block }) => {
@@ -1586,12 +1901,9 @@ function createDefaultRegistry() {
1586
1901
  fallback: Fallback,
1587
1902
  };
1588
1903
  }
1904
+ // Legacy function for backward compatibility - use getBlockTextContent instead
1589
1905
  function getString(block) {
1590
- const attrs = (block.attributes || {});
1591
- const content = attrs['content'];
1592
- const text = attrs['text'];
1593
- const value = (typeof content === 'string' ? content : typeof text === 'string' ? text : block.innerHTML || '');
1594
- return String(value);
1906
+ return getBlockTextContent(block);
1595
1907
  }
1596
1908
 
1597
1909
  const WPContent = ({ blocks, registry, className }) => {
@@ -1674,5 +1986,5 @@ class WPErrorBoundary extends React.Component {
1674
1986
  }
1675
1987
  }
1676
1988
 
1677
- export { WPContent, WPErrorBoundary, WPPage, createDefaultRegistry, findShortcodes, parseGutenbergBlocks, parseShortcodeAttrs, renderNodes, renderTextWithShortcodes };
1989
+ export { WPContent, WPErrorBoundary, WPPage, buildClassName, createDefaultRegistry, extractTextFromHTML, findShortcodes, getAlignmentClasses, getBlockTextContent, getCloudflareVariantUrl, getContainerClasses, getContentSpacingClasses, getFontSizeClasses, getImageAttributes, getImageUrl, getSectionSpacingClasses, getTextAlignClasses, isCloudflareImageUrl, parseGutenbergBlocks, parseShortcodeAttrs, renderNodes, renderTextWithShortcodes };
1678
1990
  //# sourceMappingURL=index.esm.js.map