Jake Vanderwerf
4 days ago 747d741293e064a979d7bf6c143ef969ea6d7629
inc/blocks/CustomBlocks.php
@@ -6,7 +6,9 @@
use JVBase\managers\Cache;
use JVBase\managers\LoginManager;
use JVBase\managers\SEO\BreadcrumbManager;
use JVBase\managers\SEO\render\Thing\CreativeWork\MediaObject\VideoObject;
use JVBase\utility\Image;
use JVBase\utility\Video;
use WP_Block;
use WP_Query;
@@ -22,11 +24,13 @@
   protected static ?int $currentQueryId = null;
   protected static array $counters = [];
   protected static ?WP_Query $originalQuery = null;
   protected array $ignore = ['align','alt','area','backgroundColor','borderColor','buttonText','buttonPosition','buttonUseIcon','categories','className','columns','contentPosition','customOverlayColor','dimRatio','displayAsDropdown','displayAuthor','displayFeaturedImage','displayPostContent','displayPostContentRadio','displayPostDate','excerptLength','featuredImageAlign','fontSize','gradient','height','iconColor','iconColorValue','iconColorValue','iconBackgroundColor','iconBackgroundColorValue','id','imageFill','isDark','isLink','isSearchFieldHidden','isStackedOnMobile','isUserOverlayColor','kind','label','largestFontSize','layout','level','mediaId','mediaLink','mediaSizeSlug','mediaType','metadata','minHeight','minHeightUnit','opacity','opensInNewTab','order','orderBy','ordered','overlayMenu','placeholder','postLayout','postsToShow','query', 'queryId','ref','rel','shouldSyncIcon','showEmpty','showHierarchy','showLabel','showLabels','showOnlyTopLevel','showPostCounts','showTagCounts','size','sizeSlug','slug','smallestFontSize','tagName','taxonomy','term','textAlign','textColor','theme','title','type','url','useFeaturedImage','width','widthUnit',];
   protected array $ignore = ['align','alt','area','aspectRatio','backgroundColor','borderColor','buttonText','buttonPosition','buttonUseIcon','categories','className','columns','contentPosition','customOverlayColor','dimRatio','displayAsDropdown','displayAuthor','displayFeaturedImage','displayPostContent','displayPostContentRadio','displayPostDate','excerptLength','featuredImageAlign','fontSize','gradient','hasFixedLayout','hasParallax','height','iconColor','iconColorValue','iconColorValue','iconBackgroundColor','iconBackgroundColorValue','id','imageFill','isDark','isLink','isObjectPosition','isRepeated','isSearchFieldHidden','isStackedOnMobile','isUserOverlayColor','kind','label','largestFontSize','layout','lightbox','linkDestination','linkTo','level','mediaId','mediaLink','mediaPosition','mediaSizeSlug','mediaType','mediaWidth','metadata','minHeight','minHeightUnit','opacity','opensInNewTab','order','orderBy','ordered','overlayColor','overlayMenu','placeholder','postLayout','postsToShow','query', 'queryId','ref','rel','scale','shouldSyncIcon','showContent','showEmpty','showHierarchy','showLabel','showLabels','showOnlyTopLevel','showPostCounts','showTagCounts','size','sizeSlug','slug','smallestFontSize','tagName','taxonomy','term','textAlign','textColor','theme','title','type','url','useFeaturedImage','verticalAlignment','width','widthUnit',];
   //For custom style output for nested links, etc
   protected static array $pendingStyles = [];
   protected static array $pendingClass = [];
   protected static bool $renderGallery = false;
    public function __construct()
    {
        $this->cache = Cache::for('blocks', WEEK_IN_SECONDS);
@@ -98,7 +102,7 @@
      } else if (method_exists($this, $method)) {
         $content = $this->$method($block, $content, $parent);
         return $isPrerender ? $this->maybeOutputCustomStyles().$content : $content;
      } elseif (!empty($blockName) && JVB_TESTING) {
      } elseif (JVB_TESTING && !empty($blockName)) {
         if (!in_array($block['blockName'], $this->getIgnore($isPrerender))) {
            jvbDump('No method found for '.print_r($block['blockName'], true));
         }
@@ -109,7 +113,9 @@
      {
         //Ignore for both
         $base = [
            'core/null'
            'core/null',
            'core/list-item',
            'jvb/drawer-menu'
         ];
         if ($isPrerender) {
            $base = array_merge($base, [
@@ -203,8 +209,11 @@
    {
//    jvbDump($block, 'buttons');
//    jvbDump($parent, 'Parent');
        return '<ul'.$this->getClassesAndStyles($block['attrs']??[], ['buttons','row']).'>'.
               $this->innerBlocks($block).'</ul>';
        return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($block['attrs']??[], ['buttons','row']),
               $this->innerBlocks($block)
      );
    }
    public function prerender_core_column(array $block, ?string $content, ?WP_Block $parent):?string
@@ -215,14 +224,16 @@
                   array_key_exists('width', $block['attrs'])) ?
            ['flex-basis:'.$block['attrs']['width']]
            : [];
        return '<div'.
               $this->getClassesAndStyles($block['attrs']??[], ['col'], $styles).'>'.
               $this->innerBlocks($block).'</div>';
        return sprintf(
         '<div%s>%s</div>',
               $this->getClassesAndStyles($block['attrs']??[], ['col'], $styles),
               $this->innerBlocks($block)
      );
    }
    public function prerender_core_columns(array $block, ?string $content, ?WP_Block $parent):?string
    {
      jvbDump($block, 'columns');
//    jvbDump($block, 'columns');
      $attrs = $block['attrs']??[];
      $tagName = array_key_exists('tagName', $attrs) ? $attrs['tagName'] : 'section';
@@ -295,8 +306,10 @@
//    jvbDump($block, 'spsacer');
//    jvbDump($parent, 'Parent');
        return '<div'.$this->getClassesAndStyles($block['attrs']??[], ['spacer'], ['height:2rem']).
               ' aria-hidden="true"></div>';
        return sprintf(
         '<div%s aria-hidden="true"></div>',
         $this->getClassesAndStyles($block['attrs']??[], ['spacer'], ['height:2rem'])
      );
    }
    //core_table_of_contents
    //core_text_columns
@@ -309,22 +322,31 @@
     * Media Blocks
     */
    //core_audio
   public function prerender_core_audio(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block,'audio');
      $attrs = $block['attrs']??[];
      $inside = $this->inside($block);
      return sprintf('<figure%s>%s</figure>',
         $this->getClassesAndStyles($attrs),
         $inside
      );
   }
    public function prerender_core_cover(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'cover');
//    jvbDump($parent, 'Parent');
        // Extract block attributes
        $attrs = $block['attrs'] ?:[];
        $innerContent = $this->innerBlocks($block);
      $position = 'object-position: center;';
      if (array_key_exists('focalPoint', $attrs)) {
         $x = (array_key_exists('x', $attrs['focalPoint'])) ? ($attrs['focalPoint']['x'] * 100).'%' : 'center';
         $y = (array_key_exists('y', $attrs['focalPoint'])) ? ($attrs['focalPoint']['y'] * 100).'%' : 'center';
         $position = 'object-position:'.$x.' '.$y.';';
         unset($attrs['focalPoint']);
         $attrs['isObjectPosition'] = true;
      }
        // Check for background type
        $backgroundType = $attrs['backgroundType'] ?? 'image';
        $background = '';
@@ -337,33 +359,93 @@
         $ID = (int)$attrs['id'];
      }
        if ($backgroundType === 'image' && $ID) {
      $doImage = true;
      if ($this->checkAttrs('hasParallax', $attrs) || $this->checkAttrs('isRepeated', $attrs)) {
         $doImage = false;
         $attrs['style']['background']['backgroundImage']['id'] = $ID;
      }
        if ($doImage && $backgroundType === 'image' && $ID) {
         $background .= str_replace('<img', '<img style="'.$position.'"', $this->image($ID));
        } elseif ($backgroundType === 'video' && isset($attrs['url'])) {
            $background .= '<video style="'.$position.'"autoplay muted loop playsinline src="' . esc_url($attrs['url']) . '"></video>';
        }
      $overlay = '';
      if (!isset($attrs['style']['color']['duotone']) && (array_key_exists('overlayColor', $attrs) || array_key_exists('dimRatio', $attrs))) {
         $tmp = [];
         if (array_key_exists('overlayColor', $attrs)) {
            $tmp['overlayColor'] = $attrs['overlayColor'];
         }
         if (array_key_exists('dimRatio', $attrs)) {
            $tmp['dimRatio'] = $attrs['dimRatio'];
         }
         unset($attrs['overlayColor']);
         unset($attrs['dimRatio']);
         $overlay = sprintf(
            '<div class="overlay"%s></div>',
            $this->buildStylesString($tmp)
         );
      }
      // Build classes and styles
      unset($attrs['url']);
      $classes = $this->getClassesAndStyles($attrs, ['cover row']);
      return '<section' . $classes . '>' .
               $background .
               '<div class="content">' .
               $innerContent .
               '</div></section>';
      return sprintf('<section%s>%s%s<div class="content">%s</div></section>',
         $classes,
         $overlay,
         $background,
         $innerContent
      );
    }
    //core_file
   public function prerender_core_file(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $attrs = $block['attrs']??[];
      $showButton = !array_key_exists('showDownloadButton', $attrs);
      preg_match('/>([^<]*)<\/a>/', $block['innerHTML'], $label);
      $label = $label[1]??'';
      $button = $showButton ?
         sprintf(
            '&emsp;<a class="btn chip" href="%s">%s<span>Download</span></a>',
            $attrs['href'],
            jvbIcon('cloud-arrow-down')
         ) :
         '';
      $aOpen = $showButton ? '' :
         sprintf(
            '<a href="%s">',
            $attrs['href']
         );
      $aClose = $showButton ? '' : '</a>';
      return sprintf(
         '<p%s>%s%s%s%s</p>',
         $this->getClassesAndStyles($attrs, ['file']),
         $aOpen,
         $showButton ? $label : 'Download: '.$label,
         $aClose,
         $button
      );
   }
    public function prerender_core_gallery(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'gallery');
      $attrs = $block['attrs']??[];
