Skip to content

[3.x] Render fenced code blocks through a composable Blade view - #2577

Open
emmadesilva wants to merge 15 commits into
masterfrom
v3/composable-code-blocks
Open

[3.x] Render fenced code blocks through a composable Blade view#2577
emmadesilva wants to merge 15 commits into
masterfrom
v3/composable-code-blocks

Conversation

@emmadesilva

@emmadesilva emmadesilva commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Fenced code blocks were the last Markdown construct in Hyde still assembled by string surgery. A pre-processor rewrote // filepath: comments into <!-- HYDE[Filepath] --> marker comments, and a post-processor spliced the rendered label back into the finished HTML with a regex matching <pre><code class="language-…">.

That design set the feature's ceiling. The label could only ever be a fragment wedged inside the <code> element, because that was the only place a string replacement could reach. Anything else a code block might want — a copy button, a header bar, a collapsed state — had nowhere to live. The class even carried a @todo saying as much.

Code blocks now go through the same pipeline as terminal blocks: a CommonMark extension prepares each fenced node, and a renderer hands a typed view model to a publishable Blade view. The view owns everything around the code, so block chrome is a view change rather than a framework change.

The Torchlight constraint

The view is handed the finished <pre><code> markup rather than the raw code, which is what keeps Torchlight working.

Torchlight registers its own renderer for fenced code nodes and highlights through its API, so Hyde cannot render the code itself without replacing the highlighter. Instead Hyde's renderer registers above Torchlight's and asks the renderers below it for the markup, exactly as CommonMark's document renderer would, then wraps whatever comes back. Torchlight stays in charge when enabled, CommonMark's own renderer answers when it is not, and the view is told which one it got through $highlightedByTorchlight.

For the same reason the node stays a FencedCode rather than being replaced with a Hyde node the way terminal blocks are — a replaced node is a node Torchlight's renderer is never asked about. The label is resolved onto the node's data while the document is walked, and the renderer reads it back. That listener runs after the terminal transformer has claimed its nodes and before Torchlight collects the code it sends off, so the filepath comment is stripped from the code that gets highlighted.

New: setting the label on the fence

Labels can now also be set with a title="…" modifier, using the same attribute syntax as terminal block titles:

```php title="app/Models/Example.php"
echo 'Hello World!';
```

The existing comment syntax is unchanged and still works. When a block uses both, the modifier wins — it is the explicit, structured form, and it lives on the fence rather than in the code, so a block carrying both is almost certainly mid-migration. The comment is stripped from the code either way.

Hyde\Markdown\Extensions\CodeBlockViewModel is public, unlike the terminal one, since a code block is a thing projects reasonably want to render outside Markdown, and the view contract it pins down is the documented interface for anyone publishing the view.

What is actually breaking

Renaming the label view is the breaking change. components/filepath-label.blade.phpcomponents/markdown/code-block-label.blade.php.

Published views take precedence over the framework's own, so a project that published and customized this view keeps using its copy after upgrading. The view's contract changed with the label's position, and a copy written for the old contract floats the label outside the code block when rendered in the new one — verified, it lands on the page background with the <pre> shrunk to make room. Keeping the name would have silently broken the layout of every site that customized it. Renaming leaves those copies unused instead, which costs the customization rather than the page. There is a regression test for exactly this.

The HTML changed, which is the part that reaches sites that never touched the feature. Blocks are wrapped in <div class="hyde-code-block relative">, with the label a sibling of the <pre>. Real impact on CSS that matched the old structure; hyde-code-block and hyde-code-block-label are stable hooks.

Not breaking

  • CodeblockFilepathProcessor was removed. The processor list is hardcoded in an internal trait, so there was never a supported way to register it, and its two static methods only did anything as a pair handing <!-- HYDE[Filepath] --> markers to each other. Internal cleanup, filed under Removed — it is not in the upgrade guide, since there is no migration to do.
  • Filepath comments are only read from the first line now. Closer to a fix: the first-line rule is what the docs always described, and the old pre-processor matched any line of the document only because it ran before parsing.

