fictioneer/repo/graveyard/discarded.php
2024-12-06 17:38:32 +01:00

818 lines
26 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* This is a discard pile for functions and snippets that are no longer in use
* but may prove useful in the future (or were removed by mistake). Keeping them
* in a searchable file is easier that sorting through my poorly written commits.
*/
// =============================================================================
// SHOW PATREON TIERS
// =============================================================================
/**
* Adds HTML for the Patreon tiers section to the wp-admin user profile
*
* DISABLED!
*
* @since 5.0.0
*
* @param WP_User $profile_user The profile user object. Not necessarily the one
* currently editing the profile!
*/
function fictioneer_admin_profile_patreon_tiers( $profile_user ) {
// Setup
$editing_user_is_admin = fictioneer_is_admin( get_current_user_id() );
// Abort conditions...
if ( ! $editing_user_is_admin ) return;
// Start HTML ---> ?>
<tr class="user-patreon-tiers-wrap">
<th><label for="fictioneer_patreon_tiers"><?php _e( 'Patreon Tiers', 'fictioneer' ); ?></label></th>
<td>
<input name="fictioneer_patreon_tiers" type="text" id="fictioneer_patreon_tiers" value="<?php echo esc_attr( json_encode( get_the_author_meta( 'fictioneer_patreon_tiers', $profile_user->ID ) ) ); ?>" class="regular-text" readonly>
<p class="description"><?php _e( 'Patreon tiers for the linked Patreon client. Valid for two weeks after last login.', 'fictioneer' ); ?></p>
</td>
</tr>
<?php // <--- End HTML
}
// add_action( 'fictioneer_admin_user_sections', 'fictioneer_admin_profile_patreon_tiers', 60 );
// =============================================================================
// SHOW BOOKMARKS SECTION
// =============================================================================
/**
* Adds HTML for the bookmarks section to the wp-admin user profile
*
* DISABLED!
*
* @since 5.0.0
*
* @param WP_User $profile_user The profile user object. Not necessarily the one
* currently editing the profile!
*/
function fictioneer_admin_profile_bookmarks( $profile_user ) {
// Setup
$editing_user_is_admin = fictioneer_is_admin( get_current_user_id() );
// Abort conditions...
if ( ! $editing_user_is_admin ) return;
// Start HTML ---> ?>
<tr class="user-bookmarks-wrap">
<th><label for="user_bookmarks"><?php _e( 'Bookmarks JSON', 'fictioneer' ); ?></label></th>
<td>
<textarea name="user_bookmarks" id="user_bookmarks" rows="12" cols="30" style="font-family: monospace; font-size: 12px; word-break: break-all;" disabled><?php echo esc_attr( get_the_author_meta( 'fictioneer_bookmarks', $profile_user->ID ) ); ?></textarea>
</td>
</tr>
<?php // <--- End HTML
}
// add_action( 'fictioneer_admin_user_sections', 'fictioneer_admin_profile_bookmarks', 70 );
/**
* Restrict access to admin pages/actions for moderators
*
* Prevent moderators from editing the comment content, which is rarely a good action.
* Doing this via JS is a terrible solution but better than nothing.
*
* @since 5.0.0
* @todo Do this server-side to prevent savvy moderators from circumventing it.
*/
function fictioneer_disable_moderator_comment_edit() {
wp_add_inline_script(
'fictioneer-admin-script',
"jQuery(function($) {
$('#qt_content_toolbar').remove();
$('.wp-editor-area').prop('disabled', true);
$('.wp-editor-area').prop('name', '');
});"
);
}
// =============================================================================
// REQUEST COMMENT FORM - AJAX (DOES NOT WORK WITH CURRENT USER!)
// > Issue: The current user requires a specific nonce to be present, which will
// > cause a lot of headache with caching plugins.
// =============================================================================
/**
* Register REST API endpoint to fetch comment form
*
* Endpoint URL: /wp-json/fictioneer/v1/get_comment_form
*
* Expected Parameters:
* - post_id (required): The ID of the story post.
*
* @since 5.6.3
*/
function fictioneer_register_endpoint_get_comment_form() {
register_rest_route(
'fictioneer/v1',
'/get_comment_form',
array(
'methods' => 'GET',
'callback' => 'fictioneer_rest_get_comment_form',
'args' => array(
'post_id' => array(
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
'required' => true
)
),
'permission_callback' => '__return_true' // Public
)
);
}
if ( get_option( 'fictioneer_enable_ajax_comment_form' ) ) {
add_action( 'rest_api_init', 'fictioneer_register_endpoint_get_comment_form' );
}
/**
* Sends the comment form HTML
*
* @since 5.0.0
* @since 5.6.3 Refactored for REST API.
*
* @param WP_REST_Request $WP_REST_Request Request object.
*
* @return WP_REST_Response|WP_Error Response or error.
*/
function fictioneer_rest_get_comment_form( WP_REST_Request $request ) {
// Setup
$post_id = $request->get_param( 'post_id' );
$must_login = get_option( 'comment_registration' ) && ! is_user_logged_in();
// Get buffered form
ob_start();
if ( get_option( 'fictioneer_disable_comment_form' ) ) {
comment_form( [], $post_id );
} else {
comment_form( fictioneer_comment_form_args( [], $post_id ), $post_id );
}
// Get buffer
$output = ob_get_clean();
// Response body (compatible to wp_send_json_success())
$data = array( 'html' => $output, 'postId' => $post_id, 'mustLogin' => $must_login );
// Return buffer
return rest_ensure_response( array( 'data' => $data, 'success' => true ) );
}
// =============================================================================
// REQUEST COMMENT SECTION - REST (DOES NOT WORK WITH CURRENT USER!)
// > Issue: The current user requires a specific nonce to be present, which will
// > cause a lot of headache with caching plugins.
// =============================================================================
/**
* Register REST API endpoint to fetch comment section
*
* Endpoint URL: /wp-json/fictioneer/v1/get_comment_section
*
* Expected Parameters:
* - post_id (required): The ID of the story post.
*
* @since 5.6.3
*/
function fictioneer_register_endpoint_get_comment_section() {
register_rest_route(
'fictioneer/v1',
'/get_comment_section',
array(
'methods' => 'GET',
'callback' => 'fictioneer_rest_get_comment_section',
'args' => array(
'post_id' => array(
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
'required' => true
),
'page' => array(
'validate_callback' => function( $param, $request, $key ) {
return is_numeric( $param );
},
'sanitize_callback' => 'absint',
'default' => 1
),
'commentcode' => array(
'validate_callback' => function( $param, $request, $key ) {
return empty( $param ) || preg_match( '/^[a-zA-Z0-9]{13,23}$/', $param ) === 1;
},
'sanitize_callback' => 'sanitize_text_field',
'default' => ''
)
),
'permission_callback' => '__return_true' // Public
)
);
}
if ( get_option( 'fictioneer_enable_ajax_comments' ) ) {
add_action( 'rest_api_init', 'fictioneer_register_endpoint_get_comment_section' );
}
/**
* Sends the comment section HTML
*
* @since 5.0.0
* @since 5.6.3 Refactored for REST API.
*
* @param WP_REST_Request $WP_REST_Request Request object.
*
* @return WP_REST_Response|WP_Error Response or error.
*/
function fictioneer_rest_get_comment_section( WP_REST_Request $request ) {
// Setup
$post_id = $request->get_param( 'post_id' );
$page = $request->get_param( 'page' );
$commentcode = $request->get_param( 'commentcode' ) ?: false;
$post = get_post( $post_id );
$must_login = get_option( 'comment_registration' ) && ! is_user_logged_in();
$default_error = array( 'error' => __( 'Comments could not be loaded.', 'fictioneer' ) );
// Abort if post not found
if ( empty( $post ) ) {
if ( WP_DEBUG ) {
$data = array( 'error' => __( 'Provided post ID does not match any post.', 'fictioneer' ) );
} else {
$data = $default_error;
}
return rest_ensure_response( array( 'data' => $data, 'success' => false ), 400 );
}
// Abort if password required
if ( post_password_required( $post ) ) {
$data = array( 'error' => __( 'Password required.', 'fictioneer' ) );
return rest_ensure_response( array( 'data' => $data, 'success' => false ), 403 );
}
// Abort if comments are closed
if ( ! comments_open( $post ) ) {
$data = array( 'error' => __( 'Commenting is disabled.', 'fictioneer' ) );
return rest_ensure_response( array( 'data' => $data, 'success' => false ), 403 );
}
// Query arguments
$query_args = array( 'post_id' => $post_id );
if ( ! get_option( 'fictioneer_disable_comment_query' ) ) {
$query_args['type'] = ['comment', 'private'];
$query_args['order'] = get_option( 'comment_order' );
} else {
// Still hide private comments but do not limit the types preemptively
$query_args = array( 'type__not_in' => 'private' );
}
// Filter query arguments
$query_args = apply_filters( 'fictioneer_filter_comments_query', $query_args, $post_id );
// Query comments
$comments_query = new WP_Comment_Query( $query_args );
$comments = $comments_query->comments;
// Filter comments
$comments = apply_filters( 'fictioneer_filter_comments', $comments, $post_id );
// Pagination
$max_pages = get_comment_pages_count( $comments );
$page = min( $max_pages, $page );
$page = max( 1, $page );
// Start buffer
ob_start();
// Header
fictioneer_comment_header( get_comments_number( $post_id ) );
// Form
if ( ! fictioneer_is_commenting_disabled( $post_id ) ) {
if ( get_option( 'fictioneer_disable_comment_form' ) ) {
comment_form( [], $post_id );
} else {
comment_form( fictioneer_comment_form_args( [], $post_id ), $post_id );
}
}
// List
fictioneer_ajax_list_comments(
$comments,
$page,
array(
'commentcode' => $commentcode,
'post_author_id' => $post->post_author,
'post_id' => $post_id
)
);
// Navigation
if ( $max_pages > 1 ) {
// Start HTML ---> ?>
<nav class="pagination comments-pagination _padding-top">
<?php
$steps = fictioneer_balance_pagination_array( $max_pages, $page );
foreach ( $steps as $step ) {
switch ( $step ) {
case $page:
?><span class="page-numbers current" aria-current="page"><?php echo $step; ?></span><?php
break;
case '…':
?><button type="button" class="page-numbers dots" data-page-jump><?php echo $step; ?></button><?php
break;
default:
?><button type="button" class="page-numbers" data-page="<?php echo $step; ?>"><?php echo $step; ?></button><?php
}
}
?>
</nav>
<?php // <--- End HTML
}
// Get buffer
$output = ob_get_clean();
// Response body (compatible to wp_send_json_success())
$data = array( 'html' => $output, 'postId' => $post_id, 'page' => $page, 'mustLogin' => $must_login );
// Return buffer
return rest_ensure_response( array( 'data' => $data, 'success' => true ) );
}
// =============================================================================
// VALIDATE & BALANCE BBCODES
// > Issue: Just not working, too stupid to make it work lol
// > except for the finding functions, they did their job
// =============================================================================
function find_back_closing_tag( $content, $tag ) {
$pattern = '/\[' . preg_quote( $tag, '/' ) . '[^\]]*\]/';
if ( preg_match_all( $pattern, $content, $matches, PREG_OFFSET_CAPTURE ) ) {
$last = end( $matches[1] );
$offset = $last[1];
return [$tag, $offset];
}
return false;
}
function find_back_opening_tag( $content, $tag, $closingOffset ) {
$pattern = '/\[' . preg_quote( $tag, '/' ) . '[^\]]*\]/';
$sub_content = substr( $content, 0, $closingOffset );
if ( preg_match_all( $pattern, $sub_content, $matches, PREG_OFFSET_CAPTURE )) {
$last = end( $matches[0] );
$tag = $last[0];
$offset = $last[1];
$tag_length = strlen( $tag );
$inner_offset = $offset + $tag_length;
$inner_content = substr( $sub_content, $inner_offset, strlen( $sub_content ) - $offset - $tag_length - 2 );
return [$tag, $offset, $inner_content, $inner_offset ];
}
return false;
}
function validate_tags($content) {
$stack = [];
$validTags = ['b', 'i', 's', 'img']; // List of valid tags
$maybeValidStack = []; // A secondary stack to track "maybe valid" tags
preg_match_all('/\[(\/?)([a-zA-Z0-9]+)([^\]]*)\]/', $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
foreach ($matches as $match) {
$tag = $match[2][0];
$offset = $match[0][1];
$fullTag = $match[0][0];
if (!in_array($tag, $validTags)) {
continue; // Skip invalid tags
}
$dummyString = str_repeat("#", strlen($fullTag)); // Dynamic-length dummy string
if ($match[1][0] === '/') {
// It's a closing tag
if (!empty($stack) && end($stack)['tag'] === $tag) {
array_pop($stack); // Remove the last element from the stack
if (!empty($maybeValidStack)) {
array_pop($maybeValidStack); // Remove the corresponding "maybe valid" element
}
} else {
// Remove this closing tag
$content = substr_replace($content, $dummyString, $offset, strlen($fullTag));
// Remove all "maybe valid" tags from the content and clear the secondary stack
foreach ($maybeValidStack as $unmatched) {
$dummyStringForUnmatched = str_repeat("#", strlen($unmatched['fullTag']));
$content = substr_replace($content, $dummyStringForUnmatched, $unmatched['offset'], strlen($unmatched['fullTag']));
}
$maybeValidStack = [];
}
} else {
// It's an opening tag
$stack[] = ['tag' => $tag, 'offset' => $offset, 'fullTag' => $fullTag];
$maybeValidStack[] = ['tag' => $tag, 'offset' => $offset, 'fullTag' => $fullTag];
}
}
// Remove any unmatched opening tags
foreach (array_reverse($stack) as $unmatched) {
$dummyStringForUnmatched = str_repeat("#", strlen($unmatched['fullTag']));
$content = substr_replace($content, $dummyStringForUnmatched, $unmatched['offset'], strlen($unmatched['fullTag']));
}
return $content;
}
// =============================================================================
// GET CHECKMARKS - AJAX
// =============================================================================
/**
* Sends the user's Checkmarks as JSON via Ajax
*
* @since 4.0.0
* @see fictioneer_get_validated_ajax_user()
* @see fictioneer_load_checkmarks()
*/
function fictioneer_ajax_get_checkmarks() {
// Setup and validations
$user = fictioneer_get_validated_ajax_user();
if ( ! $user ) {
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
}
// Prepare Checkmarks
$checkmarks = fictioneer_load_checkmarks( $user );
$checkmarks['timestamp'] = time() * 1000; // Compatible with Date.now() in JavaScript
// Response
wp_send_json_success( array( 'checkmarks' => json_encode( $checkmarks ) ) );
}
if ( get_option( 'fictioneer_enable_checkmarks' ) ) {
add_action( 'wp_ajax_fictioneer_ajax_get_checkmarks', 'fictioneer_ajax_get_checkmarks' );
}
// =============================================================================
// GET REMINDERS - AJAX
// =============================================================================
/**
* Sends the user's Reminders as JSON via Ajax
*
* @since 5.0.0
* @see fictioneer_get_validated_ajax_user()
* @see fictioneer_load_reminders()
*/
function fictioneer_ajax_get_reminders() {
// Setup and validations
$user = fictioneer_get_validated_ajax_user();
if ( ! $user ) {
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
}
// Prepare Reminders
$reminders = fictioneer_load_reminders( $user );
$reminders['timestamp'] = time() * 1000; // Compatible with Date.now() in JavaScript
// Response
wp_send_json_success( array( 'reminders' => json_encode( $reminders ) ) );
}
if ( get_option( 'fictioneer_enable_reminders' ) ) {
add_action( 'wp_ajax_fictioneer_ajax_get_reminders', 'fictioneer_ajax_get_reminders' );
}
// =============================================================================
// GET FOLLOWS - AJAX
// =============================================================================
/**
* Sends the user's Follows as JSON via Ajax
*
* @since 4.3.0
* @see fictioneer_get_validated_ajax_user()
*/
function fictioneer_ajax_get_follows() {
// Setup and validations
$user = fictioneer_get_validated_ajax_user();
if ( ! $user ) {
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
}
// Prepare Follows
$user_follows = fictioneer_load_follows( $user );
$user_follows['timestamp'] = time() * 1000; // Compatible with Date.now() in JavaScript
$latest = 0;
$new = false;
// New notifications?
if ( count( $user_follows['data'] ) > 0 ) {
$latest = fictioneer_query_followed_chapters(
array_keys( $user_follows['data'] ),
wp_date( 'c', $user_follows['seen'] / 1000 )
);
if ( $latest ) {
$new = count( $latest );
}
}
$user_follows['new'] = $new;
// Response
wp_send_json_success( array( 'follows' => json_encode( $user_follows ) ) );
}
if ( get_option( 'fictioneer_enable_follows' ) ) {
add_action( 'wp_ajax_fictioneer_ajax_get_follows', 'fictioneer_ajax_get_follows' );
}
// =============================================================================
// FETCH FINGERPRINT - AJAX
// =============================================================================
/**
* Sends the user's fingerprint via AJAX
*
* @since 5.0.0
* @see fictioneer_get_validated_ajax_user()
*/
function fictioneer_ajax_get_fingerprint() {
// Setup and validations
$user = fictioneer_get_validated_ajax_user();
if ( ! $user ) {
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
}
// Response
wp_send_json_success( array( 'fingerprint' => fictioneer_get_user_fingerprint( $user->ID ) ) );
}
add_action( 'wp_ajax_fictioneer_ajax_get_fingerprint', 'fictioneer_ajax_get_fingerprint' );
// =============================================================================
// GET BOOKMARKS FOR USERS - AJAX
// =============================================================================
/**
* Get an user's bookmarks via AJAX
*
* @since 4.0.0
* @see fictioneer_get_validated_ajax_user()
*/
function fictioneer_ajax_get_bookmarks() {
// Enabled?
if ( ! get_option( 'fictioneer_enable_bookmarks' ) ) {
wp_send_json_error( null, 403 );
}
// Setup and validations
$user = fictioneer_get_validated_ajax_user();
if ( ! $user ) {
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
}
// Look for saved bookmarks on user...
$bookmarks = get_user_meta( $user->ID, 'fictioneer_bookmarks', true );
$bookmarks = $bookmarks ? $bookmarks : '{}';
// Response
if ( $bookmarks ) {
wp_send_json_success( array( 'bookmarks' => $bookmarks ) );
}
}
if ( get_option( 'fictioneer_enable_bookmarks' ) ) {
add_action( 'wp_ajax_fictioneer_ajax_get_bookmarks', 'fictioneer_ajax_get_bookmarks' );
}
// =============================================================================
// CHECK USER CAPABILITIES
// =============================================================================
if ( ! function_exists( 'fictioneer_has_role' ) ) {
/**
* Checks if an user has a specific role
*
* @since 5.0.0
*
* @param WP_User|int $user The user object or ID to check.
* @param string $role The role to check for.
*
* @return boolean To be or not to be.
*/
function fictioneer_has_role( $user, $role ) {
// Setup
$user = is_int( $user ) ? get_user_by( 'ID', $user ) : $user;
// Abort conditions
if ( ! $user || ! $role ) {
return false;
}
// Check if user has role...
if ( in_array( $role, (array) $user->roles ) ) {
return true;
}
// Else...
return false;
}
}
// =============================================================================
// CLEANUP
// =============================================================================
/**
* When a fictioneer option is updated as empty, the option is deleted
*
* @since 5.7.5
*
* @param string $option The option name.
* @param mixed $old_value The previous value.
* @param mixed $value The new value.
*/
function fictioneer_delete_null_option( $option, $old_value, $value ) {
// Theme option?
if (
! array_key_exists( $option, FICTIONEER_OPTIONS['booleans'] ) &&
! array_key_exists( $option, FICTIONEER_OPTIONS['strings'] )
) {
return;
}
// Delete empty options
if ( empty( $value ) ) {
delete_option( $option );
}
}
// add_action( 'updated_option', 'fictioneer_delete_null_option', 20, 3 );
// =============================================================================
// FILTER SEARCH QUERY
// =============================================================================
/**
* Remove unlisted chapters and stories from search results
*
* @since 5.2.4
*
* @param WP_Query $query The query.
*/
function fictioneer_remove_unlisted_from_search( $query ) {
// Only for search queries on the frontend...
if ( ! is_admin() && $query->is_main_query() && $query->is_search ) {
$query->set(
'meta_query',
array(
'relation' => 'AND',
array(
'relation' => 'OR',
array(
'key' => 'fictioneer_chapter_hidden',
'compare' => 'NOT EXISTS'
),
array(
'key' => 'fictioneer_chapter_hidden',
'value' => '0'
)
),
array(
'relation' => 'OR',
array(
'key' => 'fictioneer_story_hidden',
'compare' => 'NOT EXISTS'
),
array(
'key' => 'fictioneer_story_hidden',
'value' => '0'
)
)
)
);
}
}
add_action( 'pre_get_posts', 'fictioneer_remove_unlisted_from_search', 10 );
// =============================================================================
// DEFER CSS
// =============================================================================
/**
* Defer selected enqueued CSS file
*
* @since 5.12.2
*
* @param string $html The link tag for the enqueued style.
* @param string $handle The styles registered handle.
* @param string $href The stylesheets source URL.
*
* @return string The updated or unchanged link tag.
*/
function fictioneer_defer_css( $html, $handle, $href ) {
if ( $handle === 'fictioneer-deferred-application' && ! is_customize_preview() ) {
$loading_pattern = fictioneer_get_async_css_loading_pattern();
$original = $html;
$html = "<link rel='stylesheet' id='{$handle}-css' href='{$href}' {$loading_pattern}>";
$html .= "<noscript>{$original}</noscript>";
}
return $html;
}
// if ( FICTIONEER_ENABLE_ASYNC_ONLOAD_PATTERN ) {
// add_filter( 'style_loader_tag', 'fictioneer_defer_css', 10, 3 );
// }
// =============================================================================
// SQL TO GET STORY WORD COUNT AND CHAPTERS
// =============================================================================
if ( ! function_exists( 'fictioneer_get_story_word_count' ) ) {
function fictioneer_get_story_word_and_chapter_count( $post_id, $chapter_ids = null ) {
global $wpdb;
// Prepare
$chapter_ids = $chapter_ids ?? fictioneer_get_story_chapter_ids();
$chapter_ids[] = $post_id; // Also count story post words
$placeholders = implode( ',', array_fill( 0, count( $chapter_ids ), '%d' ) );
// SQL Query
$query = $wpdb->prepare(
"
SELECT
SUM(meta_word_count.meta_value) AS total_word_count,
COUNT(posts.ID) AS matched_posts_count
FROM {$wpdb->posts} AS posts
INNER JOIN {$wpdb->postmeta} AS meta_word_count
ON (posts.ID = meta_word_count.post_id AND meta_word_count.meta_key = '_word_count')
LEFT JOIN {$wpdb->postmeta} AS meta_hidden
ON (posts.ID = meta_hidden.post_id AND meta_hidden.meta_key = 'fictioneer_chapter_hidden')
LEFT JOIN {$wpdb->postmeta} AS meta_no_chapter
ON (posts.ID = meta_no_chapter.post_id AND meta_no_chapter.meta_key = 'fictioneer_chapter_no_chapter')
WHERE (posts.post_type = 'fcn_chapter' OR posts.post_type = 'fcn_story')
AND posts.ID IN ($placeholders)
AND posts.post_status = 'publish'
AND (meta_hidden.meta_value IS NULL OR meta_hidden.meta_value = '')
AND (meta_no_chapter.meta_value IS NULL OR meta_no_chapter.meta_value = '')
",
...$chapter_ids
);
// Execute
$results = $wpdb->get_row( $query );
// Get values and apply filters
$words = $results->total_word_count;
$chapter_count = $results->matched_posts_count - 1; // Remove story post
$multiplier = floatval( get_option( 'fictioneer_word_count_multiplier', 1.0 ) );
if ( $multiplier !== 1.0 ) {
$words = intval( $words * $multiplier );
}
// Return results as array
return array(
'word_count' => $words > 0 ? $words : 0,
'chapter_count' => $chapter_count
);
}
}