Jake Vanderwerf
5 days ago 75a097a018a0090f5902758353c578fce4aa2a25
inc/blocks/CustomBlocks.php
@@ -3,7 +3,12 @@
use DateTime;
use DOMDocument;
use JVBase\managers\CacheManager;
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;
@@ -13,12 +18,26 @@
class CustomBlocks
{
    protected CacheManager $cache;
    protected Cache $cache;
   protected static ?WP_Query $currentLoop = null;
   protected static ?int $currentQueryId = null;
   protected static array $counters = [];
   protected static ?WP_Query $originalQuery = null;
   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 = CacheManager::for('blocks', WEEK_IN_SECONDS);
      add_filter('render_block', [$this, 'render'], 999, 3);
        $this->cache = Cache::for('blocks', WEEK_IN_SECONDS);
      $this->cache->connect('post')->connect('taxonomy');
      $this->cache->flush();
      add_filter('pre_render_block', [$this, 'prerender'], 10, 3);
      add_filter('render_block', [$this, 'render'], 10, 2);
        add_action('init', [$this, 'registerBlockStyles']);
    }
@@ -62,49 +81,74 @@
                'label' => __('Callout Alt', 'jvb')
            ]
        );
        register_block_style(
            'core/separator',
            [
                'name'  =>'logo',
                'label' => __('With Logo', 'jvb')
            ]
        );
    }
    public function render(string $content, array $block, WP_Block $instance)
    {
   protected function checkMethods(?string $content, array $block, ?WP_Block $parent = null, bool $isPrerender = false):?string
   {
      $blockName = $this->sanitizeBlockName($block);
        $method = 'render_'.$blockName;
      $base = ($isPrerender) ? 'prerender_' : 'render_';
      $method = $base.$blockName;
      $function = BASE.$method;
      if (function_exists($function)) {
         return $function($block, $content);
//       return $this->cache->remember(
//          $block,
//          function () use ($function, $block, $content) {
//             return $function($block, $content);
//          }
//       );
         return $function($block, $content, $parent);
      } else if (method_exists($this, $method)) {
         return $this->$method($block, $content);
//       return $this->cache->remember(
//          $block,
//          function () use ($method, $block, $content) {
//             return $this->$method($block, $content);
//          }
//       );
        } else if (!empty($block['blockName'])){
         //TESTING
         $ignore = [
            'core/null',
            'core/post-title',
            'core/list-item',
            'core/site-title',
            'jvb/forms'
         ];
//       if (!in_array($block['blockName'], $ignore)) {
//          jvbDump('No method found for '.print_r($block['blockName'], true));
//       }
         $content = $this->$method($block, $content, $parent);
         return $isPrerender ? $this->maybeOutputCustomStyles().$content : $content;
      } elseif (!empty($blockName) && JVB_TESTING) {
         if (!in_array($block['blockName'], $this->getIgnore($isPrerender))) {
            jvbDump('No method found for '.print_r($block['blockName'], true));
         }
      }
        if ($block['blockName'] === 'jvb/feed') {
            // Enqueue the feed block script (it will automatically load dependencies)
            $this->localize_feedblock();
        }
        return $content;
      return $content;
   }
      protected function getIgnore(bool $isPrerender):array
      {
         //Ignore for both
         $base = [
            'core/null'
         ];
         if ($isPrerender) {
            $base = array_merge($base, [
               'core/query-pagination',
               'core/query-pagination-previous',
               'core/query-pagination-next',
               'core/query-pagination-numbers',
               'core/query',
               'core/calendar',
               'core/archives',
            ]);
         } else {
            $base = array_merge($base, [
            ]);
         }
         return $base;
      }
   public function prerender(?string $content, array $block, ?WP_Block $parent = null):?string
   {
      $result = $this->checkMethods($content, $block, $parent, true);
      return $result;
   }
    public function render(string $content, array $block):string
    {
      if ($block['blockName'] === 'jvb/feed') {
         // Enqueue the feed block script (it will automatically load dependencies)
         $this->localize_feedblock();
      }
      if ($block['blockName'] === 'jvb/forms') {
         wp_enqueue_style('jvb-form');
      }
      return $this->checkMethods($content, $block);
    }
    /***********************************
@@ -123,8 +167,10 @@
     */
    protected function render_core_button(array $block):string
    public function prerender_core_button(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'Button');
//    jvbDump($parent, 'Parent');
      preg_match('/href="([^"]*)"/', $block['innerHTML'], $url);
      preg_match('/>([^<]*)<\/a>/', $block['innerHTML'], $label);
@@ -133,75 +179,135 @@
      }
      $icon = '';
      if (str_contains($url[1], 'google.com/maps')) {
         $icon = 'google-logo';
         $icon = jvbIcon('google-logo');
      }
      if (str_contains($url[1], 'maps.apple.com')) {
         $icon = 'apple-logo';
         $icon = jvbIcon('apple-logo');
      }
      if ($icon !== '') {
         return sprintf(
            '<li%s><a href="%s" title="Find Us On %s">%s Maps</a></li>',
            $this->getClassesAndStyles($block['attrs']),
            $this->getClassesAndStyles($block['attrs']??[]),
            esc_url($url[1]),
            esc_html($label[1]),
            jvbIcon($icon)
            $icon
         );
      }
      return sprintf(
         '<li%s><a href="%s">%s</a></li>',
         $this->getClassesAndStyles($block['attrs']),
         $this->getClassesAndStyles($block['attrs']??[]),
         esc_url($url[1]),
         esc_html($label[1])
      );
    }
    protected function render_core_buttons(array $block):string
    public function prerender_core_buttons(array $block, ?string $content, ?WP_Block $parent):?string
    {
        return '<ul'.$this->getClassesAndStyles($block['attrs'], ['buttons','row']).'>'.
               $this->innerBlocks($block).'</ul>';
//    jvbDump($block, 'buttons');
//    jvbDump($parent, 'Parent');
        return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($block['attrs']??[], ['buttons','row']),
               $this->innerBlocks($block)
      );
    }
    protected function render_core_column(array $block):string
    public function prerender_core_column(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'column');
//    jvbDump($parent, 'Parent');
        $styles = (array_key_exists('attrs', $block) &&
                   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)
      );
    }
    protected function render_core_columns(array $block):string
    public function prerender_core_columns(array $block, ?string $content, ?WP_Block $parent):?string
    {
        return '<section'.
               $this->getClassesAndStyles($block['attrs'], ['columns']).'>'.
               $this->innerBlocks($block).'</section>';
      jvbDump($block, 'columns');
      $attrs = $block['attrs']??[];
      $tagName = array_key_exists('tagName', $attrs) ? $attrs['tagName'] : 'section';
      $classes = ['row', 'nowrap'];
      if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true){
         $classes[] = 'stack-small';
      }
        return sprintf(
         '<%s%s>%s</%s>',
         $tagName,
            $this->getClassesAndStyles($attrs, $classes),
            $this->innerBlocks($block).'</section>',
         $tagName
      );
    }
    //core_comment_template
    protected function render_core_group(array $block):string
    public function prerender_core_group(array $block, ?string $content, ?WP_Block $parent):?string
    {
        $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
        $classes = ($tag === 'main') ?
            $this->getClassesAndStyles($block['attrs']) :
            $this->getClassesAndStyles($block['attrs'], ['group']);
        return '<'.$tag.$classes.'>'.$this->innerBlocks($block).'</'.$tag.'>';
//    jvbDump($block, 'group');
//    jvbDump($parent, 'Parent');
        $tag = (array_key_exists('tagName', $block['attrs']??[])) ? $block['attrs']['tagName'] : 'div';
        return sprintf(
         '<%s%s>%s</%s>',
         $tag,
         $tag === 'main' ? '' : $this->getClassesAndStyles($block['attrs']??[], ['group']),
         $this->innerBlocks($block),
         $tag
      );
    }
    //core_home_link
    //core_more
    //core_nextpage
   public function prerender_core_nextpage(array $block, ?string $content, ?WP_Block $parent):?string
   {
    protected function render_core_separator(array $block):string
      return str_replace('</a>', '</a></li>',str_replace('<a', '<li><a', wp_link_pages([
         'before'       => '<nav class="pagination x-btw"><ul>',
         'after'        => '</ul></nav>',
         'nextpagelink' => __('<span>Next </span>'.jvbIcon('caret-circle-right'), 'jvb'),
         'previouspagelink'   => __(jvbIcon('caret-circle-left').'<span> Previous</span>', 'jvb'),
         'next_or_number'=> 'next',
         'echo'         => false
      ])));
   }
    public function prerender_core_separator(array $block, ?string $content, ?WP_Block $parent):?string
    {
        return '<hr'.$this->getClassesAndStyles($block['attrs']).'>';
//    jvbDump($block, 'separator');
//    jvbDump($parent, 'Parent');
      $attrs = $block['attrs']??[];
      $logo = '';
      if (array_key_exists('className', $attrs) && $attrs['className'] === 'is-style-logo'){
         $logo = apply_filters('jvbSeparatorLogo', 'logo');
         if (!empty($logo)) {
            $logo = jvbIcon($logo);
         }
      }
        return sprintf(
         '<hr%s>',
         $this->getClassesAndStyles($attrs),
//       $logo
      );
    }
    protected function render_core_spacer(array $block):string
    public function prerender_core_spacer(array $block, ?string $content, ?WP_Block $parent):?string
    {
        return '<div'.$this->getClassesAndStyles($block['attrs'], ['spacer'], ['height:2rem']).
               ' aria-hidden="true"></div>';
//    jvbDump($block, 'spsacer');
//    jvbDump($parent, 'Parent');
        return sprintf(
         '<div%s aria-hidden="true"></div>',
         $this->getClassesAndStyles($block['attrs']??[], ['spacer'], ['height:2rem'])
      );
    }
    //core_table_of_contents
    //core_text_columns
@@ -214,21 +320,31 @@
     * Media Blocks
     */
    //core_audio
    protected function render_core_cover(array $block):string
   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'] ?? [];
        $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 = '';
@@ -241,40 +357,108 @@
         $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']);
      $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']??[];
    protected function render_core_gallery(array $block):string
      $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
    {
        return '<ul'.$this->getClassesAndStyles($block['attrs'], ['gallery']).'>'.
               $this->innerBlocks($block,'<li>', '</li>').
               '</ul>';
//    jvbDump($block, 'gallery');
      $attrs = $block['attrs']??[];
//    jvbDump($parent, 'Parent');
      static::$renderGallery = true;
        return sprintf(
      '<ul%s>%s</ul>',
         $this->getClassesAndStyles($attrs, ['gallery']),
         $this->innerBlocks($block,'<li>', '</li>')
      );
    }
    protected function render_core_image(array $block):string
    public function prerender_core_image(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'image');
//    jvbDump($parent, 'Parent');
        $ID = $this->imageID('', $block);
        if (!$ID) {
            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>' .
@@ -282,38 +466,111 @@
                wp_get_attachment_caption($ID) .
            '</figcaption>' :
            '<figcaption>' . $title . '</figcaption>';
      $size = $attrs['sizeSlug'] ?? 'large';
      $img = $this->imageLink(true, $ID, 'tiny', $size);
        return '<figure'.
               $this->getClassesAndStyles($block['attrs']).'>'.
               $this->imageLink(true, $ID) .
               $caption.'</figure>';
      $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,
      );
    }
    protected function render_core_media_text(array $block):string
    public function prerender_core_media_text(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'media text');
//    jvbDump($parent, 'Parent');
        $ID = $this->imageID('', $block);
        $imgLink = ($ID) ? $this->imageLink(true, $ID) : '';
      $attrs = $block['attrs']??[];
      $size = array_key_exists('mediaSizeSlug', $attrs) ? $attrs['mediaSizeSlug'] : 'large';
        $imgLink = ($ID) ? $this->imageLink(true, $ID, 'tiny', $size) : '';
        $inner = $this->innerBlocks($block);
      $classes = ['media-text', 'row'];
      if (array_key_exists('isStackedOnMobile', $block['attrs'])) {
         $classes[] = 'nowrap';
      $classes = ['media-text', 'row', 'nowrap'];
      if (!array_key_exists('isStackedOnMobile', $attrs) || $attrs['isStackedOnMobile'] === true) {
         $classes[] = 'stack-small';
      }
        $content = '<div'.$this->getClassesAndStyles($block['attrs'], $classes).'>';
        $content .= (array_key_exists(
            'mediaPosition',
            $block['attrs']
        ) && $block['attrs']['mediaPosition'] == 'right') ?
            '<div>'.$inner.'</div><figure>'.$imgLink.'</figure>' :
            '<figure>'.$imgLink.'</figure><div>'.$inner.'</div>';
        $content .= '</div>';
        return $content;
      $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>%s</figure>',
            $inner,$figClasses, $imgLink
         ) : sprintf(
            '<figure%s>%s</figure><div>%s</div>',
            $figClasses,$imgLink, $inner
         );
        return sprintf(
         '<div%s>%s</div>',
         $this->getClassesAndStyles($attrs, $classes),
         $inside
      );
    }
    //core_video
   public function prerender_core_video(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'video');
//    jvbDump($parent, 'Parent');
      $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);
      $inside = $this->inside($block);
      return sprintf('<figure%s>%s</figure>',
         $this->getClassesAndStyles($attrs),
         $inside
      );
   }
    /**
     * Reusable blocks
@@ -323,40 +580,93 @@
    /**
     * Text Blocks
    */
    //render_core_code
    //render_core_details
    //render_core_footnotes
    //render_core_classic
    protected function render_core_heading(array $block):string
    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,
      );
    }
   protected function render_core_list(array $block):string
   public function prerender_core_list(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $tag = (array_key_exists('ordered', $block['attrs'])) ? 'ol' : 'ul';
      return '<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.$this->innerBlocks($block).'</'.$tag.'>';
//    jvbDump($block, 'list');
//    jvbDump($parent, 'Parent');
      $tag = (array_key_exists('ordered', $block['attrs']??[])) ? 'ol' : 'ul';
      return sprintf(
         '<%s%s>%s</%s>',
         $tag,
         $this->getClassesAndStyles($block['attrs']??[]),
         $this->innerBlocks($block),
         $tag
      );
   }
