# speechkit/6.3.0/src/Component/Post/PostContentUtils.php

BeyondWords – AI audio for publishers, version 6.3.0. 403 lines.

- Page: https://pluginprobe.com/plugins/speechkit/6.3.0/code/src/Component/Post/PostContentUtils.php
- Raw: https://pluginprobe.com/plugins/speechkit/6.3.0/raw/src/Component/Post/PostContentUtils.php
- Modified: 2026-04-12T21:25:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/speechkit/6.3.0/code/src/Component/Post/PostContentUtils.php#L10-L20`.

```php
<?php

declare(strict_types=1);

namespace Beyondwords\Wordpress\Component\Post;

/**
 * BeyondWords Post Content Utilities.
 *
 * @package    Beyondwords
 * @subpackage Beyondwords/includes
 * @author     Stuart McAlpine <stu@beyondwords.io>
 * @since      3.5.0
 */
defined('ABSPATH') || exit;

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 <div> 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 <div>.
     */
    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 '<div data-beyondwords-summary="true" data-beyondwords-voice-id="' . $summaryVoiceId . '">%s</div>';
        }

        return '<div data-beyondwords-summary="true">%s</div>';
    }

    /**
     * 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);
    }
}

```
