', esc_attr( $collapse_id ), esc_attr( $body_id ), esc_attr__( 'Collapse or expand the section', 'acez-seo' ) ); } // The card header and the body opener are printed separately, each with // a LITERAL format string. The body id used to travel as a ready-made // ` id="…"` fragment in a variable, and a half-attribute like that can // never be escaped at the point of output — esc_attr() escapes a value, // not `id="x"` — which is exactly why that argument needed a suppression. printf( '
%1$s%2$s%4$s
', wp_kses( $this->tab_icon_svg( $icon ), KsesTags::svg() ), esc_html( $title ), '' !== $extra_class ? ' ' . esc_attr( $extra_class ) : '', wp_kses( $toggle, KsesTags::card_toggle() ) ); if ( '' !== $body_id ) { printf( '
', esc_attr( $body_id ) ); } else { echo '
'; } } public function card_close(): void { echo '
'; } public function form_open( string $tab ): void { printf( '
', esc_url( admin_url( 'admin-post.php' ) ) ); wp_nonce_field( self::NONCE_ACTION ); printf( '' ); printf( '', esc_attr( $tab ) ); } public function form_close(): void { echo '
'; } /** * A text field holding an attachment ID, with a "choose from Media * Library" button (wp.media modal), a clear button, and a thumbnail * preview. $field_id must be unique per page ($name may repeat across * the separate per-language forms). */ public function media_picker_field( string $name, string $field_id, string $label, int $value, string $help_key = '' ): void { $preview = $value > 0 ? wp_get_attachment_image( $value, array( 80, 80 ) ) : ''; printf( '%3$s' . ' ' . ' ' . '' . '
%9$s
' . '', esc_attr( $field_id ), esc_html( $label ), '' !== $help_key ? wp_kses( HelpIcon::render( $help_key ), KsesTags::help_icon() ) : '', esc_attr( $name ), $value > 0 ? esc_attr( (string) $value ) : '', esc_attr__( 'Choose logo', 'acez-seo' ), esc_html__( 'Choose from library', 'acez-seo' ), esc_html__( 'Clear', 'acez-seo' ), wp_kses( $preview, KsesTags::attachment_image() ) ); } /** * @param string $pattern Optional HTML5 `pattern` character class (without the * surrounding brackets or quantifier). Use it where the * server-side sanitizer would otherwise DROP the rejected * characters silently — the browser then refuses the * submit with a visible message instead of the user * finding an empty field after saving. * @param string $title Message the browser shows when $pattern fails. */ public function text_field( string $name, string $label, string $value, string $placeholder = '', string $help_key = '', string $pattern = '', string $title = '', string $ai_field = '' ): void { printf( '%5$s%8$s', esc_attr( $name ), esc_html( $label ), esc_attr( $value ), esc_attr( $placeholder ), '' !== $help_key ? wp_kses( HelpIcon::render( $help_key ), KsesTags::help_icon() ) : '', '' !== $pattern ? ' pattern="' . esc_attr( '[' . $pattern . ']+' ) . '"' : '', '' !== $title ? ' title="' . esc_attr( $title ) . '"' : '', '' !== $ai_field ? wp_kses( self::ai_button( $ai_field, $name ), KsesTags::ai_button() ) : '' ); } public function textarea_field( string $name, string $label, string $value, string $placeholder = '', string $help_key = '', string $ai_field = '' ): void { printf( '%5$s%6$s', esc_attr( $name ), esc_html( $label ), esc_textarea( $value ), esc_attr( $placeholder ), '' !== $help_key ? wp_kses( HelpIcon::render( $help_key ), KsesTags::help_icon() ) : '', '' !== $ai_field ? wp_kses( self::ai_button( $ai_field, $name ), KsesTags::ai_button() ) : '' ); } /** * The "generate with AI" trigger rendered under a settings field. * * Deliberately reuses the `.acez-seo-ai-generate` contract already * implemented in assets/js/meta-box-ai.js (data-field = what to generate, * data-target = the element id to fill) instead of shipping a second * script for settings screens — the handler is field-agnostic, so the * caller only has to enqueue that script and localize `AcezSeoAi`. * * @param string $ai_field Field key sent to the REST endpoint. * @param string $target Element id the generated text is written into. */ private static function ai_button( string $ai_field, string $target ): string { return sprintf( '

', esc_attr( $ai_field ), esc_attr( $target ), esc_html__( 'Generate with AI', 'acez-seo' ) ); } public function checkbox_field( string $name, string $label, bool $checked, string $help_key = '' ): void { printf( '%2$s%4$s', esc_attr( $name ), esc_html( $label ), checked( $checked, true, false ), '' !== $help_key ? wp_kses( HelpIcon::render( $help_key ), KsesTags::help_icon() ) : '' ); } /** * Scope radios + pages picker for a site-wide schema node (LocalBusiness, * Organization). 'site' stays the backward-compatible default. * * @param string $field_prefix e.g. 'local_business' → fields local_business_scope / local_business_page_ids[] * @param int[] $selected_ids */ public function render_node_scope_field( string $field_prefix, string $scope, array $selected_ids, string $help_key ): void { $selected_ids = array_map( 'intval', $selected_ids ); $options = array( 'site' => __( 'Entire site (default)', 'acez-seo' ), 'front_page' => __( 'Front page only', 'acez-seo' ), 'front_page_pages' => __( 'Front page + selected pages (e.g. Contact)', 'acez-seo' ), ); echo '' . esc_html__( 'Node scope', 'acez-seo' ) . wp_kses( HelpIcon::render( $help_key ), KsesTags::help_icon() ) . ''; foreach ( $options as $value => $label ) { printf( '', esc_attr( $field_prefix ), esc_attr( $value ), checked( $scope, $value, false ), esc_html( $label ) ); } $pages = get_pages( array( 'sort_column' => 'post_title' ) ); if ( is_array( $pages ) && array() !== $pages ) { printf( ''; echo '

' . esc_html__( 'The page selection applies only to the third option (Ctrl/Cmd + click to select multiple).', 'acez-seo' ) . '

'; } echo ''; } public function menu_icon_svg(): string { return ''; } public function tab_icon_svg( string $name ): string { $icons = array( 'dashboard' => '', 'onboarding' => '', 'global' => '', 'post-type' => '', 'archive' => '', 'robots' => '', 'sitemap' => '', 'llms' => '', 'ai' => '', 'integrations' => '', 'tools' => '', 'settings' => '', 'license' => '', ); /** * Filters the icon markup used for a settings-page tab/card. * * @param string $svg Inline SVG markup. * @param string $name Icon identifier ('dashboard', 'global', 'post-type', or a custom key). */ return (string) apply_filters( 'acez_seo_admin_icon', $icons[ $name ] ?? $icons['post-type'], $name ); } }