wptexturize(): fix apostrophe curling to an opening quote after an opening inline tag#12645
wptexturize(): fix apostrophe curling to an opening quote after an opening inline tag#12645irozum wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes wptexturize() incorrectly curling a leading apostrophe as an opening quote when it appears immediately after an opening inline HTML tag, by carrying minimal cross-token “previous text ended in a word character” state across delimiter boundaries.
Changes:
- Adds cross-token state in
wptexturize()to treat a leading'after recognized opening inline tags as a word-continuing apostrophe (Trac #43810). - Introduces a private helper
_wptexturize_is_inline_opening_tag()to detect eligible inline opening tags. - Adds PHPUnit coverage for apostrophes after opening inline tags.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/wp-includes/formatting.php |
Tracks word-continuation state across inline opening tags and adds an inline-tag detection helper. |
tests/phpunit/tests/formatting/wpTexturize.php |
Adds new data-driven tests covering the reported inline-opening-tag apostrophe curling bug and related cases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Replace each & with & unless it already looks like an entity. | ||
| $curl = preg_replace( '/&(?!#(?:\d+|x[a-f0-9]+);|[a-z1-4]{1,8};)/i', '&', $curl ); | ||
|
|
||
| $prev_text_ends_in_word_char = 1 === preg_match( '/[a-zA-Z0-9]$/', $curl ); |
| // Parse out the tag name, the same way _wptexturize_pushpop_element() does for opening tags. | ||
| $space = strpos( $text, ' ' ); | ||
| if ( false === $space ) { | ||
| $space = -1; | ||
| } else { | ||
| --$space; | ||
| } | ||
| $tag = substr( $text, 1, $space ); | ||
|
|
||
| return in_array( $tag, $inline_elements, true ); |
| return array( | ||
| // The reported bug: a contraction split across an inline tag boundary. | ||
| array( | ||
| "I<strong>'ve been</strong>", | ||
| 'I<strong>’ve been</strong>', | ||
| ), | ||
| array( | ||
| "That<em>'s</em> right.", | ||
| 'That<em>’s</em> right.', | ||
| ), | ||
| // Nested inline tags still continue the word. | ||
| array( | ||
| "I<strong><em>'ve been</em></strong>", | ||
| 'I<strong><em>’ve been</em></strong>', | ||
| ), |
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe 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
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
|
Nice work and good to see this getting some attention! This is the opening-boundary mirror of #18549 (PR #12249), which handles a quote/apostrophe right after a closing inline tag ( On the tag lists differing from #12249's — I'd leave yours as-is rather than force parity. This fix only fires when a word is split across an inline boundary (realistically, bolding an English-language contraction or similar), so the common formatting tags you list cover it. The extra tags #12249 recognizes ( On the Copilot notes: The Unicode one is worth taking. Core's apostrophe-in-a-word pattern is The case/whitespace note I'd leave. This helper mirrors |
This fixes the "opening tag" half of the bug described on this ticket — the mirror image of #18549 (PR #12249), which covers the closing-tag case.
wptexturize() splits text on HTML tag boundaries and processes each resulting token independently. When a token starts with a straight quote/apostrophe immediately after the opening tag of an inline element (e.g. bolding a contraction), the word before the tag is in a separate token, so the apostrophe-in-a-word pattern can't see it — the quote is read as being at the start of a string and curls as an opening quote instead:
This adds a small piece of cross-token state to wptexturize(): whether the text immediately preceding the current position ends in a letter or digit. When that's true and the very next tag is the opening tag of a recognized inline element (a, abbr, b, cite, del, em, i, ins, mark, q, s, small, span, strong, sub, sup, time, u), a leading apostrophe in the following text token is treated as continuing that word rather than opening a new quoted phrase.
Block-level tags are deliberately excluded — their boundary already reads as a break between sentences, so adjacency there doesn't imply word continuation (e.g.
word<div>'ve been</div>still curls as an opening quote). A space before the tag, or no preceding text at all (e.g.<em>'Hello'</em>), also keeps the existing opening-quote behavior.Double quotes are out of scope here — there's no realistic mid-word continuation case for
", unlike the apostrophe/contraction case this targets.Note: the recognized inline-tag list here (
a, abbr, b, cite, del, em, i, ins, mark, q, s, small, span, strong, sub, sup, time, u) was chosen independently of #18549's PR #12249, since that PR hasn't landed and its exact list isn't available from the ticket discussion alone. Worth reconciling the two lists if both land.Trac ticket: https://core.trac.wordpress.org/ticket/43810
Testing
Adds
test_apostrophe_after_opening_inline_tag()with 6 cases toTests_Formatting_wpTexturizecovering: the reported bug, a second inline tag (<em>), tags nested two deep, the no-preceding-word case, the space-before-tag case, and a block-level tag. FullTests_Formatting_wpTexturizeclass (363 tests) and the broaderformattinggroup (2077 tests) pass with no regressions. PHPCS (WordPress-Core ruleset) and PHPStan are clean on the changed files.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 irozum.
This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.