$placement->get_public_id(), "data-{$prefix}-instance" => uniqid( '', false ), ]; return self::render_attributes( $attrs ); } /** * Level 2: Generates the attributes for a group (collection). * * @param Entity $group The group object. * * @return string HTML-Attribute string */ public static function for_collection( Entity $group ): string { $prefix = self::get_prefix(); $attrs = [ "data-{$prefix}-collection" => $group->get_id(), ]; return self::render_attributes( $attrs ); } /** * Level 3: Generates the attributes for an ad. * This is the anchor for the JS tracking. * * @param Entity $ad The ad object. * @param array $context The Render-context (e.g. placement_id, group_id). * * @return string HTML-Attribute string */ public static function for_item( Entity $ad, array $context = [] ): string { $prefix = self::get_prefix(); $render_instance_id = wp_generate_uuid4(); // Item-ID for JS and Debugger $attrs = [ "data-{$prefix}-item" => $ad->get_id(), "data-{$prefix}-rid" => $render_instance_id, ]; if ( self::is_item_tracked( $ad ) ) { // 't' signalizes that the ad should be tracked. $attrs["data-{$prefix}-t"] = '1'; // Only the tracking context, under neutral names; other render-context keys (is_raw_output, ...) stay internal. $context_attrs = [ 'placement_id' => 'w-ctx', // Wrapper Context 'group_id' => 'c-ctx', // Collection Context ]; foreach ( $context_attrs as $key => $safe_key ) { if ( isset( $context[ $key ] ) && is_scalar( $context[ $key ] ) ) { $attrs["data-{$prefix}-{$safe_key}"] = $context[ $key ]; } } } return self::render_attributes( $attrs ); } /** * Whether an ad carries the tracking flag. Shared by the SSR attributes and the JS config (Ad::processBackendData()), * so both rendering paths decide the same way. * * @param Entity $ad * * @return bool */ public static function is_item_tracked( Entity $ad ): bool { return (bool) Settings::get_setting( 'enable_module_tracking', false ) && (bool) $ad->get_meta( 'tracking_enabled', true ); } /** * Level 4 (JS): Generates the attributes for the invisible Placeholder script (Client-Side Rendering). * * @param Placement $placement The placement object. * * @return string HTML-Attribute string */ public static function for_placeholder( Placement $placement ): string { $prefix = self::get_prefix(); $item = $placement->get_item(); $item_id = $item ? $item->get_id() : 0; $item_type = $item ? ( ( $item instanceof Group ) ? '2' : '1' ) : '0'; // 1 = Ad, 2 = Group, 0 = None $attrs = [ 'id' => sprintf( '%s-%s', $prefix, $placement->get_public_id() ), // Script will be removed from the DOM anyway. "data-{$prefix}-wrap" => $placement->get_public_id(), "data-{$prefix}-item" => $item_id, "data-{$prefix}-type" => $item_type, ]; // Reserve space so the ad doesn't push content down when JS inserts it (CLS). // Only when the resolved item has known dimensions - same source the SSR uses. if ( $item instanceof Ad ) { $width = (int) ( $placement->frontend_config_data['item']['width'] ?? 0 ); $height = (int) ( $placement->frontend_config_data['item']['height'] ?? 0 ); if ( $width > 0 && $height > 0 ) { $attrs['style'] = sprintf( 'display:block;width:100%%;max-width:%dpx;aspect-ratio:%d / %d;', $width, $width, $height ); } } return self::render_attributes( $attrs ); } /** * Joins CSS declaration lists, e.g. an author's inline CSS without a trailing ";" and the alignment styles. * * @param string|null ...$parts * * @return string */ public static function combine_css( ?string ...$parts ): string { $parts = array_filter( array_map( static fn( ?string $part ): string => rtrim( trim( (string) $part ), '; ' ), $parts ) ); return $parts ? implode( '; ', $parts ) . ';' : ''; } /** * Get the frontend prefix from the settings. */ private static function get_prefix(): string { return sanitize_html_class( Settings::get_setting( 'frontend_prefix', Constants::DEFAULT_FRONTEND_PREFIX ) ); } /** * Transform the array to a final HTML string. */ private static function render_attributes( array $attributes ): string { $html = []; foreach ( $attributes as $key => $value ) { // Key is always secure and doesn't need escaping. $html[] = sprintf( '%s="%s"', $key, esc_attr( (string) $value ) ); } return implode( ' ', $html ); } /** * Generates all the standard attributes (classes, ID, styles, data) for an ad item. * * @param Entity $ad The ad. * @param string $type_id The id of the ad type (e.g. 'html', 'html5'). * @param array $context The render context. * * @return string Final Attribute string (e.g. 'class="..." style="..." data-...') */ public static function get_standard_item_attributes( Entity $ad, string $type_id, array $context = [] ): string { $prefix = self::get_prefix(); $ad_id = $ad->get_id(); // CSS Classes $type_class = sprintf( '%s-type-%s', $prefix, sanitize_html_class( $type_id ) ); $custom_classes = Util::sanitize_html_classes( (string) $ad->get_meta( 'custom_classes', '' ) ); $css_classes = sprintf( '%1$s-item %2$s %1$s-i-%3$d %4$s', $prefix, $type_class, $ad_id, $custom_classes ); // Custom ID $custom_html_id = $ad->get_meta( 'custom_id', '' ); // Styles, layout and alignment $inline_css = safecss_filter_attr( $ad->get_meta( 'inline_css', '' ) ); $alignment = safecss_filter_attr( $ad->get_layout_styles_string() ); $style = self::combine_css( $inline_css, $alignment ); $data_attributes = self::for_item( $ad, $context ); $attributes = []; if ( ! empty( $custom_html_id ) ) { $attributes[] = sprintf( 'id="%s"', esc_attr( $custom_html_id ) ); } $attributes[] = sprintf( 'class="%s"', esc_attr( trim( $css_classes ) ) ); if ( ! empty( $style ) ) { $attributes[] = sprintf( 'style="%s"', esc_attr( $style ) ); } return implode( ' ', $attributes ) . ' ' . $data_attributes; } }