Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f8f8bf9
Add the emoji loader to the TypeScript checked files
westonruter Sep 17, 2026
48bf2fa
Add wp-emoji to the TypeScript checked files
westonruter Sep 17, 2026
2206a9d
Replace var with let and const in wp-emoji
westonruter Sep 17, 2026
2b6aa98
Split the wp-emoji file docblock to match the other scripts
westonruter Sep 17, 2026
4cafa8f
Remove the unreachable IE 11 workaround from wp-emoji
westonruter Sep 17, 2026
70d820e
Use MutationObserver in wp-emoji without the feature detection
westonruter Sep 17, 2026
ef39fbf
Always use the SVG emoji images in wp-emoji
westonruter Sep 17, 2026
6c395a7
Match the emoji exclusion class with classList in wp-emoji
westonruter Sep 17, 2026
5eb16e1
Describe what wpEmoji() returns in wp-emoji
westonruter Sep 17, 2026
03e88be
Iterate mutation records with for...of in wp-emoji
westonruter Sep 17, 2026
a940330
Narrow the parsed node with instanceof in wp-emoji
westonruter Sep 17, 2026
0efb75b
Restore the emoji image load failure fallback in wp-emoji
westonruter Sep 17, 2026
2fad9b2
Tweak comment per LanguageTool
westonruter Sep 17, 2026
36ae665
Describe what the emoji loader documents but does not explain
westonruter Sep 17, 2026
e0760b4
Reach the emoji error marker through dataset in wp-emoji
westonruter Sep 17, 2026
6df94a4
Correct the spacing in the emoji scripts
westonruter Sep 17, 2026
a2a34ca
Use arrow functions for the wp-emoji callbacks
westonruter Sep 17, 2026
68f7fb2
Decide the emoji image attributes once in wp-emoji
westonruter Sep 17, 2026
5aa8f9a
Add QUnit tests for wp-emoji
westonruter Sep 17, 2026
c2e0435
Cover the wp-emoji mutation observer with QUnit tests
westonruter Sep 17, 2026
560f5ff
Address static analysis issues in QUnit test
westonruter Sep 17, 2026
574526a
Spell out what wpEmoji() returns rather than referring to itself
westonruter Sep 17, 2026
6aa115b
Say which contexts the emoji support tests accept
westonruter Sep 17, 2026
8053eb8
Describe the functions the emoji support tests are handed
westonruter Sep 17, 2026
1621a4d
Type the last two loose values in the emoji loader
westonruter Sep 17, 2026
a6466d1
Let the emoji support tests be checked for exhaustiveness
westonruter Sep 17, 2026
727be9d
Report unreachable code from the command line
westonruter Sep 17, 2026
ad9284e
Cover the case which distinguishes skipping a record from abandoning …
westonruter Sep 17, 2026
4158486
Cover the elements the observer must not hand to Twemoji
westonruter Sep 17, 2026
8e3ad60
Say what the unmarked replacement test checks
westonruter Sep 17, 2026
2361670
Correct the Twemoji types against the library as it is vendored
westonruter Sep 17, 2026
98ba5b2
Point wp-emoji at the Twemoji it actually uses
westonruter Sep 17, 2026
39d1361
Merge branch 'trunk' into code-quality/typescript-emoji-loader
westonruter Sep 17, 2026
606df11
Merge branch 'trunk' into code-quality/typescript-emoji-loader
westonruter Sep 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 112 additions & 59 deletions src/js/_enqueues/lib/emoji-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,30 @@

// Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope.

/**
* Emoji Settings as exported in PHP via _print_emoji_detection_script().
* @typedef WPEmojiSettings
* @type {Object}
* @property {?object} source
* @property {?string} source.concatemoji
* @property {?string} source.twemoji
* @property {?string} source.wpemoji
*/
// Note: The WPEmojiSettings and EmojiSupports types are declared in typings/wp-emoji, since wp-emoji.js reads them back.

const selector = 'script#wp-emoji-settings';
const script = document.querySelector( selector );
if ( ! ( script instanceof HTMLScriptElement ) ) {
throw new Error( `Element missing: ${ selector }`);
throw new Error( `Element missing: ${ selector }` );
}
const settings = /** @type {WPEmojiSettings} */ ( JSON.parse( script.text ) );

// 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).
window._wpemojiSettings = settings;

/**
* Support tests.
* Results of the emoji support tests.
*
* @typedef SupportTests
* @type {Object}
* @property {?boolean} flag
* @property {?boolean} emoji
* @property {boolean} flag Whether the browser renders flag emoji.
* @property {boolean} emoji Whether the browser renders emoji.
*/

const sessionStorageKey = 'wpEmojiSettingsSupports';

