### Site/Docs/Advanced/Ast --- title: Abstract syntax tree --- # Abstract syntax tree The `Markdown.Parse(...)` method returns a `MarkdownDocument` — the root of the abstract syntax tree (AST). The AST is a tree of `Block` and `Inline` nodes that fully represents the semantic structure of the Markdown input. ## AST structure There are two general categories of node: - **Block nodes** — Represent block-level constructs: paragraphs, headings, lists, code blocks, blockquotes, etc. - **Inline nodes** — Represent inline constructs within a block: emphasis, links, code spans, images, line breaks, etc. Blocks may contain other blocks (container blocks) or inlines (leaf blocks). Inlines may contain other inlines (container inlines) but never blocks. ``` MarkdownDocument (ContainerBlock) ├── HeadingBlock (LeafBlock) │ └── LiteralInline ├── ParagraphBlock (LeafBlock) │ ├── LiteralInline │ ├── EmphasisInline (ContainerInline) │ │ └── LiteralInline │ └── LiteralInline ├── ListBlock (ContainerBlock) │ ├── ListItemBlock (ContainerBlock) │ │ └── ParagraphBlock │ └── ListItemBlock (ContainerBlock) │ └── ParagraphBlock └── FencedCodeBlock (LeafBlock) ``` ## Node hierarchy All AST nodes inherit from `MarkdownObject`, which provides: {.table} | Member | Type | Description | |---|---|---| | `Span` | `SourceSpan` | Start and end positions (inclusive) in the source text | | `Line` | `int` | Zero-based line number in the source | | `Column` | `int` | Zero-based column number | ### Block types {.table} | Base class | Description | Examples | |---|---|---| | `ContainerBlock` | Contains child blocks | `MarkdownDocument`, `ListBlock`, `ListItemBlock`, `QuoteBlock` | | `LeafBlock` | Contains inlines, no child blocks | `ParagraphBlock`, `HeadingBlock`, `CodeBlock`, `FencedCodeBlock` | A `LeafBlock` has an `Inline` property (`ContainerInline?`) that is the root of its inline content. ### Inline types {.table} | Base class | Description | Examples | |---|---|---| | `ContainerInline` | Contains child inlines | `EmphasisInline`, `LinkInline` | | `LeafInline` | No children | `LiteralInline`, `CodeInline`, `LineBreakInline` | Inlines are stored as a **doubly-linked list** — each inline has `PreviousSibling` and `NextSibling` properties, plus a `Parent` (`ContainerInline?`). ## Traversing the AST ### The Descendants API The `Descendants` extension methods provide the easiest way to traverse the tree. They yield nodes in **depth-first** order. #### All descendants ```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()) { Console.WriteLine($"H{heading.Level}: line {heading.Line}"); } // All links (not images) foreach (var link in document.Descendants().Where(l => !l.IsImage)) { Console.WriteLine($"Link: {link.Url}"); } // All images foreach (var image in document.Descendants().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() .SelectMany(item => item.Descendants()); // Find emphasis whose direct parent block is a list item var other = document.Descendants() .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().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 (`

`) | | `HeadingBlock` | A heading (`

`–`

