Skip to content

Commit d3cd233

Browse files
Copilotsirreal
andcommitted
Implement script_data_{$handle} filter correctly - outputs JSON script tags separate from wp_localize_script
Co-authored-by: sirreal <[email protected]>
1 parent f4afe13 commit d3cd233

File tree

5 files changed

+243
-86
lines changed

5 files changed

+243
-86
lines changed

src/wp-admin/includes/admin-filters.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
add_action( 'admin_print_scripts', 'print_emoji_detection_script' );
6060
add_action( 'admin_print_scripts', 'print_head_scripts', 20 );
6161
add_action( 'admin_print_footer_scripts', '_wp_footer_scripts' );
62+
add_action( 'admin_print_footer_scripts', 'wp_print_script_data', 21 );
6263
add_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' );
6364
add_action( 'admin_print_styles', 'print_emoji_styles' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_emoji_styles().
6465
add_action( 'admin_print_styles', 'print_admin_styles', 20 );

src/wp-includes/class-wp-scripts.php

Lines changed: 116 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -634,23 +634,6 @@ public function localize( $handle, $object_name, $l10n ) {
634634
}
635635
}
636636

637-
/**
638-
* Filters data associated with a given script.
639-
*
640-
* The dynamic portion of the hook name, `$handle`, refers to the script handle.
641-
*
642-
* This filter allows developers to modify the data passed to a script via
643-
* wp_localize_script() before it is output. This is analogous to the
644-
* `script_module_data_{$module_id}` filter for script modules.
645-
*
646-
* @since 6.8.0
647-
*
648-
* @param array|string $l10n The data to be localized.
649-
* @param string $object_name The JavaScript object name.
650-
* @param string $handle The script handle.
651-
*/
652-
$l10n = apply_filters( "script_data_{$handle}", $l10n, $object_name, $handle );
653-
654637
$script = "var $object_name = " . wp_json_encode( $l10n, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) . ';';
655638

