TableOfContents from the heading blocks in a parsed Markdown document." /> TableOfContents from the heading blocks in a parsed Markdown document." /> TableOfContents from the heading blocks in a parsed Markdown document." />
Class Sealed
public sealed class TocGenerator

Namespace: Moka.Docs.Parsing.Markdown

Generates a TableOfContents from the heading blocks in a parsed Markdown document.

Methods

NameDescription
Generate(MarkdownDocument document) Generates a table of contents from a Markdig document.
GenerateAnchorId(string text) static Generates a URL-safe anchor ID from heading text. Follows GitHub-style anchor generation.

Generate(MarkdownDocument document)

TableOfContents TocGenerator.Generate(MarkdownDocument document)

Generates a table of contents from a Markdig document.

Parameters

NameTypeDescription
documentMarkdownDocumentThe parsed Markdig document.

Returns: A TableOfContents with nested entries.

GenerateAnchorId(string text)

string TocGenerator.GenerateAnchorId(string text)

Generates a URL-safe anchor ID from heading text. Follows GitHub-style anchor generation.

View Source
/// <summary>
///     Generates a <see cref = "TableOfContents"/> from the heading blocks in a parsed Markdown document.
/// </summary>
public sealed class TocGenerator
{
    /// <summary>
    ///     Generates a table of contents from a Markdig document.
    /// </summary>
    /// <param name = "document">The parsed Markdig document.</param>
    /// <returns>A <see cref = "TableOfContents"/> with nested entries.</returns>
    public TableOfContents Generate(MarkdownDocument document)
    {
        var flatEntries = new List<TocEntry>();
        foreach (HeadingBlock block in document.DescendantsOfType<HeadingBlock>())
        {
            string text = ExtractHeadingText(block);
            HtmlAttributes? attributes = block.TryGetAttributes();
            string id = attributes?.Id ?? GenerateAnchorId(text);
            flatEntries.Add(new TocEntry { Level = block.Level, Text = text, Id = id });
        }

        if (flatEntries.Count == 0)
        {
            return TableOfContents.Empty;
        }

        List<TocEntry> nested = BuildNestedEntries(flatEntries);
        return new TableOfContents
        {
            Entries = nested
        };
    }

#region Private Helpers
    private static string ExtractHeadingText(HeadingBlock heading)
    {
        if (heading.Inline is null)
        {
            return "";
        }

        var text = new StringBuilder();
        foreach (Inline inline in heading.Inline)
        {
            if (inline is LiteralInline literal)
            {
                text.Append(literal.Content);
            }
            else if (inline is CodeInline code)
            {
                text.Append(code.Content);
            }
            else if (inline is EmphasisInline emphasis)
            {
                foreach (Inline child in emphasis)
                {
                    if (child is LiteralInline lit)
                    {
                        text.Append(lit.Content);
                    }
                }
            }
        }

        return text.ToString();
    }

    /// <summary>
    ///     Generates a URL-safe anchor ID from heading text.
    ///     Follows GitHub-style anchor generation.
    /// </summary>
    /// <summary>
    ///     Generates a URL-safe anchor ID from heading text.
    ///     Follows GitHub-style anchor generation.
    /// </summary>
    public static string GenerateAnchorId(string text)
    {
        var sb = new StringBuilder(text.Length);
        foreach (char ch in text)
        {
            if (char.IsLetterOrDigit(ch))
            {
                sb.Append(char.ToLowerInvariant(ch));
            }
            else if (ch is ' ' or '-')
            {
                sb.Append('-');
            }
        }

        // Other characters are stripped
        // Collapse multiple dashes
        string result = sb.ToString();
        while (result.Contains("--"))
        {
            result = result.Replace("--", "-");
        }

        return result.Trim('-');
    }

    private static List<TocEntry> BuildNestedEntries(List<TocEntry> flat)
    {
        var root = new List<TocEntry>();
        var stack = new Stack<TocEntry>();
        foreach (TocEntry entry in flat)
        {
            // Pop entries from stack that are at the same level or deeper
            while (stack.Count > 0 && stack.Peek().Level >= entry.Level)
            {
                stack.Pop();
            }

            if (stack.Count == 0)
            {
                root.Add(entry);
            }
            else
            {
                stack.Peek().Children.Add(entry);
            }

            stack.Push(entry);
        }

        return root;
    }
#endregion
}
Was this page helpful?