Skip to content

Emoji: Skip ignored tags with attributes in wp_staticize_emoji(). - #13918

Open
sanjithsarkar wants to merge 4 commits into
WordPress:trunkfrom
sanjithsarkar:fix/66134-staticize-emoji-ignored-tags
Open

sanjithsarkar wants to merge 4 commits into
WordPress:trunkfrom
sanjithsarkar:fix/66134-staticize-emoji-ignored-tags

Conversation

@sanjithsarkar

@sanjithsarkar sanjithsarkar commented Oct 2, 2026 •

Copy link
Copy Markdown

Trac ticket: https://core.trac.wordpress.org/ticket/66134

Summary

Emoji inside a <pre> or <code> element that has attributes were converted to <img> in feeds and email. That affects the Preformatted (<pre class="wp-block-preformatted">) and Verse (<pre class="wp-block-verse">) blocks, and Code blocks whose inner <code> has a class, for example from a syntax-highlighting plugin. A default Code block is not affected, because its inner bare <code> already starts an ignored section.

wp_staticize_emoji() found tags by splitting the content on /(<.*>)/U and only recognized an ignored tag when > came right after the tag name. Following @westonruter's review, this PR no longer patches that regular expression. wp_staticize_emoji() now walks the content with WP_HTML_Tag_Processor and replaces emoji only in text nodes. That fixes the original report and also handles > inside attribute values and markup inside HTML comments.

convert_smilies() has the same loop. In this PR it only gets the smaller regular expression fix; see "Open question" below.

What Changed

File Change
src/wp-includes/formatting.php, wp_staticize_emoji() The preg_split() loop is replaced by a WP_HTML_Tag_Processor loop over next_token(). Emoji are replaced in #text tokens only. Text inside CODE and PRE is skipped, with a depth counter so the section ends at its own closing tag.
same function An anonymous subclass adds get_raw_text() and replace_raw_text(), because set_modifiable_text() escapes its input and cannot insert an <img>. It uses a bookmark span and WP_HTML_Text_Replacement, the same way as the subclasses in comment.php and script-loader.php.
src/wp-includes/formatting.php, convert_smilies() Unchanged from the earlier revision of this PR: [^>]* → (?:[\s\/][^>]*)? with the i flag for the opening tag, and a case-insensitive closing-tag check that allows whitespace.
tests/phpunit/tests/formatting/emoji.php:390 test_wp_staticize_emoji_ignores_tags_with_attributes: the 5 ignored tags, with and without attributes
tests/phpunit/tests/formatting/emoji.php:427 test_wp_staticize_emoji_ignore_block_boundaries: 18 cases that count class="wp-smiley" in the output. 9 are new in this revision.
tests/phpunit/tests/formatting/convertSmilies.php:176 test_ignore_smilies_block_boundaries: 6 cases for convert_smilies()

Testing

All of these are covered by the new tests and pass:

  • <pre class="wp-block-code">&#x1f642;</pre>&#x1f642; → only the second emoji is converted
  • <pre class="wp-block-code"><code>&#x1f642;</code></pre> → nothing converted
  • <code-snippet>&#x1f642;</code-snippet>&#x1f642; → both converted (same for <preview> and <script-loader>)
  • <PRE class="foo">&#x1f642;</PRE>&#x1f642; and <pre>&#x1f642;</pre >&#x1f642; → only the second is converted
  • <img alt="1 > 0 &#x1f642;" src="a.png">&#x1f642; → only the second is converted; the attribute is left alone
  • <pre title="a > b">&#x1f642;</pre>&#x1f642; → only the second is converted
  • <!-- 1 > 0 &#x1f642; -->&#x1f642; → only the second is converted
  • <!-- > <pre> -->&#x1f642; → converted; a <pre> in a comment does not start an ignored section
  • <pre><!-- > </pre> -->&#x1f642;</pre>&#x1f642; → only the last is converted
  • <code><code>&#x1f642;</code>&#x1f642;</code>&#x1f642; → only the last is converted
  • 1 < 2 &#x1f642; <p>&#x1f642;</p> → both converted

Results of the new tests on each version, run against current trunk:

Code Emoji tests Smilies tests
trunk 17 failures 5 failures
#13598 11 failures 5 failures
This PR pass pass
  • vendor/bin/phpunit --group formatting with trunk merged in → 2,295 tests pass. The 5 warnings come from wpStripAllTags and wpTrimExcerpt tests, which this PR doesn't touch.
  • PHPCS passes on the changed files.
  • Block markup with 🙂, through wp_staticize_emoji() and through the the_content_feed filter:
Block trunk This PR
Preformatted (<pre class="wp-block-preformatted">) converted to <img> left as is
Verse (<pre class="wp-block-verse">) converted to <img> left as is
Code block with <code class="language-php"> converted to <img> left as is
Default Code block (<pre class="wp-block-code"><code>) left as is left as is
Paragraph converted to <img> converted to <img>

The earlier revision was also checked in a real /?feed=rss2 response. That check has not been repeated for this revision.

Performance

wp_staticize_emoji() runs on feeds and HTML email, and returns early when the text has no emoji, so the Tag Processor only runs on content that contains emoji. Time per call, measured in the local Docker environment (amd64 emulation, so compare the columns rather than read the absolute values):

