PluginProbe
BeyondWords – AI audio for publishers / 7.2.0
BeyondWords – AI audio for publishers v7.2.0
7.2.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 All 44 releases
speechkit / src / editor / components / block-attributes / class-block-attributes.php

class-block-attributes.php in BeyondWords – AI audio for publishers 7.2.0, at src/editor/components/block-attributes/class-block-attributes.php

236 lines 6.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 /**
6 * BeyondWords support for Gutenberg blocks.
7 *
8 * @package BeyondWords\Editor\Components
9 * @author Stuart McAlpine <[email protected]>
10 * @since 3.7.0
11 * @since 4.0.0 Renamed from BlockAudioAttribute.php to BlockAttributes.php to support multiple attributes
12 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
13 */
14
15 namespace BeyondWords\Editor\Components;
16
17 /**
18 * BlockAttributes
19 *
20 * @since 3.7.0
21 * @since 4.0.0 Renamed from BlockAudioAttribute to BlockAttributes to support multiple attributes.
22 * @since 6.0.0 Stop adding beyondwordsMarker attribute to blocks.
23 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
24 */
25 defined( 'ABSPATH' ) || exit;
26
27 class BlockAttributes {
28
29 /**
30 * Block attribute holding the per-block language code (e.g. `en_GB`).
31 *
32 * @since 7.1.0
33 */
34 public const LANGUAGE_ATTRIBUTE = 'beyondwordsLanguageCode';
35
36 /**
37 * Block attribute holding the per-block voice id, which also carries the model.
38 *
39 * @since 7.1.0
40 */
41 public const VOICE_ATTRIBUTE = 'beyondwordsVoiceId';
42
43 /**
44 * The block whose `<audio>` tag always gets the audio data attribute —
45 * every core/audio block has a file, so there is nothing to opt into.
46 *
47 * @since 7.2.0
48 */
49 public const AUDIO_BLOCK_NAME = 'core/audio';
50
51 /**
52 * Init.
53 *
54 * @since 4.0.0
55 * @since 6.0.0 Make static and remove renderBlock registration.
56 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
57 */
58 public static function init() {
59 add_filter( 'register_block_type_args', [ self::class, 'register_audio_attribute'] );
60 add_filter( 'register_block_type_args', [ self::class, 'register_marker_attribute'] );
61 add_filter( 'register_block_type_args', [ self::class, 'register_language_attribute' ] );
62 add_filter( 'register_block_type_args', [ self::class, 'register_voice_attribute' ] );
63 }
64
65 /**
66 * Register "Audio" attribute for Gutenberg blocks.
67 *
68 * @since 6.0.0 Make static.
69 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
70 */
71 public static function register_audio_attribute( $args ) {
72 return self::register_attribute(
73 $args,
74 'beyondwordsAudio',
75 [
76 'type' => 'boolean',
77 'default' => true,
78 ]
79 );
80 }
81
82 /**
83 * Register "Segment marker" attribute for Gutenberg blocks.
84 *
85 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
86 *
87 * @deprecated This attribute is no longer used as of 6.0.0, but kept for backward compatibility.
88 *
89 * @since 6.0.0 Make static.
90 */
91 public static function register_marker_attribute( $args ) {
92 return self::register_attribute(
93 $args,
94 'beyondwordsMarker',
95 [
96 'type' => 'string',
97 'default' => '',
98 ]
99 );
100 }
101
102 /**
103 * Register the per-block "Language" attribute for Gutenberg blocks.
104 *
105 * @since 7.1.0
106 */
107 public static function register_language_attribute( $args ) {
108 return self::register_attribute(
109 $args,
110 self::LANGUAGE_ATTRIBUTE,
111 [
112 'type' => 'string',
113 'default' => '',
114 ]
115 );
116 }
117
118 /**
119 * Register the per-block "Voice" attribute for Gutenberg blocks.
120 *
121 * @since 7.1.0
122 */
123 public static function register_voice_attribute( $args ) {
124 return self::register_attribute(
125 $args,
126 self::VOICE_ATTRIBUTE,
127 [
128 'type' => 'string',
129 'default' => '',
130 ]
131 );
132 }
133
134 /**
135 * Add a block attribute, leaving an existing definition of the same name alone.
136 *
137 * @since 7.1.0
138 *
139 * @param mixed $args The `register_block_type_args` args, which may be null.
140 */
141 private static function register_attribute( $args, string $name, array $schema ): array {
142 if ( ! isset( $args['attributes'] ) ) {
143 $args['attributes'] = [];
144 }
145
146 if ( ! array_key_exists( $name, $args['attributes'] ) ) {
147 $args['attributes'][ $name ] = $schema;
148 }
149
150 return $args;
151 }
152
153 /**
154 * Add the segment-scoped voice and audio data attributes to a rendered block.
155 *
156 * Not registered in init(): it is added around the API body build only, so
157 * front-end output is untouched.
158 *
159 * @since 7.1.0
160 * @since 7.2.0 Add the audio data attribute to core/audio blocks.
161 *
162 * @param string $block_content The rendered block HTML.
163 * @param array $block The parsed block.
164 */
165 public static function add_segment_attributes( $block_content, $block ): string {
166 $block_content = (string) $block_content;
167
168 $attrs = ( is_array( $block ) && is_array( $block['attrs'] ?? null ) ) ? $block['attrs'] : [];
169
170 $language = self::language_code( self::attribute_value( $attrs, self::LANGUAGE_ATTRIBUTE ) );
171 $voice_id = self::voice_id( self::attribute_value( $attrs, self::VOICE_ATTRIBUTE ) );
172 $is_audio = is_array( $block ) && self::AUDIO_BLOCK_NAME === ( $block['blockName'] ?? '' );
173
174 if ( '' === $language && '' === $voice_id && ! $is_audio ) {
175 return $block_content;
176 }
177
178 $processor = new \WP_HTML_Tag_Processor( $block_content );
179
180 // Tagless output (an empty block, or a shortcode block) has nothing to carry them.
181 if ( ! $processor->next_tag() ) {
182 return $block_content;
183 }
184
185 if ( '' !== $language ) {
186 $processor->set_attribute( 'data-beyondwords-language', $language );
187 }
188
189 if ( '' !== $voice_id ) {
190 $processor->set_attribute( 'data-beyondwords-voice-id', $voice_id );
191 }
192
193 // The audio marker belongs on the <audio> tag itself, not the figure wrapping it.
194 if ( $is_audio && $processor->next_tag( [ 'tag_name' => 'AUDIO' ] ) ) {
195 $processor->set_attribute( 'data-beyondwords-audio', 'true' );
196 }
197
198 return $processor->get_updated_html();
199 }
200
201 /**
202 * A block attribute as a trimmed string.
203 *
204 * @since 7.1.0
205 *
206 * @return string The value, or '' when unset or non-scalar.
207 */
208 private static function attribute_value( array $attrs, string $name ): string {
209 $value = $attrs[ $name ] ?? '';
210
211 return is_scalar( $value ) ? trim( (string) $value ) : '';
212 }
213
214 /**
215 * A language code in the shape the API issues, or '' — the comment
216 * delimiter is editor-writable, so it is not trusted to carry one.
217 *
218 * Fits every code the API lists, including `fil_PH`, `sr_Latn_RS` and
219 * `zh_CN_henan`; nothing with whitespace, quotes or markup does.
220 *
221 * @since 7.1.0
222 */
223 private static function language_code( string $value ): string {
224 return preg_match( '/^[a-z]{2,3}(_[A-Za-z]{2,8}){1,2}$/', $value ) ? $value : '';
225 }
226
227 /**
228 * A voice id as the API issues them — a positive integer — or ''.
229 *
230 * @since 7.1.0
231 */
232 private static function voice_id( string $value ): string {
233 return ctype_digit( $value ) && (int) $value > 0 ? (string) (int) $value : '';
234 }
235 }
236