//    jvbDump($parent, 'Parent');
        return '<ul'.$this->getClassesAndStyles($block['attrs']??[], ['gallery']).'>'.
               $this->innerBlocks($block,'<li>', '</li>').
               '</ul>';
      static::$renderGallery = true;
        return sprintf(
      '<ul%s>%s</ul>',
         $this->getClassesAndStyles($attrs, ['gallery']),
         $this->innerBlocks($block,'<li>', '</li>')
      );
    }
    public function prerender_core_image(array $block, ?string $content, ?WP_Block $parent):?string
@@ -375,6 +457,10 @@
            return '';
        }
      $attrs = $block['attrs']??[];
      static::$renderGallery = true;
        $title = (get_the_title($ID) !== '') ? '<b>'.get_the_title($ID).'</b>' : '';
        $caption = (wp_get_attachment_caption($ID)) ?
            '<figcaption>' .
@@ -382,11 +468,23 @@
                wp_get_attachment_caption($ID) .
            '</figcaption>' :
            '<figcaption>' . $title . '</figcaption>';
      $size = array_key_exists('sizeSlug', $block['attrs']??[]) ? $block['attrs']['sizeSlug'] : 'large';
        return '<figure'.
               $this->getClassesAndStyles($block['attrs']??[]).'>'.
               $this->imageLink(true, $ID, 'tiny', $size) .
               $caption.'</figure>';
      $size = $attrs['sizeSlug'] ?? 'large';
      $img = $this->imageLink(true, $ID, 'tiny', $size);
      $aspectRatio = $attrs['aspectRatio']??false;
      if ($aspectRatio) {
         $img = str_replace('<img', sprintf(
            '<img style="aspect-ratio:%s;"',
            $aspectRatio
         ), $img);
      }
        return sprintf(
         '<figure%s>%s%s</figure>',
               $this->getClassesAndStyles($block['attrs']??[]),
               $img,
               $caption,
      );
    }
    public function prerender_core_media_text(array $block, ?string $content, ?WP_Block $parent):?string
