'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.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 "<script>" and "</script>" 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.
* // Strips exact literal empty SCRIPT tags.
* $js = '<script>sayHello();</script>;
* 'sayHello();' === wp_remove_surrounding_empty_script_tags( $js );
* // Otherwise if anything is different it warns in the JS console.
* $js = '<script type="text/javascript">console.log( "hi" );</script>';
* 'console.error( ... )' === wp_remove_surrounding_empty_script_tags( $js );
* @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 );
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 ) );
$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' );
/* translators: %s: wp_remove_surrounding_empty_script_tags() */
__( 'Function %s used incorrectly in PHP.' ),
'wp_remove_surrounding_empty_script_tags()'
* Adds hooks to load block styles on demand in classic themes.
* @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() ) {
* 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() ) {
// 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() ) {
* 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() ) {
// 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.
* @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.
* Add a placeholder comment into the inline styles for wp-block-library, after which where 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:' ) );
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_block_styles = '';
$printed_late_styles = '';
$capture_late_styles = static function () use ( &$printed_block_styles, &$printed_late_styles ) {
// Gather the styles related to on-demand block enqueues.
$all_block_style_handles = array();
foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) {
foreach ( $block_type->style_handles as $style_handle ) {
$all_block_style_handles[] = $style_handle;
$all_block_style_handles = array_merge(
$all_block_style_handles,
'block-style-variation-styles',
'core-block-supports-duotone',
* First print all styles related to blocks which should 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_block_styles = array_values( array_intersect( $all_block_style_handles, wp_styles()->queue ) );
if ( count( $enqueued_block_styles ) > 0 ) {
wp_styles()->do_items( $enqueued_block_styles );
$printed_block_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).
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
* 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 );
remove_action( 'wp_print_footer_scripts', '_wp_footer_scripts', $wp_print_footer_scripts_priority );
'wp_print_footer_scripts',
static function () use ( $capture_late_styles ) {
$wp_print_footer_scripts_priority
// Replace placeholder with the captured late styles.
'wp_template_enhancement_output_buffer',
static function ( $buffer ) use ( $placeholder, &$printed_block_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, '' );
* Insert block styles right after wp-block-library (if it is present), and then insert any remaining styles
* at </head> (or else print everything there). 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
while ( $processor->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
'STYLE' === $processor->get_tag() &&
'wp-block-library-inline-css' === $processor->get_attribute( 'id' )
$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->set_modifiable_text( $css_text );
// Insert the $printed_late_styles immediately after the closing inline STYLE tag. This preserves the CSS cascade.
if ( '' !== $printed_block_styles ) {
$processor->insert_after( $printed_block_styles );
// Prevent printing them again at </head>.
$printed_block_styles = '';
// If there aren't any late styles, there's no need to continue to finding </head>.
if ( '' === $printed_late_styles ) {
} elseif ( 'HEAD' === $processor->get_tag() && $processor->is_tag_closer() ) {
$processor->insert_before( $printed_block_styles . $printed_late_styles );
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.
* // 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-' ) );
* @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 ) ) {
$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 ) ) {
* > 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
$next_dash_at = strpos( $html_attribute_name, '-', $at );
if ( false === $next_dash_at || $next_dash_at === $end - 1 ) {
// 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 );
// 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 ) );