posts ) && function_exists( 'update_post_author_caches' ) ) { update_post_author_caches( $result->posts ); } return $result; } } // ============================================================================= // SHORTCODE-BASED RELATIONSHIPS // ============================================================================= /** * Register relationships for posts with certain shortcodes * * @since 5.0.0 * * @param int $post_id The ID of the saved post. * @param WP_Post $post The saved post object. */ function fictioneer_update_shortcode_relationships( $post_id, $post ) { // Prevent multi-fire if ( fictioneer_multi_save_guard( $post_id ) ) { return; } // Setup $registry = fictioneer_get_relationship_registry(); // Look for blog shortcode if ( str_contains( $post->post_content, 'fictioneer_blog' ) ) { $registry['always'][ $post_id ] = 'shortcode'; } else { unset( $registry['always'][ $post_id ] ); } // Look for story data shortcode if ( str_contains( $post->post_content, 'fictioneer_story_data' ) || str_contains( $post->post_content, 'fictioneer_story_comments' ) || str_contains( $post->post_content, 'fictioneer_story_actions' ) || str_contains( $post->post_content, 'fictioneer_story_section' ) || str_contains( $post->post_content, 'fictioneer_subscribe_button' ) ) { $registry['always'][ $post_id ] = 'shortcode'; } else { unset( $registry['always'][ $post_id ] ); } // Look for article cards shortcode if ( str_contains( $post->post_content, 'fictioneer_article_cards' ) ) { $registry['always'][ $post_id ] = 'shortcode'; } else { unset( $registry['always'][ $post_id ] ); } // Look for showcase shortcode if ( str_contains( $post->post_content, 'fictioneer_showcase' ) ) { $registry['always'][ $post_id ] = 'shortcode'; } else { unset( $registry['always'][ $post_id ] ); } // Look for post-related shortcode if ( str_contains( $post->post_content, 'fictioneer_latest_posts' ) ) { $registry['ref_posts'][ $post_id ] = 'shortcode'; } else { unset( $registry['ref_posts'][ $post_id ] ); } // Look for chapter-related shortcodes if ( str_contains( $post->post_content, 'fictioneer_chapter_list' ) || str_contains( $post->post_content, 'fictioneer_latest_chapters' ) ) { $registry['ref_chapters'][ $post_id ] = 'shortcode'; } else { unset( $registry['ref_chapters'][ $post_id ] ); } // Look for story-related shortcodes if ( str_contains( $post->post_content, 'fictioneer_latest_stories' ) || str_contains( $post->post_content, 'fictioneer_latest_updates' ) ) { $registry['ref_stories'][ $post_id ] = 'shortcode'; } else { unset( $registry['ref_stories'][ $post_id ] ); } // Look for recommendation-related shortcodes if ( str_contains( $post->post_content, 'fictioneer_latest_recommendations' ) ) { $registry['ref_recommendations'][ $post_id ] = 'shortcode'; } else { unset( $registry['ref_recommendations'][ $post_id ] ); } // Update database fictioneer_save_relationship_registry( $registry ); } if ( FICTIONEER_RELATIONSHIP_PURGE_ASSIST ) { add_action( 'save_post', 'fictioneer_update_shortcode_relationships', 10, 2 ); } // ============================================================================= // GET SHORTCODE DEFAULT ARGS // ============================================================================= /** * Returns sanitized arguments extracted from shortcode attributes * * @since 5.7.3 * * @param array $attr Attributes passed to the shortcode. * @param int $def_count Default for the 'count' argument. * * @return array The extracted arguments. */ function fictioneer_get_default_shortcode_args( $attr, $def_count = -1 ) { //--- Sanitize attributes ---------------------------------------------------- $attr = is_array( $attr ) ? array_map( 'sanitize_text_field', $attr ) : sanitize_text_field( $attr ); //--- Extract arguments ------------------------------------------------------ $seamless_default = get_theme_mod( 'card_image_style', 'default' ) === 'seamless'; $thumbnail_default = get_theme_mod( 'card_image_style', 'default' ) !== 'none'; $uid = wp_unique_id( 'shortcode-id-' ); $args = array( 'uid' => $uid, 'type' => $attr['type'] ?? 'default', 'count' => max( -1, intval( $attr['count'] ?? $def_count ) ), 'offset' => max( 0, intval( $attr['offset'] ?? 0 ) ), 'order' => $attr['order'] ?? '', 'orderby' => $attr['orderby'] ?? '', 'page' => max( 1, get_query_var( 'page' ) ?: get_query_var( 'paged' ) ), 'posts_per_page' => absint( $attr['per_page'] ?? 0 ) ?: get_option( 'posts_per_page' ), 'post_ids' => fictioneer_explode_list( $attr['post_ids'] ?? '' ), 'author' => sanitize_title( $attr['author'] ?? '' ), 'author_ids' => fictioneer_explode_list( $attr['author_ids'] ?? '' ), 'excluded_authors' => fictioneer_explode_list( $attr['exclude_author_ids'] ?? '' ), 'excluded_tags' => fictioneer_explode_list( $attr['exclude_tag_ids'] ?? '' ), 'excluded_cats' => fictioneer_explode_list( $attr['exclude_cat_ids'] ?? '' ), 'taxonomies' => fictioneer_get_shortcode_taxonomies( $attr ), 'relation' => strtolower( $attr['rel'] ?? 'and' ) === 'or' ? 'OR' : 'AND', 'ignore_sticky' => filter_var( $attr['ignore_sticky'] ?? 0, FILTER_VALIDATE_BOOLEAN ), 'ignore_protected' => filter_var( $attr['ignore_protected'] ?? 0, FILTER_VALIDATE_BOOLEAN ), 'only_protected' => filter_var( $attr['only_protected'] ?? 0, FILTER_VALIDATE_BOOLEAN ), 'vertical' => filter_var( $attr['vertical'] ?? 0, FILTER_VALIDATE_BOOLEAN ), 'seamless' => filter_var( $attr['seamless'] ?? $seamless_default, FILTER_VALIDATE_BOOLEAN ), 'aspect_ratio' => sanitize_css_aspect_ratio( $attr['aspect_ratio'] ?? '' ), 'thumbnail' => filter_var( $attr['thumbnail'] ?? $thumbnail_default, FILTER_VALIDATE_BOOLEAN ), 'lightbox' => filter_var( $attr['lightbox'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'words' => filter_var( $attr['words'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'date' => filter_var( $attr['date'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'date_format' => sanitize_text_field( $attr['date_format'] ?? '' ), 'nested_date_format' => sanitize_text_field( $attr['nested_date_format'] ?? '' ), 'footer' => filter_var( $attr['footer'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_author' => filter_var( $attr['footer_author'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_chapters' => filter_var( $attr['footer_chapters'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_words' => filter_var( $attr['footer_words'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_date' => filter_var( $attr['footer_date'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_comments' => filter_var( $attr['footer_comments'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_status' => filter_var( $attr['footer_status'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'footer_rating' => filter_var( $attr['footer_rating'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'classes' => esc_attr( wp_strip_all_tags( $attr['classes'] ?? $attr['class'] ?? '' ) ) . " {$uid}", 'infobox' => filter_var( $attr['infobox'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'source' => filter_var( $attr['source'] ?? 1, FILTER_VALIDATE_BOOLEAN ), 'splide' => sanitize_text_field( $attr['splide'] ?? '' ), 'cache' => filter_var( $attr['cache'] ?? 1, FILTER_VALIDATE_BOOLEAN ) ); //--- Fixes ------------------------------------------------------------------ // Update count if limited to post IDs if ( ! empty( $args['post_ids'] ) ) { $args['count'] = count( $args['post_ids'] ); } // Prepare Splide JSON if ( ! empty( $args['splide'] ) ) { $args['splide'] = str_replace( "'", '"', $args['splide'] ); if ( ! fictioneer_is_valid_json( $args['splide'] ) ) { $args['splide'] = false; } else { $splide = json_decode( $args['splide'], true ); // Turn arrows off by default if ( ! preg_match( '/"arrows"\s*:\s*true/', $args['splide'] ) ) { $splide['arrows'] = false; } // Change default arrow SVG path if ( ! isset( $splide['arrowPath'] ) ) { $splide['arrowPath'] = 'M31.89 18.24c0.98 0.98 0.98 2.56 0 3.54l-15 15c-0.98 0.98-2.56 0.98-3.54 0s-0.98-2.56 0-3.54L26.45 20 13.23 6.76c-0.98-0.98-0.98-2.56 0-3.54s2.56-0.98 3.54 0l15 15'; } $args['splide'] = json_encode( $splide ); } } //--- Finish ----------------------------------------------------------------- return $args; } // ============================================================================= // GET SHORTCODE TAXONOMIES // ============================================================================= /** * Extract taxonomies from shortcode attributes * * @since 5.2.0 * * @param array $attr Attributes of the shortcode. * * @return array Array of found taxonomies. */ function fictioneer_get_shortcode_taxonomies( $attr ) { // Setup $taxonomies = []; // Tags if ( ! empty( $attr['tags'] ) ) { $taxonomies['tags'] = fictioneer_explode_list( $attr['tags'] ); } // Categories if ( ! empty( $attr['categories'] ) ) { $taxonomies['categories'] = fictioneer_explode_list( $attr['categories'] ); } // Fandoms if ( ! empty( $attr['fandoms'] ) ) { $taxonomies['fandoms'] = fictioneer_explode_list( $attr['fandoms'] ); } // Characters if ( ! empty( $attr['characters'] ) ) { $taxonomies['characters'] = fictioneer_explode_list( $attr['characters'] ); } // Genres if ( ! empty( $attr['genres'] ) ) { $taxonomies['genres'] = fictioneer_explode_list( $attr['genres'] ); } // Return return $taxonomies; } // ============================================================================= // GET SHORTCODE TAX QUERY // ============================================================================= /** * Get shortcode Tax Query * * @since 5.2.0 * * @param array $args Arguments of the shortcode partial. * * @return array Tax Query. */ function fictioneer_get_shortcode_tax_query( $args ) { // Setup $tax_query = []; // Are there taxonomies? if ( ! empty( $args['taxonomies'] ) ) { // Relationship? if ( count( $args['taxonomies'] ) > 1 ) { $tax_query['relation'] = $args['relation']; } // Tags? if ( ! empty( $args['taxonomies']['tags'] ) ) { $tax_query[] = array( 'taxonomy' => 'post_tag', 'field' => 'name', 'terms' => $args['taxonomies']['tags'] ); } // Categories? if ( ! empty( $args['taxonomies']['categories'] ) ) { $tax_query[] = array( 'taxonomy' => 'category', 'field' => 'name', 'terms' => $args['taxonomies']['categories'] ); } // Fandoms? if ( ! empty( $args['taxonomies']['fandoms'] ) ) { $tax_query[] = array( 'taxonomy' => 'fcn_fandom', 'field' => 'name', 'terms' => $args['taxonomies']['fandoms'] ); } // Characters? if ( ! empty( $args['taxonomies']['characters'] ) ) { $tax_query[] = array( 'taxonomy' => 'fcn_character', 'field' => 'name', 'terms' => $args['taxonomies']['characters'] ); } // Genres? if ( ! empty( $args['taxonomies']['genres'] ) ) { $tax_query[] = array( 'taxonomy' => 'fcn_genre', 'field' => 'name', 'terms' => $args['taxonomies']['genres'] ); } } // Return return $tax_query; } // ============================================================================= // SPLIDE // ============================================================================= /** * Returns inline script to initialize Splide ASAP * * Note: The script tag is only returned once in case multiple sliders * are active since only one is needed. * * @since 5.25.0 * @since 5.26.1 - Use wp_print_inline_script_tag(). * * @return string The inline script. */ function fictioneer_get_splide_inline_init() { static $done = null; if ( $done ) { return ''; } $done = true; return wp_get_inline_script_tag( 'document.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll(".splide:not(.no-auto-splide, .is-initialized)").forEach(e=>{e.querySelector(".splide__list")&&"undefined"!=typeof Splide&&(e.classList.remove("_splide-placeholder"),new Splide(e).mount())})});', array( 'id' => 'fictioneer-iife-splide', 'class' => 'temp-script', 'type' => 'text/javascript', 'data-jetpack-boost' => 'ignore', 'data-no-optimize' => '1', 'data-no-defer' => '1', 'data-no-minify' => '1' ) ); } // ============================================================================= // SHOWCASE SHORTCODE // ============================================================================= /** * Shortcode to display showcase * * @since 5.0.0 * * @param string $attr['for'] What the showcase is for. Allowed are chapters, * collections, recommendations, and stories. * @param string|null $attr['count'] Optional. Maximum number of items. Default 8. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['order'] Optional. Order direction. Default 'DESC'. * @param string|null $attr['orderby'] Optional. Order argument. Default 'date'. * @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['fandoms'] Optional. Limit posts to specific fandom names. * @param string|null $attr['genres'] Optional. Limit posts to specific genre names. * @param string|null $attr['characters'] Optional. Limit posts to specific character names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['vertical'] Optional. Whether to show the vertical variant. * @param string|null $attr['seamless'] Optional. Whether to render the image seamless. Default false (Customizer). * @param string|null $attr['aspect_ratio'] Optional. Aspect ratio of the item. Default empty. * @param string|null $attr['height'] Optional. Override the item height. Default empty. * @param string|null $attr['min_width'] Optional. Override the item minimum width. Default empty. * @param string|null $attr['lightbox'] Optional. Whether the thumbnail is opened in the lightbox. Default true. * @param string|null $attr['thumbnail'] Optional. Whether to show the thumbnail. Default true (Customizer). * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $args['splide'] Optional. Configuration JSON for the Splide slider. Default empty. * @param string|null $args['quality'] Optional. Size of the images. Default 'medium'. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_showcase( $attr ) { // Abort if... if ( empty( $attr['for'] ) ) { return ''; } // Defaults $args = fictioneer_get_default_shortcode_args( $attr, 8 ); // Height/Width/Quality $args['height'] = sanitize_text_field( $attr['height'] ?? '' ); $args['min_width'] = sanitize_text_field( $attr['min_width'] ?? '' ); $args['quality'] = sanitize_text_field( $attr['quality'] ?? 'medium' ); // Specifics $args['no_cap'] = filter_var( $attr['no_cap'] ?? 0, FILTER_VALIDATE_BOOLEAN ); switch ( $attr['for'] ) { case 'collections': $args['post_type'] = 'fcn_collection'; break; case 'chapters': $args['post_type'] = 'fcn_chapter'; break; case 'stories': $args['post_type'] = 'fcn_story'; break; case 'recommendations': $args['post_type'] = 'fcn_recommendation'; break; } // Abort if... if ( ! isset( $args['post_type'] ) ) { return ''; } // Extra classes if ( $args['splide'] ?? 0 ) { $args['classes'] .= ' splide _splide-placeholder'; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_showcase' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $type = $args['post_type']; $transient_key = "fictioneer_shortcode_showcase_{$type}_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); get_template_part( 'partials/_showcase', null, $args ); $html = fictioneer_minify_html( ob_get_clean() ); if ( ( $args['splide'] ?? 0 ) && strpos( $args['classes'], 'no-auto-splide' ) === false ) { $html .= fictioneer_get_splide_inline_init(); } if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_showcase', 'fictioneer_shortcode_showcase' ); // ============================================================================= // LATEST CHAPTERS SHORTCODE // ============================================================================= /** * Shortcode to show latest chapters * * @since 3.0 * * @param string|null $attr['count'] Optional. Maximum number of items. Default 4. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['type'] Optional. Choose between 'default', 'simple', and 'compact'. * @param string|null $attr['order'] Optional. Order argument. Default 'DESC'. * @param string|null $attr['orderby'] Optional. Orderby argument. Default 'date'. * @param string|null $attr['spoiler'] Optional. Whether to show spoiler content. * @param string|null $attr['source'] Optional. Whether to show the author and story. * @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['fandoms'] Optional. Limit posts to specific fandom names. * @param string|null $attr['genres'] Optional. Limit posts to specific genre names. * @param string|null $attr['characters'] Optional. Limit posts to specific character names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['vertical'] Optional. Whether to show the vertical variant. * @param string|null $attr['seamless'] Optional. Whether to render the image seamless. Default false (Customizer). * @param string|null $attr['aspect_ratio'] Optional. Aspect ratio for the image. Only with vertical. * @param string|null $attr['lightbox'] Optional. Whether the thumbnail is opened in the lightbox. Default true. * @param string|null $attr['thumbnail'] Optional. Whether to show the thumbnail. Default true (Customizer). * @param string|null $attr['date_format'] Optional. String to override the date format. Default empty. * @param string|null $attr['footer'] Optional. Whether to show the footer (if any). Default true. * @param string|null $attr['footer_author'] Optional. Whether to show the chapter author. Default true. * @param string|null $attr['footer_date'] Optional. Whether to show the chapter date. Default true. * @param string|null $attr['footer_words'] Optional. Whether to show the chapter word count. Default true. * @param string|null $attr['footer_comments'] Optional. Whether to show the chapter comment count. Default true. * @param string|null $attr['footer_status'] Optional. Whether to show the chapter status. Default true. * @param string|null $attr['footer_rating'] Optional. Whether to show the story/chapter age rating. Default true. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $args['splide'] Configuration JSON for the Splide slider. Default empty. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_latest_chapters( $attr ) { // Defaults $args = fictioneer_get_default_shortcode_args( $attr, 4 ); // Specifics $args['simple'] = false; $args['spoiler'] = filter_var( $attr['spoiler'] ?? 0, FILTER_VALIDATE_BOOLEAN ); // Type $type = sanitize_text_field( $attr['type'] ?? 'default' ); // Extra classes if ( $args['splide'] ?? 0 ) { $args['classes'] .= ' splide _splide-placeholder'; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_latest_chapters' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $transient_key = "fictioneer_shortcode_latest_chapters_{$type}_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); switch ( $type ) { case 'compact': get_template_part( 'partials/_latest-chapters-compact', null, $args ); break; case 'list': get_template_part( 'partials/_latest-chapters-list', null, $args ); break; default: $args['simple'] = $type == 'simple'; get_template_part( 'partials/_latest-chapters', null, $args ); } $html = fictioneer_minify_html( ob_get_clean() ); if ( ( $args['splide'] ?? 0 ) && strpos( $args['classes'], 'no-auto-splide' ) === false ) { $html .= fictioneer_get_splide_inline_init(); } if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_latest_chapters', 'fictioneer_shortcode_latest_chapters' ); // ============================================================================= // LATEST STORIES SHORTCODE // ============================================================================= /** * Shortcode to show latest stories * * @since 3.0 * * @param string|null $attr['count'] Optional. Maximum number of items. Default 4. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['type'] Optional. Choose between 'default' and 'compact'. * @param string|null $attr['order'] Optional. Order argument. Default 'DESC'. * @param string|null $attr['orderby'] Optional. Orderby argument. Default 'date'. * @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['fandoms'] Optional. Limit posts to specific fandom names. * @param string|null $attr['genres'] Optional. Limit posts to specific genre names. * @param string|null $attr['characters'] Optional. Limit posts to specific character names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['vertical'] Optional. Whether to show the vertical variant. * @param string|null $attr['seamless'] Optional. Whether to render the image seamless. Default false (Customizer). * @param string|null $attr['aspect_ratio'] Optional. Aspect ratio for the image. Only with vertical. * @param string|null $attr['lightbox'] Optional. Whether the thumbnail is opened in the lightbox. Default true. * @param string|null $attr['thumbnail'] Optional. Whether to show the thumbnail. Default true (Customizer). * @param string|null $attr['date_format'] Optional. String to override the date format. Default empty. * @param string|null $attr['terms'] Optional. Either 'inline', 'pills', 'none', or 'false' (only in list type). * Default 'inline'. * @param string|null $attr['max_terms'] Optional. Maximum number of shown taxonomies. Default 10. * @param string|null $attr['footer'] Optional. Whether to show the footer (if any). Default true. * @param string|null $attr['footer_author'] Optional. Whether to show the story author. Default true. * @param string|null $attr['footer_date'] Optional. Whether to show the story date. Default true. * @param string|null $attr['footer_words'] Optional. Whether to show the story word count. Default true. * @param string|null $attr['footer_chapters'] Optional. Whether to show the story chapter count. Default true. * @param string|null $attr['footer_status'] Optional. Whether to show the story status. Default true. * @param string|null $attr['footer_rating'] Optional. Whether to show the story age rating. Default true. * @param string|null $attr['footer_comments'] Optional. Whether to show the post comment count. Default false. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $args['splide'] Configuration JSON for the Splide slider. Default empty. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_latest_stories( $attr ) { // Defaults $args = fictioneer_get_default_shortcode_args( $attr, 4 ); // Type $type = sanitize_text_field( $attr['type'] ?? 'default' ); // Comments $args['footer_comments'] = filter_var( $attr['footer_comments'] ?? 0, FILTER_VALIDATE_BOOLEAN ); // Terms $args['terms'] = fictioneer_sanitize_query_var( $attr['terms'] ?? 0, ['inline', 'pills', 'none', 'false'], 'inline' ); $args['max_terms'] = absint( ( $attr['max_terms'] ?? 10 ) ?: 10 ); // Extra classes if ( $args['splide'] ?? 0 ) { $args['classes'] .= ' splide _splide-placeholder'; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_latest_stories' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $transient_key = "fictioneer_shortcode_latest_stories_{$type}_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); switch ( $type ) { case 'compact': get_template_part( 'partials/_latest-stories-compact', null, $args ); break; case 'list': get_template_part( 'partials/_latest-stories-list', null, $args ); break; default: get_template_part( 'partials/_latest-stories', null, $args ); } $html = fictioneer_minify_html( ob_get_clean() ); if ( ( $args['splide'] ?? 0 ) && strpos( $args['classes'], 'no-auto-splide' ) === false ) { $html .= fictioneer_get_splide_inline_init(); } if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_latest_stories', 'fictioneer_shortcode_latest_stories' ); // ============================================================================= // LATEST UPDATES SHORTCODE // ============================================================================= /** * Shortcode to show latest story updates * * @since 4.3.0 * * @param string|null $attr['count'] Optional. Maximum number of items. Default 4. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['type'] Optional. Choose between 'default', 'simple', 'single', and 'compact'. * @param string|null $attr['single'] Optional. Whether to show only one chapter item. Default false. * @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['fandoms'] Optional. Limit posts to specific fandom names. * @param string|null $attr['genres'] Optional. Limit posts to specific genre names. * @param string|null $attr['characters'] Optional. Limit posts to specific character names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['vertical'] Optional. Whether to show the vertical variant. * @param string|null $attr['seamless'] Optional. Whether to render the image seamless. Default false (Customizer). * @param string|null $attr['aspect_ratio'] Optional. Aspect ratio for the image. Only with vertical. * @param string|null $attr['lightbox'] Optional. Whether the thumbnail is opened in the lightbox. Default true. * @param string|null $attr['thumbnail'] Optional. Whether to show the thumbnail. Default true (Customizer). * @param string|null $attr['words'] Optional. Whether to show the word count of chapter items. Default true. * @param string|null $attr['date'] Optional. Whether to show the date of chapter items. Default true. * @param string|null $attr['date_format'] Optional. String to override the date format. Default empty. * @param string|null $attr['nested_date_format'] Optional. String to override any nested date formats. Default empty. * @param string|null $attr['terms'] Optional. Either 'inline', 'pills', 'none', or 'false' (only in list type). * Default 'inline'. * @param string|null $attr['max_terms'] Optional. Maximum number of shown taxonomies. Default 10. * @param string|null $attr['footer'] Optional. Whether to show the footer (if any). Default true. * @param string|null $attr['footer_author'] Optional. Whether to show the story author. Default true. * @param string|null $attr['footer_date'] Optional. Whether to show the story date. Default true. * @param string|null $attr['footer_words'] Optional. Whether to show the story word count. Default true. * @param string|null $attr['footer_chapters'] Optional. Whether to show the story chapter count. Default true. * @param string|null $attr['footer_status'] Optional. Whether to show the story status. Default true. * @param string|null $attr['footer_rating'] Optional. Whether to show the story/chapter age rating. Default true. * @param string|null $attr['footer_comments'] Optional. Whether to show the post comment count. Default false. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $args['splide'] Configuration JSON for the Splide slider. Default empty. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_latest_story_updates( $attr ) { // Defaults $args = fictioneer_get_default_shortcode_args( $attr, 4 ); $args['single'] = filter_var( $attr['single'] ?? 0, FILTER_VALIDATE_BOOLEAN ); // Comments $args['footer_comments'] = filter_var( $attr['footer_comments'] ?? 0, FILTER_VALIDATE_BOOLEAN ); // Type $type = sanitize_text_field( $attr['type'] ?? 'default' ); // Terms $args['terms'] = fictioneer_sanitize_query_var( $attr['terms'] ?? 0, ['inline', 'pills', 'none', 'false'], 'inline' ); $args['max_terms'] = absint( ( $attr['max_terms'] ?? 10 ) ?: 10 ); // Extra classes if ( $args['splide'] ?? 0 ) { $args['classes'] .= ' splide _splide-placeholder '; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_latest_updates' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $transient_key = "fictioneer_shortcode_latest_updates_{$type}_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); switch ( $type ) { case 'compact': get_template_part( 'partials/_latest-updates-compact', null, $args ); break; case 'list': get_template_part( 'partials/_latest-updates-list', null, $args ); break; default: get_template_part( 'partials/_latest-updates', null, $args ); } $html = fictioneer_minify_html( ob_get_clean() ); if ( ( $args['splide'] ?? 0 ) && strpos( $args['classes'], 'no-auto-splide' ) === false ) { $html .= fictioneer_get_splide_inline_init(); } if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_latest_updates', 'fictioneer_shortcode_latest_story_updates' ); // ============================================================================= // LATEST RECOMMENDATIONS SHORTCODE // ============================================================================= /** * Shortcode to show latest recommendations * * @since 4.0.0 * * @param string|null $attr['count'] Optional. Maximum number of items. Default 4. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['type'] Optional. Choose between 'default' and 'compact'. * @param string|null $attr['order'] Optional. Order argument. Default 'DESC'. * @param string|null $attr['orderby'] Optional. Orderby argument. Default 'date'. * @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['fandoms'] Optional. Limit posts to specific fandom names. * @param string|null $attr['genres'] Optional. Limit posts to specific genre names. * @param string|null $attr['characters'] Optional. Limit posts to specific character names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['vertical'] Optional. Whether to show the vertical variant. * @param string|null $attr['seamless'] Optional. Whether to render the image seamless. Default false (Customizer). * @param string|null $attr['aspect_ratio'] Optional. Aspect ratio for the image. Only with vertical. * @param string|null $attr['lightbox'] Optional. Whether the thumbnail is opened in the lightbox. Default true. * @param string|null $attr['thumbnail'] Optional. Whether to show the thumbnail. Default true (Customizer). * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $args['splide'] Configuration JSON for the Splide slider. Default empty. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_latest_recommendations( $attr ) { // Defaults $args = fictioneer_get_default_shortcode_args( $attr, 4 ); // Type $type = sanitize_text_field( $attr['type'] ?? 'default' ); // Terms $args['terms'] = fictioneer_sanitize_query_var( $attr['terms'] ?? 0, ['inline', 'pills', 'none', 'false'], 'inline' ); $args['max_terms'] = absint( ( $attr['max_terms'] ?? 10 ) ?: 10 ); // Extra classes if ( $args['splide'] ?? 0 ) { $args['classes'] .= ' splide _splide-placeholder'; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_latest_recommendations' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $transient_key = "fictioneer_shortcode_latest_recommendations_{$type}_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); switch ( $type ) { case 'compact': get_template_part( 'partials/_latest-recommendations-compact', null, $args ); break; default: get_template_part( 'partials/_latest-recommendations', null, $args ); } $html = fictioneer_minify_html( ob_get_clean() ); if ( ( $args['splide'] ?? 0 ) && strpos( $args['classes'], 'no-auto-splide' ) === false ) { $html .= fictioneer_get_splide_inline_init(); } if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_latest_recommendations', 'fictioneer_shortcode_latest_recommendations' ); // ============================================================================= // LATEST POST SHORTCODE // ============================================================================= /** * Shortcode to show the latest post * * @since 4.0.0 * * @param string|null $attr['count'] Optional. Maximum number of items. Default 1. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_latest_posts( $attr ) { // Defaults $args = fictioneer_get_default_shortcode_args( $attr, 1 ); // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_latest_posts' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $transient_key = "fictioneer_shortcode_latest_posts_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); get_template_part( 'partials/_latest-posts', null, $args ); $html = fictioneer_minify_html( ob_get_clean() ); if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_latest_posts', 'fictioneer_shortcode_latest_posts' ); // ============================================================================= // BOOKMARKS SHORTCODE // ============================================================================= /** * Shortcode to show bookmarks * * @since 4.0.0 * * @param string|null $attr['count'] Optional. Maximum number of items. Default -1 (all). * @param string|null $attr['show_empty'] Optional. Whether to show the "no bookmarks" message. Default false. * @param string|null $attr['seamless'] Optional. Whether to render the image seamless. Default false (Customizer). * @param string|null $attr['thumbnail'] Optional. Whether to show the thumbnail. Default true (Customizer). * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_bookmarks( $attr ) { // Sanitize attributes $attr = is_array( $attr ) ? array_map( 'sanitize_text_field', $attr ) : sanitize_text_field( $attr ); // Setup $seamless_default = get_theme_mod( 'card_image_style', 'default' ) === 'seamless'; $thumbnail_default = get_theme_mod( 'card_image_style', 'default' ) !== 'none'; // Buffer ob_start(); get_template_part( 'partials/_bookmarks', null, array( 'count' => max( -1, intval( $attr['count'] ?? -1 ) ), 'show_empty' => $attr['show_empty'] ?? false, 'seamless' => filter_var( $attr['seamless'] ?? $seamless_default, FILTER_VALIDATE_BOOLEAN ), 'thumbnail' => filter_var( $attr['thumbnail'] ?? $thumbnail_default, FILTER_VALIDATE_BOOLEAN ) )); // Return minified buffer return fictioneer_minify_html( ob_get_clean() ); } add_shortcode( 'fictioneer_bookmarks', 'fictioneer_shortcode_bookmarks' ); // ============================================================================= // COOKIES SHORTCODE // ============================================================================= /** * Shortcode to show cookie consent actions * * Renders buttons to handle your consent and stored cookies. * * @since 4.7.0 * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_cookie_buttons( $attr ) { ob_start(); // Start HTML ---> ?>
heading above list. * * @return string The captured HTML. */ function fictioneer_shortcode_chapter_list_empty( $attr ) { ob_start(); // Start HTML ---> ?>
heading above list. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_chapter_list( $attr ) { // Sanitize attributes $attr = is_array( $attr ) ? array_map( 'sanitize_text_field', $attr ) : sanitize_text_field( $attr ); // Return empty case if... if ( empty( $attr['story_id'] ) && empty( $attr['chapter_ids'] ) ) { return fictioneer_shortcode_chapter_list_empty( $attr ); } // Setup $cache = filter_var( $attr['cache'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $count = max( -1, intval( $attr['count'] ?? -1 ) ); $offset = max( 0, intval( $attr['offset'] ?? 0 ) ); $group = empty( $attr['group'] ) ? false : strtolower( trim( $attr['group'] ) ); $heading = empty( $attr['heading'] ) ? false : $attr['heading']; $story_id = fictioneer_validate_id( $attr['story_id'] ?? -1, 'fcn_story' ); $prefer_chapter_icon = get_option( 'fictioneer_override_chapter_status_icons' ); $hide_icons = get_option( 'fictioneer_hide_chapter_icons' ); $can_checkmarks = get_option( 'fictioneer_enable_checkmarks' ) && ( is_user_logged_in() || get_option( 'fictioneer_enable_ajax_authentication' ) ); $classes = wp_strip_all_tags( $attr['class'] ?? '' ); $chapter_ids = []; $chapters = []; // Extract chapter IDs (if any) if ( ! empty( $attr['chapter_ids'] ) ) { $chapter_ids = fictioneer_explode_list( $attr['chapter_ids'] ); } // Get chapters... if ( $story_id && empty( $chapter_ids ) ) { // ... via story $hide_icons = $hide_icons || get_post_meta( $story_id, 'fictioneer_story_hide_chapter_icons', true ); $story_data = fictioneer_get_story_data( $story_id, false ); // Does not refresh comment count! $chapters = $story_data['chapter_ids']; } elseif ( ! empty( $chapter_ids ) ) { // ... via chapter IDs $chapters = $chapter_ids; } // Extra classes if ( $hide_icons ) { $classes .= ' _no-icons'; } if ( get_option( 'fictioneer_collapse_groups_by_default' ) && ! str_contains( $classes, 'no-auto-collapse' ) ) { $classes .= ' _closed'; } // Apply offset and count if ( ! $group ) { $chapters = array_slice( $chapters, $offset ); $chapters = $count > 0 ? array_slice( $chapters, 0, $count ) : $chapters; } // Return empty case if... if ( empty( $chapters ) ) { return fictioneer_shortcode_chapter_list_empty( $attr ); } // Query chapters $query_args = array( 'fictioneer_query_name' => 'fictioneer_shortcode_chapter_list', 'post_type' => 'fcn_chapter', 'post_status' => 'publish', 'post__in' => $chapters, // Cannot be empty! 'ignore_sticky_posts' => true, 'orderby' => 'post__in', // Preserve order from meta box 'posts_per_page' => -1, // Get all chapters (this can be hundreds) 'no_found_rows' => true, // Improve performance 'update_post_term_cache' => false // Improve performance ); // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_chapter_list' ); if ( $transient_enabled && $cache ) { $base = serialize( $query_args ) . serialize( $attr ) . $classes; $base .= ( $hide_icons ? '1' : '0' ) . ( $can_checkmarks ? '1' : '0' ); $transient_key = "fictioneer_shortcode_chapter_list_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Query $chapter_query = fictioneer_shortcode_query( $query_args ); // Return empty case if... if ( ! $chapter_query->have_posts() ) { return fictioneer_shortcode_chapter_list_empty( $attr ); } // Buffer ob_start(); // Start HTML ---> ?>
    have_posts() ) { // Setup $chapter_query->the_post(); $chapter_id = get_the_ID(); $chapter_story_id = fictioneer_get_chapter_story_id( $chapter_id ); // Skip not visible chapters if ( get_post_meta( $chapter_id, 'fictioneer_chapter_hidden', true ) ) { continue; } // Check group (if any) if ( $group && $group != strtolower( trim( get_post_meta( $chapter_id, 'fictioneer_chapter_group', true ) ) ) ) { continue; } // Count renderings $render_count++; // Apply offset if limited to group (not working in query for 'posts_per_page' => -1) if ( $group && $offset > 0 && $render_count <= $offset ) { continue; } // Apply count if limited to group if ( $group && $count > 0 && $render_count > $count ) { break; } // Data $warning = get_post_meta( $chapter_id, 'fictioneer_chapter_warning', true ); $icon = fictioneer_get_icon_field( 'fictioneer_chapter_icon', $chapter_id ); $text_icon = get_post_meta( $chapter_id, 'fictioneer_chapter_text_icon', true ); $prefix = get_post_meta( $chapter_id, 'fictioneer_chapter_prefix', true ); $title = fictioneer_get_safe_title( $chapter_id, 'shortcode-chapter-list' ); $has_password = ! empty( $post->post_password ); $extra_classes = '_shortcode'; if ( $warning ) { $extra_classes .= ' _warning'; } if ( $has_password ) { $extra_classes .= ' _password'; } // Start HTML ---> ?>
  1. scheduled > text > normal if ( ! $prefer_chapter_icon && $has_password ) { $icon = ''; } elseif ( ! $prefer_chapter_icon && get_post_status( $chapter_id ) === 'future' ) { $icon = ''; } elseif ( $text_icon ) { $icon = "{$text_icon}"; } else { $icon = $icon ?: FICTIONEER_DEFAULT_CHAPTER_ICON; $icon = ""; } echo apply_filters( 'fictioneer_filter_chapter_icon', $icon, $chapter_id, $story_id ); } ?> $chapter_id, 'warning' => $warning, 'password' => $has_password, 'timestamp' => get_the_time( 'c' ), 'list_date' => get_the_date( '' ), 'words' => fictioneer_get_word_count( $chapter_id ), ) ); ?>