Content trunk This PR
55 KB, no emoji 0.08 ms 0.08 ms
3 KB, 10 emoji 3.83 ms 4.50 ms
55 KB, 1 emoji 57.4 ms 59.8 ms
55 KB, 200 emoji 61.9 ms 76.3 ms

Most of the time on both sides is the existing scan of the emoji list, not the HTML loop.

Behavior Changes

  • Emoji in the contents of TITLE, IFRAME, XMP, NOEMBED and NOFRAMES are no longer converted. The Tag Processor does not report their contents as text nodes, the same as SCRIPT, STYLE and TEXTAREA. An <img> is not valid there anyway.
  • If the content ends in an incomplete tag or comment, such as a trailing <a href="x, the Tag Processor stops there and that tail is left as is. Text before it is still processed.
  • A < that does not start a tag, as in 1 < 2, no longer hides the text after it from conversion.
  • convert_smilies(): since [45569], <code-snippet>, <preview> and similar tags have stopped smiley conversion for the rest of the content. Content with these tags will now get smilies converted after them.

Open question: convert_smilies()

convert_smilies() fails the same new cases as the old emoji loop (> in attribute values, markup in comments, nested ignored tags, < in text). I converted it to the Tag Processor locally and the tests pass, but I left it out of this PR for two reasons:

  • It is outside the ticket, which is about wp_staticize_emoji().
  • It runs on the_content, the_excerpt, comment_text and block template output on every page view, with no early return. Tokenizing a 55 KB post took about 8 ms with the Tag Processor against 1.8 ms with the regular expression split in the same environment.

I can add it here or open a follow-up ticket, whichever reviewers prefer.

Anything the Reviewer Should Know

  • Builds on Emoji: Don't staticize emoji in ignored tags that have attributes. #13598 and includes its test. Credit goes to @praxxiii / prakritee08.
  • The anonymous subclass relies on the protected $bookmarks, $lexical_updates and $html properties. If the HTML API has or gets a public way to replace a text node with raw HTML, this should use it instead.
  • To see the bug on trunk, use a Preformatted or Verse block. A default Code block already works on trunk, because the bare <code> inside it is matched by the old pattern.

Use of AI Tools

`wp_staticize_emoji()` only recognized an ignored tag (code, pre, style,
script, textarea) when `>` came right after the tag name. Emoji inside
tags with attributes, such as the Code block's
`<pre class="wp-block-code">`, were converted to `<img>` in feeds and
email.

Copying the `[^>]*` pattern from `convert_smilies()` ([45569] / #47489)
would fix that, but it also matches longer tag names that start with an
ignored name, such as `<code-snippet>`, `<preview>` or `<script-loader>`.
The matching closing tag never appears, so emoji or smilies in the rest
of the content stop being converted.

The opening-tag pattern now requires whitespace, `/` or `>` after the
tag name and ignores case. The closing-tag check now also ignores case
and allows whitespace before `>`. Both changes are applied to
`wp_staticize_emoji()` and `convert_smilies()`.

Props prakritee08, therssoftware, westonruter.
Fixes #66134.
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @praxxiii.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

Core Committers: Use this line as a base for the props when committing in SVN:

Props sanjith2023, therssoftware, mukesh27, westonruter.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.


Props Bot commands

You can trigger Props Bot actions by commenting on this PR:

  • @propsbot add <username> to give props to a GitHub user. Requires write access.
  • @propsbot remove <username> to leave a user out of the props. Requires write access.
  • @propsbot refresh to refresh this list. Adding the props-bot label works too.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@sanjithsarkar

Copy link
Copy Markdown
Author

@propsbot refresh

@sanjithsarkar

Copy link
Copy Markdown
Author

@mukeshpanchal27 This PR builds on #13598 and includes its test. Could you add props for @praxxiii (prakritee08 on WordPress.org)? @propsbot add needs write access.

The description is updated with full test results, including a feed check against trunk. Ready for another look when you have time.

@peterwilsoncc

Copy link
Copy Markdown
Contributor

@propsbot add @praxxiii
@propsbot add @robelsust

@westonruter westonruter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still seems brittle. This should not be using regex at all, but rather should be re-developed using WP_HTML_Tag_Processor. This will ensure that tags are handled that contain > in an attribute value. It will also account for markup inside of HTML comments. Neither of these are currently accounted for, as far as I can see.

…i()`.

Replace the regular expression HTML loop with the Tag Processor, so that
emoji are only staticized in text nodes. This handles `>` in attribute
values and markup inside HTML comments, neither of which the previous
loop accounted for. Text inside `CODE` and `PRE` elements is still
skipped, now with nesting taken into account.

See #66134.
@sanjithsarkar

Copy link
Copy Markdown
Author

@westonruter Thanks for the review. wp_staticize_emoji() now uses WP_HTML_Tag_Processor instead of the regex loop (8ae84b5), with tests for > in attribute values and markup inside comments. The description is updated with results and timings.

One question: convert_smilies() fails the same cases, but it runs on the_content on every page view and the Tag Processor measured about 5x slower there. Should I convert it in this PR, or leave it for a follow-up ticket?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants