The syntax tree

edit

Parse a document

MarkdownConverter.Parse returns a MarkdownDocument, the root of the syntax tree. Pass the pipeline you render with, so that the tree contains the elements of its extensions. Descendants<T>() finds the elements of a type anywhere in the tree:

var document = MarkdownConverter.Parse("# Title\n\nSee [the docs](https://example.com).");

foreach (var heading in document.Descendants<HeadingBlock>())
{
    Assert.Equal(1, heading.Level);
    Assert.Equal(0, heading.Line);
}

foreach (var link in document.Descendants<LinkInline>())
{
    Assert.Equal("https://example.com", link.Url);
}

Descendants() returns all the blocks and inlines, in document order.

Blocks and inlines

The tree has two kinds of elements, both derived from MarkdownObject:

MarkdownElementUseful members
The documentMarkdownDocumentLineCount, the blocks
# Title, or a line underlined with ===HeadingBlockLevel, IsSetext
A paragraphParagraphBlockInline
- item, 1. itemListBlock of ListItemBlockIsOrdered, BulletType, OrderedStart, IsLoose
> quoteQuoteBlockThe blocks of the quote
A fenced code blockFencedCodeBlockInfo (the language), Arguments, Lines
An indented code blockCodeBlockLines
<div>HtmlBlockType, Lines
***, ---ThematicBreakBlock
[label]: urlLinkReferenceDefinitionLabel, Url, Title
TextLiteralInlineContent; ToString() returns the text
*a*, **a**EmphasisInlineDelimiterChar, DelimiterCount (2 for strong)
[a](url), ![a](url)LinkInlineUrl, Title, IsImage, Label
<https://…>AutolinkInlineUrl, IsEmail
`code`CodeInlineContent
<b>HtmlInlineTag
&amp;HtmlEntityInlineOriginal, Transcoded
A line breakLineBreakInlineIsHard

Each extension adds its own elements, in its namespace: Table for tables, Footnote, TaskList, AlertBlock, YamlFrontMatterBlock… See Read a table from the tree.

Walk the tree

A ContainerBlock is a list of its child blocks: index it, count it or enumerate it. Every block has a Parent. Inlines are linked: a ContainerInline has a FirstChild and a LastChild, and each inline has a Parent, a PreviousSibling and a NextSibling:

var document = MarkdownConverter.Parse("- First\n- Second *item*");

// A container block is a list of blocks
var list = (ListBlock)document[0];
Assert.False(list.IsOrdered);
Assert.HasCount(2, list);

// A leaf block, such as a paragraph, holds the inlines of its text
var item = (ListItemBlock)list[1];
var paragraph = (ParagraphBlock)item[0];
Assert.Same(item, paragraph.Parent);

// Inlines are linked to their parent and their siblings
var text = (LiteralInline)paragraph.Inline!.FirstChild!;
Assert.Equal("Second ", text.ToString());
var emphasis = (EmphasisInline)text.NextSibling!;
Assert.Equal("item", emphasis.FirstChild!.ToString());

Get the text of an element

The text of a heading or a paragraph is split into inlines. To get it without the markup, concatenate its LiteralInline elements. For instance, to build a table of contents:

var document = MarkdownConverter.Parse("# Guide\n\n## Install *the* package\n\nText\n\n## Use it");

var outline = new List<string>();
foreach (var heading in document.Descendants<HeadingBlock>())
{
    // The text of a heading is in its inlines: concatenate the literals to drop the markup
    var text = string.Concat(heading.Inline!.Descendants<LiteralInline>().Select(literal => literal.ToString()));
    outline.Add(new string(' ', (heading.Level - 1) * 2) + text);
}

Assert.Equal("Guide\n  Install the package\n  Use it", string.Join('\n', outline));

The text of code spans (CodeInline) and of the other leaf inlines is in their own properties. To render an element as plain text, see Plain text and other formats.

Attach data to elements

SetData and GetData attach your own values to any element of the tree, for instance the results of an analysis. A DataKey<T> gives typed access:

var sectionNumberKey = new DataKey<int>();

var document = MarkdownConverter.Parse("# Install\n\nText\n\n# Configure");
var number = 0;
foreach (var heading in document.Descendants<HeadingBlock>())
{
    number++;
    heading.SetData(sectionNumberKey, number);
}

var configure = document.Descendants<HeadingBlock>().Last();
Assert.Equal(2, configure.GetData(sectionNumberKey));
Assert.False(document[1].TryGetData(sectionNumberKey, out _)); // Nothing on the paragraph

SetData(key, value) and GetData(key) also accept any object as the key. TryGetData tells whether a value is set, and RemoveData removes it. The renderers do not write your data.

Debug the tree

DumpTo writes the inlines of an element, one per line, indented by their depth:

var document = MarkdownConverter.Parse("Some *text* here.");
var paragraph = (ParagraphBlock)document[0];

// Writes the tree of the inlines, to understand what the parser produced
using var writer = new StringWriter();
paragraph.Inline!.DumpTo(writer);

// One line per inline, indented by its depth
var lines = writer.ToString().ReplaceLineEndings("\n").Split('\n');
Assert.Equal("  -> LiteralInline = Some ", lines[1]);
Assert.StartsWith("  -> EmphasisInline", lines[2]);
Assert.Equal("    -> LiteralInline = text", lines[3]);

To change the tree, see Editing documents. To know where each element is in the source, see Source positions.