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 )
#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)
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
#3
follow-up:
↓ 5
@
38 hours ago
- Milestone Awaiting Review → 7.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
@
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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
The
MutationObserverinwp-emoji.jsonly checksdoNotParse()(which excludes elements carrying thewp-exclude-emojiclass) for the *descendants* of the node it hands to Twemoji'sparse()— 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 inwp-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 toparse(). The vendoredtwemoji.jsis untouched — its owndoNotParse()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.htmlpage (rather than the sharedindex.htmlsuite) 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.