// protected function render_core_list_item(array $block):string
// public function prerender_core_list_item(array $block):string
// {
//    return '<li'.$this->getClassesAndStyles($block['attrs']).'>'.$this->inside($block).'</li>';
// }
    //render_core_missing
    //prerender_core_missing
    protected function render_core_paragraph(array $block):string
    public function prerender_core_paragraph(array $block, ?string $content, ?WP_Block $parent):?string
    {
        return '<p'.$this->getClassesAndStyles($block['attrs']).'>'.
               $this->inside($block, 'p').
               '</p>';
      jvbDump($block, 'paragraph');
//    jvbDump($parent, 'Parent');
      $inside = $this->inside($block);
        return empty($inside) ? '' : sprintf(
      '<p%s>%s</p>',
         $this->getClassesAndStyles($block['attrs']??[]),
         $inside
      );
    }
   protected function render_core_quote(array $block): string
   public function prerender_core_quote(array $block, ?string $content, ?WP_Block $parent): ?string
   {
//    jvbDump($block, 'quote');
//    jvbDump($parent, 'Parent');
      $innerHTML = $block['innerHTML'];
      // Extract cite content first
@@ -364,20 +674,25 @@
      $citeHtml = ($cite === '') ? '' : '<cite>—&emsp;'.$cite.'</cite>';
      // Get the blockquote content
      $content = $this->inside($block, 'blockquote');
      $content = $this->innerBlocks($block);
      // Remove the cite element from content if it exists
      if ($cite !== '') {
         $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
      );
   }
   protected function render_core_pullquote(array $block): string
   public function prerender_core_pullquote(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'pullquote');
//    jvbDump($parent, 'Parent');
      $innerHTML = $block['innerHTML'];
      // Extract cite content first
@@ -391,58 +706,151 @@
      if ($cite !== '') {
         $content = $this->stripTagContents('cite', $content);
      }
      $content = apply_filters('the_content', $content);
      $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
      );
   }
    //render_core_table
    //render_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
     */
    //core_avatar
    //core_loginout
   public function prerender_core_loginout(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $action = is_user_logged_in() ? 'logout' : 'login';
      $attrs = $block['attrs'];
      $redirect = '';
      if (array_key_exists('redirectToCurrent', $attrs) && $attrs['redirectToCurrent']) {
         global $wp;
         $redirect = get_home_url(null, $wp->request);
      }
      if (array_key_exists('displayLoginAsForm', $attrs) && $attrs['displayLoginAsForm']) {
         LoginManager::getInstance()->setAction($action);
         return LoginManager::getInstance()->renderLoginForm($action, $redirect, '<h2>Login</h2>');
      }
      return sprintf(
         '<a href="%s"%s>%s</a>',
         wp_login_url($redirect),
         $this->getClassesAndStyles($attrs),
         $action === 'login' ? jvbIcon('sign-in').'<span>Log in</span>' : jvbIcon('sign-out').'<span>Logout</span>'
      );
   }
    //core_pattern
    protected function render_core_site_logo(array $block, string $content):string
    public function prerender_core_site_logo(array $block, ?string $content, ?WP_Block $parent = null):?string
    {
//    jvbDump($block, 'site logo');
//    jvbDump($parent, 'Parent');
      $attrs = $block['attrs']??[];
        $open = $close = '';
        if (!is_home() && !is_front_page()) {
            $open = '<a href="'.get_home_url().'" rel="home">';
        if ((!is_home() && !is_front_page()) && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) {
            $open = '<a href="'.get_home_url().'" rel="home" class="logo">';
            $close = '</a>';
        }
        $img = get_theme_mod('custom_logo');
        $img = $this->image($img, 'tiny', 'thumbnail');
        $img = str_replace('<img', '<img'.$this->getClassesAndStyles($block['attrs']), $img);
        $img = sprintf(
         '<figure%s>%s</figure>',
         $this->getClassesAndStyles($attrs, ['logo']),
         $this->image($img, 'tiny', 'thumbnail')
      );
        return $open.$img.$close;
    }
    //core_site_title_tagline
   public function prerender_core_site_tagline(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $tagline = get_bloginfo('description');
    protected function render_core_site_title(array $block, string $content):string
      return empty($tagline) ? '' : sprintf(
         '<p%s>%s</p>',
         $this->getClassesAndStyles($block['attrs']??[], ['tagline']),
         $tagline
      );
   }
    public function prerender_core_site_title(array $block, ?string $content, ?WP_Block $parent):?string
    {
        $tag = (array_key_exists('level', $block['attrs'])) ? $block['attrs']['level'] : 1;
//    jvbDump($block, 'site title');
//    jvbDump($parent, 'Parent');
      $attrs = $block['attrs']??[];
        $tag = (array_key_exists('level', $attrs)) ? $attrs['level'] : 1;
        $tag = ($tag == 0) ? 'p' : 'h'.$tag;
        $open = $close = '';
        if (!is_front_page()) {
            $open = '<a href="' . get_home_url() . '" rel="home">';
        if (!is_front_page() && (!array_key_exists('isLink', $attrs) || $attrs['isLink'] === true)) {
            $open = sprintf(
            '<a href="%s" rel="home">',
            get_home_url()
         );
            $close = '</a>';
        }
        $class = ($tag === 'p') ?
            $this->getClassesAndStyles($block['attrs'], ['title']) :
            $this->getClassesAndStyles($block['attrs']);
            $this->getClassesAndStyles($block['attrs']??[], ['title']) :
            $this->getClassesAndStyles($block['attrs']??[]);
        return '<'.$tag.$class.'>'.
               $open.
               get_bloginfo('name').
               $close.
               '</'.$tag.'>';
        return sprintf(
         '<%s%s>%s%s%s</%s>',
         $tag,
         $class,
         $open,
         get_bloginfo('name'),
         $close,
         $tag
      );
    }
    /**
@@ -465,90 +873,144 @@
    /**
     * Theme Navigation Blocks
     */
    protected function render_core_navigation(array $block, string $content):string
    public function prerender_core_navigation(array $block, ?string $content, ?WP_Block $parent):?string
    {
        $ID = (array_key_exists('ref', $block['attrs'])) ? $block['attrs']['ref'] : false;
//    jvbDump($block, 'navigation');
//    jvbDump($parent, 'Parent');
//    jvbDump($block, 'navigation');
        $ID = (array_key_exists('ref', $block['attrs']??[])) ? $block['attrs']['ref'] : false;
        if (empty($block['innerBlocks']) && $ID && get_post($ID)) {
            $block['innerBlocks'] = parse_blocks(get_post($ID)->post_content);
        }
      $attrs = $block['attrs']??[];
        $toggle = (array_key_exists('overlayMenu', $block['attrs'])
                   && $block['attrs']['overlayMenu'] == 'never') ?
            '':
            '<button class="toggle main"
            data-action="toggle-menu"
            aria-label="Open Menu"
            aria-controls="navigation-' .$ID. '"
            aria-expanded="false">'.
            jvbIcon('list', ['title'=>'Toggle Menu']).
            jvbIcon('x', ['title'=>'Toggle Menu']).
            '</button>';
        $class = ($toggle === '') ?
            $this->getClassesAndStyles($block['attrs'], ['mobile']) :
            $this->getClassesAndStyles($block['attrs']);
        $helpmenu = (get_the_title($ID) === 'Main') ?
            '<nav><ul>'.jvbNotificationMenu().jvbHelpMenu().'</ul></nav>' :
            '';
      $toggle = '';
      $classes = [];
      if (!array_key_exists('overlayMenu', $attrs) || $attrs['overlayMenu'] !== 'never') {
         $toggle = sprintf(
            '<button class="toggle main"
            data-action="toggle-menu"
            aria-label="Open Menu"
            aria-controls="navigation-%d"
            aria-expanded="false">%s%s</button>',
            $ID,
            jvbIcon('list'),
            jvbIcon('x')
         );
         $classes[] = 'mobile';
         if (array_key_exists('overlayMenu', $attrs) && $attrs['overlayMenu'] === 'always') {
            $classes[] = 'always';
         }
      }
      if (!array_key_exists('layout', $attrs)) {
         $classes[] = 'left';
         $classes[] = 'row';
      }
      $class = $this->getClassesAndStyles($attrs, $classes);
      $helpmenu = '';
      $title = get_the_title($ID);
      $isMain = false;
      if ($title === 'Main') {
         $isMain = true;
         $helpmenu = sprintf(
            '<nav><ul>%s%s</ul></nav>',
            jvbNotificationMenu(),
            jvbHelpMenu()
         );
      }
      //Allows to add custom items to a menu, based on the menu name
      $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, get_the_title($ID));
      $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), get_the_title($ID), $block));
      $helpmenu = apply_filters('jvbMenuExtraAfter', $helpmenu, $title, $ID);
      $main = trim(apply_filters('jvbMenuExtra', $this->innerBlocks($block), $title, $block));
      $main = str_starts_with($main, '<ul') ? $main : '<ul>'.$main.'</ul>';
      $main = str_starts_with($main, '<ul') ? $main : sprintf('<ul>%s</ul>',$main);
        return '<nav'.$class.' id="navigation-' . $ID . '"aria-label="Navigation">
            <span class="screen-reader-text">
      $skipToContent = $isMain ? '<span class="screen-reader-text">
                <a href="#content">Skip to Content</a>
            </span>' .
               $toggle .
            $main.
         '</nav>'.$helpmenu;
            </span>' : '';
        return sprintf(
         '<nav%s id="navigation-%d"aria-label="Navigation">
            %s%s%s</nav>%s',
         $class,
         $ID,
         $skipToContent,
         $toggle,
         $main,
         $helpmenu
      );
    }
    protected function render_core_navigation_link(array $block):string
    public function prerender_core_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'navigation link');
//    jvbDump($parent, 'Parent');
        global $wp;
        $url = (str_starts_with($block['attrs']['url'],'/')) ?
            home_url($block['attrs']['url']) :
            $block['attrs']['url'];
      if (!array_key_exists('attrs', $block)) {
         return '';
      }
      $attrs = $block['attrs']??[];
        $url = (str_starts_with($attrs['url'],'/')) ?
            home_url($attrs['url']) :
            $attrs['url'];
        $current = (home_url($wp->request.'/') == $url);
      $attrs['url'] = $url;
        $classes = ($current) ?
            $this->getClassesAndStyles($block['attrs'], ['current']):
            $this->getClassesAndStyles($block['attrs']);
            $this->getClassesAndStyles($attrs, ['current']):
            $this->getClassesAndStyles($attrs);
        $aria = '';
        if ($current) {
            $aria = ' aria-current="page"';
        }
        $linkOpen = $this->build_navigation_link($block['attrs'], $aria);
        $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']
      );
    }
    protected function render_core_navigation_submenu(array $block):string
    public function prerender_core_navigation_submenu(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'navigation submenu');
//    jvbDump($parent, 'Parent');
        global $wp;
        $url = (str_starts_with($block['attrs']['url'],'/')) ?
            home_url($block['attrs']['url']) :
            $block['attrs']['url'];
        $current = (home_url($wp->request) == $url);
      $attrs = $block['attrs']??[];
      $attrs['url'] = $url;
        $classes = ($current) ?
            $this->getClassesAndStyles($block['attrs'], ['has-submenu', 'current']):
            $this->getClassesAndStyles($block['attrs'], ['has-submenu']);
            $this->getClassesAndStyles($attrs, ['has-submenu', 'current']):
            $this->getClassesAndStyles($attrs, ['has-submenu']);
        $aria = '';
        if ($current) {
            $aria = ' aria-current="page"';
        }
        $id = sanitize_title($block['attrs']['label']);
        $linkOpen = $this->build_navigation_link($block['attrs'], $aria);
        $content = '<li'.$classes.'>'.$linkOpen.$block['attrs']['label'].
                   '</a><button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open '.$block['attrs']['label'].' Submenu" aria-expanded="false" aria-controls="'.$id.'-submenu">'.
                   jvbIcon('caret-down', ['title'=>'Toggle Submenu']).
                   '</button><ul class="submenu" id='.$id.'-submenu">';
        $linkOpen = $this->buildNavigationLink($attrs, $aria);
        $content = sprintf(
         '<li%s>%s%s</a>
                  <button class="toggle" data-action="toggle-submenu" title="Toggle Submenu" aria-label="Open %s Submenu" aria-expanded="false" aria-controls="%s-submenu">
                           %s
                  </button>
                  <ul class="submenu" id=%s-submenu">',
         $classes,
         $linkOpen,
         $attrs['label'],
         $attrs['label'],
         $id,
         jvbIcon('caret-down', ['title'=>'Toggle Submenu']),
         $id
      );
        $content .= $this->innerBlocks($block);
        $content .= '</ul></li>';
