# speechkit/7.2.0/src/editor/components/block-attributes/class-block-attributes.php

BeyondWords – AI audio for publishers, version 7.2.0. 236 lines.

- Page: https://pluginprobe.com/plugins/speechkit/7.2.0/code/src/editor/components/block-attributes/class-block-attributes.php
- Raw: https://pluginprobe.com/plugins/speechkit/7.2.0/raw/src/editor/components/block-attributes/class-block-attributes.php
- Modified: 2026-09-25T15:14:58+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/7.2.0/code/src/editor/components/block-attributes/class-block-attributes.php#L10-L20`.

```php
<?php

declare( strict_types = 1 );

/**
 * BeyondWords support for Gutenberg blocks.
 *
 * @package BeyondWords\Editor\Components
 * @author  Stuart McAlpine <stu@beyondwords.io>
 * @since   3.7.0
 * @since   4.0.0 Renamed from BlockAudioAttribute.php to BlockAttributes.php to support multiple attributes
 * @since   7.0.0 Refactored to BeyondWords namespace with snake_case methods.
 */

namespace BeyondWords\Editor\Components;

/**
 * BlockAttributes
 *
 * @since 3.7.0
 * @since 4.0.0 Renamed from BlockAudioAttribute to BlockAttributes to support multiple attributes.
 * @since 6.0.0 Stop adding beyondwordsMarker attribute to blocks.
 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
 */
defined( 'ABSPATH' ) || exit;

class BlockAttributes {

	/**
	 * Block attribute holding the per-block language code (e.g. `en_GB`).
	 *
	 * @since 7.1.0
	 */
	public const LANGUAGE_ATTRIBUTE = 'beyondwordsLanguageCode';

	/**
	 * Block attribute holding the per-block voice id, which also carries the model.
	 *
	 * @since 7.1.0
	 */
	public const VOICE_ATTRIBUTE = 'beyondwordsVoiceId';

	/**
	 * The block whose `<audio>` tag always gets the audio data attribute —
	 * every core/audio block has a file, so there is nothing to opt into.
	 *
	 * @since 7.2.0
	 */
	public const AUDIO_BLOCK_NAME = 'core/audio';

	/**
	 * Init.
	 *
	 * @since 4.0.0
	 * @since 6.0.0 Make static and remove renderBlock registration.
	 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
	 */
	public static function init() {
		add_filter( 'register_block_type_args', [ self::class, 'register_audio_attribute'] );
		add_filter( 'register_block_type_args', [ self::class, 'register_marker_attribute'] );
		add_filter( 'register_block_type_args', [ self::class, 'register_language_attribute' ] );
		add_filter( 'register_block_type_args', [ self::class, 'register_voice_attribute' ] );
	}

	/**
	 * Register "Audio" attribute for Gutenberg blocks.
	 *
	 * @since 6.0.0 Make static.
	 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
	 */
	public static function register_audio_attribute( $args ) {
		return self::register_attribute(
			$args,
			'beyondwordsAudio',
			[
				'type'    => 'boolean',
				'default' => true,
			]
		);
	}

	/**
	 * Register "Segment marker" attribute for Gutenberg blocks.
	 *
	 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
	 *
	 * @deprecated This attribute is no longer used as of 6.0.0, but kept for backward compatibility.
	 *
	 * @since 6.0.0 Make static.
	 */
	public static function register_marker_attribute( $args ) {
		return self::register_attribute(
			$args,
			'beyondwordsMarker',
			[
				'type'    => 'string',
				'default' => '',
			]
		);
	}

	/**
	 * Register the per-block "Language" attribute for Gutenberg blocks.
	 *
	 * @since 7.1.0
	 */
	public static function register_language_attribute( $args ) {
		return self::register_attribute(
			$args,
			self::LANGUAGE_ATTRIBUTE,
			[
				'type'    => 'string',
				'default' => '',
			]
		);
	}

	/**
	 * Register the per-block "Voice" attribute for Gutenberg blocks.
	 *
	 * @since 7.1.0
	 */
	public static function register_voice_attribute( $args ) {
		return self::register_attribute(
			$args,
			self::VOICE_ATTRIBUTE,
			[
				'type'    => 'string',
				'default' => '',
			]
		);
	}

	/**
	 * Add a block attribute, leaving an existing definition of the same name alone.
	 *
	 * @since 7.1.0
	 *
	 * @param mixed $args The `register_block_type_args` args, which may be null.
	 */
	private static function register_attribute( $args, string $name, array $schema ): array {
		if ( ! isset( $args['attributes'] ) ) {
			$args['attributes'] = [];
		}

		if ( ! array_key_exists( $name, $args['attributes'] ) ) {
			$args['attributes'][ $name ] = $schema;
		}

		return $args;
	}

	/**
	 * Add the segment-scoped voice and audio data attributes to a rendered block.
	 *
	 * Not registered in init(): it is added around the API body build only, so
	 * front-end output is untouched.
	 *
	 * @since 7.1.0
	 * @since 7.2.0 Add the audio data attribute to core/audio blocks.
	 *
	 * @param string $block_content The rendered block HTML.
	 * @param array  $block         The parsed block.
	 */
	public static function add_segment_attributes( $block_content, $block ): string {
		$block_content = (string) $block_content;

		$attrs = ( is_array( $block ) && is_array( $block['attrs'] ?? null ) ) ? $block['attrs'] : [];

		$language = self::language_code( self::attribute_value( $attrs, self::LANGUAGE_ATTRIBUTE ) );
		$voice_id = self::voice_id( self::attribute_value( $attrs, self::VOICE_ATTRIBUTE ) );
		$is_audio = is_array( $block ) && self::AUDIO_BLOCK_NAME === ( $block['blockName'] ?? '' );

		if ( '' === $language && '' === $voice_id && ! $is_audio ) {
			return $block_content;
		}

		$processor = new \WP_HTML_Tag_Processor( $block_content );

		// Tagless output (an empty block, or a shortcode block) has nothing to carry them.
		if ( ! $processor->next_tag() ) {
			return $block_content;
		}

		if ( '' !== $language ) {
			$processor->set_attribute( 'data-beyondwords-language', $language );
		}

		if ( '' !== $voice_id ) {
			$processor->set_attribute( 'data-beyondwords-voice-id', $voice_id );
		}

		// The audio marker belongs on the <audio> tag itself, not the figure wrapping it.
		if ( $is_audio && $processor->next_tag( [ 'tag_name' => 'AUDIO' ] ) ) {
			$processor->set_attribute( 'data-beyondwords-audio', 'true' );
		}

		return $processor->get_updated_html();
	}

	/**
	 * A block attribute as a trimmed string.
	 *
	 * @since 7.1.0
	 *
	 * @return string The value, or '' when unset or non-scalar.
	 */
	private static function attribute_value( array $attrs, string $name ): string {
		$value = $attrs[ $name ] ?? '';

		return is_scalar( $value ) ? trim( (string) $value ) : '';
	}

	/**
	 * A language code in the shape the API issues, or '' — the comment
	 * delimiter is editor-writable, so it is not trusted to carry one.
	 *
	 * Fits every code the API lists, including `fil_PH`, `sr_Latn_RS` and
	 * `zh_CN_henan`; nothing with whitespace, quotes or markup does.
	 *
	 * @since 7.1.0
	 */
	private static function language_code( string $value ): string {
		return preg_match( '/^[a-z]{2,3}(_[A-Za-z]{2,8}){1,2}$/', $value ) ? $value : '';
	}

	/**
	 * A voice id as the API issues them — a positive integer — or ''.
	 *
	 * @since 7.1.0
	 */
	private static function voice_id( string $value ): string {
		return ctype_digit( $value ) && (int) $value > 0 ? (string) (int) $value : '';
	}
}

```
