Make WordPress Core

Opened 2 days ago

Last modified 16 hours ago

#66120 new defect (bug)

wp-exclude-emoji is ignored when the excluded element is the node being parsed

Reported by: kimjiwoon Owned by:
Priority: normal Milestone: 7.2
Component: Emoji Version: 6.2
Severity: normal Keywords: has-patch has-unit-tests
Cc: Focuses:

Description (last modified by kimjiwoon)

#52219 made wp-emoji.js skip elements with the wp-exclude-emoji class ([55186]) through a doNotParse() callback added to twemoji.js. That callback is only consulted in grabAllTextNodes(), for the child elements of the node passed to parse(). The node passed to parse() itself is never checked.

The MutationObserver in wp-emoji.js passes exactly such a node: for an added text node it moves up to the parent and calls parse( node ) on it. So an excluded element is parsed, and its emoji replaced, whenever it becomes that node:

  • the excluded element itself is inserted after the page has loaded, or
  • its own text is replaced, for example with textContent.

To reproduce, load a front-end page where the emoji fallback script is active (on current releases #66104 makes that every browser; otherwise one where a support test fails, such as Chromium on Windows for flag) and run in the console:

const span = document.createElement( 'span' );
span.className = 'wp-exclude-emoji';
span.textContent = '\uD83C\uDDF0\uD83C\uDDF7'; // Flag: South Korea
document.body.appendChild( span );

After the observer runs, the span contains an img.emoji whose alt is the original South Korea flag and whose src is https://s.w.org/images/core/emoji/17.0.2/svg/1f1f0-1f1f7.svg.

What I measured on a local page in Chromium on Windows with Core's wp-emoji.js, twemoji.js and emoji-loader.js (with the #66104 patch, so only flag failed), counting img.emoji after 1.5 seconds:

Case Images
span.wp-exclude-emoji in the initial HTML 0
the same span appended after load 1
the initial span's textContent replaced 1
div > p > span.wp-exclude-emoji appended in one piece 0
the span appended inside a wrapper element 0
plain text appended (control) 1

Reading the source, an element or text added anywhere inside an excluded element should be replaced the same way, since its parent becomes the node passed to parse(); I have not tested that case.

A possible fix is to skip the node in the observer when it is inside an excluded element, for example node.closest( '.wp-exclude-emoji' ) before parse( node ), or to apply doNotParse() to the node itself and its ancestors in parse(). The first keeps the change out of the vendored twemoji.js.

This matters for anything that inserts text the fallback should leave alone after load: editors on the front end (the reason for #52219), and code that draws emoji another way and marks it with this class. Wider context in Gutenberg discussion #83032.

Change History (5)

#1 @kimjiwoon
2 days ago

  • Description modified (diff)

This ticket was mentioned in PR #13568 on WordPress/wordpress-develop by @irozum.


45 hours ago
#2

  • Keywords has-patch has-unit-tests added; needs-patch removed

The MutationObserver in wp-emoji.js only checks doNotParse() (which excludes elements carrying the wp-exclude-emoji class) for the *descendants* of the node it hands to Twemoji's parse() — the node itself is never checked. When a node observed by the mutation observer turns out to be an excluded element itself (because it was appended whole after page load, its text was replaced, or text was added inside one of its descendants), Twemoji still parses it and replaces the emoji it contains with an image, even though the element is marked to be skipped.

This adds an isExcludedFromParsing() check in wp-emoji.js's mutation observer callback that walks up from the mutated node through its ancestors, so a node that is itself excluded (or nested inside an excluded ancestor) is skipped before it's ever handed to parse(). The vendored twemoji.js is untouched — its own doNotParse() callback continues to work exactly as before for descendants encountered during the recursive walk.

Added three QUnit tests in a dedicated tests/qunit/wp-includes/js/emoji.html page (rather than the shared index.html suite) covering: an excluded element appended whole after load, an excluded element whose text is replaced after load, and text appended inside an already-excluded ancestor.

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Sonnet 5
Used for: Implementation, tests, and PR description. Reviewed by Igor Rozum.

#3 follow-up: @westonruter
38 hours ago

  • Milestone Awaiting Review7.2

Before working on this, I want to first update the emoji JS to be added to the files being checked by TypeScript (#65997).

I'll open a ticket/PR for that separately.

#4 @waneezashafiq32
38 hours ago

I tested PR #13568 using WordPress Playground (?core-pr=13568). I confirmed that appending a <span class="wp-exclude-emoji"> element with a flag emoji after page load correctly skips emoji parsing. The img.emoji count is 0, which verifies the fix works as expected. Before the patch, the count was 1.

Last edited 38 hours ago by waneezashafiq32 (previous) (diff)

#5 in reply to: ↑ 3 @westonruter
16 hours ago

Replying to westonruter:

Before working on this, I want to first update the emoji JS to be added to the files being checked by TypeScript (#65997).

I'll open a ticket/PR for that separately.

See #66131.

Note: See TracTickets for help on using tickets.