[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/js/ -> wp-emoji-loader.js (source)

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


Generated : Tue Sep 15 08:20:32 2026 Cross-referenced by PHPXref