feat(markdown): pipeline — frontmatter, razor:preview, component tags, headings - #3
Merged
Conversation
…uration. Remove outdated comments and add new extensions for improved Markdown processing.
… for enhanced Markdown processing. Implement TypeRegistry for component management and RenderedDocument for structured output. Add support for extracting headings and processing slots in Markdown documents.
…Registry to validate Markdown processing and component registration functionality.
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.
Ships the render layer that every doc primitive downstream will consume. Takes raw markdown →
RenderedDocument { Html, Slots, Source, Headings }. TheSlotslist is whatShellDocs.Componentswill feed into<DynamicComponent>in the next branch.What ships to
ShellDocs.MarkdownTypeRegistry.csRegister<T>(),Register(name, type),Resolve(tagName),IsRegistered,All. Chainable.RenderedDocument.csrecord RenderedDocument(Html, Slots, Source, Headings). Two slot kinds:ComponentSlot(Id, Type, Parameters, ChildContentRaw?)andPreviewSlot(Id, Type, Parameters, Code, Language).MarkdownPipelineFactory.csMarkdownRenderer.csRender(markdown),RenderFile(path). Also exposesLastWarningsfor build-time diagnostics.SlotExtractor.cs(internal)razor:previewfences + component tags with placeholder markers, sorts slots by document order.HeadingExtractor.cs(internal)HeadingBlock, emitsHeadinglist with GitHub-style slug IDs, disambiguates duplicates (setup,setup-2,setup-3).Two markdown extensions consumers get
1.
razor:previewfenced blocks — a fenced code block with the info stringrazor:previewgets pulled out as aPreviewSlotcarrying the parsed root tag'sType, its attribute dictionary, and the raw code (for the "Code" tab). The wrapper component inShellDocs.Componentswill render this as<DocsTabs>with Preview + Code panes.2. Inline component tags — both self-closing (
<Button Variant="Default" />) and body-carrying (<Callout Type="Info">Body text.</Callout>) work. Only PascalCase tag names are considered components (<Callout>yes,<em>no — standard HTML passes through untouched). Unknown component tags emit a build-time warning and pass through as raw markup, so authors immediately notice typos.Architectural decision — text preprocessing over AST mutation
First attempt threaded custom extensions through Markdig's pipeline to rewrite
FencedCodeBlockandHtmlBlockin-place. Two problems killed it:HtmlBlock.Linesrequires explicitStringLineGroupinit. The struct default is unusable; the setter chokes on unparented replacements.<Callout>Body</Callout>on a single line isn't emitted asHtmlBlockat all. Markdig treats non-whitelist tags as inline HTML inside a paragraph — the AST shape depends on whether tags happen to be on their own line vs. inline. Block-level slot extraction would only fire for a fraction of the ways users actually write component tags.Rewrote as pure text preprocessing:
PreviewSlot+ placeholder in-line; other fences → mask token restored verbatim at the end)<Component />and<Component>Body</Component>with nesting-aware regex (depth tracking on same-name opens/closes)Slotslist reflects document orderResult: predictable, testable, code fences respected (component tags inside a plain code fence stay as code), and both self-closing and body-carrying tags work uniformly.
Design choices worth calling out
Only PascalCase tag names are treated as components.
<em>,<strong>,<div>pass through as HTML. Anything starting withA-Zgets registry-checked. Standard convention from React/Blazor, unambiguous for authors.Unknown tags don't crash — they warn and passthrough. Rationale: doc authors iterate fast and will typo constantly. A hard crash breaks the write→see loop.
LastWarningson the renderer surfaces these for build-time flagging (shelldocs buildwill fail loud on any warning infeat/cli-dev-build).Body content is captured as raw string, not re-parsed markdown. For v1,
<Callout>**bold text**</Callout>givesChildContentRaw = "**bold text**"verbatim. The wrapper inShellDocs.Componentsdecides whether to re-render that through the pipeline as a nested markdown fragment or emit as markup. Keeps the pipeline linear.Attribute parsing is strict — only double-quoted strings.
<Button Variant="Default" />yes,<Button Variant={variable} />no. Blazor's parameter binding coerces the strings to their target types at render time via reflection; the pipeline doesn't need to know types. Interpolated expressions can come in v2 if there's demand.Nesting is depth-tracked by tag name.
<Callout><Callout>...</Callout></Callout>correctly matches the outer close to the outer open. Regex alone can't do this, soFindMatchingClosewalks with a depth counter.Slot IDs are short base16 GUIDs (13 chars —
sprefix + 12 hex chars of aGuid.NewGuid().ToString("N")). Not full 32-char GUIDs — the placeholder markers embed these and shorter is easier on the eyes when debugging rendered HTML.Test coverage — 20 new / 44 total, all green
TypeRegistryTestsHeadingExtractorTests**and backticks removed), empty docMarkdownRendererTestsComponentSlot, block tag with body capturesChildContentRaw, unknown tag passthrough + warning,razor:previewfence →PreviewSlot, multi-slot document-order stability, unregistered preview tag warns, plain HTML (<em>,<strong>) passthrough, headings populated, empty document returns empty shapeScaffoldingTestsNavigationGraphBuilderTests+ friendsVerified
dotnet build shelldocs.slnx→ 0 warnings, 0 errorsdotnet test shelldocs.slnx→ 44/44, ~280msWhat this unblocks
feat/components-shell—MarkdownContentnow has a concreteRenderedDocumentshape to walk. Slots feed<DynamicComponent>withType+Parametersdictionaries.DocsSidebarstill uses the nav graph from the previous branch;MarkdownContentfills in the page-body render.feat/toc-primitive(Phase 2) — theHeadingslist onRenderedDocumentis whatTableOfContentswill render + IntersectionObserver-track.feat/search-primitives(Phase 2) — the indexer walks the nav graph, callsMarkdownRenderer.Renderon each.md, and pullsHeadings+ first-N-chars ofHtmlper heading for the excerpt.Files
.csfiles insrc/ShellDocs.Markdown/(~450 LOC total)tests/ShellDocs.Tests/