self::render_player( 'shortcode' ) );
}
/**
* Auto-prepend the player to `the_content` unless the post already embeds one.
*
* @param string $content Post content.
*
* @return string
*/
public static function auto_prepend_player( $content ) {
if ( ! is_singular() ) {
return $content;
}
// Mirrors render_player()'s own early return, but bails before the expensive
// has_custom_player() DOM scan when no player could render anyway.
$post = get_post();
if ( ! $post instanceof \WP_Post || ! self::is_enabled( $post ) ) {
return $content;
}
if ( self::has_custom_player( $content ) ) {
return $content;
}
return self::render_player( 'auto' ) . $content;
}
/**
* Convert legacy `
` placeholders to the shortcode.
*
* The regex tolerates attribute reordering, valueless (boolean) attributes,
* extra attributes, and self-closing tags.
*
* @param string $content Post content.
*
* @return string
*/
public static function replace_legacy_custom_player( $content ) {
if ( ! is_singular() ) {
return $content;
}
$pattern = '/
]*data-beyondwords-player[\s>=\/])[^>]*(?:\/>|>\s*<\/div>)/i';
return preg_replace( $pattern, '[beyondwords_player]', $content );
}
/**
* Pick a renderer for the current post and return its HTML.
*
* The `beyondwords_player_html` filter runs even when no renderer matches
* — that way third-party code can still inject markup conditionally.
*
* @param string $context 'auto' (auto-prepend) or 'shortcode'.
*/
public static function render_player( string $context = 'shortcode' ): string {
$post = get_post();
if ( ! $post instanceof \WP_Post || ! self::is_enabled( $post ) ) {
return '';
}
$html = '';
foreach ( self::$renderers as $renderer_class ) {
if (
is_callable( [ $renderer_class, 'check' ] )
&& $renderer_class::check( $post )
&& is_callable( [ $renderer_class, 'render' ] )
) {
$html = $renderer_class::render( $post, $context );
break;
}
}
$project_id = \BeyondWords\Post\Meta::get_project_id( $post->ID );
$content_id = \BeyondWords\Post\Meta::get_content_id( $post->ID, true );
/**
* Filters the HTML of the BeyondWords Player.
*
* @since 4.0.0
* @since 4.3.0 Applied to all player renderers (AMP and JavaScript).
* @since 6.1.0 Added `$context` parameter.
*
* @param string $html Audio player HTML.
* @param int $post_id WordPress post ID.
* @param int $project_id BeyondWords project ID.
* @param int $content_id BeyondWords content ID.
* @param string $context Rendering context: 'auto' or 'shortcode'.
*/
return apply_filters( 'beyondwords_player_html', $html, $post->ID, $project_id, $content_id, $context );
}
/**
* Whether the player should be active for this post.
*
* Headless mode counts as enabled — we still emit the SDK script so the
* publisher's own UI can drive it.
*
* @since 7.0.0 "Embed: None" hides the player; the legacy disabled flag is still honoured.
*
* @param \WP_Post $post Post object.
*/
public static function is_enabled( \WP_Post $post ): bool {
if ( \BeyondWords\Editor\Components\SettingsFields::is_player_disabled_for_post( $post->ID ) ) {
return false;
}
$player_ui = get_option( \BeyondWords\Settings\Fields::OPTION_PLAYER_UI, \BeyondWords\Settings\Fields::PLAYER_UI_ENABLED );
return in_array(
$player_ui,
[ \BeyondWords\Settings\Fields::PLAYER_UI_ENABLED, \BeyondWords\Settings\Fields::PLAYER_UI_HEADLESS ],
true
);
}
/**
* Detect whether the post content already contains a BeyondWords player.
*
* @param string $content Post content.
*/
public static function has_custom_player( string $content ): bool {
if ( has_shortcode( $content, 'beyondwords_player' ) ) {
return true;
}
$js_sdk_url = \BeyondWords\Core\Urls::get_js_sdk_url();
// Both XPath queries need one of these literal substrings, so when neither
// appears we can skip the expensive per-pageview DOM parse.
if (
! str_contains( $content, 'data-beyondwords-player' )
&& ! str_contains( $content, $js_sdk_url )
) {
return false;
}
$crawler = new \Symfony\Component\DomCrawler\Crawler( $content );
$script_xpath = sprintf( '//script[@async][@defer][contains(@src, "%s")]', $js_sdk_url );
if ( $crawler->filterXPath( $script_xpath )->count() > 0 ) {
return true;
}
if ( $crawler->filterXPath( '//div[@data-beyondwords-player="true"]' )->count() > 0 ) {
return true;
}
return false;
}
}