markdig

GitHub

A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET

RAW Doc

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.

text
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

csharp
var document = Markdown.Parse(markdownText);

foreach (var node in document.Descendants())
{
Console.WriteLine($"{node.GetType().Name} at {node.Line}:{node.Column}");
}

#### Filter by type

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
// 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:

csharp
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);
}

TIP

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:

csharp
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:

csharp
// 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).

IMPORTANT

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.

csharp
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:

csharp
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):

csharp
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

text
/ 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 BlockStateBreak for a single-line block, Continue for multi-line blocks.

Multi-line block parsers

For blocks that span multiple lines, use TryContinue:

csharp
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:

csharp
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. /
csharp
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):

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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)
- A custom AST node (
TaskList inline)
- A custom
HtmlObjectRenderer<TaskList> (HtmlTaskListRenderer)

Level 3: Add a new block parser + renderer

When you need to parse block-level constructs.

Example: CustomContainerExtension — Adds ::: fenced containers:

csharp
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:

csharp
var pipeline = new MarkdownPipelineBuilder()
.Use<MyExtension>()
.Build();

Option B: Instance method

For extensions that need configuration:

csharp
var ext = new MyExtension(someConfig);
var pipeline = new MarkdownPipelineBuilder()
.Use(ext)
.Build();

Create an extension method on MarkdownPipelineBuilder for a clean API:

csharp
public static class MyExtensionMethods
{
public static MarkdownPipelineBuilder UseMyExtension(
this MarkdownPipelineBuilder pipeline,
MyExtensionOptions? options = null)
{
pipeline.Extensions.ReplaceOrAdd<MyExtension>(
new MyExtension(options));
return pipeline;
}
}

Usage:

csharp
var pipeline = new MarkdownPipelineBuilder()
.UseMyExtension(new MyExtensionOptions { / ... / })
.Build();

Here's a complete, silly extension that turns %%%text%%% into <blink>text</blink>:

csharp
using Markdig;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Renderers.Html.Inlines;

/// <summary>
/// Extension that converts %%%text%%% to &lt;blink&gt; 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:

csharp
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.
3. Call
Match on matching parsers (in priority order) until one returns true.
4. If a parser matches, add the created inline to the current container.
5. If no parser matches, the
LiteralInlineParser consumes the character into a LiteralInline.
6. After all text is consumed, run post-processing (e.g., emphasis restructuring).

The InlineParser base class

csharp
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):

csharp
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

text
/ 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.
- If you need custom pairing rules (like links/images, tables, or non-trivial delimiter constraints), follow the built-in patterns:
-
EmphasisInlineParser + EmphasisDelimiterInline
-
LinkInlineParser + LinkDelimiterInline
-
PipeTableDelimiterInline (tables)

Post-processing with IPostInlineProcessor

For complex inlines that need restructuring after all inline parsing is complete, implement IPostInlineProcessor:

csharp
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:

csharp
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).
2. Inline parsing (
InlineProcessor + 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).
- Replace the container in the AST first, then call
ReplaceParentContainer(old, @new) 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.
- Returns
null 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.
- Use
UseTrivia(end) 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.
- On failure: return
false 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.
- For explicit emission, use
processor.Emit(inline).

Parser state

Use processor.GetParserState<TState>(this) or processor.GetParserState(this, factory) for per-leaf parser state.
Parser states are cleared at the beginning of each
ProcessInlineLeaf.

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 |
| Add inline syntax |
InlineParser in MarkdownPipelineBuilder.InlineParsers |
| Deferred inline resolution |
IPostInlineProcessor |
| Replace current leaf block |
InlineProcessor.BlockNew |
| Replace parent container from inline |
InlineProcessor.ReplaceParentContainer |
| Literal post-processing |
LiteralInlineParser.PostMatch |
| Post-close block transform |
BlockParser.Closed |
| Per-block inline begin/end behavior |
Block.ProcessInlinesBegin / Block.ProcessInlinesEnd |
| Post-document transform |
MarkdownPipelineBuilder.DocumentProcessed |

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:

