Auto-generated from question sections > First paragraph */ class AI_Answer_Toolkit_Summary { /** * Maximum character length for auto-generated summaries. * * @var int */ const MAX_SUMMARY_LENGTH = 500; /** * Minimum paragraph length to be considered meaningful. * * @var int */ const MIN_PARAGRAPH_LENGTH = 30; /** * Get the final summary for a post. * * Priority order: * 1. Manual summary from meta box (_aitk_summary) * 2. Auto-generated summary from content * 3. Empty string if nothing found * * @param int $post_id Post ID. * @return string Final summary content. */ public static function get_final_summary( $post_id ) { if ( ! $post_id ) { return ''; } // Priority 1: Check for manual summary $manual_summary = get_post_meta( $post_id, '_aitk_summary', true ); if ( ! empty( $manual_summary ) ) { return self::sanitize_summary( $manual_summary ); } // Priority 2: Generate auto summary from content $auto_summary = self::generate_auto_summary( $post_id ); if ( ! empty( $auto_summary ) ) { return $auto_summary; } // Priority 3: Nothing found return ''; } /** * Generate automatic summary from post content. * * Parses Gutenberg blocks and extracts content based on priority: * - Priority A: First question section (heading ending with ? + following paragraph) * - Priority B: First meaningful paragraph * * @param int $post_id Post ID. * @return string Generated summary or empty string. */ public static function generate_auto_summary( $post_id ) { $content = get_post_field( 'post_content', $post_id ); if ( empty( $content ) ) { return ''; } // Parse Gutenberg blocks $blocks = parse_blocks( $content ); if ( empty( $blocks ) ) { return ''; } // Priority A: Try to find question section $question_summary = self::extract_question_section( $blocks ); if ( ! empty( $question_summary ) ) { return $question_summary; } // Priority B: Fall back to first meaningful paragraph $paragraph_summary = self::extract_first_paragraph( $blocks ); if ( ! empty( $paragraph_summary ) ) { return $paragraph_summary; } return ''; } /** * Extract content from first question section. * * Looks for a heading (H2 or H3) ending with "?" and extracts * the paragraph immediately following it. * * @param array $blocks Parsed Gutenberg blocks. * @return string Extracted summary or empty string. */ private static function extract_question_section( $blocks ) { $found_question = false; $question_index = -1; // Find first heading that ends with a question mark foreach ( $blocks as $index => $block ) { if ( empty( $block['blockName'] ) ) { continue; } // Check if it's a heading block if ( $block['blockName'] === 'core/heading' ) { $heading_content = self::get_block_content( $block ); if ( empty( $heading_content ) ) { continue; } // Check if heading ends with question mark if ( self::is_question_heading( $heading_content ) ) { // Verify it's H2 or H3 $level = isset( $block['attrs']['level'] ) ? $block['attrs']['level'] : 2; if ( $level === 2 || $level === 3 ) { $found_question = true; $question_index = $index; break; } } } } // If question heading found, get the next paragraph if ( $found_question && isset( $blocks[ $question_index + 1 ] ) ) { $next_block = $blocks[ $question_index + 1 ]; if ( isset( $next_block['blockName'] ) && $next_block['blockName'] === 'core/paragraph' ) { $paragraph_content = self::get_block_content( $next_block ); if ( self::is_meaningful_content( $paragraph_content ) ) { return self::prepare_summary( $paragraph_content ); } } } return ''; } /** * Extract first meaningful paragraph from blocks. * * @param array $blocks Parsed Gutenberg blocks. * @return string Extracted summary or empty string. */ private static function extract_first_paragraph( $blocks ) { foreach ( $blocks as $block ) { if ( empty( $block['blockName'] ) ) { continue; } // Look for paragraph blocks if ( $block['blockName'] === 'core/paragraph' ) { $content = self::get_block_content( $block ); if ( self::is_meaningful_content( $content ) ) { return self::prepare_summary( $content ); } } // Recursively check inner blocks if ( ! empty( $block['innerBlocks'] ) ) { $inner_summary = self::extract_first_paragraph( $block['innerBlocks'] ); if ( ! empty( $inner_summary ) ) { return $inner_summary; } } } return ''; } /** * Get clean text content from a Gutenberg block. * * @param array $block Gutenberg block. * @return string Block content as plain text. */ private static function get_block_content( $block ) { if ( empty( $block['innerHTML'] ) ) { return ''; } $content = $block['innerHTML']; // Remove shortcodes $content = strip_shortcodes( $content ); // Remove HTML tags $content = wp_strip_all_tags( $content ); // Remove extra whitespace $content = preg_replace( '/\s+/', ' ', $content ); // Trim $content = trim( $content ); return $content; } /** * Check if heading text is a question. * * @param string $heading Heading text. * @return bool True if heading ends with question mark. */ private static function is_question_heading( $heading ) { $heading = trim( $heading ); return substr( $heading, -1 ) === '?'; } /** * Check if content is meaningful (not too short or empty). * * @param string $content Content to check. * @return bool True if content is meaningful. */ private static function is_meaningful_content( $content ) { if ( empty( $content ) ) { return false; } // Check minimum length if ( strlen( $content ) < self::MIN_PARAGRAPH_LENGTH ) { return false; } // Check if it's not just whitespace or special characters $clean_content = preg_replace( '/[^a-zA-Z0-9]/', '', $content ); if ( strlen( $clean_content ) < 20 ) { return false; } return true; } /** * Prepare summary for output. * * Cleans, trims, and formats the summary text. * * @param string $content Raw content. * @return string Prepared summary. */ private static function prepare_summary( $content ) { // Remove any remaining HTML $content = wp_strip_all_tags( $content ); // Remove shortcodes $content = strip_shortcodes( $content ); // Remove inline styles and scripts $content = preg_replace( '/]*>(.*?)<\/style>/is', '', $content ); $content = preg_replace( '/]*>(.*?)<\/script>/is', '', $content ); // Normalize whitespace $content = preg_replace( '/\s+/', ' ', $content ); // Trim to reasonable length (soft trim at sentence boundary) $content = self::smart_trim( $content, self::MAX_SUMMARY_LENGTH ); // Final trim $content = trim( $content ); return $content; } /** * Smart trim content to maximum length. * * Attempts to trim at sentence boundaries when possible. * * @param string $text Text to trim. * @param int $max_length Maximum length. * @return string Trimmed text. */ private static function smart_trim( $text, $max_length ) { if ( strlen( $text ) <= $max_length ) { return $text; } // Try to find a sentence boundary within the limit $trimmed = substr( $text, 0, $max_length ); // Look for sentence endings (., !, ?) $last_period = max( strrpos( $trimmed, '. ' ), strrpos( $trimmed, '! ' ), strrpos( $trimmed, '? ' ) ); if ( $last_period !== false && $last_period > ( $max_length * 0.6 ) ) { // Found a good sentence boundary return substr( $text, 0, $last_period + 1 ); } // Fall back to word boundary $last_space = strrpos( $trimmed, ' ' ); if ( $last_space !== false ) { return substr( $text, 0, $last_space ) . '...'; } // Hard trim as last resort return $trimmed . '...'; } /** * Sanitize summary content. * * Used for manual summaries that may contain HTML. * * @param string $summary Summary content. * @return string Sanitized summary. */ private static function sanitize_summary( $summary ) { // For manual summaries, we keep basic HTML but strip to plain text for output $summary = wp_strip_all_tags( $summary ); $summary = trim( $summary ); return $summary; } /** * Check if post has any summary (manual or auto-generated). * * @param int $post_id Post ID. * @return bool True if summary exists. */ public static function has_summary( $post_id ) { $summary = self::get_final_summary( $post_id ); return ! empty( $summary ); } /** * Get summary type for a post. * * @param int $post_id Post ID. * @return string 'manual', 'auto', or 'none'. */ public static function get_summary_type( $post_id ) { $manual_summary = get_post_meta( $post_id, '_aitk_summary', true ); if ( ! empty( $manual_summary ) ) { return 'manual'; } $auto_summary = self::generate_auto_summary( $post_id ); if ( ! empty( $auto_summary ) ) { return 'auto'; } return 'none'; } }