* @since 3.5.0 */ class PostContentUtils { public const DATE_FORMAT = 'Y-m-d\TH:i:s\Z'; /** * Get the content "body" param for the audio, ready to be sent to the * BeyondWords API. * * From API version 1.1 the "summary" param is going to be used differently, * so for WordPress we now prepend the WordPress excerpt to the "body" param. * * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.6.0 * * @return string The content body param. */ public static function getContentBody(int|\WP_Post $post): string|null { $post = get_post($post); if (!($post instanceof \WP_Post)) { throw new \Exception(esc_html__('Post Not Found', 'speechkit')); } $summary = PostContentUtils::getPostSummary($post); $body = PostContentUtils::getPostBody($post); if ($summary) { $format = PostContentUtils::getPostSummaryWrapperFormat($post); $body = sprintf($format, $summary) . $body; } return $body; } /** * Get the post body for the audio content. * * @since 3.0.0 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils * @since 3.8.0 Exclude Gutenberg blocks with attribute { beyondwordsAudio: false } * @since 4.0.0 Renamed from PostContentUtils::getSourceTextForAudio() to PostContentUtils::getBody() * @since 4.6.0 Renamed from PostContentUtils::getBody() to PostContentUtils::getPostBody() * @since 4.7.0 Remove wpautop filter for block editor API requests. * @since 5.0.0 Remove SpeechKit-Start shortcode. * @since 5.0.0 Remove beyondwords_content filter. * * @param int|\WP_Post $post The WordPress post ID, or post object. * * @return string The body (the processed $post->post_content). */ public static function getPostBody(int|\WP_Post $post): string|null { $post = get_post($post); if (!($post instanceof \WP_Post)) { throw new \Exception(esc_html__('Post Not Found', 'speechkit')); } $content = PostContentUtils::getContentWithoutExcludedBlocks($post); if (has_blocks($post)) { // wpautop breaks our HTML markup when block editor paragraphs are empty remove_filter('the_content', 'wpautop'); // But we still want to remove empty lines $content = preg_replace('/^\h*\v+/m', '', $content); } // Apply the_content filters to handle shortcodes etc // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying core WordPress filter $content = apply_filters('the_content', $content); // Trim to remove trailing newlines – common for WordPress content return trim($content); } /** * Get the post summary wrapper format. * * This is a
with optional attributes depending on the BeyondWords * data of the post. * * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.6.0 * * @return string The summary wrapper
. */ public static function getPostSummaryWrapperFormat(int|\WP_Post $post): string { $post = get_post($post); if (!($post instanceof \WP_Post)) { throw new \Exception(esc_html__('Post Not Found', 'speechkit')); } $summaryVoiceId = intval(get_post_meta($post->ID, 'beyondwords_summary_voice_id', true)); if ($summaryVoiceId > 0) { return '
%s
'; } return '
%s
'; } /** * Get the post summary for the audio content. * * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.0.0 * @since 4.6.0 Renamed from PostContentUtils::getSummary() to PostContentUtils::getPostSummary() * * @return string The summary. */ public static function getPostSummary(int|\WP_Post $post): string|null { $post = get_post($post); if (!($post instanceof \WP_Post)) { throw new \Exception(esc_html__('Post Not Found', 'speechkit')); } $summary = null; // Optionally send the excerpt to the REST API, if the plugin setting has been checked $prependExcerpt = get_option('beyondwords_prepend_excerpt'); if ($prependExcerpt && has_excerpt($post)) { // Escape characters $summary = htmlentities($post->post_excerpt, ENT_QUOTES | ENT_XHTML); // Apply WordPress filters // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Applying core WordPress filter $summary = apply_filters('get_the_excerpt', $summary); // Convert line breaks into paragraphs $summary = trim(wpautop($summary)); } return $summary; } /** * Get the post content without blocks which have been filtered. * * We have added buttons into the Gutenberg editor to optionally exclude selected * blocks from the source text for audio. * * This method filters all blocks, removing any which have been excluded. * * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 3.8.0 * @since 4.0.0 Replace for loop with array_reduce * @since 6.0.0 Remove beyondwordsMarker attribute from rendered blocks. * * @return string The post body without excluded blocks. */ public static function getContentWithoutExcludedBlocks(int|\WP_Post $post): string { if (! has_blocks($post)) { return trim($post->post_content); } $blocks = parse_blocks($post->post_content); $output = ''; $blocks = PostContentUtils::getAudioEnabledBlocks($post); foreach ($blocks as $block) { $output .= render_block($block); } return $output; } /** * Get audio-enabled blocks. * * @param int|\WP_Post $post The WordPress post ID, or post object. * * @since 4.0.0 * @since 5.0.0 Remove beyondwords_post_audio_enabled_blocks filter. * * @return array The blocks. */ public static function getAudioEnabledBlocks(int|\WP_Post $post): array { $post = get_post($post); if (! ($post instanceof \WP_Post)) { return []; } if (! has_blocks($post)) { return []; } $allBlocks = parse_blocks($post->post_content); return array_filter($allBlocks, function ($block) { $enabled = true; if (is_array($block['attrs']) && isset($block['attrs']['beyondwordsAudio'])) { $enabled = (bool) $block['attrs']['beyondwordsAudio']; } return $enabled; }); } /** * Get the body param we pass to the API. * * @since 3.0.0 Introduced as getBodyJson. * @since 3.3.0 Added metadata to aid custom playlist generation. * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils. * @since 3.10.4 Rename `published_at` API param to `publish_date`. * @since 4.0.0 Use new API params. * @since 4.0.3 Ensure `image_url` is always a string. * @since 4.3.0 Rename from getBodyJson to getContentParams. * @since 4.6.0 Remove summary param & prepend body with summary. * @since 5.0.0 Remove beyondwords_body_params filter. * @since 6.0.0 Cast return value to string. * * @static * @param int $postId WordPress Post ID. * * @return string JSON endoded params. **/ public static function getContentParams(int $postId): array|string { $body = [ 'type' => 'auto_segment', 'title' => get_the_title($postId), 'body' => PostContentUtils::getContentBody($postId), 'source_url' => get_the_permalink($postId), 'source_id' => strval($postId), 'author' => PostContentUtils::getAuthorName($postId), 'image_url' => strval(wp_get_original_image_url(get_post_thumbnail_id($postId))), 'metadata' => PostContentUtils::getMetadata($postId), 'publish_date' => get_post_time(PostContentUtils::DATE_FORMAT, true, $postId), ]; $status = get_post_status($postId); /* * If the post status is draft/pending then we explicity send * { published: false } to the BeyondWords API, to prevent the * generated audio from being published in playlists. * * We also omit { publish_date } because get_post_time() returns `false` * for posts which are "Pending Review". */ if (in_array($status, ['draft', 'pending'])) { $body['published'] = false; unset($body['publish_date']); } elseif (get_option('beyondwords_project_auto_publish_enabled')) { $body['published'] = true; } $languageCode = get_post_meta($postId, 'beyondwords_language_code', true); if ($languageCode) { $body['language'] = $languageCode; } $bodyVoiceId = intval(get_post_meta($postId, 'beyondwords_body_voice_id', true)); if ($bodyVoiceId > 0) { $body['body_voice_id'] = $bodyVoiceId; } $titleVoiceId = intval(get_post_meta($postId, 'beyondwords_title_voice_id', true)); if ($titleVoiceId > 0) { $body['title_voice_id'] = $titleVoiceId; } $summaryVoiceId = intval(get_post_meta($postId, 'beyondwords_summary_voice_id', true)); if ($summaryVoiceId > 0) { $body['summary_voice_id'] = $summaryVoiceId; } /** * Filters the params we send to the BeyondWords API 'content' endpoint. * * @since 4.0.0 Introduced as beyondwords_body_params * @since 4.3.0 Renamed from beyondwords_body_params to beyondwords_content_params * * @param array $body The params we send to the BeyondWords API. * @param array $postId WordPress post ID. */ $body = apply_filters('beyondwords_content_params', $body, $postId); return (string) wp_json_encode($body); } /** * Get the post metadata to send with BeyondWords API requests. * * The metadata key is defined by the BeyondWords API as "A custom object * for storing meta information". * * The metadata values are used to create filters for playlists in the * BeyondWords dashboard. * * We currently only include taxonomies by default, and the output of this * method can be filtered using the `beyondwords_post_metadata` filter. * * @since 3.3.0 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils. * @since 5.0.0 Remove beyondwords_post_metadata filter. * * @param int $postId Post ID. * * @return object The metadata object (empty if no metadata). */ public static function getMetadata(int $postId): array|object { $metadata = new \stdClass(); $taxonomy = PostContentUtils::getAllTaxonomiesAndTerms($postId); if (count((array)$taxonomy)) { $metadata->taxonomy = $taxonomy; } return $metadata; } /** * Get all taxonomies, and their selected terms, for a post. * * Returns an associative array of taxonomy names and terms. * * For example: * * array( * "categories" => array("Category 1"), * "post_tag" => array("Tag 1", "Tag 2", "Tag 3"), * ) * * @since 3.3.0 * @since 3.5.0 Moved from Core\Utils to Component\Post\PostUtils * * @param int $postId Post ID. * * @return object The taxonomies object (empty if no taxonomies). */ public static function getAllTaxonomiesAndTerms(int $postId): array|object { $postType = get_post_type($postId); $postTypeTaxonomies = get_object_taxonomies($postType); $taxonomies = new \stdClass(); foreach ($postTypeTaxonomies as $postTypeTaxonomy) { $terms = get_the_terms($postId, $postTypeTaxonomy); if (! empty($terms) && ! is_wp_error($terms)) { $taxonomies->{(string)$postTypeTaxonomy} = wp_list_pluck($terms, 'name'); } } return $taxonomies; } /** * Get author name for a post. * * @since 3.10.4 * * @param int $postId Post ID. */ public static function getAuthorName(int $postId): string { $authorId = get_post_field('post_author', $postId); return get_the_author_meta('display_name', $authorId); } }