Code reference: stop running wpautop over descriptions; render snippet source as a code block - #581
Draft
sirreal wants to merge 4 commits into
Draft
Code reference: stop running wpautop over descriptions; render snippet source as a code block#581sirreal wants to merge 4 commits into
sirreal wants to merge 4 commits into
Conversation
`<php-snippet>` carries its PHP in a `<script type="application/x-php+json">` payload, so a page without the Playground module shows nothing where a snippet should be, and the source is absent from the markup. Append a `<pre class="wp-block-code"><code class="language-php">` copy of the source inside the element. `<php-snippet>` builds its interface in a shadow root with no slot, so this is what a reader sees before the module upgrades the element and all they see if it never loads. The text goes in through `WP_HTML_Tag_Processor::set_modifiable_text()`, which encodes it for its context. `set_modifiable_text()` only accepts a `#text` token, so the code element carries a placeholder to overwrite. Wrap the element in a `<div>`. A `<pre>` start tag closes an open `<p>`, and the description renders snippets inside one, since `wpautop()` puts a bare placeholder comment in a paragraph. Unwrapped, the parser carries the code block alone out of the paragraph, tearing it off the element and leaving it outside the shadow root, showing code the element already shows. A `<div>` closes the paragraph the same way but takes the whole snippet with it.
sirreal
force-pushed
the
add/php-snippet-source-code-block
branch
from
August 26, 2026 06:38
5bbde91 to
591f4f3
Compare
sirreal
marked this pull request as draft
August 26, 2026 06:48
The parser renders the long description with Parsedown at import and then runs its own `fix_newlines()`, which turns a soft wrap after a sentence-ending `.` into a literal `<br>` and merges every other newline into a space. So `post_content` arrives as complete HTML with no bare newlines inside paragraphs. `get_description()` runs it through `the_content` for the shortcode and texturize passes, and `wpautop()` rides along at the same priority with nothing left to do. What it does instead, measured over all 12,399 published parser posts: - Wraps the snippet placeholder comment in `<p>`, or leaves an unclosed `<p>` before one inside a list item. A `<pre>` start tag closes an open `<p>`, so the code block rendered inside `<php-snippet>` is carried out of the element by the HTML parser. - Wraps the `[code]` shortcode's `<pre class="wp-block-code">` in `<p>` on 182 posts, the same malformed shape, which browsers repair by ejecting the `<pre>` and leaving an empty paragraph behind. - Respells the parser's `<br>` as `<br />` and re-breaks lines around block tags. Neither renders differently. Remove `wpautop` around the one `the_content` call, restoring it at whatever priority it held, the way `filter_code_content` is already handled here. The `[code]` shortcode's `_trim_code()` already tolerates content wpautop never touched: its rendered `<pre>` contents are byte-identical with and without.
The `<div>` existed only to survive the paragraph `wpautop()` put around the snippet placeholder: it closed that paragraph the way the `<pre>` would have, but carried the whole snippet out instead of tearing the code block off the element. With `wpautop()` no longer running over descriptions the placeholder is already at block level, so the code block nests in `<php-snippet>` on its own and the wrapper has nothing left to do. Removing it also drops the two empty paragraphs the parser left on either side of it.
The comment blamed `wpautop()` for the `<br>` in descriptions. Those are the parser's: `fix_newlines()` emits them at import and `wpautop()` only respells them `<br />`. The comment also left out the larger effect, the `[code]` shortcode's `<pre>` wrapped in a paragraph on 182 posts.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What trunk does
get_description()runs every reference description throughthe_contentwithwpautop()registered. The description is already complete HTML: phpdoc-parser renders it with Parsedown at import and then runsfix_newlines(), which turns a soft wrap after a sentence-ending.into a literal<br>and merges every other newline into a space.wpautop()has nothing left to do, so what it does is wrong:[code lang="php"]shortcode text in<p>, anddo_shortcode()then expands the shortcode inside that paragraph. 182 published posts serve<p> <pre class="wp-block-code">…</pre></p>. A<pre>start tag closes an open<p>, so browsers eject the code block and leave an empty paragraph.<p>, or leaves an unclosed<p>before one inside a list item, so<php-snippet>renders inside a paragraph.Interactive snippets keep their PHP only in a
<script type="application/x-php+json">payload and build their UI in a shadow root. Read the page without running that module and there is no code anywhere in the document.Changes
1. Stop running
wpautop()over descriptionsget_description()removeswpautoparound its onethe_contentcall and restores it at whatever priority it held, the idiom already used forfilter_code_contenton the lines above. The shortcode and texturize passes still run.Before,
/reference/classes/wpdb/insert/:After:
No
<br>is removed and nothing reflows: the line breaks in descriptions are the parser's literal<br>, whichwpautop()only respelled as<br />.Each description is now served on one line.
fix_newlines()collapsed the newlines andwpautop()was reinserting them around block tags. Rendering is identical; view-source is less readable (wpdb::prepareis 1,379 characters on one line).Corpus
Every published parser post rendered both ways on the sandbox: 12,399 posts, 3,583 with a long description.
[code]shortcode<pre>contents differwpautop()supplied<br>respelled<br />, line breaks around block tags[code]<pre>no longer wrapped in<p>The 4:
wp_check_php_mysql_versionsandupdate_core(whitespace around nested lists),get_linkobjectsbyname(a raw<li>in prose, malformed either way),WP_Duotone::get_svg_definitions(a stray……text node gains or loses a paragraph).2. Render the snippet source as a code block
render_php_code_snippet()appends<pre class="wp-block-code"><code class="language-php">holding the source inside<php-snippet>, after the payload.Before:
After:
The
<p>is gone because of change 1, not this one. Without change 1 the<pre>would close that paragraph and the browser would move the code block out of the element.The element's shadow root has no
<slot>, so the block disappears once the element upgrades. It is for the reader whose browser never gets there, and for anything reading the HTML rather than executing it. Its text is byte-identical to the JSON payload, so the shown source and the run source cannot drift.Decisions
<div>around<php-snippet>survives the ejection but leaves two empty paragraphs and the 182 broken pages. Converting the placeholder to a block element beforewpautop()runs (athe_contentfilter at priority 9) avoids the empty paragraphs, couples the block to filter ordering, and leaves the 182. CSS to hide the ejected copy hides the fallback from the readers it exists for. Only removing the filter fixes the cause.set_modifiable_text()rather thanesc_html().esc_html()does not double-encode, so a literal<egg>in a snippet (theclass_list()example has one) would pass through and the browser would show<egg>: the shown source would differ from the source that runs. The Tag Processor encodes for the text position it writes into. It only writes to a#texttoken, so the<code>element carries a placeholder for it to overwrite.get_description()has one caller, the code description block, on parser post types. Handbooks are untouched. The@seeloop in the same file has an older unguardedremove_filter/add_filterpair forwpautop; it behaves the same at the default priority and is left alone here.try/finallyaround the filter removal, matching thefilter_code_contenthandling beside it.Verifying
Read the served HTML (view-source or
curl). DevTools shows the repaired tree, which hides the bug./reference/classes/wp_html_tag_processor/class_list/:<php-snippet>at block level,<pre>inside it, no<p>touching it, and the<code>text decodes to exactly the JSON payload./reference/classes/wpdb/insert/:<pre class="wp-block-code">no longer inside<p>./reference/functions/wp_insert_post/: curly quotes and autolinked references still present.Risk and rollback
git revert, no reimport and no data change. Page caches serve the old markup until they expire.Testing
No PHP test harness here, so manual:
curl: the three pages above hold,class_listcarries the DocBlock's literal<egg>as&lt;egg&gt;, andwp_insert_posthas the same curly-quote and autolink counts as production.set_modifiable_text()/get_modifiable_text()round trip against a realWP_HTML_Tag_Processor:<,>,&, quotes, a literal</code></pre><script>alert(1)</script>, heredocs, non-ASCII. All return the original string.WP_Hook, realwpautop(), both nestedthe_contentpasses) run locally with and without the change. Trunk produces<p><php-snippet>…</php-snippet></p>; this branch produces the markup above. Core'sdo_blocks()removeswpautopbefore the outer pass reaches priority 10, so the assembled page never goes through it again.playground.wordpress.net/php-code-snippet.js: the<pre>is a child of<php-snippet>in each, hidden after upgrade, readable with the module blocked.Written by Claude in Claude Code at my direction and reviewed by me; the parser,
wpautop()and custom-element behaviour described here was measured, not assumed.