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


Generated : Wed Oct 28 08:20:35 2026 Cross-referenced by PHPXref