schema = $schema; } /** * Normalize raw args (from shortcode atts or block attributes) into a * canonical shape, applying settings defaults. * * @param array $raw Raw args. * @return array */ public function normalize( array $raw ) { $defaults = array( 'id' => 0, // Backward-compat: taxonomy term ID. 'term' => 0, 'category' => '', // Slug or comma-separated slugs. 'ids' => '', // Explicit FAQ post IDs (comma-separated). 'style' => Options::get( 'default_style', 'minimal' ), 'mode' => Options::get( 'default_mode', 'accordion' ), 'engine' => Options::get( 'engine', 'aria' ), 'heading' => Options::get( 'heading_level', 'h3' ), 'icon' => Options::get( 'icon_position', 'right' ), 'icon_style' => Options::get( 'icon_style', 'chevron' ), 'voting' => Options::boolean( 'enable_voting' ), 'views' => Options::boolean( 'enable_views' ), 'open_first' => Options::boolean( 'default_open_first' ), 'orderby' => 'date', 'order' => 'ASC', 'limit' => -1, 'search' => Options::boolean( 'show_search' ), 'filter' => Options::boolean( 'show_filter' ), 'expand_all' => Options::boolean( 'show_expand_all' ), 'toc' => Options::boolean( 'show_toc' ), 'schema' => Options::boolean( 'enable_schema' ), 'per_page' => (int) Options::get( 'per_page', 0 ), 'class' => '', ); $a = wp_parse_args( $raw, $defaults ); // Backward compatibility: `id`/`ID` meant the taxonomy term ID. $term = absint( $a['term'] ); if ( ! $term && ! empty( $a['id'] ) ) { $term = absint( $a['id'] ); } return array( 'term' => $term, 'category' => Sanitizer::slug_list( $a['category'] ), 'ids' => array_filter( array_map( 'absint', Sanitizer::slug_list( is_array( $a['ids'] ) ? $a['ids'] : str_replace( ' ', '', (string) $a['ids'] ) ) ) ), 'style' => Sanitizer::enum( $a['style'], array( 'minimal', 'bordered', 'filled', 'separated', 'card', 'modern' ), 'modern' ), 'mode' => Sanitizer::enum( $a['mode'], array( 'accordion', 'toggle' ), 'accordion' ), 'engine' => Sanitizer::enum( $a['engine'], array( 'aria', 'details' ), 'aria' ), 'heading' => Sanitizer::enum( $a['heading'], array( 'h2', 'h3', 'h4', 'h5', 'h6' ), 'h3' ), 'icon' => Sanitizer::enum( $a['icon'], array( 'left', 'right' ), 'right' ), 'icon_style' => Sanitizer::enum( $a['icon_style'], array( 'chevron', 'plus', 'arrow' ), 'chevron' ), 'voting' => Sanitizer::boolean( $a['voting'] ), 'views' => Sanitizer::boolean( $a['views'] ), 'open_first' => Sanitizer::boolean( $a['open_first'] ), 'orderby' => Sanitizer::enum( $a['orderby'], array( 'date', 'title', 'order' ), 'date' ), 'order' => Sanitizer::enum( $a['order'], array( 'ASC', 'DESC' ), 'ASC' ), 'limit' => ( '' === $a['limit'] ) ? -1 : (int) $a['limit'], 'search' => Sanitizer::boolean( $a['search'] ), 'filter' => Sanitizer::boolean( $a['filter'] ), 'expand_all' => Sanitizer::boolean( $a['expand_all'] ), 'toc' => Sanitizer::boolean( $a['toc'] ), 'schema' => Sanitizer::boolean( $a['schema'] ), 'per_page' => max( 0, (int) $a['per_page'] ), 'class' => sanitize_html_class( is_string( $a['class'] ) ? $a['class'] : '' ), ); } /** * Render FAQs to HTML. * * @param array $raw Raw args (shortcode atts / block attributes). * @return string */ public function render( array $raw ) { $args = $this->normalize( $raw ); $wpq = ( new Query() )->run( $args ); if ( ! $wpq->have_posts() ) { wp_reset_postdata(); /** * Filter the empty-state HTML. * * @param string $html Empty-state markup. * @param array $args Normalized args. */ return apply_filters( 'accordion_faq_empty_output', '', $args ); } $items = array(); while ( $wpq->have_posts() ) { $wpq->the_post(); $post_id = get_the_ID(); // Render the answer through the core content pipeline (shortcodes, // embeds, wpautop, …). Using the the_content() template tag applies // the same filters and the `]]>` guard that a normal loop would. ob_start(); the_content(); $answer_html = (string) ob_get_clean(); $question = get_the_title(); $categories = wp_get_post_terms( $post_id, Taxonomy::TAXONOMY, array( 'fields' => 'slugs' ) ); $items[] = array( 'id' => $post_id, 'question' => $question, 'answer_html' => $answer_html, 'answer_text' => wp_strip_all_tags( $answer_html ), 'icon' => (string) get_post_meta( $post_id, Meta::META_ICON, true ), 'default_open' => (bool) get_post_meta( $post_id, Meta::META_OPEN, true ), 'categories' => is_wp_error( $categories ) ? array() : $categories, 'anchor' => sanitize_title( $question ) . '-' . $post_id, 'up' => (int) get_post_meta( $post_id, '_afaq_up', true ), 'down' => (int) get_post_meta( $post_id, '_afaq_down', true ), 'views' => (int) get_post_meta( $post_id, '_afaq_views', true ), ); } wp_reset_postdata(); /** * Filter the collected FAQ items before rendering. * * @param array $items Item data. * @param array $args Normalized args. */ $items = apply_filters( 'accordion_faq_items', $items, $args ); if ( empty( $items ) ) { return apply_filters( 'accordion_faq_empty_output', '', $args ); } // Register schema entities (deduplicated in the Schema service). if ( $args['schema'] ) { $this->schema->add_items( $items ); } // Ensure assets are present. Assets::enqueue(); self::$counter++; $uid = 'afaq-' . self::$counter; // Categories available for the filter bar. $filter_terms = array(); if ( $args['filter'] ) { $filter_terms = $this->collect_filter_terms( $items ); } $html = Template::render( 'faq-list.php', array( 'uid' => $uid, 'args' => $args, 'items' => $items, 'filter_terms' => $filter_terms, ) ); /** * Filter the final rendered FAQ HTML. * * @param string $html Rendered markup. * @param array $items Item data. * @param array $args Normalized args. */ return apply_filters( 'accordion_faq_output', $html, $items, $args ); } /** * Build a label map for the categories present in the rendered items. * * @param array $items Items. * @return array[] List of [ 'slug' => ..., 'name' => ... ]. */ private function collect_filter_terms( array $items ) { $slugs = array(); foreach ( $items as $item ) { foreach ( $item['categories'] as $slug ) { $slugs[ $slug ] = true; } } $out = array(); foreach ( array_keys( $slugs ) as $slug ) { $term = get_term_by( 'slug', $slug, Taxonomy::TAXONOMY ); if ( $term && ! is_wp_error( $term ) ) { $out[] = array( 'slug' => $slug, 'name' => $term->name, ); } } return $out; } }