[ 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 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, in particular wp-includes/js/wp-emoji.js (source file: js/_enqueues/wp/emoji.js).
  24  window._wpemojiSettings = settings;
  25  
  26  /**
  27   * Support tests.
  28   * @typedef SupportTests
  29   * @type {object}
  30   * @property {?boolean} flag
  31   * @property {?boolean} emoji
  32   */
  33  
  34  const sessionStorageKey = 'wpEmojiSettingsSupports';
  35  const 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   */
  46  function 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   */
  72  function 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   */
  99  function 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   */
 135  function 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   */
 184  function 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   */
 218  function 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   */
 311  function 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   */
 350  function addScript( src ) {
 351      const script = document.createElement( 'script' );
 352      script.src = src;
 353      script.defer = true;
 354      document.head.appendChild( script );
 355  }
 356  
 357  settings.supports = {
 358      everything: true,
 359      everythingExceptFlag: true
 360  };
 361  
 362  // Obtain the emoji support from the browser, asynchronously when possible.
 363  new Promise( ( resolve ) => {
 364      let supportTests = getSessionSupportTests();
 365      if ( supportTests ) {
 366          resolve( supportTests );
 367          return;
 368      }
 369  
 370      if ( supportsWorkerOffloading() ) {
 371          try {
 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 );
 393              };
 394              return;
 395          } catch ( e ) {}
 396      }
 397  
 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 ];
 418              }
 419          }
 420  
 421          settings.supports.everythingExceptFlag =
 422              settings.supports.everythingExceptFlag &&
 423              ! settings.supports.flag;
 424  
 425          // When the browser can not render everything we need to load a polyfill.
 426          if ( ! settings.supports.everything ) {
 427              const src = settings.source || {};
 428  
 429              if ( src.concatemoji ) {
 430                  addScript( src.concatemoji );
 431              } else if ( src.wpemoji && src.twemoji ) {
 432                  addScript( src.twemoji );
 433                  addScript( src.wpemoji );
 434              }
 435          }
 436      } );


Generated : Fri Oct 10 08:20:03 2025 Cross-referenced by PHPXref