`); 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 (`
`) | | `HtmlBlock` | A raw HTML block | ## Common inline types {.table} | Type | Description | |---|---| | `LiteralInline` | Plain text content | | `EmphasisInline` | Emphasis (`` or ``); has `DelimiterChar` and `DelimiterCount` | | `CodeInline` | Inline code span | | `LinkInline` | A link or image; has `Url`, `Title`, `IsImage` | | `AutolinkInline` | An autolink (``) | | `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` and the typed helper methods: ```csharp using Markdig.Syntax; public sealed class MyExtensionState { public int Value { get; set; } } static readonly DataKey StateKey = new(); // Store node.SetData(StateKey, new MyExtensionState { Value = 123 }); // Retrieve if (node.TryGetData(StateKey, out var state)) { Console.WriteLine(state.Value); } ``` > [!TIP] > Use the explicit generic calls (`SetData`, `TryGetData`, `GetData`) to avoid ambiguity with the untyped `IMarkdownObject` methods. ### HTML attributes The most common attached data is `HtmlAttributes`, used by the [Generic attributes](../extensions/generic-attributes.md) 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()) { 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, IBlockParser { // 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; /// /// A custom note block: !!! note "Title" /// public class NoteBlock : LeafBlock { public NoteBlock(BlockParser parser) : base(parser) { } /// /// The note title. /// public string? Title { get; set; } /// /// The note type (note, warning, etc.). /// public string? NoteType { get; set; } } ``` ### Step 2: Implement the block parser ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ### Step 3: Key points about TryOpen - **Check `processor.IsCodeIndent`** — If the line is indented 4+ spaces, it's a code block, not your custom syntax. - **Set the `Parser` property** — The `Block` constructor automatically sets it from the argument. - **Set `Span`, `Line`, and `Column`** — These enable precise source location tracking. - **Support trivia when enabled** — If `processor.TrackTrivia` is `true`, take pending leading blank/trivia lines via `processor.TakeLinesBefore()` and assign them to `block.LinesBefore` (and use `processor.UseTrivia(end)` when you need exact trivia slices around markers). - **Push to `processor.NewBlocks`** — This tells the processor a new block was found. - **Return the right `BlockState`** — `Break` for a single-line block, `Continue` for multi-line blocks. ## Multi-line block parsers For blocks that span multiple lines, use `TryContinue`: ```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` to get fencing logic for free: ```csharp using Markdig.Parsers; /// /// A custom "spoiler" block: |||spoiler ... ||| /// public class SpoilerBlock : FencedCodeBlock { public SpoilerBlock(BlockParser parser) : base(parser) { } } public class SpoilerParser : FencedBlockParserBase { 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](inline-parsers.md) — Write parsers for inline elements - [Renderers](renderers.md) — Create HTML renderers for your custom blocks - [Creating extensions](creating-extensions.md) — 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(); 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 for ""..."" var emphasisRenderer = renderer.ObjectRenderers.FindExact(); 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()) { pipeline.InlineParsers.InsertBefore( new TaskListInlineParser()); } } public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { if (renderer is HtmlRenderer htmlRenderer) { htmlRenderer.ObjectRenderers.AddIfNotAlready(); } } } ``` This extension needs: - A custom `InlineParser` subclass (`TaskListInlineParser`) - A custom AST node (`TaskList` inline) - A custom `HtmlObjectRenderer` (`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()) { pipeline.BlockParsers.Insert(0, new CustomContainerParser()); } // Also add inline container support (::text::) var emphasisParser = pipeline.InlineParsers.FindExact(); 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(); htmlRenderer.ObjectRenderers.AddIfNotAlready(); } } } ``` ### 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() .Build(); ``` ### Option B: Instance method For extensions that need configuration: ```csharp var ext = new MyExtension(someConfig); var pipeline = new MarkdownPipelineBuilder() .Use(ext) .Build(); ``` ### Option C: Custom fluent extension method (recommended) Create an extension method on `MarkdownPipelineBuilder` for a clean API: ```csharp public static class MyExtensionMethods { public static MarkdownPipelineBuilder UseMyExtension( this MarkdownPipelineBuilder pipeline, MyExtensionOptions? options = null) { pipeline.Extensions.ReplaceOrAdd( new MyExtension(options)); return pipeline; } } ``` Usage: ```csharp var pipeline = new MarkdownPipelineBuilder() .UseMyExtension(new MyExtensionOptions { /* ... */ }) .Build(); ``` ## Complete example: a blink extension Here's a complete, silly extension that turns `%%%text%%%` into `text`: ```csharp using Markdig; using Markdig.Parsers.Inlines; using Markdig.Renderers; using Markdig.Renderers.Html.Inlines; /// /// Extension that converts %%%text%%% to <blink> tags. /// public sealed class BlinkExtension : IMarkdownExtension { public void Setup(MarkdownPipelineBuilder pipeline) { var parser = pipeline.InlineParsers.FindExact(); 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(); 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(); return pipeline; } } ``` Usage: ```csharp var pipeline = new MarkdownPipelineBuilder() .UseBlink() .Build(); var html = Markdown.ToHtml("This is %%%blinking%%% text.", pipeline); // =>

