@marvalt/wparser 0.1.0 → 0.1.4

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/README.md CHANGED
@@ -57,6 +57,8 @@ const registry = createDefaultRegistry();
57
57
 
58
58
  ## Supported core blocks (default registry)
59
59
  - paragraph, heading, image, list, list-item, group, columns, column, separator, buttons/button, quote, code, preformatted, table (+ row/cell)
60
+ - **cover** - Hero sections with background images, overlays, and inner content
61
+ - **media-text** - Side-by-side media and text layouts with responsive stacking
60
62
 
61
63
  ## Custom renderers
62
64
  Extend or replace mappings:
package/dist/index.cjs CHANGED
@@ -1397,8 +1397,123 @@ function renderBlock(block, registry, key, options) {
1397
1397
  return jsxRuntimeExports.jsx(React.Fragment, { children: node }, key);
1398
1398
  }
1399
1399
 
1400
- const Paragraph = ({ block }) => {
1400
+ /**
1401
+ * Parse shortcode attributes from a string
1402
+ * Supports: [shortcode attr="value" attr2="value2"]
1403
+ */
1404
+ function parseShortcodeAttrs(attrString) {
1405
+ if (!attrString || !attrString.trim()) {
1406
+ return {};
1407
+ }
1408
+ const attrs = {};
1409
+ // Match key="value" or key='value' patterns
1410
+ const attrRegex = /(\w+)=["']([^"']+)["']/g;
1411
+ let match;
1412
+ while ((match = attrRegex.exec(attrString)) !== null) {
1413
+ attrs[match[1]] = match[2];
1414
+ }
1415
+ return attrs;
1416
+ }
1417
+ /**
1418
+ * Find all shortcodes in text
1419
+ * Supports: [shortcode], [shortcode attr="value"], [shortcode]content[/shortcode]
1420
+ */
1421
+ function findShortcodes(text) {
1422
+ const shortcodes = [];
1423
+ // First, find closing tag pairs: [shortcode]content[/shortcode]
1424
+ const closingTagRegex = /\[(\w+)(?:\s+([^\]]+))?\](.*?)\[\/\1\]/gs;
1425
+ let match;
1426
+ const processedRanges = [];
1427
+ while ((match = closingTagRegex.exec(text)) !== null) {
1428
+ const name = match[1];
1429
+ const attrString = match[2] || '';
1430
+ const content = match[3] || '';
1431
+ const attrs = parseShortcodeAttrs(attrString);
1432
+ shortcodes.push({
1433
+ name,
1434
+ attrs,
1435
+ content,
1436
+ fullMatch: match[0],
1437
+ startIndex: match.index,
1438
+ endIndex: match.index + match[0].length,
1439
+ });
1440
+ processedRanges.push({
1441
+ start: match.index,
1442
+ end: match.index + match[0].length,
1443
+ });
1444
+ }
1445
+ // Then, find self-closing: [shortcode attr="value"]
1446
+ // Skip ranges already processed by closing tags
1447
+ const selfClosingRegex = /\[(\w+)(?:\s+([^\]]+))?\]/g;
1448
+ let selfClosingMatch;
1449
+ while ((selfClosingMatch = selfClosingRegex.exec(text)) !== null) {
1450
+ // Check if this match is within a processed range
1451
+ const isProcessed = processedRanges.some(range => selfClosingMatch.index >= range.start && selfClosingMatch.index < range.end);
1452
+ if (isProcessed) {
1453
+ continue;
1454
+ }
1455
+ const name = selfClosingMatch[1];
1456
+ const attrString = selfClosingMatch[2] || '';
1457
+ const attrs = parseShortcodeAttrs(attrString);
1458
+ shortcodes.push({
1459
+ name,
1460
+ attrs,
1461
+ fullMatch: selfClosingMatch[0],
1462
+ startIndex: selfClosingMatch.index,
1463
+ endIndex: selfClosingMatch.index + selfClosingMatch[0].length,
1464
+ });
1465
+ }
1466
+ // Sort by start index
1467
+ shortcodes.sort((a, b) => a.startIndex - b.startIndex);
1468
+ return shortcodes;
1469
+ }
1470
+ /**
1471
+ * Render text with shortcodes replaced by React components
1472
+ */
1473
+ function renderTextWithShortcodes(text, registry) {
1474
+ const shortcodes = findShortcodes(text);
1475
+ if (shortcodes.length === 0) {
1476
+ return [text];
1477
+ }
1478
+ const parts = [];
1479
+ let lastIndex = 0;
1480
+ for (const shortcode of shortcodes) {
1481
+ // Add text before shortcode
1482
+ if (shortcode.startIndex > lastIndex) {
1483
+ const textBefore = text.substring(lastIndex, shortcode.startIndex);
1484
+ if (textBefore) {
1485
+ parts.push(textBefore);
1486
+ }
1487
+ }
1488
+ // Render shortcode
1489
+ const renderer = registry.shortcodes[shortcode.name];
1490
+ if (renderer) {
1491
+ parts.push(jsxRuntimeExports.jsx(React.Fragment, { children: renderer(shortcode.attrs, shortcode.content) }, `shortcode-${shortcode.startIndex}`));
1492
+ }
1493
+ else {
1494
+ // Keep original shortcode if no renderer found
1495
+ parts.push(shortcode.fullMatch);
1496
+ }
1497
+ lastIndex = shortcode.endIndex;
1498
+ }
1499
+ // Add remaining text
1500
+ if (lastIndex < text.length) {
1501
+ const remainingText = text.substring(lastIndex);
1502
+ if (remainingText) {
1503
+ parts.push(remainingText);
1504
+ }
1505
+ }
1506
+ return parts;
1507
+ }
1508
+
1509
+ const Paragraph = ({ block, context }) => {
1401
1510
  const content = getString(block);
1511
+ // Check if content contains shortcodes
1512
+ const hasShortcodes = /\[(\w+)/.test(content);
1513
+ if (hasShortcodes && context.registry.shortcodes) {
1514
+ const parts = renderTextWithShortcodes(content, context.registry);
1515
+ return jsxRuntimeExports.jsx("p", { className: "prose-p", children: parts });
1516
+ }
1402
1517
  return jsxRuntimeExports.jsx("p", { className: "prose-p", children: content });
1403
1518
  };
1404
1519
  const Heading = ({ block, children }) => {
@@ -1437,6 +1552,59 @@ const ButtonBlock = ({ block }) => {
1437
1552
  return null;
1438
1553
  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' }));
1439
1554
  };
1555
+ const Cover = ({ block, children }) => {
1556
+ const attrs = block.attributes || {};
1557
+ const { url, backgroundImage, overlayColor, dimRatio = 0, align = 'full', minHeight, hasParallax, } = attrs;
1558
+ // Get background image URL from various possible sources
1559
+ const bgImageUrl = url || backgroundImage || (typeof backgroundImage === 'object' && backgroundImage?.url);
1560
+ // Build alignment classes
1561
+ const alignClass = align === 'full' ? 'w-full' : align === 'wide' ? 'max-w-7xl mx-auto' : '';
1562
+ // Build style object
1563
+ const style = {};
1564
+ if (minHeight) {
1565
+ style.minHeight = typeof minHeight === 'number' ? `${minHeight}px` : minHeight;
1566
+ }
1567
+ if (bgImageUrl) {
1568
+ style.backgroundImage = `url(${bgImageUrl})`;
1569
+ style.backgroundSize = 'cover';
1570
+ style.backgroundPosition = 'center';
1571
+ if (hasParallax) {
1572
+ style.backgroundAttachment = 'fixed';
1573
+ }
1574
+ }
1575
+ // Calculate overlay opacity
1576
+ const overlayOpacity = dimRatio / 100;
1577
+ return (jsxRuntimeExports.jsxs("div", { className: `relative ${alignClass}`, style: style, children: [overlayOpacity > 0 && (jsxRuntimeExports.jsx("span", { className: "absolute inset-0", style: {
1578
+ backgroundColor: overlayColor || '#000000',
1579
+ opacity: overlayOpacity,
1580
+ }, "aria-hidden": "true" })), jsxRuntimeExports.jsx("div", { className: "relative z-10 container mx-auto px-4 py-12", children: children })] }));
1581
+ };
1582
+ const MediaText = ({ block, children, context }) => {
1583
+ const attrs = block.attributes || {};
1584
+ const { mediaPosition = 'left', verticalAlignment = 'center', imageFill = false, align = 'wide', } = attrs;
1585
+ // Access innerBlocks to identify media vs content
1586
+ const innerBlocks = block.innerBlocks || [];
1587
+ // Find media block (image or video)
1588
+ const mediaBlockIndex = innerBlocks.findIndex((b) => b.name === 'core/image' || b.name === 'core/video');
1589
+ // Render children - media-text typically has media as first child, then content
1590
+ const childrenArray = React.Children.toArray(children);
1591
+ const mediaElement = mediaBlockIndex >= 0 && childrenArray[mediaBlockIndex]
1592
+ ? childrenArray[mediaBlockIndex]
1593
+ : null;
1594
+ // Content is all other children
1595
+ const contentElements = childrenArray.filter((_, index) => index !== mediaBlockIndex);
1596
+ // Build alignment classes
1597
+ const alignClass = align === 'full' ? 'w-full' : align === 'wide' ? 'max-w-7xl mx-auto' : 'max-w-6xl mx-auto';
1598
+ // Vertical alignment classes
1599
+ const verticalAlignClass = verticalAlignment === 'top' ? 'items-start' :
1600
+ verticalAlignment === 'bottom' ? 'items-end' :
1601
+ 'items-center';
1602
+ // Stack on mobile
1603
+ const stackClass = 'flex-col md:flex-row';
1604
+ // Media position determines order
1605
+ const isMediaRight = mediaPosition === 'right';
1606
+ return (jsxRuntimeExports.jsx("div", { className: `${alignClass} px-4`, children: jsxRuntimeExports.jsxs("div", { className: `flex ${stackClass} ${verticalAlignClass} gap-6`, children: [jsxRuntimeExports.jsx("div", { className: `${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" }) }), jsxRuntimeExports.jsx("div", { className: `${isMediaRight ? 'order-1' : 'order-2'} ${imageFill ? 'w-full md:w-1/2' : 'flex-1'} space-y-4`, children: contentElements.length > 0 ? contentElements : children })] }) }));
1607
+ };
1440
1608
  const Fallback = ({ block, children }) => {
1441
1609
  // Minimal fallback; do not render innerHTML directly in v1 for safety
1442
1610
  return jsxRuntimeExports.jsx("div", { "data-unknown-block": block.name, children: children });
@@ -1460,9 +1628,20 @@ function createDefaultRegistry() {
1460
1628
  'core/table': ({ children }) => jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: jsxRuntimeExports.jsx("table", { className: "table-auto w-full", children: children }) }),
1461
1629
  'core/table-row': ({ children }) => jsxRuntimeExports.jsx("tr", { children: children }),
1462
1630
  'core/table-cell': ({ children }) => jsxRuntimeExports.jsx("td", { className: "border px-3 py-2", children: children }),
1631
+ // Cover block - hero sections with background images
1632
+ 'core/cover': Cover,
1633
+ // Media & Text block - side-by-side media and content
1634
+ 'core/media-text': MediaText,
1635
+ // HTML block - render innerHTML as-is
1636
+ // Note: Shortcodes in HTML blocks are not parsed (they would need to be in text content)
1637
+ 'core/html': ({ block }) => {
1638
+ const html = block.innerHTML || '';
1639
+ return jsxRuntimeExports.jsx("div", { dangerouslySetInnerHTML: { __html: html } });
1640
+ },
1463
1641
  };
1464
1642
  return {
1465
1643
  renderers,
1644
+ shortcodes: {}, // Empty by default - apps extend this
1466
1645
  fallback: Fallback,
1467
1646
  };
1468
1647
  }
@@ -1558,6 +1737,9 @@ exports.WPContent = WPContent;
1558
1737
  exports.WPErrorBoundary = WPErrorBoundary;
1559
1738
  exports.WPPage = WPPage;
1560
1739
  exports.createDefaultRegistry = createDefaultRegistry;
1740
+ exports.findShortcodes = findShortcodes;
1561
1741
  exports.parseGutenbergBlocks = parseGutenbergBlocks;
1742
+ exports.parseShortcodeAttrs = parseShortcodeAttrs;
1562
1743
  exports.renderNodes = renderNodes;
1744
+ exports.renderTextWithShortcodes = renderTextWithShortcodes;
1563
1745
  //# sourceMappingURL=index.cjs.map