mirror of
https://github.com/xoofx/markdig.git
synced 2026-04-06 05:20:08 +00:00
Add comprehensive documentation site
Create a full Lunet-based documentation site in site/ with: - Site config, navigation, landing page with hero section - Core docs: getting started, usage guide, CommonMark reference - Extension docs: 19 pages covering all 30+ extensions - Developer guide: AST, pipeline, creating extensions, block/inline parsers, renderers, performance - API reference generation via lunet-io/templates - Remove obsolete parsing-*.md files (content revised into docs/) - Update AGENTS.md with website build instructions - Add site/AGENTS.md with site-specific guidance
This commit is contained in:
21
AGENTS.md
21
AGENTS.md
@@ -16,7 +16,8 @@ Paths/commands below are relative to this directory.
|
||||
- Tooling: `src/mdtoc/` (CLI to generate a Markdown TOC from a local file or GitHub URL)
|
||||
- Tooling: `src/SpecFileGen/` (regenerates `*.generated.cs` spec tests from `src/Markdig.Tests/**.md`)
|
||||
- Tooling: `src/UnicodeNormDApp/` (generates the Unicode→ASCII mapping used by `Markdig.Helpers.CharNormalizer`)
|
||||
- Docs to keep in sync with behavior: `readme.md` and the docs under `doc/` (e.g., `doc/**/*.md`)
|
||||
- Docs to keep in sync with behavior: `readme.md` and the docs under `site/docs/` (e.g., `site/docs/**/*.md`)
|
||||
- Website: `site/` — Lunet-based documentation site (https://xoofx.github.io/markdig)
|
||||
|
||||
## Build & Test
|
||||
|
||||
@@ -29,6 +30,24 @@ dotnet test -c Release
|
||||
|
||||
All tests must pass and docs must be updated before submitting.
|
||||
|
||||
## Website (Lunet)
|
||||
|
||||
The project website lives in `site/` and is built with [Lunet](https://lunet.io), a static site generator.
|
||||
|
||||
```sh
|
||||
# Prerequisites: install lunet as a .NET global tool
|
||||
dotnet tool install -g lunet
|
||||
|
||||
# Build the site (from the project root)
|
||||
cd site
|
||||
lunet build # production build → .lunet/build/www/
|
||||
lunet serve # dev server with live reload at http://localhost:4000
|
||||
```
|
||||
|
||||
### Escaping `{{` `}}` in site Markdown
|
||||
|
||||
Because the site is processed by Scriban via Lunet, any literal `{{` or `}}` in Markdown documentation must be escaped as `{{ "{{" }}` and `{{ "}}" }}` respectively. Hand-written pages must be escaped manually.
|
||||
|
||||
## Contribution Rules (Do/Don't)
|
||||
|
||||
- Keep diffs focused; avoid drive-by refactors/formatting and unnecessary dependencies.
|
||||
|
||||
46
site/AGENTS.md
Normal file
46
site/AGENTS.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Markdig Documentation Site — Agent Instructions
|
||||
|
||||
This folder contains the [Lunet](https://lunet.io)-based documentation site for Markdig.
|
||||
|
||||
Single source of truth for the overall project: read and follow `../AGENTS.md`.
|
||||
|
||||
## Orientation
|
||||
|
||||
- `config.scriban` — Site configuration (theme, metadata, API generation)
|
||||
- `menu.yml` — Top-level navigation
|
||||
- `readme.md` — Landing/home page
|
||||
- `docs/` — All documentation content
|
||||
- `docs/getting-started.md` — Installation & first steps
|
||||
- `docs/usage.md` — Parse/render, pipeline architecture
|
||||
- `docs/commonmark.md` — CommonMark syntax reference
|
||||
- `docs/extensions/` — Extension reference (one page per extension or group)
|
||||
- `docs/advanced/` — Developer guide (AST, parsers, renderers, performance)
|
||||
- `img/` — Site images (logo, banner)
|
||||
- `specs/` — Versioned API specification documents (historical reference)
|
||||
|
||||
## Build & Serve
|
||||
|
||||
```sh
|
||||
# Prerequisites: install lunet as a .NET global tool
|
||||
dotnet tool install -g lunet
|
||||
|
||||
# From this directory (site/)
|
||||
lunet build # production build → .lunet/build/www/
|
||||
lunet serve # dev server with live reload at http://localhost:4000
|
||||
```
|
||||
|
||||
## Content Conventions
|
||||
|
||||
- Pages use Markdown with YAML front matter (`title` required).
|
||||
- Navigation is defined in `menu.yml` files (one per section/folder).
|
||||
- The site uses `UseAdvancedExtensions()` so all Markdig extensions render natively — extension docs can demonstrate syntax live.
|
||||
- Because the site is processed by Scriban via Lunet, any literal `{{` or `}}` in Markdown must be escaped as `{{ "{{" }}` and `{{ "}}" }}`.
|
||||
- Keep code examples short, correct, and copy-pasteable.
|
||||
- When adding a new page, add a corresponding entry in the relevant `menu.yml`.
|
||||
|
||||
## What to Update
|
||||
|
||||
When library behavior changes:
|
||||
- Update the relevant extension or feature page in `docs/`.
|
||||
- If a new extension is added, create a page under `docs/extensions/` and add it to `docs/extensions/menu.yml`.
|
||||
- If public API changes affect developer guide content, update pages in `docs/advanced/`.
|
||||
48
site/config.scriban
Normal file
48
site/config.scriban
Normal file
@@ -0,0 +1,48 @@
|
||||
# Markdig documentation site.
|
||||
# Build from this folder with:
|
||||
# lunet build
|
||||
# lunet serve
|
||||
#
|
||||
# This site uses the shared template published as a Lunet extension.
|
||||
extend "lunet-io/templates"
|
||||
|
||||
template_theme_default_mode = "dark"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SITE SECTION (project-specific values only)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Canonical base URL used for `lunet build` (links, og:meta, sitemap...).
|
||||
site_project_baseurl = "https://xoofx.github.io"
|
||||
site_project_basepath = "/markdig"
|
||||
site_project_name = "Markdig"
|
||||
site_project_description = "A fast, powerful, CommonMark compliant, extensible Markdown processor for .NET."
|
||||
site_project_logo_path = "/img/markdig.svg"
|
||||
site_project_social_banner_path = "/img/github-banner.png"
|
||||
site_project_package_id = "Markdig"
|
||||
site_project_github_user = "xoofx"
|
||||
site_project_github_repo = "markdig"
|
||||
site_project_owner_name = "Alexandre Mutel"
|
||||
site_project_owner_alias = "xoofx"
|
||||
site_project_owner_url = "https://xoofx.github.io"
|
||||
|
||||
site_project_init
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# API reference (.NET)
|
||||
# -----------------------------------------------------------------------------
|
||||
with api.dotnet
|
||||
title = "Markdig .NET API Reference"
|
||||
path = "/api"
|
||||
menu_name = "api"
|
||||
menu_title = "API Reference"
|
||||
properties = {
|
||||
TargetFramework: "net10.0"
|
||||
}
|
||||
projects = [
|
||||
{
|
||||
name: "Markdig",
|
||||
path: "../src/Markdig/Markdig.csproj"
|
||||
}
|
||||
]
|
||||
end
|
||||
240
site/docs/advanced/ast.md
Normal file
240
site/docs/advanced/ast.md
Normal file
@@ -0,0 +1,240 @@
|
||||
---
|
||||
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}
|
||||
| Property | Type | Description |
|
||||
|---|---|---|
|
||||
| `Span` | `SourceSpan` | Start and end positions 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");
|
||||
```
|
||||
|
||||
### 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");
|
||||
```
|
||||
|
||||
## 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.
|
||||
328
site/docs/advanced/block-parsers.md
Normal file
328
site/docs/advanced/block-parsers.md
Normal file
@@ -0,0 +1,328 @@
|
||||
---
|
||||
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
|
||||
|
||||
```csharp
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
|
||||
public class NoteBlockParser : BlockParser
|
||||
{
|
||||
public NoteBlockParser()
|
||||
{
|
||||
// This parser triggers on '!' characters
|
||||
OpeningCharacters = ['!'];
|
||||
}
|
||||
|
||||
public override BlockState TryOpen(BlockProcessor processor)
|
||||
{
|
||||
// Don't match if indented as code (4+ spaces)
|
||||
if (processor.IsCodeIndent)
|
||||
return BlockState.None;
|
||||
|
||||
var line = processor.Line;
|
||||
var startPosition = line.Start;
|
||||
|
||||
// Check for "!!!" prefix
|
||||
if (line.CurrentChar != '!' || line.PeekChar(1) != '!' || line.PeekChar(2) != '!')
|
||||
return BlockState.None;
|
||||
|
||||
// Advance past "!!!"
|
||||
line.Start += 3;
|
||||
line.TrimStart(); // Skip whitespace
|
||||
|
||||
// Read the note type (e.g., "note", "warning")
|
||||
var noteType = line.ToString().Trim();
|
||||
string? title = null;
|
||||
|
||||
// Check for quoted title: !!! note "My Title"
|
||||
var quoteIndex = noteType.IndexOf('"');
|
||||
if (quoteIndex >= 0)
|
||||
{
|
||||
title = noteType[(quoteIndex + 1)..].TrimEnd('"').Trim();
|
||||
noteType = noteType[..quoteIndex].Trim();
|
||||
}
|
||||
|
||||
// Create the block and push it
|
||||
var block = new NoteBlock(this)
|
||||
{
|
||||
NoteType = noteType,
|
||||
Title = title,
|
||||
Span = new SourceSpan(startPosition, line.End),
|
||||
Line = processor.LineIndex,
|
||||
Column = processor.Column
|
||||
};
|
||||
|
||||
processor.NewBlocks.Push(block);
|
||||
return BlockState.Break; // Single-line block
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
- **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<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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The base class handles:
|
||||
- Counting opening fence characters (minimum 3)
|
||||
- Matching the closing fence
|
||||
- Storing the info string
|
||||
- Managing `TryContinue` automatically
|
||||
|
||||
This is the pattern used by `CustomContainerParser` (3+ colons `:::`) — it's extremely concise.
|
||||
|
||||
## The BlockProcessor state
|
||||
|
||||
Inside `TryOpen` and `TryContinue`, you have access to the `BlockProcessor` which provides:
|
||||
|
||||
{.table}
|
||||
| Property | Description |
|
||||
|---|---|
|
||||
| `processor.Line` | Current source line as a `StringSlice` |
|
||||
| `processor.CurrentChar` | Character at current position |
|
||||
| `processor.Column` | Current column number |
|
||||
| `processor.Indent` | Columns since the last indent reference point |
|
||||
| `processor.IsCodeIndent` | `true` if indent ≥ 4 |
|
||||
| `processor.IsBlankLine` | `true` if the line is empty |
|
||||
| `processor.LineIndex` | Zero-based line number in the source |
|
||||
| `processor.NewBlocks` | Stack where you push newly created blocks |
|
||||
| `processor.CurrentContainer` | The innermost open container block |
|
||||
| `processor.Document` | The root `MarkdownDocument` |
|
||||
|
||||
### Useful StringSlice methods
|
||||
|
||||
The `processor.Line` is a `StringSlice` — a window into the source text:
|
||||
|
||||
{.table}
|
||||
| Method | Description |
|
||||
|---|---|
|
||||
| `CurrentChar` | Character at current position |
|
||||
| `PeekChar(int offset)` | Look ahead without advancing |
|
||||
| `NextChar()` | Advance by one character |
|
||||
| `SkipChar()` | Skip the current character |
|
||||
| `TrimStart()` | Skip leading whitespace |
|
||||
| `IsEmpty` | True if no characters remain |
|
||||
| `Start` / `End` | Start/end positions (mutable) |
|
||||
|
||||
## CanInterrupt
|
||||
|
||||
The `CanInterrupt` method controls whether your parser can interrupt an open paragraph. By default it returns `true`, meaning your block syntax can start in the middle of a paragraph. Override to return `false` if you want your syntax to only work at the start of a block context.
|
||||
|
||||
```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
|
||||
264
site/docs/advanced/creating-extensions.md
Normal file
264
site/docs/advanced/creating-extensions.md
Normal file
@@ -0,0 +1,264 @@
|
||||
---
|
||||
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();
|
||||
```
|
||||
|
||||
### 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<MyExtension>(
|
||||
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 `<blink>text</blink>`:
|
||||
|
||||
```csharp
|
||||
using Markdig;
|
||||
using Markdig.Parsers.Inlines;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
|
||||
/// <summary>
|
||||
/// Extension that converts %%%text%%% to <blink> tags.
|
||||
/// </summary>
|
||||
public sealed class BlinkExtension : IMarkdownExtension
|
||||
{
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
var parser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
|
||||
if (parser != null && !parser.HasEmphasisChar('%'))
|
||||
{
|
||||
// 3 consecutive % on each side
|
||||
parser.EmphasisDescriptors.Add(new EmphasisDescriptor('%', 3, 3, false));
|
||||
}
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer is not HtmlRenderer) return;
|
||||
|
||||
var emphasisRenderer = renderer.ObjectRenderers.FindExact<EmphasisInlineRenderer>();
|
||||
if (emphasisRenderer == null) return;
|
||||
|
||||
var previousTag = emphasisRenderer.GetTag;
|
||||
emphasisRenderer.GetTag = inline =>
|
||||
(inline.DelimiterCount == 3 && inline.DelimiterChar == '%'
|
||||
? "blink"
|
||||
: null)
|
||||
?? previousTag(inline);
|
||||
}
|
||||
}
|
||||
|
||||
// Fluent API extension method
|
||||
public static class BlinkExtensionMethods
|
||||
{
|
||||
public static MarkdownPipelineBuilder UseBlink(
|
||||
this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<BlinkExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```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](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
|
||||
308
site/docs/advanced/inline-parsers.md
Normal file
308
site/docs/advanced/inline-parsers.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
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
|
||||
|
||||
```csharp
|
||||
using Markdig.Helpers;
|
||||
using Markdig.Parsers;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
public class KeyboardInlineParser : InlineParser
|
||||
{
|
||||
public KeyboardInlineParser()
|
||||
{
|
||||
// This parser triggers on '[' characters
|
||||
OpeningCharacters = ['['];
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
// Check for [[ opening
|
||||
if (slice.CurrentChar != '[' || slice.PeekChar(1) != '[')
|
||||
return false;
|
||||
|
||||
// Find the closing ]]
|
||||
var start = slice.Start;
|
||||
var contentStart = slice.Start + 2;
|
||||
var text = slice.Text;
|
||||
|
||||
for (int i = contentStart; i < slice.End; i++)
|
||||
{
|
||||
if (text[i] == ']' && i + 1 <= slice.End && text[i + 1] == ']')
|
||||
{
|
||||
// Found closing ]]
|
||||
var shortcut = text[contentStart..i];
|
||||
|
||||
// Create the inline
|
||||
var inline = new KeyboardInline
|
||||
{
|
||||
Shortcut = shortcut,
|
||||
Span = new Markdig.Syntax.SourceSpan(
|
||||
processor.GetSourcePosition(start, out int line, out int column),
|
||||
processor.GetSourcePosition(i + 1, out _, out _)),
|
||||
Line = line,
|
||||
Column = column
|
||||
};
|
||||
|
||||
// Set the result on the processor
|
||||
processor.Inline = inline;
|
||||
|
||||
// Advance the slice past the closing ]]
|
||||
slice.Start = i + 2;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// No closing ]] found — not a match
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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}
|
||||
| Property | 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.GetSourcePosition(pos, out line, out column)` | Map a slice position to source position |
|
||||
|
||||
## Container inlines and delimiters
|
||||
|
||||
For inlines that can contain other inlines (like emphasis), the pattern is more complex. You typically:
|
||||
|
||||
1. On opening delimiter: Insert a `DelimiterInline` placeholder.
|
||||
2. Continue parsing other inlines normally.
|
||||
3. On closing delimiter: Find the matching opener, replace both with the final inline, and adopt all children between them.
|
||||
|
||||
This is the strategy used by `LinkInlineParser` and the emphasis system. Here's a simplified example:
|
||||
|
||||
```csharp
|
||||
public class HighlightInlineParser : InlineParser
|
||||
{
|
||||
public HighlightInlineParser()
|
||||
{
|
||||
OpeningCharacters = ['⌈', '⌉']; // Custom delimiters
|
||||
}
|
||||
|
||||
public override bool Match(InlineProcessor processor, ref StringSlice slice)
|
||||
{
|
||||
var current = slice.CurrentChar;
|
||||
|
||||
if (current == '⌈')
|
||||
{
|
||||
// Opening delimiter — push a marker
|
||||
var delimiter = new DelimiterInline(this, new MarkdownDocument())
|
||||
{
|
||||
Type = DelimiterType.Open,
|
||||
// ... set span
|
||||
};
|
||||
processor.Inline = delimiter;
|
||||
slice.SkipChar();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (current == '⌉')
|
||||
{
|
||||
// Closing delimiter — find opener and replace
|
||||
var opener = processor.Inline?.FirstParentOfType<DelimiterInline>();
|
||||
if (opener?.Parser == this)
|
||||
{
|
||||
var highlight = new HighlightInline();
|
||||
opener.ReplaceBy(highlight);
|
||||
processor.Inline = highlight;
|
||||
processor.PostProcessInlines(0, highlight, null, false);
|
||||
slice.SkipChar();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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](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
|
||||
9
site/docs/advanced/menu.yml
Normal file
9
site/docs/advanced/menu.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
doc:
|
||||
- {path: readme.md, title: "<i class='bi bi-gear' aria-hidden='true'></i> Overview"}
|
||||
- {path: ast.md, title: "Abstract syntax tree"}
|
||||
- {path: pipeline.md, title: "Pipeline architecture"}
|
||||
- {path: creating-extensions.md, title: "Creating extensions"}
|
||||
- {path: block-parsers.md, title: "Block parsers"}
|
||||
- {path: inline-parsers.md, title: "Inline parsers"}
|
||||
- {path: renderers.md, title: "Renderers"}
|
||||
- {path: performance.md, title: "Performance"}
|
||||
267
site/docs/advanced/performance.md
Normal file
267
site/docs/advanced/performance.md
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
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<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("```"))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### stackalloc
|
||||
|
||||
For small temporary buffers, use `stackalloc`:
|
||||
|
||||
```csharp
|
||||
Span<char> buffer = stackalloc char[64];
|
||||
int written = FormatOutput(buffer);
|
||||
renderer.Write(buffer[..written]);
|
||||
```
|
||||
|
||||
### 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);
|
||||
}
|
||||
```
|
||||
|
||||
## 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("<div class=\"")
|
||||
.Write(cssClass)
|
||||
.Write("\">");
|
||||
|
||||
// Avoid — allocates intermediate strings
|
||||
renderer.Write($"<div class=\"{cssClass}\">");
|
||||
```
|
||||
|
||||
### Cache frequently used strings
|
||||
|
||||
For attribute names and CSS classes that repeat:
|
||||
|
||||
```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" }
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## 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<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 |
|
||||
205
site/docs/advanced/pipeline.md
Normal file
205
site/docs/advanced/pipeline.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
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);
|
||||
```
|
||||
|
||||
## 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 |
|
||||
| DocumentProcessed | `ProcessDocumentDelegate?` | Callback after parsing completes |
|
||||
|
||||
## 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<MyBlockParser>();
|
||||
|
||||
// Or modify an existing parser
|
||||
var emphasisParser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
|
||||
if (emphasisParser != null)
|
||||
{
|
||||
emphasisParser.EmphasisDescriptors.Add(
|
||||
new EmphasisDescriptor('%', 3, 3, false));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 2 example
|
||||
|
||||
```csharp
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer is HtmlRenderer htmlRenderer)
|
||||
{
|
||||
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlMyBlockRenderer>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## The OrderedList<T> 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);
|
||||
```
|
||||
|
||||
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.
|
||||
57
site/docs/advanced/readme.md
Normal file
57
site/docs/advanced/readme.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
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 |
|
||||
| [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<T>` or build a completely custom renderer |
|
||||
| [Performance](performance.md) | Tips for maintaining high throughput — allocation-free patterns, pooling, Span-based parsing |
|
||||
|
||||
## 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 |
|
||||
357
site/docs/advanced/renderers.md
Normal file
357
site/docs/advanced/renderers.md
Normal file
@@ -0,0 +1,357 @@
|
||||
---
|
||||
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);
|
||||
}
|
||||
```
|
||||
|
||||
## HTML renderers
|
||||
|
||||
### HtmlObjectRenderer<T>
|
||||
|
||||
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>");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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>");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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>");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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>");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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());
|
||||
```
|
||||
|
||||
## 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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<LatexRenderer>
|
||||
{
|
||||
public LatexRenderer(TextWriter writer) : base(writer)
|
||||
{
|
||||
// Register per-type renderers
|
||||
ObjectRenderers.Add(new LatexHeadingRenderer());
|
||||
ObjectRenderers.Add(new LatexParagraphRenderer());
|
||||
ObjectRenderers.Add(new LatexCodeBlockRenderer());
|
||||
// ... register all needed renderers
|
||||
}
|
||||
}
|
||||
|
||||
// Per-type renderer
|
||||
public class LatexHeadingRenderer : MarkdownObjectRenderer<LatexRenderer, HeadingBlock>
|
||||
{
|
||||
protected override void Write(LatexRenderer renderer, HeadingBlock obj)
|
||||
{
|
||||
var command = obj.Level switch
|
||||
{
|
||||
1 => "section",
|
||||
2 => "subsection",
|
||||
3 => "subsubsection",
|
||||
_ => "paragraph"
|
||||
};
|
||||
|
||||
renderer.Write($"\\{command}{{ "{{" }}");
|
||||
renderer.WriteLeafInline(obj);
|
||||
renderer.WriteLine("}");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Option 2: Implement IMarkdownRenderer directly
|
||||
|
||||
For fully custom output (JSON, binary, etc.):
|
||||
|
||||
```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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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<NoteBlockParser>();
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer is HtmlRenderer htmlRenderer)
|
||||
{
|
||||
htmlRenderer.ObjectRenderers.AddIfNotAlready<HtmlNoteBlockRenderer>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fluent API
|
||||
public static class NoteExtensionMethods
|
||||
{
|
||||
public static MarkdownPipelineBuilder UseNotes(
|
||||
this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.Extensions.AddIfNotAlready<NoteExtension>();
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```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
|
||||
271
site/docs/commonmark.md
Normal file
271
site/docs/commonmark.md
Normal file
@@ -0,0 +1,271 @@
|
||||
---
|
||||
title: CommonMark syntax
|
||||
---
|
||||
|
||||
# CommonMark syntax
|
||||
|
||||
Markdig is fully compliant with the [CommonMark specification](https://spec.commonmark.org/) (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***
|
||||
|
||||
## Links
|
||||
|
||||
### Inline links
|
||||
|
||||
```markdown
|
||||
[Link text](https://example.com)
|
||||
[Link with title](https://example.com "Example Title")
|
||||
```
|
||||
|
||||
### Reference links
|
||||
|
||||
```markdown
|
||||
[Link text][ref]
|
||||
[Another link][ref]
|
||||
|
||||
[ref]: https://example.com "Optional Title"
|
||||
```
|
||||
|
||||
### Autolinks
|
||||
|
||||
Angle brackets around a URL or email:
|
||||
|
||||
```markdown
|
||||
<https://example.com>
|
||||
<user@example.com>
|
||||
```
|
||||
|
||||
## 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**!");
|
||||
```
|
||||
````
|
||||
|
||||
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
|
||||
© & < >
|
||||
© ©
|
||||
```
|
||||
|
||||
## Blank lines
|
||||
|
||||
Blank lines separate block elements. Multiple consecutive blank lines are treated the same as a single blank line.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Extensions](extensions/readme.md) — Enable tables, task lists, math, footnotes, and 20+ more features
|
||||
- [Getting started](getting-started.md) — Install and configure Markdig
|
||||
39
site/docs/extensions/abbreviations.md
Normal file
39
site/docs/extensions/abbreviations.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
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](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
|
||||
<p>This page uses <abbr title="Hyper Text Markup Language">HTML</abbr>
|
||||
and <abbr title="Cascading Style Sheets">CSS</abbr>.</p>
|
||||
```
|
||||
|
||||
## 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.
|
||||
108
site/docs/extensions/alert-blocks.md
Normal file
108
site/docs/extensions/alert-blocks.md
Normal file
@@ -0,0 +1,108 @@
|
||||
---
|
||||
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.
|
||||
```
|
||||
|
||||
> [!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>
|
||||
```
|
||||
|
||||
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($"<span class=\"icon\">{kind}</span>");
|
||||
})
|
||||
.Build();
|
||||
```
|
||||
89
site/docs/extensions/auto-identifiers.md
Normal file
89
site/docs/extensions/auto-identifiers.md
Normal file
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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
|
||||
<h2 id="getting-started">Getting Started</h2>
|
||||
```
|
||||
|
||||
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.
|
||||
57
site/docs/extensions/auto-links.md
Normal file
57
site/docs/extensions/auto-links.md
Normal file
@@ -0,0 +1,57 @@
|
||||
---
|
||||
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
|
||||
<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();
|
||||
```
|
||||
|
||||
## Difference from CommonMark autolinks
|
||||
|
||||
CommonMark already supports **angle-bracket autolinks** (`<https://example.com>`). This extension goes further by detecting bare URLs without any markers.
|
||||
77
site/docs/extensions/custom-containers.md
Normal file
77
site/docs/extensions/custom-containers.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
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](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
|
||||
<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](generic-attributes.md) 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>
|
||||
```
|
||||
83
site/docs/extensions/definition-lists.md
Normal file
83
site/docs/extensions/definition-lists.md
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
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](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
|
||||
<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.
|
||||
72
site/docs/extensions/diagrams.md
Normal file
72
site/docs/extensions/diagrams.md
Normal file
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Diagrams
|
||||
---
|
||||
|
||||
# Diagrams
|
||||
|
||||
Enable with `.UseDiagrams()` (included in `UseAdvancedExtensions()`).
|
||||
|
||||
When a fenced code block uses a recognized diagram language as its info string, Markdig wraps the content in a `<div>` with the language as a CSS class instead of rendering it as `<pre><code>`.
|
||||
|
||||
## 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:
|
||||
|
||||
```html
|
||||
<div class="mermaid">graph LR
|
||||
A[Parse] --> B[AST]
|
||||
B --> C[Render]
|
||||
C --> D[HTML]
|
||||
</div>
|
||||
```
|
||||
|
||||
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]
|
||||
```
|
||||
````
|
||||
|
||||
## HTML output
|
||||
|
||||
Instead of the usual code block rendering:
|
||||
|
||||
```html
|
||||
<pre><code class="language-mermaid">...</code></pre>
|
||||
```
|
||||
|
||||
The diagrams extension produces:
|
||||
|
||||
```html
|
||||
<div class="mermaid">...</div>
|
||||
```
|
||||
|
||||
This allows client-side diagram libraries to find and render the content.
|
||||
102
site/docs/extensions/emoji-smartypants.md
Normal file
102
site/docs/extensions/emoji-smartypants.md
Normal file
@@ -0,0 +1,102 @@
|
||||
---
|
||||
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](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();
|
||||
```
|
||||
91
site/docs/extensions/emphasis-extras.md
Normal file
91
site/docs/extensions/emphasis-extras.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Emphasis extras
|
||||
---
|
||||
|
||||
# Emphasis extras
|
||||
|
||||
Enable with `.UseEmphasisExtras()` (included in `UseAdvancedExtensions()`).
|
||||
|
||||
This extension adds several emphasis styles beyond standard bold and italic.
|
||||
|
||||
## 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)
|
||||
91
site/docs/extensions/figures-footers-citations.md
Normal file
91
site/docs/extensions/figures-footers-citations.md
Normal file
@@ -0,0 +1,91 @@
|
||||
---
|
||||
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""
|
||||
```
|
||||
87
site/docs/extensions/footnotes.md
Normal file
87
site/docs/extensions/footnotes.md
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
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](https://michelf.ca/projects/php-markdown/extra/#footnotes).
|
||||
|
||||
## 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.
|
||||
115
site/docs/extensions/generic-attributes.md
Normal file
115
site/docs/extensions/generic-attributes.md
Normal file
@@ -0,0 +1,115 @@
|
||||
---
|
||||
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](https://michelf.ca/projects/php-markdown/extra/#spe-attr).
|
||||
|
||||
> [!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](https://example.com){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;
|
||||
```
|
||||
````
|
||||
|
||||
### 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
|
||||
|
||||
### Links
|
||||
|
||||
```markdown
|
||||
[Click me](https://example.com){.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>
|
||||
```
|
||||
82
site/docs/extensions/list-extras.md
Normal file
82
site/docs/extensions/list-extras.md
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
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` |
|
||||
77
site/docs/extensions/mathematics.md
Normal file
77
site/docs/extensions/mathematics.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
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](https://katex.org/) or [MathJax](https://www.mathjax.org/).
|
||||
|
||||
### 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
|
||||
<!-- KaTeX -->
|
||||
<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>
|
||||
renderMathInElement(document.body, {
|
||||
delimiters: [
|
||||
{ left: "\\(", right: "\\)", display: false },
|
||||
{ left: "\\[", right: "\\]", display: true }
|
||||
]
|
||||
});
|
||||
</script>
|
||||
```
|
||||
76
site/docs/extensions/media-links.md
Normal file
76
site/docs/extensions/media-links.md
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
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();
|
||||
```
|
||||
21
site/docs/extensions/menu.yml
Normal file
21
site/docs/extensions/menu.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
doc:
|
||||
- {path: readme.md, title: "<i class='bi bi-puzzle' aria-hidden='true'></i> Overview"}
|
||||
- {path: tables.md, title: "Tables"}
|
||||
- {path: emphasis-extras.md, title: "Emphasis extras"}
|
||||
- {path: task-lists.md, title: "Task lists"}
|
||||
- {path: mathematics.md, title: "Mathematics"}
|
||||
- {path: alert-blocks.md, title: "Alert blocks"}
|
||||
- {path: diagrams.md, title: "Diagrams"}
|
||||
- {path: footnotes.md, title: "Footnotes"}
|
||||
- {path: generic-attributes.md, title: "Generic attributes"}
|
||||
- {path: custom-containers.md, title: "Custom containers"}
|
||||
- {path: abbreviations.md, title: "Abbreviations"}
|
||||
- {path: definition-lists.md, title: "Definition lists"}
|
||||
- {path: auto-identifiers.md, title: "Auto-identifiers"}
|
||||
- {path: auto-links.md, title: "Auto-links"}
|
||||
- {path: figures-footers-citations.md, title: "Figures, footers & citations"}
|
||||
- {path: emoji-smartypants.md, title: "Emoji & SmartyPants"}
|
||||
- {path: media-links.md, title: "Media links"}
|
||||
- {path: list-extras.md, title: "List extras"}
|
||||
- {path: yaml-frontmatter.md, title: "YAML front matter"}
|
||||
- {path: other.md, title: "Other extensions"}
|
||||
174
site/docs/extensions/other.md
Normal file
174
site/docs/extensions/other.md
Normal file
@@ -0,0 +1,174 @@
|
||||
---
|
||||
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>`
|
||||
|
||||
## JIRA links
|
||||
|
||||
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();
|
||||
```
|
||||
|
||||
## Referral links
|
||||
|
||||
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
|
||||
<!--markdig:extensions=pipetables+footnotes-->
|
||||
|
||||
| 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. Useful for safe rendering of user-provided Markdown:
|
||||
|
||||
```csharp
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.DisableHtml()
|
||||
.Build();
|
||||
```
|
||||
84
site/docs/extensions/readme.md
Normal file
84
site/docs/extensions/readme.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
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](alert-blocks.md) | `.UseAlertBlocks()` | GitHub-style `[!NOTE]`, `[!TIP]`, etc. |
|
||||
| [Abbreviations](abbreviations.md) | `.UseAbbreviations()` | Abbreviation definitions: `*[HTML]: Hyper Text Markup Language` |
|
||||
| [Auto-identifiers](auto-identifiers.md) | `.UseAutoIdentifiers()` | Automatic `id` attributes on headings |
|
||||
| [Citations](figures-footers-citations.md) | `.UseCitations()` | Citation text with `""...""` |
|
||||
| [Custom containers](custom-containers.md) | `.UseCustomContainers()` | Fenced `:::` div containers |
|
||||
| [Definition lists](definition-lists.md) | `.UseDefinitionLists()` | `<dl>` / `<dt>` / `<dd>` lists |
|
||||
| [Emphasis extras](emphasis-extras.md) | `.UseEmphasisExtras()` | Strikethrough, sub/superscript, inserted, marked |
|
||||
| [Figures](figures-footers-citations.md) | `.UseFigures()` | `^^^` figure blocks |
|
||||
| [Footers](figures-footers-citations.md) | `.UseFooters()` | `^^` footers |
|
||||
| [Footnotes](footnotes.md) | `.UseFootnotes()` | `[^ref]` footnotes |
|
||||
| [Grid tables](tables.md) | `.UseGridTables()` | Pandoc-style grid tables |
|
||||
| [Mathematics](mathematics.md) | `.UseMathematics()` | `$...$` inline / `$$...$$` block math |
|
||||
| [Media links](media-links.md) | `.UseMediaLinks()` | Embed YouTube, Vimeo, etc. |
|
||||
| [Pipe tables](tables.md) | `.UsePipeTables()` | GitHub-style pipe tables |
|
||||
| [List extras](list-extras.md) | `.UseListExtras()` | Alpha and Roman numeral ordered lists |
|
||||
| [Task lists](task-lists.md) | `.UseTaskLists()` | `- [x]` / `- [ ]` checkboxes |
|
||||
| [Diagrams](diagrams.md) | `.UseDiagrams()` | Mermaid, nomnoml diagram blocks |
|
||||
| [Auto-links](auto-links.md) | `.UseAutoLinks()` | Auto-detect `http://`, `www.` URLs |
|
||||
| [Generic attributes](generic-attributes.md) | `.UseGenericAttributes()` | `{.class #id key=value}` attributes |
|
||||
|
||||
## Additional extensions (not in UseAdvancedExtensions)
|
||||
|
||||
{.table}
|
||||
| Extension | Method | Description |
|
||||
|---|---|---|
|
||||
| [Emoji](emoji-smartypants.md) | `.UseEmojiAndSmiley()` | `:emoji:` shortcodes and smileys |
|
||||
| [SmartyPants](emoji-smartypants.md) | `.UseSmartyPants()` | Smart quotes, dashes, ellipses |
|
||||
| [Bootstrap](other.md) | `.UseBootstrap()` | Bootstrap CSS classes |
|
||||
| [Hardline breaks](other.md) | `.UseSoftlineBreakAsHardlineBreak()` | Treat soft line breaks as `<br>` |
|
||||
| [YAML front matter](yaml-frontmatter.md) | `.UseYamlFrontMatter()` | Parse and discard YAML front matter |
|
||||
| [JIRA links](other.md) | `.UseJiraLinks(options)` | Auto-link Jira issue keys |
|
||||
| [Globalization](other.md) | `.UseGlobalization()` | Right-to-left text support |
|
||||
| [Referral links](other.md) | `.UseReferralLinks(rels)` | Add `rel` attributes to links |
|
||||
| [Self pipeline](other.md) | `.UseSelfPipeline()` | Auto-configure pipeline from document |
|
||||
| [Pragma lines](other.md) | `.UsePragmaLines()` | Line number pragma IDs |
|
||||
| [Non-ASCII no escape](other.md) | `.UseNonAsciiNoEscape()` | Disable URI escaping for non-ASCII |
|
||||
|
||||
## 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();
|
||||
```
|
||||
168
site/docs/extensions/tables.md
Normal file
168
site/docs/extensions/tables.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
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();
|
||||
```
|
||||
|
||||
## 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 |
|
||||
+-------+-------+
|
||||
```
|
||||
|
||||
### Header separator
|
||||
|
||||
Use `=` instead of `-` for the header separator line (`+===+===+`).
|
||||
50
site/docs/extensions/task-lists.md
Normal file
50
site/docs/extensions/task-lists.md
Normal file
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: Task lists
|
||||
---
|
||||
|
||||
# Task lists
|
||||
|
||||
Enable with `.UseTaskLists()` (included in `UseAdvancedExtensions()`).
|
||||
|
||||
Task lists add checkbox-style list items, inspired by [GitHub task lists](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-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>
|
||||
```
|
||||
75
site/docs/extensions/yaml-frontmatter.md
Normal file
75
site/docs/extensions/yaml-frontmatter.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
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](https://github.com/aaubry/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.
|
||||
148
site/docs/getting-started.md
Normal file
148
site/docs/getting-started.md
Normal file
@@ -0,0 +1,148 @@
|
||||
---
|
||||
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](https://www.nuget.org/packages/Markdig/):
|
||||
|
||||
```shell
|
||||
dotnet add package Markdig
|
||||
```
|
||||
|
||||
Or via the Package Manager Console:
|
||||
|
||||
```powershell
|
||||
Install-Package Markdig
|
||||
```
|
||||
|
||||
A [strong-named variant](https://www.nuget.org/packages/Markdig.Signed/) is also available:
|
||||
|
||||
```shell
|
||||
dotnet add package Markdig.Signed
|
||||
```
|
||||
|
||||
### Requirements
|
||||
|
||||
Markdig targets .NET Standard 2.0, .NET Standard 2.1, .NET 8.0, and .NET 9.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](extensions/readme.md) 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](advanced/ast.md) 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](usage.md) 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](advanced/renderers.md) guide for details on creating custom renderers.
|
||||
|
||||
## Next steps
|
||||
|
||||
- [Usage guide](usage.md) — Understand pipeline architecture, Parse+Render separation, and common patterns
|
||||
- [CommonMark syntax](commonmark.md) — Reference for all core Markdown syntax
|
||||
- [Extensions](extensions/readme.md) — Discover all 20+ built-in extensions
|
||||
- [Developer guide](advanced/readme.md) — Create your own parsers, renderers, and extensions
|
||||
7
site/docs/menu.yml
Normal file
7
site/docs/menu.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
doc:
|
||||
- {path: readme.md, title: "<i class='bi bi-book' aria-hidden='true'></i> User Guide"}
|
||||
- {path: getting-started.md, title: "<i class='bi bi-rocket-takeoff' aria-hidden='true'></i> Getting started"}
|
||||
- {path: usage.md, title: "<i class='bi bi-play-circle' aria-hidden='true'></i> Usage"}
|
||||
- {path: commonmark.md, title: "<i class='bi bi-markdown' aria-hidden='true'></i> CommonMark syntax"}
|
||||
- {path: extensions/readme.md, title: "<i class='bi bi-puzzle' aria-hidden='true'></i> Extensions", folder: true}
|
||||
- {path: advanced/readme.md, title: "<i class='bi bi-gear' aria-hidden='true'></i> Developer guide", folder: true}
|
||||
62
site/docs/readme.md
Normal file
62
site/docs/readme.md
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
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](getting-started.md) | Install Markdig, parse your first Markdown, and render to HTML |
|
||||
| [Usage](usage.md) | Parse, render, pipeline architecture, and common patterns |
|
||||
|
||||
## CommonMark syntax
|
||||
|
||||
{.table}
|
||||
| Guide | What you'll learn |
|
||||
|---|---|
|
||||
| [CommonMark syntax](commonmark.md) | Full reference for headings, paragraphs, emphasis, links, images, code, lists, blockquotes, and more |
|
||||
|
||||
## Extensions
|
||||
|
||||
{.table}
|
||||
| Guide | What it provides |
|
||||
|---|---|
|
||||
| [Extensions overview](extensions/readme.md) | Index of all 20+ built-in extensions |
|
||||
| [Tables](extensions/tables.md) | Pipe tables and grid tables |
|
||||
| [Emphasis extras](extensions/emphasis-extras.md) | Strikethrough, subscript, superscript, inserted, marked |
|
||||
| [Task lists](extensions/task-lists.md) | Checkbox task lists in GFM style |
|
||||
| [Mathematics](extensions/mathematics.md) | Inline and block LaTeX math |
|
||||
| [Diagrams](extensions/diagrams.md) | Mermaid, nomnoml, and other diagram languages |
|
||||
| [Alert blocks](extensions/alert-blocks.md) | GitHub-style alerts: NOTE, TIP, WARNING, etc. |
|
||||
| [Footnotes](extensions/footnotes.md) | Reference-style footnotes |
|
||||
| [Generic attributes](extensions/generic-attributes.md) | Attach CSS classes, IDs, and attributes to any element |
|
||||
| [Custom containers](extensions/custom-containers.md) | Fenced `:::` div containers |
|
||||
| [Abbreviations](extensions/abbreviations.md) | Abbreviation definitions and auto-expansion |
|
||||
| [Definition lists](extensions/definition-lists.md) | `<dl>` / `<dt>` / `<dd>` lists |
|
||||
| [Auto-identifiers](extensions/auto-identifiers.md) | Automatic heading IDs |
|
||||
| [Auto-links](extensions/auto-links.md) | Automatic URL detection |
|
||||
| [Figures & footers](extensions/figures-footers-citations.md) | Figures, footers, and citations |
|
||||
| [Emoji & SmartyPants](extensions/emoji-smartypants.md) | Emoji shortcodes and smart typography |
|
||||
| [Media links](extensions/media-links.md) | Embedded YouTube, Vimeo, and media players |
|
||||
| [List extras](extensions/list-extras.md) | Alpha and Roman numeral ordered lists |
|
||||
| [YAML front matter](extensions/yaml-frontmatter.md) | YAML metadata blocks |
|
||||
| [Other extensions](extensions/other.md) | Bootstrap, hardline breaks, JIRA links, globalization, and more |
|
||||
|
||||
## Developer guide
|
||||
|
||||
{.table}
|
||||
| Guide | What you'll learn |
|
||||
|---|---|
|
||||
| [Developer guide overview](advanced/readme.md) | How to extend Markdig with custom parsers and renderers |
|
||||
| [Abstract syntax tree](advanced/ast.md) | Structure and traversal of the AST |
|
||||
| [Pipeline architecture](advanced/pipeline.md) | How the parsing pipeline works |
|
||||
| [Creating extensions](advanced/creating-extensions.md) | Implement `IMarkdownExtension` and register it |
|
||||
| [Block parsers](advanced/block-parsers.md) | Write custom block-level parsers |
|
||||
| [Inline parsers](advanced/inline-parsers.md) | Write custom inline-level parsers |
|
||||
| [Renderers](advanced/renderers.md) | Write custom renderers for HTML and other formats |
|
||||
| [Performance](advanced/performance.md) | Tips for high-performance Markdown processing |
|
||||
263
site/docs/usage.md
Normal file
263
site/docs/usage.md
Normal file
@@ -0,0 +1,263 @@
|
||||
---
|
||||
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 |
|
||||
|
||||
#### 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`.
|
||||
|
||||
## 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](advanced/renderers.md) 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](commonmark.md) — Core Markdown syntax reference
|
||||
- [Extensions](extensions/readme.md) — All built-in extensions
|
||||
- [Developer guide](advanced/readme.md) — Create custom parsers and renderers
|
||||
BIN
site/favicon.ico
Normal file
BIN
site/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
BIN
site/img/github-banner.png
Normal file
BIN
site/img/github-banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
site/img/markdig.png
Normal file
BIN
site/img/markdig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
129
site/img/markdig.svg
Normal file
129
site/img/markdig.svg
Normal file
@@ -0,0 +1,129 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="192"
|
||||
height="192"
|
||||
viewBox="0 0 192 192"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
|
||||
sodipodi:docname="markdig.svg"
|
||||
inkscape:export-filename="C:\code\lunet\markdig\img\markdig.png"
|
||||
inkscape:export-xdpi="256"
|
||||
inkscape:export-ydpi="256">
|
||||
<defs
|
||||
id="defs4" />
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="6.275557"
|
||||
inkscape:cx="81.620292"
|
||||
inkscape:cy="119.68434"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="false"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2066"
|
||||
inkscape:window-x="-11"
|
||||
inkscape:window-y="-11"
|
||||
inkscape:window-maximized="1"
|
||||
units="px" />
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline"
|
||||
transform="translate(0,-860.36216)">
|
||||
<path
|
||||
style="fill:#006680;fill-opacity:1;stroke-width:1.09204066"
|
||||
d="m 34.5307,873.24547 c -1.506145,1.5557 -2.414834,3.72567 -2.403331,6.22497 l 0.04173,27.41047 -19.903814,-0.0324 c -4.824408,-0.009 -8.4381391,3.7242 -8.4300642,8.70738 0.00817,4.98312 3.6330369,8.72727 8.4573232,8.73554 l 19.903821,0.0324 -0.275791,17.12991 124.244306,-0.42328 -0.0273,-17.44297 20.80798,-0.27883 c 2.4087,0.009 4.52205,-0.92824 6.02671,-2.48239 1.50614,-1.55569 2.41486,-3.72569 2.40336,-6.225 -0.009,-4.98311 -3.63306,-8.7273 -8.45733,-8.73554 l -20.50656,0.59015 0.25961,-27.09914 c -0.31244,-2.17456 -1.213,-4.36429 -3.02472,-6.23561 -1.50705,-1.55665 -3.62738,-2.49321 -6.03475,-2.50058 l -107.054134,0.14282 c -2.408688,-0.009 -4.522059,0.92823 -6.026707,2.48237 z m 27.829505,3.83665 14.459233,0.0217 14.488635,21.31347 14.429017,-21.27019 14.45922,0.0217 0.0811,57.91281 -14.4592,-0.0217 -0.0464,-33.21443 -14.428869,21.27005 -14.488819,-21.31366 0.04632,33.21458 -14.459226,-0.0218 -0.08116,-57.91275 z"
|
||||
id="path4142"
|
||||
inkscape:connector-curvature="0" />
|
||||
<rect
|
||||
id="rect4168"
|
||||
mask="url(#a)"
|
||||
ry="0"
|
||||
height="0"
|
||||
width="0"
|
||||
x="141.51523"
|
||||
y="364.10403" />
|
||||
<rect
|
||||
id="rect4184"
|
||||
mask="url(#a)"
|
||||
ry="0"
|
||||
height="0"
|
||||
width="0"
|
||||
x="96.108383"
|
||||
y="352.01443" />
|
||||
<rect
|
||||
id="rect4200"
|
||||
mask="url(#a)"
|
||||
ry="2.1886277"
|
||||
height="0.14590852"
|
||||
width="0.17464182"
|
||||
x="87.014519"
|
||||
y="276.38696" />
|
||||
<flowRoot
|
||||
xml:space="preserve"
|
||||
id="flowRoot4797"
|
||||
style="font-style:normal;font-weight:normal;line-height:0.01%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
transform="matrix(0.1746417,0,0,0.1459084,499.69318,366.39614)"><flowRegion
|
||||
id="flowRegion4799"><rect
|
||||
id="rect4801"
|
||||
width="972.27185"
|
||||
height="618.71844"
|
||||
x="959.01355"
|
||||
y="-976.15039" /></flowRegion><flowPara
|
||||
id="flowPara4803"
|
||||
style="font-size:40px;line-height:1.25"> </flowPara></flowRoot> <g
|
||||
id="g4833"
|
||||
transform="matrix(0.09510056,0,0,0.09061765,496.09965,368.83934)">
|
||||
<rect
|
||||
id="rect4823"
|
||||
height="1"
|
||||
width="1"
|
||||
x="0"
|
||||
y="0"
|
||||
style="fill:#ffffff" />
|
||||
</g>
|
||||
<g
|
||||
transform="translate(234.63786,787.55486)"
|
||||
id="g4170" />
|
||||
<path
|
||||
sodipodi:nodetypes="cccccccccccccc"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path826"
|
||||
d="m 45.058494,943.54749 20.013843,88.87081 c -8.87e-4,1.4793 13.909884,7.9857 28.77803,7.9868 14.868153,0 27.732473,-6.0454 27.727033,-7.5199 l 17.1004,-89.73688 z m 35.561125,8.36596 h 25.853221 l 10e-6,42.86491 h 13.18189 l -26.108514,40.41734 -26.324515,-40.20925 13.397908,-0.20809 z"
|
||||
style="fill:#ff6600;stroke-width:1.2582258" />
|
||||
<path
|
||||
style="fill:#c83737;stroke-width:1.2582258"
|
||||
d="m 45.058494,943.54749 20.013843,88.87081 c -8.87e-4,1.4793 13.909884,7.9857 28.77803,7.9868 14.868153,0 27.732473,-6.0454 27.727033,-7.5199 l 17.1004,-89.73688 z m 35.561125,8.36596 h 25.853221 l 10e-6,42.86491 h 13.18189 l -26.108514,40.41734 -26.324515,-40.20925 13.397908,-0.20809 z"
|
||||
id="path828"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="cccccccccccccc" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
8
site/menu.yml
Normal file
8
site/menu.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
home:
|
||||
- {path: readme.md, title: "<i class='bi bi-house-door' aria-hidden='true'></i> Home", self: true}
|
||||
- {path: docs/readme.md, title: "<i class='bi bi-book' aria-hidden='true'></i> Docs", folder: true}
|
||||
- {path: api/readme.md, title: "<i class='bi bi-filetype-cs' aria-hidden='true'></i> API", folder: true}
|
||||
|
||||
home2:
|
||||
- {url: "https://github.com/sponsors/xoofx", title: '<i class="bi bi-heart" style="color: pink"></i> Sponsor', link_class: btn btn-outline-secondary }
|
||||
- {url: "https://github.com/xoofx/markdig/", title: '<i class="bi bi-github"></i> GitHub', link_class: btn btn-info}
|
||||
@@ -1,134 +0,0 @@
|
||||
# The Abstract Syntax Tree
|
||||
|
||||
If successful, the `Markdown.Parse(...)` method returns the abstract syntax tree (AST) of the source text.
|
||||
|
||||
This will be an object of the `MarkdownDocument` type, which is in turn derived from a more general block container and is part of a larger taxonomy of classes which represent different semantic constructs of a markdown syntax tree.
|
||||
|
||||
This document will discuss the different types of elements within the Markdig representation of the AST.
|
||||
|
||||
## Structure of the AST
|
||||
|
||||
Within Markdig, there are two general types of node in the markdown syntax tree: `Block`, and `Inline`. Block nodes may contain inline nodes, but the reverse is not true. Blocks may contain other blocks, and inlines may contain other inlines.
|
||||
|
||||
The root of the AST is the `MarkdownDocument` which is itself derived from a container block but also contains information on the line count and starting positions within the document. Nodes in the AST have links both to parent and children, allowing the edges in the tree to be traversed efficiently in either direction.
|
||||
|
||||
Different semantic constructs are represented by types derived from the `Block` and `Inline` types, which are both `abstract` themselves. These elements are produced by `BlockParser` and `InlineParser` derived types, respectively, and so new constructs can be added with the implementation of a new block or inline parser and a new block or inline type, as well as an extension to register it in the pipeline. For more information on extending Markdig this way refer to the [Extensions/Parsers](parsing-extensions.md) document.
|
||||
|
||||
The AST is assembled by the static method `Markdown.Parse(...)` using the collections of block and inline parsers contained in the `MarkdownPipeline`. For more detailed information refer to the [Markdig Parsing Overview](parsing-overview.md) document.
|
||||
|
||||
### Quick Examples: Descendants API
|
||||
|
||||
The easiest way to traverse the abstract syntax tree is with a group of extension methods that have the name `Descendants`. Several different overloads exist to allow it to search for both `Block` and `Inline` elements, starting from any node in the tree.
|
||||
|
||||
The `Descendants` methods return `IEnumerable<MarkdownObject>` or `IEnumerable<T>` as their results. Internally they are using `yield return` to perform edge traversals lazily.
|
||||
|
||||
#### Depth-First Like Traversal of All Elements
|
||||
|
||||
```csharp
|
||||
MarkdownDocument result = Markdown.Parse(sourceText, pipeline);
|
||||
|
||||
// Iterate through all MarkdownObjects in a depth-first order
|
||||
foreach (var item in result.Descendants())
|
||||
{
|
||||
Console.WriteLine(item.GetType());
|
||||
|
||||
// You can use pattern matching to isolate elements of certain type,
|
||||
// otherwise you can use the filtering mechanism demonstrated in the
|
||||
// next section
|
||||
if (item is ListItemBlock listItem)
|
||||
{
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Filtering of Specific Child Types
|
||||
|
||||
Filtering can be performed using the `Descendants<T>()` method, in which T is required to be derived from `MarkdownObject`.
|
||||
|
||||
```csharp
|
||||
MarkdownDocument result = Markdown.Parse(sourceText, pipeline);
|
||||
|
||||
// Iterate through all ListItem blocks
|
||||
foreach (var item in result.Descendants<ListItemBlock>())
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
// Iterate through all image links
|
||||
foreach (var item in result.Descendants<LinkInline>().Where(x => x.IsImage))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Combined Hierarchies
|
||||
|
||||
The `Descendants` method can be used on any `MarkdownObject`, not just the root node, so complex hierarchies can be queried.
|
||||
|
||||
```csharp
|
||||
MarkdownDocument result = Markdown.Parse(sourceText, pipeline);
|
||||
|
||||
// Find all Emphasis inlines which descend from a ListItem block
|
||||
var items = document.Descendants<ListItemBlock>()
|
||||
.Select(block => block.Descendants<EmphasisInline>());
|
||||
|
||||
// Find all Emphasis inlines whose direct parent block is a ListItem
|
||||
var other = document.Descendants<EmphasisInline>()
|
||||
.Where(inline => inline.ParentBlock is ListItemBlock);
|
||||
```
|
||||
|
||||
## Block Elements
|
||||
|
||||
Block elements all derive from `Block` and may be one of two types:
|
||||
|
||||
1. `ContainerBlock`, which is a block which holds other blocks (`MarkdownDocument` is itself derived from this)
|
||||
2. `LeafBlock`, which is a block that has no child blocks, but may contain inlines
|
||||
|
||||
Block elements in markdown refer to things like paragraphs, headings, lists, code, etc. Most blocks may contain inlines, with the exception of things like code blocks.
|
||||
|
||||
### Properties of Blocks
|
||||
|
||||
The following are properties of `Block` objects which warrant elaboration. For a full list of properties see the generated API documentation (coming soon).
|
||||
|
||||
#### Block Parent
|
||||
All blocks have a reference to a parent (`Parent`) of type `ContainerBlock?`, which allows for efficient traversal up the abstract syntax tree. The parent will be `null` in the case of the root node (the `MarkdownDocument`).
|
||||
|
||||
#### Parser
|
||||
|
||||
All blocks have a reference to a parser (`Parser`) of type `BlockParser?` which refers to the instance of the parser which created this block.
|
||||
|
||||
#### IsOpen Flag
|
||||
|
||||
Blocks have an `IsOpen` boolean flag which is set true while they're being parsed and then closed when parsing is complete.
|
||||
|
||||
Blocks are created by `BlockParser` objects which are managed by an instance of a `BlockProcessor` object. During the parsing algorithm the `BlockProcessor` maintains a list of all currently open `Block` objects as it steps through the source line by line. The `IsOpen` flag indicates to the `BlockProcessor` that the block should remain open as the next line begins. If the `IsOpen` flag is not directly set by the `BlockParser` on each line, the `BlockProcessor` will consider the `Block` fully parsed and will no longer call its `BlockParser` on it.
|
||||
|
||||
#### IsBreakable Flag
|
||||
|
||||
Blocks are either breakable or not, specified by the `IsBreakable` flag. If a block is non-breakable it indicates to the parser that the close condition of any parent container do not apply so long as the non-breakable child block is still open.
|
||||
|
||||
The only built-in example of this is the `FencedCodeBlock`, which, if existing as the child of a container block of some sort, will prevent that container from being closed before the `FencedCodeBlock` is closed, since any characters inside the `FencedCodeBlock` are considered to be valid code and not the container's close condition.
|
||||
|
||||
#### RemoveAfterProcessInlines
|
||||
|
||||
|
||||
|
||||
## Inline Elements
|
||||
|
||||
Inlines in markdown refer to things like embellishments (italics, bold, underline, etc), links, urls, inline code, images, etc.
|
||||
|
||||
Inline elements may be one of two types:
|
||||
|
||||
1. `Inline`, whose parent is always a `ContainerInline`
|
||||
2. `ContainerInline`, derived from `Inline`, which contains other inlines. `ContainerInline` also has a `ParentBlock` property of type `LeafBlock?`
|
||||
|
||||
|
||||
**(Is there anything special worth documenting about inlines or types of inlines?)**
|
||||
|
||||
## The SourceSpan Struct
|
||||
|
||||
If the pipeline was configured with `.UsePreciseSourceLocation()`, all elements in the abstract syntax tree will contain a reference to the location in the original source where they occurred. This is done with the `SourceSpan` type, a custom Markdig `struct` which provides a start and end location.
|
||||
|
||||
All objects derived from `MarkdownObject` contain the `Span` property, which is of type `SourceSpan`.
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
# Extensions and Parsers
|
||||
|
||||
Markdig was [implemented in such a way](http://xoofx.github.io/blog/2016/06/13/implementing-a-markdown-processor-for-dotnet/) as to be extremely pluggable, with even basic behaviors being mutable and extendable.
|
||||
|
||||
The basic mechanism for extension of Markdig is the `IMarkdownExtension` interface, which allows any implementing class to be registered with the pipeline builder and thus to directly modify the collections of `BlockParser` and `InlineParser` objects which end up in the pipeline.
|
||||
|
||||
This document discusses the `IMarkdownExtension` interface, the `BlockParser` abstract base class, and the `InlineParser` abstract base class, which together are the foundation of extending Markdig's parsing machinery.
|
||||
|
||||
## Creating Extensions
|
||||
|
||||
Extensions can vary from very simple to very complicated.
|
||||
|
||||
A simple extension, for example, might simply find a parser already in the pipeline and modify a setting on it. An example of this is the `SoftlineBreakAsHardlineExtension`, which locates the `LineBreakInlineParser` and modifies a single boolean flag on it.
|
||||
|
||||
A complex extension, on the other hand, might add an entire taxonomy of new `Block` and `Inline` types, as well as several related parsers and renderers, and require being added to the the pipeline in a specific order in relation to other extensions which are already configured. The `FootnoteExtension` and `PipeTableExtension` are examples of more complex extensions.
|
||||
|
||||
For extensions that don't require order considerations, the implementation of the extension itself is adequate, and the extension can be added to the pipeline with the generic `Use<TExtension>()` method on the pipeline builder. For extensions which do require order considerations, it is best to create an extension method on the `MarkdownPipelineBuilder` to perform the registration. See the following two sections for further information.
|
||||
|
||||
### Implementation of an Extension
|
||||
|
||||
The [IMarkdownExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/IMarkdownExtension.cs) interface specifies two methods which must be implemented.
|
||||
|
||||
The first, which takes only the pipeline builder as an argument, is called when the `Build()` method on the pipeline builder is invoked, and should set up any modifications to the parsers or parser collections. These parsers will then be used by the main parsing algorithm to process the source text.
|
||||
|
||||
```csharp
|
||||
void Setup(MarkdownPipelineBuilder pipeline);
|
||||
```
|
||||
|
||||
The second, which takes the pipeline itself and a renderer, is used to set up a rendering component in order to convert any special `MarkdownObject` types associated with the extension into an output. This is not relevant for parsing, but is necessary for rendering.
|
||||
|
||||
```csharp
|
||||
void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer);
|
||||
```
|
||||
|
||||
The extension can then be registered to the pipeline builder using the `Use<TExtension>()` method. A skeleton example is given below:
|
||||
|
||||
```csharp
|
||||
public class MySpecialBlockParser : BlockParser
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
public class MyExtension : IMarkdownExtension
|
||||
{
|
||||
void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
pipeline.BlockParsers.AddIfNotAlready<MySpecialBlockParser>();
|
||||
}
|
||||
|
||||
void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { }
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
var builder = new MarkdownPipelineBuilder()
|
||||
.Use<MyExtension>();
|
||||
```
|
||||
|
||||
### Pipeline Builder Extension Methods
|
||||
|
||||
For extensions which require specific ordering and/or need to perform multiple operations to register with the builder, it's recommended to create an extension method.
|
||||
|
||||
```csharp
|
||||
public static class MyExtensionMethods
|
||||
{
|
||||
public static MarkdownPipelineBuilder UseMyExtension(this MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
// Directly access or modify pipeline.Extensions here, with the ability to
|
||||
// search for other extensions, insert before or after, remove other extensions,
|
||||
// or modify their settings.
|
||||
|
||||
// ...
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Simple Extension Example
|
||||
|
||||
An example of a simple extension which does not add any new parsers, but instead creates a new, horrific emphasis tag, marked by triple percentage signs. This example is based on [CitationExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/Extensions/Citations/CitationExtension.cs)
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// An extension which applies to text of the form %%%text%%%
|
||||
/// </summary>
|
||||
public class BlinkExtension : IMarkdownExtension
|
||||
{
|
||||
// This setup method will be run when the pipeline builder's `Build()` method is invoked. As this
|
||||
// is a simple, self-contained extension we won't be adding anything new, but rather finding an
|
||||
// existing parser already in the pipeline and adding some settings to it.
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
// We check the pipeline builder's inline parser collection and see if we can find a parser
|
||||
// registered of the type EmphasisInlineParser. This is the parser which nominally handles
|
||||
// bold and italic emphasis, but we know from its documentation that it is a general parser
|
||||
// that can have new characters added to it.
|
||||
var parser = pipeline.InlineParsers.FindExact<EmphasisInlineParser>();
|
||||
|
||||
// If we find the parser and it doesn't already have the % character registered, we add
|
||||
// a descriptor for 3 consecutive % signs. This is specific to the EmphasisInlineParser and
|
||||
// is just used here as an example.
|
||||
if (parser is not null && !parser.HasEmphasisChar('%'))
|
||||
{
|
||||
parser.EmphasisDescriptors.Add(new EmphasisDescriptor('%', 3, 3, false));
|
||||
}
|
||||
}
|
||||
|
||||
// This method is called by the pipeline before rendering, which is a separate operation from
|
||||
// parsing. This implementation is just here for the purpose of the example, in which we
|
||||
// daisy-chain a delegate specific to the EmphasisInlineRenderer to cause an unconscionable tag
|
||||
// to be inserted into the HTML output wherever a %%% annotated span was placed in the source.
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
if (renderer is not HtmlRenderer) return;
|
||||
|
||||
var emphasisRenderer = renderer.ObjectRenderers.FindExact<EmphasisInlineRenderer>();
|
||||
if (emphasisRenderer is null) return;
|
||||
|
||||
var previousTag = emphasisRenderer.GetTag;
|
||||
emphasisRenderer.GetTag = inline =>
|
||||
(inline.DelimiterCount == 3 && inline.DelimiterChar == '%' ? "blink" : null)
|
||||
?? previousTag(inline);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,336 +0,0 @@
|
||||
# Markdig Parsing
|
||||
|
||||
Markdig provides efficient, regex-free parsing of markdown documents directly into an abstract syntax tree (AST). The AST is a representation of the markdown document's semantic constructs, which can be manipulated and explored programmatically.
|
||||
|
||||
* This document contains a general overview of the parsing system and components and their use
|
||||
* The [Abstract Syntax Tree](parsing-ast.md) document contains a discussion of how Markdig represents the product of the parsing operation
|
||||
* The [Extensions/Parsers](parsing-extensions.md) document explores extensions and block/inline parsers within the context of extending Markdig's parsing capabilities
|
||||
|
||||
## Introduction
|
||||
|
||||
Markdig's parsing machinery consists of two main components at its surface: the `Markdown.Parse(...)` method and the `MarkdownPipeline` type. The parsed document is represented by a `MarkdownDocument` object, which is a tree of objects derived from `MarkdownObject`, including block and inline elements.
|
||||
|
||||
The `Markdown` static class is the main entrypoint to the Markdig API. It contains the `Parse(...)` method, the main algorithm for parsing a markdown document. The `Parse(...)` method in turn uses a `MarkdownPipeline`, which is a sealed internal class which maintains some configuration information and the collections of parsers and extensions. The `MarkdownPipeline` determines how the parser behaves and what its capabilities are. The `MarkdownPipeline` can be modified with built-in as well as user developed extensions.
|
||||
|
||||
### Glossary of Relevant Types
|
||||
|
||||
The following is a table of some of the types relevant to parsing and mentioned in the related documentation. For an exhaustive list refer to API documentation (coming soon).
|
||||
|
||||
|Type|Description|
|
||||
|-|-|
|
||||
|`Markdown`|Static class with the entry point to the parsing algorithm via the `Parse(...)` method|
|
||||
|`MarkdownPipeline`|Configuration object for the parser, contains collections of block and inline parsers and registered extensions|
|
||||
|`MarkdownPipelineBuilder`|Responsible for constructing the `MarkdownPipeline`, used by client code to configure pipeline options and behaviors|
|
||||
|`IMarkdownExtension`|Interface for [Extensions](#extensions-imarkdownextension) which alter the behavior of the pipeline, this is the standard mechanism for extending Markdig|
|
||||
|`BlockParser`|Base type for an individual parsing component meant to identify `Block` elements in the markdown source|
|
||||
|`InlineParser`|Base type for an individual parsing component meant to identify `Inline` elements within a `Block`|
|
||||
|`Block`|A node in the AST representing a markdown block element, can either be a `ContainerBlock` or a `LeafBlock`|
|
||||
|`Inline`|A node in the AST representing a markdown inline element|
|
||||
|`MarkdownDocument`|The root node of the AST produced by the parser, derived from `ContainerBlock`|
|
||||
|`MarkdownObject`|The base type of all `Block` and `Inline` derived objects (as well as `HtmlAttributes`)|
|
||||
|
||||
### Simple Examples
|
||||
|
||||
*The following are simple examples of parsing to help get you started, see the following sections for an in-depth explanation of the different parts of Markdig's parsing mechanisms*
|
||||
|
||||
The `MarkdownPipeline` dictate how the parser will behave. The `Markdown.Parse(...)` method will construct a default pipeline if none is provided. A default pipeline will be CommonMark compliant but nothing else.
|
||||
|
||||
```csharp
|
||||
var markdownText = File.ReadAllText("sample.md");
|
||||
|
||||
// No pipeline provided means a default pipeline will be used
|
||||
var document = Markdown.Parse(markdownText);
|
||||
```
|
||||
|
||||
Pipelines can be created and configured manually, however this must be done using a `MarkdownPipelineBuilder` object, which then is configured through a fluent interface composed of extension methods.
|
||||
|
||||
```csharp
|
||||
var markdownText = File.ReadAllText("sample.md");
|
||||
|
||||
// Markdig's "UseAdvancedExtensions" option includes many common extensions beyond
|
||||
// CommonMark, such as citations, figures, footnotes, grid tables, mathematics
|
||||
// task lists, diagrams, and more.
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.Build();
|
||||
|
||||
var document = Markdown.Parse(markdownText, pipeline);
|
||||
```
|
||||
|
||||
Extensions can also be added individually:
|
||||
|
||||
```csharp
|
||||
var markdownText = File.ReadAllText("sample.md");
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseCitations()
|
||||
.UseFootnotes()
|
||||
.UseMyCustomExtension()
|
||||
.Build();
|
||||
|
||||
var document = Markdown.Parse(markdownText, pipeline);
|
||||
```
|
||||
|
||||
## Markdown.Parse and the MarkdownPipeline
|
||||
|
||||
As metioned in the [Introduction](#introduction), Markdig's parsing machinery involves two surface components: the `Markdown.Parse(...)` method, and the `MarkdownPipeline` type. The main parsing algorithm (not to be confused with individual `BlockParser` and `InlineParser` components) lives in the `Markdown.Parse(...)` static method. The `MarkdownPipeline` is responsible for configuring the behavior of the parser.
|
||||
|
||||
These two components are covered in further detail in the following sections.
|
||||
|
||||
### The MarkdownPipeline
|
||||
|
||||
The `MarkdownPipeline` is a sealed internal class which dictates what features the parsing algorithm has. The pipeline must be created by using a `MarkdownPipelineBuilder` as shown in the examples above.
|
||||
|
||||
The `MarkdownPipeline` holds configuration information and collections of extensions and parsers. Parsers fall into one of two categories:
|
||||
|
||||
* Block Parsers (`BlockParser`)
|
||||
* Inline Parsers (`InlineParser`)
|
||||
|
||||
Extensions are classes implementing `IMarkdownExtension` which are allowed to add to the list of parsers, or modify existing parsers and/or renderers. They are invoked to perform their mutations on the pipeline when the pipeline is built by the `MarkdownPipelineBuilder`.
|
||||
|
||||
Lastly, the `MarkdownPipeline` contains a few extra elements:
|
||||
|
||||
* A configuration setting determining whether or not trivial elements, referred to as *trivia*, (whitespace, extra heading characters, unescaped strings, etc) are to be tracked
|
||||
* A configuration setting determining whether or not nodes in the resultant abstract syntax tree will refer to their precise original locations in the source
|
||||
* An optional delegate which will be invoked when the document has been processed.
|
||||
* An optional `TextWriter` which will get debug logging from the parser
|
||||
|
||||
### The Markdown.Parse Method
|
||||
|
||||
`Markdown.Parse` is a static method which contains the overall parsing algorithm but not the actual parsing components, which instead are contained within the pipeline.
|
||||
|
||||
The `Markdown.Parse(...)` method takes a string containing raw markdown and returns a `MarkdownDocument`, which is the root node in the abstract syntax tree. The `Parse(...)` method optionally takes a pre-configured `MarkdownPipeline`, but if none is given will create a default pipeline which has minimal features.
|
||||
|
||||
Within the `Parse(...)` method, the following sequence of operations occur:
|
||||
|
||||
1. The block parsers contained in the pipeline are invoked on the raw markdown text, creating the initial tree of block elements
|
||||
2. If the pipeline is configured to track markdown trivia (trivial/non-contributing elements), the blocks are expanded to absorb neighboring trivia
|
||||
3. The inline parsers contained in the pipeline are now invoked on the blocks, populating the inline elements of the abstract syntax tree
|
||||
4. If a delegate has been configured for when the document has completed processing, it is now invoked
|
||||
5. The abstract syntax tree (`MarkdownDocument` object) is returned
|
||||
|
||||
## The Pipeline Builder and Extensions
|
||||
|
||||
The `MarkdownPipeline` determines the behavior and capabilities of the parser, and *extensions* added via the `MarkdownPipelineBuilder` determine the configuration of the pipeline.
|
||||
|
||||
This section discusses the pipeline builder and the concept of *extensions* in more detail.
|
||||
|
||||
### Extensions (IMarkdownExtension)
|
||||
|
||||
***Note**: This section discusses how to consume extensions by adding them to pipeline. For a discussion on how to implement an extension, refer to the [Extensions/Parsers](parsing-extensions.md) document.*
|
||||
|
||||
Extensions are the primary mechanism for modifying the parsers in the pipeline.
|
||||
|
||||
An extension is any class which implements the `IMarkdownExtension` interface found in [IMarkdownExtension.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/IMarkdownExtension.cs). This interface consists solely of two `Setup(...)` overloads, which both take a `MarkdownPipelineBuilder` as the first argument.
|
||||
|
||||
When the `MarkdownPipelineBuilder.Build()` method is invoked as the final stage in pipeline construction, the builder runs through the list of registered extensions in order and calls the `Setup(...)` method on each of them. The extension then has full access to modify both the parser collections themselves (by adding new parsers to it), or to find and modify existing parsers.
|
||||
|
||||
Because of this, *some* extensions may need to be ordered in relation to others, for instance if they modify a parser that gets added by a different extension. The `OrderedList<T>` class contains convenience methods to this end, which aid in finding other extensions by type and then being able to added an item before or after them.
|
||||
|
||||
### The MarkdownPipelineBuilder
|
||||
|
||||
Because the `MarkdownPipeline` is a sealed internal class, it cannot (and *should* not be attempted to) be created directly. Rather, the `MarkdownPipelineBuilder` manages the requisite construction of the pipeline after the configuration has been provided by the client code.
|
||||
|
||||
As discussed in the [section above](#the-markdownpipeline), the `MarkdownPipeline` primarily consists of a collection of block parsers and a collection of inline parsers, which are provided to the `Markdown.Parse(...)` method and thus determine its features and behavior. Both the collections and some of the parsers themselves are mutable, and the mechanism of mutation is the `Setup(...)` method of the `IMarkdownExtension` interface. This is covered in more detail in the section on [Extensions](#extensions-imarkdownextension).
|
||||
|
||||
#### The Fluent Interface
|
||||
|
||||
A collection of extension methods in the [MarkdownExtensions.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) source file provides a convenient fluent API for the configuration of the pipeline builder. This should be considered the standard way of configuring the builder.
|
||||
|
||||
##### Configuration Options
|
||||
|
||||
There are several extension methods which apply configurations to the builder which change settings in the pipeline outside of the use of typical extensions.
|
||||
|
||||
|Method|Description|
|
||||
|-|-|
|
||||
|`.ConfigureNewLine(...)`|Takes a string which will serve as the newline delimiter during parsing|
|
||||
|`.DisableHeadings()`|Disables the parsing of ATX and Setex headings|
|
||||
|`.DisableHtml()`|Disables the parsing of HTML elements|
|
||||
|`.EnableTrackTrivia()`|Enables the tracking of trivia (trivial elements like whitespace)|
|
||||
|`.UsePreciseSourceLocation()`|Maps syntax objects to their precise location in the original source, such as would be required for syntax highlighting|
|
||||
|
||||
```csharp
|
||||
var builder = new MarkdownPipelineBuilder()
|
||||
.ConfigureNewLine("\r\n")
|
||||
.DisableHeadings()
|
||||
.DisableHtml()
|
||||
.EnableTrackTrivia()
|
||||
.UsePreciseSourceLocation();
|
||||
|
||||
var pipeline = builder.Build();
|
||||
```
|
||||
|
||||
##### Adding Extensions
|
||||
|
||||
All extensions which ship with Markdig can be added through a dedicated fluent method, while user code which implements the `IMarkdownExtension` interface can be added with one of the `Use()` methods, or via a custom extension method implemented in the client code.
|
||||
|
||||
Refer to [MarkdownExtensions.cs](https://github.com/xoofx/markdig/blob/master/src/Markdig/MarkdownExtensions.cs) for a full list of extension methods:
|
||||
|
||||
```csharp
|
||||
var builder = new MarkdownPipelineBuilder()
|
||||
.UseFootnotes()
|
||||
.UseFigures();
|
||||
```
|
||||
|
||||
For custom/user-provided extensions, the `Use<TExtension>(...)` methods allow either a type to be directly added or an already constructed instance to be put into the extension container. Internally they will prevent two of the same type of extension from being added to the container.
|
||||
|
||||
```csharp
|
||||
public class MyExtension : IMarkdownExtension
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
// Only works if MyExtension has an empty constructor (aka new())
|
||||
var builder = new MarkdownPipelineBuilder()
|
||||
.Use<MyExtension>();
|
||||
```
|
||||
|
||||
Alternatively:
|
||||
|
||||
```csharp
|
||||
public class MyExtension : IMarkdownExtension
|
||||
{
|
||||
public MyExtension(object someConfigurationObject) { /* ... */ }
|
||||
// ...
|
||||
}
|
||||
|
||||
var instance = new MyExtension(configData);
|
||||
|
||||
var builder = new MarkdownPipelineBuilder()
|
||||
.Use(instance);
|
||||
```
|
||||
|
||||
##### Adding Extensions with the Configure Method
|
||||
|
||||
The `MarkdownPipelineBuilder` has one additional method for the configuration of extensions worth mentioning: the `Configure(...)` method, which takes a `string?` of `+` delimited tokens specifying which extensions should be dynamically configured. This is a convenience method for the configuration of pipelines whose extensions are only known at runtime.
|
||||
|
||||
Refer to [MarkdownExtensions.cs's `Configure(...)`](https://github.com/xoofx/markdig/blob/983187eace6ba02ee16d1443c387267ad6e78f58/src/Markdig/MarkdownExtensions.cs#L538) code for the full list of extensions.
|
||||
|
||||
|
||||
```csharp
|
||||
var builder = new MarkdownPipelineBuilder()
|
||||
.Configure("common+footnotes+figures");
|
||||
|
||||
var pipeline = builder.Build();
|
||||
```
|
||||
|
||||
#### Manual Configuration
|
||||
|
||||
Internally, the fluent interface wraps manual operations on the three primary collections:
|
||||
|
||||
* `MarkdownPipelineBuilder.BlockParsers` - this is an `OrderedList<BlockParser>` of the block parsers
|
||||
* `MarkdownPipelineBuilder.InlineParsers` - this is an `OrderedList<InlineParser>` of the inline element parsers
|
||||
* `MarkdownPipelineBuilder.Extensions` - this is an `OrderedList<IMarkdownExtension>` of the extensions
|
||||
|
||||
All three collections are `OrderedList<T>`, which is a collection type custom to Markdig which contains special methods for finding and inserting derived types. With the builder created, manual configuration can be performed by accessing these collections and their elements and modifying them as necessary.
|
||||
|
||||
***Warning**: be aware that it should not be necessary to directly modify either the `BlockParsers` or the `InlineParsers` collections directly during the pipeline configuration. Rather, these can and should be modified whenever possible through the `Setup(...)` method of extensions, which will be deferred until the pipeline is actually built and will allow for ordering such that operations dependent on other operations can be accounted for.*
|
||||
|
||||
## Block and Inline Parsers
|
||||
|
||||
Let's dive deeper into the parsing system. With a configured pipeline, the `Markdown.Parse` method will run through two two conceptual passes to produce the abstract syntax tree.
|
||||
|
||||
1. First, `BlockProcessor.ProcessLine` is called on the file's lines, one by one, trying to identify block elements in the source
|
||||
2. Next, an `InlineProcessor` is created or borrowed and run on each block to identify inline elements.
|
||||
|
||||
These two conceptual operations dictate Markdig's two types of parsers, both of which derive from `ParserBase<TProcessor>`.
|
||||
|
||||
Block parsers, derived from `BlockParser`, identify block elements from lines in the source text and push them onto the abstract syntax tree. Inline parsers, derived from `InlineParser`, identify inline elements from `LeafBlock` elements and push them into an attached container: the `ContainerInline? LeafBlock.Inline` property.
|
||||
|
||||
Both inline and block parsers are regex-free, and instead work on finding opening characters and then making fast read-only views into the source text.
|
||||
|
||||
### Block Parser
|
||||
|
||||
**(The contents of this section I am very unsure of, this is from my reading of the code but I could use some guidance here)**
|
||||
|
||||
**(Does `CanInterrupt` specifically refer to interrupting a paragraph block?)**
|
||||
|
||||
In order to be added to the parsing pipeline, all block parsers must be derived from `BlockParser`.
|
||||
|
||||
Internally, the main parsing algorithm will be stepping through the source text, using the `HasOpeningCharacter(char c)` method of the block parser collection to pre-identify parsers which *could* be opening a block at a given position in the text based on the active character. Thus any derived implementation needs to set the value of the `char[]? OpeningCharacter` property with the initial characters that might begin the block.
|
||||
|
||||
If a parser can potentially open a block at a place in the source text it should expect to have the `TryOpen(BlockProcessor processor)` method called. This is a virtual method that must be implemented on any derived class. The `BlockProcessor` argument is a reference to an object which stores the current state of parsing and the position in the source.
|
||||
|
||||
**(What are the rules concerning how the `BlockState` return type should work for `TryOpen`? I see examples returning `None`, `Continue`, `BreakDiscard`, `ContinueDiscard`. How does the return value change the algorithm behavior?)**
|
||||
|
||||
**(Should a new block always be pushed into `processor.NewBlocks` in the `TryOpen` method?)**
|
||||
|
||||
As the main parsing algorithm moves forward, it will then call `TryContinue(...)` on blocks that were opened in `TryOpen(..)`.
|
||||
|
||||
**(Is this where/how you close a block? Is there anything that needs to be done to perform that beyond `block.UpdateSpanEnd` and returning `BlockState.Break`?)**
|
||||
|
||||
|
||||
### Inline Parsers
|
||||
|
||||
Inline parsers extract inline markdown elements from the source, but their starting point is the text of each individual `LeafBlock` produced by the block parsing process. To understand the role of each inline parser it is necessary to first understand the inline parsing process as a whole.
|
||||
|
||||
#### The Inline Parsing Process
|
||||
|
||||
After the block parsing process has occurred, the abstract syntax tree of the document has been populated only with block elements, starting from the root `MarkdownDocument` node and ending with the individual `LeafBlock` derived block elements, most of which will be `ParagraphBlocks`, but also include things like `CodeBlocks`, `HeadingBlocks`, `FigureCaptions`, and so on.
|
||||
|
||||
At this point, the parsing machinery will iterate through each `LeafBlock` one by one, creating and assigning its `LeafBlock.Inline` property with an empty `ContainerInline`, and then sweeping through the `LeafBlock`'s text running the inline parsers. This occurs by the following process:
|
||||
|
||||
Starting at the first character of the text it will run through all of its `InlineParser` objects which have that character as a possible opening character for the type of inline they extract. The parsers will run in order (as such ordering is the *only* way which conflicts between parsers are resolved, and thus is important to the overall behavior of the parsing system) and the `Match(...)` method will be called on each candidate parser, in order, until one of them returns `true`.
|
||||
|
||||
The `Match(...)` method will be passed a slice of the text beginning at the *specific character* being processed and running until the end of the `LeafBlock`'s complete text. If the parser can create an `Inline` element it will do so and return `true`, otherwise it will return `false`. The parser will store the created `Inline` object in the processor's `InlineProcessor.Inline` property, which as passed into the `Match(...)` method as an argument. The parser will also advance the start of the working `StringSlice` by the characters consumed in the match.
|
||||
|
||||
* If the parser has created an inline element and returned `true`, that element is pushed into the deepest open `ContainerInline`
|
||||
* If `false` was returned, a default `LiteralInlineParser` will run instead:
|
||||
* If the `InlineProcessor.Inline` property already has an existing `LiteralInline` in it, these characters will be added to the existing `LiteralInline`, effectively growing it
|
||||
* If no `LiteralInline` exists in the `InlineProcessor.Inline` property, a new one will be created containing the consumed characters and pushed into the deepest open `ContainerInline`
|
||||
|
||||
After that, the working text of the `LeafBlock` has been conceptually shortened by the advancing start of the working `StringSlice`, moving the starting character forward. If there is still text remaining, the process repeats from the new starting character until all of the text is consumed.
|
||||
|
||||
At this point, when all of the source text from the `LeafBlock` has been consumed, a post-processing step occurs. `InlineParser` objects in the pipeline which also implement `IPostInlineProcessor` are invoked on the `LeafBlock`'s root `ContainerInline`. This, for example, is the mechanism by which the unstructured output of the `EmphasisInlineParser` is then restructured into cleanly nested `EmphasisInline` and `LiteralInline` elements.
|
||||
|
||||
|
||||
#### Responsibilities of an Inline Parser
|
||||
|
||||
Like the block parsers, an inline parser must provide an array of opening characters with the `char[]? OpeningCharacter` property.
|
||||
|
||||
However, inline parsers only require one other method, the `Match(InlineProcessor processor, ref StringSlice slice)` method, which is expected to determine if a match for the related inline is located at the starting character of the slice.
|
||||
|
||||
Within the `Match` method a parser should:
|
||||
|
||||
1. Determine if a match begins at the starting character of the `slice` argument
|
||||
2. If no match exists, the method should return `false` and not advance the `Start` property of the `slice` argument
|
||||
3. If a match does exist, perform the following actions:
|
||||
* Instantiate the appropriate `Inline` derived class and assign it to the processor argument with `processor.Inline = myInlineObject`
|
||||
* Advance the `Start` property of the `slice` argument by the number of characters contained in the match, for example by using the `NextChar()`, `SkipChar()`, or other helper methods of the `StringSlice` class
|
||||
* Return `true`
|
||||
|
||||
While parsing, the `InlineProcessor` performing the processing, which is available to the `Match` function through the `processor` argument, contains a number of properties which can be used to access the current state of parsing. For example, the `processor.Inline` property is the mechanism for returning a new inline element, but before assignment it contains the last created inline, which in turn can be accessed for its parents.
|
||||
|
||||
Additionally, in the case of inlines which can be expected to contain other inlines, a possible strategy is to inject an inline element derived from `DelimiterInline` when the opening delimiter is detected, then to replace the opening delimiter with the final desired element when the closing delimiter is found. This is the strategy used by the `LinkInlineParser`, for example. In such cases the tools described in the next section, such as the `ReplaceBy` method, can be used. Note that if this method is used the post-processing should be invoked on the `InlineProcessor` in order to finalize any emphasis elements. For example, in the following code adapted from the `LinkInlineParser`:
|
||||
|
||||
```csharp
|
||||
var parent = processor.Inline?.FirstParentOfType<MyDelimiterInline>();
|
||||
if (parent is null) return;
|
||||
|
||||
var myInline = new MySpecialInline { /* set span and other parameters here */ };
|
||||
|
||||
// Replace the delimiter inline with the final inline type, adopting all of its children
|
||||
parent.ReplaceBy(myInline);
|
||||
|
||||
// Notifies processor as we are creating an inline locally
|
||||
processor.Inline = myInline;
|
||||
|
||||
// Process emphasis delimiters
|
||||
processor.PostProcessInlines(0, myInline, null, false);
|
||||
```
|
||||
|
||||
#### Inline Post-Processing
|
||||
|
||||
The purpose of post-processing inlines is typically to re-structure inline elements after the initial parsing is complete and the entire structure of the inline elements within a parent container is now available in a way it was not during the parsing process. Generally this consists of removing, replacing, and re-ordering `Inline` elements.
|
||||
|
||||
To this end, the `Inline` abstract base class contains several helper methods intended to allow manipulation of inline elements during the post-processing phase.
|
||||
|
||||
|Method|Purpose|
|
||||
|-|-|
|
||||
|`InsertAfter(...)`|Takes a new inline as an argument and inserts it into the same parent container after this instance|
|
||||
|`InsertBefore(...)`|Takes a new inline as an argument and inserts it into the same parent container before this instance|
|
||||
|`Remove()`|Removes this inline from its parent container|
|
||||
|`ReplaceBy(...)`|Removes this instance and replaces it with a new inline specified in the argument. Has an option to move all of the original inline's children into the new inline.|
|
||||
|
||||
Additionally, the `PreviousSibling` and `NextSibling` properties can be used to determine the siblings of an inline element within its parent container. The `FirstParentOfType<T>()` method can be used to search for a parent element, which is often useful when searching for `DelimiterInline` derived elements, which are implemented as containers.
|
||||
|
||||
134
site/readme.md
Normal file
134
site/readme.md
Normal file
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: Home
|
||||
layout: simple
|
||||
og_type: website
|
||||
---
|
||||
|
||||
<section class="text-center py-5">
|
||||
<div class="container">
|
||||
<img src="{{site.basepath}}/img/markdig.svg" alt="Markdig — Fast, powerful Markdown processor for .NET" class="img-fluid" style="width: min(100%, 12rem); height: auto;">
|
||||
<h1 class="display-4 mt-4">Markdig</h1>
|
||||
<p class="lead mt-3 mb-4">
|
||||
A fast, powerful, <strong>CommonMark compliant</strong>, extensible <strong>Markdown processor</strong> for .NET.
|
||||
</p>
|
||||
<div class="d-flex justify-content-center gap-3 mt-4 flex-wrap">
|
||||
<a href="{{site.basepath}}/docs/getting-started/" class="btn btn-primary btn-lg"><i class="bi bi-rocket-takeoff"></i> Get started</a>
|
||||
<a href="{{site.basepath}}/docs/" class="btn btn-outline-secondary btn-lg"><i class="bi bi-book"></i> Documentation</a>
|
||||
<a href="https://github.com/xoofx/markdig" class="btn btn-info btn-lg"><i class="bi bi-github"></i> GitHub</a>
|
||||
</div>
|
||||
<div class="mt-4 text-start mx-auto" style="max-width: 48rem;">
|
||||
<pre class="language-shell-session"><code>dotnet add package Markdig</code></pre>
|
||||
<p class="text-center text-secondary mt-2" style="font-size: 0.85rem;">Available on <a href="https://www.nuget.org/packages/Markdig/" class="text-secondary">NuGet</a> — .NET Standard 2.0+</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Feature cards -->
|
||||
<section class="container my-5">
|
||||
<div class="row row-cols-1 row-cols-lg-2 gx-5 gy-4">
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header display-6"><i class="bi bi-lightning-charge lunet-feature-icon lunet-icon--controls"></i> Blazing fast</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Regex-free parser and HTML renderer with minimal GC pressure. <strong>20% faster than the reference C implementation</strong> (cmark) and 100× faster than regex-based processors.
|
||||
</p>
|
||||
|
||||
[Getting started](docs/getting-started.md) · [Usage guide](docs/usage.md)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header display-6"><i class="bi bi-check-circle lunet-feature-icon lunet-icon--themes"></i> CommonMark compliant</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Passes <strong>600+ tests</strong> from the latest CommonMark specification (0.31.2). Full support for all core Markdown constructs including GFM fenced code blocks.
|
||||
</p>
|
||||
|
||||
[CommonMark syntax](docs/commonmark.md)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header display-6"><i class="bi bi-puzzle lunet-feature-icon lunet-icon--data"></i> 20+ extensions</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Tables, task lists, math, diagrams, footnotes, emoji, alert blocks, abbreviations, and more — all included out of the box. Enable them individually or all at once.
|
||||
</p>
|
||||
|
||||
[Extensions](docs/extensions/readme.md)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header display-6"><i class="bi bi-gear lunet-feature-icon lunet-icon--editing"></i> Fully extensible</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Pluggable architecture — even core parsing is customizable. Create your own block parsers, inline parsers, and renderers with a clean API.
|
||||
</p>
|
||||
|
||||
[Developer guide](docs/advanced/readme.md)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header display-6"><i class="bi bi-tree lunet-feature-icon lunet-icon--chrome"></i> Rich AST</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Full abstract syntax tree with precise source locations. Power syntax highlighters, editors, and document analysis tools with the complete <code>Descendants</code> API.
|
||||
</p>
|
||||
|
||||
[AST guide](docs/advanced/ast.md)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<div class="card-header display-6"><i class="bi bi-arrow-repeat lunet-feature-icon lunet-icon--binding"></i> Roundtrip support</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
Parse with trivia tracking for lossless parse → render roundtrips. Modify Markdown documents programmatically without introducing unwanted changes.
|
||||
</p>
|
||||
|
||||
[Pipeline & roundtrip](docs/advanced/pipeline.md)
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container my-5">
|
||||
<div class="card">
|
||||
<div class="card-header display-6">
|
||||
<i class="bi bi-code-slash lunet-feature-icon lunet-icon--lists"></i> Quick example
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
```csharp
|
||||
using Markdig;
|
||||
|
||||
// Simple CommonMark conversion
|
||||
var result = Markdown.ToHtml("This is a text with some *emphasis*");
|
||||
// => "<p>This is a text with some <em>emphasis</em></p>\n"
|
||||
|
||||
// Enable all advanced extensions
|
||||
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
|
||||
var result = Markdown.ToHtml("This is a ~~strikethrough~~", pipeline);
|
||||
// => "<p>This is a <del>strikethrough</del></p>\n"
|
||||
```
|
||||
|
||||
For more examples, see the [Getting started](docs/getting-started.md) guide.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Reference in New Issue
Block a user