This is blinking text.

``` ## Next steps - [Block parsers](block-parsers.md) — How to write custom block parsers from scratch - [Inline parsers](inline-parsers.md) — How to write custom inline parsers - [Renderers](renderers.md) — 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, IInlineParser { // 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; /// /// An inline representing a keyboard shortcut: [[Ctrl+S]] /// public class KeyboardInline : LeafInline { /// /// The keyboard shortcut text. /// public string? Shortcut { get; set; } } ``` ### Step 2: Implement the inline parser ``` /* Detailed source-code truncated for AI context efficiency. */ ``` ### Step 3: Key points about Match - **Return `false` if no match** — Don't advance the slice if you don't match. - **Set `processor.Inline`** — This is how you return the matched inline to the processor. - **Advance `slice.Start`** — Move past the consumed characters. Other parsers will see text starting from the new position. - **Set source position** — Use `processor.GetSourcePosition` for accurate `Span`, `Line`, and `Column`. ## Working with StringSlice The `slice` parameter is a mutable view into the `LeafBlock`'s text. Key operations: {.table} | Member | Description | |---|---| | `slice.CurrentChar` | Character at current position | | `slice.PeekChar(offset)` | Look ahead without advancing | | `slice.NextChar()` | Advance and return the next character | | `slice.SkipChar()` | Skip one character | | `slice.Start` | Start index (mutable — advance this to consume) | | `slice.End` | End index | | `slice.Text` | The underlying string | | `slice.IsEmpty` | True if `Start > End` | ## The InlineProcessor Inside `Match`, the `processor` provides context: {.table} | Member | Description | |---|---| | `processor.Inline` | Set this to your created inline on match | | `processor.Block` | The `LeafBlock` currently being processed | | `processor.Root` | The root `ContainerInline` of the current block | | `processor.Document` | The root `MarkdownDocument` | | `processor.Context` | The per-call `MarkdownParserContext` (may be `null`) | | `processor.GetSourcePosition(pos, out line, out column)` | Map a slice position to source position | | `processor.GetParserState(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()` | 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 _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](block-parsers.md) — Write block-level parsers - [Renderers](renderers.md) — Create renderers for your custom inlines - [Creating extensions](creating-extensions.md) — 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](creating-extensions.md) 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(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(value)` / `GetData()`. - `TryGetData(key, out value)` / `GetData(key)` for explicit object keys. - `DataKey` for collision-resistant typed keys. Example: ```csharp var key = new DataKey(); block.SetData(key, state); if (block.TryGetData(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`, `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(value)` - `node.SetData(key, value)` - `node.GetData(key)` - `node.TryGetData(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<char> For hot paths, prefer `ReadOnlySpan` over `string`: ```csharp // Good — no allocation ReadOnlySpan span = slice.AsSpan(); if (span.StartsWith("```".AsSpan(), StringComparison.Ordinal)) { // ... } // Avoid — allocates a string string text = slice.ToString(); if (text.StartsWith("```")) { // ... } ``` ### stackalloc For small temporary buffers, use `stackalloc`: ```csharp Span buffer = stackalloc char[64]; int written = FormatOutput(buffer); renderer.Write(buffer[..written]); ``` ### ArrayPool For larger buffers, use `ArrayPool`: ```csharp using System.Buffers; char[] buffer = ArrayPool.Shared.Rent(1024); try { // Use buffer } finally { ArrayPool.Shared.Return(buffer); } ``` ## 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); ``` ### 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); ``` ## 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; } } ``` ### 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; } ``` ### 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; } } ``` ### Minimize string concatenation Use `StringBuilder` or the renderer's built-in `Write` chaining: ```csharp // Good — chained writes, no intermediate strings renderer.Write("
"); // Avoid — allocates intermediate strings renderer.Write($"
"); ``` ### Cache frequently used strings For attribute names and CSS classes that repeat: ```csharp public sealed class HtmlAlertRenderer : HtmlObjectRenderer { // Cache the string to avoid repeated allocations private static readonly HtmlAttributes WarningAttributes = new() { Classes = new List { "alert", "alert-warning" } }; } ``` ## 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(); ``` ### 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 { } ``` ### Annotate when reflection is unavoidable If you must use reflection, annotate with `[DynamicallyAccessedMembers]`: ```csharp public void RegisterParser( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type parserType) { // ... } ``` ## Benchmarking Markdig includes a benchmarks project for measuring performance: ```bash cd src dotnet run -c Release --project Markdig.Benchmarks ``` The benchmarks compare Markdig against other .NET Markdown processors using [BenchmarkDotNet](https://benchmarkdotnet.org/). To benchmark your extension, add a test case to the benchmarks project: ```csharp [Benchmark] public string ConvertWithMyExtension() { return Markdown.ToHtml(MarkdownText, _pipelineWithMyExtension); } ``` ## Summary of recommendations {.table} | Area | Recommendation | |---|---| | String handling | Use `StringSlice` and `ReadOnlySpan`; avoid `Substring` | | Buffers | `stackalloc` for small; `ArrayPool` 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); ``` > [!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` | 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(); ``` 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); } ``` ### Phase 1 example ```csharp public void Setup(MarkdownPipelineBuilder pipeline) { // Add a new block parser pipeline.BlockParsers.AddIfNotAlready(); // Or modify an existing parser var emphasisParser = pipeline.InlineParsers.FindExact(); if (emphasisParser != null) { emphasisParser.EmphasisDescriptors.Add( new EmphasisDescriptor('%', 3, 3, false)); } } ``` ### Phase 2 example ```csharp public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { if (renderer is HtmlRenderer htmlRenderer) { htmlRenderer.ObjectRenderers.AddIfNotAlready(); } } ``` ## The OrderedList<T> collection Both parser and extension lists are `OrderedList`, a custom Markdig collection with methods for type-safe insertion: {.table} | Method | Description | |---|---| | `AddIfNotAlready()` | Add if no instance of `T` exists | | `InsertBefore(item)` | Insert before a specific type | | `InsertAfter(item)` | Insert after a specific type | | `Find()` | Find the first instance of type `T` | | `FindExact()` | Find an exact type match (not subclasses) | | `TryFind(out T?)` | Try to find, returning success | | `Replace(newItem)` | Replace an existing item of type `T` | | `ReplaceOrAdd(newItem)` | Replace or add if not found | | `TryRemove()` | Remove the first instance of type `T` | | `Contains()` | 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); ``` When trivia is tracked, whitespace, extra heading characters, and other non-semantic elements are stored on the AST nodes, allowing the document to be re-rendered as close to the original as possible. --- ### Site/Docs/Advanced/Readme --- title: Developer guide --- # Developer guide Markdig is designed to be deeply extensible. This guide covers how the parsing pipeline works, how to traverse and manipulate the AST, and how to create your own extensions with custom block parsers, inline parsers, and renderers. ## Architecture overview Markdig's processing flow has three stages: ``` Markdown text → [Block Parsing] → [Inline Parsing] → AST (MarkdownDocument) ↓ [Rendering] → Output (HTML, etc.) ``` 1. **Block parsing** — 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](ast.md) | Structure of block/inline nodes, traversal with `Descendants`, source spans | | [Pipeline architecture](pipeline.md) | How `MarkdownPipeline`, `MarkdownPipelineBuilder`, and extensions interact | | [Creating extensions](creating-extensions.md) | Implement `IMarkdownExtension` — from simple to complex | | [Parser authoring API](parser-authoring-api.md) | Authoring contracts and advanced APIs for parser/AST parity | | [Block parsers](block-parsers.md) | Write custom `BlockParser` subclasses — `TryOpen`, `TryContinue`, `BlockState` | | [Inline parsers](inline-parsers.md) | Write custom `InlineParser` subclasses — `Match`, `StringSlice`, post-processing | | [Renderers](renderers.md) | Implement `HtmlObjectRenderer` or build a completely custom renderer | | [Performance](performance.md) | Tips for maintaining high throughput — allocation-free patterns, pooling, Span-based parsing | | [Migration notes](parser-authoring-api-migration.md) | 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` | 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 ObjectWriteBefore; event Action ObjectWriteAfter; ObjectRendererCollection ObjectRenderers { get; } object Render(MarkdownObject markdownObject); } ``` ## HTML renderers ### HtmlObjectRenderer<T> The most common way to render custom AST nodes to HTML is to create a class inheriting from `HtmlObjectRenderer`: ```csharp using Markdig.Renderers; using Markdig.Renderers.Html; public class HtmlNoteBlockRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, NoteBlock obj) { // Open the div with attributes from the AST node renderer.Write("
"); // Write the title if (!string.IsNullOrEmpty(obj.Title)) { renderer.Write("

