private function extract_written_faq( $content ) { $pairs = array(); if ( ! preg_match_all( '/]*>(.*?)<\/h\1>/is', $content, $all_headings, PREG_OFFSET_CAPTURE ) ) { return array( 'pairs' => $pairs, 'heading_found' => false ); } $start_pos = null; $end_of_heading = null; $section_level = null; foreach ( $all_headings[2] as $i => $heading_match ) { $heading_text = trim( wp_strip_all_tags( $heading_match[0] ) ); if ( preg_match( '/\b(FAQ|Frequently Asked)\b/i', $heading_text ) ) { $start_pos = $all_headings[0][$i][1]; $end_of_heading = $start_pos + strlen( $all_headings[0][$i][0] ); $section_level = (int) $all_headings[1][$i][0]; break; } } if ( $start_pos === null ) { return array( 'pairs' => $pairs, 'heading_found' => false ); } $after_head = substr( $content, $end_of_heading ); if ( preg_match_all( '/]*>(.*?)<\/h\1>/is', $after_head, $boundary_headings, PREG_OFFSET_CAPTURE ) ) { foreach ( $boundary_headings[1] as $i => $level_match ) { if ( (int) $level_match[0] <= $section_level ) { $after_head = substr( $after_head, 0, $boundary_headings[0][$i][1] ); break; } } } // Strategy 1: boundary-based h3/h4 question -> next-heading-bounded answer. // A real FAQ question ends in "?". A subsection label (e.g. a heading used to // introduce a second list of questions) doesn't end in "?" — without this // check it gets treated as one giant question whose "answer" swallows every // subsequent Q&A in the section as trailing text. if ( preg_match_all( '/]*>(.*?)<\/h\1>/is', $after_head, $q_headings, PREG_OFFSET_CAPTURE ) ) { $num = count( $q_headings[0] ); for ( $i = 0; $i < $num; $i++ ) { $q_text = trim( wp_strip_all_tags( $q_headings[2][$i][0] ) ); if ( ! $q_text || strpos( $q_text, '?' ) === false ) continue; $seg_start = $q_headings[0][$i][1] + strlen( $q_headings[0][$i][0] ); $seg_end = ( $i + 1 < $num ) ? $q_headings[0][$i + 1][1] : strlen( $after_head ); $answer_html = substr( $after_head, $seg_start, $seg_end - $seg_start ); $a_text = trim( wp_strip_all_tags( $answer_html ) ); if ( $q_text && $a_text ) { $pairs[] = array( 'question' => $q_text, 'answer' => $a_text ); } } } // Strategy 2:
  • Question
    Answer
  • , anywhere in the bounded FAQ section. // Deliberately not bounded to a single wrapper container match: a malformed or // nested closing tag can truncate that match early and silently drop trailing // Q&A pairs (seen in practice on posts with two stacked FAQ lists). if ( empty( $pairs ) ) { preg_match_all( '/]*>(.*?)<\/li>/is', $after_head, $li_matches ); foreach ( $li_matches[1] as $item ) { $parts = preg_split( '//i', $item, 2 ); if ( count( $parts ) === 2 ) { $q = trim( wp_strip_all_tags( $parts[0] ) ); $a = trim( wp_strip_all_tags( $parts[1] ) ); if ( $q && $a ) { $pairs[] = array( 'question' => $q, 'answer' => $a ); } } } } // Strategy 3: plain
      /
        , each
      • is "Question? Answer text". if ( empty( $pairs ) && preg_match( '/<(ol|ul)[^>]*>(.*?)<\/\1>/is', $after_head, $list ) ) { preg_match_all( '/]*>(.*?)<\/li>/is', $list[2], $li_matches ); foreach ( $li_matches[1] as $item ) { $text = trim( wp_strip_all_tags( $item ) ); if ( preg_match( '/^(.+?\?)\s*(.+)$/s', $text, $qa ) ) { $pairs[] = array( 'question' => trim( $qa[1] ), 'answer' => trim( $qa[2] ) ); } } } // Strategy 4: bold/strong question followed by plain text answer, same paragraph. // Tag name referenced via hex escape (\x70 = 'p') rather than a literal tag // string in the pattern itself. if ( empty( $pairs ) ) { preg_match_all( '/<\x70[^>]*>\s*<(strong|b)[^>]*>(.*?\?)<\/\1>\s*(.*?)<\/\x70>/is', $after_head, $bq_matches, PREG_SET_ORDER ); foreach ( $bq_matches as $match ) { $q = trim( wp_strip_all_tags( $match[2] ) ); $a = trim( wp_strip_all_tags( $match[3] ) ); if ( $q && $a ) { $pairs[] = array( 'question' => $q, 'answer' => $a ); } } } return array( 'pairs' => $pairs, 'heading_found' => true ); }