Make WordPress Core

Changeset 60899

Timestamp:
10/04/2025 01:49:52 AM (7 weeks 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.