/** @type {Array<keyof SupportTests>} */
const tests = [ 'flag', 'emoji' ];

/**
Expand All @@ -49,16 +44,18 @@ function supportsWorkerOffloading() {
typeof Worker !== 'undefined' &&
typeof OffscreenCanvas !== 'undefined' &&
typeof URL !== 'undefined' &&
URL.createObjectURL &&
typeof URL.createObjectURL === 'function' &&
typeof Blob !== 'undefined'
);
}

/**
* Support tests as they are stored in session storage.
*
* @typedef SessionSupportTests
* @type {Object}
* @property {number} timestamp
* @property {SupportTests} supportTests
* @property {number} timestamp When the tests were run, in milliseconds since the epoch.
* @property {SupportTests} supportTests What the tests found.
*/

/**
Expand All @@ -72,10 +69,13 @@ function supportsWorkerOffloading() {
*/
function getSessionSupportTests() {
try {
const itemJson = sessionStorage.getItem( sessionStorageKey );
if ( null === itemJson ) {
return null;
}

/** @type {SessionSupportTests} */
const item = JSON.parse(
sessionStorage.getItem( sessionStorageKey )
);
const item = JSON.parse( itemJson );
if (
typeof item === 'object' &&
typeof item.timestamp === 'number' &&
Expand All @@ -95,7 +95,7 @@ function getSessionSupportTests() {
*
* @private
*
* @param {SupportTests} supportTests Support tests.
* @param {SupportTests} supportTests What the tests found.
*/
function setSessionSupportTests( supportTests ) {
try {
Expand All @@ -112,6 +112,51 @@ function setSessionSupportTests( supportTests ) {
} catch ( e ) {}
}

/**
* A 2D context for the support tests.
*
* Which of the two it is depends on the kind of canvas it came from, and the tests use only what
* both provide.
*
* @typedef {CanvasRenderingContext2D|OffscreenCanvasRenderingContext2D} EmojiTestContext
*/

/**
* Checks if two sets of Emoji characters render the same visually.
*
* @callback EmojiSetsRenderIdentically
*
* @param {EmojiTestContext} context 2D Context.
* @param {string} set1 Set of Emoji to test.
* @param {string} set2 Set of Emoji to test.
*
* @return {boolean} True if the two sets render the same.
*/

/**
* Checks if the center point of a single emoji is empty.
*
* @callback EmojiRendersEmptyCenterPoint
*
* @param {EmojiTestContext} context 2D Context.
* @param {string} emoji Emoji to test.
*
* @return {boolean} True if the center point is empty.
*/

/**
* Determines if the browser properly renders Emoji that Twemoji can supplement.
*
* @callback BrowserSupportsEmoji
*
* @param {EmojiTestContext} context 2D Context.
* @param {keyof SupportTests} type Which support test to run.
* @param {EmojiSetsRenderIdentically} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
* @param {EmojiRendersEmptyCenterPoint} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
*
* @return {boolean} True if the browser can render emoji, false if it cannot.
*/

/**
* Checks if two sets of Emoji characters render the same visually.
*
Expand All @@ -127,9 +172,9 @@ function setSessionSupportTests( supportTests ) {
*
* @private
*
* @param {CanvasRenderingContext2D} context 2D Context.
* @param {string} set1 Set of Emoji to test.
* @param {string} set2 Set of Emoji to test.
* @param {EmojiTestContext} context 2D Context.
* @param {string} set1 Set of Emoji to test.
* @param {string} set2 Set of Emoji to test.
*
* @return {boolean} True if the two sets render the same.
*/
Expand Down Expand Up @@ -177,8 +222,8 @@ function emojiSetsRenderIdentically( context, set1, set2 ) {
*
* @private
*
* @param {CanvasRenderingContext2D} context 2D Context.
* @param {string} emoji Emoji to test.
* @param {EmojiTestContext} context 2D Context.
* @param {string} emoji Emoji to test.
*
* @return {boolean} True if the center point is empty.
*/
Expand All @@ -188,7 +233,7 @@ function emojiRendersEmptyCenterPoint( context, emoji ) {
context.fillText( emoji, 0, 0 );

// Test if the center point (16, 16) is empty (0,0,0,0).
const centerPoint = context.getImageData(16, 16, 1, 1);
const centerPoint = context.getImageData( 16, 16, 1, 1 );
for ( let i = 0; i < centerPoint.data.length; i++ ) {
if ( centerPoint.data[ i ] !== 0 ) {
// Stop checking the moment it's known not to be empty.
Expand All @@ -209,14 +254,15 @@ function emojiRendersEmptyCenterPoint( context, emoji ) {
*
* @private
*
* @param {CanvasRenderingContext2D} context 2D Context.
* @param {string} type Whether to test for support of "flag" or "emoji".
* @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
* @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
* @param {EmojiTestContext} context 2D Context.
* @param {keyof SupportTests} type Which support test to run.
* @param {EmojiSetsRenderIdentically} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
* @param {EmojiRendersEmptyCenterPoint} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
*
* @return {boolean} True if the browser can render emoji, false if it cannot.
*/
function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) {
/** @type {boolean} */
let isIdentical;

switch ( type ) {
Expand Down Expand Up @@ -256,7 +302,7 @@ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiR

/*
* Test for English flag compatibility. England is a country in the United Kingdom, it
* does not have a two letter locale code but rather a five letter sub-division code.
* does not have a two letter locale code but rather a five letter subdivision code.
*
* To test for support, we try to render it, and compare the rendering to how it would look if
* the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]).
Expand Down Expand Up @@ -288,8 +334,6 @@ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiR
const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\uDEC8' );
return ! notSupported;
}

return false;
}

/**
Expand All @@ -302,25 +346,30 @@ function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiR
*
* @private
*
* @param {string[]} tests Tests.
* @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification.
* @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
* @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
* @param {Array<keyof SupportTests>} tests Which support tests to run.
* @param {BrowserSupportsEmoji} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification.
* @param {EmojiSetsRenderIdentically} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification.
* @param {EmojiRendersEmptyCenterPoint} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification.
*
* @return {SupportTests} Support tests.
*/
function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) {
let canvas;
/** @type {?EmojiTestContext} */
let context;

if (
typeof WorkerGlobalScope !== 'undefined' &&
self instanceof WorkerGlobalScope
) {
canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement.
// Dimensions are default for HTMLCanvasElement.
context = new OffscreenCanvas( 300, 150 ).getContext( '2d', { willReadFrequently: true } );
} else {
canvas = document.createElement( 'canvas' );
context = document.createElement( 'canvas' ).getContext( '2d', { willReadFrequently: true } );
}

const context = canvas.getContext( '2d', { willReadFrequently: true } );
if ( ! context ) {
throw new Error( 'Unable to obtain a 2D context for the emoji support tests.' );
}

/*
* Chrome on OS X added native emoji rendering in M41. Unfortunately,
Expand All @@ -330,7 +379,7 @@ function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentica
context.textBaseline = 'top';
context.font = '600 32px Arial';

const supports = {};
const supports = /** @type {SupportTests} */ ( {} );
tests.forEach( ( test ) => {
supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
} );
Expand Down Expand Up @@ -361,10 +410,11 @@ settings.supports = {
};

// Obtain the emoji support from the browser, asynchronously when possible.
new Promise( ( resolve ) => {
let supportTests = getSessionSupportTests();
if ( supportTests ) {
resolve( supportTests );
/** @type {Promise<SupportTests>} */
const supportTestsPromise = new Promise( ( resolve ) => {
const sessionSupportTests = getSessionSupportTests();
if ( sessionSupportTests ) {
resolve( sessionSupportTests );
return;
}

Expand All @@ -386,36 +436,39 @@ new Promise( ( resolve ) => {
type: 'text/javascript'
} );
const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } );
worker.onmessage = ( event ) => {
supportTests = event.data;
setSessionSupportTests( supportTests );
worker.onmessage = ( /** @type {MessageEvent<SupportTests>} */ event ) => {
setSessionSupportTests( event.data );
worker.terminate();
resolve( supportTests );
resolve( event.data );
};
return;
} catch ( e ) {}
}

supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
setSessionSupportTests( supportTests );
resolve( supportTests );
} )
const testedSupportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint );
setSessionSupportTests( testedSupportTests );
resolve( testedSupportTests );
} );

supportTestsPromise
// Once the browser emoji support has been obtained from the session, finalize the settings.
.then( ( supportTests ) => {
/*
* Tests the browser support for flag emojis and other emojis, and adjusts the
* support settings accordingly.
*/
for ( const test in supportTests ) {
settings.supports[ test ] = supportTests[ test ];
const key = /** @type {keyof SupportTests} */ ( test );
const supported = supportTests[ key ];

settings.supports[ key ] = supported;

settings.supports.everything =
settings.supports.everything && settings.supports[ test ];
settings.supports.everything && supported;

if ( 'flag' !== test ) {
if ( 'flag' !== key ) {
settings.supports.everythingExceptFlag =
settings.supports.everythingExceptFlag &&
settings.supports[ test ];
settings.supports.everythingExceptFlag && supported;
}
}

Expand Down
Loading
Loading