context['airo-wp/queryId'] ) ) { $query_id = sanitize_key( (string) $block->context['airo-wp/queryId'] ); } // Authored mode — pass through unchanged. block.json `render` captures the // output buffer so we ECHO rather than return. if ( '' === $query_id ) { echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-rendered save.js output. return; } // Query mode — pull pre-rendered items from the parent container's stash. // Inner blocks render before their parent's render_callback fires, so we // can't rely on the query container having loaded the shared helpers yet. $helpers = AIRO_WP_PLUGIN_DIR . 'build/blocks/query/render-helpers.php'; if ( ! file_exists( $helpers ) ) { echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-rendered save.js output. return; } require_once $helpers; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- build artifact; path resolved from plugin directory $items_html = ''; if ( isset( $GLOBALS['airowp_query_items_html'][ $query_id ] ) ) { $items_html = (string) $GLOBALS['airowp_query_items_html'][ $query_id ]; } $atts = wp_parse_args( $attributes, array( 'minHeight' => '100vh', 'maxHeight' => '900px', 'constrainWidth' => true, 'contentWidth' => '', 'overlayColor' => '', 'overlayOpacity' => 80, 'navColor' => '', 'navActiveColor' => '', ) ); // Minimal parity with save.js's convertColorToCSSVar(): convert the WP preset // shorthand (`var:preset|color|slug`) to a real `var(...)`, otherwise pass // through as-is (hex/rgb/CSS keywords already work inline). // // Both branches run the final value through airowp_safe_css_value() so a // stored color like `red; content:'x'` or a crafted slug like `slug);}` can't // escape the `--prop: VALUE` declaration. Preset slug segments are also // constrained via sanitize_html_class() so only identifier characters survive. $color_to_css = static function ( $value ) { $value = (string) $value; if ( '' === $value ) { return ''; } if ( 0 === strpos( $value, 'var:preset|' ) ) { $parts = explode( '|', substr( $value, strlen( 'var:preset|' ) ) ); $parts = array_filter( array_map( 'sanitize_html_class', $parts ) ); $result = 'var(--wp--preset--' . implode( '--', $parts ) . ')'; return airowp_safe_css_value( $result ); } return airowp_safe_css_value( $value ); }; $classes = array( 'airo-wp-scroll-slides' ); if ( '' !== $atts['overlayColor'] ) { $classes[] = 'airo-wp-scroll-slides--has-overlay'; } if ( empty( $atts['constrainWidth'] ) ) { $classes[] = 'airo-wp-scroll-slides--no-width-constraint'; } if ( '' !== $atts['navColor'] || '' !== $atts['navActiveColor'] ) { $classes[] = 'airo-wp-scroll-slides--has-nav-color'; } $style_parts = array(); if ( '' !== $atts['overlayColor'] ) { // Clamp to [0, 100] and fall back to 80 for non-numeric values. Mirrors // the JS overlayOpacityFraction() util (src/utils/overlay-opacity.js) // used by save.js/edit.js — keep the two in sync. $overlay_opacity = is_numeric( $atts['overlayOpacity'] ) ? (float) $atts['overlayOpacity'] : 80.0; $overlay_opacity = max( 0.0, min( 100.0, $overlay_opacity ) ) / 100; $style_parts[] = '--airo-wp-overlay-color:' . $color_to_css( $atts['overlayColor'] ); $style_parts[] = '--airo-wp-overlay-opacity:' . rtrim( rtrim( sprintf( '%.4F', $overlay_opacity ), '0' ), '.' ); } if ( '' !== $atts['navColor'] ) { $style_parts[] = '--airo-wp-nav-color:' . $color_to_css( $atts['navColor'] ); } if ( '' !== $atts['navActiveColor'] ) { $style_parts[] = '--airo-wp-nav-active-color:' . $color_to_css( $atts['navActiveColor'] ); } $style_inline = implode( ';', $style_parts ); if ( '' !== $style_inline ) { $style_inline .= ';'; } $min_height = airowp_safe_css_value( $atts['minHeight'] ); if ( '' === $min_height ) { $min_height = '100vh'; } $wrapper_attrs_list = array( 'class' => implode( ' ', $classes ), 'data-airo-wp-min-height' => $min_height, ); if ( '' !== $atts['maxHeight'] ) { $max_h = airowp_safe_css_value( $atts['maxHeight'] ); if ( '' !== $max_h ) { $wrapper_attrs_list['data-airo-wp-max-height'] = $max_h; } } if ( '' !== $style_inline ) { $wrapper_attrs_list['style'] = $style_inline; } $wrapper_attrs = get_block_wrapper_attributes( $wrapper_attrs_list ); $inner_style = ''; if ( ! empty( $atts['constrainWidth'] ) ) { $max_width = '' !== $atts['contentWidth'] ? airowp_safe_css_value( $atts['contentWidth'] ) : 'var(--wp--style--global--content-size, 1140px)'; if ( '' === $max_width ) { $max_width = 'var(--wp--style--global--content-size, 1140px)'; } $inner_style = ' style="max-width:' . esc_attr( $max_width ) . ';margin-left:auto;margin-right:auto"'; } // The panels wrapper is this host's item container. Same role/id pair // airo-wp/query-results puts on its grid, so the shared view.js // plumbing (load-more append, aria-busy) finds the element that // actually holds the items. See slider/render.php for the same note. $panels_attrs = sprintf( 'class="airo-wp-scroll-slides__panels" data-airo-wp-query-results-role="container" data-airo-wp-query-id="%1$s"', esc_attr( $query_id ) ); printf( '
%4$s
', $wrapper_attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped via get_block_wrapper_attributes(). $inner_style, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- assembled from esc_attr(). $panels_attrs, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- assembled from esc_attr() escaped parts. $items_html // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- items rendered via WP_Block (WP escapes server-side). ); } } airowp_render_scroll_slides( $attributes, $content, $block );