// If nothing has been added it means there are no dash-letter pairs; return the name as-is.
return '' === $custom_name
? strtolower( substr( $html_attribute_name, 5 ) )
: ( $custom_name . strtolower( substr( $html_attribute_name, $was_at ) ) );
* Returns a corresponding HTML attribute name for the given name,
* if that name were found in a JS element’s `dataset` property.
* 'data-post-id' === wp_html_custom_data_attribute_name( 'postId' );
* 'data--before' === wp_html_custom_data_attribute_name( 'Before' );
* 'data---one---two---' === wp_html_custom_data_attribute_name( '-One--Two---' );
* // Not every attribute name will be interpreted as a custom data attribute.
* null === wp_html_custom_data_attribute_name( '/not-an-attribute/' );
* null === wp_html_custom_data_attribute_name( 'no spaces' );
* // Some very surprising names will; for example, a property whose name is the empty string.
* 'data-' === wp_html_custom_data_attribute_name( '' );
* @see https://html.spec.whatwg.org/#concept-domstringmap-pairs
* @see \wp_js_dataset_name()
* @param string $js_dataset_name Name of JS `dataset` property to transform.
* @return string|null Corresponding name of an HTML custom data attribute for the given dataset name,
* if possible to represent in HTML, otherwise `null`.
function wp_html_custom_data_attribute_name( string $js_dataset_name ): ?string {
$end = strlen( $js_dataset_name );
* If it contains characters which would end the attribute name parsing then
* something it’s not possible to represent this in HTML.
if ( strcspn( $js_dataset_name, "=/> \t\f\r\n" ) !== $end ) {
$next_upper_after = strcspn( $js_dataset_name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', $at );
$next_upper_at = $at + $next_upper_after;
if ( $next_upper_at >= $end ) {
$prefix = substr( $js_dataset_name, $was_at, $next_upper_at - $was_at );
$html_name .= strtolower( $prefix );
$html_name .= '-' . strtolower( $js_dataset_name[ $next_upper_at ] );
$at = $next_upper_at + 1;
$html_name .= strtolower( substr( $js_dataset_name, $was_at ) );