"; } if ( ! empty( $name ) ) { $fields[] = ""; } // Custom text fields for ( $i = 1; $i <= 6; $i++ ) { $field = $attr["text_{$i}"] ?? ''; if ( ! empty( $field ) ) { $fields[] = ""; } } // Custom checkboxes for ( $i = 1; $i <= 6; $i++ ) { $field = $attr["check_{$i}"] ?? ''; if ( ! empty( $field ) ) { $fields[] = ""; } } // Privacy policy checkbox $privacy_policy_link = get_option( 'wp_page_for_privacy_policy' ) ? esc_url( get_privacy_policy_url() ) : false; if ( $privacy_policy && $privacy_policy_link ) { $fields[] = ""; } // Apply filters $fields = apply_filters( 'fictioneer_filter_contact_form_fields', $fields, get_the_ID() ); // Buffer ob_start(); /* * The inline "opacity: 0" style is a reverse bot trap. They are made visible again with CSS, * but dumb bots might think this is a trap for them and ignore the fields. Not a reliable * spam protection, but this is security in depth. * * The "phone" and "terms" inputs are honeypots. */ // Start HTML ---> ?>
true ); $simple = isset( $attr['simple'] ) ? $attr['simple'] == 'true' || $attr['simple'] == '1' : false; $placeholder = $attr['placeholder'] ?? false; $type = $attr['type'] ?? false; $pre_tags = fictioneer_explode_list( $attr['tags'] ?? '' ); $pre_genres = fictioneer_explode_list( $attr['genres'] ?? '' ); $pre_fandoms = fictioneer_explode_list( $attr['fandoms'] ?? '' ); $pre_characters = fictioneer_explode_list( $attr['characters'] ?? '' ); $pre_warnings = fictioneer_explode_list( $attr['warnings'] ?? '' ); // Prepare arguments $args['expanded'] = filter_var( $attr['expanded'] ?? 0, FILTER_VALIDATE_BOOLEAN ); if ( $simple ) { $args['simple'] = $simple; } if ( $placeholder ) { $args['placeholder'] = $placeholder; } if ( $type && in_array( $type, ['any', 'story', 'chapter', 'recommendation', 'collection', 'post'] ) ) { $args['preselect_type'] = in_array( $type, ['story', 'chapter', 'recommendation', 'collection'] ) ? "fcn_{$type}" : $type; } if ( $pre_tags ) { $args['preselect_tags'] = array_map( 'absint', $pre_tags ); } if ( $pre_genres ) { $args['preselect_genres'] = array_map( 'absint', $pre_genres ); } if ( $pre_fandoms ) { $args['preselect_fandoms'] = array_map( 'absint', $pre_fandoms ); } if ( $pre_characters ) { $args['preselect_characters'] = array_map( 'absint', $pre_characters ); } if ( $pre_warnings ) { $args['preselect_warnings'] = array_map( 'absint', $pre_warnings ); } // Buffer ob_start(); get_search_form( $args ); // Return minified buffer return fictioneer_minify_html( ob_get_clean() ); } add_shortcode( 'fictioneer_search', 'fictioneer_shortcode_search' ); // ============================================================================= // BLOG SHORTCODE // ============================================================================= /** * Shortcode to show blog with pagination * * @since 5.2.0 * * @param string|null $attr['per_page'] Optional. Number of posts per page. * @param string|null $attr['ignore_sticky'] Optional. Whether to ignore sticky posts. Default false. * @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false. * @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false. * @param string|null $attr['author'] Optional. Limit posts to a specific author. * @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs. * @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs. * @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags. * @param string|null $attr['exclude_cat_ids'] Optional. Exclude posts with these categories. * @param string|null $attr['categories'] Optional. Limit posts to specific category names. * @param string|null $attr['tags'] Optional. Limit posts to specific tag names. * @param string|null $attr['rel'] Optional. Relationship between taxonomies. Default 'AND'. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_blog( $attr ) { // Defaults $args = fictioneer_get_default_shortcode_args( $attr ); // Query arguments $query_args = array( 'fictioneer_query_name' => 'blog_shortcode', 'post_type' => 'post', 'post_status' => 'publish', 'paged' => $args['page'], 'posts_per_page' => $args['posts_per_page'], 'ignore_sticky_posts' => $args['ignore_sticky'] ); // Author? if ( ! empty( $args['author'] ) ) { $query_args['author_name'] = $args['author']; } // Author IDs? if ( ! empty( $args['author_ids'] ) ) { $query_args['author__in'] = $args['author_ids']; } // Taxonomies? if ( ! empty( $args['taxonomies'] ) ) { $query_args['tax_query'] = fictioneer_get_shortcode_tax_query( $args ); } // Excluded tags? if ( ! empty( $args['excluded_tags'] ) ) { $query_args['tag__not_in'] = $args['excluded_tags']; } // Excluded categories? if ( ! empty( $args['excluded_cats'] ) ) { $query_args['category__not_in'] = $args['excluded_cats']; } // Excluded authors? if ( ! empty( $args['excluded_authors'] ) ) { $query_args['author__not_in'] = $args['excluded_authors']; } // Exclude protected if ( $args['ignore_protected'] ) { $query_args['has_password'] = false; } // Apply filters $query_args = apply_filters( 'fictioneer_filter_shortcode_blog_query_args', $query_args, $args ); // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_blog' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $query_args ) . serialize( $args ) . serialize( $attr ); $transient_key = 'fictioneer_shortcode_blog_html_' . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Query $blog_query = new WP_Query( $query_args ); // Prime author cache if ( function_exists( 'update_post_author_caches' ) ) { update_post_author_caches( $blog_query->posts ); } // Pagination $pag_args = array( 'current' => $args['page'], 'total' => $blog_query->max_num_pages, 'prev_text' => fcntr( 'previous' ), 'next_text' => fcntr( 'next' ), 'add_fragment' => '#blog' ); // Buffer ob_start(); if ( $blog_query->have_posts() ) { // Start HTML ---> ?>
have_posts() ) { $blog_query->the_post(); get_template_part( 'partials/_post', null, array( 'nested' => true, 'context' => 'shortcode_fictioneer_blog' ) ); } wp_reset_postdata(); if ( $blog_query->max_num_pages > 1 ) { echo ''; } ?>
?>
'post', 'posts' => 'post', 'page' => 'page', 'pages' => 'page', 'story' => 'fcn_story', 'stories' => 'fcn_story', 'chapter' => 'fcn_chapter', 'chapters' => 'fcn_chapter', 'collection' => 'fcn_collection', 'collections' => 'fcn_collection', 'recommendation' => 'fcn_recommendation', 'recommendations' => 'fcn_recommendation' ); // ... must be in array $query_post_types = array_map( function( $item ) use ( $allowed_post_types ) { return $allowed_post_types[ $item ] ?? null; }, $post_types); // ... remove null values $query_post_types = array_filter( $query_post_types, function( $value ) { return ! is_null( $value ); }); // ... fix array $query_post_types = array_unique( $query_post_types ); $query_post_types = array_values( $query_post_types ); $query_post_types = empty( $query_post_types ) ? ['post'] : $query_post_types; // ... add to args $args['post_type'] = $query_post_types; // Extra classes if ( $args['splide'] ?? 0 ) { $args['classes'] .= ' splide _splide-placeholder'; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_article_cards' ); if ( $transient_enabled && $args['cache'] ) { $base = serialize( $args ) . serialize( $attr ); $transient_key = "fictioneer_shortcode_article_cards_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Buffer ob_start(); get_template_part( 'partials/_article-cards', null, $args ); $html = fictioneer_minify_html( ob_get_clean() ); if ( ( $args['splide'] ?? 0 ) && strpos( $args['classes'], 'no-auto-splide' ) === false ) { $html .= fictioneer_get_splide_inline_init(); } if ( $transient_enabled && $args['cache'] ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_article_cards', 'fictioneer_shortcode_article_cards' ); // ============================================================================= // STORY SECTION SHORTCODE // ============================================================================= /** * Shortcode to show story section * * @since 5.14.0 * * @param string $attr['story_id'] The ID of the story. * @param string|null $attr['tabs'] Optional. Whether to show the tabs above chapters. Default false. * @param string|null $attr['blog'] Optional. Whether to show the blog tab. Default false. * @param string|null $attr['pages'] Optional. Whether to show the custom page tabs. Default false. * @param string|null $attr['scheduled'] Optional. Whether to show the scheduled chapter note. Default false. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_story_section( $attr ) { global $post; // Abort if... if ( ! is_page_template( 'singular-story.php' ) ) { return fictioneer_notice( __( 'The [fictioneer_story_section] shortcode requires the "Story Page" template.' ), 'warning', false ); } // Setup $cache = filter_var( $attr['cache'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $story_id = fictioneer_validate_id( $attr['story_id'] ?? 0, 'fcn_story' ); $post = get_post( $story_id ); $story_data = fictioneer_get_story_data( $story_id ); $classes = wp_strip_all_tags( $attr['class'] ?? '' ); $show_tabs = filter_var( $attr['tabs'] ?? 0, FILTER_VALIDATE_BOOLEAN ); $show_pages = filter_var( $attr['pages'] ?? 0, FILTER_VALIDATE_BOOLEAN ); $show_blog = filter_var( $attr['blog'] ?? 0, FILTER_VALIDATE_BOOLEAN ); $show_scheduled = filter_var( $attr['scheduled'] ?? 0, FILTER_VALIDATE_BOOLEAN ); $hook_args = array( 'story_id' => $story_id, 'story_data' => $story_data, 'password_required' => post_password_required() ); // Abort if... if ( ! $story_data ) { return ''; } // Prepare classes if ( ! get_option( 'fictioneer_enable_checkmarks' ) ) { $classes .= ' _no-checkmarks'; } if ( ! $show_pages || ! $show_tabs ) { $classes .= ' _no-pages'; } if ( ! $show_blog || ! $show_tabs ) { $classes .= ' _no-blog'; } // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_story_section' ); if ( $transient_enabled && $cache ) { $base = serialize( $attr ) . $classes; $transient_key = "fictioneer_shortcode_story_section_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Require functions (necessary in post editor for some reason) require_once __DIR__ . '/hooks/_story_hooks.php'; // Setup post data setup_postdata( $post ); // Buffer ob_start(); echo '
'; if ( $show_tabs ) { fictioneer_story_tabs( $hook_args ); } if ( $show_scheduled ) { fictioneer_story_scheduled_chapter( $hook_args ); } if ( $show_tabs && $show_pages ) { fictioneer_story_pages( $hook_args ); } fictioneer_story_chapters( $hook_args ); if ( $show_tabs && $show_blog ) { fictioneer_story_blog( $hook_args ); } echo '
'; // Store buffer $html = fictioneer_minify_html( ob_get_clean() ); // Reset post data wp_reset_postdata(); // Cache in Transient if ( $transient_enabled && $cache ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified buffer return $html; } add_shortcode( 'fictioneer_story_section', 'fictioneer_shortcode_story_section' ); // ============================================================================= // STORY ACTIONS SHORTCODE // ============================================================================= /** * Shortcode to show story actions * * @since 5.14.0 * * @param string $attr['story_id'] The ID of the story. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $attr['follow'] Optional. Whether to show the Follow button if enabled. Default true. * @param string|null $attr['reminder'] Optional. Whether to show the Reminder button if enabled. Default true. * @param string|null $attr['subscribe'] Optional. Whether to show the Subscribe button if enabled. Default true. * @param string|null $attr['download'] Optional. Whether to show the Download button if enabled. Default true. * @param string|null $attr['rss'] Optional. Whether to show the RSS links if enabled. Default true. * @param string|null $attr['share'] Optional. Whether to show the Share buttons if enabled. Default true. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_story_actions( $attr ) { // Abort if... if ( ! is_page_template( 'singular-story.php' ) ) { return fictioneer_notice( __( 'The [fictioneer_story_actions] shortcode requires the "Story Page" template.' ), 'warning', false ); } // Setup $cache = filter_var( $attr['cache'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $story_id = fictioneer_validate_id( $attr['story_id'] ?? 0, 'fcn_story' ); $story_data = fictioneer_get_story_data( $story_id ); $classes = wp_strip_all_tags( $attr['class'] ?? '' ); $follow = filter_var( $attr['follow'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $reminder = filter_var( $attr['reminder'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $subscribe = filter_var( $attr['subscribe'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $download = filter_var( $attr['download'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $rss = filter_var( $attr['rss'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $share = filter_var( $attr['share'] ?? 1, FILTER_VALIDATE_BOOLEAN ); // Abort if... if ( ! $story_data ) { return ''; } // Prepare hook arguments $hook_args = array( 'post_id' => $story_id, 'post_type' => 'fcn_story', 'story_id' => $story_id, 'story_data' => $story_data, 'follow' => $follow, 'reminder' => $reminder, 'subscribe' => $subscribe, 'download' => $download, 'rss' => $rss, 'share' => $share ); // Transient? $transient_enabled = fictioneer_enable_shortcode_transients( 'fictioneer_story_actions' ); if ( $transient_enabled && $cache ) { $base = serialize( $attr ) . $classes; $transient_key = "fictioneer_shortcode_story_actions_html_" . md5( $base ); $transient = get_transient( $transient_key ); if ( ! empty( $transient ) ) { return $transient; } } // Add filter for buttons add_filter( 'fictioneer_filter_story_buttons', 'fictioneer_shortcode_remove_story_buttons', 99, 2 ); // Build HTML $html = '
'; $html .= fictioneer_get_media_buttons( $hook_args ); $html .= '
' . fictioneer_get_story_buttons( $hook_args ) . '
'; // Remove filter for buttons remove_filter( 'fictioneer_shortcode_remove_story_buttons', 99, 2 ); // Minify HTML $html = fictioneer_minify_html( $html ); // Cache in Transient if ( $transient_enabled && $cache ) { set_transient( $transient_key, $html, FICTIONEER_SHORTCODE_TRANSIENT_EXPIRATION ); } // Return minified HTML return $html; } add_shortcode( 'fictioneer_story_actions', 'fictioneer_shortcode_story_actions' ); /** * Removes buttons for the [fictioneer_story_actions] shortcode * * This function only works if boolean arguments are provided for * 'follow', 'reminder', 'subscribe', and/or 'download'. * * @since 5.14.0 * * @param array $output Associative array with HTML for buttons to be rendered. * @param array $args Arguments passed to filter. * * @return array The filtered output. */ function fictioneer_shortcode_remove_story_buttons( $output, $args ) { if ( ! ( $args['follow'] ?? 1 ) ) { unset( $output['follow'] ); } if ( ! ( $args['reminder'] ?? 1 ) ) { unset( $output['reminder'] ); } if ( ! ( $args['subscribe'] ?? 1 ) ) { unset( $output['subscribe'] ); } if ( ! ( $args['download'] ?? 1 ) ) { unset( $output['epub'] ); unset( $output['ebook'] ); } return $output; }; // ============================================================================= // SUBSCRIBE BUTTON SHORTCODE // ============================================================================= /** * Shortcode to show subscribe button * * @since 5.14.0 * * @param string|null $attr['post_id'] Optional. Post ID to subscribe to (for plugins). Defaults to current ID. * @param string|null $attr['story_id'] Optional. Story ID to subscribe to (for plugins). Defaults to current ID. * @param string|null $attr['inline'] Optional. Whether the button should be wrapped in a block. Default true. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_subscribe_button( $attr ) { // Setup $post_id = absint( $attr['post_id'] ?? $attr['story_id'] ?? get_the_ID() ); $classes = wp_strip_all_tags( $attr['class'] ?? '' ); $subscribe_buttons = fictioneer_get_subscribe_options( $post_id ); if ( filter_var( $attr['inline'] ?? 0, FILTER_VALIDATE_BOOLEAN ) ) { $classes .= ' _inline'; } // Build and return button if ( ! empty( $subscribe_buttons ) ) { return sprintf( '', fcntr( 'subscribe', true ), fcntr( 'subscribe' ), $subscribe_buttons ); } // Return nothing if empty return ''; } add_shortcode( 'fictioneer_subscribe_button', 'fictioneer_shortcode_subscribe_button' ); // ============================================================================= // STORY COMMENTS SHORTCODE // ============================================================================= /** * Shortcode to show story comments * * @since 5.14.0 * * @param string|null $attr['story_id'] Optional. Story ID to get comments for. Defaults to current ID. * @param string|null $attr['header'] Optional. Whether to show the heading with count. Default true. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_story_comments( $attr ) { // Abort if... if ( ! is_page_template( 'singular-story.php' ) ) { return fictioneer_notice( __( 'The [fictioneer_story_comments] shortcode requires the "Story Page" template.' ), 'warning', false ); } // Setup $story_id = fictioneer_validate_id( $attr['story_id'] ?? get_the_ID(), 'fcn_story' ); $story_data = fictioneer_get_story_data( $story_id ?: 0 ); $header = filter_var( $attr['header'] ?? 1, FILTER_VALIDATE_BOOLEAN ); $classes = wp_strip_all_tags( $attr['classes'] ?? $attr['class'] ?? '' ); $style = esc_attr( wp_strip_all_tags( $attr['style'] ?? '' ) ); if ( ! $story_data ) { return ''; } // Require functions (necessary in post editor for some reason) require_once __DIR__ . '/hooks/_story_hooks.php'; // Buffer ob_start(); // Output story comment section fictioneer_story_comments( array( 'story_id' => $story_id, 'story_data' => $story_data, 'shortcode' => 1, 'classes' => $classes, 'header' => $header, 'style' => $style ) ); // Capture and return buffer return fictioneer_minify_html( ob_get_clean() ); } add_shortcode( 'fictioneer_story_comments', 'fictioneer_shortcode_story_comments' ); // ============================================================================= // STORY DATA SHORTCODE // ============================================================================= /** * Shortcode to show selected story data * * Data: id, word_count, chapter_count, icon, age_rating, rating_letter, comment_count, * datetime, date, time, categories, tags, genres, fandoms, characters, or warnings. * * Format: default, raw, or short. Not all of them apply to all data. * * @since 5.14.0 * * @param string|null $attr['data'] The requested data, one at a time. See description. * @param string|null $attr['story_id'] Optional. Story ID to get comments for. Defaults to current ID. * @param string|null $attr['format'] Optional. Special formatting for selected data. See description. * @param string|null $attr['date_format'] Optional. Format for date string. Defaults to WP settings. * @param string|null $attr['time_format'] Optional. Format for time string. Defaults to WP settings. * @param string|null $attr['separator'] Optional. Separator string for tags, genres, fandoms, etc. * @param string|null $attr['tag'] Optional. The wrapper HTML tag. Defaults to 'span'. * @param string|null $attr['class'] Optional. Additional CSS classes, separated by whitespace. * @param string|null $attr['inner_class'] Optional. Additional CSS classes for nested items, separated by whitespace. * @param string|null $attr['style'] Optional. Inline style applied to wrapper element. * @param string|null $attr['inner_style'] Optional. Inline style applied to nested items. * * @return string The shortcode HTML. */ function fictioneer_shortcode_story_data( $attr ) { // Setup $story_id = fictioneer_validate_id( $attr['story_id'] ?? get_the_ID(), 'fcn_story' ); $story_data = fictioneer_get_story_data( $story_id ?: 0 ); $data = $attr['data'] ?? ''; $format = $attr['format'] ?? ''; $separator = wp_strip_all_tags( $attr['separator'] ?? '' ) ?: ', '; $classes = esc_attr( wp_strip_all_tags( $attr['class'] ?? '' ) ); $inner_classes = esc_attr( wp_strip_all_tags( $attr['inner_class'] ?? '' ) ); $style = esc_attr( wp_strip_all_tags( $attr['style'] ?? '' ) ); $inner_style = esc_attr( wp_strip_all_tags( $attr['inner_style'] ?? '' ) ); $tag = wp_strip_all_tags( $attr['tag'] ?? 'span' ); $output = ''; if ( ! $story_data ) { return ''; } // Get requested data switch ( $data ) { case 'word_count': $output = $story_data['word_count'] ?? 0; $output = $format !== 'short' ? $output : fictioneer_shorten_number( $output ); $output = ( $format === 'raw' || $format === 'short' ) ? $output : number_format_i18n( $output ); break; case 'chapter_count': $output = $story_data['chapter_count'] ?? 0; $output = $format === 'raw' ? $output : number_format_i18n( $output ); break; case 'status': $output = $story_data['status'] ?? ''; break; case 'icon': $output = esc_attr( $story_data['icon'] ?? '' ); $output = $output ? "" : ''; break; case 'age_rating': $output = $story_data['rating'] ?? ''; break; case 'rating_letter': $output = $story_data['rating_letter'] ?? ''; break; case 'comment_count': $output = $story_data['comment_count'] ?? 0; $output = $format === 'raw' ? $output : number_format_i18n( $output ); break; case 'id': $output = $story_data['id'] ?? ''; break; case 'datetime': $date = get_the_date( $attr['date_format'] ?? '', $story_id ); $time = get_the_time( $attr['time_format'] ?? '', $story_id ); $output = sprintf( $format ? $format : '%s at %s', $date, $time ); break; case 'date': $output = get_the_date( $attr['date_format'] ?? '', $story_id ); break; case 'time': $output = get_the_time( $attr['time_format'] ?? '', $story_id ); break; } // Get requested terms if ( in_array( $data, ['categories', 'tags', 'genres', 'fandoms', 'characters', 'warnings'] ) ) { $terms = $data === 'categories' ? get_the_category( $story_id ) : ( $story_data[ $data ] ?? [] ); $terms = is_array( $terms ) ? $terms : []; $output = []; foreach ( $terms as $term ) { $link = get_tag_link( $term ); $output[] = "{$term->name}"; } $output = implode( $separator, $output ); } // Build and return output return $output ? "<{$tag} class='story-data {$classes}' style='{$style}'>{$output}" : ''; } add_shortcode( 'fictioneer_story_data', 'fictioneer_shortcode_story_data' ); // ============================================================================= // FONT AWESOME SHORTCODE // ============================================================================= /** * Shortcode to show a Font Awesome icon * * @since 5.14.0 * * @param string|null $attr['class'] The icon classes, separated by whitespace. * * @return string The shortcode HTML. */ function fictioneer_shortcode_font_awesome( $attr ) { // Setup $classes = esc_attr( trim( wp_strip_all_tags( $attr['class'] ?? '' ) ) ); if ( empty( $classes ) ) { return ''; } // Build and return output return ""; } add_shortcode( 'fictioneer_fa', 'fictioneer_shortcode_font_awesome' ); // ============================================================================= // SIDEBAR SHORTCODE // ============================================================================= /** * Shortcode to show sidebar * * @since 5.20.0 * * @param string|null $attr['name'] Optional. Name of the sidebar. Default 'fictioneer_sidebar'. * * @return string The captured shortcode HTML. */ function fictioneer_shortcode_sidebar( $attr ) { // Setup $name = sanitize_text_field( $attr['name'] ?? '' ) ?: 'fictioneer-sidebar'; // Buffer ob_start(); // Does sidebar exist? if ( ! is_active_sidebar( $name ) ) { return; } // Remove filters remove_filter( 'excerpt_more', 'fictioneer_excerpt_ellipsis' ); remove_filter( 'excerpt_length', 'fictioneer_custom_excerpt_length' ); // Start HTML ---> ?>
'', 'content' => '', 'footnote' => true, ); $atts = shortcode_atts( $default_atts, $atts, 'fcnt' ); $footnote_allowed = get_option( 'fictioneer_generate_footnotes_from_tooltips' ) && $atts['footnote']; $footnote_link = ''; // Sanitize user inputs $tooltip_header = trim( wp_kses_post( $atts[ 'header' ] ) ); $tooltip_content = trim( wp_kses_post( $atts[ 'content' ] ) ); // Bail if no content if ( empty( $tooltip_content ) ) { return $content; } // Increment counter $tooltip_id_counter++; // Prepare footnote if allowed if ( $footnote_allowed ) { // Create a footnote link to be appended to the tooltip content $footnote_link = sprintf( '%1$d', $tooltip_id_counter ); } // Prepare data attributes for the tooltip $tooltip_data = array( 'dialog-header' => $tooltip_header, 'dialog-content' => $tooltip_content . $footnote_link, ); // Convert data array to HTML attributes $tooltip_data_attributes = array_map( function ( $key, $value ) { return sprintf( 'data-%s="%s"', esc_attr( $key ), esc_attr( $value ) ); }, array_keys( $tooltip_data ), $tooltip_data ); $tooltip_title = _x( 'Click to see note', 'Tooltip shortcode.', 'fictioneer' ); // Construct the HTML for the tooltip $html = sprintf( '%4$s%5$s', $tooltip_id_counter, esc_attr( $tooltip_title ), implode( ' ', $tooltip_data_attributes ), $content, $footnote_link ); // Collect footnote if allowed if ( $footnote_allowed ) { do_action( 'fictioneer_collect_footnote', $tooltip_id_counter, $tooltip_content ); } return $html; } add_shortcode( 'fcnt', 'fictioneer_shortcode_tooltip' ); // ============================================================================= // TERMS SHORTCODE // ============================================================================= /** * Shortcode to show taxonomies. * * @since 5.27.2 * * @param string|null $attr['term_type'] Term type. Default 'post_tag'. * @param int|null $attr['post_id'] Post ID. Default 0. * @param int|null $attr['count'] Maximum number of terms. Default -1 (all). * @param string|null $attr['orderby'] Orderby argument. Default 'count'. * @param string|null $attr['order'] Order argument. Default 'DESC'. * @param bool|null $attr['show_empty'] Whether to show empty terms. Default false. * @param bool|null $attr['show_count'] Whether to show term counts. Default false. * @param string|null $attr['classes'] Additional section CSS classes. Default empty. * @param string|null $attr['inner_classes'] Additional term CSS classes. Default empty. * @param string|null $attr['style'] Inline section CSS style. Default empty. * @param string|null $attr['inner_style'] Inline term CSS style. Default empty. * @param string|null $attr['empty'] Override message for empty query result. * * @return string The shortcode HTML. */ function fictioneer_shortcode_terms( $attr ) { // Setup $type = sanitize_key( $attr['type'] ?? 'tag' ); $post_id = absint( $attr['post_id'] ?? 0 ); $count = max( -1, intval( $attr['count'] ?? -1 ) ); $orderby = sanitize_text_field( $attr['orderby'] ?? 'count' ); $order = sanitize_text_field( $attr['order'] ?? 'DESC' ); $show_empty = filter_var( $attr['show_empty'] ?? 0, FILTER_VALIDATE_BOOLEAN ); $show_count = filter_var( $attr['show_count'] ?? 0, FILTER_VALIDATE_BOOLEAN ); $classes = esc_attr( wp_strip_all_tags( $attr['classes'] ?? $attr['class'] ?? '' ) ); $inner_classes = esc_attr( wp_strip_all_tags( $attr['inner_classes'] ?? $attr['inner_class'] ?? '' ) ); $style = esc_attr( wp_strip_all_tags( $attr['style'] ?? '' ) ); $inner_style = esc_attr( wp_strip_all_tags( $attr['inner_style'] ?? '' ) ); $empty = sanitize_text_field( $attr['empty'] ?? __( 'No taxonomies specified yet.', 'fictioneer' ) ); $term_map = array( 'tag' => 'post_tag', 'genre' => 'fcn_genre', 'fandom' => 'fcn_fandom', 'character' => 'fcn_character', 'warning' => 'fcn_content_warning' ); $term_type = $term_map[ $type ] ?? $type; // Term exists? if ( ! taxonomy_exists( $term_type ) ) { return 'Error: Taxonomy does not exist.'; } // Post exists? if ( $post_id && ! get_post( $post_id ) ) { return 'Error: Post not found.'; } // Prepare query args $args = array( 'fictioneer_query_name' => 'terms_shortcode', 'taxonomy' => $term_type, 'orderby' => $orderby, 'order' => $order, 'hide_empty' => ! $show_empty ); if ( $count > 0 ) { $args['number'] = $count; } // Apply filters $args = apply_filters( 'fictioneer_filter_shortcode_terms_query_args', $args, $attr ); // Query terms if ( $post_id ) { $terms = wp_get_post_terms( $post_id, $term_type, $args ); } else { $terms = get_terms( $args ); } // All good? if ( is_wp_error( $terms ) ) { return 'Error: ' . $terms->get_error_message(); } // Build and return HTML $html = ''; if ( ! empty( $terms ) ) { foreach ( $terms as $term ) { $html .= sprintf( '%s', get_tag_link( $term ), str_replace( 'fcn_', '', $term->taxonomy ), $term->slug, 'tag-pill ' . $inner_classes, $inner_style, $show_count ? sprintf( _x( '%1$s (%2$s)', 'Terms shortcode with count.', 'fictioneer' ), $term->name, $term->count ) : $term->name ); } } else { $html = $empty; } return sprintf( '
%s
', "_{$term_type} {$classes} " . wp_unique_id( 'shortcode-id-' ), $style, $html ); } add_shortcode( 'fictioneer_terms', 'fictioneer_shortcode_terms' );