` is * already taken by StreamSera's own blocks. * * @package StreamSera */ namespace StreamSera\Integrations\Builders; use StreamSera\Frontend\Elements; if ( ! defined( 'ABSPATH' ) ) { exit; } class Divi5 { const PREFIX = 'streamsera-divi/'; /** * Hooks. Called when plugins load: Divi 5 builds its module dependency tree while the theme loads, before `init`. */ public static function hooks() { add_action( 'divi_module_library_modules_dependency_tree', array( __CLASS__, 'dependencies' ) ); add_action( 'divi_visual_builder_assets_before_enqueue_scripts', array( __CLASS__, 'enqueue_builder' ) ); add_filter( 'divi.moduleLibrary.conversion.moduleConversionOutline', array( __CLASS__, 'conversion_outline_filter' ), 10, 2 ); add_action( 'rest_api_init', array( __CLASS__, 'routes' ) ); } /** * REST permission: render a module preview in the Divi 5 Visual Builder (users who can edit posts). * * @return bool */ public static function can_render() { return current_user_can( 'edit_posts' ); } /** * Is Divi 5's module library loaded? * * @return bool */ public static function active() { return class_exists( '\ET\Builder\Packages\ModuleLibrary\ModuleRegistration' ) && class_exists( '\ET\Builder\Packages\Module\Module' ); } /** * Legacy (Divi 4 API) module slug of an element: `streamsera_player`, otherwise `streamsera_divi_`. * * @param string $id Element id. * @return string */ public static function legacy_slug( $id ) { return 'player' === $id ? 'streamsera_player' : 'streamsera_divi_' . str_replace( '-', '_', $id ); } /** * Element ids registered as native Divi 5 modules. * * Divi builds its module list while the theme loads (before `init`), so this is a static list rather * than the element registry. Add-ons add their element ids with the `streamsera_divi5_modules` filter and * point at their module folders with `streamsera_divi5_module_folder`. * * @return string[] */ public static function ids() { /** * Filter the elements registered as native Divi 5 modules (each needs a folder with a module.json). * * @param string[] $ids Element ids. */ $ids = (array) apply_filters( 'streamsera_divi5_modules', array( 'player', 'listen-button', 'now-playing', 'station', 'station-list' ) ); return array_values( array_filter( array_unique( $ids ), static function ( $id ) { return file_exists( self::folder( $id ) . 'module.json' ); } ) ); } /** * Module folder (holds module.json). * * @param string $id Element id. * @return string */ public static function folder( $id ) { /** * Filter the folder of a Divi 5 module (add-ons ship folders for their own elements). * * @param string $folder Folder path with a trailing slash. * @param string $id Element id. */ return trailingslashit( (string) apply_filters( 'streamsera_divi5_module_folder', STREAMSERA_PATH . 'divi5/' . $id . '/', $id ) ); } /** * Add the modules to Divi's dependency tree (they register when Divi loads its own modules). * * @param object $tree \ET\Builder\Framework\DependencyManagement\DependencyTree. */ public static function dependencies( $tree ) { if ( ! is_object( $tree ) || ! method_exists( $tree, 'add_dependency' ) || ! interface_exists( '\ET\Builder\Framework\DependencyManagement\Interfaces\DependencyInterface' ) ) { return; } require_once __DIR__ . '/Divi5_Module.php'; foreach ( self::ids() as $id ) { $tree->add_dependency( new Divi5_Module( $id ) ); } } /* --------------------------------------------------------------------------------------------------------- * Icons (24×24 line icons, one per element; shared with the Divi 4 modules) * ------------------------------------------------------------------------------------------------------ */ /** * Icon shapes: element id → list of [ tag, attributes ]. 'streamsera' is the folder / fallback icon. * * @return array */ public static function icons() { $icons = array( 'streamsera' => array( array( 'circle', array( 'cx' => 12, 'cy' => 12, 'r' => 2.5 ) ), array( 'path', array( 'd' => 'M7.8 16.2a6 6 0 0 1 0-8.4M16.2 7.8a6 6 0 0 1 0 8.4M4.9 19.1a10 10 0 0 1 0-14.2M19.1 4.9a10 10 0 0 1 0 14.2' ) ), ), // Play button in a circle. 'player' => array( array( 'circle', array( 'cx' => 12, 'cy' => 12, 'r' => 9.5 ) ), array( 'path', array( 'd' => 'M10 8.2v7.6l6-3.8z' ) ), ), // Pill button with a "live" dot. 'listen-button' => array( array( 'rect', array( 'x' => 2.5, 'y' => 7, 'width' => 19, 'height' => 10, 'rx' => 5 ) ), array( 'circle', array( 'cx' => 7.5, 'cy' => 12, 'r' => 1.5 ) ), array( 'path', array( 'd' => 'M11.5 12h6' ) ), ), // Music note. 'now-playing' => array( array( 'path', array( 'd' => 'M9 18V5.5l11-2.5v12.5' ) ), array( 'circle', array( 'cx' => 6.5, 'cy' => 18, 'r' => 2.5 ) ), array( 'circle', array( 'cx' => 17.5, 'cy' => 15.5, 'r' => 2.5 ) ), ), // Radio set with antenna. 'station' => array( array( 'rect', array( 'x' => 2.5, 'y' => 8.5, 'width' => 19, 'height' => 12, 'rx' => 2 ) ), array( 'path', array( 'd' => 'M6.5 8.5l11-5' ) ), array( 'circle', array( 'cx' => 8.5, 'cy' => 14.5, 'r' => 3 ) ), array( 'path', array( 'd' => 'M15 12.5h3M15 16.5h3' ) ), ), // Grid of cards. 'station-list' => array( array( 'rect', array( 'x' => 3, 'y' => 3, 'width' => 7.5, 'height' => 7.5, 'rx' => 1.5 ) ), array( 'rect', array( 'x' => 13.5, 'y' => 3, 'width' => 7.5, 'height' => 7.5, 'rx' => 1.5 ) ), array( 'rect', array( 'x' => 3, 'y' => 13.5, 'width' => 7.5, 'height' => 7.5, 'rx' => 1.5 ) ), array( 'rect', array( 'x' => 13.5, 'y' => 13.5, 'width' => 7.5, 'height' => 7.5, 'rx' => 1.5 ) ), ), ); /** * Filter the module icons (element id → list of [ tag, attributes ] on a 24×24 line-icon grid). * * @param array $icons Icons. */ return (array) apply_filters( 'streamsera_divi_icons', $icons ); } /* --------------------------------------------------------------------------------------------------------- * Settings definition (shared by the builder script and the conversion outline) * ------------------------------------------------------------------------------------------------------ */ /** * Colour settings: attribute → [ label, CSS property ]. * * @return array */ private static function color_settings() { return array( 'accent' => array( __( 'Accent', 'streamsera' ), '--ss-widget-accent' ), 'bg' => array( __( 'Card background', 'streamsera' ), '--ss-widget-bg' ), 'border' => array( __( 'Border', 'streamsera' ), '--ss-widget-border' ), 'text' => array( __( 'Text', 'streamsera' ), 'color' ), ); } /** * Does the element get the colour settings? (Not the player or add-on elements with their own design system.) * * @param string $id Element id. * @return bool */ private static function has_colors( $id ) { return ! Elements::has_own_styles( $id ); } /** * Full module metadata (module.json shape) for the Visual Builder. * * @param string $id Element id. * @param array $el Element. * @return array { metadata, defaultAttrs } */ public static function definition( $id, $el ) { $items = array(); $defaults = array(); $priority = 0; foreach ( $el['fields'] as $key => $field ) { $attr = Elements::attr( $key ); $priority += 10; $component = self::component( $field ); $items[ $attr ] = array( 'groupType' => 'group-item', 'item' => array( 'groupSlug' => 'display' === $field['section'] ? 'contentDisplay' : 'contentStreamsera', 'attrName' => 'streamsera.advanced.' . $attr, 'label' => $field['label'], 'description' => $field['help'], 'category' => 'configuration', 'priority' => $priority, 'render' => true, 'features' => array( 'hover' => false, 'sticky' => false, 'responsive' => false, 'preset' => 'content', ), 'component' => $component['component'], ), ); if ( null !== $component['default'] ) { $defaults[ $attr ] = array( 'desktop' => array( 'value' => $component['default'] ) ); } } $groups = array( 'contentStreamsera' => array( 'panel' => 'content', 'priority' => 10, 'multiElements' => false, 'groupName' => 'streamsera', 'component' => array( 'name' => 'divi/composite', 'props' => array( 'groupLabel' => __( 'Content', 'streamsera' ) ), ), ), 'contentDisplay' => array( 'panel' => 'content', 'priority' => 20, 'multiElements' => false, 'groupName' => 'streamseraDisplay', 'component' => array( 'name' => 'divi/composite', 'props' => array( 'groupLabel' => __( 'Display', 'streamsera' ) ), ), ), ); if ( self::has_colors( $id ) ) { $groups['designStreamsera'] = array( 'panel' => 'design', 'priority' => 5, 'multiElements' => false, 'groupName' => 'streamseraColors', 'component' => array( 'name' => 'divi/composite', 'props' => array( 'groupLabel' => __( 'StreamSera colours', 'streamsera' ), 'clipboardCategory' => 'style', ), ), ); $priority = 0; foreach ( self::color_settings() as $attr => $setting ) { $priority += 10; $items[ $attr ] = array( 'groupType' => 'group-item', 'item' => array( 'groupSlug' => 'designStreamsera', 'attrName' => 'streamsera.advanced.' . $attr, 'label' => $setting[0], 'category' => 'color_option', 'priority' => $priority, 'render' => true, 'features' => array( 'hover' => false, 'sticky' => false, 'responsive' => false, 'preset' => array( 'style' ), ), 'component' => array( 'type' => 'field', 'name' => 'divi/color-picker', ), ), ); } $items['radius'] = array( 'groupType' => 'group-item', 'item' => array( 'groupSlug' => 'designStreamsera', 'attrName' => 'streamsera.advanced.radius', 'label' => __( 'Corner radius', 'streamsera' ), 'category' => 'configuration', 'priority' => 60, 'render' => true, 'features' => array( 'hover' => false, 'sticky' => false, 'responsive' => false, 'preset' => array( 'style' ), ), 'component' => array( 'type' => 'field', 'name' => 'divi/range', 'props' => array( 'min' => 0, 'max' => 32, 'minLimit' => 0, 'step' => 1, 'allowedUnits' => array( 'px' ), 'defaultUnit' => 'px', ), ), ), ); } $module_class = 'streamsera_divi5_' . str_replace( '-', '_', $id ); /* translators: %s: element name, e.g. "Station". */ $title = sprintf( __( 'StreamSera · %s', 'streamsera' ), $el['label'] ); $metadata = array( 'name' => self::PREFIX . $id, 'd4Shortcode' => self::legacy_slug( $id ), 'moduleClassName' => $module_class, 'moduleOrderClassName' => $module_class, 'title' => $title, 'titles' => $title, 'moduleIcon' => 'streamsera/' . $id, 'category' => 'module', 'folder' => 'streamsera', 'attributes' => array( 'module' => array( 'type' => 'object', 'selector' => '{{selector}}', 'settings' => array( 'meta' => array( 'meta' => (object) array() ), 'advanced' => array( 'html' => (object) array(), 'link' => (object) array(), ), 'decoration' => array_fill_keys( array( 'animation', 'background', 'border', 'boxShadow', 'conditions', 'disabledOn', 'filters', 'overflow', 'position', 'scroll', 'sizing', 'spacing', 'sticky', 'transform', 'transition', 'zIndex' ), (object) array() ), ), ), 'streamsera' => array( 'type' => 'object', 'settings' => array( 'advanced' => $items ), ), ), 'customCssFields' => (object) array(), 'settings' => array( 'content' => 'auto', 'design' => 'auto', 'advanced' => 'auto', 'groups' => $groups, ), ); return array( 'metadata' => $metadata, 'defaultAttrs' => array( 'module' => array( 'meta' => array( 'adminLabel' => array( 'desktop' => array( 'value' => $title ) ) ), ), 'streamsera' => array( 'advanced' => $defaults ? $defaults : (object) array() ), ), ); } /** * Divi 5 field component for an element field. * * @param array $field Field. * @return array { component, default (string|null) } */ private static function component( $field ) { switch ( $field['type'] ) { case 'toggle': return array( 'component' => array( 'type' => 'field', 'name' => 'divi/toggle', ), 'default' => $field['default'] ? 'on' : 'off', ); case 'number': $min = isset( $field['min'] ) ? (int) $field['min'] : 0; $max = isset( $field['max'] ) ? (int) $field['max'] : 100; return array( 'component' => array( 'type' => 'field', 'name' => 'divi/range', 'props' => array( 'min' => $min, 'max' => $max, 'minLimit' => $min, 'maxLimit' => $max, 'step' => 1, 'allowedUnits' => array(), 'defaultUnit' => '', ), ), 'default' => (string) (int) $field['default'], ); case 'text': return array( 'component' => array( 'type' => 'field', 'name' => 'divi/text', 'props' => array( 'showPlaceholderOnEmpty' => true ), ), 'default' => '' === (string) $field['default'] ? null : (string) $field['default'], ); default: $options = array(); foreach ( Elements::options( $field ) as $value => $label ) { $options[ (string) $value ] = array( 'label' => html_entity_decode( (string) $label, ENT_QUOTES, 'UTF-8' ) ); } $default = (string) $field['default']; if ( ! isset( $options[ $default ] ) ) { $default = (string) array_key_first( $options ); } return array( 'component' => array( 'type' => 'field', 'name' => 'divi/select', 'props' => array( 'options' => (object) $options ), ), 'default' => $default, ); } } /* --------------------------------------------------------------------------------------------------------- * Divi 4 → Divi 5 conversion * ------------------------------------------------------------------------------------------------------ */ /** * Conversion outline for a module: legacy shortcode attributes → Divi 5 attributes. * * @param string $id Element id. * @return array */ public static function conversion_outline( $id ) { $el = Elements::get( $id ); if ( ! $el ) { return array(); } $module = array(); foreach ( array_keys( $el['fields'] ) as $key ) { $module[ $key ] = 'streamsera.advanced.' . Elements::attr( $key ) . '.*'; } if ( self::has_colors( $id ) ) { foreach ( array_keys( self::color_settings() ) as $attr ) { $module[ 'streamsera_' . $attr ] = 'streamsera.advanced.' . $attr . '.*'; } $module['streamsera_radius'] = 'streamsera.advanced.radius.*'; } return array( 'advanced' => array( 'admin_label' => 'module.meta.adminLabel', 'animation' => 'module.decoration.animation', 'background' => 'module.decoration.background', 'borders' => array( 'default' => 'module.decoration.border' ), 'box_shadow' => array( 'default' => 'module.decoration.boxShadow' ), 'disabled_on' => 'module.decoration.disabledOn', 'display_conditions' => 'module.decoration.conditions', 'filters' => array( 'default' => 'module.decoration.filters' ), 'height' => 'module.decoration.sizing', 'margin_padding' => 'module.decoration.spacing', 'max_width' => 'module.decoration.sizing', 'module' => 'module.advanced.htmlAttributes', 'overflow' => 'module.decoration.overflow', 'position_fields' => 'module.decoration.position', 'scroll' => 'module.decoration.scroll', 'sticky' => 'module.decoration.sticky', 'transform' => 'module.decoration.transform', 'transition' => 'module.decoration.transition', 'z_index' => 'module.decoration.zIndex', ), 'css' => array( 'before' => 'css.*.before', 'main_element' => 'css.*.mainElement', 'after' => 'css.*.after', 'free_form' => 'css.*.freeForm', ), 'module' => $module, ); } /** * Supply the conversion outline for StreamSera modules (Divi reads it when converting legacy layouts). * * @param array $outline Outline. * @param string $module_name Module name. * @return array */ public static function conversion_outline_filter( $outline, $module_name ) { if ( is_string( $module_name ) && 0 === strpos( $module_name, self::PREFIX ) ) { $id = substr( $module_name, strlen( self::PREFIX ) ); if ( in_array( $id, self::ids(), true ) ) { return self::conversion_outline( $id ); } } return $outline; } /* --------------------------------------------------------------------------------------------------------- * Rendering * ------------------------------------------------------------------------------------------------------ */ /** * Element HTML for Divi 5 attributes, wrapped with the colour variables. * * @param string $id Element id. * @param array $streamsera The module's `streamsera` attribute. * @return string */ public static function render_element( $id, $streamsera ) { $el = Elements::get( $id ); if ( ! $el ) { return ''; } $values = isset( $streamsera['advanced'] ) && is_array( $streamsera['advanced'] ) ? $streamsera['advanced'] : array(); $args = array(); foreach ( array_keys( $el['fields'] ) as $key ) { $value = self::value( $values, Elements::attr( $key ) ); if ( null !== $value ) { $args[ $key ] = $value; } } $html = Elements::render( $id, $args ); if ( '' === $html ) { return ''; } $css = ''; if ( self::has_colors( $id ) ) { foreach ( self::color_settings() as $attr => $setting ) { $value = self::color( self::value( $values, $attr ) ); if ( '' !== $value ) { $css .= $setting[1] . ':' . $value . ';'; } } $radius = self::value( $values, 'radius' ); if ( null !== $radius && preg_match( '/^\d+(\.\d+)?(px)?$/', $radius ) ) { $css .= '--ss-widget-radius:' . ( (int) $radius ) . 'px;'; } } return '
' . wp_kses( $html, streamsera_allowed_html() ) . '
'; } /** * Desktop value of an attribute (settings are not responsive). * * @param array $values `streamsera.advanced`. * @param string $attr Attribute. * @return string|null */ private static function value( $values, $attr ) { if ( ! isset( $values[ $attr ]['desktop']['value'] ) ) { return null; } $value = $values[ $attr ]['desktop']['value']; return is_scalar( $value ) ? (string) $value : null; } /** * A safe CSS colour (global colour variables resolved). * * @param string|null $value Value. * @return string */ private static function color( $value ) { if ( null === $value || '' === $value ) { return ''; } if ( ( 0 === strpos( $value, '$variable(' ) || 0 === strpos( $value, 'gcid-' ) ) && is_callable( array( '\ET\Builder\Packages\GlobalData\GlobalData', 'resolve_global_color_variable' ) ) ) { $value = (string) \ET\Builder\Packages\GlobalData\GlobalData::resolve_global_color_variable( $value ); } return preg_match( '/^[#a-z0-9(),.%\s\-]+$/i', $value ) ? $value : ''; } /* --------------------------------------------------------------------------------------------------------- * Visual Builder * ------------------------------------------------------------------------------------------------------ */ /** * Register the builder script with Divi's package manager (loaded in the builder's app frame). */ public static function enqueue_builder() { if ( ! self::active() || ! class_exists( '\ET\Builder\VisualBuilder\Assets\PackageBuildManager' ) ) { return; } $modules = array(); foreach ( self::ids() as $id ) { $el = Elements::get( $id ); if ( $el ) { $def = self::definition( $id, $el ); $def['conversionOutline'] = self::conversion_outline( $id ); $modules[] = $def; } } $file = STREAMSERA_PATH . 'assets/divi5/visual-builder.js'; \ET\Builder\VisualBuilder\Assets\PackageBuildManager::register_package_build( array( 'name' => 'streamsera-divi5', 'version' => STREAMSERA_VERSION . '.' . ( file_exists( $file ) ? filemtime( $file ) : 0 ), 'script' => array( 'src' => STREAMSERA_URL . 'assets/divi5/visual-builder.js', 'deps' => array( 'divi-module-library', 'divi-module', 'divi-vendor-wp-hooks', 'divi-vendor-react' ), 'enqueue_top_window' => false, 'enqueue_app_window' => true, 'args' => array( 'in_footer' => false ), 'data_app_window' => array( 'modules' => $modules, 'folder' => array( 'name' => 'streamsera', 'path' => '', 'title' => 'StreamSera', 'icon' => 'streamsera/streamsera', 'category' => 'module', ), 'icons' => self::icons(), 'rest' => esc_url_raw( rest_url( 'streamsera/v1/divi5/render' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), // Origins the preview may load registered scripts / styles from (this site's own). 'origins' => self::asset_origins(), 'i18n' => array( 'loading' => __( 'Loading…', 'streamsera' ), 'empty' => __( 'Nothing to show yet: check the module settings.', 'streamsera' ), ), ), ), ) ); // The preview mounts StreamSera's widgets (player, filters, live data) inside the builder frame. if ( class_exists( '\StreamSera\Frontend\Assets' ) ) { \StreamSera\Frontend\Assets::enqueue(); } } /** * Styles and scripts queued since $before (with their dependencies), as URLs the builder can load. * * @param array $before { style: handles, script: handles }. * @return array[] { type, handle, src, inline } */ private static function new_assets( $before ) { $out = array(); foreach ( array( 'style' => wp_styles(), 'script' => wp_scripts() ) as $type => $deps ) { $added = array_diff( $deps->queue, $before[ $type ] ); if ( ! $added ) { continue; } $deps->to_do = array(); $deps->all_deps( $added ); foreach ( $deps->to_do as $handle ) { $item = $deps->registered[ $handle ] ?? null; if ( ! $item || ! $item->src || ! is_string( $item->src ) ) { continue; } $src = $item->src; if ( ! preg_match( '|^(https?:)?//|', $src ) ) { $src = $deps->base_url . $src; } $ver = $item->ver ? $item->ver : $deps->default_version; if ( $ver ) { $src = add_query_arg( 'ver', $ver, $src ); } $inline = ''; if ( 'script' === $type ) { $inline = (string) $deps->get_data( $handle, 'data' ); $before_js = $deps->get_data( $handle, 'before' ); if ( is_array( $before_js ) ) { $inline .= "\n" . implode( "\n", $before_js ); } } else { $after_css = $deps->get_data( $handle, 'after' ); $inline = is_array( $after_css ) ? implode( "\n", $after_css ) : ''; } $out[] = array( 'type' => $type, 'handle' => $handle, 'src' => esc_url_raw( $src ), 'inline' => $inline, ); } $deps->to_do = array(); } return $out; } /** * Origins (scheme://host[:port]) this site serves its scripts and styles from: the site, WordPress * includes, wp-content and StreamSera itself. The builder preview only loads assets from these. * * @return string[] */ private static function asset_origins() { $origins = array(); foreach ( array( home_url( '/' ), site_url( '/' ), includes_url(), content_url( '/' ), STREAMSERA_URL ) as $url ) { $parts = wp_parse_url( $url ); if ( ! empty( $parts['scheme'] ) && ! empty( $parts['host'] ) ) { $origins[] = strtolower( $parts['scheme'] . '://' . $parts['host'] . ( ! empty( $parts['port'] ) ? ':' . (int) $parts['port'] : '' ) ); } } return array_values( array_unique( $origins ) ); } /** * Preview endpoint for the builder. */ public static function routes() { // Only sites running Divi 5 need the builder preview endpoint. if ( ! self::active() ) { return; } register_rest_route( 'streamsera/v1', '/divi5/render', array( 'methods' => 'POST', 'permission_callback' => array( __CLASS__, 'can_render' ), 'args' => array( 'module' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', 'validate_callback' => 'rest_validate_request_arg', ), 'streamsera' => array( 'type' => 'object', 'default' => array(), // Module settings: text values only (each is then normalized against the element's // field definition, and colours / radius against strict patterns). 'sanitize_callback' => static function ( $value ) { return is_array( $value ) ? map_deep( $value, static fn( $v ) => is_scalar( $v ) ? sanitize_text_field( (string) $v ) : '' ) : array(); }, ), ), 'callback' => function ( \WP_REST_Request $request ) { $id = (string) $request->get_param( 'module' ); if ( 0 === strpos( $id, self::PREFIX ) ) { $id = substr( $id, strlen( self::PREFIX ) ); } if ( ! Elements::get( $id ) ) { return new \WP_Error( 'streamsera_unknown_element', __( 'Unknown element.', 'streamsera' ), array( 'status' => 404 ) ); } $streamsera = $request->get_param( 'streamsera' ); // Front-end handles are registered on wp_enqueue_scripts, which REST requests skip. Register // exactly the StreamSera handles (core + add-ons, through `streamsera_register_standalone_assets`) // instead of firing the theme's / every plugin's wp_enqueue_scripts callbacks here, note // what is already queued, then report what the element added (e.g. an add-on's // stylesheet) so the builder can load it into its frame. ob_start(); // Defensive: a callback on the registration hook could print. if ( class_exists( '\\StreamSera\\Frontend\\Assets' ) && method_exists( '\\StreamSera\\Frontend\\Assets', 'register_standalone' ) ) { \StreamSera\Frontend\Assets::register_standalone(); } ob_end_clean(); $before = array( 'style' => wp_styles()->queue, 'script' => wp_scripts()->queue, ); ob_start(); $html = self::render_element( $id, is_array( $streamsera ) ? $streamsera : array() ); ob_end_clean(); return rest_ensure_response( array( 'html' => $html, 'assets' => self::new_assets( $before ), ) ); }, ) ); } }