source_chain=$source_chain; $this->row_values=$row_values; $this->preview=$preview;$this->post_id=$post_id?:((int)get_the_ID());$this->fallback=$fallback;$this->source_id=$source_id?:$this->post_id;$this->interaction_context=$interaction_context; } public function render(array $doc): string { $this->headings=[]; $this->collect($doc['nodes']); $s=$doc['settings'];$this->layout_settings=$s; if(class_exists(ThemeBuilder::class)){$global=ThemeBuilder::options();foreach(['accent','bodySize','headingFont'] as $key){if(empty($s[$key])||$s[$key]==='inherit'){$s[$key]=$global[$key]??($s[$key]??'');}}} $cssExtra='';if(!empty($s['bodySize'])){$cssExtra.='--dbs-body-size:'.$s['bodySize'].'px;';}if(!empty($s['lineHeight'])){$cssExtra.='--dbs-line-height:'.$s['lineHeight'].';';}if(!empty($s['headingFont'])&&$s['headingFont']!=='inherit'){$cssExtra.='--dbs-heading-font:'.$s['headingFont'].';';} $palette=Document::choice($s['palette']??'', ['legacy','sky','mint','lavender','peach','sunshine'],'legacy'); $palette_class=($palette==='legacy'?'':' dbs-palette-light dbs-palette-'.$palette).((!empty($s['headingFont'])&&$s['headingFont']!=='inherit')?' dbs-custom-headings':''); $globalColors=class_exists(ThemeBuilder::class)?ThemeBuilder::colors():['accent'=>'#2667b8','ink'=>'#19324c','surface'=>'#ffffff','soft'=>'#edf5ff','border'=>'#d5e5f6','highlight'=>'#f4c95d']; $appearance=Appearance::clean($s['appearance']??[]); if($appearance['mode']!=='none'){$appearance['preset']='none';} $hasSurfaceFill=$appearance['preset']!=='none'||$appearance['mode']!=='none'; $css='--dbs-width:'.(int)$s['width'].'px;'.$cssExtra; foreach($globalColors as $key=>$value){$css.='--dbs-global-'.sanitize_key($key).':'.Document::color($value).';';} if(!empty($s['accent'])){$css.='--dbs-accent:'.Document::color($s['accent']).';';} if(!empty($s['paper'])&&!$hasSurfaceFill){$css.='background-color:'.Document::color($s['paper']).';';} $design=DesignSystem::styles($s);$css.=$design['inline'];$palette_class.=$design['class']; $typography=Typography::styles($s);$palette_class.=$typography['class']; $surface=Appearance::surface_attributes($appearance);$buttons=Appearance::button_wrapper_attributes($s['buttonStyle']??[]); $css.=$surface['style'].$buttons['style']; if($surface['class']){$palette_class.=' '.$surface['class'];}if($buttons['class']){$palette_class.=' '.$buttons['class'];} $document_audio=Document::audio($s['audio']??[]);$audio_default=$this->audio_attributes($document_audio,true); $html=FontLibrary::links($doc).$design['css'].$typography['css'].''.$this->nodes($doc['nodes']).''; if(!$this->preview&&!$this->fallback&&class_exists(Assets::class)){Assets::enqueue_html($html);if(class_exists(PageCSS::class)){$base=PageCSS::stylesheet($html,$this->source_id);$html=$base.PageCSS::layout($html,$this->source_id);}} return $html; } private function collect(array $nodes): void { foreach ($nodes as $n) { if(!$this->preview&&!$this->visible($n['style'])){continue;} if(!empty($n['bindings'])){$n=DynamicData::apply($n,$this->post_id,$this->row_values);} if ('heading'===$n['type'] && !empty($n['props']['text'])) { $this->headings[]=['id'=>$n['id'],'text'=>$n['props']['text']]; } $this->collect($n['type']==='component'?Components::instance($n):$n['children']); } } private function nodes(array $nodes,?array $parent=null): string { $html=''; foreach ($nodes as $node) { $html.=$this->node($node,$parent); } return $html; } private function text($s): string { return esc_html((string)$s); } private function rich(string $s): string { return wpautop(wp_kses_post($s)); } private function text_align_class($value): string { return 'dbs-copy-align-'.self::safe_choice($value,['left','center','right','justify'],'left'); } private function empty(string $label): string { return $this->preview?'
'.esc_html($label).'
':''; } private function image(array $p, bool $hero=false): string { if (empty($p['url'])) { return $this->preview?'
Choose an imageClick or drop an image here
':''; } $mode=self::safe_choice($p['sizeMode']??'inherit',['inherit','card','original','custom'],'inherit'); $ratio=self::safe_choice($p['ratio']??'16/9',['21/9','16/9','3/2','4/3','1/1','3/4','2/3','9/16','auto'],'16/9'); $fit=self::safe_choice($p['fit']??'cover',['cover','contain'],'cover'); $align=self::safe_choice($p['align']??'center',['left','center','right'],'center'); $x=max(0,min(100,(float)($p['x']??50)));$y=max(0,min(100,(float)($p['y']??50))); $width=max(10,min(100,(float)($p['width']??100)));$maxWidth=max(0,min(2400,(float)($p['maxWidth']??0))); $frameHeight=$mode==='original'?0:max(0,min(1600,(float)($p['frameHeight']??0)));$zoom=max(50,min(250,(float)($p['zoom']??100))); $figureCss=''; if($mode==='card'){$figureCss.='width:100%;';} elseif($mode==='original'){$figureCss.='width:fit-content;';} elseif($mode==='custom'){$figureCss.='width:'.$width.'%;';} if($maxWidth>0){$figureCss.='max-width:'.$maxWidth.'px;';}elseif($mode!=='inherit'){$figureCss.='max-width:100%;';} if($mode!=='inherit'||$maxWidth>0){$figureCss.=$align==='left'?'margin-left:0;margin-right:auto;':($align==='right'?'margin-left:auto;margin-right:0;':'margin-left:auto;margin-right:auto;');} $frameCss='overflow:hidden;'; $useRatio=$mode!=='original'&&$ratio!=='auto'&&$frameHeight<=0; if($frameHeight>0){$frameCss.='height:'.$frameHeight.'px;';} elseif($useRatio){$frameCss.='aspect-ratio:'.$ratio.';';} $imgCss='object-fit:'.$fit.';object-position:'.$x.'% '.$y.'%;transform-origin:'.$x.'% '.$y.'%;'; if($zoom!=100){$imgCss.='transform:scale('.round($zoom/100,3).');';} if($mode==='original'){$imgCss.='width:auto;max-width:100%;height:auto;';} elseif($frameHeight>0||$useRatio){$imgCss.='width:100%;height:100%;';} else{$imgCss.='width:100%;height:auto;';} $attrs=['class'=>'dbs-img','alt'=>$p['alt'],'style'=>$imgCss,'loading'=>$hero?'eager':'lazy','decoding'=>'async']; if ($hero) { $attrs['fetchpriority']='high'; } if ($p['id']) { $img=wp_get_attachment_image($p['id'],'large',false,$attrs); } else { $img=''.esc_attr($p['alt']).''; } $figureStyle=$figureCss?' style="'.esc_attr($figureCss).'"':''; return '
preview?' data-dbs-image-drop="1"':'').'>
'.$img.'
'.($p['caption']?'
'.$this->text($p['caption']).'
':'').'
'; } private function lines(string $s): array { return array_values(array_filter(array_map('trim',preg_split('/\r?\n/',$s)),static fn($v)=>''!==$v)); } private function link_attributes(string $target='same', string $nofollow='no'): string { $attrs='';$rel=[]; if('new'===$target){$attrs.=' target="_blank"';$rel[]='noopener';$rel[]='noreferrer';} if('yes'===$nofollow){$rel[]='nofollow';} if($rel){$attrs.=' rel="'.esc_attr(implode(' ',$rel)).'"';} return $attrs; } private function button(string $label,string $url,string $variant='solid',array $options=[]): string { $variant=self::safe_choice($variant,['solid','outline','soft','text'],'solid'); $size=self::safe_choice($options['size']??'medium',['small','medium','large'],'medium'); $full=($options['fullWidth']??'no')==='yes'; $background=Document::color($options['background']??'');$text=Document::color($options['textColor']??''); $icon=Icons::clean($options['icon']??'none'); $iconPosition=self::safe_choice($options['iconPosition']??'after',['before','after'],'after'); $audio=Document::audio($options['audio']??[]);$legacyEnabled=self::safe_choice($options['audioEnabled']??'no',['no','yes'],'no'); // Explicit per-button audio keeps priority; otherwise a local or // document Audio Studio recipe is inherited without changing old data. if($audio['enabled']!=='yes'&&$audio['override']!=='yes'){$audio['enabled']=$legacyEnabled;$audio['preset']=self::safe_choice($options['audioEffect']??'none',['none','glass-break','explosion','weapon','whistle','bird','scream','cry','yes','no','chime','click','pop','whoosh'],'none');$audio['volume']=(int)Document::number($options['audioVolume']??55,0,100);$audio['cooldown']=(int)Document::number($options['audioCooldown']??180,0,2000);$audio['repeat']=self::safe_choice($options['audioRepeat']??'always',['always','once'],'always');} $classes='dbs-button dbs-button-'.$variant.' dbs-button-'.$size.($full?' dbs-button-full':'').($icon!=='none'?' dbs-button-has-icon dbs-button-icon-'.$iconPosition:'');$style=''; if($background&&$variant!=='text'){$style.='--dbs-button-bg:'.$background.';--dbs-button-border:'.$background.';';} if($text){$style.='--dbs-button-text:'.$text.';';} $iconColor=Document::color($options['iconColor']??'');$iconSize=Document::number($options['iconSize']??0,0,96); if($iconColor){$style.='--dbs-icon-color:'.$iconColor.';';}if($iconSize>0){$style.='--dbs-icon-size:'.$iconSize.'px;';} $style=$style?' style="'.esc_attr($style).'"':''; if(trim($label)===''&&trim($url)===''){return '';} $label=trim($label)===''?$url:$label; // The direct label keeps text above Button Studio pseudo-effect layers. if($icon==='none'){$content=''.$this->text($label).'';} else{$glyph=$this->button_icon($icon);$content=($iconPosition==='before'?$glyph:'').''.$this->text($label).''.($iconPosition==='after'?$glyph:'');} $audio=$this->audio_attributes($audio); if (trim($url)==='') { // An explicitly inserted button may be an audio-only/action button. // Keep it visible in preview AND publication, without a fake href // or an implicit form submission. Optional template CTAs retain // their legacy no-link suppression unless deliberately enabled. if (!empty($options['allowEmptyUrl'])) { return ''; } return $this->preview?''.$content.'':''; } return 'link_attributes((string)($options['target']??'same'),(string)($options['nofollow']??'no')).$style.$audio.'>'.$content.''; } private function button_icon(string $icon): string { if (Icons::valid($icon)) { return Icons::svg($icon,'dbs-button-icon'); } $paths=[ 'arrow'=>'M5 12h14m-5-5 5 5-5 5','chevron'=>'m9 18 6-6-6-6','plus'=>'M12 5v14M5 12h14', 'download'=>'M12 3v12m-5-5 5 5 5-5M5 21h14','play'=>'m8 5 11 7-11 7Z', 'cart'=>'M3 4h2l2 11h10l3-8H6m2 12h.01M17 19h.01','star'=>'m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2-5.6-2.9-5.6 2.9 1.1-6.2L3 9.6l6.2-.9Z', 'heart'=>'M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1.1-1.1a5.5 5.5 0 0 0-7.8 7.8L12 21l8.8-8.6a5.5 5.5 0 0 0 0-7.8Z', ]; return isset($paths[$icon])?'':''; } private function audio_attributes(array $audio,bool $default=false): string { $audio=Document::audio($audio);if($audio['enabled']!=='yes'){return !$default&&$audio['override']==='yes'?' data-dbs-audio="none" data-dbs-audio-disabled="1"':'';} $attrs=$default?' data-dbs-audio-default="1"':' data-dbs-audio="'.esc_attr($audio['preset']==='custom'?'custom':$audio['preset']).'"'; if(!$default&&$audio['preset']==='custom'){$attrs.=' data-dbs-audio-recipe="'.esc_attr(wp_json_encode($audio['recipe'],JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)).'"';} elseif($default){$attrs.=' data-dbs-audio="'.esc_attr($audio['preset']==='custom'?'custom':$audio['preset']).'"';if($audio['preset']==='custom'){$attrs.=' data-dbs-audio-recipe="'.esc_attr(wp_json_encode($audio['recipe'],JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)).'"';}} if($audio['url']){$attrs.=' data-dbs-audio-src="'.esc_url($audio['url']).'" data-dbs-audio-clip="'.esc_attr(wp_json_encode($audio['clip'])).'"';} $attrs.=' data-dbs-audio-volume="'.(int)$audio['volume'].'" data-dbs-audio-cooldown="'.(int)$audio['cooldown'].'" data-dbs-audio-repeat="'.esc_attr($audio['repeat']).'"';return $attrs; } private function action(array $p,array $audioStyle=[]): string { if(($p['action']??'no')!=='yes'){return '';} $label=trim((string)($p['label']??''));$url=(string)($p['url']??''); if($label===''){return '';} $target=(string)($p['target']??'same');$nofollow=(string)($p['nofollow']??'no');$type=(string)($p['actionType']??'button'); if($type==='link'){ $color=Document::color($p['actionTextColor']??'');$style=$color?' style="color:'.esc_attr($color).'"':''; if(!$url){return $this->preview?''.$this->text($label).'':'';} return 'link_attributes($target,$nofollow).$style.'>'.$this->text($label).''; } return $this->button($label,$url,(string)($p['actionStyle']??'solid'),['allowEmptyUrl'=>true,'size'=>$p['actionSize']??'medium','background'=>$p['actionBackground']??'','textColor'=>$p['actionTextColor']??'','fullWidth'=>$p['fullWidth']??'no','target'=>$target,'nofollow'=>$nofollow,'audio'=>$audioStyle,'audioEnabled'=>$p['actionAudioEnabled']??'no','audioEffect'=>$p['actionAudioEffect']??'none','audioVolume'=>$p['actionAudioVolume']??55,'audioCooldown'=>$p['actionAudioCooldown']??180,'audioRepeat'=>$p['actionAudioRepeat']??'always']); } private static function safe_choice($value,array $allowed,string $fallback): string {return in_array((string)$value,$allowed,true)?(string)$value:$fallback;} private function node(array $n,?array $parent=null): string { if(!empty($n['bindings'])){$n=DynamicData::apply($n,$this->post_id,$this->row_values);} if(class_exists(GlobalStyles::class)){$n['style']=GlobalStyles::resolve($n['style']);} $p=$n['props']; $type=$n['type']; $id=$n['id']; $h='';$style=$n['style']; $appearance=Appearance::clean($style['appearance']??[]); if($appearance['mode']!=='none'){$appearance['preset']='none';} $hasSurfaceFill=$appearance['preset']!=='none'||$appearance['mode']!=='none'; if(!$this->preview&&!$this->visible($n['style'])){return '';} if (!$this->preview) { if (in_array($type,['list','facts','accordion','tabs','timeline','steps'],true) && empty($p['items'])) { return ''; } if ('gallery'===$type && empty($p['images'])) { return ''; } if ('quote'===$type && ''===trim($p['quote'])) { return ''; } if ('table'===$type && ''===trim($p['rows'])) { return ''; } if ('review'===$type && ''===trim(wp_strip_all_tags($p['verdict'].$p['pros'].$p['cons']))) { return ''; } if ('sources'===$type && empty($p['items']) && ''===$p['correction']) { return ''; } if ('author'===$type && ''===trim($p['name'].$p['bio'])) { return ''; } if ('stat'===$type && ''===trim($p['value'])) { return ''; } if ('pricing'===$type && ''===trim($p['price'].$p['title'])) { return ''; } if ('countdown'===$type && !strtotime($p['date'])) { return ''; } if ('story'===$type && ''===trim((string)$p['title'].(string)$p['summary'].(string)$p['kicker'].(string)$p['byline'].(string)$p['url']) && empty($p['image']['url'])) { return ''; } if ('masthead'===$type && ''===trim($p['name'])) { return ''; } if ('animation-scene'===$type && ($p['asset']??'')==='custom' && empty($p['customImage']['url'])) { return ''; } } switch ($type) { case 'component': $resolved=Components::instance($n);$h=$resolved?$this->nodes($resolved):$this->empty('The published component is unavailable. Local values are retained.'); if($this->preview){$h=preg_replace('/ data-dbs-(?:node|row|column|image-drop)="[^"]*"/','',$h);} break; case 'masthead': $h='
'.($p['logo']['url']?'':'').'
'.($p['name']?$this->text($p['name']):$this->empty('Publication name')).'
'.($p['tagline']?'

'.$this->text($p['tagline']).'

':'').'
'.($p['issue']?'

'.$this->text($p['issue']).'

':'').'
';break; case 'story': $headline=$p['title']?$this->text($p['title']):($this->preview?$this->empty('Story headline'):'');$headlineHtml=$headline?'

'.($p['url']?''.$headline.'':$headline).'

':''; $h='
'.$this->image($p['image']).'
'.($p['kicker']?'

'.$this->text($p['kicker']).'

':'').$headlineHtml.($p['summary']?'

'.$this->text($p['summary']).'

':'').($p['byline']?'':'').'
';break; case 'section': $columns='columns'===$p['structure'];$widths=$columns?explode(',',$p['widths']):[]; $custom=$columns?';--dbs-tracks:'.implode(' ',array_map(static fn($v)=>'minmax(0,'.(float)$v.'fr)',$widths)):''; if(!$hasSurfaceFill&&!empty($p['backgroundImage']['url'])){$url=strtr($p['backgroundImage']['url'],[chr(34)=>'%22',"'"=>'%27','('=>'%28',')'=>'%29',chr(92)=>'%5C',chr(10)=>'%0A',chr(13)=>'%0D']);$custom.=';background-image:url("'.$url.'");background-size:cover;background-position:'.$p['backgroundImage']['x'].'% '.$p['backgroundImage']['y'].'%';} $layoutClass=' dbs-layout-'.$p['layout'].' dbs-flex-'.$p['direction'].' dbs-justify-'.$p['justify'].' dbs-items-'.$p['alignItems'].('no'===$p['wrap']?' dbs-no-wrap':'').('yes'===$p['parallax']?' dbs-parallax':'').('yes'===$p['reverseMobile']?' dbs-reverse-mobile':''); if($p['columnsTablet']){$custom.=';--dbs-cols-tablet:'.(int)$p['columnsTablet'];$layoutClass.=' dbs-cols-tablet-custom';}if($p['columnsMobile']){$custom.=';--dbs-cols-mobile:'.(int)$p['columnsMobile'];$layoutClass.=' dbs-cols-mobile-custom';} $h='
preview?' data-dbs-row="'.esc_attr($id).'"':'').' style="gap:'.(int)$p['gap'].'px;padding:'.(int)$p['padding'].'px'.esc_attr($custom).'">'.($n['children']?$this->nodes($n['children'],$n):$this->empty('Add modules to this section')).'
'.$this->shape($p['divider']); break; case 'column': $h='
preview?' data-dbs-column="'.esc_attr($id).'"':'').' style="gap:'.(int)$p['gap'].'px">'.$this->nodes($n['children']).(!$n['children']?$this->empty('Drop modules into this column'):'').'
';break; case 'hero': $title=$p['title']?'<'.$p['level'].' class="dbs-headline">'.$this->text($p['title']).'':$this->empty('Your headline'); $h='
'.($p['kicker']?'

