Repository navigation
Fix: Bug in _get_wptexturize_split_regex and get_html_split_regex - #12403
nickchomey wants to merge 19 commits into
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. 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. Props Bot commandsYou can trigger Props Bot actions by commenting on this PR:
|
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. |
1fe1f0f to
ef547ab
Compare
60bccb3 to
2ab6ba0
Compare
|
I made another commit that modified the regex a bit further |
|
Also took the liberty of addressing a TODO comment in |
704928e to
df24246
Compare
|
I went ahead and added/fixed types and docblocks for all the functions in the two files. Let me know if that was excessive and should be reverted... |
3c74051 to
431ac9c
Compare
IMO, always better to fix these things up while touching the files already, like you're doing here. 👍🏻 |
|
@westonruter is there anything remaining here that is blocking this from getting merged? |
|
@westonruter is this something that might get merged before the 7.2 beta period starts in 8 days? |
…r comment re: Shortcode API Roadmap
431ac9c to
766c02b
Compare
766c02b to
ec4dca1
Compare
|
now that the phpstan warning is resolved, is there anything else that might be blocking merging this? |
|
🤖 Comment by Claude Opus 5.5 A high effort code review was run on this PR. All changes were compared with None of these issues are caught by the existing tests. The Regressions in output1.
wptexturize( '<script>/*<![CDATA[*/ if (a > b && c) {} /*]]>*/</script>' );
// … if (a > b && c) … ← breaks the scriptThe old texturize-only regex had no CDATA branch. It stopped at the first 2. A quote in a tag can pair with a quote after the tag's real In the possessive pattern wptexturize( "<p>Hi</p>\n<a href=\"/x\" title=it's>It's here</a>" );
// <a href=”/x” title=it’s>A stray quote such as The same mis-split affects other 3. Unclosed tags are no longer protected (~L682) Removing the optional wptexturize( '<p>Truncated <a href="http://example.com/it\'s' );
// href=”http://example.com/it’sThe test edits confirm the change:
Scope4. To fix the Tailwind A fix limited to the split, so that 5. A chunk starting with The As a result, whether a mis-split tag is texturized depends on what comes before it. The markup from item 2 is left alone at the very start of the input, but it is texturized after Performance and maintainability6. A PHP closure now runs for every quoted attribute value (~L258)
7. Long tags match about 3× slower (~L682)
Every 8. This depends silently on A shared private helper for the inner pattern would avoid this, and Items 1–3 are worth addressing first, each with a regression test. |
|
@nickchomey I'm probably not the best to review this further. This is begging for a review from @dmsnell. It may actually become obsolete with KSES being attempted to be shied away from, although I'm not actually sure if |
dmsnell
left a comment
There was a problem hiding this comment.
@nickchomey I recommend splitting this as the PR currently addresses a number of independent issues.
You can also look at #9270 where I explored replacing wp_html_split() with the HTML API, and #9851 where I was looking at ways we can remove dependency on this function altogether and think about a future where it’s deprecated.
Thanks for your patience with my review pace. I was out of office for a week which didn’t help. There is probably a helpful change in here, but I want to make sure we’re careful and not replacing one broken ad-hoc HTML parser with another before thinking it through. Ultimately, all of these functions are fundamentally broken and need to be replaced with functionality based on the HTML API (or any other sound HTML parsing). Shortcodes are a thorn in that effort for wptexturize() (and I think we will need to make some grammatical changes to them to fix it, which has happened before).
Side note: there’s substantial noise from unrelated type annotations. Those would be more helpful in a separate PR, as those seem mostly non-controversial and don’t change the runtime of WordPress. if we have to revert the change, would be best not to revert those out of necessity.
|
Thanks for the review! I'll try to review and address your feedback in the coming week. |
Bring the PR up to date with trunk (previously merged Oct 13, now at Oct 23) before reworking it per review from westonruter and dmsnell. Conflict resolution: tests/phpunit/tests/formatting/wpHtmlSplit.php and wpTexturize.php changed on both sides since the last merge -- trunk modernized the suite (static data providers, typed signatures) while this branch added quoted-attribute tests plus type churn. Resolutions here keep the PR-side content verbatim, so this merge is behavior-neutral relative to the PR head apart from trunk updates. The review-driven rework (minimal hotfix, test replacement, churn reverts) follows in the next commit, keeping merge and fix separate for review.
Reworks the fix per review from westonruter and dmsnell. get_html_split_regex() and _get_wptexturize_split_regex() now use an =-aware element pattern. Quotes only become special after "=", so a > inside a quoted attribute value no longer ends the tag, while quote-named attributes such as <div "=noquote id="quote"> keep their previous parse. Unquoted values use [^> \t\f\r\n]*+ as dmsnell suggested. The trailing >? keeps the "if not found, match all input" behavior for unclosed tags. Reverted from the previous version of this PR: - The &-preserving callback in wptexturize(). & in attribute values is encoded as before (browsers decode &WordPress#38; identically). - Deriving the texturize pattern from get_html_split_regex() via substr(). Each function keeps its own pattern, so no CDATA handling leaks into wptexturize(). - Changes to existing test expectations for invalid HTML and the REST comment roundtrip. - Type and docblock changes to existing tests, which move to their own PR. #43785 (raw text inside <script>) is not addressed; it needs HTML API based segmentation. See #63997, #57381, #63426.
Follow-up to the previous commit, for the class of bug westonruter raised in review item 2: a quote pairing with a quote after the tag's real >. In "<p>x<=' then</p><p>don't</p>", the < is not followed by a tag name, so it is text in HTML. The =-aware pattern nonetheless read "='" as the start of a quoted value and paired it with the apostrophe in "don't", which left that text untexturized. Trunk handled this correctly. The quote handling now applies only when a letter follows the <. Any other < matches with trunk's [^>]*>? exactly as before. See #63997.
|
(this comment is from Opus 5.5. But I guided and reviewed all of the work that went into the two new commits) @westonruter @dmsnell I've reworked this PR in response to your reviews. It's now a much smaller hot-fix: only the element pattern in the two split regexes changes. I added commits on top of the existing branch rather than rebasing.
Type and docblock changes to the existing tests are now in their own PR, #14898. Weston's review items
What changes compared with trunk. This is to answer the "how do we know it's better overall" question. The pattern only differs from trunk when all three of these are true:
In that case the tag now ends at the real One case to be aware of: when a quoted value is never closed, as in Testing. The #43785 (raw text inside |
| * @ticket 63997 | ||
| */ | ||
| public function test_ampersand_in_cdata_is_not_encoded(): void { | ||
| $input = '<script>/*<![CDATA[*/ if (a > b && c) {} /*]]>*/</script>'; |
There was a problem hiding this comment.
this is not a CDATA section, so the test is claiming to test one thing and actually testing another, which is whether the ampersand is encoded within SCRIPT contents.
| public function test_quote_after_non_tag_lt_does_not_pair_across_gt(): void { | ||
| $this->assertSame( | ||
| "<p>x<=' then</p><p>don’t</p>", | ||
| wptexturize( "<p>x<=' then</p><p>don't</p>" ) |
There was a problem hiding this comment.
I find this test confusing, particularly because it’s not obvious to me from the input and expected output what the failure mode would be. I guess the expectation is that the =' then</p><p>don' would be treated as a single attribute?
these cases can be transformed into self-documented ones just by adding some description.
"Every <p> is a real <p> <=' ain't no attributes <p> either</p>"| * @ticket 57381 | ||
| */ | ||
| public function test_ampersand_in_quoted_attribute_value_is_encoded(): void { | ||
| $this->assertSame( |
There was a problem hiding this comment.
This should use assertEquivalentHTML() since it’s incidental that the ampersand is encoded as & rather than & or & or the variations of those forms.
then, the test would need to assert that the original unquoted ampersands no longer exist.
assertStringNotContainsString( $texturized, '&a' );if that assertion doesn’t exist it will need to be emulated.
| * @param string $output Expected output. | ||
| */ | ||
| public function test_gt_in_quoted_attribute_values( string $input, string $output ): void { | ||
| $this->assertSame( $output, wptexturize( $input ) ); |
There was a problem hiding this comment.
in general, we should be leaning on assertEquivalentHTML() when comparing HTML. this prevents over-fitting in tests which lead to fragile and flakey test suites.
| wp_html_split( $input ) | ||
| ); | ||
| } | ||
|
|
There was a problem hiding this comment.
I reviewed the wptexturize() tests first, but some of the same comments apply to this file.
| . '|' | ||
| . '=[ \t\f\r\n]*' // Attribute value follows. | ||
| . '(?:' | ||
| . '"[^"]*"' // Double-quoted value, which may contain >. |
There was a problem hiding this comment.
which may contain
>
this is relevant to the PR and ticket under discussion, but largely a distraction for the code it’s commenting. the values may also contain a and ? and ☂️ but we don’t call those out.
the PCRE snippet is obvious, it’s enough to label it as a double-quoted attribute.
| . '|' // ...else not a tag, so quotes are not special. | ||
| . '[^>]*+' | ||
| . ')' | ||
| . '>?'; // End of element. If not found, match all input. |
There was a problem hiding this comment.
This PR is now only this pattern, which is your recommended hot-fix
I’m not sure if this comment is already outdated, or was based on an idea that you didn’t end up pushing out, but this pattern extends far beyond my recommendation for a hot-fix but falls short of using the HTML API for parsing. in essence, it’s picking some of the worst tradeoffs of each option and combining them.
maybe you had another thought on this, so I’d like to understand better what was in your head when you wrote this.
Per dmsnell's review, the =-aware pattern with a letter check went
beyond the hot-fix he recommended. This replaces it with the smallest
change to trunk's element pattern: skip a quoted value that follows
"=", otherwise behave as [^>]*>? did.
(?:[^>=]++|=[ \t\f\r\n]*+(?:"[^"]*+"|'[^']*+')?)*+>?
Only quoted values after "=" are treated specially, so quote-named
attributes and unquoted values such as title=it's split as before, and
unclosed tags still match all remaining input.
Tests are reduced to self-describing cases for the tickets. The
texturize case uses assertEqualHTML().
See #63997, #57381, #63426.
The previous commit regressed a case trunk handles: in "<p>x <=' y</p><p>isn't</p>", the < does not start a tag, but the quote after "=" was read as an attribute value and paired with the apostrophe in "isn't", so that text was not texturized. Consume the tag name first with [^>\t\n\f\r \/]*+ (the same characters HTML stops a tag name at). For a real tag, that stops before the attributes. For "<=" it consumes the "=" itself, so no quoted value can follow it and the split matches trunk. See #63997.
|
@dmsnell I apologize for how much this drifted from the direction you gave in your previous review. I used an AI agent to rework this PR after your and Weston's reviews. I was reviewing its output, but I lost sight of what this PR was meant to do while trying to address every piece of feedback (which I squashed from about 12 to 1 commit before pushing). That's how it became a complete overhaul that tried to handle every edge case that had been mentioned, plus some new ones. I also resolved your threads myself before you had a chance to look at the changes, which I shouldn't have done. I'm sorry for the extra review time this cost you. I've reduced the PR to a single change to trunk's After the tag name, a quoted value following The leading One other difference from trunk remains. An unclosed quoted value, as in On the tests, the misnamed CDATA test is gone, the cases are self-describing, and the It's entirely possible that this regex is still inappropriate. If you have suggestions to improve it, or would like any other changes, including dropping the tag-name part, please let me know. I'm admittedly somewhat out of my depth with these regexes, which is all the more reason I appreciate the HTML API work you've been doing. Thank you for your patience with this. I'd appreciate another look when you have time. |
|
Oops, i pushed the wrong commit that cut out a bunch of tests (was trying to isolate some things). Will fix shortly. |
The previous reduction dropped too many tests, including ones guarding behavior the reviewers asked to keep. Restores the ticket reproductions for #63997, #57381 and #63426, and adds providers for inputs that must split and texturize as on trunk: an apostrophe in an unquoted value (title=it's) after an earlier tag, a stray quote after an unquoted value, an unclosed tag, a quote in an attribute name, and a quote after a < that does not start a tag. wptexturize() comparisons use assertEqualHTML(). See #63997.
No sweat here. The issue isn’t even the PCRE pattern you are updating, it’s the entire system on top of which it stands, and managing expectations. 🙃 Let’s give a think-over on this for another couple days; I think it’s probably ready, and should be able to make it into WordPress 7.2. Your diligence through the review feedback is great, so thank you. I think the current modification is the right amount of change without raising questions about all the other broken parsing cases. |
|
@dmsnell ah, i pushed another commit that restored some changes before I saw your reply. I honestly have lost track of how this PR has evolved. Its probably best for me to just look at the overall diff relative to trunk going forward, which is relatively small now. So, it shouldn't be too difficult to refine this in your desired direction. I think your previous review comments arent even all that relevant anymore, but I'll leave them for you to resolve. |
In
get_html_split_regex()and_get_wptexturize_split_regex(), the normal-element pattern[^>]*>?ends a tag at the first>, even when that>is inside a quoted attribute value. The rest of the attribute then leaks out as text:wptexturize()curls its quotes, andwp_html_split()callers see a broken tag.This PR replaces that one pattern in each function. The new pattern is attribute-aware:
=, so quote-named attributes (<div "=noquote id="quote">) parse as before.<, so<=,<3and</p>split exactly as on trunk.>?keeps the "if not found, match all input" behavior for unclosed tags.There are no other behavior changes.
&encoding, CDATA handling, and existing test expectations are all unchanged from trunk. Type and docblock changes to existing tests are in #14898.Not addressed: #43785, which is about raw text inside
<script>. Fixing it needs HTML API–based segmentation.Trac tickets:
https://core.trac.wordpress.org/ticket/63997
https://core.trac.wordpress.org/ticket/57381
https://core.trac.wordpress.org/ticket/63426
Use of AI Tools
AI assistance: Yes
Tool(s): VSCode Copilot Chat, OpenCode
Model(s): Deepseek V4 Flash, Claude Opus 5.5
Used for: Diagnosis, the initial fix, and reworking it in response to review. I reviewed and tested the result.
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.