@@ -408,13 +506,35 @@
         $classes[] = 'stack-small';
      }
      $figClasses = [];
      if (isset($attrs['mediaWidth'])) {
         $figClasses[] = 'width:'.$attrs['mediaWidth'].'%';
      }
      if (isset($attrs['imageFill']) && $attrs['imageFill'] === true) {
         $figClasses[] = 'object-fit: cover';
      }
      if (array_key_exists('focalPoint', $attrs)) {
         $attrs['isObjectPosition'] = true;
         $style = $this->getFocalPointStyle($attrs['focalPoint'], $attrs);
         $style .= ';object-fit:none;';
         $imgLink = str_replace('<img', sprintf(
            '<img style="%s"',
            $style
         ), $imgLink);
         unset($attrs['focalPoint']);
      }
      $figClasses = empty($figClasses) ? '' : ' style="'.implode(';',$figClasses).'"';
      $inside = array_key_exists('mediaPosition', $attrs) && $attrs['mediaPosition'] === 'right'
         ? sprintf(
            '<div>%s</div><figure>%s</figure>',
            $inner, $imgLink
            '<div>%s</div><figure%s>%s</figure>',
            $inner,$figClasses, $imgLink
         ) : sprintf(
            '<figure>%s</figure><div>%s</div>',
            $imgLink, $inner
            '<figure%s>%s</figure><div>%s</div>',
            $figClasses,$imgLink, $inner
         );
        return sprintf(
@@ -427,27 +547,31 @@
   public function prerender_core_video(array $block, ?string $content, ?WP_Block $parent):?string
   {
      jvbDump($block, 'video');
//    jvbDump($block, 'video');
//    jvbDump($parent, 'Parent');
      $ID = $this->imageID('', $block);
      if (!$ID) {
         return '';
      }
      jvbDump($ID);
      $attrs = $block['attrs']??[];
//
//    $ID = $attrs['id']??false;
//    if (!$ID) {
//       return '';
//    }
//    $caption = wp_get_attachment_caption($ID);
//    $title = get_the_title($ID);
//
//    $figCaption = sprintf(
//       '<figcaption><b>%s</b>%s</figcaption>',
//       $title,
//       $caption
//    );
//
//    $video = Video::get($ID);
      $title = (get_the_title($ID) !== '') ? '<b>'.get_the_title($ID).'</b>' : '';
      $caption = (wp_get_attachment_caption($ID)) ?
         '<figcaption>' .
         $title .
         wp_get_attachment_caption($ID) .
         '</figcaption>' :
         '<figcaption>' . $title . '</figcaption>';
      $size = array_key_exists('sizeSlug', $block['attrs']??[]) ? $block['attrs']['sizeSlug'] : 'large';
      return '<figure'.
         $this->getClassesAndStyles($block['attrs']??[]).'>'.
         $this->imageLink(true, $ID, 'tiny', $size) .
         $caption.'</figure>';
      $inside = $this->inside($block);
      return sprintf('<figure%s>%s</figure>',
         $this->getClassesAndStyles($attrs),
         $inside
      );
   }
    /**
@@ -458,18 +582,56 @@
    /**
     * Text Blocks
    */
    //prerender_core_code
    //prerender_core_details
    public function prerender_core_code(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'code');
      $attrs = $block['attrs']??[];
      $content = $this->inside($block);
      return str_replace('<code', sprintf(
         '<code%s',
         $this->getClassesAndStyles($attrs),
      ), $content);
   }
    public function prerender_core_details(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'details');
      $attrs = $block['attrs']??[];
      $isOpen = $this->checkAttrs('showContent', $attrs);
      $summary = $this->extractElement($block['innerHTML'], 'summary');
      $inside = $this->innerBlocks($block);
      return sprintf(
         '<details%s%s><summary>%s</summary>%s</details>',
         $this->getClassesAndStyles($attrs),
         $isOpen ? ' open' : '',
         $summary,
         $inside
      );
   }
    //prerender_core_footnotes
// public function prerender_core_footnotes(array $block, ?string $content, ?WP_Block $parent):?string
// {
//    jvbDump($block, 'footnotes');
//
//    return null;
// }
    //prerender_core_classic
    public function prerender_core_heading(array $block, ?string $content, ?WP_Block $parent):?string
    {
        $level = (array_key_exists('level', $block['attrs']??[])) ? $block['attrs']['level'] : '2';
//    jvbDump($block, 'heading');
      $attrs = $block['attrs']??[];
        $level = $attrs['level'] ?? '2';
      $content = $this->inside($block);
        $id = sanitize_title(wp_strip_all_tags($this->stripTagContents('small', $content)));
        return '<h'.$level.' id="'.$id.'"'.$this->getClassesAndStyles($block['attrs']??[]).'>'.
               $content.
               '</h'.$level.'>';
        return sprintf(
      '<h%s id="%s"%s>%s</h%s>',
         $level,
         $id,
         $this->getClassesAndStyles($attrs),
         $content,
         $level,
      );
    }
   public function prerender_core_list(array $block, ?string $content, ?WP_Block $parent):?string