") .WriteEscape(obj.Title) .WriteLine("

"); } // Write inline content (for LeafBlocks) renderer.WriteLeafInline(obj); renderer.WriteLine("
"); } } ``` 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 { protected override void Write(HtmlRenderer renderer, MyContainerBlock obj) { renderer.Write("
"); // Render all child blocks renderer.WriteChildren(obj); renderer.WriteLine("
"); } } ``` ### Writing inline renderers For custom `Inline` nodes: ```csharp public class HtmlKeyboardRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, KeyboardInline obj) { renderer.Write(""); renderer.WriteEscape(obj.Shortcut ?? ""); renderer.Write(""); } } ``` ### Rendering container inlines For `ContainerInline` types, render children inline: ```csharp public class HtmlHighlightRenderer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, HighlightInline obj) { renderer.Write(""); // Render child inlines renderer.WriteChildren(obj); renderer.Write(""); } } ``` ## 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(); htmlRenderer.ObjectRenderers.AddIfNotAlready(); } } ``` ### 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( new HtmlNoteBlockRenderer()); ``` ## 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(); if (codeRenderer != null) { codeRenderer.BlocksAsDiv.Add("mermaid"); codeRenderer.BlocksAsDiv.Add("nomnoml"); } } } ``` ## 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 }; ``` ## 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 { 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 { protected override void Write(LatexRenderer renderer, HeadingBlock obj) { var command = obj.Level switch { 1 => "section", 2 => "subsection", 3 => "subsubsection", _ => "paragraph" }; renderer.Write($"\\{command}{{ "{{" }}"); renderer.WriteLeafInline(obj); renderer.WriteLine("}"); } } ``` ### Option 2: Implement IMarkdownRenderer directly For fully custom output (JSON, binary, etc.): ```csharp public class JsonRenderer : IMarkdownRenderer { public event Action? ObjectWriteBefore; public event Action? ObjectWriteAfter; public ObjectRendererCollection ObjectRenderers { get; } = new(); public object Render(MarkdownObject markdownObject) { // Walk the AST and produce JSON // ... return jsonBuilder.ToString(); } } ``` ### 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); ``` ## 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(); } public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { if (renderer is HtmlRenderer htmlRenderer) { htmlRenderer.ObjectRenderers.AddIfNotAlready(); } } } // Fluent API public static class NoteExtensionMethods { public static MarkdownPipelineBuilder UseNotes( this MarkdownPipelineBuilder pipeline) { pipeline.Extensions.AddIfNotAlready(); return pipeline; } } ``` Usage: ```csharp var pipeline = new MarkdownPipelineBuilder() .UseNotes() .Build(); var html = Markdown.ToHtml("!!! warning \"Be careful\"\n", pipeline); ``` ## Next steps - [Block parsers](block-parsers.md) — Write the parsers that produce custom AST nodes - [Inline parsers](inline-parsers.md) — Write inline-level parsers - [Performance](performance.md) — 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 `` tag with a `title` attribute. Inspired by [PHP Markdown Extra](https://michelf.ca/projects/php-markdown/extra/#abbr). ## 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. ``` *[HTML]: Hyper Text Markup Language *[CSS]: Cascading Style Sheets This page uses HTML and CSS. ## HTML output ```html

