From beedc616d41763d739969fa1bf87bd50004d47c8 Mon Sep 17 00:00:00 2001 From: Amirreza Nazemi Date: Sat, 1 Nov 2025 11:04:34 +0330 Subject: [PATCH 1/3] feat(loop): add generic loop shortcode with else-block and utils refactor - Added [anys type="loop"]... [anys else]...[/anys] with full WP_Query support. - Moved reusable logic (split, query builder, search columns, container exclusion) to utilities. - Preserved non-default attributes in shortcodes via merge_unknown_attributes(). - Updated output flow to avoid double rendering and safely process nested shortcodes. --- includes/register-shortcodes.php | 29 +++++ includes/types/anys/loop.php | 87 +++++++++++++ includes/utilities.php | 205 +++++++++++++++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 includes/types/anys/loop.php diff --git a/includes/register-shortcodes.php b/includes/register-shortcodes.php index 1f23e8b..4cf8c2b 100644 --- a/includes/register-shortcodes.php +++ b/includes/register-shortcodes.php @@ -73,6 +73,9 @@ public function register_shortcodes() { * @return string */ public function render_shortcode( $attributes, $content ) { + // Raw attributes are captured. + $raw_attributes = is_array( $attributes ) ? $attributes : []; + // Default attributes. $defaults = [ 'type' => '', @@ -87,6 +90,9 @@ public function render_shortcode( $attributes, $content ) { $attributes = shortcode_atts( $defaults, $attributes, 'anys' ); + // Unknown keys are merged into normalized attributes. + $attributes = $this->merge_unknown_attributes( $attributes, $raw_attributes, $defaults ); + /** * Filters the shortcode attributes before processing. * @@ -191,8 +197,31 @@ public function render_shortcode( $attributes, $content ) { $content ); + if($attributes['type'] == 'loop'){ + return $output; + } + return $output . do_shortcode( $content ); } + + /** + * Merge unknown (non-default) attributes back after shortcode_atts(). + * + * Keeps keys like post_type, s, tax_query, meta_query, etc. + * + * @param array $normalized Attributes with defaults applied. + * @param array $raw Raw attributes from WP. + * @param array $defaults Default attribute map. + * + * @return array + */ + private function merge_unknown_attributes( array $normalized, array $raw, array $defaults ) : array { + // Finds non-default keys and appends them. + $extra = array_diff_key( $raw, $defaults ); + + // Keeps normalized values, adds extras. + return $normalized + $extra; + } } /** diff --git a/includes/types/anys/loop.php b/includes/types/anys/loop.php new file mode 100644 index 0000000..fd03932 --- /dev/null +++ b/includes/types/anys/loop.php @@ -0,0 +1,87 @@ +[anys type="post-field" name="post_title"] + * [anys else] + *

No posts found.

+ * [/anys] + * + * Providers: + * - name="post" (WP_Query) + * + * Attributes (post): + * - post_type, posts_per_page, orderby, order, author, paged, offset, post_status, s + * - meta_key, meta_value, meta_compare + * - meta_query (JSON), tax_query (JSON) + * - search_in ("all"|"title"|"title_excerpt") + * - exclude_current ("1"|"0") + * + * @since NEXT + */ + +defined( 'ABSPATH' ) || exit; + +// Validates attributes. +if ( ! isset( $attributes ) || ! is_array( $attributes ) ) { + return; +} + +// Validates provider. +$provider = isset( $attributes['name'] ) ? strtolower( trim( (string) $attributes['name'] ) ) : ''; +if ( $provider !== 'post' ) { + return; +} + +// Parses dynamic attribute placeholders. +$attributes = function_exists( 'anys_parse_dynamic_attributes' ) + ? anys_parse_dynamic_attributes( $attributes ) + : $attributes; + +// Extracts item/else templates. +$templates = anys_split_else_block( (string) ( $content ?? '' ) ); +$item_template = $templates['item']; +$else_template = $templates['else']; + +// Builds base WP_Query args from attributes. +$query_args = anys_build_wp_query_args( $attributes ); + +// Maps 'search_in' to 'search_columns'. +$query_args = anys_apply_search_columns( $query_args, $attributes ); + +// Excludes container post when it would self-match. +$exclude_disabled = ( isset( $attributes['exclude_current'] ) && (string) $attributes['exclude_current'] === '0' ); +if ( ! $exclude_disabled ) { + $container_id = anys_detect_container_post_id(); + if ( anys_should_exclude_container( $query_args, $attributes, $container_id ) ) { + $query_args['post__not_in'] = isset( $query_args['post__not_in'] ) && is_array( $query_args['post__not_in'] ) + ? array_unique( array_merge( $query_args['post__not_in'], [ $container_id ] ) ) + : [ $container_id ]; + } +} + +// Runs query. +$wp_query_instance = new \WP_Query( $query_args ); + +// Handles empty state via [anys else]. +if ( ! $wp_query_instance->have_posts() ) { + if ( $else_template !== '' ) { + echo wp_kses_post( do_shortcode( $else_template ) ); + } + wp_reset_postdata(); + return; +} + +// Renders items. +$final_output = ''; +while ( $wp_query_instance->have_posts() ) { + $wp_query_instance->the_post(); + $final_output .= anys_render_template_fast( $item_template ); // Avoids extra do_shortcode when not needed. +} +wp_reset_postdata(); + +// Applies before/after/fallback and outputs. +$final_output = anys_wrap_output( $final_output, $attributes ); +echo wp_kses_post( $final_output ); diff --git a/includes/utilities.php b/includes/utilities.php index 28309b1..8071a5d 100644 --- a/includes/utilities.php +++ b/includes/utilities.php @@ -500,3 +500,208 @@ function anys_has_shortcode( $content ) { return false; } + +/** + * Splits content by "[anys else]" marker. + * + * @since NEXT + * + * @param string $content + * + * @return array{item:string, else:string} + */ +function anys_split_else_block( string $content ): array { + $raw = shortcode_unautop( $content ); + $parts = preg_split( '/\[\s*anys\s+else\s*\]/i', $raw, 2 ); + + return [ + 'item' => isset( $parts[0] ) ? trim( $parts[0] ) : '', + 'else' => isset( $parts[1] ) ? trim( $parts[1] ) : '', + ]; +} + +/** + * Builds sanitized WP_Query args from shortcode attributes. + * + * @since NEXT + * + * @param array $atts + * + * @return array + */ +function anys_build_wp_query_args( array $atts ): array { + $args = []; + + foreach ( [ + 'post_type','posts_per_page','orderby','order', + 'author','paged','offset','post_status', + 'meta_key','meta_value','meta_compare', + ] as $key ) { + if ( isset( $atts[ $key ] ) && $atts[ $key ] !== '' ) { + $args[ $key ] = sanitize_text_field( (string) $atts[ $key ] ); + } + } + + foreach ( [ 'posts_per_page', 'author', 'paged', 'offset' ] as $n ) { + if ( isset( $args[ $n ] ) ) { + $args[ $n ] = (int) $args[ $n ]; + } + } + + // Passes 's' as-is (WP_Query sanitizes internally). + if ( isset( $atts['s'] ) && $atts['s'] !== '' ) { + $args['s'] = (string) wp_unslash( $atts['s'] ); + } + + // tax_query (JSON) + if ( ! empty( $atts['tax_query'] ) ) { + $decoded = json_decode( (string) wp_unslash( $atts['tax_query'] ), true ); + if ( is_array( $decoded ) ) { + $clean = []; + foreach ( $decoded as $row ) { + if ( ! is_array( $row ) ) continue; + $item = []; + if ( ! empty( $row['taxonomy'] ) ) $item['taxonomy'] = sanitize_key( $row['taxonomy'] ); + $item['field'] = ( isset( $row['field'] ) && in_array( $row['field'], [ 'term_id', 'name', 'slug' ], true ) ) + ? $row['field'] : 'slug'; + if ( isset( $row['terms'] ) ) { + $item['terms'] = is_array( $row['terms'] ) + ? array_map( 'sanitize_text_field', $row['terms'] ) + : [ sanitize_text_field( (string) $row['terms'] ) ]; + } + if ( isset( $row['operator'] ) && in_array( $row['operator'], [ 'IN', 'NOT IN', 'AND', 'EXISTS', 'NOT EXISTS' ], true ) ) { + $item['operator'] = $row['operator']; + } + if ( $item ) $clean[] = $item; + } + if ( $clean ) $args['tax_query'] = $clean; + } + } + + // meta_query (JSON) + if ( ! empty( $atts['meta_query'] ) ) { + $decoded = json_decode( (string) wp_unslash( $atts['meta_query'] ), true ); + if ( is_array( $decoded ) ) { + $clean = []; + foreach ( $decoded as $row ) { + if ( ! is_array( $row ) ) continue; + $item = []; + if ( isset( $row['key'] ) ) $item['key'] = sanitize_key( $row['key'] ); + if ( isset( $row['value'] ) ) $item['value'] = is_array( $row['value'] ) + ? array_map( 'sanitize_text_field', $row['value'] ) + : sanitize_text_field( (string) $row['value'] ); + if ( isset( $row['compare'] ) ) $item['compare'] = strtoupper( (string) $row['compare'] ); + if ( isset( $row['type'] ) ) $item['type'] = strtoupper( (string) $row['type'] ); + if ( $item ) $clean[] = $item; + } + if ( $clean ) $args['meta_query'] = $clean; + } + } + + if ( empty( $args['post_type'] ) ) { + $args['post_type'] = 'post'; + } + if ( empty( $args['posts_per_page'] ) || (int) $args['posts_per_page'] <= 0 ) { + $args['posts_per_page'] = 10; + } + + // Skips total row count for speed. + $args['no_found_rows'] = true; + // Ignores sticky posts. + $args['ignore_sticky_posts'] = true; + // Suppresses external filters. + $args['suppress_filters'] = true; + + return $args; +} + +/** + * Maps 'search_in' to 'search_columns'. + * + * @since NEXT + * + * @param array $query_args + * @param array $atts + * + * @return array + */ +function anys_apply_search_columns( array $query_args, array $atts ): array { + if ( empty( $query_args['s'] ) ) { + return $query_args; + } + + $mode = isset( $atts['search_in'] ) ? strtolower( trim( (string) $atts['search_in'] ) ) : 'all'; + + if ( $mode === 'title' ) { + $query_args['search_columns'] = [ 'post_title' ]; + } elseif ( $mode === 'title_excerpt' || $mode === 'excerpt_title' ) { + $query_args['search_columns'] = [ 'post_title', 'post_excerpt' ]; + } + + return $query_args; +} + +/** + * Detects container post ID safely. + * + * @since NEXT + * + * @return int + */ +function anys_detect_container_post_id(): int { + global $post; + if ( $post instanceof \WP_Post ) { + return (int) $post->ID; + } + + $queried_object = get_queried_object(); + return ( $queried_object instanceof \WP_Post ) ? (int) $queried_object->ID : 0; +} + +/** + * Determines if the container post should be excluded. + * + * @since NEXT + * + * @param array $query_args + * @param array $atts + * @param int $container_id + * + * @return bool + */ +function anys_should_exclude_container( array $query_args, array $atts, int $container_id ): bool { + if ( $container_id <= 0 || empty( $query_args['s'] ) ) { + return false; + } + + $policy = isset( $atts['search_in'] ) ? strtolower( trim( (string) $atts['search_in'] ) ) : 'all'; + $hay = ''; + + if ( in_array( $policy, [ 'title', 'title_excerpt', 'excerpt_title', 'all' ], true ) ) { + $hay .= ' ' . get_the_title( $container_id ); + } + if ( in_array( $policy, [ 'title_excerpt', 'excerpt_title', 'all' ], true ) ) { + $hay .= ' ' . get_the_excerpt( $container_id ); + } + if ( $policy === 'all' ) { + $hay .= ' ' . get_post_field( 'post_content', $container_id ); + } + + $hay = wp_strip_all_tags( strip_shortcodes( (string) $hay ) ); + return ( $hay !== '' && stripos( $hay, (string) $query_args['s'] ) !== false ); +} + +/** + * Renders a template with minimal overhead. + * + * @since NEXT + * + * @param string $template + * + * @return string + */ +function anys_render_template_fast( string $template ): string { + return ( function_exists( 'anys_has_shortcode' ) && anys_has_shortcode( $template ) ) + ? do_shortcode( $template ) + : $template; +} From 0dc2876c1c007a5ced486263376d2975c60cee22 Mon Sep 17 00:00:00 2001 From: Amirreza Nazemi Date: Sat, 1 Nov 2025 15:42:11 +0330 Subject: [PATCH 2/3] Update register-shortcodes.php --- includes/register-shortcodes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/register-shortcodes.php b/includes/register-shortcodes.php index 4cf8c2b..b98276f 100644 --- a/includes/register-shortcodes.php +++ b/includes/register-shortcodes.php @@ -197,6 +197,7 @@ public function render_shortcode( $attributes, $content ) { $content ); + // Returns output if the shortcode type is 'loop'. if($attributes['type'] == 'loop'){ return $output; } From 0edd389ff20ddf55ef87034b7bf5375585a5c462 Mon Sep 17 00:00:00 2001 From: Amirreza Nazemi Date: Sat, 8 Nov 2025 17:38:47 +0330 Subject: [PATCH 3/3] Sync branch structure with latest updates --- includes/modules/shortcodes/types/loop.php | 294 +++++++++++++++++++++ includes/modules/utilities.php | 189 +------------ includes/types/anys/loop.php | 87 ------ 3 files changed, 303 insertions(+), 267 deletions(-) create mode 100644 includes/modules/shortcodes/types/loop.php delete mode 100644 includes/types/anys/loop.php diff --git a/includes/modules/shortcodes/types/loop.php b/includes/modules/shortcodes/types/loop.php new file mode 100644 index 0000000..e0954c4 --- /dev/null +++ b/includes/modules/shortcodes/types/loop.php @@ -0,0 +1,294 @@ +[anys type="post-field" name="post_title"] + * [anys else] + *

No posts found.

+ * [/anys] + * + * @since NEXT + */ +final class Loop extends Base { + use Singleton; + + public function get_type() { + return 'loop'; + } + + protected function get_defaults() { + return [ + 'name' => 'post', + 'post_type' => 'post', + 'posts_per_page' => 3, + 'orderby' => 'date', + 'order' => 'DESC', + 'author' => '', + 'paged' => 1, + 'offset' => 0, + 'post_status' => 'publish', + 's' => '', + 'meta_key' => '', + 'meta_value' => '', + 'meta_compare' => '', + 'meta_query' => '', + 'tax_query' => '', + 'search_in' => '', + 'exclude_current' => '0', + 'before' => '', + 'after' => '', + 'fallback' => '', + 'format' => '', + ]; + } + + /** + * Renders the shortcode. + * + * @since NEXT + * + * @param array $attributes Shortcode attributes. + * @param string $content Enclosed content (optional). + * + * @return string + */ + public function render( array $attributes, string $content ) { + // Parse dynamic attributes first. + $attributes = $this->get_attributes( $attributes ); + $attributes = anys_parse_dynamic_attributes( $attributes ); + + // Validate provider. + $provider_name = strtolower( trim( (string) ( $attributes['name'] ?? '' ) ) ); + if ( $provider_name !== 'post' ) { + return ''; + } + + // Extract item and else templates. + $templates = anys_split_else_block( (string) ( $content ?? '' ) ); + $item_template = $templates['item']; + $else_template = $templates['else']; + + // Build WP_Query args. + $query_args = $this->anys_build_wp_query_args( $attributes ); + $query_args = $this->anys_apply_search_columns( $query_args, $attributes ); + + // Exclude container post if needed. + $exclude_disabled = ( isset( $attributes['exclude_current'] ) && (string) $attributes['exclude_current'] === '0' ); + if ( ! $exclude_disabled ) { + $container_post_id = $this->anys_detect_container_post_id(); + if ( $this->anys_should_exclude_container( $query_args, $attributes, $container_post_id ) ) { + $query_args['post__not_in'] = isset( $query_args['post__not_in'] ) && is_array( $query_args['post__not_in'] ) + ? array_unique( array_merge( $query_args['post__not_in'], [ $container_post_id ] ) ) + : [ $container_post_id ]; + } + } + + // Run query. + $wp_query_instance = new \WP_Query( $query_args ); + + // Handle empty state. + if ( ! $wp_query_instance->have_posts() ) { + if ( $else_template !== '' ) { + return wp_kses_post( do_shortcode( $else_template ) ); + } + wp_reset_postdata(); + return ''; + } + + // Render loop items. + $final_output = ''; + while ( $wp_query_instance->have_posts() ) { + $wp_query_instance->the_post(); + $final_output .= anys_render_template_fast( $item_template ); + } + wp_reset_postdata(); + + // Apply before/after/fallback. + $final_output = anys_wrap_output( $final_output, $attributes ); + + // Return sanitized output. + return wp_kses_post( $final_output ); + } + + /** + * Builds sanitized WP_Query args from shortcode attributes. + * + * @since NEXT + * + * @param array $atts + * + * @return array + */ + private function anys_build_wp_query_args( array $atts ): array { + $args = []; + + foreach ( [ + 'post_type','posts_per_page','orderby','order', + 'author','paged','offset','post_status', + 'meta_key','meta_value','meta_compare', + ] as $key ) { + if ( isset( $atts[ $key ] ) && $atts[ $key ] !== '' ) { + $args[ $key ] = sanitize_text_field( (string) $atts[ $key ] ); + } + } + + foreach ( [ 'posts_per_page', 'author', 'paged', 'offset' ] as $n ) { + if ( isset( $args[ $n ] ) ) { + $args[ $n ] = (int) $args[ $n ]; + } + } + + // Passes 's' as-is (WP_Query sanitizes internally). + if ( isset( $atts['s'] ) && $atts['s'] !== '' ) { + $args['s'] = (string) wp_unslash( $atts['s'] ); + } + + // tax_query (JSON) + if ( ! empty( $atts['tax_query'] ) ) { + $decoded = json_decode( (string) wp_unslash( $atts['tax_query'] ), true ); + if ( is_array( $decoded ) ) { + $clean = []; + foreach ( $decoded as $row ) { + if ( ! is_array( $row ) ) continue; + $item = []; + if ( ! empty( $row['taxonomy'] ) ) $item['taxonomy'] = sanitize_key( $row['taxonomy'] ); + $item['field'] = ( isset( $row['field'] ) && in_array( $row['field'], [ 'term_id', 'name', 'slug' ], true ) ) + ? $row['field'] : 'slug'; + if ( isset( $row['terms'] ) ) { + $item['terms'] = is_array( $row['terms'] ) + ? array_map( 'sanitize_text_field', $row['terms'] ) + : [ sanitize_text_field( (string) $row['terms'] ) ]; + } + if ( isset( $row['operator'] ) && in_array( $row['operator'], [ 'IN', 'NOT IN', 'AND', 'EXISTS', 'NOT EXISTS' ], true ) ) { + $item['operator'] = $row['operator']; + } + if ( $item ) $clean[] = $item; + } + if ( $clean ) $args['tax_query'] = $clean; + } + } + + // meta_query (JSON) + if ( ! empty( $atts['meta_query'] ) ) { + $decoded = json_decode( (string) wp_unslash( $atts['meta_query'] ), true ); + if ( is_array( $decoded ) ) { + $clean = []; + foreach ( $decoded as $row ) { + if ( ! is_array( $row ) ) continue; + $item = []; + if ( isset( $row['key'] ) ) $item['key'] = sanitize_key( $row['key'] ); + if ( isset( $row['value'] ) ) $item['value'] = is_array( $row['value'] ) + ? array_map( 'sanitize_text_field', $row['value'] ) + : sanitize_text_field( (string) $row['value'] ); + if ( isset( $row['compare'] ) ) $item['compare'] = strtoupper( (string) $row['compare'] ); + if ( isset( $row['type'] ) ) $item['type'] = strtoupper( (string) $row['type'] ); + if ( $item ) $clean[] = $item; + } + if ( $clean ) $args['meta_query'] = $clean; + } + } + + if ( empty( $args['post_type'] ) ) { + $args['post_type'] = 'post'; + } + if ( empty( $args['posts_per_page'] ) || (int) $args['posts_per_page'] <= 0 ) { + $args['posts_per_page'] = 10; + } + + // Skips total row count for speed. + $args['no_found_rows'] = true; + // Ignores sticky posts. + $args['ignore_sticky_posts'] = true; + // Suppresses external filters. + $args['suppress_filters'] = true; + + return $args; + } + + /** + * Maps 'search_in' to 'search_columns'. + * + * @since NEXT + * + * @param array $query_args + * @param array $atts + * + * @return array + */ + private function anys_apply_search_columns( array $query_args, array $atts ): array { + if ( empty( $query_args['s'] ) ) { + return $query_args; + } + + $mode = isset( $atts['search_in'] ) ? strtolower( trim( (string) $atts['search_in'] ) ) : 'all'; + + if ( $mode === 'title' ) { + $query_args['search_columns'] = [ 'post_title' ]; + } elseif ( $mode === 'title_excerpt' || $mode === 'excerpt_title' ) { + $query_args['search_columns'] = [ 'post_title', 'post_excerpt' ]; + } + + return $query_args; + } + + /** + * Detects container post ID safely. + * + * @since NEXT + * + * @return int + */ + private function anys_detect_container_post_id(): int { + global $post; + if ( $post instanceof \WP_Post ) { + return (int) $post->ID; + } + + $queried_object = get_queried_object(); + return ( $queried_object instanceof \WP_Post ) ? (int) $queried_object->ID : 0; + } + + /** + * Determines if the container post should be excluded. + * + * @since NEXT + * + * @param array $query_args + * @param array $atts + * @param int $container_id + * + * @return bool + */ + private function anys_should_exclude_container( array $query_args, array $atts, int $container_id ): bool { + if ( $container_id <= 0 || empty( $query_args['s'] ) ) { + return false; + } + + $policy = isset( $atts['search_in'] ) ? strtolower( trim( (string) $atts['search_in'] ) ) : 'all'; + $hay = ''; + + if ( in_array( $policy, [ 'title', 'title_excerpt', 'excerpt_title', 'all' ], true ) ) { + $hay .= ' ' . get_the_title( $container_id ); + } + if ( in_array( $policy, [ 'title_excerpt', 'excerpt_title', 'all' ], true ) ) { + $hay .= ' ' . get_the_excerpt( $container_id ); + } + if ( $policy === 'all' ) { + $hay .= ' ' . get_post_field( 'post_content', $container_id ); + } + + $hay = wp_strip_all_tags( strip_shortcodes( (string) $hay ) ); + return ( $hay !== '' && stripos( $hay, (string) $query_args['s'] ) !== false ); + } +} diff --git a/includes/modules/utilities.php b/includes/modules/utilities.php index 56efbcf..ebd3d21 100644 --- a/includes/modules/utilities.php +++ b/includes/modules/utilities.php @@ -512,184 +512,13 @@ function anys_has_shortcode( $content ) { * @return array{item:string, else:string} */ function anys_split_else_block( string $content ): array { - $raw = shortcode_unautop( $content ); - $parts = preg_split( '/\[\s*anys\s+else\s*\]/i', $raw, 2 ); + $raw = shortcode_unautop( $content ); + $parts = preg_split( '/\[\s*anys\s+else\s*\]/i', $raw, 2 ); - return [ - 'item' => isset( $parts[0] ) ? trim( $parts[0] ) : '', - 'else' => isset( $parts[1] ) ? trim( $parts[1] ) : '', - ]; -} - -/** - * Builds sanitized WP_Query args from shortcode attributes. - * - * @since NEXT - * - * @param array $atts - * - * @return array - */ -function anys_build_wp_query_args( array $atts ): array { - $args = []; - - foreach ( [ - 'post_type','posts_per_page','orderby','order', - 'author','paged','offset','post_status', - 'meta_key','meta_value','meta_compare', - ] as $key ) { - if ( isset( $atts[ $key ] ) && $atts[ $key ] !== '' ) { - $args[ $key ] = sanitize_text_field( (string) $atts[ $key ] ); - } - } - - foreach ( [ 'posts_per_page', 'author', 'paged', 'offset' ] as $n ) { - if ( isset( $args[ $n ] ) ) { - $args[ $n ] = (int) $args[ $n ]; - } - } - - // Passes 's' as-is (WP_Query sanitizes internally). - if ( isset( $atts['s'] ) && $atts['s'] !== '' ) { - $args['s'] = (string) wp_unslash( $atts['s'] ); - } - - // tax_query (JSON) - if ( ! empty( $atts['tax_query'] ) ) { - $decoded = json_decode( (string) wp_unslash( $atts['tax_query'] ), true ); - if ( is_array( $decoded ) ) { - $clean = []; - foreach ( $decoded as $row ) { - if ( ! is_array( $row ) ) continue; - $item = []; - if ( ! empty( $row['taxonomy'] ) ) $item['taxonomy'] = sanitize_key( $row['taxonomy'] ); - $item['field'] = ( isset( $row['field'] ) && in_array( $row['field'], [ 'term_id', 'name', 'slug' ], true ) ) - ? $row['field'] : 'slug'; - if ( isset( $row['terms'] ) ) { - $item['terms'] = is_array( $row['terms'] ) - ? array_map( 'sanitize_text_field', $row['terms'] ) - : [ sanitize_text_field( (string) $row['terms'] ) ]; - } - if ( isset( $row['operator'] ) && in_array( $row['operator'], [ 'IN', 'NOT IN', 'AND', 'EXISTS', 'NOT EXISTS' ], true ) ) { - $item['operator'] = $row['operator']; - } - if ( $item ) $clean[] = $item; - } - if ( $clean ) $args['tax_query'] = $clean; - } - } - - // meta_query (JSON) - if ( ! empty( $atts['meta_query'] ) ) { - $decoded = json_decode( (string) wp_unslash( $atts['meta_query'] ), true ); - if ( is_array( $decoded ) ) { - $clean = []; - foreach ( $decoded as $row ) { - if ( ! is_array( $row ) ) continue; - $item = []; - if ( isset( $row['key'] ) ) $item['key'] = sanitize_key( $row['key'] ); - if ( isset( $row['value'] ) ) $item['value'] = is_array( $row['value'] ) - ? array_map( 'sanitize_text_field', $row['value'] ) - : sanitize_text_field( (string) $row['value'] ); - if ( isset( $row['compare'] ) ) $item['compare'] = strtoupper( (string) $row['compare'] ); - if ( isset( $row['type'] ) ) $item['type'] = strtoupper( (string) $row['type'] ); - if ( $item ) $clean[] = $item; - } - if ( $clean ) $args['meta_query'] = $clean; - } - } - - if ( empty( $args['post_type'] ) ) { - $args['post_type'] = 'post'; - } - if ( empty( $args['posts_per_page'] ) || (int) $args['posts_per_page'] <= 0 ) { - $args['posts_per_page'] = 10; - } - - // Skips total row count for speed. - $args['no_found_rows'] = true; - // Ignores sticky posts. - $args['ignore_sticky_posts'] = true; - // Suppresses external filters. - $args['suppress_filters'] = true; - - return $args; -} - -/** - * Maps 'search_in' to 'search_columns'. - * - * @since NEXT - * - * @param array $query_args - * @param array $atts - * - * @return array - */ -function anys_apply_search_columns( array $query_args, array $atts ): array { - if ( empty( $query_args['s'] ) ) { - return $query_args; - } - - $mode = isset( $atts['search_in'] ) ? strtolower( trim( (string) $atts['search_in'] ) ) : 'all'; - - if ( $mode === 'title' ) { - $query_args['search_columns'] = [ 'post_title' ]; - } elseif ( $mode === 'title_excerpt' || $mode === 'excerpt_title' ) { - $query_args['search_columns'] = [ 'post_title', 'post_excerpt' ]; - } - - return $query_args; -} - -/** - * Detects container post ID safely. - * - * @since NEXT - * - * @return int - */ -function anys_detect_container_post_id(): int { - global $post; - if ( $post instanceof \WP_Post ) { - return (int) $post->ID; - } - - $queried_object = get_queried_object(); - return ( $queried_object instanceof \WP_Post ) ? (int) $queried_object->ID : 0; -} - -/** - * Determines if the container post should be excluded. - * - * @since NEXT - * - * @param array $query_args - * @param array $atts - * @param int $container_id - * - * @return bool - */ -function anys_should_exclude_container( array $query_args, array $atts, int $container_id ): bool { - if ( $container_id <= 0 || empty( $query_args['s'] ) ) { - return false; - } - - $policy = isset( $atts['search_in'] ) ? strtolower( trim( (string) $atts['search_in'] ) ) : 'all'; - $hay = ''; - - if ( in_array( $policy, [ 'title', 'title_excerpt', 'excerpt_title', 'all' ], true ) ) { - $hay .= ' ' . get_the_title( $container_id ); - } - if ( in_array( $policy, [ 'title_excerpt', 'excerpt_title', 'all' ], true ) ) { - $hay .= ' ' . get_the_excerpt( $container_id ); - } - if ( $policy === 'all' ) { - $hay .= ' ' . get_post_field( 'post_content', $container_id ); - } - - $hay = wp_strip_all_tags( strip_shortcodes( (string) $hay ) ); - return ( $hay !== '' && stripos( $hay, (string) $query_args['s'] ) !== false ); + return [ + 'item' => isset( $parts[0] ) ? trim( $parts[0] ) : '', + 'else' => isset( $parts[1] ) ? trim( $parts[1] ) : '', + ]; } /** @@ -702,9 +531,9 @@ function anys_should_exclude_container( array $query_args, array $atts, int $con * @return string */ function anys_render_template_fast( string $template ): string { - return ( function_exists( 'anys_has_shortcode' ) && anys_has_shortcode( $template ) ) - ? do_shortcode( $template ) - : $template; + return ( function_exists( 'anys_has_shortcode' ) && anys_has_shortcode( $template ) ) + ? do_shortcode( $template ) + : $template; } /** diff --git a/includes/types/anys/loop.php b/includes/types/anys/loop.php deleted file mode 100644 index fd03932..0000000 --- a/includes/types/anys/loop.php +++ /dev/null @@ -1,87 +0,0 @@ -[anys type="post-field" name="post_title"] - * [anys else] - *

No posts found.

- * [/anys] - * - * Providers: - * - name="post" (WP_Query) - * - * Attributes (post): - * - post_type, posts_per_page, orderby, order, author, paged, offset, post_status, s - * - meta_key, meta_value, meta_compare - * - meta_query (JSON), tax_query (JSON) - * - search_in ("all"|"title"|"title_excerpt") - * - exclude_current ("1"|"0") - * - * @since NEXT - */ - -defined( 'ABSPATH' ) || exit; - -// Validates attributes. -if ( ! isset( $attributes ) || ! is_array( $attributes ) ) { - return; -} - -// Validates provider. -$provider = isset( $attributes['name'] ) ? strtolower( trim( (string) $attributes['name'] ) ) : ''; -if ( $provider !== 'post' ) { - return; -} - -// Parses dynamic attribute placeholders. -$attributes = function_exists( 'anys_parse_dynamic_attributes' ) - ? anys_parse_dynamic_attributes( $attributes ) - : $attributes; - -// Extracts item/else templates. -$templates = anys_split_else_block( (string) ( $content ?? '' ) ); -$item_template = $templates['item']; -$else_template = $templates['else']; - -// Builds base WP_Query args from attributes. -$query_args = anys_build_wp_query_args( $attributes ); - -// Maps 'search_in' to 'search_columns'. -$query_args = anys_apply_search_columns( $query_args, $attributes ); - -// Excludes container post when it would self-match. -$exclude_disabled = ( isset( $attributes['exclude_current'] ) && (string) $attributes['exclude_current'] === '0' ); -if ( ! $exclude_disabled ) { - $container_id = anys_detect_container_post_id(); - if ( anys_should_exclude_container( $query_args, $attributes, $container_id ) ) { - $query_args['post__not_in'] = isset( $query_args['post__not_in'] ) && is_array( $query_args['post__not_in'] ) - ? array_unique( array_merge( $query_args['post__not_in'], [ $container_id ] ) ) - : [ $container_id ]; - } -} - -// Runs query. -$wp_query_instance = new \WP_Query( $query_args ); - -// Handles empty state via [anys else]. -if ( ! $wp_query_instance->have_posts() ) { - if ( $else_template !== '' ) { - echo wp_kses_post( do_shortcode( $else_template ) ); - } - wp_reset_postdata(); - return; -} - -// Renders items. -$final_output = ''; -while ( $wp_query_instance->have_posts() ) { - $wp_query_instance->the_post(); - $final_output .= anys_render_template_fast( $item_template ); // Avoids extra do_shortcode when not needed. -} -wp_reset_postdata(); - -// Applies before/after/fallback and outputs. -$final_output = anys_wrap_output( $final_output, $attributes ); -echo wp_kses_post( $final_output );