@@ -556,9 +1018,8 @@
        return $content;
    }
    protected function build_navigation_link(array $attrs, string $aria):string
    protected function buildNavigationLink(array $attrs, string $aria):string
    {
        global $wp;
        $url =(str_starts_with($attrs['url'],'/')) ?
            home_url($attrs['url']) :
            $attrs['url'];
@@ -586,267 +1047,902 @@
               break;
            }
        }
        return '<a href="'.$url.'"'.$aria.$rel.$target.$title.'>';
        return sprintf(
         '<a href="%s"%s%s%s%s>',
         $url,
         $aria,
         $rel,
         $target,
         $title
      );
    }
    /**
     * Theme Query Blocks
     */
    //core_post_author
    //core_post_author_biography
    //core_post_author_name
    protected function render_core_post_content(array $block, string $content = ''):string
    {
        $tag = (array_key_exists('tagName', $block['attrs'])) ?
   public function prerender_core_post_author(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $attrs = $block['attrs'] ?? [];
      $size = 96;
      if (array_key_exists('avatarSize',$attrs) && is_int($attrs['avatarSize'])) {
         $size = $attrs['avatarSize'];
      }
      $byline = $aOpen = $aClose = $avatar = $bio = '';
      global $post;
      $user = get_userdata($post->post_author);
      if (!array_key_exists('showAvatar', $attrs) || $this->checkAttrs('showAvatar', $attrs)){
         $avatar = get_avatar($post->post_author, $size);
      }
      if (!array_key_exists('showBio', $attrs) || $this->checkAttrs('showBio', $attrs)) {
         $bio = wpautop($user->description);
      }
      $target = '';
      if (array_key_exists('linkTarget', $attrs) && $attrs['linkTarget']=== '_blank') {
         $target = ' target="_blank"';
      }
      if ($this->checkAttrs('isLink', $attrs)) {
         $aOpen = sprintf(
            '<a href="%s"%s>',
            get_author_posts_url($post->post_author),
            $target
         );
         $aClose = '</a>';
      }
      if (array_key_exists('byline', $attrs)) {
         $byline = sprintf(
            '<small>%s</small> â€” ',
            $attrs['byline']
         );
      }
      $name = $user->display_name;
      return sprintf(
         '<div%s>%s%s%s<p>%s%s%s%s</p>%s</div>',
         $this->getClassesAndStyles($attrs, ['row','nowrap']),
         $aOpen,
         $avatar,
         $aClose,
         $aOpen,
         $byline,
         $name,
         $aClose,
         $bio
      );
   }
    //core_post_author_biography
   public function prerender_core_post_author_name(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $attrs = $block['attrs']??[];
      global $post;
      $aOpen = $aClose = '';
      if ($this->checkAttrs('isLink', $attrs)) {
         $aOpen = sprintf(
            '<a href="%s" rel="author">',
            get_author_posts_url($post->post_author)
         );
         $aClose = '</a>';
      }
      $author = get_userdata($post->post_author);
      return sprintf(
         '<p%s>%s%s%s</p>',
         $this->getClassesAndStyles($attrs, ['author']),
         $aOpen,
         $author->display_name,
         $aClose
      );
   }
    public function prerender_core_post_content(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'post content');
//    jvbDump($parent, 'Parent');
        $tag = (array_key_exists('tagName', $block['attrs']??[])) ?
            $block['attrs']['tagName'] :
            'main';
        if ($content == '') {
         global $post;
         $block['innerBlocks'] = parse_blocks($post->post_content);
         return $this->innerBlocks($block);
         if(is_singular()) {
            global $post, $page;
            $pages = explode('<!--nextpage-->', $post->post_content);
            $currentContent = $pages[max(0, $page - 1)] ?? $pages[0];
            if ($page > 1 && !str_contains($currentContent, '<!--nextpage-->')) {
               $currentContent = str_replace('<!-- /wp:nextpage -->','', $currentContent);
               $currentContent .= '
               <!-- wp:nextpage -->
               <!--nextpage-->
               <!-- /wp:nextpage -->';
            }
            $block['innerBlocks'] = parse_blocks($currentContent);
            $result = $this->innerBlocks($block);
         }else {
            $result = '';
         }
        } else {
            return $this->inside($block, $tag, $content);
            $result = $this->innerBlocks($block);
        }
      if(static::$renderGallery) {
         add_action('wp_footer', 'jvbRenderGallery');
      }
      return apply_filters('jvb_post_content_output', $result, $block);
    }
    //core_post_date
   protected function render_core_post_date(array $block):string
   public function prerender_core_post_date(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $postDate = get_the_date('c');
      return '<time datetime="'.$postDate.'" itemprop="datePublished"'.$this->getClassesAndStyles($block['attrs']).'>'.get_the_date().'</time>';
//    jvbDump($block, 'post date');
//    return null;
      $attrs = $block['attrs']??[];
      $postDate = null;
      $itemProp = 'datePublished';
      $format = array_key_exists('format', $attrs) ? $attrs['format'] : 'M d, Y';
      $dateFormat = null;
      if (array_key_exists('displayType', $attrs)) {
         switch ($attrs['displayType']) {
            case 'displayType':
               $postDate = get_post_modified_time('c');
               $dateFormat = get_post_modified_time($format);
               $itemProp = 'dateModified';
               break;
         }
      }
      $postDate = is_null($postDate) ? get_the_date('c') : $postDate;
      $dateFormat = is_null($dateFormat) ? get_the_date($format) : $dateFormat;
      $aOpen = $aClose = '';
      if ($this->checkAttrs('isLink', $attrs) && !is_singular()) {
         $aOpen = sprintf(
            '<a href="%s">',
            get_the_permalink()
         );
         $aClose = '</a>';
      }
//    jvbDump($parent, 'Parent');
      return sprintf(
         '<time datetime="%s" itemprop="%s"%s>%s%s%s</time>',
         $postDate,
         $itemProp,
         $this->getClassesAndStyles($attrs),
         $aOpen,
         $dateFormat,
         $aClose
      );
   }
    //core_post_excerpt
    protected function render_core_post_featured_image(array $block):string
   public function prerender_core_post_excerpt(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $attrs = $block['attrs']??[];
      $moreText = array_key_exists('moreText', $attrs) ? $attrs['moreText'] : 'Read more '.jvbIcon('arrow-circle-right');
      $showMoreOnNewLine = !array_key_exists('showMoreOnNewLine', $attrs) || $this->checkAttrs('showMoreOnNewLine', $attrs);
//    jvbDump($block);
//    jvbDump($showMoreOnNewLine);
      $excerpt = array_filter(explode('<p>',wpautop(get_the_excerpt())));
      $classes = $this->getClassesAndStyles($attrs);
      $excerpt = array_map(function ($line) use ($classes) {
         return sprintf(
            '<p%s>%s',
            $classes,
            $line
         );
      }, $excerpt);
      if (!empty($moreText)) {
         if ($showMoreOnNewLine) {
            $excerpt[] = sprintf(
               '<p%s><a href="%s" class="read-more">%s</a></p>',
               $classes,
               get_the_permalink(),
               $moreText
            );
         } else {
            $last = array_key_last($excerpt);
            $excerpt[$last] = str_replace('</p>', sprintf('<a href="%s" class="read-more">%s</a>',
            get_the_permalink(),
            $moreText), $excerpt[$last]);
         }
      }
      return implode('',$excerpt);
   }
    public function prerender_core_post_featured_image(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'featured image');
