Opened 4 years ago
Last modified 2 days ago
#57381 new defect (bug)
Filter wptexturize brokes tailwindcss brackets
| Reported by: | ArtZ91 | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | Awaiting Review |
| Component: | Formatting | Version: | |
| Severity: | normal | Keywords: | reporter-feedback has-patch has-unit-tests |
| Cc: | Focuses: |
Description
Hi there. I am developing WordPress Theme with TailwindCss frontend and gutenberg blocks.
My gutenberg block HTML was partially broken by wptexturize filter of the_content function.
Affected code:
<div class="swiper [&>.swiper-pagination]:static">...</div>
Result:
<div class="swiper [&>.swiper-pagination]:static»>...</div>
TailwindCss docs: https://tailwindcss.com/docs/hover-focus-and-other-states#using-arbitrary-variants
My current solution is remove this filter before calling the_content for my gutenberg page template:
<?php
$content = get_post_field( 'post_content', get_the_ID() );
remove_filter('the_content', 'wptexturize');
the_content();
?>
Change History (16)
#2
@
3 years ago
Hi,
I'm use tailwindcss inside ACF Blocks templates like template_parts/block/<block_name>/block.php.
The problem that some custom tailwindcss selectors can use ampersand symbol.
I think I can avoid of using ampersand by creating custom class definition in my stylesheet file.
This ticket was mentioned in PR #5697 on WordPress/wordpress-develop by co6x0.
3 years ago
#3
- Keywords has-patch has-unit-tests added
Ensures valid HTML is worked correctly by wptexturize(), wp_html_split(), etc.
I started working on this PR when I noticed that using TailwindCSS child selectors would break the layout of block theme (also reported in Trac ticket: 57381).
I have identified a problem with the regular expression defined in _get_wptexturize_split_regex() used in wptexturize().
This problem seemed to be affecting get_the_block_template_html() and causing the block theme layout collapse described above.
Changing this regex fixes the layout issue.
Also, wp_html_split() uses almost the same regex.
Other trac tickets caused by this function will also be fixed by updating to a similar regex.
According to the HTML reference at html.spec.whatwg.org, attribute values can contain a variety of characters.
With this in mind, I have modified the regex to exclude matching characters within quotation marks.
This fixes the misplacement of GREATER-THAN SIGN(>) and prevents other valid HTML structures from being mishandled.
I've included tests to cover these changes in tests/phpunit/tests/formatting/wpTexturize.php and tests/phpunit/tests/formatting/wpHtmlSplit.php. If there's anything I've missed, please let me know.
Trac ticket: https://core.trac.wordpress.org/ticket/57381
Trac ticket: https://core.trac.wordpress.org/ticket/45387
Trac ticket: https://core.trac.wordpress.org/ticket/43457
3 years ago
#4
Added commit.
Removed tranformation of & to & in HTML attribute values modified by <https://core.trac.wordpress.org/ticket/35008>.
This ticket seems to have been created because the W3C HTML Validator found it to be invalid HTML, but as of now, the & in the URL is valid.
2 years ago
#6
howdy! just wanted to stop by and mention that I've been exploring updating these same functions using the HTML API, which provides a full spec-compliant parse of the HTML stream.
You can find some rough notes on the broader roadmap
2 years ago
#7
@dmsnell
Thank you for letting me know.
Handling this with regular expressions has been challenging, so it would be wonderful if we could address it using the HTML API.
Please let me know if there's anything I can help with!
I will close this PR now.
This ticket was mentioned in PR #12403 on WordPress/wordpress-develop by @nickchomey.
3 months ago
#8
There was an error in the regex used in _get_wptexturize_split_regex() and get_html_split_regex().
As shown in the associated trac ticket, if an HTML attribute contained >, the old regex treated it as the closing > for the element — the pattern [^>]*> simply consumed everything up to the first > character.
The new regex (?:"[^"]*"|'[^']*'|[^>])*+>? places quoted attribute values before the generic character matcher, so "..." and '...' spans are consumed as atomic units - any > inside them is absorbed into the value rather than terminating the tag. The generic [^>] at the end acts as a catch-all for everything else (tag names, unquoted attributes, malformed fragments like <'word), and the possessive *+ on the outer group prevents backtracking issues.
Trac tickets:
https://core.trac.wordpress.org/ticket/57381
https://core.trac.wordpress.org/ticket/57381
https://core.trac.wordpress.org/ticket/43785
https://core.trac.wordpress.org/ticket/63426
## Use of AI Tools
AI assistance: Yes
Tool(s): VSCode Copilot Chat
Model(s): Deepseek V4 Flash
Used for: Diagnosis and implementation of fix. I reviewed and tested it.
@nickchomey commented on PR #12403:
3 months ago
#9
Also took the liberty of addressing a comment in _get_wptexturize_split_regex to replace it with get_html_split_regex(). All tests still pass.
@nickchomey commented on PR #12403:
3 months ago
#10
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...
@westonruter commented on PR #12403:
3 months ago
#11
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...
IMO, always better to fix these things up while touching the files already, like you're doing here. 👍🏻
@nickchomey commented on PR #12403:
3 months ago
#12
@westonruter is there anything remaining here that is blocking this from getting merged?
@nickchomey commented on PR #12403:
4 days ago
#13
@westonruter is this something that might get merged before the 7.2 beta period starts in 8 days?
@nickchomey commented on PR #12403:
3 days ago
#14
now that the phpstan warning is resolved, is there anything else that might be blocking merging this?
@westonruter commented on PR #12403:
2 days ago
#15
🤖 _Comment by Claude Opus 5.5_
A code review was run on this PR, along with two follow-up commits that are not yet pushed. All changes were compared with trunk. The first three issues were reproduced by running wptexturize() in a local environment. The rest come from reading the code.
None of these issues are caught by the existing tests. The formatting, shortcode, rest-api and comment PHPUnit groups all pass (3243 tests), and PHPStan reports no new errors on the changed lines.
Regressions in output
1. & is now encoded inside CDATA blocks (src/wp-includes/formatting.php ~L708)
wptexturize() now gets its split pattern from get_html_split_regex(), which treats a whole <![CDATA[ … ]]> block as one element. As a result, the & → & encoding runs over the entire CDATA body:
wptexturize( '<script>/*<![CDATA[*/ if (a > b && c) {} /*]]>*/</script>' ); // … if (a > b && c) … ← breaks the script
The old texturize-only regex had no CDATA branch. It stopped at the first >, so the code after that point was a text chunk inside <script> and was left alone. Inline scripts in posts and widgets often use this pattern, and no test covers CDATA.
2. A quote in a tag can pair with a quote after the tag's real > (~L682)
In the possessive pattern (?:"[^"]*"|'[^']*'|[^<>])*+>, an unbalanced quote matches up with a quote that comes after the tag ends. The match then hits the next < and fails, and because the pattern is possessive it cannot backtrack. The whole tag ends up being texturized as text:
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 <b class=x">hello</b> behaves the same way. The old [^>]*> pattern handled both cases correctly.
The same mis-split affects other wp_html_split() callers. In do_shortcodes_in_html_tags(), shortcodes in that tag's attributes are no longer recognized as being inside an element. They therefore skip wp_kses_one_attr() and are expanded unsanitized by do_shortcode() later.
3. Unclosed tags are no longer protected (~L682)
Removing the optional >? drops the documented "If not found, match all input" behavior. Truncated tags are now texturized as text:
wptexturize( '<p>Truncated <a href="http://example.com/it\'s' ); // href=”http://example.com/it’s
The test edits confirm the change:
<br [gallery …] … /now produces…(wpTexturize.php~L1457).word <'wordnow produces’.wpautopoutput changed too: the rest-comments-controller expectation lost a\n.
Scope
4. & is no longer encoded in any quoted attribute value (~L258)
To fix the Tailwind [&>…] class case, the change stops encoding & → & in every quoted attribute value on every element. That is a much broader behavior change than the ticket needs. For example, <a href="?a[]=1&a[]=2"> now keeps its raw & (the expectations at wpTexturize.php ~L1409/L1413 were changed to match). Ambiguous references such as ©, ¶ or < in query strings are also no longer neutralized.
A fix limited to the split, so that > inside quotes no longer ends a tag, would solve the swiper case without changing how URLs are encoded.
5. A chunk starting with < is no longer always an element (~L245)
The '<' === $first check assumes every < starts a matched delimiter. With the new regex, an unmatched < can start a text chunk, and that chunk then goes through the element branch.
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 <p>Hi</p>\n. In <p><3 "you"</p>, the text <3 "you" is treated as an element: its quotes are never curled, and its first word goes through _wptexturize_pushpop_element() as if it were a tag name.
Performance and maintainability
6. A PHP closure now runs for every quoted attribute value (~L258)
preg_replace_callback() with a PHP closure now runs for every quoted attribute value of every element. This code runs on hot paths such as the_content, titles and comments, where trunk used a single native preg_replace(). There are two cheaper alternatives:
- a single
preg_replace()with(?:"[^"]*"|'[^']*')(*SKIP)(*FAIL)|&(?!…) - at minimum, a
str_contains( $curl, '&' )guard before the callback
7. Long tags match about 3× slower (~L682)
[^<>] consumes one character per pass through the alternation group. Measured timings:
- A 5 MB unquoted data URI in
<img src=…>takes 5.8 ms, versus 1.9 ms ontrunk. - 200k repeats of
a < btake 2.1 ms, versus 0.6 ms ontrunk.
Every wp_html_split(), wptexturize(), wpautop() and do_shortcode() call pays this cost. Consuming runs of characters at once is cheaper: (?:[^<>"']++|"[^"]*+"|'[^']*+'|["'])*+>.
8. substr( get_html_split_regex(), 2, -2 ) is fragile (~L708)
This depends silently on get_html_split_regex() returning a pattern wrapped in exactly /( … )/. Any later change there, such as adding a flag, switching the delimiter or dropping the capture group, would corrupt the texturize pattern without any visible failure. Also, when there is no shortcode regex, the code just rebuilds the identical regex.
A shared private helper for the inner pattern would avoid this, and get_html_split_regex() could be returned directly when $shortcode_regex is empty.
---
Items 1–3 are worth addressing first, each with a regression test.
@westonruter commented on PR #12403:
2 days ago
#16
@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 wp_html_split() is related in any way to wp_kses_split(), other than both should be using the HTML Tag Processor!
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
Hi,
can you show an examples?
How you use tailwindcss in your gutenberg block?
Thank you
Silvio