Make WordPress Core

Changeset 60899


Ignore:
Timestamp:
10/04/2025 01:49:52 AM (10 months ago)
Author:
westonruter
Message:

Emoji: Convert the emoji loader from an inline blocking script to a (deferred) script module.

This modernizes the emoji loader script by converting it from a blocking inline script with an IIFE to a script module. Using a script module improves the performance of the FCP and LCP metrics since it does not block the HTML parser. Since script modules are deferred and run immediately before DOMContentLoaded, the logic to wait until that event is also now removed. Additionally, since the script is loaded as a module, it has been modernized to use const, let, and arrow functions. The sourceURL comment is also added. See #63887.

The emoji settings are now passed via a separate script tag with a type of application/json, following the new "pull" paradigm introduced for exporting data from PHP to script modules. See #58873. The JSON data is also now encoded in a more resilient way according to #63851. When the wp-emoji-loader.js script module executes, it continues to populate the window._wpemojiSettings global for backwards-compatibility for any extensions that may be using it.

A new uglify:emoji-loader grunt task is added which ensures wp-includes/js/wp-emoji-loader.js is processed as a module and that top-level symbols are minified.

Follow-up to [60681].

Props westonruter, jonsurrell, adamsilverstein, peterwilsoncc.
See #63851, #63887.
Fixes #63842.