//    jvbDump($parent, 'Parent');
      global $post;
      $attrs = $block['attrs']??[];
      $ID = get_post_thumbnail_id($post->ID);
      $aOpen = $aClose = '';
      if(!is_single($ID)) {
      if(!is_single($post->ID) && $this->checkAttrs('isLink', $attrs)) {
         $aOpen = '<a href="'.get_the_permalink($post->ID).'">';
         $aClose = '</a>';
      }
        return $aOpen.'<figure'.$this->getClassesAndStyles($block['attrs']).'>'.
               apply_filters('jvbCoreFeaturedImage', $this->image($ID), $post->post_type).
               '</figure>'.$aClose;
      $img = apply_filters('jvbCoreFeaturedImage', '', $post->post_type, $attrs);
      if (empty($img)) {
         $img = $this->image($ID);
      }
      $aspectRatio = $attrs['aspectRatio']??false;
      if ($aspectRatio) {
         $img = str_replace('<img', sprintf(
            '<img style="aspect-ratio:%s;"',
            $aspectRatio
         ), $img);
      }
        return !empty($img) ? sprintf(
         '<figure%s>%s%s%s</figure>',
         $this->getClassesAndStyles($attrs),
         $aOpen,
         $img,
         $aClose,
      ):'';
    }
    //core_post_navigation_link
    //core_post_template
    //core_post_terms
   protected function render_core_post_terms(array $block):string
   public function prerender_core_post_navigation_link(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $attr = $block['attrs'];
      $isPrevious = $attr['type']==='previous';
      $title = array_key_exists('showTitle', $attr)&&$attr['showTitle'];
      $linkLabel = array_key_exists('linkLabel', $attr)&&$attr['linkLabel'];
      $label = array_key_exists('label', $attr) ? $attr['label'] : '';
      $arrow = '';
      if (array_key_exists('arrow', $attr)) {
         $dir = $isPrevious ? 'left' : 'right';
         $icon = match($attr['arrow']) {
            'arrow'  => 'arrow-square-',
            'chevron' => 'caret-circle-'
         };
         if ($icon) {
            $arrow = jvbIcon($icon.$dir);
         }
      }
//    return $content;
      $linkedLabel = $unlinkedLabel = '';
      if (!empty($label)) {
         $linkedLabel = $linkLabel ? $label : '';
         $unlinkedLabel = $linkLabel ? '' : $label;
      }
      if ($title) {
         $linkedLabel .=' %title';
      } elseif (!empty($label)) {
         $linkedLabel = $label;
         $unlinkedLabel = '';
      } else {
         $linkedLabel = $isPrevious ? 'Previous' : 'Next';
         $unlinkedLabel = '';
      }
      $result = $isPrevious ?
         get_previous_post_link(
            $arrow.$unlinkedLabel.' %link',
            $linkedLabel
         ) :
         get_next_post_link(
            '%link '.$unlinkedLabel.$arrow,
            $linkedLabel
         );
      return sprintf('<div%s>%s</div>',
         $this->getClassesAndStyles($attr,['row', 'nowrap']),
         $result
      );
   }
    //core_post_template
   public function prerender_core_post_template(array $block, ?string $content):?string
   {
      $inner = '';
      if (!static::$currentLoop) {
         jvbDump('No loop stored');
         return $content;
      }
      if (static::$currentLoop->have_posts()) {
         while (static::$currentLoop->have_posts()) {
            static::$currentLoop->the_post();
            $inner .= sprintf(
               '<li>%s</li>',
               $this->innerBlocks($block, '','',$block)
            );
         }
      }
      return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($block['attrs']??[], ['loop']),
         $inner
      );
   }
    //core_post_terms
   public function prerender_core_post_terms(array $block, ?string $content, ?WP_Block $parent):?string
   {
      if (!array_key_exists('attrs', $block)) {
         return '';
      }
      $terms = get_the_terms(get_the_ID(), $block['attrs']['term']);
      $attrs = $block['attrs']??[];
      $out = '';
      if ($terms && !is_wp_error($terms)) {
         $out = '<ul class="term-list">';
            if (array_key_exists('prefix', $block['attrs'])) {
               $out .= '<li>'.$block['attrs']['prefix'].'</li>';
         $out = sprintf(
            '<ul%s>',
            $this->getClassesAndStyles($attrs, ['term-list', 'row', 'left'])
         );
            if (array_key_exists('prefix', $attrs)) {
               $out .= sprintf(
                  '<li class="prefix">%s</li>',
                  $attrs['prefix']
               );
            }
            foreach($terms as $term) {
               $out .= '<li><a href="'.get_term_link($term).'" rel="tag">'.$term->name.'</a></li>';
               $out .= sprintf(
                  '<li><a href="%s" rel="tag">%s</a></li>',
                  get_term_link($term),
                  html_entity_decode($term->name)
               );
            }
         if (array_key_exists('suffix', $block['attrs'])) {
            $out .= '<li>'.$block['attrs']['suffix'].'</li>';
         if (array_key_exists('suffix', $attrs)) {
            $out .= sprintf(
               '<li class="suffix">%s</li>',
               $attrs['suffix']
            );
         }
         $out .= '</ul>';
      }
      return $out;
   }
    //core_post_time_to_read
    protected function render_core_post_title(array $block):string
    public function prerender_core_post_title(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($parent, 'Parent');
        $open = $close = '';
        if (array_key_exists('isLink', $block['attrs'])) {
            $rel = (array_key_exists('rel', $block['attrs'])) ?
      $attrs = $block['attrs']??[];
        if ($this->checkAttrs('isLink', $attrs)) {
            $rel = (array_key_exists('rel', $attrs)) ?
                ' rel="'.$block['attrs']['rel'].'"' :
                '';
            $target = (array_key_exists('linkTarget', $block['attrs'])) ?
            $target = (array_key_exists('linkTarget', $attrs)) ?
                ' target="'.$block['attrs']['linkTarget'].'"' :
                '';
            $open = '<a href="' . get_the_permalink() . '"' . $rel . $target . '>';
            $close = '</a>';
        }
        if (is_singular(BASE.'partner')) {
            $open .= '<small>edmonton.ink partner:</small> ';
        }
        $level = (array_key_exists('attrs', $block) &&
                  array_key_exists('level', $block['attrs'])) ?
            $block['attrs']['level'] :
            2;
        return '<h'.$level.$this->getClassesAndStyles($block['attrs']).'>'.
               $open.get_the_title().$close.
               '</h'.$level.'>';
        $level = $attrs['level']??2;
      $title = (!static::$currentLoop && !is_singular())
         ? get_the_title(get_queried_object_id())
         : get_the_title();
        return sprintf(
         '<h%s%s>%s%s%s</h%s>',
         $level,
         $this->getClassesAndStyles($attrs),
         $open,
         $title,
         $close,
         $level
      );
    }
   protected function render_core_query(array $block, string $content):string
   public function prerender_core_query(array $block, ?string $content):?string
   {
      global $wp_query;
      $inherit = $block['attrs']['inherit'] ?? false;
      $queryID = $block['attrs']['queryId'];
      $args = [];
      $inherit = $block['attrs']['inherit']??false;
      if ($inherit) {
         global $wp_query;
         $loop = $wp_query;
         static::$currentLoop = $wp_query;
      } else {
         foreach ($block['attrs']['query'] as $key => $value) {
            if (empty($value)) {
               continue;
            }
         static::$currentLoop = new WP_Query($this->buildQueryArgs($block['attrs']));
      }
      static::$currentQueryId = $block['attrs']['queryId'] ?? null;
      static::$originalQuery = $wp_query;
      $inside = $this->innerBlocks($block);
      if (str_contains($inside, 'loop')) {
         $classes = $this->getClassesAndStyles($block['attrs'] ?? [], ['loop']);
         $classes = str_replace(' class="', '', $classes);
         $classes = strtok($classes, '"');
         $inside = str_replace('loop', $classes, $inside);
      }
      static::$currentQueryId = null;
      static::$currentLoop = null;
      wp_reset_postdata();
      return $inside;
   }
// public function render_core_query(array $block, string $content): string
// {
//    $inside = $this->innerBlocks($block);
//    if (str_contains($inside, 'loop')) {
//       $classes = $this->getClassesAndStyles($block['attrs']??[], ['loop']);
//       $classes = str_replace(' class="', '', $classes);
//       $classes = strtok($classes, '"');
//
//       $inside = str_replace('loop', $classes, $inside);
//    }
//    return $inside;
// }
      protected function buildQueryArgs(array $attrs): array
      {
         $queryID = $attrs['queryId'] ?? null;
         $args = [];
         foreach (($attrs['query'] ?? []) as $key => $value) {
            if (empty($value)) continue;
            switch ($key) {
               case 'postType':
                  if ($value === BASE.'progress'){
                     $args['post_parent'] = 0;
                  }
                  $args['post_type'] = $value;
                  break;
               case 'perPage':
                  $args['posts_per_page'] = $value;
                  break;
               case 'orderBy':
                  $args['orderby'] = $value;
               case 'postType':   $args['post_type']       = $value; break;
               case 'perPage':    $args['posts_per_page']  = $value; break;
               case 'orderBy':    $args['orderby']         = $value; break;
               case 'sticky':
                  match ($value) {
                     'ignore'  => $args['ignore_sticky_posts'] = true,
                     'exclude' => $args['post__not_in'] = get_option('sticky_posts'),
                     'only'    => $args['post__in']     = get_option('sticky_posts'),
                     default   => null
                  };
                  break;
               case 'taxQuery':
                  $taxQuery = [];
                  foreach ($value as $tax => $terms) {
                     $taxQuery[] = [
                        'taxonomy'  => $tax,
                        'terms'     => $terms
                     ];
                  }
                  if (!empty($taxQuery)) {
                     $args['tax_query'] = $taxQuery;
                     if (count($taxQuery) > 1) {
                        $args['tax_query']['relation'] = 'OR';
                     }
                  }
                  $taxQuery = array_map(fn($tax, $terms) => [
                     'taxonomy' => $tax, 'terms' => $terms
                  ], array_keys($value), $value);
                  if (count($taxQuery) > 1) $taxQuery['relation'] = 'OR';
                  $args['tax_query'] = $taxQuery;
                  break;
               case 'sticky':
                  if ($value === 'ignore') {
                     $args['ignore_sticky_posts'] = true;
                  } else if ($value === 'exclude'){
                     $args['post__not_in'] = get_option('sticky_posts');
                  } else if ($value === 'only') {
                     $args['include'] = get_option('sticky_posts');
                  }
                  break;
               case 'search':
                  $args['s'] = $value;
                  break;
               default:
                  $args[$key] = $value;
                  break;
               case 'search':     $args['s']    = $value; break;
               default:           $args[$key]   = $value; break;
            }
         }
         //Add in any args from the query string
         $search = 'query-'.$queryID;
         // Handle pagination from query string
         $search = 'q-' . $queryID.'-';
         foreach ($_GET as $key => $value) {
            if (str_contains($key, $search)) {
               $key = str_replace($search, '', $key);
               if ($key === 'page') {
                  $args['paged'] = (int)$value;
               }
            if (str_contains($key, $search) && str_replace($search, '', $key) === 'page') {
               $args['paged'] = (int)$value;
            }
         }
         $loop = new WP_Query($args);
         return $args;
      }
      $inner = '';
      foreach ($block['innerBlocks'] as $innerBlock) {
         switch ($innerBlock['blockName']) {
            case 'core/post-template':
               $inner .= '<section class="item-grid">';
               if ($loop->have_posts()) {
                  while($loop->have_posts()) {
                     $loop->the_post();
                     $postType = get_post_type();
                     $inner .= '<div class="item '.jvbNoBase($postType).'">'.$this->innerBlocks($innerBlock).'</div>';
                  }
               }
               $inner .= '</section>';
               break;
         }
      protected function buildPaginationUrl(int $page): string
      {
         $param = 'q-' . static::$currentQueryId . '-page';
         $url = remove_query_arg($param);
         return $page > 1 ? add_query_arg($param, $page, $url) : $url;
      }
      $tagName = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : 'div';
      $out =  '<'.$tagName.' class="loop">'.$inner.'</'.$tagName.'>';
      if ($inherit) {
         wp_reset_postdata();
      protected function getCurrentPage(): int
      {
         $param = 'q-' . static::$currentQueryId . '-page';
         return isset($_GET[$param]) ? (int)$_GET[$param] : 1;
      }
      return $out;
   }
// public function render_core_query(array $block, string $content): string
// {
//
////     $queryID = $block['attrs']['queryId'] ?? null;
////     $inherit = $block['attrs']['inherit'] ?? false;
////
////     if ($inherit) {
////        global $wp_query;
////        $loop = $wp_query;
////     } else {
////        $args = [];
////        foreach (($block['attrs']['query'] ?? []) as $key => $value) {
////           if (empty($value)) {
////              continue;
////           }
////           switch ($key) {
////              case 'postType':
////                 if ($value === BASE.'progress'){
////                    $args['post_parent'] = 0;
////                 }
////                 $args['post_type'] = $value;
////                 break;
////              case 'perPage':
////                 $args['posts_per_page'] = $value;
////                 break;
////              case 'orderBy':
////                 $args['orderby'] = $value;
////                 break;
////              case 'taxQuery':
////                 $taxQuery = [];
////                 foreach ($value as $tax => $terms) {
////                    $taxQuery[] = [
////                       'taxonomy'  => $tax,
////                       'terms'     => $terms
////                    ];
////                 }
////                 if (!empty($taxQuery)) {
////                    $args['tax_query'] = $taxQuery;
////                    if (count($taxQuery) > 1) {
////                       $args['tax_query']['relation'] = 'OR';
////                    }
////                 }
////                 break;
////              case 'sticky':
////                 if ($value === 'ignore') {
////                    $args['ignore_sticky_posts'] = true;
////                 } else if ($value === 'exclude'){
////                    $args['post__not_in'] = get_option('sticky_posts');
////                 } else if ($value === 'only') {
////                    $args['include'] = get_option('sticky_posts');
////                 }
////                 break;
////              case 'search':
////                 $args['s'] = $value;
////                 break;
////              default:
////                 $args[$key] = $value;
////                 break;
////
////           }
////        }
////        $search = 'query-' . $queryID;
////        foreach ($_GET as $key => $value) {
////           if (str_contains($key, $search)) {
////              $key = str_replace($search, '', $key);
////              if ($key === 'page') {
////                 $args['paged'] = (int)$value;
////              }
////           }
////        }
////        $loop = new WP_Query($args);
////     }
////
////     $inner = '';
////     foreach ($block['innerBlocks'] as $innerBlock) {
////        switch ($innerBlock['blockName']) {
////           case 'core/post-template':
////              $inner .= '<section class="item-grid">';
////              if ($loop->have_posts()) {
////                 while ($loop->have_posts()) {
////                    $loop->the_post();
////                    $postType = get_post_type();
////                    $inner .= '<div class="item ' . jvbNoBase($postType) . '">' . $this->innerBlocks($innerBlock) . '</div>';
////                 }
////              }
////              $inner .= '</section>';
////              break;
////        }
////     }
////
////     // Reset only after a custom query, not the main query
////     if (!$inherit) {
////        wp_reset_postdata();
////     }
//
//    $tagName = $block['attrs']['tagName'] ?? 'div';
////     return sprintf(
////        '<%s class="loop">%s</%s>',
////        $tagName,
////        $this->innerBlocks($block),
////        $tagName
////     );
//    return $this->innerBlocks($block);
// }
    //core_query_no_results
   public function prerender_core_query_no_results(array $block, ?string $content):?string
   {
      if (!static::$currentLoop || static::$currentLoop->have_posts()) {
         return '';
      }
      $inside = $this->innerBlocks($block);
      return empty($inside) ? '' : sprintf(
         '<div%s>%s</div>',
         $this->getClassesAndStyles($block['attrs']??[], ['no-results']),
         $inside
      );
   }
    //core_query_pagination
   public function prerender_core_query_pagination(array $block, ?string $content):?string
   {
      return sprintf(
         '<nav%s>%s</nav>',
         $this->getClassesAndStyles($block['attrs']??[], ['pagination', 'condensed','btw']),
         $this->innerBlocks($block)
      );
   }
    //core_query_pagination_next
    //core_query_pagination_numbers
    //core_query_pagination_previous
    //core_query_title
    //core_read_more
    protected function render_core_template_part(array $block, string $content):string
    {
      $isHeaderTemplate = (
         (array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'header')) ||
         (array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'header'))
      ) ? 'header' : false;
      $isFooterTemplate = (
         (array_key_exists('slug', $block['attrs']) && str_contains(strtolower($block['attrs']['slug']), 'footer')) ||
         (array_key_exists('tagName', $block['attrs']) && str_contains(strtolower($block['attrs']['tagName']), 'footer'))
      ) ? 'footer' : false;
   public function prerender_core_query_pagination_next(array $block, ?string $content, ?WP_Block $parent):?string
   {
      if (!static::$currentLoop) return '';
      $currentPage = $this->getCurrentPage();
      $maxPages = static::$currentLoop->max_num_pages;
        if (($isHeaderTemplate || $isFooterTemplate)) {
      if ($currentPage >= $maxPages) return '';
         $tag = $isHeaderTemplate ?: $isFooterTemplate ?: 'div';
            $breadcrumbs = $themeSwitch = $afterHeader = $beforeHeader = $footerText= '';
            if ($isHeaderTemplate) {
            $beforeHeader = apply_filters('jvbAboveHeader', $beforeHeader);
            if ($beforeHeader !== '') {
               $beforeHeader = '<aside class="pre-header">'.$beforeHeader.'</aside>';
            }
                $checked = (is_user_logged_in() && current_user_can('prefers_dark_theme', true)) ? ' checked' : '';
                $title = ($checked == '') ? 'Toggle Dark Mode' : 'Toggle Light Mode';
            $showThemeSwitch = (bool)apply_filters('jvb_show_theme_switch', true);
                $themeSwitch = ($showThemeSwitch) ? '<label title="'.$title.'" id="theme-switch" class="toggle-switch" for="theme-switcher">
                    <input class="theme-switch row" id="theme-switcher" type="checkbox"'.$checked.' data-setting="theme" data-theme role="switch" name="dark-mode"><span class="slider">'.
               jvbIcon('sun-dim', ['title'=> 'Light Mode']).
               jvbIcon('moon', ['title'=>'Dark Mode']).
               '</span></label>' : '';
                $breadcrumbs = jvbBuildBreadcrumbs();
            $afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
            if ($afterHeader !== '') {
               $afterHeader = '<aside class="sub-header">'.$afterHeader.'</aside>';
            }
            } elseif ($isFooterTemplate) {
            $beforeHeader = apply_filters('jvbBeforeFooter', '');
            if ($beforeHeader !== '') {
               $beforeHeader = '<section class="pre-footer">'.$beforeHeader.'</section>';
            }
               $footerText = jvbRandomFooterText();
      $nextLabel = $rArrow = '';
      $type = get_post_type_object(get_post_type())->label;
      if ($parent) {
         $attrs = $parent->attributes;
         if (array_key_exists('paginationArrow', $attrs)){
            $rArrow = match($attrs['paginationArrow']) {
               'chevron'   => jvbIcon('caret-circle-right'),
               default => jvbIcon('arrow-circle-right')
            };
         }
//       jvbDump($beforeHeader,'beforeHeader');
//       jvbDump('<'.$tag.$this->getClassesAndStyles($block['attrs']).'>','tag');
//       jvbDump($themeSwitch,'themeSwitch');
//       jvbDump($this->inside($block, $tag, $content),'inside');
//       jvbDump($footerText,'footerText');
//       jvbDump($afterHeader, 'afterheader');
//       jvbDump($breadcrumbs, 'breadcrumbs');
         if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) {
            return $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']).'>'.
                   $themeSwitch .
               $this->inside($block, $tag, $content) .
                   $footerText.'</'.$tag.'>'.$afterHeader.$breadcrumbs;
        }
            $nextLabel = 'Next '.$type;
         }
      } else {
         $rArrow = jvbIcon('caret-circle-right');
      }
        return $content;
      $aOpen = sprintf(
         '<a class="nav next" href="%s" title="Next %s">',
         $this->buildPaginationUrl($currentPage + 1),
         $type
      );
      $aClose = '</a>';
      return sprintf(
         '%s%s%s%s',
         $aOpen,
         $nextLabel,
         $rArrow,
         $aClose
      );
   }
    //core_query_pagination_numbers
   public function prerender_core_query_pagination_numbers(array $block, ?string $content):?string
   {
      if (!static::$currentLoop) return '';
      $currentPage = $this->getCurrentPage();
      $maxPages = (int)static::$currentLoop->max_num_pages;
      $attrs = $block['attrs']??[];
      if ($maxPages <= 1) return '';
      $midSize = $attrs['midSize'] ?? 2;
      $endSize = 1;
      $items = '';
      $gap = false;
      for ($i = 1; $i <= $maxPages; $i++) {
         if (($i <= min($endSize + 1, $maxPages)) ||
            ($i >= max(1, $currentPage - $midSize) && $i <= min($maxPages, $currentPage + $midSize)) ||
            ($i >= max(1, $maxPages - $endSize) && $i <= $maxPages)) {
            $gap = true;
            $items .= ($i === $currentPage)
               ? sprintf('<li aria-current="page" class="current">%d</li>', $i)
               : sprintf('<li><a href="%s">%d</a></li>', $this->buildPaginationUrl($i), $i);
         } elseif ($gap) {
            $gap = false;
            $items .= sprintf(
               '<li class="dots"><span>%s</span></li>',
               jvbIcon('dots-three')
            );
         }
      }
      return sprintf('<ul%s>%s</ul>',
         $this->getClassesAndStyles($attrs, ['row', 'nowrap']),
         $items
      );
   }
    //core_query_pagination_previous
   public function prerender_core_query_pagination_previous(array $block, ?string $content, ?WP_Block $parent):?string
   {
      if (!static::$currentLoop) return '';
      $currentPage = $this->getCurrentPage();
      $maxPages = static::$currentLoop->max_num_pages;
      if ($currentPage <= 1) return '';
      $nextLabel = $rArrow = '';
      $type = get_post_type_object(get_post_type())->label;
      if ($parent) {
         $attrs = $parent->attributes;
         if (array_key_exists('paginationArrow', $attrs)){
            $rArrow = match($attrs['paginationArrow']) {
               'chevron'   => jvbIcon('caret-circle-left'),
               default => jvbIcon('arrow-circle-left')
            };
         }
         if (!array_key_exists('showLabel', $attrs) || $attrs['showLabel'] === true) {
            $nextLabel = 'Previous '.$type;
         }
      } else {
         $rArrow = jvbIcon('caret-circle-left');
      }
      $aOpen = sprintf(
         '<a class="nav prev" href="%s" title="Previous %s">',
         $this->buildPaginationUrl($currentPage - 1),
         $type
      );
      $aClose = '</a>';
      return sprintf(
         '%s%s%s%s',
         $aOpen,
         $nextLabel,
         $rArrow,
         $aClose
      );
   }
   public function prerender_core_query_title(array $block, ?string $content, ?WP_Block $parent):?string
   {
      jvbDump($block, 'query title');
      $attr = $block['attrs'];
      $name = '';
      $showPrefix = $attr['showPrefix']??false;
      $obj = get_queried_object();
      if (is_tax()) {
         $name = $showPrefix ? $obj->label.':' : '';
         $name .= $obj->name;
      } else if (is_post_type_archive()) {
         $name = $showPrefix ? 'Archive: ' : '';
         $name .= $obj->label;
      } elseif (is_search()) {
         $name = '<small>Search results for:</small> '.get_search_query();
      } elseif (is_singular()) {
         $name = $obj->post_title;
      }
      $level = array_key_exists('level', $attr) ? 'h'.$attr['level'] : 'h1';
      return sprintf(
         '<%s id="%s">%s</%s>',
         $level,
         sanitize_title($name),
         $name,
         $level
      );
   }
    //core_read_more
    public function prerender_core_template_part(array $block, ?string $content, ?WP_Block $parent):?string
    {
//    jvbDump($block, 'template part');
//    jvbDump($parent, 'Parent');
      $slug  = $block['attrs']['slug'] ?? null;
      $theme = $block['attrs']['theme'] ?? get_stylesheet();
      $tag = $block['attrs']['tagName'] ?? 'div';
      if (!$slug) {
         return $content;
      }
      // Try to get the template part post (customized via FSE)
      $template_part = get_block_template( "$theme//$slug", 'wp_template_part' );
      if ( $template_part && ! empty( $template_part->content ) ) {
         $block['innerBlocks'] = parse_blocks( $template_part->content );
         $before = $themeSwitch = $after = $beforeClose = '';
         switch ($tag) {
            case 'header':
               $before = apply_filters('jvbAboveHeader', '');
               if (!empty($before)) {
                  $before = sprintf(
                     '<aside class="pre header row x-btw">%s</aside>',
                     $before
                  );
               }
               $themeSwitch = jvbDarkModeToggle();
               $after = apply_filters('jvbBelowHeader', $after);
               if (!empty($after)) {
                  $after = sprintf(
                     '<aside class="sub header row x-btw">%s</aside>',
                     $after
                  );
               }
               $after .= BreadcrumbManager::getInstance()->renderNavigation();
               $beforeClose = '<div class="scroll-progress"><div class="bar"></div></div>';
               break;
            case 'footer':
               $before = apply_filters('jvbBeforeFooter', $before);
               if (!empty($before)) {
                  $before = sprintf(
                     '<aside class="pre footer">%s</aside>',
                     $before
                  );
               }
               $beforeClose = jvbRandomFooterText();
               break;
         }
         return sprintf(
            '%s<%s%s>%s%s%s</%s>%s',
            $before,
            $tag,
            $this->getClassesAndStyles($block['attrs']??[]),
            $themeSwitch,
            $this->innerBlocks($block),
            $beforeClose,
            $tag,
            $after
         );
      }
      if (JVB_TESTING) {
         jvbDump('Could not create template part block for '.$block['blockName']);
      }
      return $content;
//    $isHeaderTemplate = (
//       (array_key_exists('slug', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['slug']), 'header')) ||
//       (array_key_exists('tagName', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['tagName']), 'header'))
//    ) ? 'header' : false;
//    $isFooterTemplate = (
//       (array_key_exists('slug', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['slug']), 'footer')) ||
//       (array_key_exists('tagName', $block['attrs']??[]) && str_contains(strtolower($block['attrs']['tagName']), 'footer'))
//    ) ? 'footer' : false;
//        if (($isHeaderTemplate || $isFooterTemplate)) {
//       $innerContent = $content;
//
//       $tag = $isHeaderTemplate ?: $isFooterTemplate ?: 'div';
//
//            $breadcrumbs = $themeSwitch = $afterHeader = $beforeHeader = $footerText= '';
//            if ($isHeaderTemplate) {
//
//          $beforeHeader = apply_filters('jvbAboveHeader', $beforeHeader);
//          if ($beforeHeader !== '') {
//             $beforeHeader = '<aside class="pre header row x-btw">'.$beforeHeader.'</aside>';
//          }
//                $themeSwitch = jvbDarkModeToggle();
//                $breadcrumbs = BreadcrumbManager::getInstance()->renderNavigation();
//          $afterHeader = apply_filters('jvbBelowHeader', $afterHeader);
//
//          if ($afterHeader !== '') {
//             $afterHeader = '<aside class="sub header row x-btw">'.$afterHeader.'</aside>';
//          }
//          $footerText = '<div class="scroll-progress"><div class="bar"></div>
//</div>';
//            } elseif ($isFooterTemplate) {
//          $beforeHeader = apply_filters('jvbBeforeFooter', '');
//          if ($beforeHeader !== '') {
//             $beforeHeader = '<aside class="footer">'.$beforeHeader.'</aside>';
//          }
//             $footerText = jvbRandomFooterText();
//       }
//
//            $content = $beforeHeader.'<'.$tag.$this->getClassesAndStyles($block['attrs']??[]).'>'.
//                   $themeSwitch .
//             $this->innerBlocks($block).
////              $this->innerBlocks($block).
////              $innerContent.
//                   $footerText.'</'.$tag.'>'.$afterHeader.$breadcrumbs;
//        }
//
//        return $content;
    }
    //core_term_description
@@ -854,32 +1950,427 @@
     * Widgets Blocks
     */
    //core_archives
   public function render_core_archives(array $block, string $content):string
   {
      jvbDump($block, 'archives');
      $attrs = $block['attrs']??[];
      $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
      $replace = strtok($content,'>').'>';
      $content = str_replace($replace, '', $content);
      if ($isDropdown) {
         $content = sprintf(
            '<div%s>%s',
            $this->getClassesAndStyles($attrs, ['archive dropdown']),
            $content
         );
      } else {
         $content = sprintf(
            '<ul%s>%s',
            $this->getClassesAndStyles($attrs, ['archive-list']),
            $content
         );
      }
      return $content;
   }
    //core_calendar
   public function render_core_calendar(array $block, string $content):string
   {
      $content = $this->inside($block, false, $content);
      $replace = strtok($content, '>').'>';
      $content = str_replace($replace, '', $content);
      return sprintf(
         '<table%s>%s',
         $this->getClassesAndStyles($block['attrs']??[], ['calendar']),
         $content
      );
   }
    //core_categories
   public function prerender_core_categories(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $attrs = $block['attrs']??[];
      $args = [
         'taxonomy'     => 'category',
         'hide_empty'   => !$this->checkAttrs('showEmpty', $attrs)
      ];
      $showHierarchy = $this->checkAttrs('showHierarchy', $attrs);
      if ($this->checkAttrs('showOnlyTopLevel', $attrs) || $showHierarchy){
         $args['parent'] = 0;
      }
      $terms = $this->getTerms($args, $showHierarchy);
      if (!$terms){
         return '';
      }
      $showPostCounts = $this->checkAttrs('showPostCounts', $attrs);
      $isDropdown = $this->checkAttrs('displayAsDropdown', $attrs);
      if ($isDropdown) {
         $this->counter('core_categories');
      }
      $tax = get_taxonomy($args['taxonomy']);
      $taxonomyName = $tax->label??'Categories';
      $taxonomySingular = $tax->labels->singular_name??'Category';
      $inner = $this->buildTermList($terms, $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts);
      if ($isDropdown) {
         return sprintf(
            '<div%s>%s</div>',
            $this->getClassesAndStyles($attrs, ['taxonomy-dropdown']),
            $inner
         );
      }
      return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($attrs, ['taxonomy-list', jvbNoBase($args['taxonomy'])]),
         $inner
      );
   }
      public function getTerms(array $args, bool $showHierarchy = false):array|false
      {
         $terms = get_terms($args);
         if (!$terms || is_wp_error($terms)) {
            return false;
         }
         $terms = array_map(function ($term) {
            return (array) $term;
         }, $terms);
         if ($showHierarchy) {
            $terms = array_map(function ($term) use ($args) {
               $args['parent'] = $term['term_id'];
               $children = $this->getTerms($args, true);
               $term['children'] = $children?:[];
               return $term;
            }, $terms);
         }
         return $terms;
      }
      protected function buildTermList(array $terms, string $taxonomyName, string $taxonomySingular, bool $isDropdown, bool $showPostCounts, bool $isOpening = true, int $level = 0):string
      {
         $out = '';
         if ($isOpening) {
            $out = $isDropdown ?
               sprintf(
                  '<label for="taxonomy-select-%s">%s</label>
                  <select name="%s_name" id="taxonomy-select-%s"><option value="">Select %s</option>',
                  static::$counters['core_categories'],
                  $taxonomyName,
                  str_replace('-', '_',sanitize_title(strtolower($taxonomyName))),
                  static::$counters['core_categories'],
                  $taxonomyName
               ) :
               '';
         } elseif (!$isDropdown) {
            $out .= '<ul>';
         }
         $prefix = '';
         if ($isDropdown) {
            $base = '&emsp;';
            for ($i = 1; $i <= $level; $i++) {
               $prefix .= $base;
            }
            $prefix .= empty($prefix) ? '' : '- ';
         }
         $theTerms = array_map(function ($term) use ($taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, $prefix, $level) {
            if ($isDropdown) {
               return sprintf(
                  '<option value="%s">%s%s%s</option>%s',
                  $term['slug'],
                  $prefix,
                  $term['name'],
                  $showPostCounts ? ' ('.$term['count'].')' : '',
                  empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1)
               );
            }
            return sprintf(
               '<li><a href="%s">%s%s</a>%s</li>',
               get_term_link($term['term_id']),
               $term['name'],
               $showPostCounts ? ' <span class="count">'.$term['count'].'</span>' : '',
               empty($term['children']??[]) ? '' : $this->buildTermList($term['children'], $taxonomyName, $taxonomySingular, $isDropdown, $showPostCounts, false, $level+1)
            );
         }, $terms);
         $out .= implode('', $theTerms);
         if ($isOpening) {
            $out .= $isDropdown ?
               '</select>' :
               '';
         } else if (!$isDropdown) {
            $out .= '</ul>';
         }
         return $out;
      }
    //core_html
    //core_latest_comments
    //core_latest_posts
   public function prerender_core_latest_posts(array $block, ?string $content, ?WP_Block $parent):?string {
      $attrs = $block['attrs']??[];
//    jvbDump($block, 'latest posts');
      $args = [];
      $title = 'Latest Posts';
      $args['order'] = array_key_exists('order', $attrs) ? strtoupper($attrs['order']) : 'DESC';
      $args['orderby'] = array_key_exists('orderBy', $attrs) ? $attrs['orderBy'] : 'date';
      $args['posts_per_page'] = array_key_exists('postsToShow', $attrs) ? $attrs['postsToShow'] : 5;
      if (array_key_exists('categories', $attrs)) {
         $list = jvbCommaList(array_column($attrs['categories'], 'name'));
         $args['tax_query'] = [];
         $args['tax_query'][] = [
            'taxonomy'  => 'category',
            'terms'     => array_column($attrs['categories'], 'id')
         ];
         $title .= ' in '.$list;
      }
      $posts = new WP_Query($args);
      if (!$posts->have_posts()) {
         return '';
      }
      $posts = array_map(function ($post) use ($attrs) {
         $img = $this->checkAttrs('displayFeaturedImage', $attrs)
            ? $this->image(get_post_thumbnail_id($post->ID), 'tiny', 'thumbnail')
            : '';
         $author = $this->checkAttrs('displayAuthor', $attrs)
            ? sprintf(
               '<a href="%s">%s</a>',
               get_author_posts_url($post->post_author),
               get_userdata($post->post_author)->display_name
            )
            : '';
         $date = $this->checkAttrs('displayPostDate', $attrs)
            ? sprintf(
               '<time datetime="%s">%s</time>',
               date('Y-m-d', strtotime($post->post_date)),
               date_i18n('M j, Y', strtotime($post->post_date))
            )
            : '';
         $authorDate = $author;
         if (!empty($authorDate) && !empty($date)) {
            $authorDate .= ' | '.$date;
         } else if (!empty($date)) {
            $authorDate = $date;
         }
         $excerpt = '';
         if ($this->checkAttrs('displayPostContent', $attrs)) {
            if (array_key_exists('excerptLength', $attrs)) {
               $excerpt = wp_trim_words(get_the_content($post->ID), $attrs['excerptLength'], '...');
            } else {
               $excerpt = get_the_excerpt($post->ID);
            }
         }
         if (!empty($excerpt)) {
            $excerpt = wpautop($excerpt);
         }
         return sprintf(
            '<li>%s<p><a href="%s">%s</a>%s</p>%s</li>',
            $img,
            get_the_permalink($post->ID),
            $post->post_title,
            !empty($authorDate) ? ' <small>— '.$authorDate.'</small>' : '',
            $excerpt
         );
      }, $posts->posts);
      wp_reset_postdata();
      return sprintf(
         '<ul%s>%s</ul>',
//       $title,
         $this->getClassesAndStyles($attrs, ['post-list']),
         implode('', $posts)
      );
   }
    //core_page_list
    //core_page_list_item
    //core_rss
   public function prerender_core_page_list(array $block, ?string $content, ?WP_Block $parent):?string{
      $attrs = $block['attrs']??[];
      $parent = array_key_exists('parentPageID', $attrs) ? $attrs['parentPageID'] : 0;
      $pages = new WP_Query([
         'post_type'       => 'page',
         'posts_per_page'  => -1,
         'parent'       => $parent
      ]);
      if (!$pages->have_posts()) {
         return '';
      }
      $inside = [];
      foreach($pages->posts as $page) {
         jvbDump($page);
         $inside[] = sprintf(
            '<li><a href="%s">%s</a>',
            get_the_permalink($page->ID),
            $page->post_title
         );
      }
      wp_reset_postdata();
      return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($attrs, ['page-list']),
         implode('',$inside)
      );
   }
    //core_page_list_item (doesn't seem to be a thing)
// public function prerender_core_page_list_item(array $block, ?string $content, ?WP_Block $parent):?string{
//    return $content;
// }
    //core_
// public function prerender_core_rss(array $block, ?string $content, ?WP_Block $parent):?string
// {
//    jvbDump($block, 'rss');
//    return $content;
// }
    //core_search
    //core_shortcode
   protected function render_core_social_link(array $block, string $content):string
   public function prerender_core_search(array $block, ?string $content, ?WP_Block $parent):?string
   {
      $url = $block['attrs']['url'];
      $service = $block['attrs']['service'];
//    jvbDump($block, 'search');
      $attrs = $block['attrs']??[];
      $label = array_key_exists('label', $attrs) && !empty($attrs['label']) ? $attrs['label'] : '';
      if (array_key_exists('showLabel', $attrs) && $attrs['showLabel'] === false) {
         $label = '';
      }
      $placeholder = array_key_exists('placeholder', $attrs) ? $attrs['placeholder'] : 'Search...';
      $buttonText = array_key_exists('buttonText', $attrs) && !empty($attrs['buttonText']) ? $attrs['buttonText'] : '';
      $isInside = array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-inside';
      $hideInput = $this->checkAttrs('isSearchFieldHidden', $attrs) || (array_key_exists('buttonPosition', $attrs) && $attrs['buttonPosition'] === 'button-only');
      return str_replace('<div class="search-container row left nowrap"', sprintf(
         '<div%s',
         $this->getClassesAndStyles($attrs, ['search-container', 'row', 'left', 'nowrap'])
      ), jvbSearch($placeholder, uniqid(), $label, $buttonText, $isInside, $hideInput));
   }
    //core_shortcode
   public function prerender_core_social_link(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'social link');
//    jvbDump($parent, 'Parent');
      $parentAttrs = false;
      if ($parent) {
         $parentAttrs = $parent->attributes;
      }
      $attrs = $block['attrs']??[];
      $url = $attrs['url']??'';
      $service = $attrs['service']?:'';
      $iconName = ($service === 'bluesky') ? 'butterfly' : $service.'-logo';
      $icon = jvbIcon($iconName);
      if (!$icon) {
         $icon = jvbIcon('link');
      }
      return '<li><a href="'.$url.'" target="_blank" rel="nofollow" title="Find us on '.ucfirst($service).'">'.$icon.'<span class="screen-reader-text">Find us on '.ucfirst($service).'</span></a></li>';
      $serviceName = $this->getServiceName($service);
      $label = $parentAttrs && (!array_key_exists('className', $parentAttrs) || !str_contains($parentAttrs['className'], 'logos-only'))
            ? sprintf(
               '<span>%s</span>',
               $serviceName
            )
            : sprintf(
               '<span class="screen-reader-text">Find us on %s</span>',
            $serviceName
         );
      $pillShaped = $parentAttrs && (array_key_exists('className', $parentAttrs) && str_contains($parentAttrs['className'], 'pill-shape'))
         ? 'style="border-radius:var(--radius-outer);"'
         : '';
      return sprintf(
         '<li><a href="%s" target="_blank" rel="nofollow" title="Find us on %s"%s>%s%s</a></li>',
         $url,
         $serviceName,
         $pillShaped,
         $icon,
         $label
      );
   }
   protected function render_core_social_links(array $block, string $content):string
      private function getServiceName(string $service) {
         return match($service){
            'wordpress' => 'WordPress',
            default => ucfirst($service)
         };
      }
   public function prerender_core_social_links(array $block, ?string $content, ?WP_Block $parent):?string
   {
      return '<ul class="socials">'.$this->innerBlocks($block).'</ul>';
//    jvbDump($block['attrs']??[], 'social links');
//    jvbDump($parent, 'Parent');
      return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($block['attrs']??[], ['socials']),
         $this->innerBlocks($block, '','',$block)
      );
   }
    //core_tag_cloud
   public function prerender_core_tag_cloud(array $block, ?string $content, ?WP_Block $parent):?string
   {
//    jvbDump($block, 'tag cloud');
      $attrs = $block['attrs']??[];
      $taxonomy = (array_key_exists('taxonomy', $attrs) && !empty($attrs['taxonomy']))
         ? $attrs['taxonomy']
         : 'post_tag';
      $showCounts = $this->checkAttrs('showTagCounts', $attrs);
      $terms = get_terms([
         'taxonomy'     => $taxonomy,
         'hide_empty'   => true,
      ]);
      if (!$terms || is_wp_error($terms)) {
         return '';
      }
      $inside = '';
      foreach ($terms as $term) {
         $url = get_term_link($term->term_id, $taxonomy);
         $count = $showCounts ?
            sprintf(
               '<span class="count">%d</span>',
               $term->count
            ) :
            '';
         $size = match(true) {
            $term->count <= 2 => 'small',
            $term->count <= 5 => 'x-small',
            $term->count <= 10 => 'medium',
            $term->count <= 15 => 'x-medium',
            $term->count <= 20 => 'large',
            $term->count <= 25 => 'x-large',
            $term->count <= 30 => 'xx-large',
            $term->count > 30 => 'xxx-large',
         };
         $fontSize = 'font-size: var(--txt-'.$size.');';
         $inside .= sprintf(
            '<li class="%s");"><a href="%s" rel="tag">%s%s</a></li>',
            $size,
//          $fontSize,
            $url,
            $term->name,
            $count
         );
      }
      return sprintf(
         '<ul%s>%s</ul>',
         $this->getClassesAndStyles($attrs, ['term-list','cloud', jvbNoBase($taxonomy)]),
         $inside
      );
   }
    /**
@@ -899,62 +2390,51 @@
    /***********************************
     * Helpers
     **********************************/
   public function stripTagContents(string $tag, string $content):string
   public function stripTagContents(string $tag, ?string $content):string
   {
      $clean = preg_replace('/<'.$tag.'\b[^>]*>.*?<\/'.$tag.'>/is', '', $content);
      $clean = preg_replace('/\s+/', ' ', $clean);
      return trim($clean);
   }
    public function innerBlocks(array $block, string $before = '', string $after = ''):string
    public function innerBlocks(array $block, string $before = '', string $after = '', ?array $parent = null):string
    {
      if ($parent) {
         $parent = new WP_Block($parent);
      }
      $content = '';
      foreach ($block['innerBlocks'] as $b) {
         $method = 'render_'.$this->sanitizeBlockName($b);
         $function = BASE.$method;
         $content .= $before;
         if (function_exists($function)) {
            $content .= $function($b, '');
         } else if (method_exists($this, $method)) {
            $content .= $this->$method($b, '');
         } else {
            $content .= render_block($b);
         }
         $content .= $after;
         $rendered = $parent
            ? $this->checkMethods(null, $b, $parent, true)
            : render_block($b);
         $content .= sprintf('%s%s%s',
            $before,
            $rendered,
            $after
         );
      }
      return $content;
    }
    public function inside(array $block, mixed $tag = false, mixed $o = false):string
    {
        if (!$o) {
            $o = trim($block['innerHTML']);
        }
        if (!$tag) {
            //check to see if there was one dynamically set first
            $tag = (array_key_exists('tagName', $block['attrs'])) ? $block['attrs']['tagName'] : '';
            $tag = ($tag == '') ? str_replace('<', '', strtok($o, '>')) : '';
            $tag = (str_contains($tag, ' class')) ? strtok($tag, ' class') : $tag;
            $tag = trim($tag);
        }
      if (!str_starts_with($o, '<'.$tag)) {
         return $o;
   public function inside(array $block, mixed $tag = false, mixed $o = false): string
   {
      $html = $o ?: trim($block['innerHTML']);
      if (empty($html)) {
         return '';
      }
        $len = strlen('</'.$tag.'>');
      if (preg_match('/^<(\w+)[^>]*>(.*)<\/\1>$/s', $html, $matches)) {
         if ($tag && strtolower($matches[1]) !== strtolower($tag)) {
            return $html;
         }
         return trim($matches[2]);
      }
        return substr_replace(
            str_replace(
                strtok($o, '>').'>',
                ' ',
                $o
            ),
            '',
            -$len,
            $len
        );
    }
      return $html;
   }
   /**
    * Extract content from a specific nested element
@@ -962,7 +2442,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 '';
@@ -990,7 +2470,7 @@
                 (!array_key_exists('attrs', $block) && !array_key_exists('id', $block['attrs'])))) {
                $ID = get_post_thumbnail_id();
            } else {
                if (array_key_exists('id', $block['attrs'])) {
                if (array_key_exists('id', $block['attrs']??[])) {
                    $ID = $block['attrs']['id'];
                } elseif (array_key_exists('mediaId', $block['attrs'])) {
                    $ID = $block['attrs']['mediaId'];
@@ -1096,30 +2576,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);
        // Build attribute strings
        $class_string = !empty($classes) ? ' class="' . implode(' ', $classes) . '"' : '';
        $style_string = !empty($styles) ? ' style="' . implode(';', $styles) . '"' : '';
        $return = trim($class_string . $style_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
     *
@@ -1147,135 +2661,60 @@
        $classes = [];
        foreach ($attrs as $key => $value) {
            $class = $this->getClass($key, $value, $attrs);
            if (is_array($class)) {
                $classes = array_merge($classes, $class);
            } else {
                $classes[] = $class;
            }
         if (is_string($class)) {
            $class = explode(' ', $class);
         }
         $classes = array_merge($classes, $class);
        }
        $classes =  array_filter($classes, function ($class) {
            return $class!=='' && !str_starts_with($class, 'wp');
        });
        return $classes;
      return array_unique(array_filter($classes, function ($class) {
         return $class!=='' && !str_starts_with($class, 'wp');
      }));
    }
    protected function getClass(string $key, string|bool|array|int $value, array $attrs):string|array
    {
      //TODO: gradient
        switch ($key) {
            //Any additional classes the user adds
            case 'className':
                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':
            $classes = [];
            $pos = explode(' ', $value);
            foreach($pos as $p) {
               switch ($p) {
                  case 'top':
                     $classes[] = 'a-start';
                     break;
                  case 'right':
                     $classes[] = 'end';
                     break;
                  case 'bottom':
                     $classes[] = 'a-end';
                     break;
                  case 'left':
                     $classes[] = 'start';
                     break;
               }
            }
            return implode(' ', $classes);
            return $this->getContentPosition($value);
         case 'term':
         case 'taxonomy':
            return jvbNoBase($value);
            //Layout attributes
            case 'layout':
                $classes = [];
            $type = 'row';
                if (array_key_exists('type', $value)) {
               $type = 'col';
                    if ($value['type'] === 'constrained') {
                        $classes[] = 'container col';
                    }
                }
            if (array_key_exists('orientation', $value)) {
               $type = 'col';
                    if ($value['orientation'] === 'vertical') {
                  $classes[] = 'col';
                  if (in_array('row', $classes)) {
                     $index = array_search('row', $classes);
                     unset($classes[$index]);
                  }
               }
                }else if (array_key_exists('type', $value) && $value['type'] === 'flex') {
               $classes[] = 'row';
               if (in_array('col', $classes)) {
                  $index = array_search('col', $classes);
                  unset($classes[$index]);
               }
            }
//jvbDump($type);
//jvbDump($value);
//          $check = [$value, $attrs];
//          foreach ($check as $ch) {
//
//          }
            if (!array_key_exists('justifyContent', $value) && !array_key_exists('contentPosition', $attrs)) {
               $classes[] = ($type === 'row') ? 'start' : 'a-start';
            }
            if (array_key_exists('justifyContent', $value)  && !array_key_exists('contentPosition', $attrs)) {
               if (in_array($value['justifyContent'], ['left', 'right','space-between'])) {
//                jvbDump($type);
                  switch ($value['justifyContent']) {
                     case 'right':
                        $classes[] = ($type === 'row') ? 'end' : 'a-end';
                        break;
                     case 'space-between':
                        $classes[] = 'btw';
                        break;
                  }
               }
            }
                if (array_key_exists('flexWrap', $value)) {
                    if ($value['flexWrap'] === 'nowrap') {
                        $classes[] = 'nowrap';
                    }
                }
                return implode(' ', $classes);
                return $this->getLayout($value, $attrs);
            case 'align':
                return !empty($value) ? 'align-'.$value : '';
            case 'verticalAlignment':
                return !empty($value) ? 'v-align-'.$value : '';
            case 'isStackedMobile':
            switch ($value) {
               case 'bottom':
                  $value = 'btm';
                  break;
               case 'center':
                  $value = 'y-mid';
               default:
            }
                return !empty($value) ? $value : '';
            case 'isStackedOnMobile':
                return ($value === true) ? 'stack-small' : '';
            case 'justifyContent':
                return !empty($value) ? 'j-'.$value : '';
            case 'orientation':
                return $value==='column' ? 'column' : '';
            case 'width':
            return $this->getWidth($value);
            case 'dimRatio':
                if (is_numeric($value)) {
                    $width = match (true) {
                        $value < 25 => '25',
                        $value < 33 => '33',
                        $value <= 50 => '50',
                        $value < 66 => '66',
                        $value < 75 => '75',
                        default => 'full',
                    };
                    switch ($key) {
                        case 'width':
                            return 'width-'.$width;
                        case 'dimRatio':
                            return 'overlay-'.$width;
                    }
                }
                return '';
                return $this->getDimRatio($value, $attrs);
         case 'overlayColor':
            return $value;
            break;
            //Typography
            case 'textAlign':
                return !empty($value) ? 'text-'.$value : '';
@@ -1284,177 +2723,306 @@
            //Media
            case 'hasParallax':
                return $value === true ? 'bg-parallax' : '';
                return $value === true ? 'bg-fixed' : '';
            case 'isRepeated':
                return $value === true ? 'bg-repeat' : '';
            //Style base:
            case 'style':
                $classes = [];
                //Margin and Padding
            if (array_key_exists('spacing', $value)) {
               foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) {
                  if (array_key_exists($search, $value['spacing'])) {
                     $directions = [];
                     // Collect ONLY preset spacing values for classes
                     foreach ($value['spacing'][$search] as $direction => $size) {
                        $presetSize = $this->getPresetSpacing($size);
                        if ($presetSize) {
                           $directions[$direction] = $presetSize;
                        }
                        // Non-preset values are skipped here and handled by inline styles below
                     }
                     if (empty($directions)) {
                        continue;
                     }
                     // Check what directions we have
                     $hasTop = isset($directions['top']);
                     $hasBottom = isset($directions['bottom']);
                     $hasLeft = isset($directions['left']);
                     $hasRight = isset($directions['right']);
                     // Check if axes match
                     $xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right'];
                     $yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom'];
                     // All 4 directions exist and match â†’ p-3
                     if ($hasTop && $hasBottom && $hasLeft && $hasRight &&
                        count(array_unique($directions)) === 1) {
                        $classes[] = $c . '-' . reset($directions);
                     }
                     // Both axes match â†’ px-3 py-2
                     elseif ($xMatch && $yMatch) {
                        $classes[] = $c . 'x-' . $directions['left'];
                        $classes[] = $c . 'y-' . $directions['top'];
                     }
                     // Only X axis matches â†’ px-3 (+ individual for top/bottom)
                     elseif ($xMatch) {
                        $classes[] = $c . 'x-' . $directions['left'];
                        if ($hasTop) {
                           $classes[] = $c . 't-' . $directions['top'];
                        }
                        if ($hasBottom) {
                           $classes[] = $c . 'b-' . $directions['bottom'];
                        }
                     }
                     // Only Y axis matches â†’ py-3 (+ individual for left/right)
                     elseif ($yMatch) {
                        $classes[] = $c . 'y-' . $directions['top'];
                        if ($hasLeft) {
                           $classes[] = $c . 'l-' . $directions['left'];
                        }
                        if ($hasRight) {
                           $classes[] = $c . 'r-' . $directions['right'];
                        }
                     }
                     // No matches - individual directions
                     else {
                        foreach ($directions as $direction => $size) {
                           $dir = match($direction) {
                              'top' => 't',
                              'bottom' => 'b',
                              'left' => 'l',
                              'right' => 'r',
                              default => $direction
                           };
                           $classes[] = $c . $dir . '-' . $size;
                        }
                     }
                  }
               }
            }
                if (array_key_exists('fontSize', $value)) {
                    if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) {
                        $classes[] = 'font-'.$value['fontSize'];
                    }
                    if (in_array('fontWeight', $value)) {
                        $classes[] = 'text-'.$value['fontWeight'];
                    }
                    if (in_array('textTransform', $value)) {
                        if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) {
                            $classes[] = $value['textTransform'];
                        }
                    }
                }
                return implode(' ', $classes);
                return $this->getPresetStyles($value);
         case 'fontSize':
            $classes[] = 'font-'.$value;
            return implode(' ', $classes);
         case 'isStackedOnMobile':
            return ($value === true) ? 'stack-small' : '';
         case 'width':
            if (is_numeric($value)) {
               $width = match (true) {
                  $value < 25 => '25',
                  $value < 33 => '33',
                  $value <= 50 => '50',
                  $value < 66 => '66',
                  $value < 75 => '75',
                  default => 'full',
               };
               switch ($key) {
                  case 'width':
                     return 'width-'.$width;
                  case 'dimRatio':
                     return 'overlay-'.$width;
               }
         case 'postLayout':
            $classes[] = 'item-grid';
            if (isset($attrs['columns']) && $attrs['columns']!== 3){
               $classes[] = sprintf(
                  'split-%d',
                  $attrs['columns']
               );
            }
            return '';
            return $classes;
            default:
            $ignore = [
               'useFeaturedImage',
               'opacity',
               'borderColor',
               'backgroundColor',
               'textColor',
               'minHeight',
               'minHeightUnit',
               'isDark',
               'sizeSlug',
               'isUserOverlayColor',
               'customOverlayColor',
               'dimRatio',
               'placeholder',
               'alt',
               'imageFill',
               'mediaSizeSlug',
               'isLink',
               'kind',
               'label',
               'type',
               'id',
               'url',
               'label',
               'shouldSyncIcon',
               'rel',
               'opensInNewTab',
               'title',
               'ref',
               'overlayMenu',
               'slug',
               'theme',
               'tagName',
               'level',
               'ordered',
               'area',
               'mediaId',
               'mediaLink',
               'mediaType',
               'height', //maybe still need?
            ];
            if (!is_admin() &&!in_array($key, $ignore)) {
            if (JVB_TESTING && !is_admin() &&!in_array($key, $this->ignore)) {
//             TESTING
//             jvbDump($key, 'getClass');
//             jvbDump($attrs);
               jvbDump($attrs, '[getClass] '.$key);
            }
                return '';
        }
    }
      /*** CLASS HELPERS ***/
      private function getContentPosition(string $value):string
      {
         $classes = [];
         $pos = explode(' ', $value);
         foreach($pos as $p) {
            switch ($p) {
               case 'top':
                  $classes[] = 'top';
                  break;
               case 'right':
                  $classes[] = 'right';
                  break;
               case 'bottom':
                  $classes[] = 'btm';
                  break;
               case 'left':
                  $classes[] = 'left';
                  break;
            }
         }
         return implode(' ', $classes);
      }
      private function getLayout(array $value, array $attrs):array
      {
//       jvbDump($value, 'getLayout');
         $classes = [];
         $type = 'row';
         $isRow = true;
         //Determine type
         if ((array_key_exists('type', $value) && !in_array($value['type'], ['flex', 'grid'])) ||
            (array_key_exists('orientation', $value) && $value['orientation'] === 'vertical')) {
            $type = 'col';
            $isRow = false;
         } elseif (array_key_exists('type', $value) && $value['type'] === 'grid') {
            $type = 'item-grid';
            $isRow = false;
            if (array_key_exists('columnCount', $value) && $value['columnCount']!== 3) {
               $classes[] = sprintf(
                  'split-%s',
                  $value['columnCount']
               );
            }
         }
         if (array_key_exists('justifyContent', $value)  && !array_key_exists('contentPosition', $attrs)) {
            switch ($value['justifyContent']) {
               case 'right':
                  $classes[] = 'right';
                  break;
               case 'center':
                  $classes[] = 'x-mid';
                  break;
               case 'space-between':
                  $classes[] = 'x-btw';
                  break;
               case 'left':
                  $classes[] = 'left';
                  break;
               case 'space-evenly':
                  $classes[] = 'x-even';
                  break;
               case 'space-around':
                  $classes[] = 'x-around';
                  break;
               case 'stretch':
                  $classes[] = 'stretch';
            }
         } else {
            $classes[] = 'left';
         }
         if (array_key_exists('verticalAlignment', $value)) {
            switch ($value['verticalAlignment']) {
               case 'bottom':
                  $classes[] = 'btm';
                  break;
               case 'top':
                  $classes[] = 'top';
                  break;
               case 'center':
                  $classes[] = 'y-mid';
                  break;
               case 'space-between':
                  $classes[] = 'y-btw';
                  break;
               case 'space-around':
                  $classes[] = 'y-around';
                  break;
               case 'space-even':
                  $classes[] = 'y-even';
            }
         }
         if (array_key_exists('flexWrap', $value)) {
            if ($value['flexWrap'] === 'nowrap') {
               $classes[] = 'nowrap';
            }
         }
         $classes[] = $type;
         return $classes;
      }
      private function getWidth(string $value):string
      {
         $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) {
               $value <= 25 => '25',
               $value <= 33 => '33',
               $value <= 50 => '50',
               $value <= 66 => '66',
               $value <= 75 => '75',
               default => 'full',
            }
         );
      }
      private function getDimRatio(string $value, array $attrs):string
      {
         if (array_key_exists('overlayColor', $attrs)) {
            return '';
         }
         if (is_numeric($value)) {
            return sprintf(
               'op-%d',
               match (true) {
                  $value <= 14 => '1',
                  $value <= 28 => '2',
                  $value <= 42 => '3',
                  $value <= 56 => '45',
                  $value <= 70 => '4',
                  $value <= 84 => '5',
                  default => '6',
               }
            );
         }
         return '';
      }
      private function getPresetStyles(array $value):string
      {
         $classes = [];
         //Margin and Padding
         if (array_key_exists('spacing', $value)) {
            $classes = array_merge($classes, $this->buildSpacingClasses($value));
         }
         if (array_key_exists('color', $value)) {
            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 {
                  $preset = [$preset];
               }
               $classes[] = 'duotone';
               foreach ($preset as $p) {
                  $classes[] = $p;
               }
            }
         }
         if (array_key_exists('fontSize', $value)) {
            if (in_array($value['fontSize'], ['small', 'large', 'extra-large', 'huge'])) {
               $classes[] = 'font-'.$value['fontSize'];
            }
            if (in_array('fontWeight', $value)) {
               $classes[] = 'text-'.$value['fontWeight'];
            }
            if (in_array('textTransform', $value)) {
               if (in_array($value['textTransform'], ['uppercase', 'capitalize', 'lowercase'])) {
                  $classes[] = $value['textTransform'];
               }
            }
         }
         return implode(' ', $classes);
      }
         private function buildSpacingClasses(array $value):array
         {
            $classes = [];
            foreach (['margin' => 'm', 'padding'=>'p'] as $search => $c) {
               if (array_key_exists($search, $value['spacing'])) {
                  $directions = [];
                  // Collect ONLY preset spacing values for classes
                  foreach ($value['spacing'][$search] as $direction => $size) {
                     $presetSize = $this->getPresetSpacing($size);
                     if ($presetSize) {
                        $directions[$direction] = $presetSize;
                     }
                     // Non-preset values are skipped here and handled by inline styles below
                  }
                  if (empty($directions)) {
                     continue;
                  }
                  // Check what directions we have
                  $hasTop = isset($directions['top']);
                  $hasBottom = isset($directions['bottom']);
                  $hasLeft = isset($directions['left']);
                  $hasRight = isset($directions['right']);
                  // Check if axes match
                  $xMatch = $hasLeft && $hasRight && $directions['left'] === $directions['right'];
                  $yMatch = $hasTop && $hasBottom && $directions['top'] === $directions['bottom'];
                  // All 4 directions exist and match â†’ p-3
                  if ($hasTop && $hasBottom && $hasLeft && $hasRight &&
                     count(array_unique($directions)) === 1) {
                     $classes[] = $c . '-' . reset($directions);
                  }
                  // Both axes match â†’ px-3 py-2
                  elseif ($xMatch && $yMatch) {
                     $classes[] = $c . 'x-' . $directions['left'];
                     $classes[] = $c . 'y-' . $directions['top'];
                  }
                  // Only X axis matches â†’ px-3 (+ individual for top/bottom)
                  elseif ($xMatch) {
                     $classes[] = $c . 'x-' . $directions['left'];
                     if ($hasTop) {
                        $classes[] = $c . 't-' . $directions['top'];
                     }
                     if ($hasBottom) {
                        $classes[] = $c . 'b-' . $directions['bottom'];
                     }
                  }
                  // Only Y axis matches â†’ py-3 (+ individual for left/right)
                  elseif ($yMatch) {
                     $classes[] = $c . 'y-' . $directions['top'];
                     if ($hasLeft) {
                        $classes[] = $c . 'l-' . $directions['left'];
                     }
                     if ($hasRight) {
                        $classes[] = $c . 'r-' . $directions['right'];
                     }
                  }
                  // No matches - individual directions
                  else {
                     foreach ($directions as $direction => $size) {
                        $dir = match($direction) {
                           'top' => 't',
                           'bottom' => 'b',
                           'left' => 'l',
                           'right' => 'r',
                           default => $direction
                        };
                        $classes[] = $c . $dir . '-' . $size;
                     }
                  }
               }
            }
            return $classes;
         }
    protected function getInlineStyles(array $attrs):array
    {
@@ -1464,258 +3032,83 @@
        $styles = [];
        foreach ($attrs as $key => $value) {
            $style = $this->getStyle($key, $value, $attrs);
            $styles = array_merge($styles, $style);
         $styles = array_unique($styles);
        }
        return $styles;
    }
    protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array
    protected function getStyle(string $key, string|bool|array|int $value, array $attrs):array|string
    {
        $styles = [];
        switch ($key) {
            // Font family settings
            case 'fontFamily':
                if ($value === 'body') {
                    $styles[] = 'font-family: "Open Sans", system-ui, -apple-system, sans-serif';
                } elseif ($value === 'heading') {
                    $styles[] = 'font-family: "Josefin Sans", system-ui, -apple-system, sans-serif';
                } elseif (!empty($value)) {
                    $styles[] = 'font-family: '.$value;
                }
                break;
         case 'size':
            return $this->getIconSizeStyle($value);
         case 'fontFamily':
                return $this->getFontFamilyStyle($value);
            // Icon color (for icon blocks)
            case 'iconColorValue':
                if (!empty($value)) {
                    $styles[] = 'color: '.$value;
                }
                break;
            return $this->getColorStyle($value);
            // Minimum height settings
            case 'minHeight':
                if (!empty($value) && isset($attrs['minHeightUnit'])) {
                    $styles[] = 'min-height: '.$value.$attrs['minHeightUnit'];
                } elseif (!empty($value)) {
                    $styles[] = 'min-height: '.$value.'px'; // Default to px if no unit specified
                }
                break;
                return $this->getMinHeightStyle($value, $attrs);
            // Background URL (for cover, media blocks)
            case 'url':
                if (!empty($value) && str_starts_with($value, 'http')) {
                    $styles[] = 'background-image: url('.$value.')';
                    return ['background-image' => 'url('.$value.')'];
                }
                break;
            // Focal point for background images
            case 'focalPoint':
            $x = (array_key_exists('x', $attrs['focalPoint'])) ? $attrs['focalPoint']['x'] * 100 : 'center';
            $y = (array_key_exists('y', $attrs['focalPoint'])) ? $attrs['focalPoint']['y'] * 100 : 'center';
            $styles[] = 'background-position:'.$x.' '.$y.';';
                break;
            return $this->getFocalPointStyle($value, $attrs);
            // Complex style object
            case 'style':
                // Border styles
                if (isset($value['border'])) {
                    $border = $value['border'];
                return $this->extractStyles($value, $attrs);
                    if (isset($border['radius'])) {
                        $styles[] = 'border-radius: '.$border['radius'];
                    }
                    if (isset($border['width'])) {
                        $styles[] = 'border-width: '.$border['width'];
                    }
                    if (isset($border['style']) && isset($border['width']) && !empty($border['style'])) {
                        $styles[] = 'border-style: '.$border['style'];
                    }
                    if (isset($border['color'])) {
                        $styles[] = 'border-color: '.$border['color'];
                    }
                }
                // Color styles
                if (isset($value['color'])) {
                    $color = $value['color'];
                    if (isset($color['background'])) {
                        $styles[] = 'background-color: '.$color['background'];
                    }
                    if (isset($color['text'])) {
                        $styles[] = 'color: '.$color['text'];
                    }
                    if (isset($color['gradient'])) {
                        $styles[] = 'background: '.$color['gradient'];
                    }
                }
                // Layout styles
                if (isset($value['layout'])) {
                    foreach ($value['layout'] as $layout => $option) {
                        switch ($layout) {
                            case 'selfStretch':
                                if ($option === 'fixed' && isset($value['layout']['selfStretchValue'])) {
                                    $styles[] = 'width: '.$value['layout']['selfStretchValue'];
                                }
                                break;
                        }
                    }
                }
                // Typography styles
                if (isset($value['typography'])) {
                    $typography = $value['typography'];
                    if (isset($typography['fontSize'])) {
                        $styles[] = 'font-size: '.$typography['fontSize'];
                    }
                    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'];
                    }
                }
                // Spacing styles
                if (isset($value['spacing'])) {
                    $spacing = $value['spacing'];
                    // Don't duplicate margin/padding that's handled by classes
                    // Only add specific CSS values here that wouldn't work well as classes
                    if (isset($spacing['margin'])) {
                        foreach ($spacing['margin'] as $direction => $size) {
                            // If not a preset value, add as inline style
                            if (!str_contains($size, 'var:preset')) {
                                $styles[] = 'margin-'.$direction.': '.$size;
                            }
                        }
                    }
                    if (isset($spacing['padding'])) {
                        foreach ($spacing['padding'] as $direction => $size) {
                            // If not a preset value, add as inline style
                            if (!str_contains($size, 'var:preset')) {
                                $styles[] = 'padding-'.$direction.': '.$size;
                            }
                        }
                    }
                }
                break;
         case 'dimRatio':
            $ratio = (ceil($value /25) *25);
            $s = 'background-color: rgba(var(--base-rgb), ';
            switch ($ratio) {
               case 0:
                  $s .= 'var(--rgb-subtle-hover));';
                  break;
               case 25:
                  $s .= 'var(--rgb-light));';
                  break;
               case 50:
                  $s .= 'var(--rgb-medium));';
                  break;
               default:
                  $s .= 'var(--rgb-heavy));';
                  break;
            }
            $styles[] = $s;
            break;
            return $this->getDimRatioStyle($value, $attrs);
            // Custom styles (any other attributes that need inline styling)
            case 'backgroundType':
                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'])) {
                    $styles[] = '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 = ($key === 'backgroundColor') ? 'background-color:' : (($key === 'borderColor') ? 'border-color:' : 'color:');
            $defaults = apply_filters('jvbColours', ['base', 'contrast', 'action', 'secondary']);
            $continue = true;
            foreach ($defaults as $default) {
               if (str_starts_with($value, $default)) {
                  $continue = false;
                  $styles[] = $type.'var(--'.$value.')';
               }
            $type = str_replace('Color', '-color', $key);
            $type = str_replace('text-', '', $type);
            if ($key === 'borderColor' && isset($attrs['border']['width'])) {
               break;
            }
            if ($continue) {
               $styles[] = $type.$value;
            }
            break;
            return $this->getColorStyle($value, $type);
            // Any other attributes that need direct styling
            default:
            $ignore = [
               'useFeaturedImage',
               'opacity',
               'textAlign',
               'minHeightUnit',
               'isDark',
               'isUserOverlayColor',
               'contentPosition',
               'sizeSlug',
               'customOverlayColor',
               'alt',
               'placeholder',
               'imageFill',
               'mediaSizeSlug',
               'isLink',
               'kind',
               'label',
               'type',
               'id',
               'url',
               'label',
               'shouldSyncIcon',
               'rel',
               'opensInNewTab',
               'title',
               'ref',
               'overlayMenu',
               'slug',
               'theme',
               'tagName',
               'level',
               'ordered',
               'area',
               'className',
               'fontSize',
               'layout',
               'align',
               'mediaId',
               'mediaLink',
               'mediaType',
               'isStackedOnMobile',
               'width',
               'height', // maybe still need?
            ];
            if (!is_admin() && !in_array($key, $ignore)) {
            if (JVB_TESTING && !is_admin() && !in_array($key, $this->ignore)) {
               //TESTING
//             jvbDump($key, 'getStyle');
//             jvbDump($attrs);
               jvbDump($attrs, '[getStyle] '.$key);
            }
                // No default inline styles
                break;
@@ -1723,6 +3116,410 @@
        return $styles;
    }
      private function getIconSizeStyle(string $value):array
      {
         $values = explode(' ', $value);
         $styles = [];
         foreach ($values as $v) {
            switch ($value) {
               case 'has-small-icon-size':
                  $styles['--w'] = 'var(--txt-x-small)';
                  break;
               case 'has-large-icon-size':
                  $styles['--w'] = 'var(--txt-large)';
                  break;
               case 'has-huge-icon-size':
                  $styles['--w'] = 'var(--txt-xx-large)';
                  break;
               default:
                  if (JVB_TESTING) {
                     jvbDump($value, 'No preset found for size: '.print_r($value, true));
                  }
            }
         }
         return $styles;
      }
      private function getFontFamilyStyle(string $value):array
      {
         return match($value) {
            'body'      => ['font-family' =>  'var(--body)'],
            'heading'   => ['font-family' =>  'var(--heading)'],
            default     => ['font-family' => $value]
         };
      }
      private function getColorStyle(string $value, ?string $type = 'color'):array
      {
         if (!in_array($type, ['color', 'background','background-color','border-color'])) {
            $type = null;
         }
         if (!$type) {
            return [];
         }
         return [
            $type => $this->getColor($value)
         ];
      }
      private function getMinHeightStyle(string $value, array $attrs):array
      {
         $out = [];
         if (!empty($value)) {
            if (isset($attrs['minHeightUnit'])) {
               $out['min-height'] = sprintf('%s%s', $value, $attrs['minHeightUnit']);
            } else {
               $out['min-height'] = sprintf(
                  '%spx',
                  $value
               );
            }
         }
         return $out;
      }
      private function getFocalPointStyle(array $value, array $attrs):array
      {
         $x = array_key_exists('x', $value) ? ($value['x'] * 100).'%' : 'center';
         $y = array_key_exists('y', $value) ? ($value['y'] * 100).'%' : 'center';
         $y = $x === $y ? '' : ' '.$y;
         $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
      {
         $styles = [];
         foreach ($value as $k => $v) {
            switch ($k) {
               case 'border':
                  $styles = array_merge($styles, $this->getBorderStyle($v, $attrs));
                  break;
               case 'color':
                  if (isset($v['background'])) {
                     $styles['background-color'] = $this->getColor($v['background']);
                  }
                  if (isset($v['text'])) {
                     $styles['color'] = $this->getColor($v['text']);
                  }
                  if (isset($v['gradient'])) {
                     jvbDump($v, 'Gradient');
                  }
                  break;
               case 'layout':
                  $styles = array_merge($styles, $this->getLayoutStyle($v, $attrs));
                  break;
               case 'typography':
                  $styles = array_merge($styles, $this->getTypographyStyle($v, $attrs));
                  break;
               case 'spacing':
                  if (isset($v['blockGap'])) {
                     if (is_array($v['blockGap'])) {
                        $inner = [];
                        foreach ($v['blockGap'] as $gap) {
                           $inner[] = sprintf(
                              'var(--sp%s)',
                              $this->getPresetSpacing($gap)
                           );
                        }
                        if (!empty($inner)) {
                           $styles['--gap'] = sprintf(
                              '%s',
                              implode(' ', $inner)
                           );
                        }
                     } else {
                        $styles['--gap'] = 'var(--sp'.$this->getPresetSpacing($v['blockGap']).')';
                     }
                  }
                  // Don't duplicate margin/padding that's handled by classes
                  // Only add specific CSS values here that wouldn't work well as classes
                  if (isset($v['margin'])) {
                     foreach ($v['margin'] as $direction => $size) {
                        if (!str_contains($size, 'var:preset')) {
                           $styles['margin-'.$direction] = $size;
                        }
                     }
                  }
                  if (isset($v['padding'])) {
                     foreach($v['padding'] as $dir => $size) {
                        if (!str_contains($size, 'var:preset')) {
                           $styles['padding-'.$dir] = $size;
                        }
                     }
                  }
                  break;
               case 'background':
                  if (array_key_exists('backgroundImage', $v)) {
                     $data = Image::getData($v['backgroundImage']['id']);
                     if (!empty($data) && array_key_exists('tiny', $data)) {
                        $styles['background-image'] = sprintf(
                           'url(%s)',
                           $data['tiny']
                        );
                     }
                  }
                  break;
               case 'dimensions':
                  foreach ($v as $sk => $sv) {
                     if ($sk === 'minHeight') {
                        $styles['min-height'] = $sv;
                     } else {
                        jvbDump('No config set for dimension '.$sk.': '.print_r($sv, true));
                     }
                  }
                  break;
               case 'elements':
                  if (!empty($v)) {
                     // Generate a unique class tied to this block instance
                     $uid = 'b-'.substr(md5(serialize($attrs)), 0, 8);
                     $this->extractElementStyles($v, $uid);
                     // We need the uid added as a class â€” store it for getClassesAndStyles to pick up
                     static::$pendingClass[] = $uid;
                  }
                  break;
               default:
                  if (JVB_TESTING) {
                     jvbDump($v,'No config set for '.$k.': ');
                  }
            }
         }
         return $styles;
      }
         private function getBorderStyle(array $border, array $attrs):array
         {
            $styles = [];
            if (isset($border['radius'])) {
               $styles['border-radius'] = $border['radius'];
            }
            if (isset($border['width']) && (isset($attrs['borderColor']) || isset($border['color']))) {
               $st = $border['style'] ?? 'solid';
               $color = $border['color']??$attrs['borderColor'];
               $styles['border'] = sprintf(
                  '%s %s %s',
                  $border['width'],
                  $st,
                  $this->getColor($color)
               );
            } 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;
         }
         private function getLayoutStyle(array $layout, array $attrs):array
         {
            $styles = [];
//          jvbDump($layout);
            foreach ($layout as $l => $option) {
               switch ($l) {
                  case 'selfStretch':
                     if ($option === 'fixed' && isset($layout['selfStretchValue'])) {
                        $styles['width'] = $layout['selfStretchValue'];
                     } elseif ($option === 'fill') {
                        $styles['flex'] = 1;
                     }
                     break;
                  default:
                     $ignore = [
                        'selfStretchValue',
                        'flexSize',
                     ];
                     if (JVB_TESTING && !in_array($l, $ignore)) {
                        jvbDump($l, 'No layout style set for: ');
                     }
//                   case 'type':
//                      if ($option === 'grid' && $value['layout']['columnCount'] !== 3) {
//                         $styles[] = sprintf(
//                            'grid-template-columns: repeat(1fr, %s)',
//                            $value['layout']['columnCount']
//                         );
//                      }
//                      break;
               }
            }
            return $styles;
         }
         private function getTypographyStyle(array $typography, array $attrs):array
         {
            $styles = [];
            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.': ');
                     }
               }
            }
            return $styles;
         }
         private function extractElementStyles(array $elements, string $uid):void
         {
            foreach ($elements as $element => $states) {
               $selector = match($element) {
                  'link'    => "a",
                  'heading' => "h1,h2,h3,h4,h5,h6",
                  'button'  => "button,.button",
                  default   => $element,
               };
               $selectors = explode(',',$selector);
               foreach ($states as $state => $rules) {
                  $css = [];
                  $fullSelector = array_map(function($sel) use ($uid, $state) {
                     return str_starts_with($state, ':')
                        ? ".{$uid} {$sel}{$state}"
                        : ".{$uid} {$sel}";
                  }, $selectors);
                  $fullSelector = implode(',', $fullSelector);
                  jvbDump($state, 'state');
                  jvbDump($rules, 'rules');
                  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).' }';
                  }
               }
            }
         }
   public function maybeOutputCustomStyles(): string
   {
      if (empty(static::$pendingStyles)) return '';
      $out = '<style>'.implode(' ', static::$pendingStyles).'</style>';
      static::$pendingStyles = [];
      return $out;
   }
      private function getDimRatioStyle(int $value, array $attrs):array
      {
         //TODO: This likely isn't working correctly
//       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 ['background-color' => $s];
      }
      protected function getDataset(array $attrs):array
      {
         $dataset = [];
         if (array_key_exists('style', $attrs)) {
            if (array_key_exists('background', $attrs['style'])){
               if (array_key_exists('backgroundImage', $attrs['style']['background'])) {
                  $id = $attrs['style']['background']['backgroundImage']['id']??false;
                  if ($id) {
                     $data = Image::getData($id);
                     $dataset['bg-small'] = $data['small'];
                     $dataset['bg-med'] = $data['medium'];
                     $dataset['bg-large'] = $data['large'];
                  }
               }
            }
         }
         return $dataset;
      }
    public function formatImage(int $ID = 0, string $start = 'tiny', string $replace = 'large'):string
    {
@@ -1735,6 +3532,32 @@
      return jvbFormatImage($ID, $start, $replace);
    }
}
   protected function checkAttrs(string $test, array $attrs):bool
   {
      return array_key_exists($test, $attrs) && $attrs[$test]===true;
   }
new CustomBlocks();
   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 $prefix ? 'var(--'.$value.')' : $value;
         }
      }
      //We removed the presets
      if (str_contains($value, 'var:preset')) {
         return '';
      }
      return $value;
   }
   protected function counter(string $key):void
   {
      if (!array_key_exists($key, static::$counters)) {
         static::$counters[$key] = 1;
      } else {
         static::$counters[$key]++;
      }
   }
}