656639
if ( ! empty( $after ) ) {
@@ -1199,4 +1182,120 @@ protected function get_dependency_warning_message( $handle, $missing_dependency_
11991182
implode( ', ', $missing_dependency_handles )
12001183
);
12011184
}
1185+
1186+
/**
1187+
* Prints data associated with scripts.
1188+
*
1189+
* The data will be embedded in the page HTML and can be read by scripts on page load.
1190+
*
1191+
* Data can be associated with a script via the {@see "script_data_{$handle}"} filter.
1192+
*
1193+
* The data for a script will be serialized as JSON in a script tag with an ID of the
1194+
* form `wp-script-data-{$handle}`.
1195+
*
1196+
* @since 6.8.0
1197+
*/
1198+
public function print_script_data() {
1199+
$scripts = array();
1200+
1201+
// Collect all enqueued scripts and their dependencies.
1202+
foreach ( array_unique( $this->queue ) as $handle ) {
1203+
$scripts[ $handle ] = true;
1204+
}
1205+
1206+
foreach ( array_keys( $scripts ) as $handle ) {
1207+
/**
1208+
* Filters data associated with a given script.
1209+
*
1210+
* The dynamic portion of the hook name, `$handle`, refers to the script handle.
1211+
*
1212+
* Scripts may require data that is required for initialization or is essential
1213+
* to have immediately available on page load. These are suitable use cases for
1214+
* this data.
1215+
*
1216+
* This is best suited to pass essential data that must be available to the script for
1217+
* initialization or immediately on page load. It does not replace the REST API or
1218+
* fetching data from the client.
1219+
*
1220+
* Example:
1221+
*
1222+
* add_filter(
1223+
* 'script_data_my-script-handle',
1224+
* function ( array $data ): array {
1225+
* $data['myData'] = array(
1226+
* 'option' => get_option( 'my_option' ),
1227+
* );
1228+
* return $data;
1229+
* }
1230+
* );
1231+
*
1232+
* If the filter returns no data (an empty array), nothing will be embedded in the page.
1233+
*
1234+
* The data for a given script, if provided, will be JSON serialized in a script
1235+
* tag with an ID of the form `wp-script-data-{$handle}`.
1236+
*
1237+
* The data can be read on the client with a pattern like this:
1238+
*
1239+
* Example:
1240+
*
1241+
* const dataContainer = document.getElementById( 'wp-script-data-my-script-handle' );
1242+
* let data = {};
1243+
* if ( dataContainer ) {
1244+
* try {
1245+
* data = JSON.parse( dataContainer.textContent );
1246+
* } catch {}
1247+
* }
1248+
* initMyScriptWithData( data );
1249+
*
1250+
* @since 6.8.0
1251+
*
1252+
* @param array $data The data associated with the script.
1253+
*/
1254+
$data = apply_filters( "script_data_{$handle}", array() );
1255+
1256+
if ( is_array( $data ) && array() !== $data ) {
1257+
/*
1258+
* This data will be printed as JSON inside a script tag like this:
1259+
* <script type="application/json"></script>
1260+
*
1261+
* A script tag must be closed by a sequence beginning with `</`. It's impossible to
1262+
* close a script tag without using `<`. We ensure that `<` is escaped and `/` can
1263+
* remain unescaped, so `</script>` will be printed as `\u003C/script>`.
1264+
*
1265+
* - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E.
1266+
* - JSON_UNESCAPED_SLASHES: Don't escape /.
1267+
*
1268+
* If the page will use UTF-8 encoding, it's safe to print unescaped unicode:
1269+
*
1270+
* - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`).
1271+
* - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when
1272+
* JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was
1273+
* before PHP 7.1 without this constant. Available as of PHP 7.1.0.
1274+
*
1275+
* The JSON specification requires encoding in UTF-8, so if the generated HTML page
1276+
* is not encoded in UTF-8 then it's not safe to include those literals. They must
1277+
* be escaped to avoid encoding issues.
1278+
*
1279+
* @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements.
1280+
* @see https://www.php.net/manual/en/json.constants.php for details on these constants.
1281+
* @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing.
1282+
*/
1283+
$json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS;
1284+
if ( ! is_utf8_charset() ) {
1285+
$json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES;
1286+
}
1287+
1288+
wp_print_inline_script_tag(
1289+
(string) wp_json_encode(
1290+
$data,
1291+
$json_encode_flags
1292+
),
1293+
array(
1294+
'type' => 'application/json',
1295+
'id' => "wp-script-data-{$handle}",
1296+
)
1297+
);
1298+
}
1299+
}
1300+
}
12021301
}

src/wp-includes/default-filters.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@
361361
add_action( 'wp_head', 'wp_site_icon', 99 );
362362
add_action( 'wp_footer', 'wp_print_speculation_rules' );
363363
add_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
364+
add_action( 'wp_footer', 'wp_print_script_data', 21 );
364365
add_action( 'template_redirect', 'wp_shortlink_header', 11, 0 );
365366
add_action( 'wp_print_footer_scripts', '_wp_footer_scripts' );
366367
add_action( 'init', '_register_core_block_patterns_and_categories' );

src/wp-includes/functions.wp-scripts.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,3 +450,20 @@ function wp_script_is( $handle, $status = 'enqueued' ) {
450450
function wp_script_add_data( $handle, $key, $value ) {
451451
return wp_scripts()->add_data( $handle, $key, $value );
452452
}
453+
454+
/**
455+
* Prints data associated with enqueued scripts.
456+
*
457+
* @since 6.8.0
458+
*
459+
* @see WP_Scripts::print_script_data()
460+
*/
461+
function wp_print_script_data() {
462+
global $wp_scripts;
463+
464+
if ( ! ( $wp_scripts instanceof WP_Scripts ) ) {
465+
return;
466+
}
467+
468+
$wp_scripts->print_script_data();
469+
}

0 commit comments

Comments
 (0)