PluginProbe
BeyondWords – AI audio for publishers / 6.0.3
BeyondWords – AI audio for publishers v6.0.3
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.0.3, at src/Core/Player/Player.php

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