This page uses HTML and CSS.

``` ## 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 `
` 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. ``` > [!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

Note

Your alert content here.

``` 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! > ``` ``` > [!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($"{kind}"); }) .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](https://pandoc.org/MANUAL.html#extension-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

Getting Started

``` You can then link to it: ```markdown See [Getting Started](#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](generic-attributes.md): ```markdown ## My Heading {#custom-id} ``` The explicit `#custom-id` takes precedence over the auto-generated one. --- ### Site/Docs/Extensions/Auto Links --- title: Auto-links --- # Auto-links Enable with `.UseAutoLinks()` (included in `UseAdvancedExtensions()`). This extension automatically detects URLs and email addresses in plain text and converts them into clickable links — no angle brackets or explicit Markdown link syntax required. ## Detected patterns URLs starting with these protocols are detected automatically: - `http://` - `https://` - `ftp://` - `mailto:` - `www.` (rendered as `http://www.`) ```markdown Check out https://github.com/xoofx/markdig for more info. Visit www.example.com for details. Contact support@example.com for help. ``` Check out https://github.com/xoofx/markdig for more info. Visit www.example.com for details. ## HTML output ```html

Check out https://github.com/xoofx/markdig for more info.

``` ## 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(); ``` ## Difference from CommonMark autolinks CommonMark already supports **angle-bracket autolinks** (``). 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](https://github.com/tats-u/markdown-cjk-friendly/) and mitigates the CommonMark emphasis limitation discussed in [commonmark-spec#650](https://github.com/commonmark/commonmark-spec/issues/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 `
` elements from fenced `:::` blocks, similar to how fenced code blocks work. They are inspired by this [CommonMark discussion](https://talk.commonmark.org/t/custom-container-for-block-and-inline/2051). ## 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

This is a warning container...

``` The text after `:::` becomes the CSS class of the `
`. ## 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](generic-attributes.md) for full control: ```markdown ::: {.alert .alert-info #my-alert role="alert"} This is an informational alert. ::: ``` This produces: ```html ``` --- ### Site/Docs/Extensions/Definition Lists --- title: Definition lists --- # Definition lists Enable with `.UseDefinitionLists()` (included in `UseAdvancedExtensions()`). Definition lists render as `
` / `
` / `
` HTML elements. Inspired by [PHP Markdown Extra](https://michelf.ca/projects/php-markdown/extra/#def-list). ## 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
Term 1
Definition of term 1.
Term 2
Definition of term 2.
Another definition of term 2.
``` ## 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 `` element) so client-side diagram libraries can consume the raw text easily. By default: - `mermaid` renders as a `
...
` block - `nomnoml` renders as a `
...
` block ## Supported languages {.table} | Language | Info string | |---|---| | [Mermaid](https://mermaid.js.org/) | `mermaid` | | [nomnoml](https://github.com/skanaar/nomnoml) | `nomnoml` | ## Mermaid example ````markdown ```mermaid graph LR A[Parse] --> B[AST] B --> C[Render] C --> D[HTML] ``` ```` 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 ``` ## nomnoml example ````markdown ```nomnoml [Markdown] -> [Parser] [Parser] -> [AST] [AST] -> [Renderer] [Renderer] -> [HTML] ``` ```` ## HTML output Instead of the usual code block rendering: ```html
...
``` The diagrams extension produces a plain block for recognized languages, for example: ```html
...
...
``` 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 { { ":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](https://daringfireball.net/projects/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](cjk-friendly-emphasis.md). ## 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~~` | `text` | | `~text~` | `text` | | `^text^` | `text` | | `++text++` | `text` | | `==text==` | `text` | ## 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 `
` blocks with optional `
`: ```markdown ^^^ ^^^ A beautiful mountain landscape ``` ### HTML output ```html

A scenic mountain

A beautiful mountain landscape
``` ### 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 `