This guide explains how the MarkdownPipeline works, how extensions configure it, and how the parsing flow proceeds from source text to AST to rendered output.
Markdig's processing involves three objects:
MarkdownPipelineBuilder — A mutable builder where you configure extensions, parsers, and options.MarkdownPipeline — An immutable, thread-safe object produced by the builder. Contains the final parser and extension lists.Markdown — The static class that uses a pipeline to parse and render.// 1. Configure
var builder = new MarkdownPipelineBuilder()
.UseAdvancedExtensions();
// 2. Build (immutable from here on)
var pipeline = builder.Build();
// 3. Use (thread-safe, reusable)
var html = Markdown.ToHtml(markdownText, pipeline);
MarkdownPipelineBuilder is mutable and not thread-safe. Build pipelines during configuration/startup, then share the built MarkdownPipeline instances.
The MarkdownPipeline contains:
| Component | Type | Description |
|---|---|---|
| Block parsers | BlockParserList |
Ordered list of BlockParser objects |
| Inline parsers | InlineParserList |
Ordered list of InlineParser objects |
| Extensions | OrderedList<IMarkdownExtension> |
Registered extensions |
| TrackTrivia | bool |
Whether trivia parsing is enabled |
| DocumentProcessed | ProcessDocumentDelegate? |
Callback after parsing completes |
When you call builder.Build():
Setup(MarkdownPipelineBuilder) method is called in order.MarkdownPipeline.Build() again returns the same instance.This is why extension ordering matters: an extension may look for parsers added by a previous extension.
Markdown.Parse(string, pipeline) executes these steps:
A BlockProcessor is created with the pipeline's block parsers. It processes the source text line by line:
TryContinue).BlockParser to see if a new block opens at the current position (TryOpen).OpeningCharacters — parsers are only tried when their opening character matches.The result is a tree of Block nodes rooted at the MarkdownDocument.
If TrackTrivia is enabled, blocks are expanded to absorb neighboring whitespace and trivia. This supports lossless roundtripping.
An InlineProcessor visits each LeafBlock and runs the pipeline's inline parsers over the block's text content:
InlineParser objects whose OpeningCharacters match.Match method is called on each candidate parser (in order) until one returns true.Inline container.LiteralInlineParser consumes the character.The DocumentProcessed delegate (if set) is invoked on the completed document.
The MarkdownDocument is returned.
Rendering is a separate phase. When you call document.ToHtml(pipeline):
HtmlRenderer is created (or borrowed from a pool).pipeline.Setup(renderer) is called — this invokes Setup(MarkdownPipeline, IMarkdownRenderer) on every registered extension, giving each one the chance to register its ObjectRenderer.ObjectRenderer by type.This is why the same pipeline must be used for both parsing and rendering. The parse-phase Setup registers parsers that produce custom AST node types. The render-phase Setup registers renderers that know how to output those types. Mismatched pipelines = missing renderers.
Every IMarkdownExtension has two Setup methods:
public interface IMarkdownExtension
{
// Phase 1: Called during Build() — register/modify parsers
void Setup(MarkdownPipelineBuilder pipeline);
// Phase 2: Called during rendering — register object renderers
void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer);
}
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Add a new block parser
pipeline.BlockParsers.AddIfNotAlready<MyBlockParser>();
// Or modify an existing parser
var emphasisParser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
if (emphasisParser != null)
{
emphasisParser.EmphasisDescriptors.Add(
new EmphasisDescriptor('%', 3, 3, false));
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlMyBlockRenderer>();
}
}
Both parser and extension lists are OrderedList<T>, a custom Markdig collection with methods for type-safe insertion:
| Method | Description |
|---|---|
AddIfNotAlready<T>() |
Add if no instance of T exists |
InsertBefore<TBefore>(item) |
Insert before a specific type |
InsertAfter<TAfter>(item) |
Insert after a specific type |
Find<T>() |
Find the first instance of type T |
FindExact<T>() |
Find an exact type match (not subclasses) |
TryFind<T>(out T?) |
Try to find, returning success |
Replace<T>(newItem) |
Replace an existing item of type T |
ReplaceOrAdd<T>(newItem) |
Replace or add if not found |
TryRemove<T>() |
Remove the first instance of type T |
Contains<T>() |
Check if an instance of type T exists |
Without any extensions, the pipeline contains these parsers:
ThematicBreakParserHeadingBlockParserQuoteBlockParserListBlockParserHtmlBlockParserFencedCodeBlockParserIndentedCodeBlockParserParagraphBlockParserHtmlEntityParserLinkInlineParserEscapeInlineParserEmphasisInlineParserCodeInlineParserAutolinkInlineParserLineBreakInlineParserExtensions add to or modify these lists.
Enable trivia tracking for lossless parse→render roundtrips:
var pipeline = new MarkdownPipelineBuilder()
.EnableTrackTrivia()
.Build();
var document = Markdown.Parse(markdownText, pipeline);
var normalized = Markdown.Normalize(markdownText, pipeline: pipeline);
When trivia is tracked, whitespace, extra heading characters, and other non-semantic elements are stored on the AST nodes, allowing the document to be re-rendered as close to the original as possible.