csharp
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.
- Typed metadata helpers (
DataKey<T>, 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.
2. Use
TakeLinesBefore() instead of internal trivia helpers.
3. For inline-driven parent replacement:
- replace the parent container in the AST first,
- then call
ReplaceParentContainer(old, @new).
4. Replace manual child-move loops (
RemoveAt(0) 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:

csharp
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&lt;char&gt;

For hot paths, prefer ReadOnlySpan<char> over string:

csharp
// Good — no allocation
ReadOnlySpan<char> span = slice.AsSpan();
if (span.StartsWith("
".AsSpan(), StringComparison.Ordinal))
{
// ...
}

// Avoid — allocates a string
string text = slice.ToString();
if (text.StartsWith("
`"))
{
// ...
}

text

stackalloc

For small temporary buffers, use stackalloc:

csharp
Span<char> buffer = stackalloc char[64];
int written = FormatOutput(buffer);
renderer.Write(buffer[..written]);
text

ArrayPool

For larger buffers, use ArrayPool<T>:

csharp
using System.Buffers;

char[] buffer = ArrayPool<char>.Shared.Rent(1024);
try
{
// Use buffer
}
finally
{
ArrayPool<char>.Shared.Return(buffer);
}

text

Pipeline and renderer reuse

Build once, use many times

The MarkdownPipeline is immutable and thread-safe after Build(). Always cache it:

csharp
// 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);

text

Renderer pooling

Markdown.ToHtml and friends internally pool HtmlRenderer instances. If you create renderers manually, consider pooling them:

csharp
// 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);

text

Extension authoring tips

Use sealed classes

Mark classes as sealed when they're not designed for inheritance. This allows the JIT to devirtualize method calls:

csharp
// Good — allows devirtualization
public sealed class NoteBlock : LeafBlock
{
public NoteBlock(BlockParser parser) : base(parser) { }
public string? NoteType { get; set; }
}
text

Prefer struct over class for small types

For data-only types that are short-lived, prefer struct:

csharp
// Used only during parsing, never stored long-term
public readonly struct ParseResult
{
public readonly bool Success;
public readonly int EndPosition;
}
text

Avoid LINQ in hot paths

LINQ allocates enumerator objects. In parser code, prefer for/foreach loops:

csharp
// 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;
}
}

text

Minimize string concatenation

Use StringBuilder or the renderer's built-in Write chaining:

csharp
// Good — chained writes, no intermediate strings
renderer.Write("<div class=\"")
.Write(cssClass)
.Write("\">");

// Avoid — allocates intermediate strings
renderer.Write($"<div class=\"{cssClass}\">");

text

Cache frequently used strings

For attribute names and CSS classes that repeat:

csharp
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" }
};
}
text

AOT and trimming compatibility

Markdig is designed to be compatible with Native AOT and IL trimming.

Avoid reflection

Do not use Type.GetMethod(), Activator.CreateInstance(), or similar reflection APIs in parsers and renderers:

csharp
// Bad — breaks trimming
var parser = (BlockParser)Activator.CreateInstance(parserType)!;

// Good — direct construction
var parser = new NoteBlockParser();

text

Use source generators when applicable

For serialization scenarios, prefer source generators over reflection-based serialization:

csharp
// Good — trimmer-friendly
[JsonSerializable(typeof(MarkdownMetadata))]
internal partial class MetadataJsonContext : JsonSerializerContext { }
text

Annotate when reflection is unavoidable

If you must use reflection, annotate with [DynamicallyAccessedMembers]:

csharp
public void RegisterParser(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
Type parserType)
{
// ...
}
text

Benchmarking

Markdig includes a benchmarks project for measuring performance:

bash
cd src
dotnet run -c Release --project Markdig.Benchmarks
text
The benchmarks compare Markdig against other .NET Markdown processors using BenchmarkDotNet.

To benchmark your extension, add a test case to the benchmarks project:

csharp
[Benchmark]
public string ConvertWithMyExtension()
{
return Markdown.ToHtml(MarkdownText, _pipelineWithMyExtension);
}
text

Summary of recommendations

{.table}
| Area | Recommendation |
|---|---|
| String handling | Use
StringSlice and ReadOnlySpan<char>; avoid Substring |
| Buffers |
stackalloc for small; ArrayPool<T> for large |
| Pipeline | Build once, pass everywhere, reuse across threads |
| Classes |
sealed by default; struct for small data types |
| Loops |
for/foreach over LINQ in parsers |
| Output | Chain
Write calls; avoid string interpolation in renderers |
| 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 MarkdownPipeline works, how extensions configure it, and how the parsing flow proceeds from source text to AST to rendered output.

Overview

Markdig's processing involves three objects:

1. MarkdownPipelineBuilder — A mutable builder where you configure extensions, parsers, and options.
2.
MarkdownPipeline — An immutable, thread-safe object produced by the builder. Contains the final parser and extension lists.
3.
Markdown — The static class that uses a pipeline to parse and render.

csharp
// 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);

text
NOTE

MarkdownPipelineBuilder is mutable and not thread-safe. Build pipelines during configuration/startup, then share the built MarkdownPipeline instances.

What the pipeline holds

The MarkdownPipeline contains:

{.table}
| Component | Type | Description |
|---|---|---|
| Block parsers |
BlockParserList | Ordered list of BlockParser objects |
| Inline parsers |
InlineParserList | Ordered list of InlineParser objects |
| Extensions |
OrderedList<IMarkdownExtension> | Registered extensions |
| TrackTrivia |
bool | Whether trivia parsing is enabled |
| MaximumNestingDepth |
int | Maximum AST nesting depth allowed while parsing and rendering |
| DocumentProcessed |
ProcessDocumentDelegate? | Callback after parsing completes |

Nesting 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 MaximumNestingDepth before calling Build():

csharp
var pipeline = new MarkdownPipelineBuilder
{
MaximumNestingDepth = 512,
}
.UseListExtras()
.Build();
text
Only raise this for trusted content: a larger limit allows deeper documents but can increase parsing cost and rendering stack usage.

How Build() works

When you call builder.Build():

1. Each registered extension's Setup(MarkdownPipelineBuilder) method is called in order.
2. Extensions may add/remove/modify block parsers, inline parsers, or other settings.
3. The builder's mutable state is frozen into an immutable
MarkdownPipeline.
4. The builder caches the result — calling
Build() again returns the same instance.

This is why extension ordering matters: an extension may look for parsers added by a previous extension.

How Parse() works

Markdown.Parse(string, pipeline) executes these steps:

Step 1: Block parsing

A BlockProcessor is created with the pipeline's block parsers. It processes the source text line by line:

1. For each line, the processor checks all open blocks to see if they continue (TryContinue).
2. Blocks that don't continue are closed.
3. The processor tries each
BlockParser to see if a new block opens at the current position (TryOpen).
4. Characters are dispatched based on
OpeningCharacters — parsers are only tried when their opening character matches.

The result is a tree of Block nodes rooted at the MarkdownDocument.

Step 2: Trivia expansion (optional)

If TrackTrivia is enabled, blocks are expanded to absorb neighboring whitespace and trivia. This supports lossless roundtripping.

Step 3: Inline parsing

An InlineProcessor visits each LeafBlock and runs the pipeline's inline parsers over the block's text content:

1. Starting from the first character, the processor finds InlineParser objects whose OpeningCharacters match.
2. The
Match method is called on each candidate parser (in order) until one returns true.
3. The matched inline is added to the block's
Inline container.
4. If no parser matches, a
LiteralInlineParser consumes the character.
5. After all characters are consumed, post-processing runs (e.g., emphasis restructuring).

Step 4: Post-processing

The DocumentProcessed delegate (if set) is invoked on the completed document.

Step 5: Return

The MarkdownDocument is returned.

How rendering works

Rendering is a separate phase. When you call document.ToHtml(pipeline):

1. An HtmlRenderer is created (or borrowed from a pool).
2.
pipeline.Setup(renderer) is called — this invokes Setup(MarkdownPipeline, IMarkdownRenderer) on every registered extension, giving each one the chance to register its ObjectRenderer.
3. The renderer walks the AST, dispatching each node to the appropriate
ObjectRenderer by type.
4. The rendered output is returned.

IMPORTANT

This is why the same pipeline must be used for both parsing and rendering. The parse-phase Setup registers parsers that produce custom AST node types. The render-phase Setup registers renderers that know how to output those types. Mismatched pipelines = missing renderers.

Extension setup: two phases

Every IMarkdownExtension has two Setup methods:

csharp
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);
}

text

Phase 1 example

csharp
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));
}
}

text

Phase 2 example

csharp
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is HtmlRenderer htmlRenderer)
{
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlMyBlockRenderer>();
}
}
text

The OrderedList&lt;T&gt; collection

Both parser and extension lists are OrderedList<T>, a custom Markdig collection with methods for type-safe insertion:

{.table}
| Method | Description |
|---|---|
|
AddIfNotAlready<T>() | Add if no instance of T exists |
|
InsertBefore<TBefore>(item) | Insert before a specific type |
|
InsertAfter<TAfter>(item) | Insert after a specific type |
|
Find<T>() | Find the first instance of type T |
|
FindExact<T>() | Find an exact type match (not subclasses) |
|
TryFind<T>(out T?) | Try to find, returning success |
|
Replace<T>(newItem) | Replace an existing item of type T |
|
ReplaceOrAdd<T>(newItem) | Replace or add if not found |
|
TryRemove<T>() | Remove the first instance of type T |
|
Contains<T>() | Check if an instance of type T exists |

Default parsers

Without any extensions, the pipeline contains these parsers:

Default block parsers

1. ThematicBreakParser
2.
HeadingBlockParser
3.
QuoteBlockParser
4.
ListBlockParser
5.
HtmlBlockParser
6.
FencedCodeBlockParser
7.
IndentedCodeBlockParser
8.
ParagraphBlockParser

Default inline parsers

1. HtmlEntityParser
2.
LinkInlineParser
3.
EscapeInlineParser
4.
EmphasisInlineParser
5.
CodeInlineParser
6.
AutolinkInlineParser
7.
LineBreakInlineParser

Extensions add to or modify these lists.

Trivia and roundtripping

Enable trivia tracking for lossless parse→render roundtrips:

csharp
var pipeline = new MarkdownPipelineBuilder()
.EnableTrackTrivia()
.Build();

var document = Markdown.Parse(markdownText, pipeline);
var normalized = Markdown.Normalize(markdownText, pipeline: pipeline);

text
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.)
text
1. Block parsing — The BlockProcessor walks through the source text line by line, using registered BlockParser objects to identify block-level elements (paragraphs, headings, lists, code blocks, etc.) and build the AST skeleton.

2. Inline parsing — The InlineProcessor visits every LeafBlock in the AST and runs registered InlineParser objects over the block's text to identify inline elements (emphasis, links, code spans, etc.).

3. Rendering — A renderer (typically HtmlRenderer) walks the complete AST and dispatches each node to a matching ObjectRenderer for output.

Extensions 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
Descendants, source spans |
| Pipeline architecture | How
MarkdownPipeline, MarkdownPipelineBuilder, and extensions interact |
| Creating extensions | Implement
IMarkdownExtension — from simple to complex |
| Parser authoring API | Authoring contracts and advanced APIs for parser/AST parity |
| Block parsers | Write custom
BlockParser subclasses — TryOpen, TryContinue, BlockState |
| Inline parsers | Write custom
InlineParser subclasses — Match, StringSlice, post-processing |
| Renderers | Implement
HtmlObjectRenderer<T> or build a completely custom renderer |
| 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 |
|---|---|
|
Markdown | Static entry point — Parse, ToHtml, Convert |
|
MarkdownPipeline | Immutable, thread-safe configuration object |
|
MarkdownPipelineBuilder | Fluent builder for MarkdownPipeline |
|
IMarkdownExtension | Interface all extensions implement |
|
BlockParser | Abstract base for block-level parsers |
|
InlineParser | Abstract base for inline-level parsers |
|
MarkdownDocument | Root AST node (a ContainerBlock) |
|
Block | Base for all block AST nodes |
|
Inline | Base for all inline AST nodes |
|
MarkdownObject | Base for all AST nodes — provides Span, Line, Column, data storage |
|
HtmlRenderer | Built-in HTML output renderer |
|
HtmlObjectRenderer<T> | Base for per-type HTML rendering |
|
IMarkdownRenderer | Interface for custom renderers |

---

Site/Docs/Advanced/Renderers

---
title: Renderers
---

Renderers

Renderers walk the AST and produce output. Markdig ships with an HtmlRenderer (HTML output), a NormalizeRenderer (canonical Markdown output), and supports fully custom renderers for any output format.

How rendering works

When you call document.ToHtml(pipeline):

1. An HtmlRenderer is created (internally pooled for performance).
2.
pipeline.Setup(renderer) is called — each extension's Setup(MarkdownPipeline, IMarkdownRenderer) runs, registering per-type ObjectRenderers.
3. The renderer walks the AST depth-first, dispatching each node to the
ObjectRenderer registered for its runtime type.
4. Output is written to the underlying
TextWriter.

The IMarkdownRenderer interface

csharp
public interface IMarkdownRenderer
{
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteBefore;
event Action<IMarkdownRenderer, MarkdownObject> ObjectWriteAfter;
ObjectRendererCollection ObjectRenderers { get; }
object Render(MarkdownObject markdownObject);
}
text

HTML renderers

HtmlObjectRenderer&lt;T&gt;

The most common way to render custom AST nodes to HTML is to create a class inheriting from HtmlObjectRenderer<T>:

csharp
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>");
}
}

text
The generic parameter T automatically registers this renderer for all NoteBlock nodes.

HtmlRenderer write methods

The HtmlRenderer provides these commonly used methods:

{.table}
| Method | Description |
|---|---|
|
Write(string) | 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 |

Writing container blocks

For custom ContainerBlock nodes, render children with WriteChildren:

csharp
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>");
}
}

text

Writing inline renderers

For custom Inline nodes:

csharp
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>");
}
}
text

Rendering container inlines

For ContainerInline types, render children inline:

csharp
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>");
}
}

text

Registering renderers

Renderers are registered in the extension's Setup(MarkdownPipeline, IMarkdownRenderer) method:

csharp
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>();
}
}
text

Insertion ordering

Like parsers, renderers can be inserted at specific positions:

csharp
// Insert at the beginning (highest priority)
htmlRenderer.ObjectRenderers.Insert(0, new HtmlNoteBlockRenderer());

// Insert before a specific type
htmlRenderer.ObjectRenderers.InsertBefore<HtmlCodeBlockRenderer>(
new HtmlNoteBlockRenderer());

text

Modifying existing renderers

You can modify built-in renderers without replacing them:

csharp
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");
}
}
}
text

HtmlRenderer properties

The HtmlRenderer has several useful properties:

{.table}
| Property | Type | Default | Description |
|---|---|---|---|
|
EnableHtmlForInline | bool | true | Emit HTML tags for inlines |
|
EnableHtmlForBlock | bool | true | Emit HTML tags for blocks |
|
EnableHtmlEscape | bool | true | HTML-escape special characters |

When all three are false, the renderer produces plain text — this is how Markdown.ToPlainText works.

Events: Before and After

You can hook into the rendering pipeline with events:

csharp
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
};

text

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:

csharp
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("}");
}
}

text

Option 2: Implement IMarkdownRenderer directly

For fully custom output (JSON, binary, etc.):

csharp
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();
}
}

text

Using custom renderers

Pass your renderer to Markdown.Convert:

csharp
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);

text

Complete example: rendering NoteBlock

Putting it all together — the block parser, renderer, and extension:

csharp
// 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;
}
}

text
Usage:
csharp
var pipeline = new MarkdownPipelineBuilder()
.UseNotes()
.Build();

var html = Markdown.ToHtml("!!! warning \"Be careful\"\n", pipeline);

text

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 .UseAbbreviations() (included in UseAdvancedExtensions()).

Abbreviations define expansions for acronyms and short terms. When the abbreviation appears in the text, it is wrapped in an <abbr> tag with a title attribute. Inspired by PHP Markdown Extra.

Syntax

Define abbreviations anywhere in the document using *[ABBR]: Full text:

markdown
*[HTML]: Hyper Text Markup Language
*[CSS]: Cascading Style Sheets

This page uses HTML and CSS.

text
*[HTML]: Hyper Text Markup Language
*[CSS]: Cascading Style Sheets

This page uses HTML and CSS.

HTML output

html
<p>This page uses <abbr title="Hyper Text Markup Language">HTML</abbr>
and <abbr title="Cascading Style Sheets">CSS</abbr>.</p>
text

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 .UseAlertBlocks() (included in UseAdvancedExtensions()).

Alert blocks are GitHub-style callouts for highlighting important content in documentation. They are rendered as styled <div> elements.

Syntax

Alert blocks are blockquotes that begin with a special [!TYPE] marker:

markdown
NOTE

Useful information that users should know, even when skimming content.

TIP

Helpful advice for doing things better or more easily.

IMPORTANT

Key information users need to know to achieve their goal.

WARNING

Urgent info that needs immediate user attention to avoid problems.

CAUTION

Advises about risks or negative outcomes of certain actions.


text
NOTE

Useful information that users should know, even when skimming content.

TIP

Helpful advice for doing things better or more easily.

IMPORTANT

Key information users need to know to achieve their goal.

WARNING

Urgent info that needs immediate user attention to avoid problems.

CAUTION

Advises about risks or negative outcomes of certain actions.

Alert types

{.table}
| Type | Purpose |
|---|---|
|
[!NOTE] | Supplementary information |
|
[!TIP] | Helpful suggestions |
|
[!IMPORTANT] | Critical information |
|
[!WARNING] | Potential problems |
|
[!CAUTION] | Risk of negative outcomes |

HTML output

Each alert renders as:

html
<div class="markdown-alert markdown-alert-note">
<p class="markdown-alert-title">Note</p>
<p>Your alert content here.</p>
</div>
text
The markdown-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.:

markdown
TIP

You can use bold, italic, and code in alerts.


> - Even lists work

- Inside alert blocks

> `csharp

var x = 42; // And code blocks too!

`

text
TIP

You can use bold, italic, and code in alerts.


> - Even lists work

- Inside alert blocks

>
csharp
var x = 42; // And code blocks too!

`

Custom rendering

Pass a custom renderer delegate to UseAlertBlocks to override the kind rendering:

csharp
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:

markdown

Getting Started

Renders as:

html
<h2 id="getting-started">Getting Started</h2>

You can then link to it:

markdown
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:

markdown

Section


Section


Section

Produces: section, section-1, section-2.

Options

UseAutoIdentifiers accepts an AutoIdentifierOptions flags enum:

csharp
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:

markdown

My Heading {#custom-id}

The explicit #custom-id takes precedence over the auto-generated one.

---

---
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.)

markdown
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

html
<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:

csharp
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();

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

csharp
using Markdig;

var pipeline = new MarkdownPipelineBuilder()
.UseCjkFriendlyEmphasis()
.Build();

You can also enable it via string configuration:

csharp
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:

markdown
この文を強調できますか?残念ながらできません。
我可以强调这个
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:

markdown
::: 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

html
<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:

markdown
This has a :custom-span[styled word]{.highlight} in it.

Nesting

Containers can be nested using more colons:

markdown
:::: outer
::: inner
Nested content.
:::
::::

Attributes

Combine with Generic attributes for full control:

markdown
::: {.alert .alert-info #my-alert role="alert"}
This is an informational alert.
:::

This produces:

html
<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):

markdown
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:

markdown
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

markdown
Term A
Term B
: Shared definition for both terms.

Term A
Term B
: Shared definition for both terms.

HTML output

html
<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> block
-
nomnoml renders as a <div class="nomnoml">...</div> block

Supported languages

{.table}
| Language | Info string |
|---|---|
| Mermaid |
mermaid |
| nomnoml |
nomnoml |

Mermaid example

markdown
mermaid
graph LR
A[Parse] --> B[AST]
B --> C[Render]
C --> D[HTML]
text

This renders as:

mermaid
graph 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
nomnoml
[Markdown] -> [Parser]
[Parser] -> [AST]
[AST] -> [Renderer]
[Renderer] -> [HTML]
text

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:

csharp
var pipeline = new MarkdownPipelineBuilder()
.UseEmojiAndSmiley(enableSmileys: false)
.Build();

Custom emoji mappings

csharp
using 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

csharp
var pipeline = new MarkdownPipelineBuilder()
.UseSmartyPants()
.Build();

var html = Markdown.ToHtml("He said \"Hello\" -- she replied 'Hi'...", pipeline);

Options

csharp
using 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:

markdown
This is ~~deleted~~ text.

This is ~~deleted~~ text.

Subscript

Wrap text with ~ for subscript:

markdown
H~2~O is water.

H~2~O is water.

Superscript

Wrap text with ^ for superscript:

markdown
2^10^ is 1024.

2^10^ is 1024.

Inserted text

Wrap text with ++ for inserted/underlined text:

markdown
This text has been ++inserted++.

This text has been ++inserted++.

Marked/highlighted text

Wrap text with == for marked/highlighted text:

markdown
This 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:

csharp
using 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

markdown
As 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:

markdown
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.

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:

markdown
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

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):

markdown
This 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.

IMPORTANT

UseGenericAttributes() should be the last extension added to the pipeline, as it modifies other parsers to recognize attribute syntax.

Syntax

Place {...} after a Markdown element:

markdown

Heading {#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:

markdown
A paragraph. {#intro .highlight data-section="overview"}

Applying to blocks

Headings

markdown

My 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:

markdown
This is a styled paragraph. {.lead .text-center}

Code blocks

markdown
csharp {.highlight-lines}
var x = 42;
text

Blockquotes

markdown
A 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

markdown
Click me{.btn .btn-primary}

Images

markdown
{.rounded width="200"}

Emphasis

markdown
Important text{.text-danger}

HTML output

markdown
A 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 .:

markdown
a. First item
b. Second item
c. Third item

a. First item
b. Second item
c. Third item

Uppercase

markdown
A. 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 .:

markdown
i. 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

markdown
I. 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 $:

markdown
The 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>

---

---
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

csharp
using 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 |

csharp
var 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.

csharp
var pipeline = new MarkdownPipelineBuilder()
.UseSoftlineBreakAsHardlineBreak()
.Build();

Without this extension:

markdown
Line one
Line two

Renders as: <p>Line one Line two</p>

With this extension, it renders as: <p>Line one<br />Line two</p>

Enable with .UseJiraLinks(options).

Automatically converts JIRA-style project references (e.g., PROJECT-123) into clickable links.

csharp
using 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.

csharp
var pipeline = new MarkdownPipelineBuilder()
.UseGlobalization()
.Build();

Enable with .UseReferralLinks(rels).

Adds rel attributes to all rendered links:

csharp
var 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 |

WARNING

UseSelfPipeline cannot be combined with other extensions on the same builder — it replaces the pipeline entirely based on the document content.

csharp
var 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).

csharp
var 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.

CAUTION

Only use this extension if you specifically need IE/Edge compatibility with non-ASCII file paths. It changes standard URL encoding behavior.

csharp
var 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.

csharp
var 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:

csharp
var pipeline = new MarkdownPipelineBuilder()
.DisableHtml()
.Build();

IMPORTANT

Markdig 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:

csharp
var pipeline = new MarkdownPipelineBuilder()
.UseAdvancedExtensions()
.Build();

Or enable specific extensions individually:

csharp
var 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.

csharp
var 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:

markdown
Name | 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:

csharp
var 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.

csharp
var 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:

markdown
1. [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

csharp
using 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:

csharp
using 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:

markdown

Heading 1


Heading 2


Heading 3


#### Heading 4
##### Heading 5
###### Heading 6

Setext headings

Underline text with = (level 1) or - (level 2):

markdown
Heading 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.

markdown
This 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 \:

markdown
First line  
Second line (two trailing spaces above)

First line\
Second line (backslash above)

Emphasis and strong emphasis

markdown
italic 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

markdown
[Link text][ref]
[Another link][ref]

[ref]: https://example.com "Optional Title"

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

markdown
Use the Markdown.ToHtml() method.

Use the Markdown.ToHtml() method.

Fenced code blocks

Use triple backticks or triple tildes, optionally with a language identifier:

markdown
csharp
var html = Markdown.ToHtml("Hello world!");
text

Or with tildes:

markdown
~~~python
print("Hello, world!")
~~~

Indented code blocks

Indent every line by 4 spaces or 1 tab:

markdown
var x = 42;
Console.WriteLine(x);

Blockquotes

Prefix lines with >:

markdown
This 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 ):

markdown
1. First item
2. Second item
3. Third item

1) 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:

markdown
1. 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:

markdown
This 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
&copy; &amp; &lt; &gt; &nbsp;
&#169; &#x00A9;

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:

shell
dotnet add package Markdig

Or via the Package Manager Console:

powershell
Install-Package Markdig

A strong-named variant is also available:

shell
dotnet 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:

csharp
using 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:

csharp
using 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:

csharp
var 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):

csharp
using 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:

csharp
using 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):

csharp
var 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:

csharp
var 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).

csharp
using 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:

csharp
var 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:

csharp
var 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 |

CAUTION

Markdig 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:

csharp
var 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:

csharp
var 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:

csharp
using 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

csharp
var 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

csharp
using 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

csharp
var 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

- WPF/XAML Markdown Renderer: 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.ScriptCs

Documentation

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:

csharp
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)

csharp
// 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);

Try it online!

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

---