_data( $args['handle'], 'path', $rtl_file_path ); } } } // Enqueue the stylesheet. wp_enqueue_style( $args['handle'] ); return $content; }; $hook = did_action( 'wp_enqueue_scripts' ) ? 'wp_footer' : 'wp_enqueue_scripts'; if ( wp_should_load_block_assets_on_demand() ) { /** * Callback function to register and enqueue styles. * * @param string $content The block content. * @param array $block The full block, including name and attributes. * @return string Block content. */ $callback_separate = static function ( $content, $block ) use ( $block_name, $callback ) { if ( ! empty( $block['blockName'] ) && $block_name === $block['blockName'] ) { return $callback( $content ); } return $content; }; /* * The filter's callback here is an anonymous function because * using a named function in this case is not possible. * * The function cannot be unhooked, however, users are still able * to dequeue the stylesheets registered/enqueued by the callback * which is why in this case, using an anonymous function * was deemed acceptable. */ add_filter( 'render_block', $callback_separate, 10, 2 ); return; } /* * The filter's callback here is an anonymous function because * using a named function in this case is not possible. * * The function cannot be unhooked, however, users are still able * to dequeue the stylesheets registered/enqueued by the callback * which is why in this case, using an anonymous function * was deemed acceptable. */ add_filter( $hook, $callback ); // Enqueue assets in the editor. add_action( 'enqueue_block_assets', $callback ); } /** * Loads classic theme styles on classic themes in the frontend. * * This is used for backwards compatibility for Button and File blocks specifically. * * @since 6.1.0 * @since 6.2.0 Added File block styles. * @since 6.8.0 Moved stylesheet registration outside of this function. */ function wp_enqueue_classic_theme_styles() { if ( ! wp_theme_has_theme_json() ) { wp_enqueue_style( 'classic-theme-styles' ); } } /** * Enqueues the assets required for the Command Palette. * * @since 6.9.0 * * @global array $menu * @global array $submenu */ function wp_enqueue_command_palette_assets() { global $menu, $submenu; $command_palette_settings = array( 'is_network_admin' => is_network_admin(), ); if ( $menu ) { $menu_commands = array(); foreach ( $menu as $menu_item ) { if ( empty( $menu_item[0] ) || ! empty( $menu_item[1] ) && ! current_user_can( $menu_item[1] ) ) { continue; } // Remove all HTML tags and their contents. $menu_label = $menu_item[0]; while ( preg_match( '/<[^>]*>/', $menu_label ) ) { $menu_label = preg_replace( '/<[^>]*>.*?<\/[^>]*>|<[^>]*\/>|<[^>]*>/s', '', $menu_label ); } $menu_label = trim( $menu_label ); $menu_url = ''; $menu_slug = $menu_item[2]; if ( preg_match( '/\.php($|\?)/', $menu_slug ) || wp_http_validate_url( $menu_slug ) ) { $menu_url = $menu_slug; } elseif ( ! empty( menu_page_url( $menu_slug, false ) ) ) { $menu_url = html_entity_decode( menu_page_url( $menu_slug, false ), ENT_QUOTES, get_bloginfo( 'charset' ) ); } if ( $menu_url ) { $menu_commands[] = array( 'label' => $menu_label, 'url' => $menu_url, 'name' => $menu_slug, ); } if ( array_key_exists( $menu_slug, $submenu ) ) { foreach ( $submenu[ $menu_slug ] as $submenu_item ) { if ( empty( $submenu_item[0] ) || ! empty( $submenu_item[1] ) && ! current_user_can( $submenu_item[1] ) ) { continue; } // Remove all HTML tags and their contents. $submenu_label = $submenu_item[0]; while ( preg_match( '/<[^>]*>/', $submenu_label ) ) { $submenu_label = preg_replace( '/<[^>]*>.*?<\/[^>]*>|<[^>]*\/>|<[^>]*>/s', '', $submenu_label ); } $submenu_label = trim( $submenu_label ); $submenu_url = ''; $submenu_slug = $submenu_item[2]; if ( preg_match( '/\.php($|\?)/', $submenu_slug ) || wp_http_validate_url( $submenu_slug ) ) { $submenu_url = $submenu_slug; } elseif ( ! empty( menu_page_url( $submenu_slug, false ) ) ) { $submenu_url = html_entity_decode( menu_page_url( $submenu_slug, false ), ENT_QUOTES, get_bloginfo( 'charset' ) ); } if ( $submenu_url ) { $menu_commands[] = array( 'label' => sprintf( /* translators: 1: Menu label, 2: Submenu label. */ __( '%1$s > %2$s' ), $menu_label, $submenu_label ), 'url' => $submenu_url, 'name' => $menu_slug . '-' . $submenu_item[2], ); } } } } $command_palette_settings['menu_commands'] = $menu_commands; } wp_enqueue_script( 'wp-commands' ); wp_enqueue_style( 'wp-commands' ); wp_enqueue_script( 'wp-core-commands' ); wp_add_inline_script( 'wp-core-commands', sprintf( 'wp.coreCommands.initializeCommandPalette( %s );', wp_json_encode( $command_palette_settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ) ); } /** * Removes leading and trailing _empty_ script tags. * * This is a helper meant to be used for literal script tag construction * within `wp_get_inline_script_tag()` or `wp_print_inline_script_tag()`. * It removes the literal values of "" from * around an inline script after trimming whitespace. Typically this * is used in conjunction with output buffering, where `ob_get_clean()` * is passed as the `$contents` argument. * * Example: * * // Strips exact literal empty SCRIPT tags. * $js = '; * 'sayHello();' === wp_remove_surrounding_empty_script_tags( $js ); * * // Otherwise if anything is different it warns in the JS console. * $js = ''; * 'console.error( ... )' === wp_remove_surrounding_empty_script_tags( $js ); * * @since 6.4.0 * @access private * * @see wp_print_inline_script_tag() * @see wp_get_inline_script_tag() * * @param string $contents Script body with manually created SCRIPT tag literals. * @return string Script body without surrounding script tag literals, or * original contents if both exact literals aren't present. */ function wp_remove_surrounding_empty_script_tags( $contents ) { $contents = trim( $contents ); $opener = ''; if ( strlen( $contents ) > strlen( $opener ) + strlen( $closer ) && strtoupper( substr( $contents, 0, strlen( $opener ) ) ) === $opener && strtoupper( substr( $contents, -strlen( $closer ) ) ) === $closer ) { return substr( $contents, strlen( $opener ), -strlen( $closer ) ); } else { $error_message = __( 'Expected string to start with script tag (without attributes) and end with script tag, with optional whitespace.' ); _doing_it_wrong( __FUNCTION__, $error_message, '6.4' ); return sprintf( 'console.error(%s)', wp_json_encode( sprintf( /* translators: %s: wp_remove_surrounding_empty_script_tags() */ __( 'Function %s used incorrectly in PHP.' ), 'wp_remove_surrounding_empty_script_tags()' ) . ' ' . $error_message ) ); } } /** * Adds hooks to load block styles on demand in classic themes. * * @since 6.9.0 * * @see _add_default_theme_supports() */ function wp_load_classic_theme_block_styles_on_demand() { // This is not relevant to block themes, as they are opted in to loading separate styles on demand via _add_default_theme_supports(). if ( wp_is_block_theme() ) { return; } /* * Make sure that wp_should_output_buffer_template_for_enhancement() returns true even if there aren't any * `wp_template_enhancement_output_buffer` filters added, but do so at priority zero so that applications which * wish to stream responses can more easily turn this off. */ add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true', 0 ); // If a site has opted out of the template enhancement output buffer, then bail. if ( ! wp_should_output_buffer_template_for_enhancement() ) { return; } // The following two filters are added by default for block themes in _add_default_theme_supports(). /* * Load separate block styles so that the large block-library stylesheet is not enqueued unconditionally, and so * that block-specific styles will only be enqueued when they are used on the page. A priority of zero allows for * this to be easily overridden by themes which wish to opt out. If a site has explicitly opted out of loading * separate block styles, then abort. */ add_filter( 'should_load_separate_core_block_assets', '__return_true', 0 ); if ( ! wp_should_load_separate_core_block_assets() ) { return; } /* * Also ensure that block assets are loaded on demand (although the default value is from should_load_separate_core_block_assets). * As above, a priority of zero allows for this to be easily overridden by themes which wish to opt out. If a site * has explicitly opted out of loading block styles on demand, then abort. */ add_filter( 'should_load_block_assets_on_demand', '__return_true', 0 ); if ( ! wp_should_load_block_assets_on_demand() ) { return; } // Add hooks which require the presence of the output buffer. Ideally the above two filters could be added here, but they run too early. add_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ); } /** * Adds the hooks needed for CSS output to be delayed until after the content of the page has been established. * * @since 6.9.0 * * @see wp_load_classic_theme_block_styles_on_demand() * @see _wp_footer_scripts() */ function wp_hoist_late_printed_styles() { // Skip the embed template on-demand styles aren't relevant, and there is no wp_head action. if ( is_embed() ) { return; } // Capture the styles enqueued at the enqueue_block_assets action, so that non-core block styles and global styles can be inserted afterwards during hoisting. $style_handles_at_enqueue_block_assets = array(); add_action( 'enqueue_block_assets', static function () use ( &$style_handles_at_enqueue_block_assets ) { $style_handles_at_enqueue_block_assets = wp_styles()->queue; }, PHP_INT_MIN ); add_action( 'enqueue_block_assets', static function () use ( &$style_handles_at_enqueue_block_assets ) { $style_handles_at_enqueue_block_assets = array_values( array_diff( wp_styles()->queue, $style_handles_at_enqueue_block_assets ) ); }, PHP_INT_MAX ); /* * Add a placeholder comment into the inline styles for wp-block-library, after which the late block styles * can be hoisted from the footer to be printed in the header by means of a filter below on the template enhancement * output buffer. The `wp_print_styles` action is used to ensure that if the inline style gets replaced at * `enqueue_block_assets` or `wp_enqueue_scripts` that the placeholder will be sure to be present. */ $placeholder = sprintf( '/*%s*/', uniqid( 'wp_block_styles_on_demand_placeholder:' ) ); add_action( 'wp_print_styles', static function () use ( $placeholder ) { wp_add_inline_style( 'wp-block-library', $placeholder ); } ); /* * Create a substitute for `print_late_styles()` which is aware of block styles. This substitute does not print * the styles, but it captures what would be printed for block styles and non-block styles so that they can be * later hoisted to the HEAD in the template enhancement output buffer. This will run at `wp_print_footer_scripts` * before `print_footer_scripts()` is called. */ $printed_core_block_styles = ''; $printed_other_block_styles = ''; $printed_global_styles = ''; $printed_late_styles = ''; $capture_late_styles = static function () use ( &$printed_core_block_styles, &$printed_other_block_styles, &$printed_global_styles, &$printed_late_styles ) { // Gather the styles related to on-demand block enqueues. $all_core_block_style_handles = array(); $all_other_block_style_handles = array(); foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { if ( str_starts_with( $block_type->name, 'core/' ) ) { foreach ( $block_type->style_handles as $style_handle ) { $all_core_block_style_handles[] = $style_handle; } } else { foreach ( $block_type->style_handles as $style_handle ) { $all_other_block_style_handles[] = $style_handle; } } } /* * First print all styles related to blocks which should be inserted right after the wp-block-library stylesheet * to preserve the CSS cascade. The logic in this `if` statement is derived from `wp_print_styles()`. */ $enqueued_core_block_styles = array_values( array_intersect( $all_core_block_style_handles, wp_styles()->queue ) ); if ( count( $enqueued_core_block_styles ) > 0 ) { ob_start(); wp_styles()->do_items( $enqueued_core_block_styles ); $printed_core_block_styles = ob_get_clean(); } // Non-core block styles get printed after the classic-theme-styles stylesheet. $enqueued_other_block_styles = array_values( array_intersect( $all_other_block_style_handles, wp_styles()->queue ) ); if ( count( $enqueued_other_block_styles ) > 0 ) { ob_start(); wp_styles()->do_items( $enqueued_other_block_styles ); $printed_other_block_styles = ob_get_clean(); } // Capture the global-styles so that it can be printed separately after classic-theme-styles and other styles enqueued at enqueue_block_assets. if ( wp_style_is( 'global-styles' ) ) { ob_start(); wp_styles()->do_items( array( 'global-styles' ) ); $printed_global_styles = ob_get_clean(); } /* * Print all remaining styles not related to blocks. This contains a subset of the logic from * `print_late_styles()`, without admin-specific logic and the `print_late_styles` filter to control whether * late styles are printed (since they are being hoisted anyway). */ ob_start(); wp_styles()->do_footer_items(); $printed_late_styles = ob_get_clean(); }; /* * If `_wp_footer_scripts()` was unhooked from the `wp_print_footer_scripts` action, or if `wp_print_footer_scripts()` * was unhooked from running at the `wp_footer` action, then only add a callback to `wp_footer` which will capture the * late-printed styles. * * Otherwise, in the normal case where `_wp_footer_scripts()` will run at the `wp_print_footer_scripts` action, then * swap out `_wp_footer_scripts()` with an alternative which captures the printed styles (for hoisting to HEAD) before * proceeding with printing the footer scripts. */ $wp_print_footer_scripts_priority = has_action( 'wp_print_footer_scripts', '_wp_footer_scripts' ); if ( false === $wp_print_footer_scripts_priority || false === has_action( 'wp_footer', 'wp_print_footer_scripts' ) ) { // The normal priority for wp_print_footer_scripts() is to run at 20. add_action( 'wp_footer', $capture_late_styles, 20 ); } else { remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts', $wp_print_footer_scripts_priority ); add_action( 'wp_print_footer_scripts', static function () use ( $capture_late_styles ) { $capture_late_styles(); print_footer_scripts(); }, $wp_print_footer_scripts_priority ); } // Replace placeholder with the captured late styles. add_filter( 'wp_template_enhancement_output_buffer', static function ( $buffer ) use ( $placeholder, &$style_handles_at_enqueue_block_assets, &$printed_core_block_styles, &$printed_other_block_styles, &$printed_global_styles, &$printed_late_styles ) { // Anonymous subclass of WP_HTML_Tag_Processor which exposes underlying bookmark spans. $processor = new class( $buffer ) extends WP_HTML_Tag_Processor { /** * Gets the span for the current token. * * @return WP_HTML_Span Current token span. */ private function get_span(): WP_HTML_Span { // Note: This call will never fail according to the usage of this class, given it is always called after ::next_tag() is true. $this->set_bookmark( 'here' ); return $this->bookmarks['here']; } /** * Inserts text before the current token. * * @param string $text Text to insert. */ public function insert_before( string $text ) { $this->lexical_updates[] = new WP_HTML_Text_Replacement( $this->get_span()->start, 0, $text ); } /** * Inserts text after the current token. * * @param string $text Text to insert. */ public function insert_after( string $text ) { $span = $this->get_span(); $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start + $span->length, 0, $text ); } /** * Removes the current token. */ public function remove() { $span = $this->get_span(); $this->lexical_updates[] = new WP_HTML_Text_Replacement( $span->start, $span->length, '' ); } }; // Locate the insertion points in the HEAD. while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) { if ( 'STYLE' === $processor->get_tag() && 'wp-block-library-inline-css' === $processor->get_attribute( 'id' ) ) { $processor->set_bookmark( 'wp_block_library' ); } elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) { $processor->set_bookmark( 'head_end' ); break; } elseif ( ( 'STYLE' === $processor->get_tag() || 'LINK' === $processor->get_tag() ) && $processor->get_attribute( 'id' ) ) { $id = $processor->get_attribute( 'id' ); $handle = null; if ( 'STYLE' === $processor->get_tag() ) { if ( preg_match( '/^(.+)-inline-css$/', $id, $matches ) ) { $handle = $matches[1]; } } elseif ( preg_match( '/^(.+)-css$/', $id, $matches ) ) { $handle = $matches[1]; } if ( 'classic-theme-styles' === $handle ) { $processor->set_bookmark( 'classic_theme_styles' ); } if ( $handle && in_array( $handle, $style_handles_at_enqueue_block_assets, true ) ) { if ( ! $processor->has_bookmark( 'first_style_at_enqueue_block_assets' ) ) { $processor->set_bookmark( 'first_style_at_enqueue_block_assets' ); } $processor->set_bookmark( 'last_style_at_enqueue_block_assets' ); } } } /* * Insert block styles right after wp-block-library (if it is present). The placeholder CSS comment will * always be added to the wp-block-library inline style since it gets printed at `wp_head` before the blocks * are rendered. This means that there may not actually be any block styles to hoist from the footer to * insert after this inline style. The placeholder CSS comment needs to be added so that the inline style * gets printed, but if the resulting inline style is empty after the placeholder is removed, then the * inline style is removed. */ if ( $processor->has_bookmark( 'wp_block_library' ) ) { $processor->seek( 'wp_block_library' ); $css_text = $processor->get_modifiable_text(); /* * A placeholder CSS comment is added to the inline style in order to force an inline STYLE tag to * be printed. Now that we've located the inline style, the placeholder comment can be removed. If * there is no CSS left in the STYLE tag after removing the placeholder (aside from the sourceURL * comment), then remove the STYLE entirely. */ $css_text = str_replace( $placeholder, '', $css_text ); if ( preg_match( ':^/\*# sourceURL=\S+? \*/$:', trim( $css_text ) ) ) { $processor->remove(); } else { $processor->set_modifiable_text( $css_text ); } $inserted_after = $printed_core_block_styles; $printed_core_block_styles = ''; // If the classic-theme-styles is absent, then the third-party block styles cannot be inserted after it, so they get inserted here. if ( ! $processor->has_bookmark( 'classic_theme_styles' ) ) { if ( '' !== $printed_other_block_styles ) { $inserted_after .= $printed_other_block_styles; } $printed_other_block_styles = ''; // If there aren't any other styles printed at enqueue_block_assets either, then the global styles need to also be printed here. if ( ! $processor->has_bookmark( 'last_style_at_enqueue_block_assets' ) ) { if ( '' !== $printed_global_styles ) { $inserted_after .= $printed_global_styles; } $printed_global_styles = ''; } } if ( '' !== $inserted_after ) { $processor->insert_after( "\n" . $inserted_after ); } } // Insert global-styles after the styles enqueued at enqueue_block_assets. if ( '' !== $printed_global_styles && $processor->has_bookmark( 'last_style_at_enqueue_block_assets' ) ) { $processor->seek( 'last_style_at_enqueue_block_assets' ); $processor->insert_after( "\n" . $printed_global_styles ); $printed_global_styles = ''; if ( ! $processor->has_bookmark( 'classic_theme_styles' ) && '' !== $printed_other_block_styles ) { $processor->insert_after( "\n" . $printed_other_block_styles ); $printed_other_block_styles = ''; } } // Insert third-party block styles right after the classic-theme-styles. if ( '' !== $printed_other_block_styles && $processor->has_bookmark( 'classic_theme_styles' ) ) { $processor->seek( 'classic_theme_styles' ); $processor->insert_after( "\n" . $printed_other_block_styles ); $printed_other_block_styles = ''; } // Print all remaining styles. $remaining_styles = $printed_core_block_styles . $printed_other_block_styles . $printed_global_styles . $printed_late_styles; if ( $remaining_styles && $processor->has_bookmark( 'head_end' ) ) { $processor->seek( 'head_end' ); $processor->insert_before( $remaining_styles . "\n" ); } return $processor->get_updated_html(); } ); } /** * Return the corresponding JavaScript `dataset` name for an attribute * if it represents a custom data attribute, or `null` if not. * * Custom data attributes appear in an element's `dataset` property in a * browser, but there's a specific way the names are translated from HTML * into JavaScript. This function indicates how the name would appear in * JavaScript if a browser would recognize it as a custom data attribute. * * Example: * * // Dash-letter pairs turn into capital letters. * 'postId' === wp_js_dataset_name( 'data-post-id' ); * 'Before' === wp_js_dataset_name( 'data--before' ); * '-One--Two---' === wp_js_dataset_name( 'data---one---two---' ); * * // Not every attribute name will be interpreted as a custom data attribute. * null === wp_js_dataset_name( 'post-id' ); * null === wp_js_dataset_name( 'data' ); * * // Some very surprising names will; for example, a property whose name is the empty string. * '' === wp_js_dataset_name( 'data-' ); * 0 === strlen( wp_js_dataset_name( 'data-' ) ); * * @since 6.9.0 * * @see https://html.spec.whatwg.org/#concept-domstringmap-pairs * @see \wp_html_custom_data_attribute_name() * * @param string $html_attribute_name Raw attribute name as found in the source HTML. * @return string|null Transformed `dataset` name, if interpretable as a custom data attribute, else `null`. */ function wp_js_dataset_name( string $html_attribute_name ): ?string { if ( 0 !== substr_compare( $html_attribute_name, 'data-', 0, 5, true ) ) { return null; } $end = strlen( $html_attribute_name ); /* * If it contains characters which would end the attribute name parsing then * something else is wrong and this contains more than just an attribute name. */ if ( ( $end - 5 ) !== strcspn( $html_attribute_name, "=/> \t\f\r\n", 5 ) ) { return null; } /** * > For each name in list, for each U+002D HYPHEN-MINUS character (-) * > in the name that is followed by an ASCII lower alpha, remove the * > U+002D HYPHEN-MINUS character (-) and replace the character that * > followed it by the same character converted to ASCII uppercase. * * @see https://html.spec.whatwg.org/#concept-domstringmap-pairs */ $custom_name = ''; $at = 5; $was_at = $at; while ( $at < $end ) { $next_dash_at = strpos( $html_attribute_name, '-', $at ); if ( false === $next_dash_at || $next_dash_at === $end - 1 ) { break; } // Transform `-a` to `A`, for example. $c = $html_attribute_name[ $next_dash_at + 1 ]; if ( ( $c >= 'A' && $c <= 'Z' ) || ( $c >= 'a' && $c <= 'z' ) ) { $prefix = substr( $html_attribute_name, $was_at, $next_dash_at - $was_at ); $custom_name .= strtolower( $prefix ); $custom_name .= strtoupper( $c ); $at = $next_dash_at + 2; $was_at = $at; continue; } $at = $next_dash_at + 1; } // 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. * * Example: * * '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( '' ); * * @since 6.9.0 * * @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 ( 0 === $end ) { return 'data-'; } /* * 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 ) { return null; } $html_name = 'data-'; $at = 0; $was_at = $at; while ( $at < $end ) { $next_upper_after = strcspn( $js_dataset_name, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', $at ); $next_upper_at = $at + $next_upper_after; if ( $next_upper_at >= $end ) { break; } $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; $was_at = $at; } if ( $was_at < $end ) { $html_name .= strtolower( substr( $js_dataset_name, $was_at ) ); } return $html_name; }