@@ -477,8 +639,13 @@
//    jvbDump($block, 'list');
//    jvbDump($parent, 'Parent');
      $tag = (array_key_exists('ordered', $block['attrs']??[])) ? 'ol' : 'ul';
      $output = '<'.$tag.$this->getClassesAndStyles($block['attrs']??[]).'>'.$this->innerBlocks($block).'</'.$tag.'>';
      return $output;
      return sprintf(
         '<%s%s>%s</%s>',
         $tag,
         $this->getClassesAndStyles($block['attrs']??[]),
         $this->innerBlocks($block),
         $tag
      );
   }
// public function prerender_core_list_item(array $block):string
@@ -516,10 +683,13 @@
         $content = $this->stripTagContents('cite', $content);
      }
      return '<blockquote'.$this->getClassesAndStyles($block['attrs']??[]).'>
        <div class="content">'.$content.'</div>'.
         $citeHtml.
         '</blockquote>';
      return sprintf(
         '<blockquote%s>%s%s%s</blockquote>',
         $this->getClassesAndStyles($block['attrs']??[]),
         jvbIcon('quotes',['style' => 'fill']),
         $content,
         $citeHtml
      );
   }
   public function prerender_core_pullquote(array $block, ?string $content, ?WP_Block $parent):?string
   {
@@ -540,13 +710,58 @@
      }
      $content = jvb_filter_content( $content);
      return '<blockquote'.$this->getClassesAndStyles($block['attrs']??[], ['pull']).'>'.
         $content.
         $citeHtml.
         '</blockquote>';
      return sprintf(
         '<blockquote%s>%s%s</blockquote>',
         $this->getClassesAndStyles($block['attrs']??[], ['pull']),
         $content,
         $citeHtml
      );
   }
    //prerender_core_table
    //prerender_core_verse
    public function prerender_core_table(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'table');
      $attrs = $block['attrs']??[];
      $figAttrs = [
         'align' => $attrs['align']??''
      ];
      unset($attrs['align']);
      $inside = $this->inside($block);             // inside the figure
      $parts = explode('<figcaption', $inside); // inside the table
      $table = $parts[0];
      $table = str_replace(strtok($table, '>'),sprintf(
         '<table%s',
         $this->getClassesAndStyles($attrs)
      ), $table);
      $caption = str_replace(strtok($parts[1], '>'), '<figcaption', $parts[1]);
      return sprintf(
         '<figure%s>%s%s</figure>',
         $this->buildClassesString($figAttrs),
         $table,
         $caption
      );
   }
    public function prerender_core_preformatted(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'verse');
      $attrs = $block['attrs']??[];
      return sprintf(
         '<pre%s>%s</pre>',
         $this->getClassesAndStyles($attrs),
         $this->inside($block)
      );
   }
    public function prerender_core_verse(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'verse');
      $attrs = $block['attrs']??[];
      return sprintf(
         '<pre%s>%s</pre>',
         $this->getClassesAndStyles($attrs),
         $this->inside($block)
      );
   }
    /**
     * Theme Blocks
@@ -754,7 +969,12 @@
        $linkOpen = $this->buildNavigationLink($attrs, $aria);
        return '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].'</a></li>';
        return sprintf(
         '<li%s>%s%s</a></li>',
         $classes,
         $linkOpen,
         $block['attrs']['label']
      );
    }
    public function prerender_core_navigation_submenu(array $block, ?string $content, ?WP_Block $parent):?string
@@ -829,7 +1049,14 @@
               break;
            }
        }
        return '<a href="'.$url.'"'.$aria.$rel.$target.$title.'>';
        return sprintf(
         '<a href="%s"%s%s%s%s>',
         $url,
         $aria,
         $rel,
         $target,
         $title
      );
    }
    /**
@@ -952,6 +1179,10 @@
            $result = $this->innerBlocks($block);
        }
      if(static::$renderGallery) {
         add_action('wp_footer', 'jvbRenderGallery');
      }
      return apply_filters('jvb_post_content_output', $result, $block);
    }
    //core_post_date
@@ -1045,20 +1276,23 @@
      global $post;
      $attrs = $block['attrs']??[];
      $ID = get_post_thumbnail_id($post->ID);
      $aspectRatio = $aOpen = $aClose = '';
      $aOpen = $aClose = '';
      if(!is_single($post->ID) && $this->checkAttrs('isLink', $attrs)) {
         $aOpen = '<a href="'.get_the_permalink($post->ID).'">';
         $aClose = '</a>';
      }
      if (array_key_exists('aspectRatio', $attrs)) {
         $aspectRatio = $attrs['aspectRatio'];
      }
      $img = apply_filters('jvbCoreFeaturedImage', '', $post->post_type, $attrs);
      if (empty($img)) {
         $img = $this->image($ID);
         $img = empty($aspectRatio) ? $img : str_replace('<img', '<img style="aspect-ratio:'.$aspectRatio.';"', $img);
      }
      $aspectRatio = $attrs['aspectRatio']??false;
      if ($aspectRatio) {
         $img = str_replace('<img', sprintf(
            '<img style="aspect-ratio:%s;"',
            $aspectRatio
         ), $img);
      }
@@ -1127,7 +1361,10 @@
      $inner = '';
      if (!static::$currentLoop) {
         jvbDump('No loop stored');
         if (JVB_TESTING) {
            jvbDump('No loop stored');
         }
         return $content;
      }
      if (static::$currentLoop->have_posts()) {
@@ -1567,7 +1804,7 @@
   public function prerender_core_query_title(array $block, ?string $content, ?WP_Block $parent):?string
   {
      jvbDump($block, 'query title');
//    jvbDump($block, 'query title');
      $attr = $block['attrs'];
      $name = '';
      $showPrefix = $attr['showPrefix']??false;
@@ -1720,7 +1957,7 @@
    //core_archives
   public function render_core_archives(array $block, string $content):string
   {
      jvbDump($block, 'archives');
//    jvbDump($block, 'archives');
      $attrs = $block['attrs']??[];
      $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
@@ -1982,7 +2219,6 @@
      }
      $inside = [];
      foreach($pages->posts as $page) {
         jvbDump($page);
         $inside[] = sprintf(
            '<li><a href="%s">%s</a>',
            get_the_permalink($page->ID),
@@ -2210,7 +2446,7 @@
    * @param string $tag The tag name to extract
    * @return string The content of the first matching element, or empty string
    */
   protected function extractElement(string $html, string $tag): string
   public function extractElement(string $html, string $tag): string
   {
      if (empty($html)) {
         return '';
@@ -2344,51 +2580,64 @@
        return $out;
    }
    protected function getClassesAndStyles(
    public function getClassesAndStyles(
        array $attrs,
        array $classes = [],
        array $styles = []
    ):string {
        // Get styles and classes from attributes
        $attr_styles = $this->getInlineStyles($attrs);
        $attr_classes = $this->getClasses($attrs);
      if(array_key_exists('slug', $attrs) && $attrs['slug'] === 'footer') {
         $classes[] = 'col';
      }
        // Merge with passed classes and styles
        $styles = array_merge($attr_styles, $styles);
        $classes = array_merge($attr_classes, $classes);
      if (!empty(static::$pendingClass)) {
         $classes = array_merge($classes, static::$pendingClass);
         static::$pendingClass = [];
      }
      $classes = array_unique($classes);
      $data = $this->getDataset($attrs);
        // Build attribute strings
        $class_string = !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : '';
        $style_string = !empty($styles) ? ' style="' . implode(';', $styles) . '"' : '';
      $data_string = '';
      if (!empty($data)) {
         foreach ($data as $d => $v) {
            if ($d === 'bg-small') {
               $data_string .= ' data-bg-img';
            }
            $data_string .= sprintf(
               ' data-%s="%s"',
               $d,
               $v
            );
         }
      }
        $return = trim($class_string . $style_string . $data_string);
        $return = trim($this->buildClassesString($attrs, $classes) . $this->buildStylesString($attrs,$styles) . $this->buildDataset($attrs));
        return ($return=='')? '' : ' '.$return;
    }
      protected function buildStylesString(array $attrs, array $custom = []):string
      {
         $attr_styles = $this->getInlineStyles($attrs);
         $styles = array_merge($attr_styles, $custom);
         $styles = array_map(function ($property, $value) {
            return sprintf('%s:%s', $property, $value);
         }, array_keys($styles), $styles);
         return !empty($styles) ? ' style="' . implode(';', $styles) . '"' : '';
      }
      protected function buildClassesString(array $attrs, array $custom = []):string
      {
         $attr_classes = $this->getClasses($attrs);
         if(array_key_exists('slug', $attrs) && $attrs['slug'] === 'footer') {
            $attr_classes[] = 'col';
         }
         // Merge with passed classes and styles
         $classes = array_merge($attr_classes, $custom);
         if (!empty(static::$pendingClass)) {
            $classes = array_merge($classes, static::$pendingClass);
            static::$pendingClass = [];
         }
         $classes = array_unique($classes);
         // Build attribute strings
         return !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : '';
      }
      protected function buildDataset(array $attrs):string
      {
         $data = $this->getDataset($attrs);
         $data_string = '';
         if (!empty($data)) {
            foreach ($data as $d => $v) {
               if ($d === 'bg-small') {
                  $data_string .= ' data-bg-img';
               }
               $data_string .= sprintf(
                  ' data-%s="%s"',
                  $d,
                  $v
               );
            }
         }
         return $data_string;
      }
    /**
     * @param string $spacing
     *
@@ -2434,6 +2683,7 @@
                return match ($value) {
                    'is-style-floating' => 'always mobile fixed',
               'is-style-fixed' => 'fixed bottom',
               'is-style-default' => '',
                    default => str_replace('is-style-', '', $value),
                };
         case 'contentPosition':
@@ -2465,7 +2715,7 @@
            case 'width':
            return $this->getWidth($value);
            case 'dimRatio':
                return $this->getDimRatio($value);
                return $this->getDimRatio($value, $attrs);
         case 'overlayColor':
            return $value;
            break;
@@ -2477,7 +2727,7 @@
            //Media
            case 'hasParallax':
                return $value === true ? 'bg-parallax' : '';
                return $value === true ? 'bg-fixed' : '';
            case 'isRepeated':
                return $value === true ? 'bg-repeat' : '';
@@ -2615,7 +2865,17 @@
      private function getWidth(string $value):string
      {
         $value = (int)str_replace('%', '', $value);
         $value = str_replace('%', '', $value);
         if (str_contains($value, 'px') ||
            str_contains($value, 'em') ||
            str_contains($value, 'rem') ||
            str_contains($value, 'vw') ||
            str_contains($value, 'vh')) {
            return '';
         }
         return sprintf(
            'width-%d',
            match (true) {
@@ -2628,8 +2888,11 @@
            }
         );
      }
      private function getDimRatio(string $value):string
      private function getDimRatio(string $value, array $attrs):string
      {
         if (array_key_exists('overlayColor', $attrs)) {
            return '';
         }
         if (is_numeric($value)) {
            return sprintf(
               'op-%d',
@@ -2658,6 +2921,8 @@
            if (array_key_exists('duotone', $value['color'])) {
               $preset = explode('|', $value['color']['duotone']);
               $preset = $preset[array_key_last($preset)];
               $preset = $this->getColor($preset, false);
               if (str_contains($preset, '-')) {
                  $preset = explode('-', $preset);
               } else {
@@ -2771,10 +3036,9 @@
        $styles = [];
        foreach ($attrs as $key => $value) {
            $style = $this->getStyle($key, $value, $attrs);
         if (is_string($style)) {
            $style = [$style];
         }
            $styles = array_merge($styles, $style);
         $styles = array_unique($styles);
        }
        return $styles;
    }
@@ -2800,13 +3064,13 @@
            // Background URL (for cover, media blocks)
            case 'url':
                if (!empty($value) && str_starts_with($value, 'http')) {
                    return 'background-image: url('.$value.')';
                    return ['background-image' => 'url('.$value.')'];
                }
                break;
            // Focal point for background images
            case 'focalPoint':
            return $this->getFocalPointStyle($value);
            return $this->getFocalPointStyle($value, $attrs);
            // Complex style object
            case 'style':
@@ -2820,16 +3084,26 @@
                if ($value === 'video' && isset($attrs['backgroundUrl'])) {
                    // Don't set a background image for videos - it will be handled by the video element
                } elseif (isset($attrs['backgroundUrl'])) {
                    return 'background-image: url('.$attrs['backgroundUrl'].')';
                    return ['background-image' => 'url('.$attrs['backgroundUrl'].')'];
                }
                break;
         case 'width':
            if (str_contains($value, 'px') ||
               str_contains($value, 'em') ||
               str_contains($value, 'rem') ||
               str_contains($value, 'vw') ||
               str_contains($value, 'vh')) {
               return ['width' => $value];
            }
            break;
         case 'backgroundColor':
         case 'borderColor':
         case 'textColor':
            $type = str_replace('Color', '-color', $key);
            $type = str_replace('text-', '', $type);
            if (isset($attrs['border']['width']) && $key === 'borderColor') {
            if ($key === 'borderColor' && isset($attrs['border']['width'])) {
               break;
            }
            return $this->getColorStyle($value, $type);
@@ -2853,13 +3127,13 @@
         foreach ($values as $v) {
            switch ($value) {
               case 'has-small-icon-size':
                  $styles[] = '--w:var(--txt-x-small)';
                  $styles['--w'] = 'var(--txt-x-small)';
                  break;
               case 'has-large-icon-size':
                  $styles[] = '--w:var(--txt-large)';
                  $styles['--w'] = 'var(--txt-large)';
                  break;
               case 'has-huge-icon-size':
                  $styles[] = '--w:var(--txt-xx-large)';
                  $styles['--w'] = 'var(--txt-xx-large)';
                  break;
               default:
                  if (JVB_TESTING) {
@@ -2869,38 +3143,36 @@
         }
         return $styles;
      }
      private function getFontFamilyStyle(string $value):string
      private function getFontFamilyStyle(string $value):array
      {
         return match($value) {
            'body'      => 'font-family: var(--body)',
            'heading'   => 'font-family: var(--heading)',
            default     => sprintf('font-family: %s', $value)
            'body'      => ['font-family' =>  'var(--body)'],
            'heading'   => ['font-family' =>  'var(--heading)'],
            default     => ['font-family' => $value]
         };
      }
      private function getColorStyle(string $value, ?string $type = 'color'):string
      private function getColorStyle(string $value, ?string $type = 'color'):array
      {
         if (!in_array($type, ['color', 'background','background-color','border-color'])) {
            $type = null;
         }
         return sprintf(
            '%s%s',
            $type ? $type.': ' : '',
            $this->getColor($value)
         );
         if (!$type) {
            return [];
         }
         return [
            $type => $this->getColor($value)
         ];
      }
      private function getMinHeightStyle(string $value, array $attrs):string
      private function getMinHeightStyle(string $value, array $attrs):array
      {
         $out = '';
         $out = [];
         if (!empty($value)) {
            if (isset($attrs['minHeightUnit'])) {
               $out = sprintf(
                  'min-height: %s%s',
                  $value,
                  $attrs['minHeightUnit']
               );
               $out['min-height'] = sprintf('%s%s', $value, $attrs['minHeightUnit']);
            } else {
               $out = sprintf(
                  'min-height: %spx',
               $out['min-height'] = sprintf(
                  '%spx',
                  $value
               );
            }
@@ -2908,18 +3180,20 @@
         return $out;
      }
      private function getFocalPointStyle(array $value):string
      private function getFocalPointStyle(array $value, array $attrs):array
      {
         jvbDump($value, 'Focal Point');
         $x = array_key_exists('x', $value) ? $value['x'] * 100 : 'center';
         $y = array_key_exists('y', $value) ? $value['y'] * 100 : 'center';
         $x = array_key_exists('x', $value) ? ($value['x'] * 100).'%' : 'center';
         $y = array_key_exists('y', $value) ? ($value['y'] * 100).'%' : 'center';
         $y = $x === $y ? '' : ' '.$y;
         return sprintf(
            'background-position:%s%s',
         $key = array_key_exists('isObjectPosition', $attrs) ? 'object-position' : 'background-position';
         return [
            $key => sprintf(
            '%s%s',
            $x,
            $y
         );
         )];
      }
      private function extractStyles(array $value, array $attrs):array
@@ -2933,13 +3207,16 @@
               case 'color':
                  if (isset($v['background'])) {
                     $styles[] = $this->getColorStyle($v['background'], 'background-color');
                     $styles['background-color'] = $this->getColor($v['background']);
                  }
                  if (isset($v['text'])) {
                     $styles[] = $this->getColorStyle($v['text']);
                     $styles['color'] = $this->getColor($v['text']);
                  }
                  if (isset($v['gradient'])) {
                     jvbDump($v, 'Gradient');
                     if (JVB_TESTING) {
                        jvbDump($v, 'Gradient');
                     }
                  }
                  break;
@@ -2962,13 +3239,13 @@
                           );
                        }
                        if (!empty($inner)) {
                           $styles[] = sprintf(
                              '--gap: %s',
                           $styles['--gap'] = sprintf(
                              '%s',
                              implode(' ', $inner)
                           );
                        }
                     } else {
                        $styles[] = '--gap: var(--sp'.$this->getPresetSpacing($v['blockGap']).')';
                        $styles['--gap'] = 'var(--sp'.$this->getPresetSpacing($v['blockGap']).')';
                     }
                  }
@@ -2977,7 +3254,7 @@
                  if (isset($v['margin'])) {
                     foreach ($v['margin'] as $direction => $size) {
                        if (!str_contains($size, 'var:preset')) {
                           $styles[] = 'margin-'.$direction.': '.$size;
                           $styles['margin-'.$direction] = $size;
                        }
                     }
                  }
@@ -2985,7 +3262,7 @@
                  if (isset($v['padding'])) {
                     foreach($v['padding'] as $dir => $size) {
                        if (!str_contains($size, 'var:preset')) {
                           $styles[] = 'padding-'.$dir.': '.$size;
                           $styles['padding-'.$dir] = $size;
                        }
                     }
                  }
@@ -2995,8 +3272,8 @@
                  if (array_key_exists('backgroundImage', $v)) {
                     $data = Image::getData($v['backgroundImage']['id']);
                     if (!empty($data) && array_key_exists('tiny', $data)) {
                        $styles[] = sprintf(
                           'background-image: url(%s)',
                        $styles['background-image'] = sprintf(
                           'url(%s)',
                           $data['tiny']
                        );
                     }
@@ -3007,12 +3284,11 @@
               case 'dimensions':
                  foreach ($v as $sk => $sv) {
                     if ($sk === 'minHeight') {
                        $styles[] = sprintf(
                           'min-height: %s',
                           $sv
                        );
                        $styles['min-height'] = $sv;
                     } else {
                        jvbDump('No config set for dimension '.$sk.': '.print_r($sv, true));
                        if (JVB_TESTING) {
                           jvbDump('No config set for dimension '.$sk.': '.print_r($sv, true));
                        }
                     }
                  }
@@ -3030,7 +3306,10 @@
                  break;
               default:
                  jvbDump('No config set for '.$k.': '.print_r($v, true));
                  if (JVB_TESTING) {
                     jvbDump($v,'No config set for '.$k.': ');
                  }
            }
         }
@@ -3041,21 +3320,37 @@
            $styles = [];
            if (isset($border['radius'])) {
               $styles[] = 'border-radius: '.$border['radius'];
               $styles['border-radius'] = $border['radius'];
            }
            if (isset($border['width']) && isset($attrs['borderColor'])) {
            if (isset($border['width']) && (isset($attrs['borderColor']) || isset($border['color']))) {
               $st = $border['style'] ?? 'solid';
               $styles[] = sprintf(
                  'border: %s %s %s',
               $color = $border['color']??$attrs['borderColor'];
               $styles['border'] = sprintf(
                  '%s %s %s',
                  $border['width'],
                  $st,
                  $this->getColor($attrs['borderColor'])
                  $this->getColor($color)
               );
            } elseif (isset($border['width'])) {
               $styles[] = 'border-width: '.$border['width'];
            } elseif (isset($border['style'])) {
               $styles[] = 'border-style: '.$border['style'];
            } else {
               if (isset($border['color'])) {
                  $styles['border-color'] = $border['color'];
               }
               if (isset($border['width'])) {
                  $styles['border-width'] = $border['width'];
               }
               if (isset($border['style'])) {
                  $styles['border-style'] = $border['style'];
               }
            }
            if (JVB_TESTING) {
               unset($border['radius']);
               unset($border['width']);
               unset($border['style']);
               unset($border['color']);
               if (!empty($border)) {
                  jvbDump($border, '[getBorderStyle] Leftover styles:');
               }
            }
            return $styles;
@@ -3070,9 +3365,9 @@
               switch ($l) {
                  case 'selfStretch':
                     if ($option === 'fixed' && isset($layout['selfStretchValue'])) {
                        $styles[] = 'width: '.$layout['selfStretchValue'];
                        $styles['width'] = $layout['selfStretchValue'];
                     } elseif ($option === 'fill') {
                        $styles[] = 'flex:1';
                        $styles['flex'] = 1;
                     }
                     break;
                  default:
@@ -3099,30 +3394,41 @@
         private function getTypographyStyle(array $typography, array $attrs):array
         {
            $styles = [];
            if (isset($typography['fontSize'])) {
               $styles[] = 'font-size: '.$typography['fontSize'];
            foreach ($typography as $property => $value) {
               switch ($property) {
                  case 'fontSize':
                     $styles['font-size'] = $value;
                     break;
                  case 'fontWeight':
                     $styles['font-weight'] = $value;
                     break;
                  case 'textDecoration':
                     $styles['text-decoration'] = $value;
                     break;
                  case 'textTransform':
                     $styles['text-transform'] = $value;
                     break;
                  case 'letterSpacing':
                     $styles['letter-spacing'] = $value;
                     break;
                  case 'lineHeight':
                     $styles['line-height'] = $value;
                     break;
                  case 'fontStyle':
                     $styles['font-style'] = $value;
                     break;
                  case 'writingMode':
                     $styles['writing-mode'] = $value;
                     break;
                  case 'textAlign':
                     $styles['text-align'] = $value;
                  default:
                     if (JVB_TESTING) {
                        jvbDump($value,'[getTypographyStyle] No property set for '.$property.': ');
                     }
               }
            }
            if (isset($typography['fontWeight'])) {
               $styles[] = 'font-weight: '.$typography['fontWeight'];
            }
            if (isset($typography['textDecoration'])) {
               $styles[] = 'text-decoration: '.$typography['textDecoration'];
            }
            if (isset($typography['textTransform'])) {
               $styles[] = 'text-transform: '.$typography['textTransform'];
            }
            if (isset($typography['letterSpacing'])) {
               $styles[] = 'letter-spacing: '.$typography['letterSpacing'];
            }
            if (isset($typography['lineHeight'])) {
               $styles[] = 'line-height: '.$typography['lineHeight'];
            }
            return $styles;
         }
         private function extractElementStyles(array $elements, string $uid):void
@@ -3135,18 +3441,36 @@
                  default   => $element,
               };
               $selectors = explode(',',$selector);
               foreach ($states as $state => $rules) {
                  $css = [];
                  $fullSelector = str_starts_with($state, ':')
                     ? ".{$uid} {$selector}{$state}"
                     : ".{$uid} {$selector}";
                  if (isset($rules['color']['text'])) {
                     $css[] = 'color: '.$this->getColor($rules['color']['text']);
                  $fullSelector = array_map(function($sel) use ($uid, $state) {
                     return str_starts_with($state, ':')
                        ? ".{$uid} {$sel}{$state}"
                        : ".{$uid} {$sel}";
                  }, $selectors);
                  $fullSelector = implode(',', $fullSelector);
                  if (JVB_TESTING) {
                     jvbDump($state, 'state');
                     jvbDump($rules, 'rules');
                  }
                  if (isset($rules['color']['background'])) {
                     $css[] = 'background-color: '.$this->getColor($rules['color']['background']);
                  if (isset($rules['color']['text']) || isset($rules['text'])) {
                     $css['color'] = $this->getColor($rules['color']['text'] ?? $rules['text']);
                  }
                  if (isset($rules['color']['background']) || isset($rules['background'])) {
                     $css['background-color'] = $this->getColor($rules['color']['background']??$rules['background']);
                  }
                  //clean out possible empty values
                  $css = array_filter($css);
                  $css = array_map(function ($property, $value) {
                     return $property.': '.$value;
                  }, array_keys($css), $css);
                  if (!empty($css)) {
                     static::$pendingStyles[] = $fullSelector.' { '.implode('; ', $css).' }';
                  }
@@ -3162,31 +3486,32 @@
      return $out;
   }
      private function getDimRatioStyle(int $value, array $attrs):string
      private function getDimRatioStyle(int $value, array $attrs):array
      {
         //TODO: This likely isn't working correctly
         jvbDump($value, 'dimRatio');
         jvbDump($attrs, 'dimRatio attrs');
         $s = '';
         if (!array_key_exists('overlayColor', $attrs)) {
            $s = 'background-color: rgba(var(--base-rgb), ';
            if ($value <= 14) {
               $s .= 'var(--op-1));';
            } elseif ($value <= 28) {
               $s .= 'var(--op-2));';
            } elseif ($value <= 42) {
               $s .= 'var(--op-3));';
            } elseif ($value <= 56) {
               $s .= 'var(--op-45));';
            } elseif ($value <= 70) {
               $s .= 'var(--op-4));';
            } elseif ($value <= 84) {
               $s .= 'var(--op-5));';
            } else {
               $s .= 'var(--op-6));';
            }
//       jvbDump($value, 'dimRatio');
//       jvbDump($attrs, 'dimRatio attrs');
         $ratio = [];
         $s = array_key_exists('overlayColor', $attrs) ? 'var(--'.$attrs['overlayColor'].')' : 'var(--base)';
         $s = 'rgba('.$s.', ';
         if ($value <= 14) {
            $s .= 'var(--op-1))';
         } elseif ($value <= 28) {
            $s .= 'var(--op-2))';
         } elseif ($value <= 42) {
            $s .= 'var(--op-3))';
         } elseif ($value <= 56) {
            $s .= 'var(--op-45))';
         } elseif ($value <= 70) {
            $s .= 'var(--op-4))';
         } elseif ($value <= 84) {
            $s .= 'var(--op-5))';
         } else {
            $s .= 'var(--op-6))';
         }
         return $s;
         return ['background-color' => $s];
      }
      protected function getDataset(array $attrs):array
@@ -3224,14 +3549,18 @@
      return array_key_exists($test, $attrs) && $attrs[$test]===true;
   }
   protected function getColor(string $value):string
   protected function getColor(string $value, bool $prefix = true):string
   {
      $defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']);
      foreach ($defaults as $default) {
         if (str_starts_with($value, $default)) {
            return 'var(--'.$value.')';
            return $prefix ? 'var(--'.$value.')' : $value;
         }
      }
      //We removed the presets
      if (str_contains($value, 'var:preset')) {
         return '';
      }
      return $value;
   }