Torchlight is intact, and there are now tests proving it

The renderer delegating downward is the load-bearing part of this design, so it is now covered by tests that fake the Torchlight API and assert on what actually reaches it:

  • Torchlight's markup (not CommonMark's) ends up inside the code block view.
  • The filepath comment is stripped from the code before it is sent to the API — the request payload contains echo 'Hello World!';, not the comment.
  • $highlightedByTorchlight is true under Torchlight and false without it.
  • Terminal blocks are still never submitted to Torchlight.

Visual check

Rendered before and after with identical content, screenshotted headless, then pixel-diffed:

Comparison Differing pixels Diff bounding box
Plain code block, old vs new 0.35% the label only
Torchlight code block, old vs new 0.38% the label only

Everything outside the label's bounding box is pixel-identical — block position, size, spacing, paragraph flow, syntax colours. The label itself renders ~7% wider: it used to inherit <small>'s UA smaller size from the 14px <pre> (~11.6px), and now sets text-xs (12px) explicitly, because outside the <pre> there is no code font size to inherit. Happy to pin it to an arbitrary value if you would rather it be exact.

The label previously inherited --tw-prose-pre-code from the <pre> it lived in. Outside the <pre> it would have inherited the body text colour, leaving it dark-on-dark in light mode, so the shipped view sets that colour explicitly.

Testing

  • New CodeBlocksTest (34 tests, including the Torchlight and stale-view regressions) and CodeBlockViewModelUnitTest (9 tests).
  • ComposableMarkdownBlocksDocumentationTest gains a Code Blocks section, so every claim the rewritten masterclass page makes is asserted (103 tests).
  • Full suite green, except a FeaturedImageUnitTest failure that reproduces on master and is unrelated to this change.
  • HydeStan reports no errors.

Documentation

  • docs/digging-deeper/composable-markdown-blocks.md — code blocks get their own section covering both label syntaxes, precedence, the view contract, class hooks, and a customization example. The pipeline and limitations sections are corrected.
  • docs/digging-deeper/advanced-markdown.md — the title modifier, the precedence rule, and how to publish the views.
  • UPGRADE.md, CHANGELOG.md, HYDEPHP_V3_PLANNING.md.

🤖 Generated with Claude Code

@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 01:54 — with GitHub Actions Inactive
emmadesilva and others added 3 commits August 1, 2026 03:55
The terminal block transformer owned the info string token grammar, but
the same grammar is about to back code block titles. Moving it into a
trait keeps one parser, one error message shape, and one place to change
when a modifier is added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code blocks were the one Markdown construct still assembled by string
surgery: a pre-processor rewrote filepath comments into HTML markers, and
a post-processor spliced the rendered label back into the finished HTML,
which meant the label could only ever be a fragment wedged inside <code>.

They now go through the same pipeline as terminal blocks. A CommonMark
extension prepares each fenced node and a renderer hands a typed view
model to components/markdown/code-block.blade.php, so the whole block is
the view's to shape, and future chrome has somewhere to live.

The renderer delegates the highlighted markup to the renderers below it,
which keeps Torchlight in charge of highlighting when it is enabled, and
the listener runs before Torchlight collects its blocks so the filepath
comment is stripped from the code it sends off.

Labels can now also be set with a title="..." modifier, matching terminal
blocks. The attribute is the explicit form, so it wins when a block sets
both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The label is now positioned against the block wrapper instead of floating
inside the code element, which needs a utility the bundled stylesheet had
never been asked for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva force-pushed the v3/composable-code-blocks branch from fcfd68d to 0c65cff Compare August 1, 2026 01:56
@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 01:56 — with GitHub Actions Inactive
@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 01:56 — with GitHub Actions Inactive
emmadesilva and others added 6 commits August 1, 2026 03:57
The page described filepath labels as a fragment spliced into finished
HTML by a post-processor, which is no longer how they get there. Code
blocks now have their own section covering both label syntaxes, their
precedence, the view contract, and the class hooks, and the pipeline and
limitations sections are corrected to match.

