PluginProbe
BeyondWords – AI audio for publishers / 6.3.0
BeyondWords – AI audio for publishers v6.3.0
7.1.0 trunk 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.1.0 4.1.1 4.1.2 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.3.0 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.7.0 All 43 releases
speechkit / src / Core / Player / Player.php

Player.php in BeyondWords – AI audio for publishers 6.3.0, at src/Core/Player/Player.php

193 lines 5.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Beyondwords\Wordpress\Core\Player;
6
7 use Beyondwords\Wordpress\Component\Post\PostMetaUtils;
8 use Beyondwords\Wordpress\Component\Settings\Fields\PlayerUI\PlayerUI;
9 use Beyondwords\Wordpress\Core\Environment;
10 use Symfony\Component\DomCrawler\Crawler;
11
12 /**
13 * Class Player
14 *
15 * Entry point for registering player-related WordPress hooks.
16 */
17 defined('ABSPATH') || exit;
18
19 class Player
20 {
21 /**
22 * List of renderer class names (must have static check() and render()).
23 *
24 * @var string[]
25 */
26 protected static array $renderers = [
27 Renderer\Amp::class,
28 Renderer\Javascript::class,
29 ];
30
31 /**
32 * Add WordPress hooks.
33 */
34 public static function init(): void
35 {
36 // Actions.
37 add_action('init', [self::class, 'registerShortcodes']);
38
39 // Filters.
40 add_filter('the_content', [self::class, 'replaceLegacyCustomPlayer'], 5);
41 add_filter('the_content', [self::class, 'autoPrependPlayer'], 1000000);
42 add_filter('newsstand_the_content', [self::class, 'autoPrependPlayer']);
43 }
44
45 /**
46 * Register the [beyondwords_player] shortcode.
47 */
48 public static function registerShortcodes(): void
49 {
50 add_shortcode('beyondwords_player', fn() => self::renderPlayer('shortcode'));
51 }
52 /**
53 * Conditionally prepend the player to a string (the post content).
54 *
55 *
56 */
57 public static function autoPrependPlayer($content)
58 {
59 if (! is_singular() || self::hasCustomPlayer($content)) {
60 return $content;
61 }
62
63 return self::renderPlayer('auto') . $content;
64 }
65
66 /**
67 * Replace the legacy custom player div with the shortcode.
68 *
69 * @since 6.0.0
70 * @since 6.0.1 Use regex to match legacy player divs.
71 *
72 * @param string $content The post content.
73 *
74 * @return string The post content.
75 */
76 public static function replaceLegacyCustomPlayer($content)
77 {
78 if (! is_singular()) {
79 return $content;
80 }
81
82 // Use regex to match legacy player divs with any whitespace or attribute ordering.
83 // data-beyondwords-player is a boolean attribute - its presence indicates a player div,
84 // regardless of the value (true, false, or any other value).
85 // This handles variations like:
86 // - <div data-beyondwords-player="true"></div>
87 // - <div data-beyondwords-player="anything"></div>
88 // - <div data-beyondwords-player></div> (boolean attribute)
89 // - <div data-beyondwords-player contenteditable="false"></div>
90 // - <div contenteditable="false" data-beyondwords-player> </div>
91 // - <div data-beyondwords-player />
92 $pattern = '/<div\s+(?=[^>]*data-beyondwords-player[\s>=\/])[^>]*(?:\/>|>\s*<\/div>)/i';
93
94 return preg_replace($pattern, '[beyondwords_player]', $content);
95 }
96
97 /**
98 * Render a player (AMP/JS depending on context).
99 *
100 * @param string $context The context in which the player is being rendered.
101 * One of 'auto' or 'shortcode'.
102 */
103 public static function renderPlayer(string $context = 'shortcode'): string
104 {
105 $post = get_post();
106
107 if (! $post instanceof \WP_Post || ! self::isEnabled($post)) {
108 return '';
109 }
110
111 $html = '';
112
113 foreach (self::$renderers as $rendererClass) {
114 if (is_callable([$rendererClass, 'check']) && $rendererClass::check($post)) {
115 if (is_callable([$rendererClass, 'render'])) {
116 $html = $rendererClass::render($post, $context);
117 break;
118 }
119 }
120 }
121
122 $projectId = PostMetaUtils::getProjectId($post->ID);
123 $contentId = PostMetaUtils::getContentId($post->ID, true);
124
125 /**
126 * Filters the HTML of the BeyondWords Player.
127 *
128 * @since 4.0.0
129 * @since 4.3.0 Applied to all player renderers (AMP and JavaScript).
130 * @since 6.1.0 Add $context parameter.
131 *
132 * @param string $html The HTML for the audio player.
133 * @param int $postId WordPress post ID.
134 * @param int $projectId BeyondWords project ID.
135 * @param int $contentId BeyondWords content ID.
136 * @param string $context The context: 'auto' or 'shortcode'.
137 */
138 $html = apply_filters('beyondwords_player_html', $html, $post->ID, $projectId, $contentId, $context);
139
140 return $html;
141 }
142
143 /**
144 * Check if the player is enabled for a post. This considers "Headless" mode
145 * as enabled since we still want to output the player script tag for Headless.
146 *
147 * @param \WP_Post $post Post object.
148 *
149 * @return bool True if the player is enabled.
150 */
151 public static function isEnabled(\WP_Post $post): bool
152 {
153 if (PostMetaUtils::getDisabled($post->ID)) {
154 return false;
155 }
156
157 // Default to "Enabled".
158 $playerUI = get_option(PlayerUI::OPTION_NAME, PlayerUI::ENABLED);
159
160 $enabled = [PlayerUI::ENABLED, PlayerUI::HEADLESS];
161
162 return in_array($playerUI, $enabled, true);
163 }
164
165 /**
166 * Detect if a custom player is already in the content.
167 *
168 *
169 */
170 public static function hasCustomPlayer(string $content): bool
171 {
172 // Detect shortcode.
173 if (has_shortcode($content, 'beyondwords_player')) {
174 return true;
175 }
176
177 $crawler = new Crawler($content);
178
179 // Detect player script tag.
180 $scriptXpath = sprintf('//script[@async][@defer][contains(@src, "%s")]', Environment::getJsSdkUrl());
181 if ($crawler->filterXPath($scriptXpath)->count() > 0) {
182 return true;
183 }
184
185 // Detect legacy player div.
186 if ($crawler->filterXPath('//div[@data-beyondwords-player="true"]')->count() > 0) {
187 return true;
188 }
189
190 return false;
191 }
192 }
193