From afa1407f428a76d6e2cf3e9624d8bd11ae09934b Mon Sep 17 00:00:00 2001 From: Phil Scott Date: Tue, 7 Jul 2026 09:12:40 -0400 Subject: [PATCH] feat(markdown): let preprocessors opt out of code-block chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A CodeBlockPreprocessResult can now set SkipChrome to emit its HTML verbatim, with none of the standard wrapper markup (code-highlight-wrapper, language head bar, container divs). This serves preprocessors whose output is not a code block at all — a rendered diagram, a chart, a widget — which previously got a language label bar and, via the wrapper''s data-language attribute, were mangled back into a fake code fence by the HTML-to-markdown converter in the per-page Markdown twins and Book composition. --- .../code-block-preprocessor.md | 2 +- .../Extensions/CodeBlockRenderingService.cs | 7 ++++-- .../Extensions/ICodeBlockPreprocessor.cs | 6 ++++- .../Extensions/CodeBlockPreprocessorTests.cs | 22 +++++++++++++++++++ .../CodeBlockRenderingServiceTests.cs | 22 +++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md b/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md index d2c40319..71735a8c 100644 --- a/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md +++ b/docs/Pennington.Docs/Content/how-to/markdown-pipeline/code-block-preprocessor.md @@ -24,7 +24,7 @@ Implement as a sealed class. `Try examples/ExtensibilityLabExample/LineCountPreprocessor.cs ``` -The returned `CodeBlockPreprocessResult` carries the pre-rendered HTML, the `BaseLanguage` CSS class Pennington stamps on the block, and `SkipTransform`. Set `SkipTransform` to `true` when the output is final and the `[!code ...]` annotation pass should not re-process it. +The returned `CodeBlockPreprocessResult` carries the pre-rendered HTML, the `BaseLanguage` CSS class Pennington stamps on the block, and two opt-out flags. Set `SkipTransform` to `true` when the output is final and the `[!code ...]` annotation pass should not re-process it. Set `SkipChrome` to `true` when the output is not a code block at all — a rendered diagram, a chart, an embedded widget — and the standard wrapper markup (the `code-highlight-wrapper` div, the language head bar, and the container divs) should not surround it; the HTML is emitted verbatim. A `SkipChrome` block also stops registering as a code fence in the per-page Markdown twins: without the wrapper's `data-language` attribute, the HTML→markdown converter treats the output as ordinary markup instead of reconstructing a fence around it. ## Pick a Priority value diff --git a/src/Pennington/Markdown/Extensions/CodeBlockRenderingService.cs b/src/Pennington/Markdown/Extensions/CodeBlockRenderingService.cs index 87f8cc4a..2e3b363f 100644 --- a/src/Pennington/Markdown/Extensions/CodeBlockRenderingService.cs +++ b/src/Pennington/Markdown/Extensions/CodeBlockRenderingService.cs @@ -8,7 +8,8 @@ namespace Pennington.Markdown.Extensions; /// s first, falls back to /// on the base language, applies /// , and wraps the result via -/// . Used by both the Markdig renderer +/// (unless a preprocessor opts out with +/// ). Used by both the Markdig renderer /// and the <CodeBlock> Razor component so markdown fences and /// Razor usages produce identical HTML. /// @@ -47,7 +48,9 @@ public string Render( var html = result.SkipTransform ? result.HighlightedHtml : CodeTransformer.Transform(result.HighlightedHtml); - return CodeBlockHtmlBuilder.BuildHtml(html, effectiveOptions, isInTabGroup, languageId); + return result.SkipChrome + ? html + : CodeBlockHtmlBuilder.BuildHtml(html, effectiveOptions, isInTabGroup, languageId); } } diff --git a/src/Pennington/Markdown/Extensions/ICodeBlockPreprocessor.cs b/src/Pennington/Markdown/Extensions/ICodeBlockPreprocessor.cs index 0d025b28..a87a1aed 100644 --- a/src/Pennington/Markdown/Extensions/ICodeBlockPreprocessor.cs +++ b/src/Pennington/Markdown/Extensions/ICodeBlockPreprocessor.cs @@ -20,7 +20,11 @@ public interface ICodeBlockPreprocessor /// Fully highlighted HTML (wrapped in pre/code tags). /// The base language for CSS class purposes. /// If true, skip CodeTransformer on the output. +/// If true, emit as-is with none of the standard +/// code-block chrome (outer code-highlight-wrapper, language head bar, container divs) — the preprocessor +/// owns the entire output. Use for fences that render to something other than a code block, such as a diagram. public record CodeBlockPreprocessResult( string HighlightedHtml, string BaseLanguage, - bool SkipTransform = false); \ No newline at end of file + bool SkipTransform = false, + bool SkipChrome = false); \ No newline at end of file diff --git a/tests/Pennington.Tests/Markdown/Extensions/CodeBlockPreprocessorTests.cs b/tests/Pennington.Tests/Markdown/Extensions/CodeBlockPreprocessorTests.cs index 7b5f1b37..4d9aba26 100644 --- a/tests/Pennington.Tests/Markdown/Extensions/CodeBlockPreprocessorTests.cs +++ b/tests/Pennington.Tests/Markdown/Extensions/CodeBlockPreprocessorTests.cs @@ -116,6 +116,28 @@ public void SkipTransform_PreventsCodeTransformer() result.ShouldNotContain(""); } + [Fact] + public void SkipChrome_OmitsCodeBlockWrapper() + { + // With SkipChrome=true the preprocessor owns the entire output: no + // code-highlight-wrapper, no language head bar, no data-language attribute. + var preprocessor = new TestPreprocessor("test:owned", + new CodeBlockPreprocessResult( + "
", + "test", + SkipTransform: true, + SkipChrome: true)); + + var markdown = "```test:owned\nsome code\n```"; + + var result = RenderMarkdown(markdown, [preprocessor]); + + result.ShouldContain("owned-output"); + result.ShouldNotContain("code-highlight-wrapper"); + result.ShouldNotContain("codeblock-head"); + result.ShouldNotContain("data-language"); + } + private class TestPreprocessor( string matchLanguageId, CodeBlockPreprocessResult? result, diff --git a/tests/Pennington.Tests/Markdown/Extensions/CodeBlockRenderingServiceTests.cs b/tests/Pennington.Tests/Markdown/Extensions/CodeBlockRenderingServiceTests.cs index 8bffe654..b1272487 100644 --- a/tests/Pennington.Tests/Markdown/Extensions/CodeBlockRenderingServiceTests.cs +++ b/tests/Pennington.Tests/Markdown/Extensions/CodeBlockRenderingServiceTests.cs @@ -95,6 +95,28 @@ public void Render_PreprocessorPriority_HighestRunsFirst() html.ShouldNotContain(">low<"); } + [Fact] + public void Render_SkipChrome_EmitsPreprocessorHtmlUnwrapped() + { + // A preprocessor whose output is not a code block (a rendered diagram) opts out + // of the wrapper chrome entirely — the result is its HTML verbatim. + var preprocessor = new StubPreprocessor( + matchLanguageId: "beck", + result: new CodeBlockPreprocessResult( + HighlightedHtml: "
", + BaseLanguage: "beck", + SkipTransform: true, + SkipChrome: true)); + + var service = new CodeBlockRenderingService( + new HighlightingService([]), + [preprocessor]); + + var html = service.Render(code: "nodes: []", languageId: "beck"); + + html.ShouldBe("
"); + } + [Fact] public void Render_NoPreprocessors_FallsBackToHighlighter() {