[ 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  /* eslint-env es6 */
   6  
   7  // Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope.
   8  
   9  /**
  10   * Emoji Settings as exported in PHP via _print_emoji_detection_script().
  11   * @typedef WPEmojiSettings
  12   * @type {object}
  13   * @property {?object} source
  14   * @property {?string} source.concatemoji
  15   * @property {?string} source.twemoji
  16   * @property {?string} source.wpemoji
  17   */
  18  
  19  const selector = 'script#wp-emoji-settings';
  20  const script = document.querySelector( selector );
  21  if ( ! ( script instanceof HTMLScriptElement ) ) {
  22      throw new Error( `Element missing: ${ selector }`);
  23  }
  24  const settings = /** @type {WPEmojiSettings} */ ( JSON.parse( script.text ) );
  25  
  26  // 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).
  27  window._wpemojiSettings = settings;
  28  
  29  /**
  30   * Support tests.
  31   * @typedef SupportTests
  32   * @type {object}
  33   * @property {?boolean} flag
  34   * @property {?boolean} emoji
  35   */
  36  
  37  const sessionStorageKey = 'wpEmojiSettingsSupports';
  38  const 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() {
  76      try {
  77          /** @type {SessionSupportTests} */
  78          const 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          const item = {
 106              supportTests: supportTests,
 107              timestamp: new Date().valueOf()
 108          };
 109  
 110          sessionStorage.setItem(
 111              sessionStorageKey,
 112              JSON.stringify( item )
 113          );
 114      } catch ( e ) {}
 115  }
 116  
 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      const 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      const 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( ( 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      const centerPoint = context.getImageData(16, 16, 1, 1);
 194      for ( let 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;
 198          }
 199      }
 200  
 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      let 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               * Is there a large, hairy, humanoid mythical creature living in the browser?
 278               *
 279               * To test for Emoji 17.0 support, try to render a new emoji: Hairy Creature.
 280               *
 281               * The hairy creature emoji is a single code point emoji. Testing for browser
 282               * support required testing the center point of the emoji to see if it is empty.
 283               *
 284               * 0xD83E 0x1FAC8 (\uD83E\u1FAC8) == 🫈 Hairy creature.
 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              const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\u1FAC8' );
 291              return ! notSupported;
 292      }
 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      let 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      const 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      const supports = {};
 336      tests.forEach( ( 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      const 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  // Obtain the emoji support from the browser, asynchronously when possible.
 366  new Promise( ( resolve ) => {
 367      let supportTests = getSessionSupportTests();
 368      if ( supportTests ) {
 369          resolve( supportTests );
 370          return;
 371      }
 372  
 373      if ( supportsWorkerOffloading() ) {
 374          try {
 375              // Note that the functions are being passed as arguments due to minification.
 376              const workerScript =
 377                  'postMessage(' +
 378                  testEmojiSupports.toString() +
 379                  '(' +
 380                  [
 381                      JSON.stringify( tests ),
 382                      browserSupportsEmoji.toString(),
 383                      emojiSetsRenderIdentically.toString(),
 384                      emojiRendersEmptyCenterPoint.toString()
 385                  ].join( ',' ) +
 386                  '));';
 387              const blob = new Blob( [ workerScript ], {
 388                  type: 'text/javascript'
 389              } );
 390              const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } );
 391              worker.onmessage = ( event ) => {
 392                  supportTests = event.data;
 393                  setSessionSupportTests( supportTests );
 394                  worker.terminate();
 395                  resolve( supportTests );
 396              };
 397              return;
 398          } catch ( e ) {}
 399      }
 400  
 401      supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
 402      setSessionSupportTests( supportTests );
 403      resolve( supportTests );
 404  } )
 405      // Once the browser emoji support has been obtained from the session, finalize the settings.
 406      .then( ( supportTests ) => {
 407          /*
 408           * Tests the browser support for flag emojis and other emojis, and adjusts the
 409           * support settings accordingly.
 410           */
 411          for ( const test in supportTests ) {
 412              settings.supports[ test ] = supportTests[ test ];
 413  
 414              settings.supports.everything =
 415                  settings.supports.everything && settings.supports[ test ];
 416  
 417              if ( 'flag' !== test ) {
 418                  settings.supports.everythingExceptFlag =
 419                      settings.supports.everythingExceptFlag &&
 420                      settings.supports[ test ];
 421              }
 422          }
 423  
 424          settings.supports.everythingExceptFlag =
 425              settings.supports.everythingExceptFlag &&
 426              ! settings.supports.flag;
 427  
 428          // When the browser can not render everything we need to load a polyfill.
 429          if ( ! settings.supports.everything ) {
 430              const src = settings.source || {};
 431  
 432              if ( src.concatemoji ) {
 433                  addScript( src.concatemoji );
 434              } else if ( src.wpemoji && src.twemoji ) {
 435                  addScript( src.twemoji );
 436                  addScript( src.wpemoji );
 437              }
 438          }
 439      } );


Generated : Wed Aug 26 08:20:24 2026 Cross-referenced by PHPXref