From 154ed405124a3da1259a63d7e658fabb094a1364 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 19 Aug 2025 12:19:01 -0700 Subject: [PATCH 01/12] Use script module for emoji-loader with settings exported via JSON --- src/js/_enqueues/lib/emoji-loader.js | 5 +++++ src/wp-includes/formatting.php | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index 1cee787acac00..7ad42aace09d4 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -14,6 +14,11 @@ * @property {?Function} readyCallback */ +// For compatibility with other scripts that read from this global. +window._wpemojiSettings = /** @type {WPEmojiSettings} */ ( + JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) +); + /** * Support tests. * @typedef SupportTests diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 7f69321a7199c..8df1a888001d8 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -6137,8 +6137,18 @@ function _print_emoji_detection_script() { } wp_print_inline_script_tag( - sprintf( 'window._wpemojiSettings = %s;', wp_json_encode( $settings ) ) . "\n" . - file_get_contents( ABSPATH . WPINC . '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js' ) + wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), + array( + 'id' => 'wp-emoji-settings', + 'type' => 'application/json', + ) + ); + + wp_print_inline_script_tag( + file_get_contents( ABSPATH . WPINC . '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js' ), + array( + 'type' => 'module', + ) ); } From c41608f956db60107502a07f51b6f79c1b2a9a3c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 19 Aug 2025 13:41:54 -0700 Subject: [PATCH 02/12] Update emoji tests to account for new JSON encoding --- tests/phpunit/tests/formatting/emoji.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index 9377fb9419eaf..b2d40f2ffd309 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -19,8 +19,8 @@ public function test_unfiltered_emoji_cdns() { self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); $output = get_echo( '_print_emoji_detection_script' ); - $this->assertStringContainsString( wp_json_encode( $this->png_cdn ), $output ); - $this->assertStringContainsString( wp_json_encode( $this->svn_cdn ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); } public function _filtered_emoji_svn_cdn( $cdn = '' ) { @@ -41,9 +41,9 @@ public function test_filtered_emoji_svn_cdn() { self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); $output = get_echo( '_print_emoji_detection_script' ); - $this->assertStringContainsString( wp_json_encode( $this->png_cdn ), $output ); - $this->assertStringNotContainsString( wp_json_encode( $this->svn_cdn ), $output ); - $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringNotContainsString( wp_json_encode( $this->svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); remove_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svn_cdn' ) ); } @@ -66,9 +66,9 @@ public function test_filtered_emoji_png_cdn() { self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); $output = get_echo( '_print_emoji_detection_script' ); - $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn ), $output ); - $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn ), $output ); - $this->assertStringContainsString( wp_json_encode( $this->svn_cdn ), $output ); + $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); remove_filter( 'emoji_url', array( $this, '_filtered_emoji_png_cdn' ) ); } From 97bb8f2535bce93b213bdb3710bfd387f5981e80 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Sep 2025 21:25:33 -0700 Subject: [PATCH 03/12] Add test for printing SCRIPT tags for emoji --- tests/phpunit/tests/formatting/emoji.php | 45 +++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index b2d40f2ffd309..c1a0f24765ba2 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -7,7 +7,44 @@ class Tests_Formatting_Emoji extends WP_UnitTestCase { private $png_cdn = 'https://s.w.org/images/core/emoji/16.0.1/72x72/'; - private $svn_cdn = 'https://s.w.org/images/core/emoji/16.0.1/svg/'; + private $svg_cdn = 'https://s.w.org/images/core/emoji/16.0.1/svg/'; + + /** + * @ticket 63842 + * + * @covers ::_print_emoji_detection_script + */ + public function test_script_tag_printing() { + // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: + self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); + $output = get_echo( '_print_emoji_detection_script' ); + + $processor = new WP_HTML_Tag_Processor( $output ); + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SCRIPT', $processor->get_tag() ); + $this->assertSame( 'wp-emoji-settings', $processor->get_attribute( 'id' ) ); + $this->assertSame( 'application/json', $processor->get_attribute( 'type' ) ); + $text = $processor->get_modifiable_text(); + $settings = json_decode( $text, true ); + $this->assertIsArray( $settings ); + + $this->assertEqualSets( + array( 'baseUrl', 'ext', 'svgUrl', 'svgExt', 'source' ), + array_keys( $settings ) + ); + $this->assertSame( $this->png_cdn, $settings['baseUrl'] ); + $this->assertSame( '.png', $settings['ext'] ); + $this->assertSame( $this->svg_cdn, $settings['svgUrl'] ); + $this->assertSame( '.svg', $settings['svgExt'] ); + $this->assertIsArray( $settings['source'] ); + $this->assertArrayHasKey( 'wpemoji', $settings['source'] ); + $this->assertArrayHasKey( 'twemoji', $settings['source'] ); + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SCRIPT', $processor->get_tag() ); + $this->assertSame( 'module', $processor->get_attribute( 'type' ) ); + $this->assertNull( $processor->get_attribute( 'src' ) ); + $this->assertFalse( $processor->next_tag() ); + } /** * @ticket 36525 @@ -20,7 +57,7 @@ public function test_unfiltered_emoji_cdns() { $output = get_echo( '_print_emoji_detection_script' ); $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); - $this->assertStringContainsString( wp_json_encode( $this->svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); } public function _filtered_emoji_svn_cdn( $cdn = '' ) { @@ -42,7 +79,7 @@ public function test_filtered_emoji_svn_cdn() { $output = get_echo( '_print_emoji_detection_script' ); $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); - $this->assertStringNotContainsString( wp_json_encode( $this->svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringNotContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); remove_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svn_cdn' ) ); @@ -68,7 +105,7 @@ public function test_filtered_emoji_png_cdn() { $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); - $this->assertStringContainsString( wp_json_encode( $this->svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); remove_filter( 'emoji_url', array( $this, '_filtered_emoji_png_cdn' ) ); } From 2bc812d22cfef7cd2b321255259dd88ccd9478ca Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Oct 2025 12:12:30 -0700 Subject: [PATCH 04/12] Add sourceURL comment Co-authored-by: Jon Surrell --- src/wp-includes/formatting.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index cbdb6bba0a199..44a7e6b085c76 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5985,8 +5985,10 @@ function _print_emoji_detection_script() { ) ); + $emoji_loader_script_path = '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js'; wp_print_inline_script_tag( - file_get_contents( ABSPATH . WPINC . '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js' ), + rtrim( file_get_contents( ABSPATH . WPINC . $emoji_loader_script_path ) ) . "\n" . + '//# sourceURL=' . includes_url( $emoji_loader_script_path ), array( 'type' => 'module', ) From ad2fcfb78090db6b7551e7c79a319a5d9b0bdc3c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Oct 2025 12:17:07 -0700 Subject: [PATCH 05/12] Remove IIFE since now part of module Co-authored-by: Jon Surrell --- src/js/_enqueues/lib/emoji-loader.js | 793 +++++++++++++-------------- 1 file changed, 392 insertions(+), 401 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index 7ad42aace09d4..78a46161d23a8 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -2,6 +2,8 @@ * @output wp-includes/js/wp-emoji-loader.js */ +// Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope. + /** * Emoji Settings as exported in PHP via _print_emoji_detection_script(). * @typedef WPEmojiSettings @@ -14,11 +16,13 @@ * @property {?Function} readyCallback */ -// For compatibility with other scripts that read from this global. -window._wpemojiSettings = /** @type {WPEmojiSettings} */ ( +const settings = /** @type {WPEmojiSettings} */ ( JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) ); +// For compatibility with other scripts that read from this global. +window._wpemojiSettings = settings; + /** * Support tests. * @typedef SupportTests @@ -27,438 +31,425 @@ window._wpemojiSettings = /** @type {WPEmojiSettings} */ ( * @property {?boolean} emoji */ +var sessionStorageKey = 'wpEmojiSettingsSupports'; +var tests = [ 'flag', 'emoji' ]; + /** - * IIFE to detect emoji support and load Twemoji if needed. + * Checks whether the browser supports offloading to a Worker. * - * @param {Window} window - * @param {Document} document - * @param {WPEmojiSettings} settings + * @since 6.3.0 + * + * @private + * + * @returns {boolean} */ -( function wpEmojiLoader( window, document, settings ) { - if ( typeof Promise === 'undefined' ) { - return; - } +function supportsWorkerOffloading() { + return ( + typeof Worker !== 'undefined' && + typeof OffscreenCanvas !== 'undefined' && + typeof URL !== 'undefined' && + URL.createObjectURL && + typeof Blob !== 'undefined' + ); +} - var sessionStorageKey = 'wpEmojiSettingsSupports'; - var tests = [ 'flag', 'emoji' ]; - - /** - * Checks whether the browser supports offloading to a Worker. - * - * @since 6.3.0 - * - * @private - * - * @returns {boolean} - */ - function supportsWorkerOffloading() { - return ( - typeof Worker !== 'undefined' && - typeof OffscreenCanvas !== 'undefined' && - typeof URL !== 'undefined' && - URL.createObjectURL && - typeof Blob !== 'undefined' - ); - } +/** + * @typedef SessionSupportTests + * @type {object} + * @property {number} timestamp + * @property {SupportTests} supportTests + */ - /** - * @typedef SessionSupportTests - * @type {object} - * @property {number} timestamp - * @property {SupportTests} supportTests - */ +/** + * Get support tests from session. + * + * @since 6.3.0 + * + * @private + * + * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. + */ +function getSessionSupportTests() { + try { + /** @type {SessionSupportTests} */ + var item = JSON.parse( + sessionStorage.getItem( sessionStorageKey ) + ); + if ( + typeof item === 'object' && + typeof item.timestamp === 'number' && + new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. + typeof item.supportTests === 'object' + ) { + return item.supportTests; + } + } catch ( e ) {} + return null; +} - /** - * Get support tests from session. - * - * @since 6.3.0 - * - * @private - * - * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. - */ - function getSessionSupportTests() { - try { - /** @type {SessionSupportTests} */ - var item = JSON.parse( - sessionStorage.getItem( sessionStorageKey ) - ); - if ( - typeof item === 'object' && - typeof item.timestamp === 'number' && - new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. - typeof item.supportTests === 'object' - ) { - return item.supportTests; - } - } catch ( e ) {} - return null; - } +/** + * Persist the supports in session storage. + * + * @since 6.3.0 + * + * @private + * + * @param {SupportTests} supportTests Support tests. + */ +function setSessionSupportTests( supportTests ) { + try { + /** @type {SessionSupportTests} */ + var item = { + supportTests: supportTests, + timestamp: new Date().valueOf() + }; + + sessionStorage.setItem( + sessionStorageKey, + JSON.stringify( item ) + ); + } catch ( e ) {} +} - /** - * Persist the supports in session storage. - * - * @since 6.3.0 - * - * @private - * - * @param {SupportTests} supportTests Support tests. - */ - function setSessionSupportTests( supportTests ) { - try { - /** @type {SessionSupportTests} */ - var item = { - supportTests: supportTests, - timestamp: new Date().valueOf() - }; +/** + * Checks if two sets of Emoji characters render the same visually. + * + * This is used to determine if the browser is rendering an emoji with multiple data points + * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji + * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser + * does not support the emoji correctly. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.9.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} set1 Set of Emoji to test. + * @param {string} set2 Set of Emoji to test. + * + * @return {boolean} True if the two sets render the same. + */ +function emojiSetsRenderIdentically( context, set1, set2 ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set1, 0, 0 ); + var rendered1 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); + + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set2, 0, 0 ); + var rendered2 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); + + return rendered1.every( function ( rendered2Data, index ) { + return rendered2Data === rendered2[ index ]; + } ); +} - sessionStorage.setItem( - sessionStorageKey, - JSON.stringify( item ) - ); - } catch ( e ) {} +/** + * Checks if the center point of a single emoji is empty. + * + * This is used to determine if the browser is rendering an emoji with a single data point + * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly + * rendered emoji will have a non-zero value at the center point. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.8.2 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} emoji Emoji to test. + * + * @return {boolean} True if the center point is empty. + */ +function emojiRendersEmptyCenterPoint( context, emoji ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( emoji, 0, 0 ); + + // Test if the center point (16, 16) is empty (0,0,0,0). + var centerPoint = context.getImageData(16, 16, 1, 1); + for ( var i = 0; i < centerPoint.data.length; i++ ) { + if ( centerPoint.data[ i ] !== 0 ) { + // Stop checking the moment it's known not to be empty. + return false; + } } - /** - * Checks if two sets of Emoji characters render the same visually. - * - * This is used to determine if the browser is rendering an emoji with multiple data points - * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji - * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser - * does not support the emoji correctly. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 4.9.0 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} set1 Set of Emoji to test. - * @param {string} set2 Set of Emoji to test. - * - * @return {boolean} True if the two sets render the same. - */ - function emojiSetsRenderIdentically( context, set1, set2 ) { - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( set1, 0, 0 ); - var rendered1 = new Uint32Array( - context.getImageData( - 0, - 0, - context.canvas.width, - context.canvas.height - ).data - ); + return true; +} - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( set2, 0, 0 ); - var rendered2 = new Uint32Array( - context.getImageData( - 0, - 0, - context.canvas.width, - context.canvas.height - ).data - ); +/** + * Determines if the browser properly renders Emoji that Twemoji can supplement. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.2.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} type Whether to test for support of "flag" or "emoji". + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {boolean} True if the browser can render emoji, false if it cannot. + */ +function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + var isIdentical; - return rendered1.every( function ( rendered2Data, index ) { - return rendered2Data === rendered2[ index ]; - } ); - } + switch ( type ) { + case 'flag': + /* + * Test for Transgender flag compatibility. Added in Unicode 13. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (white flag emoji + transgender symbol). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence + '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space + ); - /** - * Checks if the center point of a single emoji is empty. - * - * This is used to determine if the browser is rendering an emoji with a single data point - * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly - * rendered emoji will have a non-zero value at the center point. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 6.8.2 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} emoji Emoji to test. - * - * @return {boolean} True if the center point is empty. - */ - function emojiRendersEmptyCenterPoint( context, emoji ) { - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( emoji, 0, 0 ); - - // Test if the center point (16, 16) is empty (0,0,0,0). - var centerPoint = context.getImageData(16, 16, 1, 1); - for ( var i = 0; i < centerPoint.data.length; i++ ) { - if ( centerPoint.data[ i ] !== 0 ) { - // Stop checking the moment it's known not to be empty. + if ( isIdentical ) { return false; } - } - return true; - } + /* + * Test for Sark flag compatibility. This is the least supported of the letter locale flags, + * so gives us an easy test for full support. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly ([C] + [Q]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points + '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space + ); - /** - * Determines if the browser properly renders Emoji that Twemoji can supplement. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 4.2.0 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} type Whether to test for support of "flag" or "emoji". - * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. - * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. - * - * @return {boolean} True if the browser can render emoji, false if it cannot. - */ - function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - var isIdentical; - - switch ( type ) { - case 'flag': - /* - * Test for Transgender flag compatibility. Added in Unicode 13. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (white flag emoji + transgender symbol). - */ - isIdentical = emojiSetsRenderIdentically( - context, - '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence - '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space - ); - - if ( isIdentical ) { - return false; - } - - /* - * Test for Sark flag compatibility. This is the least supported of the letter locale flags, - * so gives us an easy test for full support. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly ([C] + [Q]). - */ - isIdentical = emojiSetsRenderIdentically( - context, - '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points - '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space - ); - - if ( isIdentical ) { - return false; - } - - /* - * Test for English flag compatibility. England is a country in the United Kingdom, it - * does not have a two letter locale code but rather a five letter sub-division code. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). - */ - isIdentical = emojiSetsRenderIdentically( - context, - // as the flag sequence - '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', - // with each code point separated by a zero-width space - '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' - ); - - return ! isIdentical; - case 'emoji': - /* - * Does Emoji 16.0 cause the browser to go splat? - * - * To test for Emoji 16.0 support, try to render a new emoji: Splatter. - * - * The splatter emoji is a single code point emoji. Testing for browser support - * required testing the center point of the emoji to see if it is empty. - * - * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter. - * - * When updating this test, please ensure that the emoji is either a single code point - * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width - * joiner vs a zero-width space. - */ - var notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); - return ! notSupported; - } + if ( isIdentical ) { + return false; + } - return false; - } + /* + * Test for English flag compatibility. England is a country in the United Kingdom, it + * does not have a two letter locale code but rather a five letter sub-division code. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + // as the flag sequence + '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', + // with each code point separated by a zero-width space + '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' + ); - /** - * Checks emoji support tests. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 6.3.0 - * - * @private - * - * @param {string[]} tests Tests. - * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. - * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. - * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. - * - * @return {SupportTests} Support tests. - */ - function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - var canvas; - if ( - typeof WorkerGlobalScope !== 'undefined' && - self instanceof WorkerGlobalScope - ) { - canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. - } else { - canvas = document.createElement( 'canvas' ); - } + return ! isIdentical; + case 'emoji': + /* + * Does Emoji 16.0 cause the browser to go splat? + * + * To test for Emoji 16.0 support, try to render a new emoji: Splatter. + * + * The splatter emoji is a single code point emoji. Testing for browser support + * required testing the center point of the emoji to see if it is empty. + * + * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter. + * + * When updating this test, please ensure that the emoji is either a single code point + * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width + * joiner vs a zero-width space. + */ + var notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); + return ! notSupported; + } - var context = canvas.getContext( '2d', { willReadFrequently: true } ); + return false; +} - /* - * Chrome on OS X added native emoji rendering in M41. Unfortunately, - * it doesn't work when the font is bolder than 500 weight. So, we - * check for bold rendering support to avoid invisible emoji in Chrome. - */ - context.textBaseline = 'top'; - context.font = '600 32px Arial'; - - var supports = {}; - tests.forEach( function ( test ) { - supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); - } ); - return supports; +/** + * Checks emoji support tests. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.3.0 + * + * @private + * + * @param {string[]} tests Tests. + * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {SupportTests} Support tests. + */ +function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + var canvas; + if ( + typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope + ) { + canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. + } else { + canvas = document.createElement( 'canvas' ); } - /** - * Adds a script to the head of the document. - * - * @ignore - * - * @since 4.2.0 - * - * @param {string} src The url where the script is located. - * - * @return {void} + var context = canvas.getContext( '2d', { willReadFrequently: true } ); + + /* + * Chrome on OS X added native emoji rendering in M41. Unfortunately, + * it doesn't work when the font is bolder than 500 weight. So, we + * check for bold rendering support to avoid invisible emoji in Chrome. */ - function addScript( src ) { - var script = document.createElement( 'script' ); - script.src = src; - script.defer = true; - document.head.appendChild( script ); - } + context.textBaseline = 'top'; + context.font = '600 32px Arial'; - settings.supports = { - everything: true, - everythingExceptFlag: true - }; + var supports = {}; + tests.forEach( function ( test ) { + supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + } ); + return supports; +} - // Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired. - var domReadyPromise = new Promise( function ( resolve ) { - document.addEventListener( 'DOMContentLoaded', resolve, { - once: true - } ); +/** + * Adds a script to the head of the document. + * + * @ignore + * + * @since 4.2.0 + * + * @param {string} src The url where the script is located. + * + * @return {void} + */ +function addScript( src ) { + var script = document.createElement( 'script' ); + script.src = src; + script.defer = true; + document.head.appendChild( script ); +} + +settings.supports = { + everything: true, + everythingExceptFlag: true +}; + +// Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired. +var domReadyPromise = new Promise( function ( resolve ) { + document.addEventListener( 'DOMContentLoaded', resolve, { + once: true } ); +} ); - // Obtain the emoji support from the browser, asynchronously when possible. - new Promise( function ( resolve ) { - var supportTests = getSessionSupportTests(); - if ( supportTests ) { - resolve( supportTests ); - return; - } +// Obtain the emoji support from the browser, asynchronously when possible. +new Promise( function ( resolve ) { + var supportTests = getSessionSupportTests(); + if ( supportTests ) { + resolve( supportTests ); + return; + } - if ( supportsWorkerOffloading() ) { - try { - // Note that the functions are being passed as arguments due to minification. - var workerScript = - 'postMessage(' + - testEmojiSupports.toString() + - '(' + - [ - JSON.stringify( tests ), - browserSupportsEmoji.toString(), - emojiSetsRenderIdentically.toString(), - emojiRendersEmptyCenterPoint.toString() - ].join( ',' ) + - '));'; - var blob = new Blob( [ workerScript ], { - type: 'text/javascript' - } ); - var worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); - worker.onmessage = function ( event ) { - supportTests = event.data; - setSessionSupportTests( supportTests ); - worker.terminate(); - resolve( supportTests ); - }; - return; - } catch ( e ) {} - } + if ( supportsWorkerOffloading() ) { + try { + // Note that the functions are being passed as arguments due to minification. + var workerScript = + 'postMessage(' + + testEmojiSupports.toString() + + '(' + + [ + JSON.stringify( tests ), + browserSupportsEmoji.toString(), + emojiSetsRenderIdentically.toString(), + emojiRendersEmptyCenterPoint.toString() + ].join( ',' ) + + '));'; + var blob = new Blob( [ workerScript ], { + type: 'text/javascript' + } ); + var worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); + worker.onmessage = function ( event ) { + supportTests = event.data; + setSessionSupportTests( supportTests ); + worker.terminate(); + resolve( supportTests ); + }; + return; + } catch ( e ) {} + } - supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); - setSessionSupportTests( supportTests ); - resolve( supportTests ); - } ) - // Once the browser emoji support has been obtained from the session, finalize the settings. - .then( function ( supportTests ) { - /* - * Tests the browser support for flag emojis and other emojis, and adjusts the - * support settings accordingly. - */ - for ( var test in supportTests ) { - settings.supports[ test ] = supportTests[ test ]; + supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + setSessionSupportTests( supportTests ); + resolve( supportTests ); +} ) + // Once the browser emoji support has been obtained from the session, finalize the settings. + .then( function ( supportTests ) { + /* + * Tests the browser support for flag emojis and other emojis, and adjusts the + * support settings accordingly. + */ + for ( var test in supportTests ) { + settings.supports[ test ] = supportTests[ test ]; - settings.supports.everything = - settings.supports.everything && settings.supports[ test ]; + settings.supports.everything = + settings.supports.everything && settings.supports[ test ]; - if ( 'flag' !== test ) { - settings.supports.everythingExceptFlag = - settings.supports.everythingExceptFlag && - settings.supports[ test ]; - } + if ( 'flag' !== test ) { + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + settings.supports[ test ]; } + } - settings.supports.everythingExceptFlag = - settings.supports.everythingExceptFlag && - ! settings.supports.flag; + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + ! settings.supports.flag; - // Sets DOMReady to false and assigns a ready function to settings. - settings.DOMReady = false; - settings.readyCallback = function () { - settings.DOMReady = true; - }; - } ) - .then( function () { - return domReadyPromise; - } ) - .then( function () { - // When the browser can not render everything we need to load a polyfill. - if ( ! settings.supports.everything ) { - settings.readyCallback(); - - var src = settings.source || {}; - - if ( src.concatemoji ) { - addScript( src.concatemoji ); - } else if ( src.wpemoji && src.twemoji ) { - addScript( src.twemoji ); - addScript( src.wpemoji ); - } + // Sets DOMReady to false and assigns a ready function to settings. + settings.DOMReady = false; + settings.readyCallback = function () { + settings.DOMReady = true; + }; + } ) + .then( function () { + return domReadyPromise; + } ) + .then( function () { + // When the browser can not render everything we need to load a polyfill. + if ( ! settings.supports.everything ) { + settings.readyCallback(); + + var src = settings.source || {}; + + if ( src.concatemoji ) { + addScript( src.concatemoji ); + } else if ( src.wpemoji && src.twemoji ) { + addScript( src.twemoji ); + addScript( src.wpemoji ); } - } ); -} )( window, document, window._wpemojiSettings ); + } + } ); From e3278bad5daee9da1a2adf57f84a7397321861a9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Oct 2025 12:24:15 -0700 Subject: [PATCH 06/12] Use let and const --- src/js/_enqueues/lib/emoji-loader.js | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index 78a46161d23a8..ec46e1f59cbfc 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -31,8 +31,8 @@ window._wpemojiSettings = settings; * @property {?boolean} emoji */ -var sessionStorageKey = 'wpEmojiSettingsSupports'; -var tests = [ 'flag', 'emoji' ]; +const sessionStorageKey = 'wpEmojiSettingsSupports'; +const tests = [ 'flag', 'emoji' ]; /** * Checks whether the browser supports offloading to a Worker. @@ -72,7 +72,7 @@ function supportsWorkerOffloading() { function getSessionSupportTests() { try { /** @type {SessionSupportTests} */ - var item = JSON.parse( + const item = JSON.parse( sessionStorage.getItem( sessionStorageKey ) ); if ( @@ -99,7 +99,7 @@ function getSessionSupportTests() { function setSessionSupportTests( supportTests ) { try { /** @type {SessionSupportTests} */ - var item = { + const item = { supportTests: supportTests, timestamp: new Date().valueOf() }; @@ -136,7 +136,7 @@ function emojiSetsRenderIdentically( context, set1, set2 ) { // Cleanup from previous test. context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); context.fillText( set1, 0, 0 ); - var rendered1 = new Uint32Array( + const rendered1 = new Uint32Array( context.getImageData( 0, 0, @@ -148,7 +148,7 @@ function emojiSetsRenderIdentically( context, set1, set2 ) { // Cleanup from previous test. context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); context.fillText( set2, 0, 0 ); - var rendered2 = new Uint32Array( + const rendered2 = new Uint32Array( context.getImageData( 0, 0, @@ -187,8 +187,8 @@ function emojiRendersEmptyCenterPoint( context, emoji ) { context.fillText( emoji, 0, 0 ); // Test if the center point (16, 16) is empty (0,0,0,0). - var centerPoint = context.getImageData(16, 16, 1, 1); - for ( var i = 0; i < centerPoint.data.length; i++ ) { + const centerPoint = context.getImageData(16, 16, 1, 1); + for ( let i = 0; i < centerPoint.data.length; i++ ) { if ( centerPoint.data[ i ] !== 0 ) { // Stop checking the moment it's known not to be empty. return false; @@ -216,7 +216,7 @@ function emojiRendersEmptyCenterPoint( context, emoji ) { * @return {boolean} True if the browser can render emoji, false if it cannot. */ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - var isIdentical; + let isIdentical; switch ( type ) { case 'flag': @@ -284,7 +284,7 @@ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiR * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width * joiner vs a zero-width space. */ - var notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); + const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); return ! notSupported; } @@ -309,7 +309,7 @@ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiR * @return {SupportTests} Support tests. */ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - var canvas; + let canvas; if ( typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope @@ -319,7 +319,7 @@ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentica canvas = document.createElement( 'canvas' ); } - var context = canvas.getContext( '2d', { willReadFrequently: true } ); + const context = canvas.getContext( '2d', { willReadFrequently: true } ); /* * Chrome on OS X added native emoji rendering in M41. Unfortunately, @@ -329,7 +329,7 @@ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentica context.textBaseline = 'top'; context.font = '600 32px Arial'; - var supports = {}; + const supports = {}; tests.forEach( function ( test ) { supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); } ); @@ -348,7 +348,7 @@ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentica * @return {void} */ function addScript( src ) { - var script = document.createElement( 'script' ); + const script = document.createElement( 'script' ); script.src = src; script.defer = true; document.head.appendChild( script ); @@ -360,7 +360,7 @@ settings.supports = { }; // Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired. -var domReadyPromise = new Promise( function ( resolve ) { +const domReadyPromise = new Promise( function ( resolve ) { document.addEventListener( 'DOMContentLoaded', resolve, { once: true } ); @@ -368,7 +368,7 @@ var domReadyPromise = new Promise( function ( resolve ) { // Obtain the emoji support from the browser, asynchronously when possible. new Promise( function ( resolve ) { - var supportTests = getSessionSupportTests(); + let supportTests = getSessionSupportTests(); if ( supportTests ) { resolve( supportTests ); return; @@ -377,7 +377,7 @@ new Promise( function ( resolve ) { if ( supportsWorkerOffloading() ) { try { // Note that the functions are being passed as arguments due to minification. - var workerScript = + const workerScript = 'postMessage(' + testEmojiSupports.toString() + '(' + @@ -388,10 +388,10 @@ new Promise( function ( resolve ) { emojiRendersEmptyCenterPoint.toString() ].join( ',' ) + '));'; - var blob = new Blob( [ workerScript ], { + const blob = new Blob( [ workerScript ], { type: 'text/javascript' } ); - var worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); + const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); worker.onmessage = function ( event ) { supportTests = event.data; setSessionSupportTests( supportTests ); @@ -412,7 +412,7 @@ new Promise( function ( resolve ) { * Tests the browser support for flag emojis and other emojis, and adjusts the * support settings accordingly. */ - for ( var test in supportTests ) { + for ( const test in supportTests ) { settings.supports[ test ] = supportTests[ test ]; settings.supports.everything = @@ -443,7 +443,7 @@ new Promise( function ( resolve ) { if ( ! settings.supports.everything ) { settings.readyCallback(); - var src = settings.source || {}; + const src = settings.source || {}; if ( src.concatemoji ) { addScript( src.concatemoji ); From 1d2cdd05dd1a6efe16e330b1de67c08d4e7f5129 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Oct 2025 12:39:53 -0700 Subject: [PATCH 07/12] Use arrow functions instead of anonymous closures --- src/js/_enqueues/lib/emoji-loader.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index ec46e1f59cbfc..6cfbe5e31f264 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -157,7 +157,7 @@ function emojiSetsRenderIdentically( context, set1, set2 ) { ).data ); - return rendered1.every( function ( rendered2Data, index ) { + return rendered1.every( ( rendered2Data, index ) => { return rendered2Data === rendered2[ index ]; } ); } @@ -330,7 +330,7 @@ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentica context.font = '600 32px Arial'; const supports = {}; - tests.forEach( function ( test ) { + tests.forEach( ( test ) => { supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); } ); return supports; @@ -360,14 +360,14 @@ settings.supports = { }; // Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired. -const domReadyPromise = new Promise( function ( resolve ) { +const domReadyPromise = new Promise( ( resolve ) => { document.addEventListener( 'DOMContentLoaded', resolve, { once: true } ); } ); // Obtain the emoji support from the browser, asynchronously when possible. -new Promise( function ( resolve ) { +new Promise( ( resolve ) => { let supportTests = getSessionSupportTests(); if ( supportTests ) { resolve( supportTests ); @@ -392,7 +392,7 @@ new Promise( function ( resolve ) { type: 'text/javascript' } ); const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); - worker.onmessage = function ( event ) { + worker.onmessage = ( event ) => { supportTests = event.data; setSessionSupportTests( supportTests ); worker.terminate(); @@ -407,7 +407,7 @@ new Promise( function ( resolve ) { resolve( supportTests ); } ) // Once the browser emoji support has been obtained from the session, finalize the settings. - .then( function ( supportTests ) { + .then( ( supportTests ) => { /* * Tests the browser support for flag emojis and other emojis, and adjusts the * support settings accordingly. @@ -431,14 +431,14 @@ new Promise( function ( resolve ) { // Sets DOMReady to false and assigns a ready function to settings. settings.DOMReady = false; - settings.readyCallback = function () { + settings.readyCallback = () => { settings.DOMReady = true; }; } ) - .then( function () { + .then( () => { return domReadyPromise; } ) - .then( function () { + .then( () => { // When the browser can not render everything we need to load a polyfill. if ( ! settings.supports.everything ) { settings.readyCallback(); From 2792eade9610b39ed8a236c2574ad26e549b80e6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Oct 2025 12:55:05 -0700 Subject: [PATCH 08/12] Use async module for emoji loader --- src/js/_enqueues/lib/emoji-loader.js | 4 +++- src/wp-includes/formatting.php | 3 ++- tests/phpunit/tests/formatting/emoji.php | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index 6cfbe5e31f264..b14f2762bcd5a 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -359,7 +359,9 @@ settings.supports = { everythingExceptFlag: true }; -// Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired. +// Since this is part of an async module, the emoji test worker script can start running before the document has fully +// loaded and yet it may finish executing after the DOMContentLoaded event. So this promise is created here to ensure +// the readyCallback is always called at or after DCL. const domReadyPromise = new Promise( ( resolve ) => { document.addEventListener( 'DOMContentLoaded', resolve, { once: true diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index 44a7e6b085c76..f233439739b7e 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5990,7 +5990,8 @@ function _print_emoji_detection_script() { rtrim( file_get_contents( ABSPATH . WPINC . $emoji_loader_script_path ) ) . "\n" . '//# sourceURL=' . includes_url( $emoji_loader_script_path ), array( - 'type' => 'module', + 'type' => 'module', + 'async' => true, ) ); } diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index c1a0f24765ba2..1add501cfc787 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -42,6 +42,7 @@ public function test_script_tag_printing() { $this->assertTrue( $processor->next_tag() ); $this->assertSame( 'SCRIPT', $processor->get_tag() ); $this->assertSame( 'module', $processor->get_attribute( 'type' ) ); + $this->assertTrue( (bool) $processor->get_attribute( 'async' ) ); $this->assertNull( $processor->get_attribute( 'src' ) ); $this->assertFalse( $processor->next_tag() ); } From a0e9ae049f2f4702f1923ed187708bc4116700b9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 2 Oct 2025 22:24:39 -0700 Subject: [PATCH 09/12] Switch back to deferred script module and remove DCL Co-authored-by: Jon Surrell --- src/js/_enqueues/lib/emoji-loader.js | 12 ------------ src/wp-includes/formatting.php | 3 +-- tests/phpunit/tests/formatting/emoji.php | 1 - 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index b14f2762bcd5a..dfd5cd35438bf 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -359,15 +359,6 @@ settings.supports = { everythingExceptFlag: true }; -// Since this is part of an async module, the emoji test worker script can start running before the document has fully -// loaded and yet it may finish executing after the DOMContentLoaded event. So this promise is created here to ensure -// the readyCallback is always called at or after DCL. -const domReadyPromise = new Promise( ( resolve ) => { - document.addEventListener( 'DOMContentLoaded', resolve, { - once: true - } ); -} ); - // Obtain the emoji support from the browser, asynchronously when possible. new Promise( ( resolve ) => { let supportTests = getSessionSupportTests(); @@ -437,9 +428,6 @@ new Promise( ( resolve ) => { settings.DOMReady = true; }; } ) - .then( () => { - return domReadyPromise; - } ) .then( () => { // When the browser can not render everything we need to load a polyfill. if ( ! settings.supports.everything ) { diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index f233439739b7e..44a7e6b085c76 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5990,8 +5990,7 @@ function _print_emoji_detection_script() { rtrim( file_get_contents( ABSPATH . WPINC . $emoji_loader_script_path ) ) . "\n" . '//# sourceURL=' . includes_url( $emoji_loader_script_path ), array( - 'type' => 'module', - 'async' => true, + 'type' => 'module', ) ); } diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index 1add501cfc787..c1a0f24765ba2 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -42,7 +42,6 @@ public function test_script_tag_printing() { $this->assertTrue( $processor->next_tag() ); $this->assertSame( 'SCRIPT', $processor->get_tag() ); $this->assertSame( 'module', $processor->get_attribute( 'type' ) ); - $this->assertTrue( (bool) $processor->get_attribute( 'async' ) ); $this->assertNull( $processor->get_attribute( 'src' ) ); $this->assertFalse( $processor->next_tag() ); } From 88aea37361154ae56b22091e29323ae31bcc484f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Oct 2025 13:09:01 -0700 Subject: [PATCH 10/12] Wrap module in async IIFE --- src/js/_enqueues/lib/emoji-loader.js | 813 ++++++++++++++------------- 1 file changed, 409 insertions(+), 404 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index dfd5cd35438bf..73c0f3ca13227 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -3,443 +3,448 @@ */ // Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope. +// Nevertheless, an IIFE is still used for two reasons: (1) it allows for the use of await in browsers that don't +// support top-level await yet, and (2) it ensures all symbols can be minified where uglify is not aware. +( async () => { + + /** + * Emoji Settings as exported in PHP via _print_emoji_detection_script(). + * @typedef WPEmojiSettings + * @type {object} + * @property {?object} source + * @property {?string} source.concatemoji + * @property {?string} source.twemoji + * @property {?string} source.wpemoji + * @property {?boolean} DOMReady + * @property {?Function} readyCallback + */ -/** - * Emoji Settings as exported in PHP via _print_emoji_detection_script(). - * @typedef WPEmojiSettings - * @type {object} - * @property {?object} source - * @property {?string} source.concatemoji - * @property {?string} source.twemoji - * @property {?string} source.wpemoji - * @property {?boolean} DOMReady - * @property {?Function} readyCallback - */ - -const settings = /** @type {WPEmojiSettings} */ ( - JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) -); - -// For compatibility with other scripts that read from this global. -window._wpemojiSettings = settings; - -/** - * Support tests. - * @typedef SupportTests - * @type {object} - * @property {?boolean} flag - * @property {?boolean} emoji - */ - -const sessionStorageKey = 'wpEmojiSettingsSupports'; -const tests = [ 'flag', 'emoji' ]; - -/** - * Checks whether the browser supports offloading to a Worker. - * - * @since 6.3.0 - * - * @private - * - * @returns {boolean} - */ -function supportsWorkerOffloading() { - return ( - typeof Worker !== 'undefined' && - typeof OffscreenCanvas !== 'undefined' && - typeof URL !== 'undefined' && - URL.createObjectURL && - typeof Blob !== 'undefined' + const settings = /** @type {WPEmojiSettings} */ ( + JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) ); -} -/** - * @typedef SessionSupportTests - * @type {object} - * @property {number} timestamp - * @property {SupportTests} supportTests - */ + // For compatibility with other scripts that read from this global. + window._wpemojiSettings = settings; -/** - * Get support tests from session. - * - * @since 6.3.0 - * - * @private - * - * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. - */ -function getSessionSupportTests() { - try { - /** @type {SessionSupportTests} */ - const item = JSON.parse( - sessionStorage.getItem( sessionStorageKey ) - ); - if ( - typeof item === 'object' && - typeof item.timestamp === 'number' && - new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. - typeof item.supportTests === 'object' - ) { - return item.supportTests; - } - } catch ( e ) {} - return null; -} + /** + * Support tests. + * @typedef SupportTests + * @type {object} + * @property {?boolean} flag + * @property {?boolean} emoji + */ -/** - * Persist the supports in session storage. - * - * @since 6.3.0 - * - * @private - * - * @param {SupportTests} supportTests Support tests. - */ -function setSessionSupportTests( supportTests ) { - try { - /** @type {SessionSupportTests} */ - const item = { - supportTests: supportTests, - timestamp: new Date().valueOf() - }; - - sessionStorage.setItem( - sessionStorageKey, - JSON.stringify( item ) + const sessionStorageKey = 'wpEmojiSettingsSupports'; + const tests = [ 'flag', 'emoji' ]; + + /** + * Checks whether the browser supports offloading to a Worker. + * + * @since 6.3.0 + * + * @private + * + * @returns {boolean} + */ + function supportsWorkerOffloading() { + return ( + typeof Worker !== 'undefined' && + typeof OffscreenCanvas !== 'undefined' && + typeof URL !== 'undefined' && + URL.createObjectURL && + typeof Blob !== 'undefined' ); - } catch ( e ) {} -} - -/** - * Checks if two sets of Emoji characters render the same visually. - * - * This is used to determine if the browser is rendering an emoji with multiple data points - * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji - * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser - * does not support the emoji correctly. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 4.9.0 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} set1 Set of Emoji to test. - * @param {string} set2 Set of Emoji to test. - * - * @return {boolean} True if the two sets render the same. - */ -function emojiSetsRenderIdentically( context, set1, set2 ) { - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( set1, 0, 0 ); - const rendered1 = new Uint32Array( - context.getImageData( - 0, - 0, - context.canvas.width, - context.canvas.height - ).data - ); - - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( set2, 0, 0 ); - const rendered2 = new Uint32Array( - context.getImageData( - 0, - 0, - context.canvas.width, - context.canvas.height - ).data - ); + } - return rendered1.every( ( rendered2Data, index ) => { - return rendered2Data === rendered2[ index ]; - } ); -} + /** + * @typedef SessionSupportTests + * @type {object} + * @property {number} timestamp + * @property {SupportTests} supportTests + */ -/** - * Checks if the center point of a single emoji is empty. - * - * This is used to determine if the browser is rendering an emoji with a single data point - * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly - * rendered emoji will have a non-zero value at the center point. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 6.8.2 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} emoji Emoji to test. - * - * @return {boolean} True if the center point is empty. - */ -function emojiRendersEmptyCenterPoint( context, emoji ) { - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( emoji, 0, 0 ); - - // Test if the center point (16, 16) is empty (0,0,0,0). - const centerPoint = context.getImageData(16, 16, 1, 1); - for ( let i = 0; i < centerPoint.data.length; i++ ) { - if ( centerPoint.data[ i ] !== 0 ) { - // Stop checking the moment it's known not to be empty. - return false; - } + /** + * Get support tests from session. + * + * @since 6.3.0 + * + * @private + * + * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. + */ + function getSessionSupportTests() { + try { + /** @type {SessionSupportTests} */ + const item = JSON.parse( + sessionStorage.getItem( sessionStorageKey ) + ); + if ( + typeof item === 'object' && + typeof item.timestamp === 'number' && + new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. + typeof item.supportTests === 'object' + ) { + return item.supportTests; + } + } catch ( e ) {} + return null; } - return true; -} - -/** - * Determines if the browser properly renders Emoji that Twemoji can supplement. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 4.2.0 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} type Whether to test for support of "flag" or "emoji". - * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. - * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. - * - * @return {boolean} True if the browser can render emoji, false if it cannot. - */ -function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - let isIdentical; + /** + * Persist the supports in session storage. + * + * @since 6.3.0 + * + * @private + * + * @param {SupportTests} supportTests Support tests. + */ + function setSessionSupportTests( supportTests ) { + try { + /** @type {SessionSupportTests} */ + const item = { + supportTests: supportTests, + timestamp: new Date().valueOf() + }; - switch ( type ) { - case 'flag': - /* - * Test for Transgender flag compatibility. Added in Unicode 13. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (white flag emoji + transgender symbol). - */ - isIdentical = emojiSetsRenderIdentically( - context, - '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence - '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space + sessionStorage.setItem( + sessionStorageKey, + JSON.stringify( item ) ); + } catch ( e ) {} + } - if ( isIdentical ) { - return false; - } + /** + * Checks if two sets of Emoji characters render the same visually. + * + * This is used to determine if the browser is rendering an emoji with multiple data points + * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji + * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser + * does not support the emoji correctly. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.9.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} set1 Set of Emoji to test. + * @param {string} set2 Set of Emoji to test. + * + * @return {boolean} True if the two sets render the same. + */ + function emojiSetsRenderIdentically( context, set1, set2 ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set1, 0, 0 ); + const rendered1 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); - /* - * Test for Sark flag compatibility. This is the least supported of the letter locale flags, - * so gives us an easy test for full support. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly ([C] + [Q]). - */ - isIdentical = emojiSetsRenderIdentically( - context, - '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points - '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space - ); + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set2, 0, 0 ); + const rendered2 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); - if ( isIdentical ) { + return rendered1.every( ( rendered2Data, index ) => { + return rendered2Data === rendered2[ index ]; + } ); + } + + /** + * Checks if the center point of a single emoji is empty. + * + * This is used to determine if the browser is rendering an emoji with a single data point + * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly + * rendered emoji will have a non-zero value at the center point. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.8.2 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} emoji Emoji to test. + * + * @return {boolean} True if the center point is empty. + */ + function emojiRendersEmptyCenterPoint( context, emoji ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( emoji, 0, 0 ); + + // Test if the center point (16, 16) is empty (0,0,0,0). + const centerPoint = context.getImageData(16, 16, 1, 1); + for ( let i = 0; i < centerPoint.data.length; i++ ) { + if ( centerPoint.data[ i ] !== 0 ) { + // Stop checking the moment it's known not to be empty. return false; } + } - /* - * Test for English flag compatibility. England is a country in the United Kingdom, it - * does not have a two letter locale code but rather a five letter sub-division code. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). - */ - isIdentical = emojiSetsRenderIdentically( - context, - // as the flag sequence - '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', - // with each code point separated by a zero-width space - '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' - ); - - return ! isIdentical; - case 'emoji': - /* - * Does Emoji 16.0 cause the browser to go splat? - * - * To test for Emoji 16.0 support, try to render a new emoji: Splatter. - * - * The splatter emoji is a single code point emoji. Testing for browser support - * required testing the center point of the emoji to see if it is empty. - * - * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter. - * - * When updating this test, please ensure that the emoji is either a single code point - * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width - * joiner vs a zero-width space. - */ - const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); - return ! notSupported; + return true; } - return false; -} + /** + * Determines if the browser properly renders Emoji that Twemoji can supplement. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.2.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} type Whether to test for support of "flag" or "emoji". + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {boolean} True if the browser can render emoji, false if it cannot. + */ + function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + let isIdentical; + + switch ( type ) { + case 'flag': + /* + * Test for Transgender flag compatibility. Added in Unicode 13. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (white flag emoji + transgender symbol). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence + '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space + ); + + if ( isIdentical ) { + return false; + } + + /* + * Test for Sark flag compatibility. This is the least supported of the letter locale flags, + * so gives us an easy test for full support. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly ([C] + [Q]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points + '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space + ); + + if ( isIdentical ) { + return false; + } + + /* + * Test for English flag compatibility. England is a country in the United Kingdom, it + * does not have a two letter locale code but rather a five letter sub-division code. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + // as the flag sequence + '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', + // with each code point separated by a zero-width space + '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' + ); + + return ! isIdentical; + case 'emoji': + /* + * Does Emoji 16.0 cause the browser to go splat? + * + * To test for Emoji 16.0 support, try to render a new emoji: Splatter. + * + * The splatter emoji is a single code point emoji. Testing for browser support + * required testing the center point of the emoji to see if it is empty. + * + * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter. + * + * When updating this test, please ensure that the emoji is either a single code point + * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width + * joiner vs a zero-width space. + */ + const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); + return ! notSupported; + } -/** - * Checks emoji support tests. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 6.3.0 - * - * @private - * - * @param {string[]} tests Tests. - * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. - * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. - * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. - * - * @return {SupportTests} Support tests. - */ -function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - let canvas; - if ( - typeof WorkerGlobalScope !== 'undefined' && - self instanceof WorkerGlobalScope - ) { - canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. - } else { - canvas = document.createElement( 'canvas' ); + return false; } - const context = canvas.getContext( '2d', { willReadFrequently: true } ); - - /* - * Chrome on OS X added native emoji rendering in M41. Unfortunately, - * it doesn't work when the font is bolder than 500 weight. So, we - * check for bold rendering support to avoid invisible emoji in Chrome. + /** + * Checks emoji support tests. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.3.0 + * + * @private + * + * @param {string[]} tests Tests. + * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {SupportTests} Support tests. */ - context.textBaseline = 'top'; - context.font = '600 32px Arial'; + function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + let canvas; + if ( + typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope + ) { + canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. + } else { + canvas = document.createElement( 'canvas' ); + } - const supports = {}; - tests.forEach( ( test ) => { - supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); - } ); - return supports; -} + const context = canvas.getContext( '2d', { willReadFrequently: true } ); -/** - * Adds a script to the head of the document. - * - * @ignore - * - * @since 4.2.0 - * - * @param {string} src The url where the script is located. - * - * @return {void} - */ -function addScript( src ) { - const script = document.createElement( 'script' ); - script.src = src; - script.defer = true; - document.head.appendChild( script ); -} - -settings.supports = { - everything: true, - everythingExceptFlag: true -}; - -// Obtain the emoji support from the browser, asynchronously when possible. -new Promise( ( resolve ) => { - let supportTests = getSessionSupportTests(); - if ( supportTests ) { - resolve( supportTests ); - return; + /* + * Chrome on OS X added native emoji rendering in M41. Unfortunately, + * it doesn't work when the font is bolder than 500 weight. So, we + * check for bold rendering support to avoid invisible emoji in Chrome. + */ + context.textBaseline = 'top'; + context.font = '600 32px Arial'; + + const supports = {}; + tests.forEach( ( test ) => { + supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + } ); + return supports; } - if ( supportsWorkerOffloading() ) { - try { - // Note that the functions are being passed as arguments due to minification. - const workerScript = - 'postMessage(' + - testEmojiSupports.toString() + - '(' + - [ - JSON.stringify( tests ), - browserSupportsEmoji.toString(), - emojiSetsRenderIdentically.toString(), - emojiRendersEmptyCenterPoint.toString() - ].join( ',' ) + - '));'; - const blob = new Blob( [ workerScript ], { - type: 'text/javascript' - } ); - const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); - worker.onmessage = ( event ) => { - supportTests = event.data; - setSessionSupportTests( supportTests ); - worker.terminate(); - resolve( supportTests ); - }; - return; - } catch ( e ) {} + /** + * Adds a script to the head of the document. + * + * @ignore + * + * @since 4.2.0 + * + * @param {string} src The url where the script is located. + * + * @return {void} + */ + function addScript( src ) { + const script = document.createElement( 'script' ); + script.src = src; + script.defer = true; + document.head.appendChild( script ); } - supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); - setSessionSupportTests( supportTests ); - resolve( supportTests ); -} ) - // Once the browser emoji support has been obtained from the session, finalize the settings. - .then( ( supportTests ) => { - /* - * Tests the browser support for flag emojis and other emojis, and adjusts the - * support settings accordingly. - */ - for ( const test in supportTests ) { - settings.supports[ test ] = supportTests[ test ]; - - settings.supports.everything = - settings.supports.everything && settings.supports[ test ]; + settings.supports = { + everything: true, + everythingExceptFlag: true + }; - if ( 'flag' !== test ) { - settings.supports.everythingExceptFlag = - settings.supports.everythingExceptFlag && - settings.supports[ test ]; - } + // Obtain the emoji support from the browser, asynchronously when possible. + new Promise( ( resolve ) => { + let supportTests = getSessionSupportTests(); + if ( supportTests ) { + resolve( supportTests ); + return; } - settings.supports.everythingExceptFlag = - settings.supports.everythingExceptFlag && - ! settings.supports.flag; + if ( supportsWorkerOffloading() ) { + try { + // Note that the functions are being passed as arguments due to minification. + const workerScript = + 'postMessage(' + + testEmojiSupports.toString() + + '(' + + [ + JSON.stringify( tests ), + browserSupportsEmoji.toString(), + emojiSetsRenderIdentically.toString(), + emojiRendersEmptyCenterPoint.toString() + ].join( ',' ) + + '));'; + const blob = new Blob( [ workerScript ], { + type: 'text/javascript' + } ); + const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); + worker.onmessage = ( event ) => { + supportTests = event.data; + setSessionSupportTests( supportTests ); + worker.terminate(); + resolve( supportTests ); + }; + return; + } catch ( e ) {} + } - // Sets DOMReady to false and assigns a ready function to settings. - settings.DOMReady = false; - settings.readyCallback = () => { - settings.DOMReady = true; - }; + supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + setSessionSupportTests( supportTests ); + resolve( supportTests ); } ) - .then( () => { - // When the browser can not render everything we need to load a polyfill. - if ( ! settings.supports.everything ) { - settings.readyCallback(); - - const src = settings.source || {}; - - if ( src.concatemoji ) { - addScript( src.concatemoji ); - } else if ( src.wpemoji && src.twemoji ) { - addScript( src.twemoji ); - addScript( src.wpemoji ); + // Once the browser emoji support has been obtained from the session, finalize the settings. + .then( ( supportTests ) => { + /* + * Tests the browser support for flag emojis and other emojis, and adjusts the + * support settings accordingly. + */ + for ( const test in supportTests ) { + settings.supports[ test ] = supportTests[ test ]; + + settings.supports.everything = + settings.supports.everything && settings.supports[ test ]; + + if ( 'flag' !== test ) { + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + settings.supports[ test ]; + } } - } - } ); + + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + ! settings.supports.flag; + + // Sets DOMReady to false and assigns a ready function to settings. + settings.DOMReady = false; + settings.readyCallback = () => { + settings.DOMReady = true; + }; + } ) + .then( () => { + // When the browser can not render everything we need to load a polyfill. + if ( ! settings.supports.everything ) { + settings.readyCallback(); + + const src = settings.source || {}; + + if ( src.concatemoji ) { + addScript( src.concatemoji ); + } else if ( src.wpemoji && src.twemoji ) { + addScript( src.twemoji ); + addScript( src.wpemoji ); + } + } + } ); + +} )(); From 050fa6f3d87be2013e87b60c5de37285fab0e046 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Oct 2025 13:11:17 -0700 Subject: [PATCH 11/12] Revert "Wrap module in async IIFE" This reverts commit 88aea37361154ae56b22091e29323ae31bcc484f. --- src/js/_enqueues/lib/emoji-loader.js | 813 +++++++++++++-------------- 1 file changed, 404 insertions(+), 409 deletions(-) diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index 73c0f3ca13227..dfd5cd35438bf 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -3,448 +3,443 @@ */ // Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope. -// Nevertheless, an IIFE is still used for two reasons: (1) it allows for the use of await in browsers that don't -// support top-level await yet, and (2) it ensures all symbols can be minified where uglify is not aware. -( async () => { - - /** - * Emoji Settings as exported in PHP via _print_emoji_detection_script(). - * @typedef WPEmojiSettings - * @type {object} - * @property {?object} source - * @property {?string} source.concatemoji - * @property {?string} source.twemoji - * @property {?string} source.wpemoji - * @property {?boolean} DOMReady - * @property {?Function} readyCallback - */ - const settings = /** @type {WPEmojiSettings} */ ( - JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) +/** + * Emoji Settings as exported in PHP via _print_emoji_detection_script(). + * @typedef WPEmojiSettings + * @type {object} + * @property {?object} source + * @property {?string} source.concatemoji + * @property {?string} source.twemoji + * @property {?string} source.wpemoji + * @property {?boolean} DOMReady + * @property {?Function} readyCallback + */ + +const settings = /** @type {WPEmojiSettings} */ ( + JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) +); + +// For compatibility with other scripts that read from this global. +window._wpemojiSettings = settings; + +/** + * Support tests. + * @typedef SupportTests + * @type {object} + * @property {?boolean} flag + * @property {?boolean} emoji + */ + +const sessionStorageKey = 'wpEmojiSettingsSupports'; +const tests = [ 'flag', 'emoji' ]; + +/** + * Checks whether the browser supports offloading to a Worker. + * + * @since 6.3.0 + * + * @private + * + * @returns {boolean} + */ +function supportsWorkerOffloading() { + return ( + typeof Worker !== 'undefined' && + typeof OffscreenCanvas !== 'undefined' && + typeof URL !== 'undefined' && + URL.createObjectURL && + typeof Blob !== 'undefined' ); +} - // For compatibility with other scripts that read from this global. - window._wpemojiSettings = settings; +/** + * @typedef SessionSupportTests + * @type {object} + * @property {number} timestamp + * @property {SupportTests} supportTests + */ - /** - * Support tests. - * @typedef SupportTests - * @type {object} - * @property {?boolean} flag - * @property {?boolean} emoji - */ +/** + * Get support tests from session. + * + * @since 6.3.0 + * + * @private + * + * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. + */ +function getSessionSupportTests() { + try { + /** @type {SessionSupportTests} */ + const item = JSON.parse( + sessionStorage.getItem( sessionStorageKey ) + ); + if ( + typeof item === 'object' && + typeof item.timestamp === 'number' && + new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. + typeof item.supportTests === 'object' + ) { + return item.supportTests; + } + } catch ( e ) {} + return null; +} - const sessionStorageKey = 'wpEmojiSettingsSupports'; - const tests = [ 'flag', 'emoji' ]; - - /** - * Checks whether the browser supports offloading to a Worker. - * - * @since 6.3.0 - * - * @private - * - * @returns {boolean} - */ - function supportsWorkerOffloading() { - return ( - typeof Worker !== 'undefined' && - typeof OffscreenCanvas !== 'undefined' && - typeof URL !== 'undefined' && - URL.createObjectURL && - typeof Blob !== 'undefined' +/** + * Persist the supports in session storage. + * + * @since 6.3.0 + * + * @private + * + * @param {SupportTests} supportTests Support tests. + */ +function setSessionSupportTests( supportTests ) { + try { + /** @type {SessionSupportTests} */ + const item = { + supportTests: supportTests, + timestamp: new Date().valueOf() + }; + + sessionStorage.setItem( + sessionStorageKey, + JSON.stringify( item ) ); - } + } catch ( e ) {} +} - /** - * @typedef SessionSupportTests - * @type {object} - * @property {number} timestamp - * @property {SupportTests} supportTests - */ +/** + * Checks if two sets of Emoji characters render the same visually. + * + * This is used to determine if the browser is rendering an emoji with multiple data points + * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji + * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser + * does not support the emoji correctly. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.9.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} set1 Set of Emoji to test. + * @param {string} set2 Set of Emoji to test. + * + * @return {boolean} True if the two sets render the same. + */ +function emojiSetsRenderIdentically( context, set1, set2 ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set1, 0, 0 ); + const rendered1 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); - /** - * Get support tests from session. - * - * @since 6.3.0 - * - * @private - * - * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. - */ - function getSessionSupportTests() { - try { - /** @type {SessionSupportTests} */ - const item = JSON.parse( - sessionStorage.getItem( sessionStorageKey ) - ); - if ( - typeof item === 'object' && - typeof item.timestamp === 'number' && - new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. - typeof item.supportTests === 'object' - ) { - return item.supportTests; - } - } catch ( e ) {} - return null; - } + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set2, 0, 0 ); + const rendered2 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); - /** - * Persist the supports in session storage. - * - * @since 6.3.0 - * - * @private - * - * @param {SupportTests} supportTests Support tests. - */ - function setSessionSupportTests( supportTests ) { - try { - /** @type {SessionSupportTests} */ - const item = { - supportTests: supportTests, - timestamp: new Date().valueOf() - }; + return rendered1.every( ( rendered2Data, index ) => { + return rendered2Data === rendered2[ index ]; + } ); +} - sessionStorage.setItem( - sessionStorageKey, - JSON.stringify( item ) - ); - } catch ( e ) {} +/** + * Checks if the center point of a single emoji is empty. + * + * This is used to determine if the browser is rendering an emoji with a single data point + * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly + * rendered emoji will have a non-zero value at the center point. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.8.2 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} emoji Emoji to test. + * + * @return {boolean} True if the center point is empty. + */ +function emojiRendersEmptyCenterPoint( context, emoji ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( emoji, 0, 0 ); + + // Test if the center point (16, 16) is empty (0,0,0,0). + const centerPoint = context.getImageData(16, 16, 1, 1); + for ( let i = 0; i < centerPoint.data.length; i++ ) { + if ( centerPoint.data[ i ] !== 0 ) { + // Stop checking the moment it's known not to be empty. + return false; + } } - /** - * Checks if two sets of Emoji characters render the same visually. - * - * This is used to determine if the browser is rendering an emoji with multiple data points - * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji - * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser - * does not support the emoji correctly. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 4.9.0 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} set1 Set of Emoji to test. - * @param {string} set2 Set of Emoji to test. - * - * @return {boolean} True if the two sets render the same. - */ - function emojiSetsRenderIdentically( context, set1, set2 ) { - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( set1, 0, 0 ); - const rendered1 = new Uint32Array( - context.getImageData( - 0, - 0, - context.canvas.width, - context.canvas.height - ).data - ); + return true; +} - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( set2, 0, 0 ); - const rendered2 = new Uint32Array( - context.getImageData( - 0, - 0, - context.canvas.width, - context.canvas.height - ).data - ); +/** + * Determines if the browser properly renders Emoji that Twemoji can supplement. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.2.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} type Whether to test for support of "flag" or "emoji". + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {boolean} True if the browser can render emoji, false if it cannot. + */ +function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + let isIdentical; - return rendered1.every( ( rendered2Data, index ) => { - return rendered2Data === rendered2[ index ]; - } ); - } + switch ( type ) { + case 'flag': + /* + * Test for Transgender flag compatibility. Added in Unicode 13. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (white flag emoji + transgender symbol). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence + '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space + ); - /** - * Checks if the center point of a single emoji is empty. - * - * This is used to determine if the browser is rendering an emoji with a single data point - * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly - * rendered emoji will have a non-zero value at the center point. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 6.8.2 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} emoji Emoji to test. - * - * @return {boolean} True if the center point is empty. - */ - function emojiRendersEmptyCenterPoint( context, emoji ) { - // Cleanup from previous test. - context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); - context.fillText( emoji, 0, 0 ); - - // Test if the center point (16, 16) is empty (0,0,0,0). - const centerPoint = context.getImageData(16, 16, 1, 1); - for ( let i = 0; i < centerPoint.data.length; i++ ) { - if ( centerPoint.data[ i ] !== 0 ) { - // Stop checking the moment it's known not to be empty. + if ( isIdentical ) { return false; } - } - return true; - } + /* + * Test for Sark flag compatibility. This is the least supported of the letter locale flags, + * so gives us an easy test for full support. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly ([C] + [Q]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points + '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space + ); - /** - * Determines if the browser properly renders Emoji that Twemoji can supplement. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 4.2.0 - * - * @private - * - * @param {CanvasRenderingContext2D} context 2D Context. - * @param {string} type Whether to test for support of "flag" or "emoji". - * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. - * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. - * - * @return {boolean} True if the browser can render emoji, false if it cannot. - */ - function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - let isIdentical; - - switch ( type ) { - case 'flag': - /* - * Test for Transgender flag compatibility. Added in Unicode 13. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (white flag emoji + transgender symbol). - */ - isIdentical = emojiSetsRenderIdentically( - context, - '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence - '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space - ); - - if ( isIdentical ) { - return false; - } - - /* - * Test for Sark flag compatibility. This is the least supported of the letter locale flags, - * so gives us an easy test for full support. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly ([C] + [Q]). - */ - isIdentical = emojiSetsRenderIdentically( - context, - '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points - '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space - ); - - if ( isIdentical ) { - return false; - } - - /* - * Test for English flag compatibility. England is a country in the United Kingdom, it - * does not have a two letter locale code but rather a five letter sub-division code. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). - */ - isIdentical = emojiSetsRenderIdentically( - context, - // as the flag sequence - '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', - // with each code point separated by a zero-width space - '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' - ); - - return ! isIdentical; - case 'emoji': - /* - * Does Emoji 16.0 cause the browser to go splat? - * - * To test for Emoji 16.0 support, try to render a new emoji: Splatter. - * - * The splatter emoji is a single code point emoji. Testing for browser support - * required testing the center point of the emoji to see if it is empty. - * - * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter. - * - * When updating this test, please ensure that the emoji is either a single code point - * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width - * joiner vs a zero-width space. - */ - const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); - return ! notSupported; - } + if ( isIdentical ) { + return false; + } - return false; - } + /* + * Test for English flag compatibility. England is a country in the United Kingdom, it + * does not have a two letter locale code but rather a five letter sub-division code. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + // as the flag sequence + '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', + // with each code point separated by a zero-width space + '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' + ); - /** - * Checks emoji support tests. - * - * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing - * scope. Everything must be passed by parameters. - * - * @since 6.3.0 - * - * @private - * - * @param {string[]} tests Tests. - * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. - * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. - * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. - * - * @return {SupportTests} Support tests. - */ - function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { - let canvas; - if ( - typeof WorkerGlobalScope !== 'undefined' && - self instanceof WorkerGlobalScope - ) { - canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. - } else { - canvas = document.createElement( 'canvas' ); - } + return ! isIdentical; + case 'emoji': + /* + * Does Emoji 16.0 cause the browser to go splat? + * + * To test for Emoji 16.0 support, try to render a new emoji: Splatter. + * + * The splatter emoji is a single code point emoji. Testing for browser support + * required testing the center point of the emoji to see if it is empty. + * + * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter. + * + * When updating this test, please ensure that the emoji is either a single code point + * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width + * joiner vs a zero-width space. + */ + const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' ); + return ! notSupported; + } - const context = canvas.getContext( '2d', { willReadFrequently: true } ); + return false; +} - /* - * Chrome on OS X added native emoji rendering in M41. Unfortunately, - * it doesn't work when the font is bolder than 500 weight. So, we - * check for bold rendering support to avoid invisible emoji in Chrome. - */ - context.textBaseline = 'top'; - context.font = '600 32px Arial'; - - const supports = {}; - tests.forEach( ( test ) => { - supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); - } ); - return supports; +/** + * Checks emoji support tests. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.3.0 + * + * @private + * + * @param {string[]} tests Tests. + * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {SupportTests} Support tests. + */ +function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + let canvas; + if ( + typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope + ) { + canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. + } else { + canvas = document.createElement( 'canvas' ); } - /** - * Adds a script to the head of the document. - * - * @ignore - * - * @since 4.2.0 - * - * @param {string} src The url where the script is located. - * - * @return {void} + const context = canvas.getContext( '2d', { willReadFrequently: true } ); + + /* + * Chrome on OS X added native emoji rendering in M41. Unfortunately, + * it doesn't work when the font is bolder than 500 weight. So, we + * check for bold rendering support to avoid invisible emoji in Chrome. */ - function addScript( src ) { - const script = document.createElement( 'script' ); - script.src = src; - script.defer = true; - document.head.appendChild( script ); - } + context.textBaseline = 'top'; + context.font = '600 32px Arial'; - settings.supports = { - everything: true, - everythingExceptFlag: true - }; + const supports = {}; + tests.forEach( ( test ) => { + supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + } ); + return supports; +} - // Obtain the emoji support from the browser, asynchronously when possible. - new Promise( ( resolve ) => { - let supportTests = getSessionSupportTests(); - if ( supportTests ) { - resolve( supportTests ); - return; - } +/** + * Adds a script to the head of the document. + * + * @ignore + * + * @since 4.2.0 + * + * @param {string} src The url where the script is located. + * + * @return {void} + */ +function addScript( src ) { + const script = document.createElement( 'script' ); + script.src = src; + script.defer = true; + document.head.appendChild( script ); +} + +settings.supports = { + everything: true, + everythingExceptFlag: true +}; + +// Obtain the emoji support from the browser, asynchronously when possible. +new Promise( ( resolve ) => { + let supportTests = getSessionSupportTests(); + if ( supportTests ) { + resolve( supportTests ); + return; + } - if ( supportsWorkerOffloading() ) { - try { - // Note that the functions are being passed as arguments due to minification. - const workerScript = - 'postMessage(' + - testEmojiSupports.toString() + - '(' + - [ - JSON.stringify( tests ), - browserSupportsEmoji.toString(), - emojiSetsRenderIdentically.toString(), - emojiRendersEmptyCenterPoint.toString() - ].join( ',' ) + - '));'; - const blob = new Blob( [ workerScript ], { - type: 'text/javascript' - } ); - const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); - worker.onmessage = ( event ) => { - supportTests = event.data; - setSessionSupportTests( supportTests ); - worker.terminate(); - resolve( supportTests ); - }; - return; - } catch ( e ) {} - } + if ( supportsWorkerOffloading() ) { + try { + // Note that the functions are being passed as arguments due to minification. + const workerScript = + 'postMessage(' + + testEmojiSupports.toString() + + '(' + + [ + JSON.stringify( tests ), + browserSupportsEmoji.toString(), + emojiSetsRenderIdentically.toString(), + emojiRendersEmptyCenterPoint.toString() + ].join( ',' ) + + '));'; + const blob = new Blob( [ workerScript ], { + type: 'text/javascript' + } ); + const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); + worker.onmessage = ( event ) => { + supportTests = event.data; + setSessionSupportTests( supportTests ); + worker.terminate(); + resolve( supportTests ); + }; + return; + } catch ( e ) {} + } - supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); - setSessionSupportTests( supportTests ); - resolve( supportTests ); - } ) - // Once the browser emoji support has been obtained from the session, finalize the settings. - .then( ( supportTests ) => { - /* - * Tests the browser support for flag emojis and other emojis, and adjusts the - * support settings accordingly. - */ - for ( const test in supportTests ) { - settings.supports[ test ] = supportTests[ test ]; + supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + setSessionSupportTests( supportTests ); + resolve( supportTests ); +} ) + // Once the browser emoji support has been obtained from the session, finalize the settings. + .then( ( supportTests ) => { + /* + * Tests the browser support for flag emojis and other emojis, and adjusts the + * support settings accordingly. + */ + for ( const test in supportTests ) { + settings.supports[ test ] = supportTests[ test ]; - settings.supports.everything = - settings.supports.everything && settings.supports[ test ]; + settings.supports.everything = + settings.supports.everything && settings.supports[ test ]; - if ( 'flag' !== test ) { - settings.supports.everythingExceptFlag = - settings.supports.everythingExceptFlag && - settings.supports[ test ]; - } + if ( 'flag' !== test ) { + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + settings.supports[ test ]; } + } - settings.supports.everythingExceptFlag = - settings.supports.everythingExceptFlag && - ! settings.supports.flag; + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + ! settings.supports.flag; - // Sets DOMReady to false and assigns a ready function to settings. - settings.DOMReady = false; - settings.readyCallback = () => { - settings.DOMReady = true; - }; - } ) - .then( () => { - // When the browser can not render everything we need to load a polyfill. - if ( ! settings.supports.everything ) { - settings.readyCallback(); - - const src = settings.source || {}; - - if ( src.concatemoji ) { - addScript( src.concatemoji ); - } else if ( src.wpemoji && src.twemoji ) { - addScript( src.twemoji ); - addScript( src.wpemoji ); - } + // Sets DOMReady to false and assigns a ready function to settings. + settings.DOMReady = false; + settings.readyCallback = () => { + settings.DOMReady = true; + }; + } ) + .then( () => { + // When the browser can not render everything we need to load a polyfill. + if ( ! settings.supports.everything ) { + settings.readyCallback(); + + const src = settings.source || {}; + + if ( src.concatemoji ) { + addScript( src.concatemoji ); + } else if ( src.wpemoji && src.twemoji ) { + addScript( src.twemoji ); + addScript( src.wpemoji ); } - } ); - -} )(); + } + } ); From 589e11c31ab1ae47a2ea80ba0c282a2828bd7150 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 3 Oct 2025 13:20:07 -0700 Subject: [PATCH 12/12] Add uglify:emoji-loader to force top-level symbol minification --- Gruntfile.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index 729f1117522e4..a13c6e49526c5 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -838,8 +838,17 @@ module.exports = function(grunt) { '!**/*.min.js', '!wp-admin/js/custom-header.js', // Why? We should minify this. '!wp-admin/js/farbtastic.js', + '!wp-includes/js/wp-emoji-loader.js', // This is a module. See the emoji-loader task below. ] }, + 'emoji-loader': { + options: { + module: true, + toplevel: true, + }, + src: WORKING_DIR + 'wp-includes/js/wp-emoji-loader.js', + dest: WORKING_DIR + 'wp-includes/js/wp-emoji-loader.min.js', + }, 'jquery-ui': { options: { // Preserve comments that start with a bang. @@ -1549,6 +1558,7 @@ module.exports = function(grunt) { grunt.registerTask( 'uglify:all', [ 'uglify:core', + 'uglify:emoji-loader', 'uglify:jquery-ui', 'uglify:imgareaselect', 'uglify:jqueryform',