'.$this->text($p['kicker']).'

':'').$title.($p['summary']?'

'.$this->text($p['summary']).'

':'').($p['byline']?'':'').'
'.$this->image($p['image'],true).'
'; break; case 'heading': $h=$p['text']?'<'.$p['level'].' class="dbs-heading">'.$this->text($p['text']).'':$this->empty('Heading'); break; case 'text': $h=($p['content']||$this->preview)?'
'.($p['content']?$this->rich($p['content']):$this->empty('Write your text')).'
':''; break; case 'image': $h=$this->image($p['image']); break; case 'bubble': $h=''; break; case 'tooltip': if ('click'===$p['mode']) { $h='
'.$this->text($p['label']).'
'.$this->rich($p['content']).'
'; } else { $h=''.$this->text($p['label']).' ⓘ'.$this->rich($p['content']).''; } break; case 'quote': $h='

'.$this->text($p['quote']).'

'.($p['author']?''.$this->text($p['author']).'':'').'
'; break; case 'button': $h=$this->button($p['label'],$p['url'],$p['variant'],['allowEmptyUrl'=>true,'size'=>$p['size'],'background'=>$p['background'],'textColor'=>$p['textColor'],'fullWidth'=>$p['fullWidth'],'target'=>$p['target'],'nofollow'=>$p['nofollow'],'icon'=>$p['icon'],'iconPosition'=>$p['iconPosition'],'iconColor'=>$p['iconColor']??'','iconSize'=>$p['iconSize']??0,'audio'=>$style['audio']??[],'audioEnabled'=>$p['audioEnabled']??'no','audioEffect'=>$p['audioEffect']??'none','audioVolume'=>$p['audioVolume']??55,'audioCooldown'=>$p['audioCooldown']??180,'audioRepeat'=>$p['audioRepeat']??'always']);break; case 'divider': $h='
'; break; case 'spacer': $h=''; break; case 'list': $tag='number'===$p['style']?'ol':'ul'; $h='<'.$tag.' class="dbs-list dbs-list-'.esc_attr($p['style']).'">'; foreach ($p['items'] as $r) { $h.='
  • '.$this->text($r['text']).'
  • '; } $h.=''; break; case 'facts': $h=''; break; case 'table': $headers=array_slice($this->lines($p['headers']),0,12); $h='
    '.($p['caption']?'':'').''; foreach ($headers as $cell) { $h.=''; } $h.=''; foreach (array_slice($this->lines($p['rows']),0,100) as $row) { $cells=explode('|',$row);$h.='';for($i=0;$itext(trim($cells[$i]??'')).''; } $h.=''; } $h.='
    '.$this->text($p['caption']).'
    '.$this->text($cell).'
    '.($p['note']?'

    '.$this->text($p['note']).'

    ':'');break; case 'toc': $h='';break; case 'accordion': $h='
    ';foreach($p['items'] as $r) { $h.='
    '.$this->text($r['title']).'
    '.$this->rich($r['content']).'
    '; }$h.='
    ';break; case 'tabs': $h='
    '; foreach($p['items'] as $i=>$r) { $h.=''; }$h.='
    '; foreach($p['items'] as $i=>$r) { $h.='
    '.$this->rich($r['content']).'
    '; }$h.='
    ';break; case 'gallery': $h='';break; case 'video': $h=$this->video($p['url']).($p['caption']?'

    '.$this->text($p['caption']).'

    ':'');break; case 'audio': $h=$p['url']?'
    '.$this->text($p['caption']).'
    ':$this->empty('Add an audio URL');break; case 'animation-scene': $h=$this->animation_scene($p,$style,$id);break; case 'stat': $h='
    '.$this->text($p['value']).''.$this->text($p['label']).''.$this->text($p['source']).'
    ';break; case 'progress': $h='
    '.$this->text($p['label']).''.(float)$p['value'].'%
    '.(float)$p['value'].'%
    ';break; case 'rating': $h='
    '.$this->text($p['label']).''.str_repeat('★',(int)round($p['value'])).str_repeat('☆',5-(int)round($p['value'])).' '.(float)$p['value'].'/5

    '.$this->text($p['note']).'

    ';break; // Stateful controls are rendered from published metadata while the // plugin is active. Do not bake inert forms into post_content as a // deactivation fallback. case 'quiz': $h=$this->fallback?'':($this->interaction_allowed()?$this->quiz($p,$id,$n['_sourceId']??$id):$this->empty('Quiz is available only on its direct page or assigned site region.'));break; case 'poll': $h=$this->fallback?'':($this->interaction_allowed()?$this->poll($p,$id,$n['_sourceId']??$id):$this->empty('Poll is available only on its direct page or assigned site region.'));break; case 'review': $h='
    '.(float)$p['score'].'/10

    '.$this->text($p['title']).'

    '.$this->rich($p['verdict']).'
    ';foreach(['pros'=>__('Pros','digiter-builder-lite'),'cons'=>__('Cons','digiter-builder-lite')] as $key=>$label) { $h.='

    '.esc_html($label).'

      ';foreach($this->lines($p[$key]) as $line) { $h.='
    • '.$this->text($line).'
    • '; }$h.='
    '; }$h.='
    ';break; case 'timeline': case 'steps': $h='
      ';foreach($p['items'] as $i=>$r) { $h.='
    1. '.('steps'===$type?($i+1):$this->text($r['date'])).'

      '.$this->text($r['title']).'

      '.$this->rich($r['content']).'
    2. '; }$h.='
    ';break; case 'author': $h='';break; case 'sources': $h='';break; case 'posts': $h=$this->posts($p,$n['_sourceId']??$id);break; case 'pricing': $h='

    '.$this->text($p['title']).'

    '.$this->text($p['price']).''.$this->text($p['period']).''.$this->action($p,$style['audio']??[]).'
    ';break; case 'countdown': $timestamp=strtotime($p['date']);$h='

    '.$this->text($p['title']).'

    '.($timestamp?'':$this->empty('Add a valid target date with timezone')).'
    ';break; case 'code': $h='
    '.$this->text($p['language']).'
    '.$this->text($p['code']).'
    ';break; case 'html': $h=wp_kses_post($p['html']);break; default: $h=class_exists(Modules::class)&&Modules::has($type)?Modules::render($n,['post_id'=>$this->post_id,'source_id'=>$this->source_id,'preview'=>$this->preview,'fallback'=>$this->fallback]):$this->advanced($n);break; } if (''===$h && !$this->preview) { return ''; } $cls='dbs-node dbs-module-'.$type.' dbs-align-'.$style['align'].($style['hideMobile']?' dbs-hide-mobile':'').($style['hideDesktop']?' dbs-hide-desktop':''); $css='margin-bottom:'.(int)$style['space'].'px;--dbs-space:'.(int)$style['space'].'px;'; if(!empty($style['cardSurface'])){$cls.=' dbs-card-surface';$css.='--dbs-card-radius:'.(float)$style['radius'].'px;';} if($style['customSpacing']){$cls.=' dbs-space-custom';} $tokenMap=['background'=>['token'=>'backgroundToken','property'=>'background-color'],'textColor'=>['token'=>'textColorToken','property'=>'color'],'borderColor'=>['token'=>'borderColorToken','property'=>'border-color']]; foreach($tokenMap as $key=>$rule){if($key==='background'&&$hasSurfaceFill){continue;}$token=$style[$rule['token']]??'';$value=$token?'var(--dbs-global-'.$token.')':($style[$key]??'');if($value){$css.=$rule['property'].':'.$value.';';if($key==='textColor'){$css.='--dbs-ink:'.$value.';--dbs-local-ink:'.$value.';';}}} if($style['borderWidth']){$css.='border-style:solid;border-width:'.$style['borderWidth'].'px;';} if($style['radius']){$css.='border-radius:'.$style['radius'].'px;';} foreach(['Top','Right','Bottom','Left'] as $side){if($style['padding'.$side]){$css.='padding-'.strtolower($side).':'.$style['padding'.$side].'px;';}} if('none'!==$style['shadow']){$cls.=' dbs-shadow-'.$style['shadow'];} foreach(['Desktop','Tablet','Mobile'] as $device){if($style['font'.$device]){$cls.=' dbs-font-'.strtolower($device).'-custom';$css.='--dbs-font-'.strtolower($device).':'.$style['font'.$device].'px;';}} $advanced=$this->style_attributes($style);$css.=$advanced['css'];$cls.=$advanced['class']; $surface=Appearance::surface_attributes($appearance);$buttons=Appearance::button_wrapper_attributes($style['buttonStyle']??[]); $css.=$surface['style'].$buttons['style']; if($surface['class']){$cls.=' '.$surface['class'];}if($buttons['class']){$cls.=' '.$buttons['class'];} $slot=$this->preview&&!empty($n['slot']['label'])?''.$this->text($n['slot']['label']).'':''; return NodeTypography::css($n,$this->layout_settings).FieldTypography::css($n,$this->layout_settings).Grid::css($n,$parent,$this->layout_settings).(class_exists(Flex::class)?Flex::css($n,$parent,$this->layout_settings):'').'
    '.$slot.$h.'
    '; } private function animation_scene(array $p,array $style,string $id): string { $asset=self::safe_choice($p['asset']??'woman',['woman','man','dog','cat','bird','butterfly','robot','alien','rocket','custom'],'woman'); $visual=self::safe_choice($p['visual']??'flat',['flat','outline','neon','glass','mono'],'flat'); $path=self::safe_choice($p['path']??'left-right',['float','left-right','right-left','bounce','diagonal','orbit','custom'],'left-right'); $trigger=self::safe_choice($p['trigger']??'viewport',['load','viewport','click','hover','scroll'],'viewport'); $repeat=self::safe_choice($p['repeat']??'loop',['once','loop','pingpong'],'loop'); $easing=self::safe_choice($p['easing']??'ease-in-out',['linear','ease','ease-in','ease-out','ease-in-out'],'ease-in-out'); $area=max(80,min(1200,(float)($p['areaHeight']??260)));$size=max(6,min(90,(float)($p['size']??24)));$duration=max(400,min(60000,(float)($p['duration']??7000)));$delay=max(0,min(30000,(float)($p['delay']??0)));$rotation=max(-360,min(360,(float)($p['rotation']??0))); $startX=max(0,min(100,(float)($p['startX']??12)));$startY=max(0,min(100,(float)($p['startY']??54)));$endX=max(0,min(100,(float)($p['endX']??88)));$endY=max(0,min(100,(float)($p['endY']??46))); $label=trim((string)($p['label']??''))?:'Animated character'; if($asset==='custom'){$img=Document::image($p['customImage']??[]);if(!$img['url']){return $this->empty('Choose a local animation image');}$actor='';} else{$actor=$this->animation_asset($asset,$visual);} $sound=self::safe_choice($p['sound']??'none',['none','click','pop','chime','whoosh','glass-break','explosion','weapon','whistle','bird','scream','cry','yes','no','audio-studio'],'none');$audioAttrs=''; if($sound==='audio-studio'){$audio=Document::audio($style['audio']??[]);if($audio['enabled']==='yes'){$audioAttrs=$this->animation_audio_attributes($audio);}} elseif($sound!=='none'){$audio=Document::audio(['enabled'=>'yes','preset'=>$sound,'volume'=>$p['soundVolume']??55]);$audioAttrs=$this->animation_audio_attributes($audio);} $attrs=' data-dbs-animation data-dbs-animation-id="'.esc_attr($id).'" data-dbs-animation-path="'.esc_attr($path).'" data-dbs-animation-trigger="'.esc_attr($trigger).'" data-dbs-animation-repeat="'.esc_attr($repeat).'" data-dbs-animation-easing="'.esc_attr($easing).'" data-dbs-animation-start-x="'.esc_attr((string)$startX).'" data-dbs-animation-start-y="'.esc_attr((string)$startY).'" data-dbs-animation-end-x="'.esc_attr((string)$endX).'" data-dbs-animation-end-y="'.esc_attr((string)$endY).'" data-dbs-animation-rotation="'.esc_attr((string)$rotation).'" data-dbs-animation-sound-delay="'.esc_attr((string)max(0,min(5000,(float)($p['soundDelay']??0)))).'" data-dbs-animation-preview="'.($this->preview?'1':'0').'"'; return ''; } private function animation_audio_attributes(array $audio): string { $audio=Document::audio($audio);if($audio['enabled']!=='yes'){return '';}$attrs=' data-dbs-animation-audio="'.esc_attr($audio['preset']==='custom'?'custom':$audio['preset']).'" data-dbs-animation-volume="'.(int)$audio['volume'].'"'; if($audio['url']){$attrs.=' data-dbs-animation-audio-src="'.esc_url($audio['url']).'" data-dbs-animation-audio-clip="'.esc_attr(wp_json_encode($audio['clip'])).'"';} if($audio['preset']==='custom'){$attrs.=' data-dbs-animation-audio-recipe="'.esc_attr(wp_json_encode($audio['recipe'],JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)).'"';} return $attrs; } private function animation_asset(string $asset,string $visual): string { $class='dbs-animation-asset dbs-animation-asset-'.$visual; $svg=[ 'woman'=>'', 'man'=>'', 'dog'=>'', 'cat'=>'', 'bird'=>'', 'butterfly'=>'', 'robot'=>'', 'alien'=>'', 'rocket'=>'', ]; return $svg[$asset]??$svg['woman']; } private function visible(array $s): bool { if($this->fallback&&(($s['audience']??'all')!=='all'||!empty($s['start'])||!empty($s['end'])||!empty($s['visibility']))){return false;} return DisplayRules::visible($s,$this->post_id); } private function style_attributes(array $s): array { $css='';$cls='';$attrs=''; if(!empty($s['hideTablet'])){$cls.=' dbs-hide-tablet';} foreach(['Tablet','Mobile'] as $device){ $lower=strtolower($device);$align=$s['align'.$device]??'inherit';if($align!=='inherit'){$cls.=' dbs-align-'.$lower.'-'.$align;} foreach(['Top','Right','Bottom','Left'] as $side){$value=$s['padding'.$side.$device]??-1;if($value>=0){$css.='--dbs-pad-'.strtolower($side).'-'.$lower.':'.$value.'px;';$cls.=' dbs-pad-'.strtolower($side).'-'.$lower;}} if(($s['space'.$device]??-1)>=0){$css.='--dbs-space-'.$lower.':'.$s['space'.$device].'px;';$cls.=' dbs-space-'.$lower;} } foreach(['Top','Right','Bottom','Left'] as $side){$key='padding'.$side;if(in_array($key,$s['layoutOverrides']??[],true)&&empty($s[$key])){$css.='padding-'.strtolower($side).':0px;';}} foreach(['marginTop'=>'margin-top','maxWidth'=>'max-width','minHeight'=>'min-height'] as $key=>$property){if(!empty($s[$key])){$css.=$property.':'.$s[$key].'px;';}} if(($s['opacity']??100)<100){$css.='opacity:'.($s['opacity']/100).';';} if(!empty($s['rotate'])){$css.='rotate:'.$s['rotate'].'deg;';} if(!empty($s['sticky'])){$cls.=' dbs-sticky';$css.='--dbs-sticky-top:'.$s['stickyTop'].'px;';} if(!empty($s['zIndex'])){$css.='z-index:'.$s['zIndex'].';position:relative;';} if(($s['hover']??'none')!=='none'){$cls.=' dbs-hover-'.$s['hover'];} if(($s['animation']??'none')!=='none'&&!$this->preview){$attrs.=' data-dbs-reveal="'.esc_attr($s['animation']).'"';$css.='--dbs-duration:'.$s['duration'].'ms;--dbs-delay:'.$s['delay'].'ms;';} if(!empty($s['cssClass'])){$cls.=' '.$s['cssClass'];}if(!empty($s['customCSS'])){$css.=$s['customCSS'].';';} return ['css'=>$css,'class'=>$cls,'attributes'=>$attrs]; } private function shape(string $type): string { $paths=['wave'=>'M0 50 Q250 100 500 50 T1000 50 V100 H0 Z','triangle'=>'M0 0 L500 100 L1000 0 V100 H0 Z','curve'=>'M0 0 Q500 150 1000 0 V100 H0 Z']; return isset($paths[$type])?'':''; } private function interaction_attributes(string $type,string $node): string { return ' data-source-chain="'.esc_attr(wp_json_encode($this->source_chain)).'" data-dbs-'.$type.' data-nonce="'.esc_attr(is_user_logged_in()?wp_create_nonce('wp_rest'):'').'" data-post="'.$this->source_id.'" data-node="'.esc_attr($node).'" data-endpoint="'.esc_url(rest_url('digiter-builder/v1/interactions')).'" data-preview="'.($this->preview?'1':'0').'"'; } private function quiz(array $p,string $id,string $node): string { $questions=[]; foreach(array_slice($p['questions'],0,20) as $sourceIndex=>$question){ $options=array_slice($this->lines((string)$question['options']),0,12); if(trim((string)$question['question'])===''||count($options)<2){continue;} $questions[]=['question'=>$question['question'],'options'=>$options,'sourceIndex'=>$sourceIndex]; } if(!$questions){return $this->empty('Add at least one quiz question with two answers.');} $h='
    interaction_attributes('quiz',$node).' data-layout="'.esc_attr($p['layout']).'" data-shuffle="'.esc_attr($p['shuffle']).'">'; if($p['title']){$h.='

    '.$this->text($p['title']).'

    ';}if($p['intro']){$h.='

    '.$this->text($p['intro']).'

    ';} $h.='

    '; foreach($questions as $i=>$question){$h.='
    '.($i+1).'. '.$this->text($question['question']).'
    '; foreach($question['options'] as $j=>$option){$input='dbs-quiz-'.sanitize_html_class($id).'-'.$i.'-'.$j;$h.='';} $h.='
    ';} $review_title='dbs-quiz-review-title-'.sanitize_html_class($id); $h.='
    '; return $h; } private function interaction_allowed(): bool { return $this->interaction_context && ($this->preview || PostTypes::public_source($this->source_id,$this->source_chain)); } private function poll(array $p,string $id,string $node): string { $options=[];foreach($p['options'] as $sourceIndex=>$option){if(trim((string)($option['label']??''))!==''){$option['sourceIndex']=$sourceIndex;$options[]=$option;}if(count($options)>=12){break;}} if(count($options)<2){return $this->empty('Add at least two poll choices.');} $closed=$p['closes']&&time()>=strtotime($p['closes']); $h='
    interaction_attributes('poll',$node).' data-results="'.esc_attr($p['results']).'">'; if($p['title']){$h.='

    '.$this->text($p['title']).'

    ';}$h.=''.$this->text($p['question']).'
    '; foreach($options as $i=>$option){$input='dbs-poll-'.sanitize_html_class($id).'-'.$i;$h.='';} $h.='

    '.($closed?$this->text($p['closedMessage']):'').'
    '; return $h; } private function video(string $url): string { if (!$url) { return $this->empty('Add a video URL'); } $host=strtolower((string)wp_parse_url($url,PHP_URL_HOST)); $path=(string)wp_parse_url($url,PHP_URL_PATH);$src=''; if (in_array($host,['youtube.com','www.youtube.com','m.youtube.com','youtu.be'],true)) { parse_str((string)wp_parse_url($url,PHP_URL_QUERY),$q);$id=$host==='youtu.be'?trim($path,'/'):($q['v']??basename($path)); if (is_string($id) && preg_match('/^[a-zA-Z0-9_-]{11}$/D',$id)) { $src='https://www.youtube-nocookie.com/embed/'.$id; } } elseif (in_array($host,['vimeo.com','www.vimeo.com','player.vimeo.com'],true) && preg_match('/\/(\d+)\/?$/D',$path,$m)) { $src='https://player.vimeo.com/video/'.$m[1]; } if($src) { return '
    '; } if(preg_match('/\.(mp4|webm|ogg)$/i',$path)) { return ''; } return ''.esc_html__('Open video','digiter-builder-lite').''; } private function post_excerpt(\WP_Post $post): string { // Avoid the_content filters here: during a main-loop render they can re-enter this page's builder. $text=trim($post->post_excerpt); if(''===$text){$text=strip_shortcodes($post->post_content);} return wp_trim_words(wp_strip_all_tags($text),28); } public function query_fragment(array $node): string {return in_array($node['type'],['posts','loop'],true)?$this->node($node):'';} private function posts(array $p,string $node=''): string { $q=($p['source']??'custom')==='current-query'&&!$this->preview?$GLOBALS['wp_query']:ContentQuery::query($p,$this->post_id);$interactive=($p['source']??'custom')==='custom'&&!$this->fallback&&$this->interaction_allowed();$more=$interactive&&ContentQuery::has_more($p,$q);$align=$this->text_align_class($p['textAlign']??'left');$h='
    source_id,$node,$this->preview,$more,$this->source_chain):'').'>'.($p['title']?'

    '.$this->text($p['title']).'

    ':'').($interactive?ContentQuery::controls($p):'').'
    '; foreach(($q->posts??[]) as $post) { if($post->post_status!=='publish'||post_password_required($post)){continue;} $h.=''; } if(!$q->posts){$h.='

    No matching content.

    ';}return $h.'
    '.($interactive?ContentQuery::tail($p,$more):'').'
    '; } }