Allowed fallback options. */ function ai4seo_get_metadata_fallback_allowed_values( string $metadata_identifier ): array { // Preserve key order because the settings page renders these translated choices directly. switch ( $metadata_identifier ) { case 'meta-title': return array( 'no-fallback' => esc_html__( 'No fallback', 'ai-for-seo' ), 'post-title' => esc_html__( 'Use post title', 'ai-for-seo' ), 'facebook-title' => esc_html__( 'Use Facebook title', 'ai-for-seo' ), 'twitter-title' => esc_html__( 'Use Twitter title', 'ai-for-seo' ), ); case 'meta-description': return array( 'no-fallback' => esc_html__( 'No fallback', 'ai-for-seo' ), 'post-excerpt' => esc_html__( 'Use post excerpt', 'ai-for-seo' ), 'content' => esc_html__( 'Use post content', 'ai-for-seo' ), 'facebook-description' => esc_html__( 'Use Facebook description', 'ai-for-seo' ), 'twitter-description' => esc_html__( 'Use Twitter description', 'ai-for-seo' ), ); case 'facebook-title': return array( 'no-fallback' => esc_html__( 'No fallback', 'ai-for-seo' ), 'post-title' => esc_html__( 'Use post title', 'ai-for-seo' ), 'meta-title' => esc_html__( 'Use meta title', 'ai-for-seo' ), 'twitter-title' => esc_html__( 'Use Twitter title', 'ai-for-seo' ), ); case 'facebook-description': return array( 'no-fallback' => esc_html__( 'No fallback', 'ai-for-seo' ), 'post-excerpt' => esc_html__( 'Use post excerpt', 'ai-for-seo' ), 'content' => esc_html__( 'Use post content', 'ai-for-seo' ), 'meta-description' => esc_html__( 'Use meta description', 'ai-for-seo' ), 'twitter-description' => esc_html__( 'Use Twitter description', 'ai-for-seo' ), ); case 'twitter-title': return array( 'no-fallback' => esc_html__( 'No fallback', 'ai-for-seo' ), 'post-title' => esc_html__( 'Use post title', 'ai-for-seo' ), 'meta-title' => esc_html__( 'Use meta title', 'ai-for-seo' ), 'facebook-title' => esc_html__( 'Use Facebook title', 'ai-for-seo' ), ); case 'twitter-description': return array( 'no-fallback' => esc_html__( 'No fallback', 'ai-for-seo' ), 'post-excerpt' => esc_html__( 'Use post excerpt', 'ai-for-seo' ), 'content' => esc_html__( 'Use post content', 'ai-for-seo' ), 'meta-description' => esc_html__( 'Use meta description', 'ai-for-seo' ), 'facebook-description' => esc_html__( 'Use Facebook description', 'ai-for-seo' ), ); } return array(); } /** * Return the available fallback identifiers for a metadata identifier. * * @param string $metadata_identifier Metadata identifier. * * @return array Allowed fallback identifiers. */ function ai4seo_get_metadata_fallback_allowed_value_identifiers( string $metadata_identifier ): array { // Keep validation independent from translated labels by returning the same contract as identifiers only. switch ( $metadata_identifier ) { case 'meta-title': return array( 'no-fallback', 'post-title', 'facebook-title', 'twitter-title', ); case 'meta-description': return array( 'no-fallback', 'post-excerpt', 'content', 'facebook-description', 'twitter-description', ); case 'facebook-title': return array( 'no-fallback', 'post-title', 'meta-title', 'twitter-title', ); case 'facebook-description': return array( 'no-fallback', 'post-excerpt', 'content', 'meta-description', 'twitter-description', ); case 'twitter-title': return array( 'no-fallback', 'post-title', 'meta-title', 'facebook-title', ); case 'twitter-description': return array( 'no-fallback', 'post-excerpt', 'content', 'meta-description', 'facebook-description', ); } return array(); } /** * Get a shortened fallback text from the post title. * * @param int $post_id Current post ID. * * @return string Prepared fallback text. */ function ai4seo_get_metadata_fallback_post_title( int $post_id ): string { // Normalize the WordPress title before applying the title-specific fallback length. $post_title = get_the_title( $post_id ); $post_title = ai4seo_prepare_metadata_fallback_text( $post_title ); // Skip the length helper when WordPress did not provide usable source text. if ( '' === $post_title ) { return ''; } return ai4seo_limit_metadata_fallback_text( $post_title, 100 ); } /** * Get a shortened fallback text from the post excerpt. * * @param int $post_id Current post ID. * * @return string Prepared fallback text. */ function ai4seo_get_metadata_fallback_post_excerpt( int $post_id ): string { // Normalize the WordPress excerpt before applying the description fallback length. $post_excerpt = get_the_excerpt( $post_id ); $post_excerpt = ai4seo_prepare_metadata_fallback_text( $post_excerpt ); // Skip the length helper when WordPress did not provide usable source text. if ( '' === $post_excerpt ) { return ''; } return ai4seo_limit_metadata_fallback_text( $post_excerpt, 150 ); } /** * Get a shortened fallback text from the post content. * * @param int $post_id Current post ID. * * @return string Prepared fallback text. */ function ai4seo_get_metadata_fallback_post_content( int $post_id ): string { // Read raw post content so fallback cleanup owns markup removal and whitespace normalization. $post_content = get_post_field( 'post_content', $post_id, 'raw' ); $post_content = ai4seo_prepare_metadata_fallback_text( $post_content ); // Skip the length helper when WordPress did not provide usable source text. if ( '' === $post_content ) { return ''; } return ai4seo_limit_metadata_fallback_text( $post_content, 150 ); } /** * Prepare a string for fallback usage by removing markup and normalizing whitespace. * * @param mixed $text Input text. * * @return string Cleaned text. */ function ai4seo_prepare_metadata_fallback_text( $text ): string { // Reject non-string WordPress values instead of coercing unsupported source types. if ( ! is_string( $text ) ) { return ''; } // Remove rendered markup and collapse all Unicode whitespace before measuring fallback length. $text = wp_strip_all_tags( $text ); $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $text = preg_replace( '/\s+/u', ' ', $text ); // preg_replace() may return null for invalid UTF-8, which should behave like an unavailable source. if ( ! is_string( $text ) ) { $text = ''; } return trim( $text ); } /** * Limit fallback text length while keeping current words intact where possible. * * @param string $text Prepared text. * @param int $base_length Hard character limit before soft extension. * * @return string Shortened text. */ function ai4seo_limit_metadata_fallback_text( string $text, int $base_length ): string { // Preserve empty inputs without invoking multibyte helpers. if ( '' === $text ) { return ''; } // Values within the base limit require no soft extension or additional normalization. if ( ai4seo_mb_strlen( $text ) <= $base_length ) { return $text; } // Inspect only the next 20 characters so a fallback can finish the current word without growing freely. $base_snippet = ai4seo_mb_substr( $text, 0, $base_length ); $remaining_segment = ai4seo_mb_substr( $text, $base_length, 20 ); // Build the optional soft extension separately so spacing remains correct at the hard split point. $append = ''; if ( '' !== $remaining_segment ) { $remaining_segment_trimmed = ltrim( $remaining_segment ); $leading_whitespace_removed = ai4seo_mb_strlen( $remaining_segment_trimmed ) !== ai4seo_mb_strlen( $remaining_segment ); // Capture at most the remainder of the split word from the bounded look-ahead segment. if ( '' !== $remaining_segment_trimmed && preg_match( '/^\S{0,20}/u', $remaining_segment_trimmed, $match ) && isset( $match[0] ) ) { $append_segment = $match[0]; // Restore one separator only when ltrim() removed the boundary whitespace. if ( $leading_whitespace_removed && '' !== $append_segment && ! preg_match( '/\s$/u', $base_snippet ) ) { $append = ' ' . $append_segment; } else { $append = $append_segment; } } } // Normalize only boundary whitespace after recombining the fixed base and optional word extension. $result = $base_snippet . $append; return trim( $result ); }