Location:
trunk
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • trunk/Gruntfile.js

    r60765 r60899  
    839839                                        '!wp-admin/js/custom-header.js', // Why? We should minify this.
    840840                                        '!wp-admin/js/farbtastic.js',
     841                                        '!wp-includes/js/wp-emoji-loader.js', // This is a module. See the emoji-loader task below.
    841842                                ]
     843                        },
     844                        'emoji-loader': {
     845                                options: {
     846                                        module: true,
     847                                        toplevel: true,
     848                                },
     849                                src: WORKING_DIR + 'wp-includes/js/wp-emoji-loader.js',
     850                                dest: WORKING_DIR + 'wp-includes/js/wp-emoji-loader.min.js',
    842851                        },
    843852                        'jquery-ui': {
     
    15501559        grunt.registerTask( 'uglify:all', [
    15511560                'uglify:core',
     1561                'uglify:emoji-loader',
    15521562                'uglify:jquery-ui',
    15531563                'uglify:imgareaselect',
  • trunk/src/js/_enqueues/lib/emoji-loader.js

    r60227 r60899  
    22 * @output wp-includes/js/wp-emoji-loader.js
    33 */
     4
     5// Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope.
    46
    57/**
     
    1517 */
    1618
     19const settings = /** @type {WPEmojiSettings} */ (
     20        JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent )
     21);
     22
     23// For compatibility with other scripts that read from this global.
     24window._wpemojiSettings = settings;
     25
    1726/**
    1827 * Support tests.
     
    2332 */
    2433
    25 /**
    26  * IIFE to detect emoji support and load Twemoji if needed.
    27  *
    28  * @param {Window} window
    29  * @param {Document} document
    30  * @param {WPEmojiSettings} settings
    31  */
    32 ( function wpEmojiLoader( window, document, settings ) {
    33         if ( typeof Promise === 'undefined' ) {
     34const sessionStorageKey = 'wpEmojiSettingsSupports';
     35const tests = [ 'flag', 'emoji' ];
     36
     37/**
     38 * Checks whether the browser supports offloading to a Worker.
     39 *
     40 * @since 6.3.0
     41 *
     42 * @private
     43 *
     44 * @returns {boolean}
     45 */
     46function supportsWorkerOffloading() {
     47        return (
     48                typeof Worker !== 'undefined' &&
     49                typeof OffscreenCanvas !== 'undefined' &&
     50                typeof URL !== 'undefined' &&
     51                URL.createObjectURL &&
     52                typeof Blob !== 'undefined'
     53        );
     54}
     55
     56/**
     57 * @typedef SessionSupportTests
     58 * @type {object}
     59 * @property {number} timestamp
     60 * @property {SupportTests} supportTests
     61 */
     62
     63/**
     64 * Get support tests from session.
     65 *
     66 * @since 6.3.0
     67 *
     68 * @private
     69 *
     70 * @returns {?SupportTests} Support tests, or null if not set or older than 1 week.
     71 */
     72function getSessionSupportTests() {
     73        try {
     74                /** @type {SessionSupportTests} */
     75                const item = JSON.parse(
     76                        sessionStorage.getItem( sessionStorageKey )
     77                );
     78                if (
     79                        typeof item === 'object' &&
     80                        typeof item.timestamp === 'number' &&
     81                        new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds.
     82                        typeof item.supportTests === 'object'
     83                ) {
     84                        return item.supportTests;
     85                }
     86        } catch ( e ) {}
     87        return null;
     88}
     89
     90/**
     91 * Persist the supports in session storage.
     92 *
     93 * @since 6.3.0
     94 *
     95 * @private
     96 *
     97 * @param {SupportTests} supportTests Support tests.
     98 */
     99function setSessionSupportTests( supportTests ) {
     100        try {
     101                /** @type {SessionSupportTests} */
     102                const item = {
     103                        supportTests: supportTests,
     104                        timestamp: new Date().valueOf()
     105                };
     106
     107                sessionStorage.setItem(
     108                        sessionStorageKey,
     109                        JSON.stringify( item )
     110                );
     111        } catch ( e ) {}
     112}
     113
     114/**
     115 * Checks if two sets of Emoji characters render the same visually.
     116 *
     117 * This is used to determine if the browser is rendering an emoji with multiple data points
     118 * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji
     119 * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser
     120 * does not support the emoji correctly.
     121 *
     122 * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     123 * scope. Everything must be passed by parameters.
     124 *
     125 * @since 4.9.0
     126 *
     127 * @private
     128 *
     129 * @param {CanvasRenderingContext2D} context 2D Context.
     130 * @param {string} set1 Set of Emoji to test.
     131 * @param {string} set2 Set of Emoji to test.
     132 *
     133 * @return {boolean} True if the two sets render the same.
     134 */
     135function emojiSetsRenderIdentically( context, set1, set2 ) {
     136        // Cleanup from previous test.
     137        context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
     138        context.fillText( set1, 0, 0 );
     139        const rendered1 = new Uint32Array(
     140                context.getImageData(
     141                        0,
     142                        0,
     143                        context.canvas.width,
     144                        context.canvas.height
     145                ).data
     146        );
     147
     148        // Cleanup from previous test.
     149        context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
     150        context.fillText( set2, 0, 0 );
     151        const rendered2 = new Uint32Array(
     152                context.getImageData(
     153                        0,
     154                        0,
     155                        context.canvas.width,
     156                        context.canvas.height
     157                ).data
     158        );
     159
     160        return rendered1.every( ( rendered2Data, index ) => {
     161                return rendered2Data === rendered2[ index ];
     162        } );
     163}
     164
     165/**
     166 * Checks if the center point of a single emoji is empty.
     167 *
     168 * This is used to determine if the browser is rendering an emoji with a single data point
     169 * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly
     170 * rendered emoji will have a non-zero value at the center point.
     171 *
     172 * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     173 * scope. Everything must be passed by parameters.
     174 *
     175 * @since 6.8.2
     176 *
     177 * @private
     178 *
     179 * @param {CanvasRenderingContext2D} context 2D Context.
     180 * @param {string} emoji Emoji to test.
     181 *
     182 * @return {boolean} True if the center point is empty.
     183 */
     184function emojiRendersEmptyCenterPoint( context, emoji ) {
     185        // Cleanup from previous test.
     186        context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
     187        context.fillText( emoji, 0, 0 );
     188
     189        // Test if the center point (16, 16) is empty (0,0,0,0).
     190        const centerPoint = context.getImageData(16, 16, 1, 1);
     191        for ( let i = 0; i < centerPoint.data.length; i++ ) {
     192                if ( centerPoint.data[ i ] !== 0 ) {
     193                        // Stop checking the moment it's known not to be empty.
     194                        return false;
     195                }
     196        }
     197
     198        return true;
     199}
     200
     201/**
     202 * Determines if the browser properly renders Emoji that Twemoji can supplement.
     203 *
     204 * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     205 * scope. Everything must be passed by parameters.
     206 *
     207 * @since 4.2.0
     208 *
     209 * @private
     210 *
     211 * @param {CanvasRenderingContext2D} context 2D Context.
     212 * @param {string} type Whether to test for support of "flag" or "emoji".
     213 * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
     214 * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
     215 *
     216 * @return {boolean} True if the browser can render emoji, false if it cannot.
     217 */
     218function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) {
     219        let isIdentical;
     220
     221        switch ( type ) {
     222                case 'flag':
     223                        /*
     224                         * Test for Transgender flag compatibility. Added in Unicode 13.
     225                         *
     226                         * To test for support, we try to render it, and compare the rendering to how it would look if
     227                         * the browser doesn't render it correctly (white flag emoji + transgender symbol).
     228                         */
     229                        isIdentical = emojiSetsRenderIdentically(
     230                                context,
     231                                '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence
     232                                '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space
     233                        );
     234
     235                        if ( isIdentical ) {
     236                                return false;
     237                        }
     238
     239                        /*
     240                         * Test for Sark flag compatibility. This is the least supported of the letter locale flags,
     241                         * so gives us an easy test for full support.
     242                         *
     243                         * To test for support, we try to render it, and compare the rendering to how it would look if
     244                         * the browser doesn't render it correctly ([C] + [Q]).
     245                         */
     246                        isIdentical = emojiSetsRenderIdentically(
     247                                context,
     248                                '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points
     249                                '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space
     250                        );
     251
     252                        if ( isIdentical ) {
     253                                return false;
     254                        }
     255
     256                        /*
     257                         * Test for English flag compatibility. England is a country in the United Kingdom, it
     258                         * does not have a two letter locale code but rather a five letter sub-division code.
     259                         *
     260                         * To test for support, we try to render it, and compare the rendering to how it would look if
     261                         * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]).
     262                         */
     263                        isIdentical = emojiSetsRenderIdentically(
     264                                context,
     265                                // as the flag sequence
     266                                '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F',
     267                                // with each code point separated by a zero-width space
     268                                '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F'
     269                        );
     270
     271                        return ! isIdentical;
     272                case 'emoji':
     273                        /*
     274                         * Does Emoji 16.0 cause the browser to go splat?
     275                         *
     276                         * To test for Emoji 16.0 support, try to render a new emoji: Splatter.
     277                         *
     278                         * The splatter emoji is a single code point emoji. Testing for browser support
     279                         * required testing the center point of the emoji to see if it is empty.
     280                         *
     281                         * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter.
     282                         *
     283                         * When updating this test, please ensure that the emoji is either a single code point
     284                         * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width
     285                         * joiner vs a zero-width space.
     286                         */
     287                        const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' );
     288                        return ! notSupported;
     289        }
     290
     291        return false;
     292}
     293
     294/**
     295 * Checks emoji support tests.
     296 *
     297 * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
     298 * scope. Everything must be passed by parameters.
     299 *
     300 * @since 6.3.0
     301 *
     302 * @private
     303 *
     304 * @param {string[]} tests Tests.
     305 * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification.
     306 * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
     307 * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
     308 *
     309 * @return {SupportTests} Support tests.
     310 */
     311function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) {
     312        let canvas;
     313        if (
     314                typeof WorkerGlobalScope !== 'undefined' &&
     315                self instanceof WorkerGlobalScope
     316        ) {
     317                canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement.
     318        } else {
     319                canvas = document.createElement( 'canvas' );
     320        }
     321
     322        const context = canvas.getContext( '2d', { willReadFrequently: true } );
     323
     324        /*
     325         * Chrome on OS X added native emoji rendering in M41. Unfortunately,
     326         * it doesn't work when the font is bolder than 500 weight. So, we
     327         * check for bold rendering support to avoid invisible emoji in Chrome.
     328         */
     329        context.textBaseline = 'top';
     330        context.font = '600 32px Arial';
     331
     332        const supports = {};
     333        tests.forEach( ( test ) => {
     334                supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
     335        } );
     336        return supports;
     337}
     338
     339/**
     340 * Adds a script to the head of the document.
     341 *
     342 * @ignore
     343 *
     344 * @since 4.2.0
     345 *
     346 * @param {string} src The url where the script is located.
     347 *
     348 * @return {void}
     349 */
     350function addScript( src ) {
     351        const script = document.createElement( 'script' );
     352        script.src = src;
     353        script.defer = true;
     354        document.head.appendChild( script );
     355}
     356
     357settings.supports = {
     358        everything: true,
     359        everythingExceptFlag: true
     360};
     361
     362// Obtain the emoji support from the browser, asynchronously when possible.
     363new Promise( ( resolve ) => {
     364        let supportTests = getSessionSupportTests();
     365        if ( supportTests ) {
     366                resolve( supportTests );
    34367                return;
    35368        }
    36369
    37         var sessionStorageKey = 'wpEmojiSettingsSupports';
    38         var tests = [ 'flag', 'emoji' ];
    39 
    40         /**
    41          * Checks whether the browser supports offloading to a Worker.
    42          *
    43          * @since 6.3.0
    44          *
    45          * @private
    46          *
    47          * @returns {boolean}
    48          */
    49         function supportsWorkerOffloading() {
    50                 return (
    51                         typeof Worker !== 'undefined' &&
    52                         typeof OffscreenCanvas !== 'undefined' &&
    53                         typeof URL !== 'undefined' &&
    54                         URL.createObjectURL &&
    55                         typeof Blob !== 'undefined'
    56                 );
    57         }
    58 
    59         /**
    60          * @typedef SessionSupportTests
    61          * @type {object}
    62          * @property {number} timestamp
    63          * @property {SupportTests} supportTests
    64          */
    65 
    66         /**
    67          * Get support tests from session.
    68          *
    69          * @since 6.3.0
    70          *
    71          * @private
    72          *
    73          * @returns {?SupportTests} Support tests, or null if not set or older than 1 week.
    74          */
    75         function getSessionSupportTests() {
     370        if ( supportsWorkerOffloading() ) {
    76371                try {
    77                         /** @type {SessionSupportTests} */
    78                         var item = JSON.parse(
    79                                 sessionStorage.getItem( sessionStorageKey )
    80                         );
    81                         if (
    82                                 typeof item === 'object' &&
    83                                 typeof item.timestamp === 'number' &&
    84                                 new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds.
    85                                 typeof item.supportTests === 'object'
    86                         ) {
    87                                 return item.supportTests;
    88                         }
    89                 } catch ( e ) {}
    90                 return null;
    91         }
    92 
    93         /**
    94          * Persist the supports in session storage.
    95          *
    96          * @since 6.3.0
    97          *
    98          * @private
    99          *
    100          * @param {SupportTests} supportTests Support tests.
    101          */
    102         function setSessionSupportTests( supportTests ) {
    103                 try {
    104                         /** @type {SessionSupportTests} */
    105                         var item = {
    106                                 supportTests: supportTests,
    107                                 timestamp: new Date().valueOf()
     372                        // Note that the functions are being passed as arguments due to minification.
     373                        const workerScript =
     374                                'postMessage(' +
     375                                testEmojiSupports.toString() +
     376                                '(' +
     377                                [
     378                                        JSON.stringify( tests ),
     379                                        browserSupportsEmoji.toString(),
     380                                        emojiSetsRenderIdentically.toString(),
     381                                        emojiRendersEmptyCenterPoint.toString()
     382                                ].join( ',' ) +
     383                                '));';
     384                        const blob = new Blob( [ workerScript ], {
     385                                type: 'text/javascript'
     386                        } );
     387                        const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } );
     388                        worker.onmessage = ( event ) => {
     389                                supportTests = event.data;
     390                                setSessionSupportTests( supportTests );
     391                                worker.terminate();
     392                                resolve( supportTests );
    108393                        };
    109 
    110                         sessionStorage.setItem(
    111                                 sessionStorageKey,
    112                                 JSON.stringify( item )
    113                         );
     394                        return;
    114395                } catch ( e ) {}
    115396        }
    116397
    117         /**
    118          * Checks if two sets of Emoji characters render the same visually.
    119          *
    120          * This is used to determine if the browser is rendering an emoji with multiple data points
    121          * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji
    122          * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser
    123          * does not support the emoji correctly.
    124          *
    125          * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
    126          * scope. Everything must be passed by parameters.
    127          *
    128          * @since 4.9.0
    129          *
    130          * @private
    131          *
    132          * @param {CanvasRenderingContext2D} context 2D Context.
    133          * @param {string} set1 Set of Emoji to test.
    134          * @param {string} set2 Set of Emoji to test.
    135          *
    136          * @return {boolean} True if the two sets render the same.
    137          */
    138         function emojiSetsRenderIdentically( context, set1, set2 ) {
    139                 // Cleanup from previous test.
    140                 context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
    141                 context.fillText( set1, 0, 0 );
    142                 var rendered1 = new Uint32Array(
    143                         context.getImageData(
    144                                 0,
    145                                 0,
    146                                 context.canvas.width,
    147                                 context.canvas.height
    148                         ).data
    149                 );
    150 
    151                 // Cleanup from previous test.
    152                 context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
    153                 context.fillText( set2, 0, 0 );
    154                 var rendered2 = new Uint32Array(
    155                         context.getImageData(
    156                                 0,
    157                                 0,
    158                                 context.canvas.width,
    159                                 context.canvas.height
    160                         ).data
    161                 );
    162 
    163                 return rendered1.every( function ( rendered2Data, index ) {
    164                         return rendered2Data === rendered2[ index ];
    165                 } );
    166         }
    167 
    168         /**
    169          * Checks if the center point of a single emoji is empty.
    170          *
    171          * This is used to determine if the browser is rendering an emoji with a single data point
    172          * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly
    173          * rendered emoji will have a non-zero value at the center point.
    174          *
    175          * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
    176          * scope. Everything must be passed by parameters.
    177          *
    178          * @since 6.8.2
    179          *
    180          * @private
    181          *
    182          * @param {CanvasRenderingContext2D} context 2D Context.
    183          * @param {string} emoji Emoji to test.
    184          *
    185          * @return {boolean} True if the center point is empty.
    186          */
    187         function emojiRendersEmptyCenterPoint( context, emoji ) {
    188                 // Cleanup from previous test.
    189                 context.clearRect( 0, 0, context.canvas.width, context.canvas.height );
    190                 context.fillText( emoji, 0, 0 );
    191 
    192                 // Test if the center point (16, 16) is empty (0,0,0,0).
    193                 var centerPoint = context.getImageData(16, 16, 1, 1);
    194                 for ( var i = 0; i < centerPoint.data.length; i++ ) {
    195                         if ( centerPoint.data[ i ] !== 0 ) {
    196                                 // Stop checking the moment it's known not to be empty.
    197                                 return false;
     398        supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
     399        setSessionSupportTests( supportTests );
     400        resolve( supportTests );
     401} )
     402        // Once the browser emoji support has been obtained from the session, finalize the settings.
     403        .then( ( supportTests ) => {
     404                /*
     405                 * Tests the browser support for flag emojis and other emojis, and adjusts the
     406                 * support settings accordingly.
     407                 */
     408                for ( const test in supportTests ) {
     409                        settings.supports[ test ] = supportTests[ test ];
     410
     411                        settings.supports.everything =
     412                                settings.supports.everything && settings.supports[ test ];
     413
     414                        if ( 'flag' !== test ) {
     415                                settings.supports.everythingExceptFlag =
     416                                        settings.supports.everythingExceptFlag &&
     417                                        settings.supports[ test ];
    198418                        }
    199419                }
    200420
    201                 return true;
    202         }
    203 
    204         /**
    205          * Determines if the browser properly renders Emoji that Twemoji can supplement.
    206          *
    207          * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
    208          * scope. Everything must be passed by parameters.
    209          *
    210          * @since 4.2.0
    211          *
    212          * @private
    213          *
    214          * @param {CanvasRenderingContext2D} context 2D Context.
    215          * @param {string} type Whether to test for support of "flag" or "emoji".
    216          * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
    217          * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
    218          *
    219          * @return {boolean} True if the browser can render emoji, false if it cannot.
    220          */
    221         function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) {
    222                 var isIdentical;
    223 
    224                 switch ( type ) {
    225                         case 'flag':
    226                                 /*
    227                                  * Test for Transgender flag compatibility. Added in Unicode 13.
    228                                  *
    229                                  * To test for support, we try to render it, and compare the rendering to how it would look if
    230                                  * the browser doesn't render it correctly (white flag emoji + transgender symbol).
    231                                  */
    232                                 isIdentical = emojiSetsRenderIdentically(
    233                                         context,
    234                                         '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence
    235                                         '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space
    236                                 );
    237 
    238                                 if ( isIdentical ) {
    239                                         return false;
    240                                 }
    241 
    242                                 /*
    243                                  * Test for Sark flag compatibility. This is the least supported of the letter locale flags,
    244                                  * so gives us an easy test for full support.
    245                                  *
    246                                  * To test for support, we try to render it, and compare the rendering to how it would look if
    247                                  * the browser doesn't render it correctly ([C] + [Q]).
    248                                  */
    249                                 isIdentical = emojiSetsRenderIdentically(
    250                                         context,
    251                                         '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points
    252                                         '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space
    253                                 );
    254 
    255                                 if ( isIdentical ) {
    256                                         return false;
    257                                 }
    258 
    259                                 /*
    260                                  * Test for English flag compatibility. England is a country in the United Kingdom, it
    261                                  * does not have a two letter locale code but rather a five letter sub-division code.
    262                                  *
    263                                  * To test for support, we try to render it, and compare the rendering to how it would look if
    264                                  * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]).
    265                                  */
    266                                 isIdentical = emojiSetsRenderIdentically(
    267                                         context,
    268                                         // as the flag sequence
    269                                         '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F',
    270                                         // with each code point separated by a zero-width space
    271                                         '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F'
    272                                 );
    273 
    274                                 return ! isIdentical;
    275                         case 'emoji':
    276                                 /*
    277                                  * Does Emoji 16.0 cause the browser to go splat?
    278                                  *
    279                                  * To test for Emoji 16.0 support, try to render a new emoji: Splatter.
    280                                  *
    281                                  * The splatter emoji is a single code point emoji. Testing for browser support
    282                                  * required testing the center point of the emoji to see if it is empty.
    283                                  *
    284                                  * 0xD83E 0xDEDF (\uD83E\uDEDF) == 🫟 Splatter.
    285                                  *
    286                                  * When updating this test, please ensure that the emoji is either a single code point
    287                                  * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width
    288                                  * joiner vs a zero-width space.
    289                                  */
    290                                 var notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEDF' );
    291                                 return ! notSupported;
     421                settings.supports.everythingExceptFlag =
     422                        settings.supports.everythingExceptFlag &&
     423                        ! settings.supports.flag;
     424
     425                // Sets DOMReady to false and assigns a ready function to settings.
     426                settings.DOMReady = false;
     427                settings.readyCallback = () => {
     428                        settings.DOMReady = true;
     429                };
     430        } )
     431        .then( () => {
     432                // When the browser can not render everything we need to load a polyfill.
     433                if ( ! settings.supports.everything ) {
     434                        settings.readyCallback();
     435
     436                        const src = settings.source || {};
     437
     438                        if ( src.concatemoji ) {
     439                                addScript( src.concatemoji );
     440                        } else if ( src.wpemoji && src.twemoji ) {
     441                                addScript( src.twemoji );
     442                                addScript( src.wpemoji );
     443                        }
    292444                }
    293 
    294                 return false;
    295         }
    296 
    297         /**
    298          * Checks emoji support tests.
    299          *
    300          * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing
    301          * scope. Everything must be passed by parameters.
    302          *
    303          * @since 6.3.0
    304          *
    305          * @private
    306          *
    307          * @param {string[]} tests Tests.
    308          * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification.
    309          * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
    310          * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
    311          *
    312          * @return {SupportTests} Support tests.
    313          */
    314         function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) {
    315                 var canvas;
    316                 if (
    317                         typeof WorkerGlobalScope !== 'undefined' &&
    318                         self instanceof WorkerGlobalScope
    319                 ) {
    320                         canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement.
    321                 } else {
    322                         canvas = document.createElement( 'canvas' );
    323                 }
    324 
    325                 var context = canvas.getContext( '2d', { willReadFrequently: true } );
    326 
    327                 /*
    328                  * Chrome on OS X added native emoji rendering in M41. Unfortunately,
    329                  * it doesn't work when the font is bolder than 500 weight. So, we
    330                  * check for bold rendering support to avoid invisible emoji in Chrome.
    331                  */
    332                 context.textBaseline = 'top';
    333                 context.font = '600 32px Arial';
    334 
    335                 var supports = {};
    336                 tests.forEach( function ( test ) {
    337                         supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
    338                 } );
    339                 return supports;
    340         }
    341 
    342         /**
    343          * Adds a script to the head of the document.
    344          *
    345          * @ignore
    346          *
    347          * @since 4.2.0
    348          *
    349          * @param {string} src The url where the script is located.
    350          *
    351          * @return {void}
    352          */
    353         function addScript( src ) {
    354                 var script = document.createElement( 'script' );
    355                 script.src = src;
    356                 script.defer = true;
    357                 document.head.appendChild( script );
    358         }
    359 
    360         settings.supports = {
    361                 everything: true,
    362                 everythingExceptFlag: true
    363         };
    364 
    365         // Create a promise for DOMContentLoaded since the worker logic may finish after the event has fired.
    366         var domReadyPromise = new Promise( function ( resolve ) {
    367                 document.addEventListener( 'DOMContentLoaded', resolve, {
    368                         once: true
    369                 } );
    370445        } );
    371 
    372         // Obtain the emoji support from the browser, asynchronously when possible.
    373         new Promise( function ( resolve ) {
    374                 var supportTests = getSessionSupportTests();
    375                 if ( supportTests ) {
    376                         resolve( supportTests );
    377                         return;
    378                 }
    379 
    380                 if ( supportsWorkerOffloading() ) {
    381                         try {
    382                                 // Note that the functions are being passed as arguments due to minification.
    383                                 var workerScript =
    384                                         'postMessage(' +
    385                                         testEmojiSupports.toString() +
    386                                         '(' +
    387                                         [
    388                                                 JSON.stringify( tests ),
    389                                                 browserSupportsEmoji.toString(),
    390                                                 emojiSetsRenderIdentically.toString(),
    391                                                 emojiRendersEmptyCenterPoint.toString()
    392                                         ].join( ',' ) +
    393                                         '));';
    394                                 var blob = new Blob( [ workerScript ], {
    395                                         type: 'text/javascript'
    396                                 } );
    397                                 var worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } );
    398                                 worker.onmessage = function ( event ) {
    399                                         supportTests = event.data;
    400                                         setSessionSupportTests( supportTests );
    401                                         worker.terminate();
    402                                         resolve( supportTests );
    403                                 };
    404                                 return;
    405                         } catch ( e ) {}
    406                 }
    407 
    408                 supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
    409                 setSessionSupportTests( supportTests );
    410                 resolve( supportTests );
    411         } )
    412                 // Once the browser emoji support has been obtained from the session, finalize the settings.
    413                 .then( function ( supportTests ) {
    414                         /*
    415                          * Tests the browser support for flag emojis and other emojis, and adjusts the
    416                          * support settings accordingly.
    417                          */
    418                         for ( var test in supportTests ) {
    419                                 settings.supports[ test ] = supportTests[ test ];
    420 
    421                                 settings.supports.everything =
    422                                         settings.supports.everything && settings.supports[ test ];
    423 
    424                                 if ( 'flag' !== test ) {
    425                                         settings.supports.everythingExceptFlag =
    426                                                 settings.supports.everythingExceptFlag &&
    427                                                 settings.supports[ test ];
    428                                 }
    429                         }
    430 
    431                         settings.supports.everythingExceptFlag =
    432                                 settings.supports.everythingExceptFlag &&
    433                                 ! settings.supports.flag;
    434 
    435                         // Sets DOMReady to false and assigns a ready function to settings.
    436                         settings.DOMReady = false;
    437                         settings.readyCallback = function () {
    438                                 settings.DOMReady = true;
    439                         };
    440                 } )
    441                 .then( function () {
    442                         return domReadyPromise;
    443                 } )
    444                 .then( function () {
    445                         // When the browser can not render everything we need to load a polyfill.
    446                         if ( ! settings.supports.everything ) {
    447                                 settings.readyCallback();
    448 
    449                                 var src = settings.source || {};
    450 
    451                                 if ( src.concatemoji ) {
    452                                         addScript( src.concatemoji );
    453                                 } else if ( src.wpemoji && src.twemoji ) {
    454                                         addScript( src.twemoji );
    455                                         addScript( src.wpemoji );
    456                                 }
    457                         }
    458                 } );
    459 } )( window, document, window._wpemojiSettings );
  • trunk/src/wp-includes/formatting.php

    r60793 r60899  
    59795979
    59805980        wp_print_inline_script_tag(
    5981                 sprintf( 'window._wpemojiSettings = %s;', wp_json_encode( $settings ) ) . "\n" .
    5982                         file_get_contents( ABSPATH . WPINC . '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js' )
     5981                wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
     5982                array(
     5983                        'id'   => 'wp-emoji-settings',
     5984                        'type' => 'application/json',
     5985                )
     5986        );
     5987
     5988        $emoji_loader_script_path = '/js/wp-emoji-loader' . wp_scripts_get_suffix() . '.js';
     5989        wp_print_inline_script_tag(
     5990                rtrim( file_get_contents( ABSPATH . WPINC . $emoji_loader_script_path ) ) . "\n" .
     5991                '//# sourceURL=' . includes_url( $emoji_loader_script_path ),
     5992                array(
     5993                        'type' => 'module',
     5994                )
    59835995        );
    59845996}
  • trunk/tests/phpunit/tests/formatting/emoji.php

    r60227 r60899  
    88
    99        private $png_cdn = 'https://s.w.org/images/core/emoji/16.0.1/72x72/';
    10         private $svn_cdn = 'https://s.w.org/images/core/emoji/16.0.1/svg/';
     10        private $svg_cdn = 'https://s.w.org/images/core/emoji/16.0.1/svg/';
     11
     12        /**
     13         * @ticket 63842
     14         *
     15         * @covers ::_print_emoji_detection_script
     16         */
     17        public function test_script_tag_printing() {
     18                // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present:
     19                self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' );
     20                $output = get_echo( '_print_emoji_detection_script' );
     21
     22                $processor = new WP_HTML_Tag_Processor( $output );
     23                $this->assertTrue( $processor->next_tag() );
     24                $this->assertSame( 'SCRIPT', $processor->get_tag() );
     25                $this->assertSame( 'wp-emoji-settings', $processor->get_attribute( 'id' ) );
     26                $this->assertSame( 'application/json', $processor->get_attribute( 'type' ) );
     27                $text     = $processor->get_modifiable_text();
     28                $settings = json_decode( $text, true );
     29                $this->assertIsArray( $settings );
     30
     31                $this->assertEqualSets(
     32                        array( 'baseUrl', 'ext', 'svgUrl', 'svgExt', 'source' ),
     33                        array_keys( $settings )
     34                );
     35                $this->assertSame( $this->png_cdn, $settings['baseUrl'] );
     36                $this->assertSame( '.png', $settings['ext'] );
     37                $this->assertSame( $this->svg_cdn, $settings['svgUrl'] );
     38                $this->assertSame( '.svg', $settings['svgExt'] );
     39                $this->assertIsArray( $settings['source'] );
     40                $this->assertArrayHasKey( 'wpemoji', $settings['source'] );
     41                $this->assertArrayHasKey( 'twemoji', $settings['source'] );
     42                $this->assertTrue( $processor->next_tag() );
     43                $this->assertSame( 'SCRIPT', $processor->get_tag() );
     44                $this->assertSame( 'module', $processor->get_attribute( 'type' ) );
     45                $this->assertNull( $processor->get_attribute( 'src' ) );
     46                $this->assertFalse( $processor->next_tag() );
     47        }
    1148
    1249        /**
     
    2057                $output = get_echo( '_print_emoji_detection_script' );
    2158
    22                 $this->assertStringContainsString( wp_json_encode( $this->png_cdn ), $output );
    23                 $this->assertStringContainsString( wp_json_encode( $this->svn_cdn ), $output );
    24         }
    25 
    26         public function _filtered_emoji_svn_cdn( $cdn = '' ) {
     59                $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     60                $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     61        }
     62
     63        public function _filtered_emoji_svg_cdn( $cdn = '' ) {
    2764                return 'https://s.wordpress.org/images/core/emoji/svg/';
    2865        }
     
    3471         */
    3572        public function test_filtered_emoji_svn_cdn() {
    36                 $filtered_svn_cdn = $this->_filtered_emoji_svn_cdn();
    37 
    38                 add_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svn_cdn' ) );
    39 
    40                 // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present:
    41                 self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' );
    42                 $output = get_echo( '_print_emoji_detection_script' );
    43 
    44                 $this->assertStringContainsString( wp_json_encode( $this->png_cdn ), $output );
    45                 $this->assertStringNotContainsString( wp_json_encode( $this->svn_cdn ), $output );
    46                 $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn ), $output );
    47 
    48                 remove_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svn_cdn' ) );
     73                $filtered_svn_cdn = $this->_filtered_emoji_svg_cdn();
     74
     75                add_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svg_cdn' ) );
     76
     77                // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present:
     78                self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' );
     79                $output = get_echo( '_print_emoji_detection_script' );
     80
     81                $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     82                $this->assertStringNotContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     83                $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     84
     85                remove_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svg_cdn' ) );
    4986        }
    5087
     
    67104                $output = get_echo( '_print_emoji_detection_script' );
    68105
    69                 $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn ), $output );
    70                 $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn ), $output );
    71                 $this->assertStringContainsString( wp_json_encode( $this->svn_cdn ), $output );
     106                $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     107                $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
     108                $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output );
    72109
    73110                remove_filter( 'emoji_url', array( $this, '_filtered_emoji_png_cdn' ) );
Note: See TracChangeset for help on using the changeset viewer.