From eb90b5f42fa76ffad1603dc40c04ba1013751c99 Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Sat, 1 Aug 2026 02:37:51 +0200 Subject: [PATCH 01/22] Extract the fence modifier parsing into a shared concern 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 --- .../Concerns/ParsesFenceModifiers.php | 83 +++++++++++++++++++ .../Processing/TransformTerminalBlocks.php | 53 +++--------- 2 files changed, 95 insertions(+), 41 deletions(-) create mode 100644 packages/framework/src/Markdown/Extensions/Concerns/ParsesFenceModifiers.php diff --git a/packages/framework/src/Markdown/Extensions/Concerns/ParsesFenceModifiers.php b/packages/framework/src/Markdown/Extensions/Concerns/ParsesFenceModifiers.php new file mode 100644 index 00000000000..3a42940996a --- /dev/null +++ b/packages/framework/src/Markdown/Extensions/Concerns/ParsesFenceModifiers.php @@ -0,0 +1,83 @@ +[\w-]+)=(?:"(?[^"]*)"|\'(?[^\']*)\')|(?\S+))(?=\s|$)/'; + + /** + * Tokenize the modifiers following the language, which are order-independent. + * + * @return array + */ + protected function tokenizeModifiers(string $info): array + { + preg_match_all(static::TOKEN_PATTERN, $info, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL); + + return array_slice($matches, 1); + } + + /** + * Resolve the title modifier from a tokenized info string. + * + * @param array $tokens + * @param string $blockName The block being parsed, used in the error message. + */ + protected function parseTitleModifier(array $tokens, string $blockName): ?string + { + $title = null; + + foreach ($tokens as $token) { + if ($token['word'] === null) { + if (strtolower($token['key']) === 'title') { + $title = $token['double'] ?? $token['single']; + } + + continue; + } + + $this->assertTitleModifierIsNotMalformed($token['word'], $blockName); + } + + return $title; + } + + /** + * A modifier we don't know about may mean something in a future version, so it is ignored. + * A malformed title, on the other hand, is a typo we should not silently discard. + */ + protected function assertTitleModifierIsNotMalformed(string $word, string $blockName): void + { + $normalized = strtolower($word); + + if ($normalized === 'title' || str_starts_with($normalized, 'title=')) { + throw new InvalidArgumentException(sprintf( + 'Invalid %s title [%s]. Expected syntax like title="My title".', $blockName, $word + )); + } + } +} diff --git a/packages/framework/src/Markdown/Extensions/Processing/TransformTerminalBlocks.php b/packages/framework/src/Markdown/Extensions/Processing/TransformTerminalBlocks.php index f4a2d2bccfc..e708fac1dc4 100644 --- a/packages/framework/src/Markdown/Extensions/Processing/TransformTerminalBlocks.php +++ b/packages/framework/src/Markdown/Extensions/Processing/TransformTerminalBlocks.php @@ -4,30 +4,18 @@ namespace Hyde\Markdown\Extensions\Processing; +use Hyde\Markdown\Extensions\Concerns\ParsesFenceModifiers; use Hyde\Markdown\Extensions\Nodes\TerminalBlock; use Hyde\Markdown\Extensions\TerminalBlockViewModel; -use InvalidArgumentException; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; -use function array_slice; -use function preg_match_all; -use function sprintf; -use function str_starts_with; use function strtolower; -use const PREG_SET_ORDER; -use const PREG_UNMATCHED_AS_NULL; - /** @internal */ class TransformTerminalBlocks { - /** - * Matches one info string token: either an HTML-style attribute with a quoted value - * (which may contain spaces), or a bare space-free word. The surrounding assertions - * keep a token from being found inside another one, as modifiers are whitespace separated. - */ - protected const TOKEN_PATTERN = '/(?[\w-]+)=(?:"(?[^"]*)"|\'(?[^\']*)\')|(?\S+))(?=\s|$)/'; + use ParsesFenceModifiers; public function __invoke(DocumentParsedEvent $event): void { @@ -58,37 +46,20 @@ protected function makeViewModel(FencedCode $node): TerminalBlockViewModel */ protected function parseModifiers(string $info): array { - $usesSymfonyFormatting = false; - $title = null; - - preg_match_all(static::TOKEN_PATTERN, $info, $matches, PREG_SET_ORDER | PREG_UNMATCHED_AS_NULL); - - foreach (array_slice($matches, 1) as $token) { - if ($token['word'] === null) { - if (strtolower($token['key']) === 'title') { - $title = $token['double'] ?? $token['single']; - } - - continue; - } - - $word = strtolower($token['word']); - - if ($word === 'xml') { - $usesSymfonyFormatting = true; + $tokens = $this->tokenizeModifiers($info); - continue; - } + return [$this->usesSymfonyFormatting($tokens), $this->parseTitleModifier($tokens, 'terminal block')]; + } - // A modifier we don't know about may mean something in a future version, so it is ignored. - // A malformed title, on the other hand, is a typo we should not silently discard. - if ($word === 'title' || str_starts_with($word, 'title=')) { - throw new InvalidArgumentException(sprintf( - 'Invalid terminal block title [%s]. Expected syntax like title="My title".', $token['word'] - )); + /** @param array $tokens */ + protected function usesSymfonyFormatting(array $tokens): bool + { + foreach ($tokens as $token) { + if ($token['word'] !== null && strtolower($token['word']) === 'xml') { + return true; } } - return [$usesSymfonyFormatting, $title]; + return false; } } From 2587d8c2de15188535bba2a5d80330f127ffc9cf Mon Sep 17 00:00:00 2001 From: Emma De Silva Date: Sun, 2 Aug 2026 19:31:31 +0200 Subject: [PATCH 02/22] Render fenced code blocks through a composable Blade view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The view receives the finished code block markup as $contents, along with $language and $label, and decides what goes around it, so highlighting is unaffected: the fence is still rendered by whichever renderer the environment already had for it. Labels are set with a title="…" modifier on the fence, replacing the v2 // filepath: comment syntax. The filepath-label.blade.php view goes with it, as the label markup now lives in the code block view. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 5 +- HYDEPHP_V3_PLANNING.md | 146 +++++ UPGRADE.md | 52 ++ docs/advanced-features/build-tasks.md | 3 +- docs/advanced-features/in-memory-pages.md | 4 +- docs/creating-content/blog-posts.md | 12 +- docs/creating-content/documentation-pages.md | 42 +- docs/digging-deeper/advanced-customization.md | 23 +- docs/digging-deeper/advanced-markdown.md | 61 +- docs/digging-deeper/collections.md | 6 +- .../composable-markdown-blocks.md | 145 +++-- docs/digging-deeper/customization.md | 70 +-- docs/digging-deeper/helpers.md | 15 +- docs/digging-deeper/navigation.md | 21 +- docs/extensions/realtime-compiler.md | 12 +- .../gh-pages-config/_pages/markdown-page.md | 7 +- .../views/components/filepath-label.blade.php | 5 - .../components/markdown/code-block.blade.php | 6 + .../Internal/SetsUpMarkdownConverter.php | 40 +- .../Framework/Services/MarkdownService.php | 4 + .../Extensions/CodeBlockViewModel.php | 36 ++ .../Concerns/ParsesFenceModifiers.php | 88 ++- .../Markdown/Extensions/Nodes/CodeBlock.php | 18 + .../Processing/CodeBlockRenderer.php | 42 ++ .../Processing/PrepareCodeBlocks.php | 57 ++ .../Processing/TransformTerminalBlocks.php | 10 +- .../Extensions/Processing/WrapCodeBlocks.php | 36 ++ .../BladeBlocks/BladeBlockExtractor.php | 14 + .../Processing/CodeblockFilepathProcessor.php | 156 ----- .../tests/Feature/BladeBlocksTest.php | 26 + .../tests/Feature/CodeBlocksTest.php | 558 ++++++++++++++++++ ...posableMarkdownBlocksDocumentationTest.php | 206 ++++--- .../tests/Feature/IncludesFacadeTest.php | 9 +- .../CodeblockFilepathProcessorTest.php | 252 -------- .../tests/Feature/TerminalCodeBlocksTest.php | 4 +- .../tests/Unit/BladeBlockExtractorTest.php | 42 ++ .../tests/Unit/CodeBlockViewModelUnitTest.php | 95 +++ tests/fixtures/markdown-features.md | 7 +- 38 files changed, 1557 insertions(+), 778 deletions(-) delete mode 100644 packages/framework/resources/views/components/filepath-label.blade.php create mode 100644 packages/framework/resources/views/components/markdown/code-block.blade.php create mode 100644 packages/framework/src/Markdown/Extensions/CodeBlockViewModel.php create mode 100644 packages/framework/src/Markdown/Extensions/Nodes/CodeBlock.php create mode 100644 packages/framework/src/Markdown/Extensions/Processing/CodeBlockRenderer.php create mode 100644 packages/framework/src/Markdown/Extensions/Processing/PrepareCodeBlocks.php create mode 100644 packages/framework/src/Markdown/Extensions/Processing/WrapCodeBlocks.php delete mode 100644 packages/framework/src/Markdown/Processing/CodeblockFilepathProcessor.php create mode 100644 packages/framework/tests/Feature/CodeBlocksTest.php delete mode 100644 packages/framework/tests/Feature/Services/Markdown/CodeblockFilepathProcessorTest.php create mode 100644 packages/framework/tests/Unit/CodeBlockViewModelUnitTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d311a44260..938bc8dfed0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ This serves two purposes: ### Added - Added a configuration option to disable the footer scroll-to-top button independently of the footer in https://github.com/hydephp/develop/pull/2459 - Added Blade Blocks for rendering Blade and Blade components from fenced code blocks in Markdown pages. They are controlled by the existing `markdown.enable_blade` option. ([#2504](https://github.com/hydephp/develop/pull/2504)) -- Added built-in `terminal` fenced code blocks with command prompt styling and optional Symfony Console formatting using the `terminal xml` info string. ([#2188](https://github.com/hydephp/develop/issues/2188), [#2485](https://github.com/hydephp/develop/issues/2485)) +- Added built-in `terminal` fenced code blocks with command prompt styling, an optional window title using the `title="…"` modifier, and optional Symfony Console formatting using the `terminal xml` info string. ([#2188](https://github.com/hydephp/develop/issues/2188), [#2485](https://github.com/hydephp/develop/issues/2485)) - Added support for lazy `InMemoryPage` contents closures. The current page is passed as the first argument whenever the contents are requested. ### Changed @@ -27,6 +27,9 @@ This serves two purposes: - Raw HTML in Markdown is now enabled by default. Set `markdown.allow_html` to `false` when compiling untrusted or unreviewed Markdown to strip potentially unsafe HTML tags. - `InMemoryPage` now requires callers to select either `contents` or `view`; configuring both throws an `InvalidArgumentException` instead of silently giving contents precedence. - `InMemoryPage` now treats an empty string as an omitted `view`, matching the existing compile-time behavior and allowing literal contents to be used with an empty view value. +- Fenced code blocks are now rendered through the publishable `components/markdown/code-block.blade.php` view, which receives the block's finished markup as `$contents`, along with `$language` and `$label`. Syntax highlighting is unchanged, as the view wraps the highlighted code rather than producing it. The generated markup around the code has changed accordingly, with `hyde-code-block` and `hyde-code-block-label` as stable hooks for your own CSS. +- Breaking: Code block labels are now set with a `title="…"` modifier on the fence, replacing the `// filepath:` comment syntax, which is no longer recognized. The label is no longer tied to file paths, and the language is optional, so ` ``` title=".env" ` labels a block that declares none, which is then treated as `plaintext`. +- Breaking: Removed the `components/filepath-label.blade.php` view, as the label markup now lives in the code block view. A published copy of the old view is no longer used, so port any customizations into `components/markdown/code-block.blade.php`. ### Deprecated - for changes that will be removed in upcoming releases. diff --git a/HYDEPHP_V3_PLANNING.md b/HYDEPHP_V3_PLANNING.md index d01d85878c5..bb6063ad451 100644 --- a/HYDEPHP_V3_PLANNING.md +++ b/HYDEPHP_V3_PLANNING.md @@ -5,6 +5,7 @@ Having this document in code lets us know the devlopment state at any given poin ## Planned features - Change all HydePHP reposotiries to use `main` instead of `master` as the default branch. This change will be executed around the time of the release. +- Ship an upgrade script that migrates v2 source files to the v3 syntax. It needs to convert code block `// filepath:` comments to the `title="…"` fence modifier, following the rules in [Dropping the filepath comment syntax](#dropping-the-filepath-comment-syntax). The upgrade guide currently describes that conversion as a manual step, and should point at the script once it exists. ## Checklist before release: @@ -28,6 +29,8 @@ Having this document in code lets us know the devlopment state at any given poin ### Feature Changes +- Fenced code blocks are now rendered through a publishable Blade view, `components/markdown/code-block.blade.php`, in the same way terminal blocks are. The view receives the rendered code block markup as `$contents`, along with `$language` and `$label`, and decides what goes around it, so changing what surrounds a code block is a view change instead of a framework change. Highlighting itself is unaffected: the fence stays in the syntax tree as the wrapper's child, and is rendered by whichever renderer the environment already had for it, be it Torchlight, a third-party extension, or CommonMark's own. +- Code block labels are now set with a `title="…"` modifier on the fence, using the same attribute syntax as terminal block titles, such as ` ```php title="app/Model.php" `. The language is optional, so ` ``` title=".env" ` labels a block that declares none, which is then treated as `plaintext`. The label is no longer tied to file paths, so a block can be titled with anything. The v2 `// filepath:` comment syntax is removed. - Blade in Markdown is now enabled by default. The `markdown.enable_blade` option controls both `[Blade]:` directives and executable Blade Blocks. Hyde sites generally treat project content as trusted and reviewed; sites that compile untrusted or unreviewed Markdown can disable both forms with this option. - Raw HTML in Markdown is now enabled by default. Hyde sites generally treat project content as trusted and reviewed; sites that compile untrusted or unreviewed Markdown can set `markdown.allow_html` to `false` to strip potentially unsafe HTML tags. - `InMemoryPage` contents now accept lazy closures in addition to literal strings. Closures are invoked each time contents are requested with the current page as their first argument, without being rebound. @@ -37,6 +40,9 @@ Having this document in code lets us know the devlopment state at any given poin - The `Redirect` page class constructor now accepts an optional `$matter` parameter, used by the framework to hide the generated documentation root redirect from navigation menus. Existing usages are unaffected. +- Removed `Hyde\Markdown\Processing\CodeblockFilepathProcessor`, along with the `` marker comments it passed between its own pre- and post-processing steps. Both were internal implementation details: the processor list is hardcoded in an internal trait, so there was no supported way to register the class, and the markers only ever existed part-way through a single conversion. Neither is documented in the changelog or upgrade guide for that reason. Labels are now resolved on the syntax tree by `PrepareCodeBlocks`. +- Changed the generated HTML for fenced code blocks, which now comes from the Blade view. Site output is not part of the backward compatibility promise, so this is noted for awareness rather than as a breaking change. The `hyde-code-block` and `hyde-code-block-label` classes are stable hooks for projects styling code blocks from their own CSS. + - Removed the legacy `checkForDeprecatedRunMixCommandUsage` check and the placeholder `--run-dev`/`--run-prod` options from the `build` command, which were kept in v2 only to surface a helpful error message. ([#2461](https://github.com/hydephp/develop/pull/2461)) - Removed the deprecated `hyde.server.dashboard` boolean config check from `DashboardController::enabled()`, which was kept in v2 for backwards compatibility but had since then been replaced with `hyde.server.dashboard.enabled`. ([#2461](https://github.com/hydephp/develop/pull/2462)) - Upgraded the bundled `vite` dependency from v7 to v8, and widened the `hyde-vite-plugin`'s `vite` peer dependency range from `>=6.3.5 <8.0.0` to `>=6.3.5 <9.0.0` so downstream projects can adopt Vite 8 without waiting for a new plugin major. The plugin's build config now targets Vite 8's Rolldown-based bundler (`rolldownOptions` instead of `rollupOptions`). ([#2414](https://github.com/hydephp/develop/pull/2414)) @@ -47,6 +53,8 @@ Having this document in code lets us know the devlopment state at any given poin - Removed the `InMemoryPage` instance macro API. Dynamic contents should now be supplied with a closure, while custom methods and other behavior belong in an `InMemoryPage` subclass. - Removed `InMemoryPage` content-source precedence. Calls that previously supplied both `contents` and `view` must retain only the intended source; positional view calls that used an empty-string contents placeholder must use `null` instead. +- Removed the `// filepath:` code block comment syntax, along with its `#`, `/* */`, and `` variants. Labels are set with the `title="…"` modifier instead. A comment left behind is no longer recognized, so it stays in the code as an ordinary first line rather than being silently dropped. +- Removed the `components/filepath-label.blade.php` view. The label markup now lives in `components/markdown/code-block.blade.php` alongside the rest of what surrounds the code. **A published copy of the old view is ignored after upgrading**, and the site renders with the shipped label until the customizations are ported over. That is the intended outcome: published views take precedence over the framework's own, and a copy written for the label's old position inside `` places it outside the code block entirely, so keeping the view in use could have produced incorrect layouts for those customized copies. - Removed the `rebuild` command (`RebuildPageCommand`). It was originally added to build a single file to disk before the realtime compiler existed, and later used internally by the RC to build-and-serve a path, but the RC now renders everything in-memory, leaving `rebuild` with no remaining consumer. It also had no safe user-facing use case: a single-page build only produces a correct `_site` when the page is self-contained, while a page change routinely invalidates aggregate outputs (sitemap, RSS, search index, post listings, navigation), so single-path building could silently leave a stale output directory that looked complete. The underlying single-page build capability remains available internally via the `StaticPageBuilder` action. ([#2490](https://github.com/hydephp/develop/pull/2490)) ### Upgrade guide @@ -60,6 +68,9 @@ Please fill in UPGRADE.md as you make changes. - Move any calls to `Redirect::create()` or `Redirect::store()` into the `redirects` array in `config/hyde.php`, using the old path as the key and the destination as the value. - Move `InMemoryPage` `compile` macro callbacks into the contents argument, and replace other instance macros with methods on an `InMemoryPage` subclass. - Update `InMemoryPage` calls to supply only `contents` or `view`. Replace an empty-string positional contents placeholder with `null`, or use the named `view` argument. +- Replace `// filepath:` code block comments with the `title="…"` fence modifier, including the `#`, `/* */`, and `` comment variants. +- Compare a few pages against your old site if you have custom CSS for code blocks or their labels, since the generated markup changed. The `hyde-code-block` and `hyde-code-block-label` classes are stable hooks to target instead of the markup structure. +- Port any customizations from a published `filepath-label.blade.php` to `markdown/code-block.blade.php`. The old file is ignored after upgrading, so the site renders with the shipped label until they are moved. ## `InMemoryPage` content-source motivation @@ -150,3 +161,138 @@ terminal window could be rendered from PHP without writing Markdown. Each is a f change, judged on its own merits. More blocks may gain the same backing later, which is also a separate change; terminal blocks are the only one today. + +## Composable code block motivation + +Code blocks were the last Markdown construct still assembled by string manipulation. A pre-processor rewrote filepath +comments into `` markers, and a post-processor spliced the rendered label back into the finished +HTML with a regex matching `
`. That approach limited what the feature could ever be: the
+label could only be a fragment placed inside the `` element, because that was the only place a string
+replacement could reach. There was nowhere to put a copy button, a header bar, or a collapsed state.
+
+Rendering the whole block through a Blade view removes that limit. The view owns the markup around the code, so
+changing what surrounds a code block becomes a view change instead of a framework change, which is what terminal
+blocks already offer.
+
+The view is given the rendered code block markup rather than the raw code, which is what keeps highlighting working.
+Torchlight highlights through its API, so Hyde cannot render the code itself without replacing the highlighter.
+
+Getting hold of that markup is the whole design problem, because the highlighter is another renderer registered for
+the same node, and CommonMark's registry is not a middleware chain. The document renderer owns the iteration, asking
+each renderer in priority order and stopping at the first non-null result, and a node renderer only ever receives a
+`ChildNodeRendererInterface`, which renders child nodes and offers no way to call the next renderer.
+
+Three attempts are worth recording, because each one failed for a reason that shaped the next.
+
+The first reached back into the environment with `getRenderersForClass()` and called the renderers below itself,
+replaying the dispatcher by hand with no cursor for which renderers had already been tried.
+
+The second named Torchlight directly and injected it, with a fallback to CommonMark's renderer. That only worked for
+the highlighter we happen to ship with: a project using any other one got its code rendered by the fallback instead.
+
+The third rendered the node through a second converter built from the same extensions but without Hyde's renderer.
+That was highlighter-agnostic on the surface and wrong underneath. Registering an extension with two environments
+calls `register()` twice, so an extension that creates state there — a listener and a renderer sharing a collection,
+which is the ordinary way to write a highlighter — ends up with the document populating one copy and the renderer
+reading the other. It also invoked any renderer above Hyde's twice for the same block, once per environment, and
+skipped the document render events that `renderDocument()` dispatches but `renderNodes()` does not.
+
+What it settled on is structural rather than a second dispatch. Once the document is parsed and every listener has
+seen it, each remaining `FencedCode` is wrapped in a `CodeBlock` node of Hyde's own, and it is that wrapper the
+renderer is registered for. The fence is still in the tree, as the wrapper's only child, and rendering it through the
+`ChildNodeRendererInterface` the renderer is handed dispatches it back through the same environment — where Hyde is
+not a candidate, because Hyde renders `CodeBlock`, not `FencedCode`.
+
+One environment, one registration per extension, and each fence rendered exactly once by whoever the environment
+already had for it. Priority stops being part of the contract: a highlighter renders inside the view whether it
+registers above or below anything of ours. A block is only taken over completely by replacing the node itself, which
+is a deliberate act rather than a side effect of picking a number.
+
+Wrapping happens on `DocumentPreRenderEvent` at the lowest priority, so every parse-time listener has already run
+against a tree shaped the way it expects. The label is resolved separately, on `DocumentParsedEvent`, above the
+priority listeners register at by default, so a highlighter collects fences that have already had the Hyde syntax
+taken out of them. The old pre-processor achieved the same thing by running before the parser.
+
+The title modifier is taken out of the info string once it has been read: CommonMark treats the first info word as the
+language, so a fence that sets only a title would otherwise hand `title="foo.php"` to the highlighter as one. Other
+modifiers are left alone, since a highlighter may read its own from there, as Torchlight does with `theme:`.
+
+The syntax is `title="…"`, the terminal block title modifier applied to code blocks, which is the kind of reuse the
+extensibility argument for that syntax anticipated. It also matches what Docusaurus and Expressive Code call the same
+thing, so readers coming from other documentation generators will recognize it.
+
+A fence may set a title without a language, which the terminal block case never had to consider, since its first word
+is always `terminal`. The first info token is therefore only taken as the language when it does not look like a title
+modifier. That also keeps the error for a malformed title consistent, so ` ```title=Foo ` is rejected the same way
+` ```php title=Foo ` is, rather than being quietly read as a language named `title=Foo`.
+
+A `blade` fence carrying a title is a labelled code sample rather than a block to execute, so the Blade block
+extractor leaves it to the code block pipeline instead of reporting an unknown directive. It reads the fence through
+the same tokenizer the code block parser uses, so modifier order does not matter and a `title=` written inside another
+modifier's quoted value is not read as one. Splitting the info string on whitespace instead would accept
+` ```blade meta='a title="x" b' ` as a code sample rather than reporting the unknown directive it is.
+
+The label markup lives in the code block view rather than a view of its own. It is a single element with no logic to
+reuse, and the code block view is short enough that anyone changing its markup can edit it in place, while anyone only
+restyling it has the `hyde-code-block-label` class hook. A second view would have added a public view name, another
+publishable file, and another migration target, for no customization that is not already possible.
+
+The v2 `filepath-label.blade.php` view is therefore removed rather than kept. Published views take precedence over the
+framework's own, so keeping it would have left every customized copy in use, and a copy written for the old position
+can float the label outside the code block once the label is no longer inside the `` element. Removing the view
+means such a copy is ignored instead, costing the customization rather than the page.
+
+`CodeBlockViewModel` is internal, matching `TerminalBlockViewModel`. It exists to type the view data and document its
+shape in one place, not to be an entry point: a code block is rendered by writing Markdown, and the interface projects
+work against is the Blade view, not the class that feeds it. Exposing it publicly was considered and dropped, since
+nothing needs it yet and a public class is much harder to take back than to add.
+
+The view is given a `$language` variable it does not currently use, because it is intrinsic to the block and views
+building their own header bars want it. It was not given a flag for whether Torchlight produced the markup. The v2
+label needed one to offset its position, but the wrapper makes that offset uniform, so the flag would have been view
+data that nothing reads, and it would have named a specific highlighter in the view contract.
+
+The extension registration model is unchanged by all of this. Torchlight is registered by class name exactly as it was
+in v2, each extension is constructed once for one environment, and the code block wiring sits alongside the heading
+renderer in the converter setup, where the rest of Hyde's own rendering already lives.
+
+The preparation listener is the one piece registered out of line: it goes on before any extension, at the highest
+priority, because listeners sharing a priority run in registration order and a third-party listener is free to claim
+the same number. Wrapping is the mirror of that, registered afterwards at the lowest priority, so it happens once
+every other listener has had the document in the shape it expects.
+
+## Dropping the filepath comment syntax
+
+An intermediate v3 branch supported both syntaxes, with the modifier winning when a block set both. That was dropped in
+favour of one syntax, and the comment removed entirely.
+
+The comment's one real advantage was portability. `// filepath: app/Model.php` still conveys the file when the same
+Markdown is read somewhere that knows nothing about Hyde, such as a GitHub pull request. That is not worth what it
+costs: it says the label must be a file path when the label is really a title, it puts presentation metadata inside
+the displayed code, it needs a comment variant per language, it makes Hyde delete a line that could be legitimate
+first-line code, and it doubles the syntax, documentation, and tests for a small feature. Two syntaxes also need
+precedence rules, and precedence rules need explaining. Degrading to a plain code block elsewhere is acceptable for
+framework-specific presentation metadata, which is how `title="…"` already behaves for terminal blocks.
+
+Keeping the comment working at runtime was rejected as well. Nothing on the v3 branch is released, so the only
+compatibility question is with v2, and that is a one-time migration rather than a permanent second syntax. Recognizing
+the comment only to warn about it would have kept every cost above, minus the label.
+
+The migration is mechanical and belongs in the v3 upgrade script rather than in the framework. It should work on parsed
+fenced blocks rather than a document-wide regex, so it should:
+
+- only inspect the first line of a fenced code block;
+- accept every comment form v2 documented, including the closing delimiters of `/* */` and `` comments;
+- preserve the language and any other fence modifiers, and add no second title when the fence already has one;
+- take the blank line separating the old comment from the code with it;
+- quote the title with single quotes when the label contains a double quote;
+- leave anything it cannot express unchanged and report it for manual migration, which is what a label containing both
+  quote characters needs.
+
+The framework's own documentation was migrated the same way, and every label in it converted cleanly, which is the
+evidence that the mechanical rules above cover realistic usage.
+
+The option gating the feature is renamed from `markdown.features.codeblock_filepaths` to
+`markdown.features.codeblock_titles`, since it no longer gates anything filepath-shaped. It is absent from the
+published `config/markdown.php` and was never documented, so the rename is recorded here rather than in the changelog
+or the upgrade guide, where it would read as a supported option that projects were expected to set.
diff --git a/UPGRADE.md b/UPGRADE.md
index e8783a21569..5d69be5d95f 100644
--- a/UPGRADE.md
+++ b/UPGRADE.md
@@ -302,6 +302,55 @@ new InMemoryPage('example', view: '');
 new InMemoryPage('example', view: null);
 ```
 
+## Replace Your Code Block Filepath Comments
+
+Code block labels are now set with a `title="…"` modifier on the fence, and the `// filepath:` comment is no longer
+recognized. A comment left behind stays in the code as written, where it renders as an ordinary first line.
+
+A block written like this:
+
+````markdown
+```php
+// filepath: app/Models/Post.php
+echo 'Hello World!';
+```
+````
+
+Becomes:
+
+````markdown
+```php title="app/Models/Post.php"
+echo 'Hello World!';
+```
+````
+
+Search your source files for `filepath` to find the blocks to convert. All the documented comment forms are affected,
+so also check for `#`, `/* */`, and `` comments. A blank line left between the old comment and the code can be
+removed with it.
+
+Labels are no longer tied to file paths, so a block can be titled with anything.
+
+## Move Your Filepath Label Customizations
+
+Fenced code blocks are now rendered through the `components/markdown/code-block.blade.php` view, which also holds the
+label markup. The `components/filepath-label.blade.php` view is gone.
+
+If you never published the label view, there is nothing to do here.
+
+If you did publish and customize it, your copy is no longer used, and your site renders with Hyde's default label
+until you move your changes over. Publish the code block view, choosing it individually so your other published views
+are left alone, then re-apply your changes and delete the old file:
+
+```bash
+php hyde publish:views
+```
+
+>warning Passing the group name, as in `php hyde publish:views components`, publishes the whole group and overwrites every component view you have already customized.
+
+The markup around code blocks has also changed, so compare a few pages against your old site if you have custom CSS
+for them. The `hyde-code-block` and `hyde-code-block-label` classes are stable hooks you can target instead of
+matching the markup structure. Syntax highlighting is unaffected.
+
 ## Review Drafts and Future-Dated Blog Posts
 
 HydePHP v3 keeps two kinds of blog post out of your built site: those marked `draft: true` in front matter, and those whose date is set in the future. Drafts and scheduled posts are skipped during auto-discovery, so they get no route, are not compiled to `_site`, and are left out of post listings, the sitemap, and the RSS feed. The date rule applies to both front matter dates and filename date prefixes.
@@ -324,6 +373,9 @@ Use this checklist to track your upgrade progress:
 - [ ] Moved `InMemoryPage` `compile` macro callbacks into the contents argument and replaced other macros with subclass methods
 - [ ] Updated `InMemoryPage` calls to supply only one of `contents` and `view`
 - [ ] Checked `_posts` for drafts and blog posts dated in the future, and set up recurring builds if scheduling posts
+- [ ] Replaced `// filepath:` code block comments with the `title="…"` fence modifier
+- [ ] Ported any `filepath-label.blade.php` customizations to `markdown/code-block.blade.php`, and deleted the old file
+- [ ] Compared pages against your old site if you have custom CSS for code blocks or their labels
 
 ## Troubleshooting
 
diff --git a/docs/advanced-features/build-tasks.md b/docs/advanced-features/build-tasks.md
index b85e9c0c4e7..a88a8203c5b 100644
--- a/docs/advanced-features/build-tasks.md
+++ b/docs/advanced-features/build-tasks.md
@@ -122,8 +122,7 @@ For example: `app/Actions/ExampleBuildTask.php`.
 
 If you want, you can also register build tasks of any namespace in the convenient `build_tasks` array which is in the main configuration file, `config/hyde.php`.
 
-```php
-// filepath config/hyde.php
+```php title="config/hyde.php"
 'build_tasks' => [
     \App\Actions\SimpleTask::class,
     \MyPackage\Tasks\MyBuildTask::class,
diff --git a/docs/advanced-features/in-memory-pages.md b/docs/advanced-features/in-memory-pages.md
index 7ed8b458e53..c8f24b30381 100644
--- a/docs/advanced-features/in-memory-pages.md
+++ b/docs/advanced-features/in-memory-pages.md
@@ -117,9 +117,7 @@ class ReportPage extends InMemoryPage
 Register the page in the `boot` method of your `AppServiceProvider`. The `booting` callback runs before Hyde's discovery
 process, allowing the route to be generated automatically.
 
-```php
-// filepath: app/Providers/AppServiceProvider.php
-
+```php title="app/Providers/AppServiceProvider.php"
 namespace App\Providers;
 
 use Hyde\Hyde;
diff --git a/docs/creating-content/blog-posts.md b/docs/creating-content/blog-posts.md
index 911cd2a6880..1fa25daa72a 100644
--- a/docs/creating-content/blog-posts.md
+++ b/docs/creating-content/blog-posts.md
@@ -81,8 +81,7 @@ and understand how your front matter is used. You can read more about the Front
 
 Before digging deeper into all the supported options, let's take a look at what a basic post with front matter looks like.
 
-```markdown
-// filepath _posts/my-new-post.md
+```markdown title="_posts/my-new-post.md"
 ---
 title: My New Post
 description: A short description used in previews and SEO
@@ -117,8 +116,7 @@ compiled to `_site`, and do not appear in post listings, the sitemap, or the RSS
 
 Set `draft: true` to keep a post out of your built site while the property remains true:
 
-```markdown
-// filepath _posts/work-in-progress.md
+```markdown title="_posts/work-in-progress.md"
 ---
 title: Work in progress
 draft: true
@@ -137,8 +135,7 @@ default, omitting the property keeps the front matter cleaner.
 A post whose date is set in the future is scheduled: it is excluded from builds until that date has passed, and is then
 published by the next build. This works with both front matter dates and [date prefixes](#date-prefixes):
 
-```markdown
-// filepath _posts/my-upcoming-post.md
+```markdown title="_posts/my-upcoming-post.md"
 ---
 title: My Upcoming Post
 date: 2099-01-01
@@ -173,8 +170,7 @@ point. If you deploy your site once and leave it, a post dated next Tuesday will
 
 To have a post go live on its own, run builds on a schedule. For example, with GitHub Actions:
 
-```yaml
-// filepath .github/workflows/deploy.yml
+```yaml title=".github/workflows/deploy.yml"
 on:
   push:
     branches: [main]
diff --git a/docs/creating-content/documentation-pages.md b/docs/creating-content/documentation-pages.md
index 9d68d79489e..31ec0c3c218 100644
--- a/docs/creating-content/documentation-pages.md
+++ b/docs/creating-content/documentation-pages.md
@@ -109,8 +109,7 @@ A nice and simple way to define the order of pages is to add their route keys as
 
 It may be useful to know that Hyde internally will assign a priority calculated according to its position in the list, plus an offset of `500`. The offset is added to make it easier to place pages earlier in the list using front matter or with explicit priority settings.
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'sidebar' => [
     'order' => [
         'readme', // Priority: 500
@@ -124,8 +123,7 @@ It may be useful to know that Hyde internally will assign a priority calculated
 
 You can also specify explicit priorities by adding a value to the array keys. Hyde will then use these exact values as the priorities.
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'sidebar' => [
     'order' => [
         'readme' => 10,
@@ -192,8 +190,7 @@ If you want to store the compiled documentation pages in a different directory t
 
 The path is relative to the site output, typically `_site`.
 
-```php
-// filepath: config/hyde.php
+```php title="config/hyde.php"
 'output_directories' => [
     \Hyde\Pages\DocumentationPage::class => 'docs' // default [tl! --]
     \Hyde\Pages\DocumentationPage::class => 'docs/1.x' // What the Hyde docs use [tl! ++]
@@ -218,9 +215,7 @@ By default, the site title shown in the sidebar header is generated from the con
 
 The sidebar footer contains, by default, a link to your site homepage. You can change this in the `config/docs.php` file.
 
-```php
-// filepath: config/docs.php
-
+```php title="config/docs.php"
 'sidebar' => [
     'footer' => 'My **Markdown** Footer Text',
 ],
@@ -248,9 +243,7 @@ See [the chapter in the customization page](customization#navigation-menu--sideb
 
 When using the automatic sidebar grouping feature the titles of the groups are generated from the subdirectory names. If these are not to your liking, for example if you need to use special characters, you can override them in the configuration file. The array key is the directory name, and the value is the label.
 
-```php
-// filepath: config/docs.php
-
+```php title="config/docs.php"
 'sidebar' => [
     'labels' => [
         'questions-and-answers' => 'Questions & Answers',
@@ -266,8 +259,7 @@ By default, each group will be assigned the lowest priority found inside the gro
 
 Just use the sidebar group key as instead of the page identifier/route key:
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'sidebar' => [
     'order' => [
         'readme',
@@ -347,8 +339,7 @@ If this setting is set to true, Hyde will output all documentation pages into th
 
 If you set this to false, Hyde will match the directory structure of the source files (just like all other pages).
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'flattened_output_paths' => true,
 ```
 
@@ -364,8 +355,7 @@ Hyde includes a built-in search feature for documentation pages powered by Alpin
 
 The search feature is enabled by default. You can disable it by removing the `DocumentationSearch` option from the Hyde `Features` config array:
 
-```php
-// filepath: config/hyde.php
+```php title="config/hyde.php"
 'features' => [
     Feature::DocumentationSearch, // [tl! --]
 ],
@@ -395,8 +385,7 @@ The search implementation includes:
 
 For large pages like changelogs, you may want to exclude them from the search index. Add the page identifier to the `exclude_from_search` array in the docs config:
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'exclude_from_search' => [
   'changelog',
 ]
@@ -422,9 +411,7 @@ The feature is automatically enabled when you specify a base URL in the Docs con
 
 Here's an example configuration from the official HydePHP.com documentation:
 
-```php
-// filepath: config/docs.php
-
+```php title="config/docs.php"
 'source_file_location_base' => 'https://github.com/hydephp/docs/blob/master/',
 ```
 
@@ -432,8 +419,7 @@ Here's an example configuration from the official HydePHP.com documentation:
 
 Changing the label is easy, just change the following config setting:
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'edit_source_link_text' => 'Edit Source on GitHub',
 ```
 
@@ -441,8 +427,7 @@ Changing the label is easy, just change the following config setting:
 
 By default, the button will be shown in the documentation page footer. You can change this by setting the following config setting to `'header'`, `'footer'`, or `'both'`
 
-```php
-// filepath: config/docs.php
+```php title="config/docs.php"
 'edit_source_link_position' => 'header',
 ```
 
@@ -450,8 +435,7 @@ By default, the button will be shown in the documentation page footer. You can c
 
 This is not included out of the box, but is easy to add with some CSS! Just target the `.edit-page-link` class.
 
-```css
-// filepath e.g. app.css
+```css title="e.g. app.css"
 .edit-page-link::before {content: "✏ "}
 ```
 
diff --git a/docs/digging-deeper/advanced-customization.md b/docs/digging-deeper/advanced-customization.md
index 534c52a9802..68f284215e8 100644
--- a/docs/digging-deeper/advanced-customization.md
+++ b/docs/digging-deeper/advanced-customization.md
@@ -40,9 +40,7 @@ posts in the same directory as documentation pages, Hyde will not know which pag
 
 ### In the config file
 
-```php
-// filepath config/hyde.php
-
+```php title="config/hyde.php"
 'source_directories' => [
     HtmlPage::class => '_pages',
     BladePage::class => '_pages',
@@ -54,8 +52,7 @@ posts in the same directory as documentation pages, Hyde will not know which pag
 
 ### In a service provider
 
-```php
-// filepath app/AppServiceProvider.php
+```php title="app/AppServiceProvider.php"
 use Hyde\Framework\Concerns\RegistersFileLocations;
 
 public function register(): void
@@ -79,8 +76,7 @@ Each option is relative to the site's `output_directory` setting. Setting a valu
 
 ### In the config file
 
-```php
-// filepath config/hyde.php
+```php title="config/hyde.php"
 'output_directories' => [
     HtmlPage::class => '',
     BladePage::class => '',
@@ -92,8 +88,7 @@ Each option is relative to the site's `output_directory` setting. Setting a valu
 
 ### In a service provider
 
-```php
-// filepath app/AppServiceProvider.php
+```php title="app/AppServiceProvider.php"
 use Hyde\Framework\Concerns\RegistersFileLocations;
 
 public function register(): void
@@ -121,9 +116,7 @@ HydePHP will by default look for the source directories shown above in the root
 If you're not happy with this, it's easy to change! For example, you might want everything in a `'src'` subdirectory.
 That's easy enough, just set the value of the `source_root` setting in `config/hyde.php` to `'src'`, or whatever you prefer!
 
-```php
-// filepath config/hyde.php
-
+```php title="config/hyde.php"
 'source_root' => '', // [TL! --]
 'source_root' => 'src', // [TL! ++]
 ```
@@ -146,8 +139,7 @@ Hyde will copy all files in this directory to `_site/media` (or whatever your co
 
 You can change the path to this directory by setting the `media_directory` option in `config/hyde.php`.
 
-```php
-// filepath config/hyde.php
+```php title="config/hyde.php"
 'media_directory' => '_media',
 ```
 
@@ -169,7 +161,6 @@ from the output directory, so files in `_assets` will be copied to `_site/assets
 If you want to store your compiled website in a different directory than the default `_site`, you can change the path
 using the following configuration option in `config/hyde.php`. The path is expected to be relative to your project root.
 
-```php
-// filepath config/hyde.php
+```php title="config/hyde.php"
 'output_directory' => '_site',
 ```
diff --git a/docs/digging-deeper/advanced-markdown.md b/docs/digging-deeper/advanced-markdown.md
index c80380b2676..4cdcaa9830d 100644
--- a/docs/digging-deeper/advanced-markdown.md
+++ b/docs/digging-deeper/advanced-markdown.md
@@ -100,8 +100,7 @@ be reviewed both for the text they publish and for executable directives hidden
 If your site accepts Markdown outside that trusted review process, or builds pull requests before they have been
 reviewed, disable Blade in Markdown in the `config/markdown.php` file:
 
-```php
-// filepath: config/markdown.php
+```php title="config/markdown.php"
 'enable_blade' => false,
 ```
 
@@ -251,61 +250,56 @@ The coloured blockquotes also support inline Markdown, just like normal blockquo
 
 Note that these currently do not support multi-line blockquotes.
 
-## Code Block Filepaths
+## Code Block Titles
 
-When browsing these documentation pages you may have noticed a label in the top right corner of code blocks specifying the file path.
-These are also created by using a custom Hyde feature that turns code comments into automatic code blocks.
+When browsing these documentation pages you may have noticed a label in the top right corner of code blocks naming the
+file the code belongs to. Add a `title` modifier to a fenced code block to label it like that.
 
 ### Usage
 
-Simply add a code comment with the path in the **first line** of a fenced code block like so:
-
-````markdown
-// filepath: _docs/advanced-markdown.md
-```php
-‎// filepath: hello-world.php
-
+````markdown title="_docs/advanced-markdown.md"
+```php title="hello-world.php"
 echo 'Hello World!';
 ```
 ````
 
 Which becomes:
 
-```php
-// filepath: hello-world.php
-
+```php title="hello-world.php"
 echo 'Hello World!';
 ```
 
-### Alternative syntax
+Titles follow the same quoting rules as [terminal window titles](#window-titles), so single quotes work just as well,
+and a title may contain spaces. They are not limited to file paths, so use whatever describes the block best.
 
-The syntax is rather forgiving, by design, and supports using both `//` and `#` for comments.
-The colon is also optional, and the 'filepath' string is case-insensitive. So the following is also perfectly valid:
+The language is optional, so you can label a block that declares none, which is then treated as `plaintext`. Put the
+language first if you want one, as anything written after the title stays a modifier.
 
 ````markdown
-```js
-‎// filepath hello.js
-console.log('Hello World!');
+``` title=".env"
+APP_NAME=HydePHP
 ```
 ````
 
-If you have a newline after the filepath, like in the first example, it will be removed so your code stays readable.
-
 ### Advanced usage
 
-Since HTML in Markdown is enabled by default, anything within the path label will be rendered as HTML. This means you
+Since HTML in Markdown is enabled by default, anything within the title will be rendered as HTML. This means you
 can add links, or even images to the label. This requires `allow_html` to remain `true` in `config/markdown.php`.
 
-````markdown
-// filepath: View file on Github
-```markdown
-‎// filepath: View file on Github
+````markdown title='View file on Github'
+```markdown title='View file on Github'
+Hello World!
 ```
 ````
 
 ### Limitations
 
-The filepaths are hidden on mobile devices using CSS to prevent them from overlapping with the code block.
+The titles are hidden on mobile devices using CSS to prevent them from overlapping with the code block.
+
+### Customizations
+
+See [Composable Markdown Blocks](composable-markdown-blocks#code-blocks) for the code block view contract, its class
+hooks, and how to publish and customize the markup.
 
 
 ## Heading Permalinks
@@ -318,8 +312,7 @@ The feature is enabled by default for documentation pages. When enabled, Hyde wi
 
 You can enable it for other page types by adding the page class to the `permalinks.pages` array in the `config/markdown.php` file, or disable it for all pages by setting the array to an empty array.
 
-```php
-// filepath: config/markdown.php
+```php title="config/markdown.php"
 'permalinks' => [
     'pages' => [
         \Hyde\Pages\DocumentationPage::class,
@@ -397,8 +390,7 @@ To convert Markdown, HydePHP uses the GitHub Flavored Markdown extension. HydePH
 project source is normally trusted and reviewed. If you process Markdown from outside your trusted review process, set
 the `allow_html` option to `false` in your `config/markdown.php` file to strip potentially unsafe HTML tags.
 
-```php
-// filepath: config/markdown.php
+```php title="config/markdown.php"
 'allow_html' => false,
 ```
 
@@ -411,8 +403,7 @@ We do this by adding the `.prose` CSS class to the HTML elements containing the
 
 You can easily edit these classes, for example if you want to customize the prose colours, in the `config/markdown.php` file.
 
-```php
-// filepath: config/markdown.php
+```php title="config/markdown.php"
 'prose_classes' => 'prose dark:prose-invert', // [tl! remove]
 'prose_classes' => 'prose dark:prose-invert prose-img:inline', // [tl! add]
 ```
diff --git a/docs/digging-deeper/collections.md b/docs/digging-deeper/collections.md
index 3215e7f6a3e..c98a7e2e975 100644
--- a/docs/digging-deeper/collections.md
+++ b/docs/digging-deeper/collections.md
@@ -160,8 +160,7 @@ The Markdown will be parsed into a `MarkdownDocument` object which parses any op
 
 Here is the sample Markdown we will use:
 
-```blade
-// filepath: resources/collections/testimonials/1.md
+```blade title="resources/collections/testimonials/1.md"
 ---
 author: John Doe
 ---
@@ -216,8 +215,7 @@ php hyde make:page "Testimonials" --type="blade"
 And we can use the collection almost like any other Laravel one. As you can see, since each entry is a `MarkdownDocument` class,
 we are able to get the author from the front matter, and the content from the body.
 
-```blade
-// filepath _pages/testimonials.blade.php
+```blade title="_pages/testimonials.blade.php"
 @foreach(DataCollection::markdown('testimonials') as $testimonial)
     

{{ $testimonial->body }}

diff --git a/docs/digging-deeper/composable-markdown-blocks.md b/docs/digging-deeper/composable-markdown-blocks.md index 2f30dc96587..3b1e55878fe 100644 --- a/docs/digging-deeper/composable-markdown-blocks.md +++ b/docs/digging-deeper/composable-markdown-blocks.md @@ -53,10 +53,10 @@ loops, `@include`, config calls, and the full Tailwind class set inside them. | Block | Syntax | View | Mechanism | |-----------------------------------------|-------------------------------|------------------------------------|----------------------| +| [Code blocks](#code-blocks) | ` ```php ` | `markdown/code-block.blade.php` | CommonMark renderer | | [Terminal blocks](#terminal-blocks) | ` ```terminal ` | `markdown/terminal.blade.php` | CommonMark renderer | | [Coloured blockquotes](#coloured-blockquotes) | `>info Text` | `colored-blockquote.blade.php` | Markdown pre-processor | | [Headings](#headings) | `## Heading` | `markdown-heading.blade.php` | CommonMark renderer | -| [Filepath labels](#code-block-filepath-labels) | `// filepath: foo.php` | `filepath-label.blade.php` | Markdown post-processor | | [Blade component blocks](#blade-component-blocks) | ` ```blade component="x" ` | *any component you write* | Markdown pre-processor | All view paths are relative to `resources/views/components/` in the framework package, and to @@ -78,9 +78,9 @@ Published views land in `resources/views/vendor/hyde/components/`, mirroring the ``` resources/views/vendor/hyde/components/ ├── colored-blockquote.blade.php -├── filepath-label.blade.php ├── markdown-heading.blade.php └── markdown/ + ├── code-block.blade.php └── terminal.blade.php ``` @@ -106,8 +106,7 @@ If a class you added has no visible effect, an un-recompiled stylesheet is almos You don't always need to publish. Hyde's block markup includes stable, non-utility class hooks specifically so you can restyle blocks from your own CSS: -```css -/* filepath: resources/assets/app.css */ +```css title="resources/assets/app.css" .hyde-terminal-body { background-color: #1a1b26; color: #a9b1d6; @@ -128,24 +127,69 @@ phases, orchestrated by the `MarkdownService`: placeholder comment so nothing downstream tries to parse their contents. - `BladeDownProcessor` — handles single-line `[Blade]:` directives. - `ShortcodeProcessor` — expands coloured blockquotes into rendered HTML. -- `CodeblockFilepathProcessor` — converts filepath comments into `` markers. **2. CommonMark conversion** parses the Markdown into an abstract syntax tree and renders it. - `TerminalExtension` is always registered. It converts matching fenced code nodes into `TerminalBlock` nodes and renders them through the terminal view. +- Remaining fenced code nodes get their label resolved, and are wrapped in the code block view. Your highlighter still + renders the code itself. - `HeadingRenderer` replaces CommonMark's default heading renderer with Hyde's Blade-backed one. - Any extensions listed in `markdown.extensions` are registered here too, as is the Torchlight extension when enabled. **3. Post-processors** run against the resulting HTML string. - `BladeBlockProcessor` swaps the placeholders back out for the rendered Blade output. -- `CodeblockFilepathProcessor` replaces the markers with the rendered filepath label view. - `DynamicMarkdownLinkProcessor` resolves source-file links to routes. -The distinction that matters: **AST-based blocks** (terminal, headings) are structurally aware — they only ever match -real Markdown nodes. **String-based blocks** (shortcodes, filepath labels) work on lines of text and are cheaper to -implement, but they are not fence-aware. See [Limitations](#limitations-and-gotchas). +The distinction that matters: **AST-based blocks** (code blocks, terminals, headings) are structurally aware — they +only ever match real Markdown nodes. **String-based blocks** (shortcodes, Blade blocks) work on lines of text and are +cheaper to implement, but they are not fence-aware. See [Limitations](#limitations-and-gotchas). + +## Code Blocks + +Fenced code blocks go through a Blade view. The view doesn't render the code itself, it receives the rendered code +block markup and decides what goes around it. Syntax highlighting is unaffected: whichever highlighter your site uses +still renders the code, and the view wraps it. + +Indented code blocks are not affected. + +See [Advanced Markdown](advanced-markdown#code-block-titles) for the `title` modifier that labels a block. + +### View contract + +**View:** `hyde::components.markdown.code-block` + +| Variable | Type | Description | +|-------------|-----------------------------|--------------------------------------------------------------------------------------| +| `$contents` | `string` | The rendered code block markup, as your highlighter produced it. Echo with `{!! !!}`. | +| `$language` | `string`/`null` | The fence language, or `null` when the block declared none. A fence labelled with a `title` modifier instead of a language is `plaintext`. | +| `$label` | `HtmlString`/`string`/`null`| The resolved label, or `null` when the block set none. An `HtmlString` when `markdown.allow_html` is enabled, so the label can contain links. | + +>danger `$contents` is finished markup, which is why the view echoes it unescaped. Do not re-escape it with `{{ }}` (you will see markup as text). + +### Class hooks + +| Class | Targets | +|---------------------------|--------------------------------------------| +| `hyde-code-block` | The outer `
` wrapping the code block | +| `hyde-code-block-label` | The block's title label | + +### Customization example + +Say you want a header bar above the code, showing the language next to the label: + +```blade title="resources/views/vendor/hyde/components/markdown/code-block.blade.php" +
+ @if($label || $language) +
+ {{ $label }} + {{ $language }} +
+ @endif + {!! $contents !!} +
+``` ## Terminal Blocks @@ -257,8 +301,7 @@ untitled block falls back to. The shipped view escapes it with `{{ }}` and falls Say you want blocks that set no title of their own to show the current working directory instead of the word "Terminal". Publish the view and edit its fallback: -```blade - +```blade title="resources/views/vendor/hyde/components/markdown/terminal.blade.php"