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 image Click 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=' '; }
$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 ''.$content.' ';
}
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='';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']?'
'.$this->text($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']).''.$p['level'].'>':$this->empty('Your headline');
$h=''; break;
case 'heading': $h=$p['text']?'<'.$p['level'].' class="dbs-heading">'.$this->text($p['text']).''.$p['level'].'>':$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=''.($p['icon']?''.Icons::symbol($p['icon']).' ':'').($p['title']?''.$this->text($p['title']).' ':'').''.($p['content']?$this->rich($p['content']):$this->empty('Write inside this bubble')).'
'; 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.=''.$tag.'>'; break;
case 'facts':
$h=''; break;
case 'table':
$headers=array_slice($this->lines($p['headers']),0,12); $h=''.($p['caption']?''.$this->text($p['caption']).' ':'').'';
foreach ($headers as $cell) { $h.=''.$this->text($cell).' '; } $h.=' ';
foreach (array_slice($this->lines($p['rows']),0,100) as $row) { $cells=explode('|',$row);$h.='';for($i=0;$i'.$this->text(trim($cells[$i]??'')).''; } $h.=' '; }
$h.='
'.($p['note']?''.$this->text($p['note']).'
':'');break;
case 'toc': $h=''.$this->text($p['title']).' ';foreach($this->headings as $head) { $h.=''.$this->text($head['text']).' '; }$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.=''.$this->text($r['title']).' '; }$h.='
';
foreach($p['items'] as $i=>$r) { $h.='
'.$this->rich($r['content']).' '; }$h.='
';break;
case 'gallery':
$h='';foreach($p['images'] as $img) { $img['ratio']=$p['ratio'];if($img['url']) {$h.='
'.$this->image($img).' ';}}$h.=(!$p['images']?$this->empty('Choose gallery images'):'').'
';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->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.=''.('steps'===$type?($i+1):$this->text($r['date'])).' '.$this->text($r['title']).' '.$this->rich($r['content']).'
'; }$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']).' ';foreach($this->lines($p['features']) as $line) { $h.=''.$this->text($line).' '; }$h.=' '.$this->action($p,$style['audio']??[]).'
';break;
case 'countdown':
$timestamp=strtotime($p['date']);$h='
'.$this->text($p['title']).' '.($timestamp?''.$this->text($p['date']).' ':$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='';
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='';
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):'').''.($interactive?ContentQuery::tail($p,$more):'').' ';
}
}