Site/Docs/Advanced/Ast
---
title: Abstract syntax tree
---
Abstract syntax tree
The Markdown.Parse(...) method returns a MarkdownDocument — the root of the abstract syntax tree (AST). The AST is a tree of Block and Inline nodes that fully represents the semantic structure of the Markdown input.
AST structure
There are two general categories of node:
- Block nodes — Represent block-level constructs: paragraphs, headings, lists, code blocks, blockquotes, etc.
- Inline nodes — Represent inline constructs within a block: emphasis, links, code spans, images, line breaks, etc.
Blocks may contain other blocks (container blocks) or inlines (leaf blocks). Inlines may contain other inlines (container inlines) but never blocks.
MarkdownDocument (ContainerBlock)
├── HeadingBlock (LeafBlock)
│ └── LiteralInline
├── ParagraphBlock (LeafBlock)
│ ├── LiteralInline
│ ├── EmphasisInline (ContainerInline)
│ │ └── LiteralInline
│ └── LiteralInline
├── ListBlock (ContainerBlock)
│ ├── ListItemBlock (ContainerBlock)
│ │ └── ParagraphBlock
│ └── ListItemBlock (ContainerBlock)
│ └── ParagraphBlock
└── FencedCodeBlock (LeafBlock)Node hierarchy
All AST nodes inherit from MarkdownObject, which provides:
{.table}
| Member | Type | Description |
|---|---|---|
| Span | SourceSpan | Start and end positions (inclusive) in the source text |
| Line | int | Zero-based line number in the source |
| Column | int | Zero-based column number |
Block types
{.table}
| Base class | Description | Examples |
|---|---|---|
| ContainerBlock | Contains child blocks | MarkdownDocument, ListBlock, ListItemBlock, QuoteBlock |
| LeafBlock | Contains inlines, no child blocks | ParagraphBlock, HeadingBlock, CodeBlock, FencedCodeBlock |
A LeafBlock has an Inline property (ContainerInline?) that is the root of its inline content.
Inline types
{.table}
| Base class | Description | Examples |
|---|---|---|
| ContainerInline | Contains child inlines | EmphasisInline, LinkInline |
| LeafInline | No children | LiteralInline, CodeInline, LineBreakInline |
Inlines are stored as a doubly-linked list — each inline has PreviousSibling and NextSibling properties, plus a Parent (ContainerInline?).
Traversing the AST
The Descendants API
The Descendants extension methods provide the easiest way to traverse the tree. They yield nodes in depth-first order.
#### All descendants
var document = Markdown.Parse(markdownText);foreach (var node in document.Descendants())
{
Console.WriteLine($"{node.GetType().Name} at {node.Line}:{node.Column}");
}
#### Filter by type
// All headings
foreach (var heading in document.Descendants<HeadingBlock>())
{
Console.WriteLine($"H{heading.Level}: line {heading.Line}");
}// All links (not images)
foreach (var link in document.Descendants<LinkInline>().Where(l => !l.IsImage))
{
Console.WriteLine($"Link: {link.Url}");
}
// All images
foreach (var image in document.Descendants<LinkInline>().Where(l => l.IsImage))
{
Console.WriteLine($"Image: {image.Url}");
}
#### Querying from any node
Descendants works from any MarkdownObject, not just the root:
// Find all emphasis inside list items
var items = document.Descendants<ListItemBlock>()
.SelectMany(item => item.Descendants<EmphasisInline>());// Find emphasis whose direct parent block is a list item
var other = document.Descendants<EmphasisInline>()
.Where(em => em.ParentBlock is ListItemBlock);
Manual traversal
For containers you can iterate children directly:
// Block children of a ContainerBlock
foreach (var child in document)
{
// child is a Block
}// Inline children of a ContainerInline
var paragraph = document.Descendants<ParagraphBlock>().First();
var inline = paragraph.Inline; // ContainerInline?
if (inline != null)
{
var child = inline.FirstChild;
while (child != null)
{
Console.WriteLine(child.GetType().Name);
child = child.NextSibling;
}
}
Common block types
{.table}
| Type | Description |
|---|---|
| MarkdownDocument | Root node, a ContainerBlock |
| ParagraphBlock | A paragraph (<p>) |
| HeadingBlock | A heading (<h1>–<h6>); has Level property |
| ListBlock | An ordered or unordered list |
| ListItemBlock | A single list item |
| QuoteBlock | A blockquote |
| FencedCodeBlock | A fenced code block; has Info (language) and Lines properties |
| CodeBlock | An indented code block |
| ThematicBreakBlock | A horizontal rule (<hr>) |
| HtmlBlock | A raw HTML block |
Common inline types
{.table}
| Type | Description |
|---|---|
| LiteralInline | Plain text content |
| EmphasisInline | Emphasis (<em> or <strong>); has DelimiterChar and DelimiterCount |
| CodeInline | Inline code span |
| LinkInline | A link or image; has Url, Title, IsImage |
| AutolinkInline | An autolink (<url>) |
| LineBreakInline | A line break (hard or soft) |
| HtmlInline | Inline raw HTML |
| HtmlEntityInline | An HTML entity |
Attached data
Every MarkdownObject supports attaching arbitrary key-value data:
// Store data
node.SetData("my-key", someValue);// Retrieve data
var value = node.GetData("my-key");
// Check existence
if (node.ContainsData("my-key")) { ... }
// Remove
node.RemoveData("my-key");
Typed metadata helpers
For extension state, prefer collision-resistant typed keys via DataKey<T> and the typed helper methods:
using Markdig.Syntax;public sealed class MyExtensionState
{
public int Value { get; set; }
}
static readonly DataKey<MyExtensionState> StateKey = new();
// Store
node.SetData<MyExtensionState>(StateKey, new MyExtensionState { Value = 123 });
// Retrieve
if (node.TryGetData<MyExtensionState>(StateKey, out var state))
{
Console.WriteLine(state.Value);
}
Use the explicit generic calls (SetData<T>, TryGetData<T>, GetData<T>) to avoid ambiguity with the untyped IMarkdownObject methods.
HTML attributes
The most common attached data is HtmlAttributes, used by the Generic attributes extension:
using Markdig.Renderers.Html;var attrs = node.GetAttributes(); // Creates if not present
attrs.AddClass("my-class");
attrs.Id = "my-id";
attrs.AddProperty("data-value", "42");
Mutating the AST safely
Markdig provides a few small helpers for common AST "surgery" operations:
// Remove a block from its parent container
block.Remove();// Replace a block in its parent container (optionally transferring children)
block.ReplaceBy(replacementBlock, moveChildren: true);
// Transfer children efficiently (preserves order)
sourceContainerBlock.TransferChildrenTo(destinationContainerBlock);
sourceContainerInline.TransferChildrenTo(destinationContainerInline);
These helpers preserve parent/child ownership invariants and avoid common O(n²) patterns (for example repeatedly calling RemoveAt(0) on a ContainerBlock).
These helpers do not automatically recompute spans or trivia. If your transform needs exact source fidelity after mutation, update Span, Line, Column, and trivia properties explicitly.
The SourceSpan struct
Every node has a Span property of type SourceSpan with Start and End positions (inclusive) in the original source. When .UsePreciseSourceLocation() is enabled, these values are accurate for all nodes.
var pipeline = new MarkdownPipelineBuilder()
.UsePreciseSourceLocation()
.Build();var document = Markdown.Parse(markdownText, pipeline);
foreach (var heading in document.Descendants<HeadingBlock>())
{
var span = heading.Span;
var sourceText = markdownText[span.Start..(span.End + 1)];
Console.WriteLine($"Heading source: '{sourceText}'");
}
Block properties
IsOpen
While a block is being parsed, IsOpen is true. Once the parser finishes building the block, IsOpen is set to false. In a fully parsed document, all blocks have IsOpen == false.
IsBreakable
Indicates whether the block can be interrupted by new blocks. FencedCodeBlock is the only built-in non-breakable block — its parent container cannot be closed while the code block is still open, because content inside the fence is treated as literal code.
Parent
Every block has a Parent (ContainerBlock?) property for upward traversal. The root MarkdownDocument has Parent == null.
Parser
Every block stores a reference to the BlockParser that created it. This is useful when post-processing needs to identify which parser produced a given node.
---
Site/Docs/Advanced/Block Parsers
---
title: Block parsers
---
Block parsers
Block parsers identify block-level elements (paragraphs, headings, lists, code blocks, custom containers, etc.) from the Markdown source text. They run during the first phase of parsing, processing the document line by line.
How block parsing works
The BlockProcessor orchestrates block parsing. For each line in the source:
1. Continue — All currently open blocks are asked if they continue on the current line (TryContinue). Blocks that don't continue are closed.
2. Open — The processor tries to open new blocks by calling TryOpen on block parsers whose OpeningCharacters match the current character.
3. Dispatch — Parsers are dispatched based on their OpeningCharacters array — a parser is only tried when one of its opening characters matches the current position.
The BlockParser base class
All block parsers inherit from BlockParser:
public abstract class BlockParser : ParserBase<BlockProcessor>, IBlockParser<BlockProcessor>
{
// Characters that trigger this parser
public char[]? OpeningCharacters { get; set; } // Whether this parser can interrupt an open paragraph
public virtual bool CanInterrupt(BlockProcessor processor, Block block) => true;
// Try to open a new block at the current position
public abstract BlockState TryOpen(BlockProcessor processor);
// Try to continue an already-open block
public virtual BlockState TryContinue(BlockProcessor processor, Block block)
=> BlockState.None;
// Called when a block is closing. Return false to remove it from the AST.
public virtual bool Close(BlockProcessor processor, Block block) => true;
// Event fired when a block is closed
public event ProcessBlockDelegate? Closed;
}
BlockState return values
The TryOpen and TryContinue methods return a BlockState enum:
{.table}
| Value | Meaning |
|---|---|
| None | No match — parser did not recognize anything |
| Skip | Skip this parser for the current line, try others |
| Continue | Block stays open; for leaf blocks, the line content is appended |
| ContinueDiscard | Block stays open; line is consumed but not appended to the block |
| Break | Block is closed; current line remains available for other parsers |
| BreakDiscard | Block is closed; current line is consumed (not available to others) |
Writing a custom block parser
Step 1: Define the AST node
Create a class inheriting from LeafBlock (for blocks with inline content) or ContainerBlock (for blocks containing other blocks):
using Markdig.Parsers;
using Markdig.Syntax;/// <summary>
/// A custom note block: !!! note "Title"
/// </summary>
public class NoteBlock : LeafBlock
{
public NoteBlock(BlockParser parser) : base(parser)
{
}
/// <summary>
/// The note title.
/// </summary>
public string? Title { get; set; }
/// <summary>
/// The note type (note, warning, etc.).
/// </summary>
public string? NoteType { get; set; }
}
Step 2: Implement the block parser
/ Detailed source-code truncated for AI context efficiency. /Step 3: Key points about TryOpen
- Check processor.IsCodeIndent — If the line is indented 4+ spaces, it's a code block, not your custom syntax.
- Set the Parser property — The Block constructor automatically sets it from the argument.
- Set Span, Line, and Column — These enable precise source location tracking.
- Support trivia when enabled — If processor.TrackTrivia is true, take pending leading blank/trivia lines via processor.TakeLinesBefore() and assign them to block.LinesBefore (and use processor.UseTrivia(end) when you need exact trivia slices around markers).
- Push to processor.NewBlocks — This tells the processor a new block was found.
- Return the right BlockState — Break for a single-line block, Continue for multi-line blocks.
Multi-line block parsers
For blocks that span multiple lines, use TryContinue:
public class AdmonitionParser : BlockParser
{
public AdmonitionParser()
{
OpeningCharacters = ['!'];
} public override BlockState TryOpen(BlockProcessor processor)
{
if (processor.IsCodeIndent) return BlockState.None;
var line = processor.Line;
if (line.CurrentChar != '!' || line.PeekChar(1) != '!' || line.PeekChar(2) != '!')
return BlockState.None;
line.Start += 3;
line.TrimStart();
var block = new AdmonitionBlock(this)
{
// ... set properties
};
processor.NewBlocks.Push(block);
return BlockState.Continue; // Expect more lines
}
public override BlockState TryContinue(BlockProcessor processor, Block block)
{
// Continue while lines are indented (part of the admonition)
if (processor.IsBlankLine)
return BlockState.Continue; // Blank lines are allowed
if (processor.Indent >= 4)
{
// Indented content belongs to this block
processor.GoToColumn(processor.ColumnBeforeIndent + 4);
return BlockState.Continue;
}
// Not indented — close the block
return BlockState.Break;
}
}
Using FencedBlockParserBase
For blocks that use opening/closing fences (like ::: custom containers or `` code blocks), inherit from FencedBlockParserBase<T> to get fencing logic for free:
using Markdig.Parsers;/// <summary>
/// A custom "spoiler" block: |||spoiler ... |||
/// </summary>
public class SpoilerBlock : FencedCodeBlock
{
public SpoilerBlock(BlockParser parser) : base(parser) { }
}
public class SpoilerParser : FencedBlockParserBase<SpoilerBlock>
{
public SpoilerParser()
{
OpeningCharacters = ['|'];
InfoPrefix = null; // No info string prefix required
}
protected override SpoilerBlock CreateFencedBlock(BlockProcessor processor)
{
return new SpoilerBlock(this);
}
}
/ Detailed source-code truncated for AI context efficiency. /
public override bool CanInterrupt(BlockProcessor processor, Block block)
{
// Only allow after a blank line, not in the middle of a paragraph
return false;
}The Close method
Close is called when a block is being finalized. Return false to remove the block from the AST (useful if the block turned out to be invalid):
public override bool Close(BlockProcessor processor, Block block)
{
var myBlock = (MyBlock)block;
if (!myBlock.IsValid)
{
return false; // Remove from AST
}
return true; // Keep in AST
}Trivia tracking
When TrackTrivia is enabled, use processor.TakeLinesBefore() in TryOpen to capture pending blank/trivia lines:
public override BlockState TryOpen(BlockProcessor processor)
{
// ... detection logic ... var block = new MyBlock(this);
block.LinesBefore = processor.TakeLinesBefore();
processor.NewBlocks.Push(block);
return BlockState.Continue;
}
Next steps
- Inline parsers — Write parsers for inline elements
- Renderers — Create HTML renderers for your custom blocks
- Creating extensions — Wire everything together
---
Site/Docs/Advanced/Creating Extensions
---
title: Creating extensions
---
Creating extensions
Extensions are the primary mechanism for adding new features to Markdig. An extension can add new parsers, modify existing parsers, and register custom renderers.
The IMarkdownExtension interface
Every extension implements IMarkdownExtension, which has two methods:
public interface IMarkdownExtension
{
void Setup(MarkdownPipelineBuilder pipeline);
void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer);
}- Setup(MarkdownPipelineBuilder) — Called when the pipeline is built. Register or modify block/inline parsers here.
- Setup(MarkdownPipeline, IMarkdownRenderer) — Called before rendering. Register object renderers here.
Extension complexity spectrum
Extensions range from trivial to complex:
Level 1: Modify an existing parser
The simplest extensions don't add new parsers at all — they just tweak existing ones.
Example: CitationExtension — Adds ""..."" citations by configuring the existing EmphasisInlineParser:
using Markdig;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html.Inlines;public sealed class CitationExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Find the existing emphasis parser
var emphasisParser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
if (emphasisParser != null && !emphasisParser.HasEmphasisChar('"'))
{
// Add " as a 2-character emphasis delimiter: ""text""
emphasisParser.EmphasisDescriptors.Add(
new EmphasisDescriptor('"', 2, 2, false));
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not HtmlRenderer) return;
// Hook into the emphasis renderer to emit <cite> for ""...""
var emphasisRenderer = renderer.ObjectRenderers.FindExact<EmphasisInlineRenderer>();
if (emphasisRenderer == null) return;
var previousTag = emphasisRenderer.GetTag;
emphasisRenderer.GetTag = inline =>
(inline.DelimiterCount == 2 && inline.DelimiterChar == '"' ? "cite" : null)
?? previousTag(inline);
}
}
Key pattern: Reuse EmphasisInlineParser for delimiter-based inlines. Many extensions follow this approach.
Level 2: Add a new inline parser + renderer
When you need custom inline syntax that doesn't fit the emphasis model.
Example: TaskListExtension — Adds [ ] / [x] checkbox parsing:
public sealed class TaskListExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Insert the task list parser before the link parser
if (!pipeline.InlineParsers.Contains<TaskListInlineParser>())
{
pipeline.InlineParsers.InsertBefore<LinkInlineParser>(
new TaskListInlineParser());
}
} public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlTaskListRenderer>();
}
}
}
This extension needs:
- A custom InlineParser subclass (TaskListInlineParser)TaskList
- A custom AST node ( inline)HtmlObjectRenderer<TaskList>
- A custom (HtmlTaskListRenderer)
Level 3: Add a new block parser + renderer
When you need to parse block-level constructs.
Example: CustomContainerExtension — Adds ::: fenced containers:
public sealed class CustomContainerExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
// Add the block parser at position 0 (high priority)
if (!pipeline.BlockParsers.Contains<CustomContainerParser>())
{
pipeline.BlockParsers.Insert(0, new CustomContainerParser());
} // Also add inline container support (::text::)
var emphasisParser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
if (emphasisParser != null && !emphasisParser.HasEmphasisChar(':'))
{
emphasisParser.EmphasisDescriptors.Add(
new EmphasisDescriptor(':', 2, 2, false));
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlCustomContainerRenderer>();
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlCustomContainerInlineRenderer>();
}
}
}
Level 4: Complex extension with multiple parsers and ordering
Some extensions (like FootnoteExtension) add multiple block parsers, inline parsers, and renderers, and need specific ordering relative to other extensions. These are more complex but follow the same fundamental patterns.
Registering your extension
Option A: Generic Use method
For extensions with a parameterless constructor:
var pipeline = new MarkdownPipelineBuilder()
.Use<MyExtension>()
.Build();Option B: Instance method
For extensions that need configuration:
var ext = new MyExtension(someConfig);
var pipeline = new MarkdownPipelineBuilder()
.Use(ext)
.Build();Option C: Custom fluent extension method (recommended)
Create an extension method on MarkdownPipelineBuilder for a clean API:
public static class MyExtensionMethods
{
public static MarkdownPipelineBuilder UseMyExtension(
this MarkdownPipelineBuilder pipeline,
MyExtensionOptions? options = null)
{
pipeline.Extensions.ReplaceOrAdd<MyExtension>(
new MyExtension(options));
return pipeline;
}
}Usage:
var pipeline = new MarkdownPipelineBuilder()
.UseMyExtension(new MyExtensionOptions { / ... / })
.Build();Complete example: a blink extension
Here's a complete, silly extension that turns %%%text%%% into <blink>text</blink>:
using Markdig;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html.Inlines;/// <summary>
/// Extension that converts %%%text%%% to <blink> tags.
/// </summary>
public sealed class BlinkExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
var parser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
if (parser != null && !parser.HasEmphasisChar('%'))
{
// 3 consecutive % on each side
parser.EmphasisDescriptors.Add(new EmphasisDescriptor('%', 3, 3, false));
}
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is not HtmlRenderer) return;
var emphasisRenderer = renderer.ObjectRenderers.FindExact<EmphasisInlineRenderer>();
if (emphasisRenderer == null) return;
var previousTag = emphasisRenderer.GetTag;
emphasisRenderer.GetTag = inline =>
(inline.DelimiterCount == 3 && inline.DelimiterChar == '%'
? "blink"
: null)
?? previousTag(inline);
}
}
// Fluent API extension method
public static class BlinkExtensionMethods
{
public static MarkdownPipelineBuilder UseBlink(
this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<BlinkExtension>();
return pipeline;
}
}
Usage:
var pipeline = new MarkdownPipelineBuilder()
.UseBlink()
.Build();var html = Markdown.ToHtml("This is %%%blinking%%% text.", pipeline);
// => <p>This is <blink>blinking</blink> text.</p>
Next steps
- Block parsers — How to write custom block parsers from scratch
- Inline parsers — How to write custom inline parsers
- Renderers — How to create HTML or custom renderers
---
Site/Docs/Advanced/Inline Parsers
---
title: Inline parsers
---
Inline parsers
Inline parsers identify inline-level elements (emphasis, links, code spans, custom syntax, etc.) from the text content of LeafBlock nodes. They run during the second phase of parsing, after all blocks have been identified.
How inline parsing works
After block parsing produces a tree of blocks, the InlineProcessor visits every LeafBlock and processes its text:
1. Walk through the text character by character.
2. At each position, check if any InlineParser has that character in its OpeningCharacters.Match
3. Call on matching parsers (in priority order) until one returns true.LiteralInlineParser
4. If a parser matches, add the created inline to the current container.
5. If no parser matches, the consumes the character into a LiteralInline.
6. After all text is consumed, run post-processing (e.g., emphasis restructuring).
The InlineParser base class
public abstract class InlineParser : ParserBase<InlineProcessor>, IInlineParser<InlineProcessor>
{
// Characters that trigger this parser
public char[]? OpeningCharacters { get; set; } // Try to match an inline at the current position
public abstract bool Match(InlineProcessor processor, ref StringSlice slice);
}
The interface is deliberately simple: set OpeningCharacters and implement Match.
Writing a custom inline parser
Step 1: Define the AST node
Create a class inheriting from LeafInline (for simple inlines) or ContainerInline (for inlines that contain other inlines):
using Markdig.Syntax.Inlines;/// <summary>
/// An inline representing a keyboard shortcut: [[Ctrl+S]]
/// </summary>
public class KeyboardInline : LeafInline
{
/// <summary>
/// The keyboard shortcut text.
/// </summary>
public string? Shortcut { get; set; }
}
Step 2: Implement the inline parser
/ Detailed source-code truncated for AI context efficiency. /Step 3: Key points about Match
- Return false if no match — Don't advance the slice if you don't match.
- Set processor.Inline — This is how you return the matched inline to the processor.
- Advance slice.Start — Move past the consumed characters. Other parsers will see text starting from the new position.
- Set source position — Use processor.GetSourcePosition for accurate Span, Line, and Column.
Working with StringSlice
The slice parameter is a mutable view into the LeafBlock's text. Key operations:
{.table}
| Member | Description |
|---|---|
| slice.CurrentChar | Character at current position |slice.PeekChar(offset)
| | Look ahead without advancing |slice.NextChar()
| | Advance and return the next character |slice.SkipChar()
| | Skip one character |slice.Start
| | Start index (mutable — advance this to consume) |slice.End
| | End index |slice.Text
| | The underlying string |slice.IsEmpty
| | True if Start > End |
The InlineProcessor
Inside Match, the processor provides context:
{.table}
| Member | Description |
|---|---|
| processor.Inline | Set this to your created inline on match |processor.Block
| | The LeafBlock currently being processed |processor.Root
| | The root ContainerInline of the current block |processor.Document
| | The root MarkdownDocument |processor.Context
| | The per-call MarkdownParserContext (may be null) |processor.GetSourcePosition(pos, out line, out column)
| | Map a slice position to source position |processor.GetParserState<TState>(this)
| | Get/create parser state scoped to the current leaf processing pass |processor.Emit(inline)
| | Append inline into the deepest open inline container and set processor.Inline |processor.BlockNew
| | Request replacing the current leaf block after inline processing completes |processor.ReplaceParentContainer(old, @new)
| | Advanced: synchronize traversal if you replace a parent container block during inline processing |
Container inlines and delimiters
Some inline syntaxes are paired delimiters: they open, later close, and the content between them becomes children of a ContainerInline node.
Markdig implements this pattern with temporary delimiter nodes (subclasses of DelimiterInline) plus a post-processing step that rewires the inline linked-list into the final AST shape.
Before building your own delimiter system, consider:
- If your syntax can be expressed as a simple paired delimiter (~~, ==, ^^, ""..."", etc.), prefer extending EmphasisInlineParser by adding an EmphasisDescriptor. This gives you correct nesting rules and integrates with existing HTML renderers.EmphasisInlineParser
- If you need custom pairing rules (like links/images, tables, or non-trivial delimiter constraints), follow the built-in patterns:
- + EmphasisDelimiterInlineLinkInlineParser
- + LinkDelimiterInlinePipeTableDelimiterInline
- (tables)
Post-processing with IPostInlineProcessor
For complex inlines that need restructuring after all inline parsing is complete, implement IPostInlineProcessor:
public interface IPostInlineProcessor
{
bool PostProcess(
InlineProcessor state,
Inline? root,
Inline? lastChild,
int postInlineProcessorIndex,
bool isFinalProcessing);
}The emphasis system uses this to restructure nested delimiter runs into properly ordered EmphasisInline nodes.
Inline manipulation helpers
When post-processing, use these helper methods on Inline:
{.table}
| Method | Description |
|---|---|
| InsertAfter(inline) | Insert a new inline after this one in the parent |InsertBefore(inline)
| | Insert before this one |Remove()
| | Remove this inline from its parent |ReplaceBy(newInline)
| | Replace this inline with another, optionally moving children |PreviousSibling
| | Previous sibling in the linked list |NextSibling
| | Next sibling |FirstParentOfType<T>()
| | Find the nearest ancestor of type T |
Example: simple emoji parser
A complete inline parser that converts :name: shortcodes:
public class SimpleEmojiParser : InlineParser
{
private readonly Dictionary<string, string> _emojis = new()
{
{ "smile", "😊" },
{ "heart", "❤️" },
{ "rocket", "🚀" }
}; public SimpleEmojiParser()
{
OpeningCharacters = [':'];
}
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
var start = slice.Start;
// Skip opening ':'
var c = slice.NextChar();
// Read the emoji name
var nameStart = slice.Start;
while (c != ':' && c != '\0' && !c.IsWhitespace())
{
c = slice.NextChar();
}
if (c != ':')
{
// No closing ':', reset and fail
slice.Start = start;
return false;
}
var name = slice.Text[nameStart..slice.Start];
if (!_emojis.TryGetValue(name, out var emoji))
{
slice.Start = start;
return false;
}
// Skip closing ':'
slice.NextChar();
processor.Inline = new LiteralInline(emoji)
{
Span = new SourceSpan(
processor.GetSourcePosition(start, out int line, out int column),
processor.GetSourcePosition(slice.Start - 1, out _, out _)),
Line = line,
Column = column
};
return true;
}
}
Next steps
- Block parsers — Write block-level parsers
- Renderers — Create renderers for your custom inlines
- Creating extensions — Wire parsers and renderers into an extension
---
Site/Docs/Advanced/Parser Authoring Api
---
title: Parser authoring API
---
Parser + AST authoring API
This page documents parser authoring contracts and the public APIs intended to give third-party extensions parity with built-in extensions (and to make safe AST manipulation less error-prone).
If you're new to writing extensions, start with Creating extensions and then come back here for the "engine contract" details.
Scope
The authoring model remains two-phase:
1. Block parsing (BlockProcessor + BlockParser).InlineProcessor
2. Inline parsing ( + InlineParser + IPostInlineProcessor).
Performance characteristics are unchanged: parser dispatch still relies on opening-character maps and pooled processors.
Public API Additions
InlineProcessor.ReplaceParentContainer(...)
Use this when an inline parser replaces a parent container block during inline processing.
Contract:
- Call only while processing the current leaf (ProcessInlineLeaf pass).ReplaceParentContainer(old, @new)
- Replace the container in the AST first, then call to synchronize traversal state.
- Only one replacement request is allowed per leaf processing pass.
BlockProcessor.TakeLinesBefore()
Use this in TryOpen when TrackTrivia is enabled and the new block should own pending leading blank/trivia lines.
Contract:
- Returns current LinesBefore and clears it.null
- Returns when there is no pending list.
BlockProcessor.IsOpen(Block) and BlockProcessor.TryDiscard(Block)
Use these to safely reason about/modify the open block stack.
Contract:
- IsOpen is a read-only stack membership check.TryDiscard
- removes an open non-root block from both parent container and open stack, returning true only when a discard happened.
MarkdownPipelineBuilder.TrackTrivia
TrackTrivia is now publicly settable and flows into the built pipeline/processors.
Block Parser Contract
BlockState semantics
- None: no match for this parser/block.Skip
- : skip parser for this line and continue with others.Continue
- : block stays open; leaf blocks append line content.ContinueDiscard
- : block stays open; line consumed but not appended.Break
- : block closes; current line remains available for further parsing.BreakDiscard
- : block closes; current line is consumed.
NewBlocks invariants
- Every pushed block must have Parser set to the creating parser.
- Leaf blocks must be pushed last.
- Push order is outer container to inner container to leaf (LIFO pop in processor).
Trivia rules
When TrackTrivia is enabled:
- Use TakeLinesBefore() in TryOpen to assign pending leading blank/trivia lines.UseTrivia(end)
- Use when a parser needs exact trivia slices around syntax markers.
Inline Parser Contract
Match behavior
- On success: advance slice and set processor.Inline when emitting a node.false
- On failure: return without mutating parser output state.
Emission behavior
- If a matched parser sets a parentless processor.Inline, the processor appends it to the deepest open inline container.processor.Emit(inline)
- For explicit emission, use .
Parser state
Use processor.GetParserState<TState>(this) or processor.GetParserState(this, factory) for per-leaf parser state.ProcessInlineLeaf
Parser states are cleared at the beginning of each .
Block transforms from inline parsing
- processor.BlockNew replaces the current leaf block after the current leaf pass returns.processor.ReplaceParentContainer(old, @new)
- keeps traversal coherent when a parent container has already been replaced in the AST.
Hook Selection
| Goal | Preferred hook |
|---|---|
| Add block syntax | BlockParser in MarkdownPipelineBuilder.BlockParsers |InlineParser
| Add inline syntax | in MarkdownPipelineBuilder.InlineParsers |IPostInlineProcessor
| Deferred inline resolution | |InlineProcessor.BlockNew
| Replace current leaf block | |InlineProcessor.ReplaceParentContainer
| Replace parent container from inline | |LiteralInlineParser.PostMatch
| Literal post-processing | |BlockParser.Closed
| Post-close block transform | |Block.ProcessInlinesBegin
| Per-block inline begin/end behavior | / Block.ProcessInlinesEnd |MarkdownPipelineBuilder.DocumentProcessed
| Post-document transform | |
AST Mutation Helpers
Block-level
- Block.Remove(): remove from parent container.Block.ReplaceBy(replacement, moveChildren: true)
- : replace in parent container and optionally move children.ContainerBlock.TransferChildrenTo(destination)
- : move child blocks in order.
Inline-level
- ContainerInline.TransferChildrenTo(destination): move child inlines in order.
Mutation helpers do not auto-recompute spans/trivia; callsites remain responsible for source metadata correctness when required.
Typed Metadata Helpers
For extension state attached to AST nodes:
- SetData<T>(value) / GetData<T>().TryGetData<T>(key, out value)
- / GetData<T>(key) for explicit object keys.DataKey<T>
- for collision-resistant typed keys.
Example:
var key = new DataKey<MyState>();
block.SetData<MyState>(key, state);
if (block.TryGetData<MyState>(key, out var existing))
{
// use existing
}---
Site/Docs/Advanced/Parser Authoring Api Migration
---
title: Parser authoring API migration notes
---
Parser + AST API migration notes (potential breaking changes)
This document tracks compatibility risks for consumers adopting the new parser/AST authoring APIs and for future releases that may tighten contracts.
Current Release Impact
The implemented changes are additive/public-surface expansions:
- InlineProcessor.ReplaceParentContainer(...) is now public.BlockProcessor.TakeLinesBefore()
- is public.BlockProcessor.IsOpen(Block)
- is public.BlockProcessor.TryDiscard(Block)
- is new.InlineProcessor.GetParserState(...)
- and InlineProcessor.Emit(...) are new.Block.Remove()
- , Block.ReplaceBy(...), ContainerBlock.TransferChildrenTo(...), and ContainerInline.TransferChildrenTo(...) are new.MarkdownPipelineBuilder.TrackTrivia
- setter is public.DataKey<T>
- Typed metadata helpers (, MarkdownObjectDataExtensions) are new.
No runtime behavior change is required for existing consumers unless they opt into these APIs.
Potential Future Breaking Changes
These are not implemented in this release, but consumers should avoid relying on ambiguous behavior.
1. ReplaceParentContainer replacement limits
Current contract allows a single replacement request per leaf pass. If this evolves to support multiple requests, error behavior and ordering guarantees may change.
Guidance:
- Keep replacements localized and deterministic.
- Avoid depending on repeated replacement attempts in one pass.
2. Mutation-helper metadata behavior
Mutation helpers currently do not recalculate spans/trivia automatically. A future strict mode might validate or enforce metadata consistency.
Guidance:
- Explicitly set/update Span, Line, Column, and trivia fields in transforms where source fidelity matters.
3. Internal helper cleanup
Internal compatibility shims may be removed in a later major release after migration (for example, internal aliases kept during transition).
Guidance:
- Use public methods (TakeLinesBefore, TryDiscard, etc.) directly.
4. Typed metadata API naming and overload resolution
Some helper overloads share names with existing instance methods. Advanced consumers should prefer explicit generic invocations to avoid ambiguity.
Guidance:
- Prefer explicit generic calls:
- node.SetData<MyType>(value)node.SetData<MyType>(key, value)
- node.GetData<MyType>(key)
- node.TryGetData<MyType>(key, out var value)
-
Migration Checklist for Extension Authors
1. Replace custom open-stack discard patterns with TryDiscard where appropriate.TakeLinesBefore()
2. Use instead of internal trivia helpers.ReplaceParentContainer(old, @new)
3. For inline-driven parent replacement:
- replace the parent container in the AST first,
- then call .RemoveAt(0)
4. Replace manual child-move loops ( patterns) with transfer helpers.
5. Add regression tests for transformed AST shape (parent/child ownership and ordering).
Versioning Guidance
For a future major version, consider:
- deprecating internal transition shims,
- optionally adding strict validation mode for parser invariants and mutation metadata,
- documenting any tightened contracts as migration steps in this file.
---
Site/Docs/Advanced/Performance
---
title: Performance
---
Performance
Markdig is designed for high throughput and low allocations. This page covers the patterns used internally and recommendations for extension authors.
Allocation-free parsing
StringSlice
The core parsing type is StringSlice — a struct that holds a reference to a string plus start/end indices:
public struct StringSlice
{
public string? Text;
public int Start;
public int End; public readonly char CurrentChar => Start <= End ? Text![Start] : '\0';
public readonly int Length => End - Start + 1;
}
All parsers work on StringSlice rather than allocating substrings. When writing parsers, always use StringSlice methods:
{.table}
| Instead of | Use |
|---|---|
| text.Substring(...) | slice.ToString() (only when you need a string) |text[i]
| | slice.PeekCharExtra(offset) |text.Trim()
| | slice.Trim() (mutates the struct) |text.IndexOf(c)
| | slice.IndexOf(c) |
ReadOnlySpan<char>
For hot paths, prefer ReadOnlySpan<char> over string:
// Good — no allocation
ReadOnlySpan<char> span = slice.AsSpan();
if (span.StartsWith("{
// ...
}
// Avoid — allocates a string For small temporary buffers, use For larger buffers, use
string text = slice.ToString();
if (text.StartsWith("`"))
{
// ...
}stackallocstackalloc
:
Span<char> buffer = stackalloc char[64];
int written = FormatOutput(buffer);
renderer.Write(buffer[..written]);ArrayPool<T>ArrayPool
:
using System.Buffers;
char[] buffer = ArrayPool<char>.Shared.Rent(1024);
try
{
// Use buffer
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}
MarkdownPipelinePipeline and renderer reuse
Build once, use many times
The
is immutable and thread-safe afterBuild(). Always cache it:
// Good — build once, use across threads
private static readonly MarkdownPipeline Pipeline =
new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
// Called from many threads
public string Convert(string markdown)
=> Markdown.ToHtml(markdown, Pipeline);
Markdown.ToHtmlRenderer pooling
and friends internally poolHtmlRendererinstances. If you create renderers manually, consider pooling them:
// The static API handles pooling for you:
Markdown.ToHtml(text, pipeline); // Preferred
// Only create renderers manually when you need to customize them
using var writer = new StringWriter();
var renderer = new HtmlRenderer(writer);
pipeline.Setup(renderer);
renderer.Render(document);
sealedExtension authoring tips
Use sealed classes
Mark classes as
when they're not designed for inheritance. This allows the JIT to devirtualize method calls:
// Good — allows devirtualization
public sealed class NoteBlock : LeafBlock
{
public NoteBlock(BlockParser parser) : base(parser) { }
public string? NoteType { get; set; }
}
structPrefer struct over class for small types
For data-only types that are short-lived, prefer
:
// Used only during parsing, never stored long-term
public readonly struct ParseResult
{
public readonly bool Success;
public readonly int EndPosition;
}
forAvoid LINQ in hot paths
LINQ allocates enumerator objects. In parser code, prefer
/foreachloops:
// Avoid in parsers
var match = list.FirstOrDefault(x => x.Type == type);
// Prefer
MyType? match = null;
for (int i = 0; i < list.Count; i++)
{
if (list[i].Type == type)
{
match = list[i];
break;
}
}
StringBuilderMinimize string concatenation
Use
or the renderer's built-inWritechaining:
// Good — chained writes, no intermediate strings
renderer.Write("<div class=\"")
.Write(cssClass)
.Write("\">");
// Avoid — allocates intermediate strings
renderer.Write($"<div class=\"{cssClass}\">");
Cache frequently used strings
For attribute names and CSS classes that repeat:
public sealed class HtmlAlertRenderer : HtmlObjectRenderer<AlertBlock>
{
// Cache the string to avoid repeated allocations
private static readonly HtmlAttributes WarningAttributes = new()
{
Classes = new List<string> { "alert", "alert-warning" }
};
}
Type.GetMethod()AOT and trimming compatibility
Markdig is designed to be compatible with Native AOT and IL trimming.
Avoid reflection
Do not use
,Activator.CreateInstance(), or similar reflection APIs in parsers and renderers:
// Bad — breaks trimming
var parser = (BlockParser)Activator.CreateInstance(parserType)!;
// Good — direct construction
var parser = new NoteBlockParser();
Use source generators when applicable
For serialization scenarios, prefer source generators over reflection-based serialization:
// Good — trimmer-friendly
[JsonSerializable(typeof(MarkdownMetadata))]
internal partial class MetadataJsonContext : JsonSerializerContext { }
[DynamicallyAccessedMembers]Annotate when reflection is unavoidable
If you must use reflection, annotate with
:
public void RegisterParser(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
Type parserType)
{
// ...
}
Benchmarking
Markdig includes a benchmarks project for measuring performance:
cd src
dotnet run -c Release --project Markdig.Benchmarks
The benchmarks compare Markdig against other .NET Markdown processors using BenchmarkDotNet.To benchmark your extension, add a test case to the benchmarks project:
[Benchmark]
public string ConvertWithMyExtension()
{
return Markdown.ToHtml(MarkdownText, _pipelineWithMyExtension);
}
StringSliceSummary of recommendations
{.table}
| Area | Recommendation |
|---|---|
| String handling | UseandReadOnlySpan<char>; avoidSubstring|stackalloc
| Buffers |for small;ArrayPool<T>for large |sealed
| Pipeline | Build once, pass everywhere, reuse across threads |
| Classes |by default;structfor small data types |for
| Loops |/foreachover LINQ in parsers |Write
| Output | Chaincalls; avoid string interpolation in renderers |MarkdownPipeline
| AOT | No reflection; use source generators; annotate if unavoidable |
| Verify | Run benchmarks before/after changes |---
Site/Docs/Advanced/Pipeline
---
title: Pipeline architecture
---Pipeline architecture
This guide explains how the
works, how extensions configure it, and how the parsing flow proceeds from source text to AST to rendered output.MarkdownPipelineBuilderOverview
Markdig's processing involves three objects:
1.
— A mutable builder where you configure extensions, parsers, and options.MarkdownPipeline
2.— An immutable, thread-safe object produced by the builder. Contains the final parser and extension lists.Markdown
3.— 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);
MarkdownPipelineNOTEMarkdownPipelineBuilder
is mutable and not thread-safe. Build pipelines during configuration/startup, then share the builtMarkdownPipelineinstances.What the pipeline holds
The
contains:BlockParserList{.table}
| Component | Type | Description |
|---|---|---|
| Block parsers || Ordered list ofBlockParserobjects |InlineParserList
| Inline parsers || Ordered list ofInlineParserobjects |OrderedList<IMarkdownExtension>
| Extensions || Registered extensions |bool
| TrackTrivia || Whether trivia parsing is enabled |int
| MaximumNestingDepth || Maximum AST nesting depth allowed while parsing and rendering |ProcessDocumentDelegate?
| DocumentProcessed || Callback after parsing completes |MaximumNestingDepthNesting depth limit
Markdig protects parsing and rendering with a conservative default nesting limit of 128 levels. If you process trusted input that can legitimately produce deeper trees, configure
before callingBuild():
var pipeline = new MarkdownPipelineBuilder
{
MaximumNestingDepth = 512,
}
.UseListExtras()
.Build();
Only raise this for trusted content: a larger limit allows deeper documents but can increase parsing cost and rendering stack usage.builder.Build()How Build() works
When you call
:Setup(MarkdownPipelineBuilder)1. Each registered extension's
method is called in order.MarkdownPipeline
2. Extensions may add/remove/modify block parsers, inline parsers, or other settings.
3. The builder's mutable state is frozen into an immutable.Build()
4. The builder caches the result — callingagain returns the same instance.Markdown.Parse(string, pipeline)This is why extension ordering matters: an extension may look for parsers added by a previous extension.
How Parse() works
executes these steps:BlockProcessorStep 1: Block parsing
A
is created with the pipeline's block parsers. It processes the source text line by line:TryContinue1. For each line, the processor checks all open blocks to see if they continue (
).BlockParser
2. Blocks that don't continue are closed.
3. The processor tries eachto see if a new block opens at the current position (TryOpen).OpeningCharacters
4. Characters are dispatched based on— parsers are only tried when their opening character matches.BlockThe result is a tree of
nodes rooted at theMarkdownDocument.TrackTriviaStep 2: Trivia expansion (optional)
If
is enabled, blocks are expanded to absorb neighboring whitespace and trivia. This supports lossless roundtripping.InlineProcessorStep 3: Inline parsing
An
visits eachLeafBlockand runs the pipeline's inline parsers over the block's text content:InlineParser1. Starting from the first character, the processor finds
objects whoseOpeningCharactersmatch.Match
2. Themethod is called on each candidate parser (in order) until one returnstrue.Inline
3. The matched inline is added to the block'scontainer.LiteralInlineParser
4. If no parser matches, aconsumes the character.DocumentProcessed
5. After all characters are consumed, post-processing runs (e.g., emphasis restructuring).Step 4: Post-processing
The
delegate (if set) is invoked on the completed document.MarkdownDocumentStep 5: Return
The
is returned.document.ToHtml(pipeline)How rendering works
Rendering is a separate phase. When you call
:HtmlRenderer1. An
is created (or borrowed from a pool).pipeline.Setup(renderer)
2.is called — this invokesSetup(MarkdownPipeline, IMarkdownRenderer)on every registered extension, giving each one the chance to register itsObjectRenderer.ObjectRenderer
3. The renderer walks the AST, dispatching each node to the appropriateby type.IMarkdownExtension
4. The rendered output is returned.IMPORTANTThis 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-phaseSetupregisters renderers that know how to output those types. Mismatched pipelines = missing renderers.Extension setup: two phases
Every
has twoSetupmethods:
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);
}
Phase 1 example
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));
}
}
Phase 2 example
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlMyBlockRenderer>();
}
}
OrderedList<T>The OrderedList<T> collection
Both parser and extension lists are
, a custom Markdig collection with methods for type-safe insertion:AddIfNotAlready<T>(){.table}
| Method | Description |
|---|---|
|| Add if no instance ofTexists |InsertBefore<TBefore>(item)
|| Insert before a specific type |InsertAfter<TAfter>(item)
|| Insert after a specific type |Find<T>()
|| Find the first instance of typeT|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 typeT|ReplaceOrAdd<T>(newItem)
|| Replace or add if not found |TryRemove<T>()
|| Remove the first instance of typeT|Contains<T>()
|| Check if an instance of typeTexists |ThematicBreakParserDefault parsers
Without any extensions, the pipeline contains these parsers:
Default block parsers
1.
HeadingBlockParser
2.QuoteBlockParser
3.ListBlockParser
4.HtmlBlockParser
5.FencedCodeBlockParser
6.IndentedCodeBlockParser
7.ParagraphBlockParser
8.HtmlEntityParserDefault inline parsers
1.
LinkInlineParser
2.EscapeInlineParser
3.EmphasisInlineParser
4.CodeInlineParser
5.AutolinkInlineParser
6.LineBreakInlineParser
7.Extensions add to or modify these lists.
Trivia and roundtripping
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.---
Site/Docs/Advanced/Readme
---
title: Developer guide
---
Developer guide
Markdig is designed to be deeply extensible. This guide covers how the parsing pipeline works, how to traverse and manipulate the AST, and how to create your own extensions with custom block parsers, inline parsers, and renderers.
Architecture overview
Markdig's processing flow has three stages:
Markdown text → [Block Parsing] → [Inline Parsing] → AST (MarkdownDocument)
↓
[Rendering] → Output (HTML, etc.)
1. Block parsing — TheBlockProcessorwalks through the source text line by line, using registeredBlockParserobjects to identify block-level elements (paragraphs, headings, lists, code blocks, etc.) and build the AST skeleton.InlineProcessor2. Inline parsing — The
visits everyLeafBlockin the AST and runs registeredInlineParserobjects over the block's text to identify inline elements (emphasis, links, code spans, etc.).HtmlRenderer3. Rendering — A renderer (typically
) walks the complete AST and dispatches each node to a matchingObjectRendererfor output.DescendantsExtensions can modify any of these stages: adding new parsers, modifying existing ones, or registering custom renderers.
Guides
{.table}
| Guide | What you'll learn |
|---|---|
| Abstract syntax tree | Structure of block/inline nodes, traversal with, source spans |MarkdownPipeline
| Pipeline architecture | How,MarkdownPipelineBuilder, and extensions interact |IMarkdownExtension
| Creating extensions | Implement— from simple to complex |BlockParser
| Parser authoring API | Authoring contracts and advanced APIs for parser/AST parity |
| Block parsers | Write customsubclasses —TryOpen,TryContinue,BlockState|InlineParser
| Inline parsers | Write customsubclasses —Match,StringSlice, post-processing |HtmlObjectRenderer<T>
| Renderers | Implementor build a completely custom renderer |Markdown
| Performance | Tips for maintaining high throughput — allocation-free patterns, pooling, Span-based parsing |
| Migration notes | Compatibility risks and future contract tightening to avoid depending on ambiguous behavior |Quick reference: key types
{.table}
| Type | Role |
|---|---|
|| Static entry point —Parse,ToHtml,Convert|MarkdownPipeline
|| Immutable, thread-safe configuration object |MarkdownPipelineBuilder
|| Fluent builder forMarkdownPipeline|IMarkdownExtension
|| Interface all extensions implement |BlockParser
|| Abstract base for block-level parsers |InlineParser
|| Abstract base for inline-level parsers |MarkdownDocument
|| Root AST node (aContainerBlock) |Block
|| Base for all block AST nodes |Inline
|| Base for all inline AST nodes |MarkdownObject
|| Base for all AST nodes — providesSpan,Line,Column, data storage |HtmlRenderer
|| Built-in HTML output renderer |HtmlObjectRenderer<T>
|| Base for per-type HTML rendering |IMarkdownRenderer
|| Interface for custom renderers |HtmlRenderer---
Site/Docs/Advanced/Renderers
---
title: Renderers
---Renderers
Renderers walk the AST and produce output. Markdig ships with an
(HTML output), aNormalizeRenderer(canonical Markdown output), and supports fully custom renderers for any output format.document.ToHtml(pipeline)How rendering works
When you call
:HtmlRenderer1. An
is created (internally pooled for performance).pipeline.Setup(renderer)
2.is called — each extension'sSetup(MarkdownPipeline, IMarkdownRenderer)runs, registering per-typeObjectRenderers.ObjectRenderer
3. The renderer walks the AST depth-first, dispatching each node to theregistered for its runtime type.TextWriter
4. Output is written to the underlying.The IMarkdownRenderer interface
public interface IMarkdownRenderer
{
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
ObjectRendererCollection ObjectRenderers { get; }
object Render(MarkdownObject markdownObject);
}
HtmlObjectRenderer<T>HTML renderers
HtmlObjectRenderer<T>
The most common way to render custom AST nodes to HTML is to create a class inheriting from
:
using Markdig.Renderers;
using Markdig.Renderers.Html;
public class HtmlNoteBlockRenderer : HtmlObjectRenderer<NoteBlock>
{
protected override void Write(HtmlRenderer renderer, NoteBlock obj)
{
// Open the div with attributes from the AST node
renderer.Write("<div class=\"note note-")
.Write(obj.NoteType ?? "info")
.Write("\"");
// Write any HTML attributes attached to the node
renderer.WriteAttributes(obj);
renderer.WriteLine(">");
// Write the title
if (!string.IsNullOrEmpty(obj.Title))
{
renderer.Write("<p class=\"note-title\">")
.WriteEscape(obj.Title)
.WriteLine("</p>");
}
// Write inline content (for LeafBlocks)
renderer.WriteLeafInline(obj);
renderer.WriteLine("</div>");
}
}
The generic parameterTautomatically registers this renderer for allNoteBlocknodes.HtmlRendererHtmlRenderer write methods
The
provides these commonly used methods:Write(string){.table}
| Method | Description |
|---|---|
|| Write raw text |Write(char)
|| Write a single character |WriteEscape(string)
|| Write HTML-escaped text |WriteEscape(StringSlice)
|| Write HTML-escaped slice |WriteLeafInline(LeafBlock)
|| Render all inlines of a leaf block |WriteLeafRawLines(LeafBlock)
|| Write raw line content (for code blocks) |WriteAttributes(MarkdownObject)
|| Write attached HTML attributes |WriteLine()
|| Write a newline |EnsureLine()
|| Write a newline only if not already at line start |PushIndent(string)
|| Push indentation for nested content |PopIndent()
|| Pop indentation |ContainerBlockWriting container blocks
For custom
nodes, render children withWriteChildren:
public class HtmlMyContainerRenderer : HtmlObjectRenderer<MyContainerBlock>
{
protected override void Write(HtmlRenderer renderer, MyContainerBlock obj)
{
renderer.Write("<div class=\"my-container\"");
renderer.WriteAttributes(obj);
renderer.WriteLine(">");
// Render all child blocks
renderer.WriteChildren(obj);
renderer.WriteLine("</div>");
}
}
InlineWriting inline renderers
For custom
nodes:
public class HtmlKeyboardRenderer : HtmlObjectRenderer<KeyboardInline>
{
protected override void Write(HtmlRenderer renderer, KeyboardInline obj)
{
renderer.Write("<kbd");
renderer.WriteAttributes(obj);
renderer.Write(">");
renderer.WriteEscape(obj.Shortcut ?? "");
renderer.Write("</kbd>");
}
}
ContainerInlineRendering container inlines
For
types, render children inline:
public class HtmlHighlightRenderer : HtmlObjectRenderer<HighlightInline>
{
protected override void Write(HtmlRenderer renderer, HighlightInline obj)
{
renderer.Write("<mark");
renderer.WriteAttributes(obj);
renderer.Write(">");
// Render child inlines
renderer.WriteChildren(obj);
renderer.Write("</mark>");
}
}
Setup(MarkdownPipeline, IMarkdownRenderer)Registering renderers
Renderers are registered in the extension's
method:
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
// Add if not already present
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlNoteBlockRenderer>();
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlKeyboardRenderer>();
}
}
Insertion ordering
Like parsers, renderers can be inserted at specific positions:
// Insert at the beginning (highest priority)
htmlRenderer.ObjectRenderers.Insert(0, new HtmlNoteBlockRenderer());
// Insert before a specific type
htmlRenderer.ObjectRenderers.InsertBefore<HtmlCodeBlockRenderer>(
new HtmlNoteBlockRenderer());
Modifying existing renderers
You can modify built-in renderers without replacing them:
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
// Modify the CodeBlockRenderer to output divs for specific languages
var codeRenderer = htmlRenderer.ObjectRenderers.FindExact<CodeBlockRenderer>();
if (codeRenderer != null)
{
codeRenderer.BlocksAsDiv.Add("mermaid");
codeRenderer.BlocksAsDiv.Add("nomnoml");
}
}
}
HtmlRendererHtmlRenderer properties
The
has several useful properties:EnableHtmlForInline{.table}
| Property | Type | Default | Description |
|---|---|---|---|
||bool|true| Emit HTML tags for inlines |EnableHtmlForBlock
||bool|true| Emit HTML tags for blocks |EnableHtmlEscape
||bool|true| HTML-escape special characters |falseWhen all three are
, the renderer produces plain text — this is howMarkdown.ToPlainTextworks.Events: Before and After
You can hook into the rendering pipeline with events:
renderer.ObjectWriteBefore += (r, obj) =>
{
if (obj is HeadingBlock heading)
{
Console.WriteLine($"About to render H{heading.Level}");
}
};
renderer.ObjectWriteAfter += (r, obj) =>
{
// Post-processing after each node is rendered
};
Building a completely custom renderer
For non-HTML output (LaTeX, XAML, JSON, etc.), you can implement a full custom renderer:
Option 1: Inherit TextRendererBase
For text-based output formats:
using Markdig.Renderers;
public class LatexRenderer : TextRendererBase<LatexRenderer>
{
public LatexRenderer(TextWriter writer) : base(writer)
{
// Register per-type renderers
ObjectRenderers.Add(new LatexHeadingRenderer());
ObjectRenderers.Add(new LatexParagraphRenderer());
ObjectRenderers.Add(new LatexCodeBlockRenderer());
// ... register all needed renderers
}
}
// Per-type renderer
public class LatexHeadingRenderer : MarkdownObjectRenderer<LatexRenderer, HeadingBlock>
{
protected override void Write(LatexRenderer renderer, HeadingBlock obj)
{
var command = obj.Level switch
{
1 => "section",
2 => "subsection",
3 => "subsubsection",
_ => "paragraph"
};
renderer.Write($"\\{command}{{ "{{" }}");
renderer.WriteLeafInline(obj);
renderer.WriteLine("}");
}
}
Option 2: Implement IMarkdownRenderer directly
For fully custom output (JSON, binary, etc.):
public class JsonRenderer : IMarkdownRenderer
{
public event Action<IMarkdownRenderer, MarkdownObject>? ObjectWriteBefore;
public event Action<IMarkdownRenderer, MarkdownObject>? ObjectWriteAfter;
public ObjectRendererCollection ObjectRenderers { get; } = new();
public object Render(MarkdownObject markdownObject)
{
// Walk the AST and produce JSON
// ...
return jsonBuilder.ToString();
}
}
Markdown.ConvertUsing custom renderers
Pass your renderer to
:
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var renderer = new LatexRenderer(writer);
// Setup extensions for the custom renderer
pipeline.Setup(renderer);
// Render
Markdown.Convert(markdownText, renderer, pipeline);
Complete example: rendering NoteBlock
Putting it all together — the block parser, renderer, and extension:
// Extension
public sealed class NoteExtension : IMarkdownExtension
{
public void Setup(MarkdownPipelineBuilder pipeline)
{
pipeline.BlockParsers.AddIfNotAlready<NoteBlockParser>();
}
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlNoteBlockRenderer>();
}
}
}
// Fluent API
public static class NoteExtensionMethods
{
public static MarkdownPipelineBuilder UseNotes(
this MarkdownPipelineBuilder pipeline)
{
pipeline.Extensions.AddIfNotAlready<NoteExtension>();
return pipeline;
}
}
Usage:var pipeline = new MarkdownPipelineBuilder()
.UseNotes()
.Build();
var html = Markdown.ToHtml("!!! warning \"Be careful\"\n", pipeline);
.UseAbbreviations()Next steps
- Block parsers — Write the parsers that produce custom AST nodes
- Inline parsers — Write inline-level parsers
- Performance — Optimize rendering performance---
Site/Docs/Extensions/Abbreviations
---
title: Abbreviations
---Abbreviations
Enable with
(included inUseAdvancedExtensions()).<abbr>Abbreviations define expansions for acronyms and short terms. When the abbreviation appears in the text, it is wrapped in an
tag with atitleattribute. Inspired by PHP Markdown Extra.*[ABBR]: Full textSyntax
Define abbreviations anywhere in the document using
:
*[HTML]: Hyper Text Markup Language
*[CSS]: Cascading Style Sheets
This page uses HTML and CSS.
*[HTML]: Hyper Text Markup Language
*[CSS]: Cascading Style SheetsThis page uses HTML and CSS.
HTML output
<p>This page uses <abbr title="Hyper Text Markup Language">HTML</abbr>
and <abbr title="Cascading Style Sheets">CSS</abbr>.</p>
.UseAlertBlocks()Rules
- Abbreviation definitions are not rendered as visible content.
- Matching is case-sensitive and matches whole words only.
- Abbreviation definitions can appear anywhere in the document — they apply globally.
- Multiple abbreviations can be defined in the same document.---
Site/Docs/Extensions/Alert Blocks
---
title: Alert blocks
---Alert blocks
Enable with
(included inUseAdvancedExtensions()).<div>Alert blocks are GitHub-style callouts for highlighting important content in documentation. They are rendered as styled
elements.[!TYPE]Syntax
Alert blocks are blockquotes that begin with a special
marker:
Useful information that users should know, even when skimming content.
Helpful advice for doing things better or more easily.
Key information users need to know to achieve their goal.
Urgent info that needs immediate user attention to avoid problems.
Advises about risks or negative outcomes of certain actions.
[!NOTE]NOTEUseful information that users should know, even when skimming content.
TIPHelpful advice for doing things better or more easily.
IMPORTANTKey information users need to know to achieve their goal.
WARNINGUrgent info that needs immediate user attention to avoid problems.
CAUTIONAdvises about risks or negative outcomes of certain actions.
Alert types
{.table}
| Type | Purpose |
|---|---|
|| Supplementary information |[!TIP]
|| Helpful suggestions |[!IMPORTANT]
|| Critical information |[!WARNING]
|| Potential problems |[!CAUTION]
|| Risk of negative outcomes |HTML output
Each alert renders as:
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Your alert content here.</p>
</div>
Themarkdown-alert-{type}CSS class allows you to apply custom styling for each alert kind.Content within alerts
Alerts support full Markdown content — paragraphs, lists, code blocks, emphasis, etc.:
You can use bold, italic, and code in alerts.
> - Even lists work
- Inside alert blocks
> `csharpvar x = 42; // And code blocks too!
`TIPYou can use bold, italic, and code in alerts.
> - Even lists work
- Inside alert blocks
>
var x = 42; // And code blocks too!
`Custom rendering
Pass a custom renderer delegate to UseAlertBlocks to override the kind rendering:
var pipeline = new MarkdownPipelineBuilder()
.UseAlertBlocks(renderKind: (renderer, kind) =>
{
renderer.Write($"<span class=\"icon\">{kind}</span>");
})
.Build();---
Site/Docs/Extensions/Auto Identifiers
---
title: Auto-identifiers
---
Auto-identifiers
Enable with .UseAutoIdentifiers() (included in UseAdvancedExtensions()).
This extension automatically generates id attributes for all headings, similar to Pandoc auto identifiers. This is essential for linking to specific sections.
How it works
Every heading gets an id attribute derived from its text content:
Getting Started
Renders as:
<h2 id="getting-started">Getting Started</h2>You can then link to it:
See Getting Started.ID generation rules
1. Convert to lowercase
2. Remove anything that is not a letter, number, space, or hyphen
3. Replace spaces with hyphens
4. Remove leading/trailing hyphens
Examples:
{.table}
| Heading | Generated ID |
|---|---|
| # Hello World | hello-world |## C# Tips & Tricks
| | c-tips--tricks |### 2. Installation
| | 2-installation |
Duplicate handling
If two headings produce the same ID, a numeric suffix is appended:
Section
Section
Section
Produces: section, section-1, section-2.
Options
UseAutoIdentifiers accepts an AutoIdentifierOptions flags enum:
using Markdig.Extensions.AutoIdentifiers;var pipeline = new MarkdownPipelineBuilder()
.UseAutoIdentifiers(AutoIdentifierOptions.GitHub)
.Build();
Available options:
{.table}
| Option | Description |
|---|---|
| AutoIdentifierOptions.Default | Standard auto-identifier behavior |AutoIdentifierOptions.GitHub
| | GitHub-compatible ID generation |AutoIdentifierOptions.AutoLink
| | Also create a self-link anchor |AutoIdentifierOptions.AllowOnlyAscii
| | Strip non-ASCII characters from IDs |
Combining with generic attributes
You can override the auto-generated ID using Generic attributes:
My Heading {#custom-id}
The explicit #custom-id takes precedence over the auto-generated one.
---
Site/Docs/Extensions/Auto Links
---
title: Auto-links
---
Auto-links
Enable with .UseAutoLinks() (included in UseAdvancedExtensions()).
This extension automatically detects URLs and email addresses in plain text and converts them into clickable links — no angle brackets or explicit Markdown link syntax required.
Detected patterns
URLs starting with these protocols are detected automatically:
- http://https://
- ftp://
- mailto:
- www.
- (rendered as http://www.)
Check out https://github.com/xoofx/markdig for more info.Visit www.example.com for details.
Contact [email protected] for help.
Check out https://github.com/xoofx/markdig for more info.
Visit www.example.com for details.
HTML output
<p>Check out <a href="https://github.com/xoofx/markdig">https://github.com/xoofx/markdig</a> for more info.</p>Options
UseAutoLinks accepts an AutoLinkOptions object:
using Markdig.Extensions.AutoLinks;var pipeline = new MarkdownPipelineBuilder()
.UseAutoLinks(new AutoLinkOptions
{
OpenInNewWindow = true, // Add target="_blank"
UseHttpsForWWWLinks = true // www. links become https:// instead of http://
})
.Build();
Difference from CommonMark autolinks
CommonMark already supports angle-bracket autolinks (<https://example.com>). This extension goes further by detecting bare URLs without any markers.
---
Site/Docs/Extensions/Cjk Friendly Emphasis
---
title: CJK friendly emphasis
---
CJK friendly emphasis
Enable with .UseCjkFriendlyEmphasis() (not included in UseAdvancedExtensions()).
This extension adjusts emphasis delimiter rules to better support Chinese/Japanese/Korean (CJK) text, where words are commonly written without spaces. It follows the markdown-cjk-friendly specification and mitigates the CommonMark emphasis limitation discussed in commonmark-spec#650.
Enable
using Markdig;var pipeline = new MarkdownPipelineBuilder()
.UseCjkFriendlyEmphasis()
.Build();
You can also enable it via string configuration:
var pipeline = new MarkdownPipelineBuilder()
.Configure("common+cjk-friendly-emphasis")
.Build();Examples
Some emphasis sequences that often fail with plain CommonMark in CJK text become parseable with this extension enabled:
この文を強調できますか?残念ながらできません。code
我可以强调这个吗?
이 용어(This term)를 강조해 주세요.
Notes
- This extension intentionally deviates from strict CommonMark emphasis behavior to improve real-world CJK authoring.
- It only affects how emphasis delimiters are interpreted; it does not add new syntax.
---
Site/Docs/Extensions/Custom Containers
---
title: Custom containers
---
Custom containers
Enable with .UseCustomContainers() (included in UseAdvancedExtensions()).
Custom containers generate <div> elements from fenced ::: blocks, similar to how fenced code blocks work. They are inspired by this CommonMark discussion.
Block containers
Use ::: to open and close a container block:
::: warning
This is a warning container. You can put any Markdown content here.- Including lists
- And other blocks
:::
::: warning
This is a warning container. You can put any Markdown content here.
- Including lists
- And other blocks
:::
HTML output
<div class="warning">
<p>This is a warning container...</p>
</div>The text after ::: becomes the CSS class of the <div>.
Inline containers
Use a single : pair for inline containers:
This has a :custom-span[styled word]{.highlight} in it.Nesting
Containers can be nested using more colons:
:::: outer
::: inner
Nested content.
:::
::::Attributes
Combine with Generic attributes for full control:
::: {.alert .alert-info #my-alert role="alert"}
This is an informational alert.
:::This produces:
<div class="alert alert-info" id="my-alert" role="alert">
<p>This is an informational alert.</p>
</div>---
Site/Docs/Extensions/Definition Lists
---
title: Definition lists
---
Definition lists
Enable with .UseDefinitionLists() (included in UseAdvancedExtensions()).
Definition lists render as <dl> / <dt> / <dd> HTML elements. Inspired by PHP Markdown Extra.
Syntax
A definition list consists of terms followed by their definitions. Definitions are prefixed with : (colon followed by a space):
Term 1
: Definition of term 1.Term 2
: Definition of term 2.
: Another definition of term 2.
Term 1
: Definition of term 1.
Term 2
: Definition of term 2.
: Another definition of term 2.
Multi-line definitions
Definitions can span multiple lines and contain block-level content:
Apple
: A fruit that grows on trees. Apples come in many varieties including
Granny Smith and Fuji.
Orange
: A citrus fruit.
Apple
: A fruit that grows on trees.
Apples come in many varieties including
Granny Smith and Fuji.
Orange
: A citrus fruit.
Multiple terms per definition
Term A
Term B
: Shared definition for both terms.Term A
Term B
: Shared definition for both terms.
HTML output
<dl>
<dt>Term 1</dt>
<dd>Definition of term 1.</dd>
<dt>Term 2</dt>
<dd>Definition of term 2.</dd>
<dd>Another definition of term 2.</dd>
</dl>Rules
- There must be a blank line before the first term (or it will be treated as a paragraph).
- The : marker must be followed by at least one space.
- Continuation lines must be indented.
---
Site/Docs/Extensions/Diagrams
---
title: Diagrams
---
Diagrams
Enable with .UseDiagrams() (included in UseAdvancedExtensions()).
When a fenced code block uses a recognized diagram language as its info string, Markdig renders it as a plain HTML block (without the nested <code> element) so client-side diagram libraries can consume the raw text easily.
By default:
- mermaid renders as a <pre class="mermaid">...</pre> blocknomnoml
- renders as a <div class="nomnoml">...</div> block
Supported languages
{.table}
| Language | Info string |
|---|---|
| Mermaid | mermaid |nomnoml
| nomnoml | |
Mermaid example
graph LR
A[Parse] --> B[AST]
B --> C[Render]
C --> D[HTML]
This renders as:
mermaidgraph LR
A[Parse] --> B[AST]
B --> C[Render]
C --> D[HTML]
To display the diagram in a browser, include the Mermaid JavaScript library:
html<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true });</script>
nomnoml example
[Markdown] -> [Parser]
[Parser] -> [AST]
[AST] -> [Renderer]
[Renderer] -> [HTML]
HTML output
Instead of the usual code block rendering:
html<pre><code class="language-mermaid">...</code></pre>
The diagrams extension produces a plain block for recognized languages, for example:
html<pre class="mermaid">...</pre>
<div class="nomnoml">...</div>
This allows client-side diagram libraries to find and render the content.
---
Site/Docs/Extensions/Emoji Smartypants
---
title: "Emoji & SmartyPants"
---
Emoji & SmartyPants
Emoji
Enable with
.UseEmojiAndSmiley() (not included in UseAdvancedExtensions()).This extension converts emoji shortcodes and (optionally) ASCII smileys into Unicode emoji characters.
Shortcode syntax
markdown:smile: :+1: :heart: :rocket: :warning:
Disable smileys
By default, ASCII smileys like
:) are also converted. To use only named shortcodes:csharpvar pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(enableSmileys: false)
.Build();
Custom emoji mappings
csharpusing Markdig.Extensions.Emoji;var mapping = new EmojiMapping(
new Dictionary<string, string>
{
{ ":custom:", "🎉" },
{ ":markdig:", "📝" }
});
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(mapping)
.Build();
Common shortcodes
{.table}
| Shortcode | Emoji |
|---|---|
|
:smile: | 😄 |
| :+1: | 👍 |
| :heart: | ❤️ |
| :rocket: | 🚀 |
| :warning: | ⚠️ |
| :star: | ⭐ |
| :fire: | 🔥 |
| :bug: | 🐛 |
| :bulb: | 💡 |
| :memo: | 📝 |SmartyPants
Enable with
.UseSmartyPants() (not included in UseAdvancedExtensions()).SmartyPants converts ASCII punctuation into typographically correct HTML entities. Inspired by Daring Fireball — SmartyPants.
Transformations
{.table}
| Input | Output | Description |
|---|---|---|
|
"Hello" | "Hello" | Smart double quotes |
| 'Hello' | 'Hello' | Smart single quotes |
| -- | – | En dash |
| --- | — | Em dash |
| ... | … | Ellipsis |
| << | « | Left guillemet |
| >> | » | Right guillemet |Usage
csharpvar pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants()
.Build();var html = Markdown.ToHtml("He said \"Hello\" -- she replied 'Hi'...", pipeline);
Options
csharpusing Markdig.Extensions.SmartyPants;var options = new SmartyPantOptions();
// Configure options as needed
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants(options)
.Build();
---
Site/Docs/Extensions/Emphasis Extras
---
title: Emphasis extras
---
Emphasis extras
Enable with
.UseEmphasisExtras() (included in UseAdvancedExtensions()).This extension adds several emphasis styles beyond standard bold and italic.
If you need improved emphasis parsing for Chinese/Japanese/Korean (CJK) text, see CJK friendly emphasis.
Strikethrough
Wrap text with
~~ for strikethrough:markdownThis is ~~deleted~~ text.
This is ~~deleted~~ text.
Subscript
Wrap text with
~ for subscript:markdownH~2~O is water.
H~2~O is water.
Superscript
Wrap text with
^ for superscript:markdown2^10^ is 1024.
2^10^ is 1024.
Inserted text
Wrap text with
++ for inserted/underlined text:markdownThis text has been ++inserted++.
This text has been ++inserted++.
Marked/highlighted text
Wrap text with
== for marked/highlighted text:markdownThis is ==highlighted== text.
This is ==highlighted== text.
HTML output
{.table}
| Syntax | HTML output |
|---|---|
|
~~text~~ | <del>text</del> |
| ~text~ | <sub>text</sub> |
| ^text^ | <sup>text</sup> |
| ++text++ | <ins>text</ins> |
| ==text== | <mark>text</mark> |Selective activation
By default, all emphasis extras are enabled. Use
EmphasisExtraOptions to enable only specific ones:csharpusing Markdig.Extensions.EmphasisExtras;var pipeline = new MarkdownPipelineBuilder()
.UseEmphasisExtras(EmphasisExtraOptions.Strikethrough | EmphasisExtraOptions.Superscript)
.Build();
Available options:
-
EmphasisExtraOptions.Strikethrough
- EmphasisExtraOptions.Subscript
- EmphasisExtraOptions.Superscript
- EmphasisExtraOptions.Inserted
- EmphasisExtraOptions.Marked
- EmphasisExtraOptions.Default (all of the above)---
Site/Docs/Extensions/Figures Footers Citations
---
title: "Figures, footers & citations"
---
Figures, footers & citations
These three related extensions add HTML5 semantic elements to Markdown.
Figures
Enable with
.UseFigures() (included in UseAdvancedExtensions()).Use
^^^ to create <figure> blocks with optional <figcaption>:markdown^^^^^^ A beautiful mountain landscape
HTML output
html<figure>
<p><img src="mountain.jpg" alt="A scenic mountain" /></p>
<figcaption>A beautiful mountain landscape</figcaption>
</figure>
Multiple items in a figure
markdown^^^
^^^ A gallery of photos
Footers
Enable with
.UseFooters() (included in UseAdvancedExtensions()).Use
^^ at the start of a line to create <footer> elements:markdown^^ This is a footer element.
HTML output
html<footer>This is a footer element.</footer>
Multi-line footers
markdown^^ This is the first line of the footer.
^^ This is the second line.
Citations
Enable with
.UseCitations() (included in UseAdvancedExtensions()).Wrap text in double quotes
"" to create a <cite> element:markdown""The Art of Computer Programming""
HTML output
html<p><cite>The Art of Computer Programming</cite></p>
In context
markdownAs described in ""Design Patterns"" by the Gang of Four, the Observer pattern
is used to define a one-to-many dependency between objects.
Combining figures with citations
markdown^^^
"The best way to predict the future is to invent it." — Alan Kay
^^^ ""Computing in the 21st Century""
---
Site/Docs/Extensions/Footnotes
---
title: Footnotes
---
Footnotes
Enable with
.UseFootnotes() (included in UseAdvancedExtensions()).Footnotes allow you to add references that appear at the bottom of the document, inspired by PHP Markdown Extra.
Syntax
Define a footnote reference inline with
[^label] and the footnote content elsewhere:markdownHere is a sentence with a footnote[^1].And another with a named footnote[^note].
[^1]: This is the first footnote content.
[^note]: Footnotes can have any label, not just numbers.
Here is a sentence with a footnote[^1].
And another with a named footnote[^note].
[^1]: This is the first footnote content.
[^note]: Footnotes can have any label, not just numbers.
Multi-line footnotes
Indent continuation lines to include multiple paragraphs in a footnote:
markdownThis has a long footnote[^long].[^long]: This is the first paragraph of the footnote.
This is the second paragraph. It must be indented to be
included in the footnote.
- Even lists work in footnotes
- Like this one
This has a long footnote[^long].
[^long]: This is the first paragraph of the footnote.
This is the second paragraph. It must be indented to be
included in the footnote.
- Even lists work in footnotes
- Like this one
Inline footnotes
You can also define footnotes inline (though this is less common):
markdownThis has an inline footnote^[This is the inline footnote content].
HTML output
Footnote references become superscript links, and footnote definitions are collected into a
<section> at the end of the page:html<p>Text with a footnote<a href="#fn:1" class="footnote-ref"><sup>1</sup></a>.</p><section class="footnotes">
<ol>
<li id="fn:1">
<p>This is the footnote content.
<a href="#fnref:1" class="footnote-back-ref">↩</a></p>
</li>
</ol>
</section>
Rules
- Footnote labels are case-insensitive.
- Footnote definitions can appear anywhere in the document — they are always rendered at the end.
- Unused footnote definitions are not rendered.
- Multiple references to the same footnote share the same content.
---
Site/Docs/Extensions/Generic Attributes
---
title: Generic attributes
---
Generic attributes
Enable with
.UseGenericAttributes() (included in UseAdvancedExtensions()).This extension allows attaching CSS classes, IDs, and arbitrary HTML attributes to nearly any Markdown element using
{...} syntax. Inspired by PHP Markdown Extra — Special Attributes.IMPORTANTUseGenericAttributes() should be the last extension added to the pipeline, as it modifies other parsers to recognize attribute syntax.
Syntax
Place
{...} after a Markdown element:markdownHeading {#custom-id .special-class}
A paragraph with attributes. {.lead}
A link{target="_blank" rel="noopener"}
Supported attribute types
{.table}
| Syntax | Meaning | Example |
|---|---|---|
|
#value | HTML id | {#my-id} |
| .value | CSS class | {.my-class} |
| key=value | HTML attribute | {data-count=5} |
| key="value" | Quoted attribute | {title="Hello World"} |Multiple attributes can be combined:
markdownA paragraph. {#intro .highlight data-section="overview"}
Applying to blocks
Headings
markdownMy Section {#section-1 .special}
Renders as:
<h2 id="section-1" class="special">My Section</h2>Paragraphs
Place the attributes at the end of the paragraph:
markdownThis is a styled paragraph. {.lead .text-center}
Code blocks
var x = 42;
Blockquotes
markdownA styled blockquote {.fancy-quote}
Tables
The
.table class is commonly used to apply Bootstrap-style table formatting:markdown{.table .table-striped}
| A | B |
|---|---|
| 1 | 2 |
Applying to inlines
Links
markdownClick me{.btn .btn-primary}
Images
markdown{.rounded width="200"}
Emphasis
markdownImportant text{.text-danger}
HTML output
markdownA paragraph. {.lead #intro}
Produces:
html<p class="lead" id="intro">A paragraph.</p>
---
Site/Docs/Extensions/List Extras
---
title: List extras
---
List extras
Enable with
.UseListExtras() (included in UseAdvancedExtensions()).This extension adds support for additional ordered list item types beyond the standard numeric
1. markers.Alpha lists
Use lowercase or uppercase letters followed by
.:markdowna. First item
b. Second item
c. Third item
a. First item
b. Second item
c. Third item
Uppercase
markdownA. First item
B. Second item
C. Third item
A. First item
B. Second item
C. Third item
Roman numeral lists
Use lowercase or uppercase Roman numerals followed by
.:markdowni. First item
ii. Second item
iii. Third item
iv. Fourth item
i. First item
ii. Second item
iii. Third item
iv. Fourth item
Uppercase Roman
markdownI. First item
II. Second item
III. Third item
I. First item
II. Second item
III. Third item
HTML output
The
type attribute is set on the <ol> element:html<ol type="a">
<li>First item</li>
<li>Second item</li>
</ol>
{.table}
| Marker | HTML
type |
|---|---|
| a. | a |
| A. | A |
| i. | i |
| I. | I |---
Site/Docs/Extensions/Mathematics
---
title: Mathematics
---
Mathematics
Enable with
.UseMathematics() (included in UseAdvancedExtensions()).This extension supports LaTeX-style math using
$ for inline and $$ for display (block) math.Inline math
Wrap math expressions with single
$:markdownThe quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.
The quadratic formula is $x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}$.
Block math
Wrap display equations with
$$ on their own lines:markdown$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
$$
\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}
$$
HTML output
- Inline math renders as
<span class="math">\(...\)</span>
- Block math renders as <div class="math">\[...\]</div>This HTML is designed to be consumed by math rendering libraries such as KaTeX or MathJax.
Example output
html<p>The formula is <span class="math">\(E = mc^2\)</span>.</p>
html<div class="math">\[
\sum_{i=1}^n i = \frac{n(n+1)}{2}
\]</div>
Rules
- Inline math (
$...$) must not have a space immediately after the opening $ or before the closing $.
- Block math ($$...$$) uses $$ on separate lines.
- A single $ surrounded by spaces is treated as a literal dollar sign, not math.Integrating with KaTeX or MathJax
After rendering to HTML, include a math library in your page to render the formulas:
html<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex/dist/contrib/auto-render.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\(', right: '\\)', display: false},
{left: '\\[', right: '\\]', display: true}
],
// • rendering keys, e.g.:
throwOnError : false
});
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body, {
// customised options
// • auto-render specific keys, e.g.:
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\(', right: '\\)', display: false},
{left: '\\[', right: '\\]', display: true}
],
// • rendering keys, e.g.:
throwOnError : false
});
});
</script>---
Site/Docs/Extensions/Media Links
---
title: Media links
---
Media links
Enable with
.UseMediaLinks() (included in UseAdvancedExtensions()).This extension converts image links (
) pointing to known media services into embedded players. When a Markdown image link targets a YouTube video, Vimeo clip, or other supported media URL, Markdig renders an <iframe> instead of an <img>.Supported services
{.table}
| Service | URL pattern |
|---|---|
| YouTube |
youtube.com/watch?v=..., youtu.be/... |
| Vimeo | vimeo.com/... |
| Dailymotion | dailymotion.com/video/... |
| Yandex | video.yandex.ru/... |
| Odnoklassniki | ok.ru/video/... |Syntax
Use standard Markdown image syntax with a media URL:
markdown
HTML output
html<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
width="500" height="281"
frameborder="0"
allowfullscreen></iframe>
Direct media files
For direct links to media files the extension supports:
{.table}
| Format | Type |
|---|---|
|
.mp4, .webm, .ogg | <video> element |
| .mp3, .wav, .ogg | <audio> element |markdown
Produces:
html<video width="500" height="281" controls>
<source type="video/mp4" src="video.mp4"></source>
</video>
Options
csharpusing Markdig.Extensions.MediaLinks;var options = new MediaOptions
{
Width = "800",
Height = "450",
AddControlsProperty = true
};
var pipeline = new MarkdownPipelineBuilder()
.UseMediaLinks(options)
.Build();
---
Site/Docs/Extensions/Other
---
title: Other extensions
---
Other extensions
This page covers smaller or more specialized extensions that are not part of
UseAdvancedExtensions().Bootstrap
Enable with
.UseBootstrap().Adds Bootstrap CSS classes to generated HTML elements:
{.table}
| Element | Class applied |
|---|---|
|
<table> | table |
| <blockquote> | blockquote |
| <figure> | figure |
| <figcaption> | figure-caption |
| <img> | img-fluid |csharpvar pipeline = new MarkdownPipelineBuilder()
.UseBootstrap()
.Build();
Hardline breaks
Enable with
.UseSoftlineBreakAsHardlineBreak().Makes every soft line break (a single newline inside a paragraph) render as a
<br> tag instead of a space. Useful when you want each line to appear as written.csharpvar pipeline = new MarkdownPipelineBuilder()
.UseSoftlineBreakAsHardlineBreak()
.Build();
Without this extension:
markdownLine one
Line two
Renders as:
<p>Line one Line two</p>With this extension, it renders as:
<p>Line one<br />Line two</p>JIRA links
Enable with
.UseJiraLinks(options).Automatically converts JIRA-style project references (e.g.,
PROJECT-123) into clickable links.csharpusing Markdig.Extensions.JiraLinks;var pipeline = new MarkdownPipelineBuilder()
.UseJiraLinks(new JiraLinkOptions("https://jira.example.com/browse/"))
.Build();
var html = Markdown.ToHtml("Fixed in PROJ-456.", pipeline);
// => <p>Fixed in <a href="https://jira.example.com/browse/PROJ-456">PROJ-456</a>.</p>
Globalization
Enable with
.UseGlobalization().Adds appropriate
dir attributes on HTML elements for right-to-left content. Detects the text direction of each block and annotates it.csharpvar pipeline = new MarkdownPipelineBuilder()
.UseGlobalization()
.Build();
Referral links
Enable with
.UseReferralLinks(rels).Adds
rel attributes to all rendered links:csharpvar pipeline = new MarkdownPipelineBuilder()
.UseReferralLinks("nofollow", "noopener", "noreferrer")
.Build();
All links will have
rel="nofollow noopener noreferrer" added.Self pipeline
Enable with
.UseSelfPipeline().Detects the pipeline configuration from the Markdown document itself via a special HTML comment tag:
markdown| A | B |
|---|---|
| 1 | 2 |
WARNINGUseSelfPipeline cannot be combined with other extensions on the same builder — it replaces the pipeline entirely based on the document content.
csharpvar pipeline = new MarkdownPipelineBuilder()
.UseSelfPipeline() // Must be the only extension
.Build();
Pragma lines
Enable with
.UsePragmaLines().Inserts
<span id="pragma-line-N"></span> markers into the HTML output for each source line. This is useful for editor synchronization (scrolling an editor to the rendered position).csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePragmaLines()
.Build();
Non-ASCII no escape
Enable with
.UseNonAsciiNoEscape().Disables percent-encoding of non-ASCII characters in URLs. This works around a rendering bug in Internet Explorer/Edge with local file links containing non-US-ASCII characters.
CAUTIONOnly use this extension if you specifically need IE/Edge compatibility with non-ASCII file paths. It changes standard URL encoding behavior.
csharpvar pipeline = new MarkdownPipelineBuilder()
.UseNonAsciiNoEscape()
.Build();
Precise source location
Enable with
.UsePreciseSourceLocation().Maps every AST node to its exact position in the original source text via the
Span property. Useful for syntax highlighting, editors, and linters.csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePreciseSourceLocation()
.Build();var document = Markdown.Parse(text, pipeline);
foreach (var node in document.Descendants())
{
Console.WriteLine($"{node.GetType().Name}: {node.Span}");
}
Disable HTML
Use
.DisableHtml() (a configuration option, not an extension).Removes the HTML block parser and disables inline HTML parsing. This prevents raw HTML injection, but it is not a complete security solution on its own:
csharpvar pipeline = new MarkdownPipelineBuilder()
.DisableHtml()
.Build();
IMPORTANTMarkdig does not sanitize the generated HTML. If you render untrusted Markdown in a browser, you should still sanitize the output HTML (and consider filtering/rewriting link and image URLs) to prevent XSS.
---
Site/Docs/Extensions/Readme
---
title: Extensions
---
Extensions
Markdig ships with 20+ built-in extensions that go beyond CommonMark. Extensions are enabled via the
MarkdownPipelineBuilder fluent API.Quick start
Enable all advanced extensions at once:
csharpvar pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
Or enable specific extensions individually:
csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePipeTables()
.UseFootnotes()
.UseMathematics()
.Build();
What UseAdvancedExtensions includes
UseAdvancedExtensions() enables these extensions:{.table}
| Extension | Method | Description |
|---|---|---|
| Alert blocks |
.UseAlertBlocks() | GitHub-style [!NOTE], [!TIP], etc. |
| Abbreviations | .UseAbbreviations() | Abbreviation definitions: *[HTML]: Hyper Text Markup Language |
| Auto-identifiers | .UseAutoIdentifiers() | Automatic id attributes on headings |
| Citations | .UseCitations() | Citation text with ""..."" |
| Custom containers | .UseCustomContainers() | Fenced ::: div containers |
| Definition lists | .UseDefinitionLists() | <dl> / <dt> / <dd> lists |
| Emphasis extras | .UseEmphasisExtras() | Strikethrough, sub/superscript, inserted, marked |
| Figures | .UseFigures() | ^^^ figure blocks |
| Footers | .UseFooters() | ^^ footers |
| Footnotes | .UseFootnotes() | [^ref] footnotes |
| Grid tables | .UseGridTables() | Pandoc-style grid tables |
| Mathematics | .UseMathematics() | $...$ inline / $$...$$ block math |
| Media links | .UseMediaLinks() | Embed YouTube, Vimeo, etc. |
| Pipe tables | .UsePipeTables() | GitHub-style pipe tables |
| List extras | .UseListExtras() | Alpha and Roman numeral ordered lists |
| Task lists | .UseTaskLists() | - [x] / - [ ] checkboxes |
| Diagrams | .UseDiagrams() | Mermaid, nomnoml diagram blocks |
| Auto-links | .UseAutoLinks() | Auto-detect http://, www. URLs |
| Generic attributes | .UseGenericAttributes() | {.class #id key=value} attributes |Additional extensions (not in UseAdvancedExtensions)
{.table}
| Extension | Method | Description |
|---|---|---|
| Emoji |
.UseEmojiAndSmiley() | :emoji: shortcodes and smileys |
| SmartyPants | .UseSmartyPants() | Smart quotes, dashes, ellipses |
| Bootstrap | .UseBootstrap() | Bootstrap CSS classes |
| Hardline breaks | .UseSoftlineBreakAsHardlineBreak() | Treat soft line breaks as <br> |
| YAML front matter | .UseYamlFrontMatter() | Parse and discard YAML front matter |
| JIRA links | .UseJiraLinks(options) | Auto-link Jira issue keys |
| Globalization | .UseGlobalization() | Right-to-left text support |
| Referral links | .UseReferralLinks(rels) | Add rel attributes to links |
| Self pipeline | .UseSelfPipeline() | Auto-configure pipeline from document |
| Pragma lines | .UsePragmaLines() | Line number pragma IDs |
| Non-ASCII no escape | .UseNonAsciiNoEscape() | Disable URI escaping for non-ASCII |
| CJK friendly emphasis | .UseCjkFriendlyEmphasis() | Improved emphasis delimiter rules for CJK text |Extension ordering
Extensions are applied in the order they are added to the builder. Most are order-independent, but
UseGenericAttributes() should be added last because it modifies other parsers.csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePipeTables()
.UseFootnotes()
.UseMathematics()
.UseGenericAttributes() // Always last!
.Build();
---
Site/Docs/Extensions/Tables
---
title: Tables
---
Tables
Markdig supports two kinds of tables: pipe tables (GitHub-style) and grid tables (Pandoc-style).
Pipe tables
Enable with
.UsePipeTables() (included in UseAdvancedExtensions()).Basic syntax
Columns are separated by
|. A header row is separated from the body by a line of dashes:markdown| Name | Language | Stars |
|----------|----------|-------|
| Markdig | C# | 4.5k |
| cmark | C | 1.6k |
| markdown-it | JavaScript | 18k |
| Name | Language | Stars |
|----------|----------|-------|
| Markdig | C# | 4.5k |
| cmark | C | 1.6k |
| markdown-it | JavaScript | 18k |
Column alignment
Use colons in the separator row to control alignment:
markdown| Left | Center | Right |
|:-------|:-------:|-------:|
| one | two | three |
| four | five | six |
| Left | Center | Right |
|:-------|:-------:|-------:|
| one | two | three |
| four | five | six |
Optional leading/trailing pipes
The outer pipes are optional:
markdownName | Language
-----|--------
Markdig | C#
cmark | C
Name | Language
-----|--------
Markdig | C#
cmark | C
Inline formatting in cells
Cells support inline Markdown — emphasis, code, links, etc.:
markdown| Feature | Status |
|---------------|---------------|
| Bold | ~~removed~~ |
| code | link |
| Feature | Status |
|---------------|---------------|
| Bold | ~~removed~~ |
|
code | link |Escaped pipes
Use
\| to include a literal pipe inside a cell:markdown| Expression | Result |
|-------------|--------|
| a \| b | a or b |
Options
UsePipeTables accepts a PipeTableOptions object:csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePipeTables(new PipeTableOptions
{
UseHeaderForColumnCount = true // GFM-compatible column counting
})
.Build();
| Option | Default | Description |
|----------------------------------|---------|-------------|
|
RequireHeaderSeparator | true | Whether the dashed separator row is required. Set to false for Kramdown-style tables that allow headerless tables. |
| UseHeaderForColumnCount | false | When true, the header row's column count is authoritative — short rows are padded with empty cells and extra cells in wider rows are dropped. When false, the widest row determines the column count. |
| InferColumnWidthsFromSeparator | false | When true, populates TableColumnDefinition.Width based on the dash count of each column in the separator row, normalized to percentages that sum to 100. When false, Width stays 0 and no width information is emitted. |#### Inferring column widths from the separator
With
InferColumnWidthsFromSeparator = true, the width of each column is proportional to the number of - characters under it in the separator row. This is useful when you want authors to control relative column widths directly in the Markdown source.csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePipeTables(new PipeTableOptions { InferColumnWidthsFromSeparator = true })
.Build();
Given this input:
markdown| A | B |
|---|--------|
| 1 | 2 |
the first column gets
Width = 25 and the second Width = 75 (a 3:9 ratio of dashes, normalized to 100). The HTML renderer emits a <colgroup> with <col style="width:N%" /> entries so the widths flow through to the rendered table. The values are also available on Table.ColumnDefinitions[i].Width for custom renderers.Grid tables
Enable with
.UseGridTables() (included in UseAdvancedExtensions()).Grid tables use
+, -, and | characters to draw a grid. They support multi-line cells, column spanning, and richer content than pipe tables.Basic grid table
markdown+-----------+-----------+
| Header 1 | Header 2 |
+===========+===========+
| Cell 1 | Cell 2 |
+-----------+-----------+
| Cell 3 | Cell 4 |
+-----------+-----------+
+-----------+-----------+
| Header 1 | Header 2 |
+===========+===========+
| Cell 1 | Cell 2 |
+-----------+-----------+
| Cell 3 | Cell 4 |
+-----------+-----------+
Multi-line cells
Grid table cells can contain multiple lines and block-level content:
markdown+-----------+-------------------+
| Name | Description |
+===========+===================+
| Markdig | A fast, powerful |
| | Markdown parser. |
+-----------+-------------------+
| cmark | The C reference |
| | implementation. |
+-----------+-------------------+
+-----------+-------------------+
| Name | Description |
+===========+===================+
| Markdig | A fast, powerful |
| | Markdown parser. |
+-----------+-------------------+
| cmark | The C reference |
| | implementation. |
+-----------+-------------------+
Column spanning
Use a continuous line (without
+ separators) to span columns:markdown+-------+-------+
| A | B |
+=======+=======+
| Cell spanning |
+-------+-------+
+-------+-------+
| A | B |
+=======+=======+
| Cell spanning |
+-------+-------+Header separator
Use
= instead of - for the header separator line (+===+===+).---
Site/Docs/Extensions/Task Lists
---
title: Task lists
---
Task lists
Enable with
.UseTaskLists() (included in UseAdvancedExtensions()).Task lists add checkbox-style list items, inspired by GitHub task lists.
Syntax
Start a list item with
[ ] (unchecked) or [x]/[X] (checked):markdown- [x] Write documentation
- [x] Implement feature
- [ ] Write tests
- [ ] Release
- [x] Write documentation
- [x] Implement feature
- [ ] Write tests
- [ ] Release
In ordered lists
Task lists also work with ordered list items:
markdown1. [x] First task
2. [ ] Second task
3. [ ] Third task
1. [x] First task
2. [ ] Second task
3. [ ] Third task
HTML output
Checked items render as
<input type="checkbox" disabled checked />, unchecked items as <input type="checkbox" disabled />:html<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled checked /> Write docs</li>
<li class="task-list-item"><input type="checkbox" disabled /> Write tests</li>
</ul>
---
Site/Docs/Extensions/Yaml Frontmatter
---
title: YAML front matter
---
YAML front matter
Enable with
.UseYamlFrontMatter() (not included in UseAdvancedExtensions()).This extension parses YAML front matter blocks at the beginning of a document. The YAML content is parsed into the AST as a
YamlFrontMatterBlock but is not rendered in the HTML output.Syntax
The YAML front matter is enclosed between
--- delimiters at the very beginning of the document:markdown---
title: My Document
author: John Doe
date: 2025-01-15
tags:
- markdown
- documentation
---My Document
Content starts here.
Usage
csharpusing Markdig;
using Markdig.Extensions.Yaml;
using Markdig.Syntax;var pipeline = new MarkdownPipelineBuilder()
.UseYamlFrontMatter()
.Build();
var document = Markdown.Parse(markdownText, pipeline);
// The YAML front matter is in the AST but not rendered
var html = document.ToHtml(pipeline); // Front matter is excluded
// Access the YAML block from the AST
var yamlBlock = document.Descendants<YamlFrontMatterBlock>().FirstOrDefault();
if (yamlBlock != null)
{
// Get the raw YAML content (you can then parse it with a YAML library)
var yaml = yamlBlock.Lines.ToString();
}
Rules
- The
--- opener must be the very first line of the document (no leading blank lines).
- The closing --- must appear on its own line.
- Only one YAML front matter block is recognized per document.Processing the YAML content
Markdig only parses the YAML front matter — it does not evaluate it. To process the YAML content, use a YAML library such as YamlDotNet:
csharpusing YamlDotNet.Serialization;var yamlContent = yamlBlock.Lines.ToString();
var deserializer = new DeserializerBuilder().Build();
var metadata = deserializer.Deserialize<Dictionary<string, object>>(yamlContent);
Common use case
YAML front matter is widely used in static site generators (Hugo, Jekyll, Lunet) to store document metadata like title, date, author, and tags. Markdig's YAML extension lets you preview such documents while stripping the metadata from the rendered output.
---
Site/Docs/Commonmark
---
title: CommonMark syntax
---
CommonMark syntax
Markdig is fully compliant with the CommonMark specification (v0.31.2), passing 600+ spec tests. This page is a reference for all core Markdown syntax supported out of the box — no extensions required.
Headings
ATX headings
Use
# characters (1–6) followed by a space:markdownHeading 1
Heading 2
Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
Setext headings
Underline text with
= (level 1) or - (level 2):markdownHeading 1
=========Heading 2
---------
Paragraphs
Paragraphs are separated by one or more blank lines. A single newline within a paragraph does not create a line break — it's treated as a space.
markdownThis is the first paragraph.This is the second paragraph. This sentence
continues on the next line but renders inline.
Line breaks
To create a hard line break within a paragraph, end a line with two or more spaces or a backslash
\:markdownFirst line
Second line (two trailing spaces above)First line\
Second line (backslash above)
Emphasis and strong emphasis
markdownitalic text or _italic text_
bold text or __bold text__
bold and italic or ___bold and italic___
Renders as:
- italic text
- bold text
- bold and italic
Links
Inline links
markdownLink text
Link with title
Reference links
markdown[Link text][ref]
[Another link][ref][ref]: https://example.com "Optional Title"
Autolinks
Angle brackets around a URL or email:
markdown<https://example.com>
<[email protected]>
Images
markdown![Alt text][imgref][imgref]: https://example.com/image.png "Title"
Code
Inline code
markdownUse the Markdown.ToHtml() method.
Use the
Markdown.ToHtml() method.Fenced code blocks
Use triple backticks or triple tildes, optionally with a language identifier:
var html = Markdown.ToHtml("Hello world!");
Or with tildes:
~~~python
print("Hello, world!")
~~~Indented code blocks
Indent every line by 4 spaces or 1 tab:
markdownvar x = 42;
Console.WriteLine(x);
Blockquotes
Prefix lines with
>:markdownThis is a blockquote.
> It can span multiple paragraphs.
> > And be nested.
This is a blockquote.
> It can span multiple paragraphs.
> > And be nested.
Lists
Unordered lists
Use
-, *, or + as markers:markdown- Item one
- Item two
- Nested item
- Item three
- Item one
- Item two
- Nested item
- Item three
Ordered lists
Use numbers followed by
. or ):markdown1. First item
2. Second item
3. Third item1) Also valid
2) With parentheses
1. First item
2. Second item
3. Third item
List continuation
Indent content to align with the list item text:
markdown1. First paragraph of item. Second paragraph of the same item.
2. Another item.
Thematic breaks
Three or more
-, *, or _ on a line (optionally with spaces):markdown---
*
___
---
HTML blocks
Raw HTML can be included directly:
markdown<div class="custom">
This is raw HTML.
</div>
Inline HTML
HTML tags can appear within inline content:
markdownThis is <em>inline HTML</em> in a paragraph.
Escaping
Use a backslash
\ to escape special characters:markdown\Not italic\
\# Not a heading
\[Not a link\]
The following characters can be escaped:
\ * _ { } [ ] ( ) # + - . ! | `Entities
HTML entities are supported:
markdown© & < >
© ©
Blank lines
Blank lines separate block elements. Multiple consecutive blank lines are treated the same as a single blank line.
Next steps
- Extensions — Enable tables, task lists, math, footnotes, and 20+ more features
- Getting started — Install and configure Markdig
---
Site/Docs/Getting Started
---
title: Getting started
---
Getting started
This guide walks you through installing Markdig, converting your first Markdown string to HTML, and configuring the pipeline for extended features.
Installation
Install the Markdig NuGet package:
shelldotnet add package Markdig
Or via the Package Manager Console:
powershellInstall-Package Markdig
A strong-named variant is also available:
shelldotnet add package Markdig.Signed
Requirements
Markdig targets
net462, netstandard2.0, netstandard2.1, net8.0, and net10.0. It works with .NET Framework 4.6.2+, .NET Core 2.0+, and .NET 5+.Your first conversion
The main entry point is the static
Markdown class in the Markdig namespace. The simplest operation converts a Markdown string to HTML:csharpusing Markdig;var html = Markdown.ToHtml("Hello Markdig!");
Console.WriteLine(html);
// Output: <p>Hello <strong>Markdig</strong>!</p>
By default, Markdig uses a plain CommonMark parser — no extensions are enabled.
Enabling extensions
Most projects benefit from Markdig's rich set of extensions. Use
MarkdownPipelineBuilder to configure a pipeline, then pass it to Markdown.ToHtml:csharpusing Markdig;// Build a pipeline with all advanced extensions
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var html = Markdown.ToHtml("This is ~~deleted~~ text.", pipeline);
Console.WriteLine(html);
// Output: <p>This is <del>deleted</del> text.</p>
UseAdvancedExtensions() activates most extensions at once (tables, task lists, math, footnotes, diagrams, and more). See the Extensions section for the full list and individual activation.You can also enable specific extensions individually:
csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePipeTables()
.UseFootnotes()
.UseEmphasisExtras()
.Build();
Parsing to an AST
If you need to inspect or manipulate the document structure, parse into an abstract syntax tree (AST):
csharpusing Markdig;
using Markdig.Syntax;var document = Markdown.Parse("# Hello\n\nA paragraph with bold text.");
// Iterate all descendants
foreach (var node in document.Descendants())
{
Console.WriteLine(node.GetType().Name);
}
// Find specific node types
foreach (var heading in document.Descendants<HeadingBlock>())
{
Console.WriteLine($"Heading level {heading.Level}");
}
The
MarkdownDocument returned by Markdown.Parse(...) is the root of a tree of Block and Inline nodes. See the AST guide for more details.Rendering a parsed document
After parsing, you can render the document to HTML separately:
csharpusing Markdig;
using Markdig.Syntax;var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
// Step 1: Parse
var document = Markdown.Parse("A ~~strikethrough~~ example.", pipeline);
// Step 2: Render
var html = document.ToHtml(pipeline);
Console.WriteLine(html);
Important: Always pass the same pipeline to both
Parse and ToHtml (or Convert). The pipeline configures both parser extensions (which produce custom AST nodes) and renderer extensions (which know how to render those nodes). Using mismatched pipelines results in missing or incorrect output. See Usage for a detailed explanation.Converting to plain text
Markdig can also convert Markdown to plain text (all HTML tags and formatting stripped):
csharpvar text = Markdown.ToPlainText("Hello world!");
Console.WriteLine(text);
// Output: Hello world!
Using a custom renderer
For output formats other than HTML (e.g. LaTeX, XAML), use
Markdown.Convert:csharpvar document = Markdown.Convert(markdownText, myCustomRenderer, pipeline);
See the Renderers guide for details on creating custom renderers.
Next steps
- Usage guide — Understand pipeline architecture, Parse+Render separation, and common patterns
- CommonMark syntax — Reference for all core Markdown syntax
- Extensions — Discover all 20+ built-in extensions
- Developer guide — Create your own parsers, renderers, and extensions
---
Site/Docs/Readme
---
title: "Markdig — User Guide"
---
Markdig — User Guide
Welcome to the Markdig documentation. Whether you are new to Markdig or an experienced user, this guide helps you make the most of the library.
Getting started
{.table}
| Guide | What you'll learn |
|---|---|
| Getting started | Install Markdig, parse your first Markdown, and render to HTML |
| Usage | Parse, render, pipeline architecture, and common patterns |
CommonMark syntax
{.table}
| Guide | What you'll learn |
|---|---|
| CommonMark syntax | Full reference for headings, paragraphs, emphasis, links, images, code, lists, blockquotes, and more |
Extensions
{.table}
| Guide | What it provides |
|---|---|
| Extensions overview | Index of all 20+ built-in extensions |
| Tables | Pipe tables and grid tables |
| Emphasis extras | Strikethrough, subscript, superscript, inserted, marked |
| Task lists | Checkbox task lists in GFM style |
| Mathematics | Inline and block LaTeX math |
| Diagrams | Mermaid, nomnoml, and other diagram languages |
| Alert blocks | GitHub-style alerts: NOTE, TIP, WARNING, etc. |
| Footnotes | Reference-style footnotes |
| Generic attributes | Attach CSS classes, IDs, and attributes to any element |
| Custom containers | Fenced
::: div containers |
| Abbreviations | Abbreviation definitions and auto-expansion |
| Definition lists | <dl> / <dt> / <dd> lists |
| Auto-identifiers | Automatic heading IDs |
| Auto-links | Automatic URL detection |
| Figures & footers | Figures, footers, and citations |
| Emoji & SmartyPants | Emoji shortcodes and smart typography |
| Media links | Embedded YouTube, Vimeo, and media players |
| List extras | Alpha and Roman numeral ordered lists |
| YAML front matter | YAML metadata blocks |
| Other extensions | Bootstrap, hardline breaks, JIRA links, globalization, and more |Developer guide
{.table}
| Guide | What you'll learn |
|---|---|
| Developer guide overview | How to extend Markdig with custom parsers and renderers |
| Abstract syntax tree | Structure and traversal of the AST |
| Pipeline architecture | How the parsing pipeline works |
| Creating extensions | Implement
IMarkdownExtension and register it |
| Parser authoring API | Advanced authoring contracts and parser/AST parity APIs |
| Block parsers | Write custom block-level parsers |
| Inline parsers | Write custom inline-level parsers |
| Renderers | Write custom renderers for HTML and other formats |
| Performance | Tips for high-performance Markdown processing |
| Migration notes | Compatibility notes for parser/AST authoring APIs |---
Site/Docs/Usage
---
title: Usage
---
Usage
This guide covers the core Markdig workflow: parsing Markdown, rendering output, and understanding how the pipeline ties everything together.
The Markdown static class
The
Markdown static class is the main entry point. It provides several methods:{.table}
| Method | Description |
|---|---|
|
Markdown.ToHtml(...) | Convert Markdown to HTML |
| Markdown.Parse(...) | Parse Markdown to an AST (MarkdownDocument) |
| Markdown.ToPlainText(...) | Convert Markdown to plain text |
| Markdown.Normalize(...) | Normalize Markdown to a canonical form |
| Markdown.Convert(...) | Convert using any custom IMarkdownRenderer |
| document.ToHtml(...) | Extension method — render a parsed document to HTML |All methods optionally accept a
MarkdownPipeline and a MarkdownParserContext.Parse + Render: the two-phase model
Markdig uses a two-phase model:
1. Parse — Convert Markdown text into an Abstract Syntax Tree (AST).
2. Render — Walk the AST and produce output (HTML, plain text, or any custom format).
csharpusing Markdig;var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
// Phase 1: Parse
var document = Markdown.Parse(markdownText, pipeline);
// Phase 2: Render
var html = document.ToHtml(pipeline);
The convenience method
Markdown.ToHtml(string, pipeline) does both phases in a single call, but understanding the separation is important for advanced use.Why the same pipeline must be passed to both Parse and Render
This is the most common mistake new users make. The pipeline serves two distinct purposes:
1. During parsing, extensions register custom
BlockParser and InlineParser objects that produce extension-specific AST nodes (e.g., MathInline, TaskList, Table).
2. During rendering, extensions register custom ObjectRenderers that know how to convert those AST nodes into output (e.g., HtmlMathInlineRenderer, HtmlTaskListRenderer).If you parse with one pipeline but render with another (or with no pipeline), the renderer won't know how to handle the extension-specific nodes — they'll be silently skipped or produce incorrect output.
csharp// ✅ Correct — same pipeline for parse and render
var pipeline = new MarkdownPipelineBuilder().UsePipeTables().Build();
var document = Markdown.Parse(markdownText, pipeline);
var html = document.ToHtml(pipeline);// ❌ Wrong — pipeline mismatch
var parsePipeline = new MarkdownPipelineBuilder().UsePipeTables().Build();
var renderPipeline = new MarkdownPipelineBuilder().Build(); // missing PipeTables
var document = Markdown.Parse(markdownText, parsePipeline);
var html = document.ToHtml(renderPipeline); // Tables won't render correctly!
// ❌ Also wrong — no pipeline for render
var document = Markdown.Parse(markdownText, pipeline);
var html = document.ToHtml(); // Uses default pipeline — no extensions!
Rule of thumb: Create the pipeline once, store it, and pass the same instance everywhere. Pipelines are thread-safe and immutable after building.
The MarkdownPipeline
The
MarkdownPipeline is an immutable, thread-safe object that holds:- The collection of block parsers (identify block-level elements like paragraphs, headings, lists)
- The collection of inline parsers (identify inline elements like emphasis, links, code spans)
- The list of registered extensions (which add/modify parsers and renderers)
- Configuration flags (trivia tracking, precise source location, etc.)
You create a pipeline using the
MarkdownPipelineBuilder:csharpvar pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions() // Enable extensions
.UsePreciseSourceLocation() // Track precise source spans
.Build(); // Produce the immutable pipeline
Once built, the pipeline can be reused across threads and calls.
The MarkdownPipelineBuilder
The builder provides a fluent API for configuration. All extension methods return the builder for chaining:
csharpvar pipeline = new MarkdownPipelineBuilder()
.UsePipeTables()
.UseFootnotes()
.UseEmphasisExtras()
.UseAutoLinks()
.UseGenericAttributes() // Must be last (modifies other parsers)
.Build();
#### Configuration options
{.table}
| Method | Description |
|---|---|
|
.UseAdvancedExtensions() | Enable most extensions at once |
| .UsePreciseSourceLocation() | Map AST nodes to their exact source location |
| .EnableTrackTrivia() | Track whitespace and trivia for roundtripping |
| .ConfigureNewLine(string) | Set the newline string for output |
| .DisableHeadings() | Disable ATX and Setex heading parsing |
| .DisableHtml() | Disable HTML block and inline HTML parsing |CAUTIONMarkdig is a Markdown processor, not an HTML sanitizer. Disabling HTML parsing reduces risk from raw HTML input, but it does not make rendering untrusted Markdown to HTML "safe" by itself. If you accept user-provided Markdown, sanitize the generated HTML and consider filtering/rewriting link and image URLs.
#### Extension ordering
Extensions are applied in the order they are added. Most extensions are order-independent, but a few need specific positioning:
-
UseGenericAttributes() should be last — it modifies other parsers to support {.class #id} syntax.
- Extensions that modify the same parser (e.g., adding emphasis characters) should be aware of potential conflicts.#### Dynamic configuration with strings
For scenarios where extensions are configured at runtime (e.g., from a config file), use the
Configure method:csharpvar pipeline = new MarkdownPipelineBuilder()
.Configure("common+pipetables+footnotes+figures")
.Build();
Available extension tokens:
common, advanced, alerts, pipetables, gfm-pipetables, emphasisextras, listextras, hardlinebreak, footnotes, footers, citations, attributes, gridtables, abbreviations, emojis, definitionlists, customcontainers, figures, mathematics, bootstrap, medialinks, smartypants, autoidentifiers, tasklists, diagrams, nofollowlinks, noopenerlinks, noreferrerlinks, nohtml, yaml, nonascii-noescape, autolinks, globalization, cjk-friendly-emphasis.The MarkdownParserContext
For advanced scenarios, a
MarkdownParserContext lets you pass per-call state to parsers:csharpvar context = new MarkdownParserContext();
// Extensions or custom parsers can read properties from the context
var document = Markdown.Parse(markdownText, pipeline, context);
The context is useful when custom parsers need external information (e.g., base URLs for link resolution).
Thread safety
The
MarkdownPipeline is thread-safe and should be shared. Do not create a new pipeline for every call — building a pipeline has a cost (extension setup, parser allocation).csharp// ✅ Good — build once, use everywhere
private static readonly MarkdownPipeline Pipeline =
new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();public string RenderMarkdown(string input)
=> Markdown.ToHtml(input, Pipeline);
The
Markdown.ToHtml(string, pipeline) and Markdown.Parse(string, pipeline) methods are thread-safe when given a shared pipeline.Outputting to a TextWriter
For streaming output (e.g., directly to an HTTP response), use the
TextWriter overloads:csharpusing var writer = new StreamWriter(responseStream);// Returns the parsed MarkdownDocument
var document = Markdown.ToHtml(markdownText, writer, pipeline);
This avoids building the complete HTML string in memory.
Rendering to other formats
Markdig's architecture separates parsing from rendering, so you can render the same AST to different formats:
csharp// Render to HTML
var html = document.ToHtml(pipeline);// Render to plain text
var plainText = Markdown.ToPlainText(markdownText, pipeline);
// Render to normalized Markdown
var normalized = Markdown.Normalize(markdownText, pipeline: pipeline);
// Render to a custom format
Markdown.Convert(markdownText, myCustomRenderer, pipeline);
See the Renderers guide for how to implement custom renderers.
Common patterns
Parse once, render multiple times
csharpvar document = Markdown.Parse(markdownText, pipeline);// Render to HTML
var html = document.ToHtml(pipeline);
// Analyze the AST
var headings = document.Descendants<HeadingBlock>().ToList();
var links = document.Descendants<LinkInline>().Where(l => !l.IsImage).ToList();
Extract metadata from the AST
csharpusing Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();
var document = Markdown.Parse(markdownText, pipeline);
// Get all headings
foreach (var heading in document.Descendants<HeadingBlock>())
{
// Extract the heading text
var text = heading.Inline?.FirstChild?.ToString();
Console.WriteLine($"H{heading.Level}: {text}");
}
// Get all links
foreach (var link in document.Descendants<LinkInline>())
{
Console.WriteLine($"Link: {link.Url} (image: {link.IsImage})");
}
Modify the AST before rendering
csharpvar document = Markdown.Parse(markdownText, pipeline);// Add a CSS class to all tables
foreach (var table in document.Descendants<Table>())
{
table.GetAttributes().AddClass("table table-striped");
}
// Render the modified document
var html = document.ToHtml(pipeline);
Next steps
- CommonMark syntax — Core Markdown syntax reference
- Extensions — All built-in extensions
- Developer guide — Create custom parsers and renderers
---
Contributing
How to Contribute
Thanks for your interest in contributing to
Markdig! Here are a few general guidelines on contributing and
reporting bugs that we ask you to review. Following these guidelines helps to communicate that you respect the time of
the contributors managing and developing this open source project.Reporting Issues
Before reporting a new issue, please ensure that the issue was not already reported or fixed by searching through our
issues list.
When creating a new issue, please be sure to include a title and clear description, as much relevant information as
possible, and, if possible, a test case.
Sending Pull Requests
Before sending a new pull request, take a look at existing pull requests and issues to see if the proposed change or fix
has been discussed in the past, or if the change was already implemented but not yet released.
We expect new pull requests to include tests for any affected behavior, and, as we follow semantic versioning, we may
reserve breaking changes until the next major version release.
Other Ways to Contribute
We welcome anyone that wants to contribute to
Markdig to triage and reply to open issues to help troubleshoot
and fix existing bugs. Here is what you can do:- Help ensure that existing issues follows the recommendations from the _Reporting Issues_ section,
providing feedback to the issue's author on what might be missing.
instructions and code samples.
- Review existing pull requests, and testing patches against real existing applications that use
Markdig.
- Write a test, or add a missing test case to an existing test.Thanks again for your interest on contributing to
Markdig!:heart:
---
Readme
Markdig [](https://github.com/xoofx/markdig/actions/workflows/ci.yml) [](https://coveralls.io/github/xoofx/markdig?branch=master) [](https://www.nuget.org/packages/Markdig/)
<img align="right" width="160px" height="160px" src="img/markdig.png">
Markdig is a fast, powerful, CommonMark compliant, extensible Markdown processor for .NET.
Documentation: https://xoofx.github.io/markdig
You can try Markdig online and compare it to other implementations on babelmark3
Features
- Very fast parser and html renderer (no-regexp), very lightweight in terms of GC pressure. See benchmarks
- Abstract Syntax Tree with precise source code location for syntax tree, useful when building a Markdown editor.
- Checkout Markdown Editor v2 for Visual Studio 2022 powered by Markdig!
- Converter to HTML
- Passing more than 600+ tests from the latest CommonMark specs (0.31.2)
- Includes all the core elements of CommonMark:
- including GFM fenced code blocks.
- Extensible architecture
- Even the core Markdown/CommonMark parsing is pluggable, so it allows to disable builtin Markdown/Commonmark parsing (e.g Disable HTML parsing) or change behaviour (e.g change matching
# of a headers with @)
- Roundtrip support: Parses trivia (whitespace, newlines and other characters) to support lossless parse ⭢ render roundtrip. This enables changing markdown documents without introducing undesired trivia changes.
- Built-in with 20+ extensions, including:
- Alerts for GitHub style alerts including: [!Note], [!Tip], [!Important], [!Warning], [!Caution]
- 2 kind of tables:
- Pipe tables (inspired from GitHub tables and PanDoc - Pipe Tables)
- Grid tables (inspired from Pandoc - Grid Tables)
- Extra emphasis (inspired from Pandoc - Emphasis and Markdown-it)
- strike through ~~,
- Subscript ~
- Superscript ^
- Inserted ++
- Marked ==
- Special attributes or attached HTML attributes (inspired from PHP Markdown Extra - Special Attributes)
- Definition lists (inspired from PHP Markdown Extra - Definitions Lists)
- Footnotes (inspired from PHP Markdown Extra - Footnotes)
- Auto-identifiers for headings (similar to Pandoc - Auto Identifiers)
- Auto-links generates links if a text starts with http:// or https:// or ftp:// or mailto: or www.xxx.yyy
- Task Lists inspired from Github Task lists.
- Extra bullet lists, supporting alpha bullet a. b. and roman bullet (i, ii...etc.)
- Media support for media url (youtube, vimeo, mp4...etc.) (inspired from this CommonMark discussion)
- Abbreviations (inspired from PHP Markdown Extra - Abbreviations)
- Citation text by enclosing ""..."" (inspired by this CommonMark discussion )
- Custom containers similar to fenced code block ::: for generating a proper <div>...</div> instead (inspired by this CommonMark discussion )
- Figures (inspired from this CommonMark discussion)
- Footers (inspired from this CommonMark discussion)
- Mathematics/Latex extension by enclosing $$ for block and $ for inline math (inspired from this CommonMark discussion)
- Soft lines as hard lines
- Emoji support (inspired from Markdown-it)
- SmartyPants (inspired from Daring Fireball - SmartyPants)
- Bootstrap class (to output bootstrap class)
- Diagrams extension whenever a fenced code block contains a special keyword, it will be converted to a div block with the content as-is (currently, supports mermaid and nomnoml diagrams)
- YAML Front Matter to parse without evaluating the front matter and to discard it from the HTML output (typically used for previewing without the front matter in MarkdownEditor)
- JIRA links to automatically generate links for JIRA project references (Thanks to @clarkd: https://github.com/clarkd/MarkdigJiraLinker)
- CJK-friendly Emphasis to mitigate a CommonMark specification issue in CJK languages (Thanks to @tats-u: https://github.com/tats-u/markdown-cjk-friendly)
- Starting with Markdig version 0.20.0+, Markdig is compatible only with NETStandard 2.0, NETStandard 2.1, NETCoreApp 2.1 and NETCoreApp 3.1.If you are looking for support for an old .NET Framework 3.5 or 4.0, you can download Markdig
0.18.3.
Third Party Extensions
markdig.wpf
- WPF/XAML Markdown Renderer: Neo.Markdig.Xaml
- Syntax highlighting: Markdig.SyntaxHighlighting
- Syntax highlighting using ColorCode-Universal: Markdown.ColorCode
- Syntax highlighting using Prism.js: WebStoating.Markdig.Prism
- Embedded C# scripting: Markdig.Extensions.ScriptCsDocumentation
Full documentation is available at https://xoofx.github.io/markdig — covering getting started, usage, all extensions, and the developer guide for writing custom parsers and renderers.
For detailed specs and corner cases of each extension, see the specs documentation.
For a "behind the scene" article about Markdig, see the blog post "Implementing a Markdown Engine for .NET".
Download
Markdig is available as a NuGet package: [](https://www.nuget.org/packages/Markdig/)
Also Markdig.Signed NuGet package provides signed assemblies.
Usage
The main entry point for the API is the
Markdig.Markdown` class:By default, without any options, Markdig is using the plain CommonMark parser:
var result = Markdown.ToHtml("This is a text with some emphasis");
Console.WriteLine(result); // prints: <p>This is a text with some <em>emphasis</em></p>In order to activate most of all advanced extensions (except Emoji, SoftLine as HardLine, Bootstrap, YAML Front Matter, JiraLinks and SmartyPants)
// Configure the pipeline with all advanced extensions active
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
var result = Markdown.ToHtml("This is a text with some emphasis", pipeline);You can have a look at the MarkdownExtensions that describes all actionable extensions (by modifying the MarkdownPipeline)
Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. For detailed contributing guidelines, please see contributing.md.
Build
In order to build Markdig, you need to install .NET 10.0
License
This software is released under the BSD-Clause 2 license.
Sponsors
Supports this project with a monthly donation and help me continue improving it. \[Become a sponsor\]
<img src="https://github.com/lilith.png?size=200" width="64px;" style="border-radius: 50%" alt="lilith"/> Lilith River, author of Imageflow Server, an easy on-demand
image editing, optimization, and delivery server
Credits
Thanks to the fantastic work done by John Mac Farlane for the CommonMark specs and all the people involved in making Markdown a better standard!
This project would not have been possible without this huge foundation.
Thanks also to the project BenchmarkDotNet that makes benchmarking so easy to setup!
Some decoding part (e.g HTML EntityHelper.cs) have been re-used from CommonMark.NET
Thanks to the work done by @clarkd on the JIRA Link extension, now included with this project!
Author
Alexandre MUTEL aka xoofx
---