The documentation driven tests follow the page as before, so every claim
the new section makes is asserted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The advanced Markdown page is where authors look up how to write these
blocks, so it gets the second label syntax, the precedence rule, and a
pointer to the views now that the whole block is publishable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Includes the motivation section, since the two decisions most likely to
be questioned later are why the view is handed finished markup instead
of the raw code, and why the node stays a FencedCode. Both come down to
leaving Torchlight in charge of highlighting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The markup change is the part that reaches sites which never touched the
feature, so the upgrade guide leads with a before and after of the HTML
and points at the class hooks that survive it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sitting inside <code>, the label inherited the prose pre-code colour,
which is light against the dark code background in both themes. As a
sibling of the <pre> it would have inherited the body text colour
instead, leaving it dark on dark in light mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva force-pushed the v3/composable-code-blocks branch from d66709d to 9614813 Compare August 1, 2026 01:57
@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 01:57 — with GitHub Actions Inactive
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ae6e160) to head (94aa177).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff             @@
##              master     #2577   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity      1756      1770   +14     
===========================================
  Files            180       184    +4     
  Lines           4395      4410   +15     
===========================================
+ Hits            4395      4410   +15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

emmadesilva and others added 3 commits August 1, 2026 04:09
The renderer delegating to the renderers below it is the load-bearing
part of this design, and nothing was asserting it. These fake the
Torchlight API and check what actually reaches it: that its markup ends
up inside the code block view, that the filepath comment is stripped
from the code before it is sent off, and that the view is told which
renderer produced the markup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The label view's contract changed along with the label's position: a
copy written for the old one floats the label outside the code block
when rendered in the new one. Keeping the name would have quietly
broken the layout of every site that published and customized it, since
a published view keeps taking precedence.

Renaming it makes those copies inert instead. A site that customized the
label falls back to the shipped one until the customizations are ported,
which loses the customization but never renders a broken page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The release notes listed four changes as equally breaking. Only one is a
PHP API removal, and even that had no supported registration point. The
rest are changes to rendered output and shipped views, and the first-line
comment rule is closer to a fix than a break, since it is what the
documentation always described.

Sorting them that way makes the one entry that costs a reader real work,
porting a customized label view, easier to find rather than lost among
notes that do not apply to them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 02:09 — with GitHub Actions Inactive
The two lines Codecov flagged are the fall-through taken when no
renderer below this one produces markup. Nothing reaches it through a
Markdown render, since CommonMark's own fenced code renderer always
returns something, so the test drives the renderer against an
environment where it is the only one registered.

Worth keeping rather than trimming: returning null hands the node back
to the document renderer instead of emitting a code block with no code
in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 02:16 — with GitHub Actions Inactive
Removing CodeblockFilepathProcessor is not a breaking change. The
processor list is hardcoded in an internal trait, so there was never a
supported way to register the class, and its two static methods only did
anything as a pair handing markers to each other. It moves to the
cleanup notes, and out of the upgrade guide, where it was giving readers
a migration step they cannot need.

Renaming the label view is the breaking change, since published views
take precedence and a customized copy keeps being used after upgrading.

Also rewrites the prose that had drifted into jargon. "Chrome the
framework does not ship is a view change" was a sentence I would not
want to read either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@emmadesilva
emmadesilva temporarily deployed to pr-documentation-2577 August 1, 2026 14:59 — with GitHub Actions Inactive
The pages I added had drifted into a register the rest of the docs don't
use: portentous fragments, metaphors that weren't carrying anything
("that design decided the feature's ceiling"), and sentences built to
sound significant instead of to be read.

Rewritten against the voice of the surrounding pages, which is plain and
speaks to the reader directly. "Simply add a code comment", "rather
forgiving, by design", "you may have noticed".

Also drops the invented **Breaking:** prefix in the changelog for the
plain "Breaking:" the file already used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant