Beskar.Markdown 1.0.4

dotnet add package Beskar.Markdown --version 1.0.4
                    
NuGet\Install-Package Beskar.Markdown -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Beskar.Markdown" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Beskar.Markdown" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="Beskar.Markdown" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Beskar.Markdown --version 1.0.4
                    
#r "nuget: Beskar.Markdown, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Beskar.Markdown@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Beskar.Markdown&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=Beskar.Markdown&version=1.0.4
                    
Install as a Cake Tool

Beskar.Markdown

NuGet

Beskar.Markdown is a high-performance, low-allocation Markdown parser and HTML renderer for .NET. It is built from the ground up to leverage modern C# features like Span<T>, ReadOnlySequence<T>, and efficient memory management to provide a blazing-fast and lean experience.

Table of Contents


Disclaimer: This is just my fun project i do on the side. I do not want to replace any of the major markdown solutions for csharp, neither could I do that even if i wanted. It's just for me internally to use.

Why use this library?

  • Performance First: Designed for scenarios where every microsecond and every byte counts.
  • Low Allocation: Minimizes pressure on the Garbage Collector by using stack-allocated buffers and pooling where possible.
  • Modern C#: Built for modern .NET, taking advantage of the latest language and runtime optimizations.
  • Simplicity: A clean, easy-to-use API that gets the job done without unnecessary complexity.
  • Tests: 1,009 passing tests (652 CommonMark Spec Tests)

Motivation

I created this library because I love to learn about deep-level code topics. Building a Markdown parser is a fantastic way to explore memory layout optimization, and the intricacies of text processing at scale. It's a passion project aimed at achieving technical excellence and pushing the boundaries of what's possible in .NET.

Getting Started

Getting started with Beskar.Markdown is easy. Call the static ToHtml method:

using Beskar.Markdown;

string markdown = "# Hello World\nThis is a **bold** statement.";
string html = BeMarkdown.ToHtml(markdown);

Console.WriteLine(html);
// Output: <h1>Hello World</h1><p>This is a <strong>bold</strong> statement.</p>

Features

Main Features

  • Fast: Beskar.Markdown is designed to be fast and leaner than existing solutions.
  • Modern: Built for modern .NET, taking advantage of the latest language and runtime optimizations.
  • Easy to Use: A clean, intuitive API that makes Markdown processing straightforward.
  • Extensible: Easily add support for new Markdown features or extensions.
  • Frontmatter: Built-in support for parsing document frontmatter.
  • Sluggable Headers: Automatically generate id attributes for headers.
  • Advanced: Supports contextual rendering
  • Tests: 1,009 passing tests (652 CommonMark Spec Tests)

Currently Supported Blocks & Inlines

  • Blocks:
    • Headers (ATX & Setext)
    • Paragraphs
    • Blockquotes
    • Lists (Ordered & Unordered)
    • Task list items (like GitHub)
    • Fenced Code Blocks
    • Indented Code Blocks
    • Thematic Breaks (Horizontal Rules)
    • HTML Blocks
    • GitHub like Tables
    • Full reference links
  • Inlines:
    • Emphasis (Bold, Italic)
    • Links
    • Autolinks
    • Inline Code
    • Inline HTML
    • Line Breaks
    • Strikethrough
    • Images

Future Plans

  • In memory assembly baking

⚠️ Security Warning

Important: Beskar.Markdown does not perform HTML sanitization by default. If you are processing Markdown input from untrusted users, you MUST sanitize the resulting HTML to prevent Cross-Site Scripting (XSS) attacks.

Example:

var rawHtml = BeMarkdown.ToHtml(userContent);
var sanitizer = new HtmlSanitizer();
var safeHtml = sanitizer.Sanitize(rawHtml);

If your sanitizer supports spans, you can use the following to prevent double allocation unlike above:

var options = RenderOptions.HtmlDefault;
options.SanitizerFunc = (span) => HtmlSanitizer.Sanitize(span);

var safeHtml = BeMarkdown.ToHtml(userContent, renderOptions: options);

Frontmatter Parsing

Beskar.Markdown can automatically parse YAML-like frontmatter into key-value pairs. To enable this, use the WithFrontMatter() option and the Parse method:

var options = MarkdownOptionBuilder.Create()
    .WithFrontMatter()
    .Build();

var markdown = """
               ---
               title: My Awesome Page
               author: Marvin
               ---
               # Content
               """;

var result = BeMarkdown.Parse(markdown, options);

Console.WriteLine(result.Context.FrontMatter["title"]); // My Awesome Page
Console.WriteLine(result.Html); // <h1>Content</h1>

Sluggable Headers

Beskar.Markdown can automatically generate id attributes for headers based on their text content. To enable this, use the WithSluggableHeaders() option:

var options = MarkdownOptionBuilder.Create()
    .WithSluggableHeaders()
    .Build();

var markdown = "# My Header Text";
var html = BeMarkdown.ToHtml(markdown, options);

Console.WriteLine(html);
// Output: <h1 id="my-header-text">My Header Text</h1>

If you need a table of contents or anchor list, use Parse instead of ToHtml. The returned context exposes headers in document order through Context.Headers. Each item contains the generated slug, the plain text, and the heading level:

var markdown = """
               # My Header Text
               ## Details
               """;

var result = BeMarkdown.Parse(markdown, options);

foreach (var header in result.Context.Headers)
{
    Console.WriteLine($"{header.Level}: {header.PlainText} -> #{header.Slug}");
}

// Output:
// 1: My Header Text -> #my-header-text
// 2: Details -> #details

Code Block Intercept

You can intercept the rendering of code blocks (both fenced and indented) to provide your own HTML output. This is particularly useful for server-side syntax highlighting (e.g., using libraries like ColorCode or calling a highlighting service).

To use this, implement the ICodeBlockRenderer interface and register it via WithCodeBlockRenderer:

public sealed class MySyntaxHighlighter : ICodeBlockRenderer
{
    public bool TryRender<TData>(
        MarkdownContext<TData> context,
        ref TextWriterIndentSlim writer,
        ReadOnlySpan<char> code,
        ReadOnlySpan<char> language)
    {
        // Intercept only C# blocks
        if (language.Equals("csharp", StringComparison.OrdinalIgnoreCase))
        {
            writer.Write("<div class=\"highlight\">");
            writer.WriteHtmlDecodedAndEncoded(language);
            writer.Write(":");
            
            // Your custom highlighting logic here
            writer.Write(code); 
            
            writer.Write("</div>");
            return true; // Successfully intercepted
        }

        return false; // Fallback to default renderer
    }
}

// Usage:
var options = MarkdownOptionBuilder.Create()
    .WithCodeBlockRenderer(new MySyntaxHighlighter())
    .Build();

var html = BeMarkdown.ToHtml("```csharp\nvar x = 1;\n```", options);

Note: When writing the language span to the output, always use writer.WriteHtmlDecodedAndEncoded(language) to ensure proper HTML encoding.

Simple custom markdown extensions

Simple inline extension

This example inline extension adds a random emoji if you use .RandomEmoji.:

var options = MarkdownOptionBuilder.Create()
   .WithExtension(new EmojiInlineExtension())
   .Build();

const string markdown = 
   """
   Hello, World! .RandomEmoji.
   """;
var result = BeMarkdown.ToHtml(markdown, options);
Console.WriteLine(result); // <p>Hello, World! <span class="emoji">💻</span></p>

Implementation:

public sealed class EmojiInlineExtension : BaseInlineExtension
{
   private const int _targetTypeValue = BeMarkdown.BuiltInNodeTypeValueOffset + 4;
   private static readonly ImmutableArray<string> _emojis = ImmutableArray.CreateRange([
      "😀", "🎉", "🚀", "🌟", "🔥", "🐱", "🍕", "💻", "☕"]);

   public EmojiInlineExtension()
   {
      Parsers = [new EmojiInlineParser()];
      Renderers = [new HtmlEmojiInlineRenderer()];
   }

   private sealed class HtmlEmojiInlineRenderer : INodeRenderer
   {
      public int TargetTypeValue => _targetTypeValue;

      public void Render<TData>(
         MarkdownContext<TData> context,
         ReadOnlySpan<char> rawText, 
         ref TextWriterIndentSlim writer, 
         in MarkdownNode current, 
         ReadOnlySpan<MarkdownNode> nodes,
         RenderOptions options)
      {
         writer.Write("<span class=\"emoji\">");
         writer.Write(_emojis[Random.Shared.Next(0, _emojis.Length)]);
         writer.Write("</span>");
      }
   }
   
   private sealed class EmojiInlineParser : IInlineParser
   {
      private const string _identifier = ".RandomEmoji.";
      
      public int Priority => 8_000;
      public int SupportedTypeValue => _targetTypeValue;

      public char TriggerChar => '.';
      public char TriggerAltChar => '.';

      public bool TryMatch<TData>(
         ref InlineState<TData> state, 
         int parentIndex, 
         ref BufferWriter<MarkdownNode> writer, 
         scoped ref InlineParser<TData> parser,
         ParserOptions options)
      {
         if (state.RemainingText.Length < _identifier.Length) 
            return false;
         
         if (!state.RemainingText.StartsWith(_identifier))
            return false;

         var nodeIndex = writer.WrittenSpan.Length;
         writer.Add(new MarkdownNode()
         {
            Type = (NodeType)SupportedTypeValue,
            TextSpan = new TextSpan(state.GlobalOffset, _identifier.Length),
            
            NextSiblingIndex = -1,
            FirstChildIndex = -1,
            LastChildIndex = -1
         });
         
         parser.LinkInlineNode(ref writer, parentIndex, nodeIndex);
         state.Advance(_identifier.Length);
         return true;
      }
   }
}

Simple block extension

This example block extension adds a basic parsing for a div that is red:

var options = MarkdownOptionBuilder.Create()
   .WithExtension(new RedBlockExtension())
   .WithMaxBlockDepth(16)
   .Build();

const string markdown = 
   """
   +red block+
   inside of `code` inline
   red
   > blockquote

   """;
var result = BeMarkdown.ToHtml(markdown, options);
Console.WriteLine(result); // <div class="red-block"><p>inside of <code>code</code> inline\nred</p><blockquote><p>blockquote</p></blockquote></div>

Implementation:

public sealed class RedBlockExtension : BaseBlockExtension
{
   private const int _targetTypeValue = BeMarkdown.BuiltInNodeTypeValueOffset + 5;
   
   public RedBlockExtension()
   {
      Parsers = [new RedBlockParser()];
      Renderers = [new HtmlRedBlockRenderer()];
   }

   private sealed class HtmlRedBlockRenderer : INodeRenderer
   {
      public int TargetTypeValue => _targetTypeValue;

      public void Render<TData>(
         MarkdownContext<TData> context,
         ReadOnlySpan<char> rawText, 
         ref TextWriterIndentSlim writer, 
         in MarkdownNode current, 
         ReadOnlySpan<MarkdownNode> nodes,
         RenderOptions options)
      {
         writer.Write("<div class=\"red-block\">");
         current.RenderChildren(context, rawText, nodes, ref writer, options);
         writer.Write("</div>");
      }
   }

   private sealed class RedBlockParser : IBlockParser
   {
      private const string _identifier = "+red block+";
      
      public int Priority => 10; // low priority
      public int SupportedTypeValue => _targetTypeValue;
      
      public int TryMatch<TData>(ref LineState<TData> state, int parentIndex, ref BufferWriter<MarkdownNode> writer)
      {
         if (state.IsBlank || state.LeadingSpaces > 0)
         {
            return -1;
         }
         
         if (state.FirstChar != '+' && state.RawLine.Length < _identifier.Length)
         {
            return -1;
         }
         
         if (!state.RawLine.StartsWith(_identifier))
         {
            return -1;
         }
         
         var nodeIndex = writer.WrittenSpan.Length;
         writer.Add(new MarkdownNode()
         {
            Type = (NodeType)SupportedTypeValue,
            TextSpan = new TextSpan(-1, 0),
            
            FirstChildIndex = -1,
            NextSiblingIndex = -1,
            LastChildIndex = -1
         });

         state.ConsumeRest();
         return nodeIndex;
      }

      public bool CanContinue<TData>(ref MarkdownNode node, ref LineState<TData> state, ref BufferWriter<MarkdownNode> writer)
      {
         // simple example only an empty line can stop the block
         if (state.IsBlank)
         {
            return false;
         }
         
         if (node.TextSpan.Start == -1)
         {
            node.TextSpan = new TextSpan(state.GlobalOffset, state.RawLine.Length);
         }
         else
         {
            var newLength = (state.GlobalOffset - node.TextSpan.Start) + state.RawLine.Length;
            node.TextSpan = node.TextSpan with { Length = newLength };
         }

         return true;
      }
   }
}

Benchmark Results

Beskar.Markdown is designed to be fast and leaner than existing solutions. Here is a comparison with some other libraries (especially when it comes to memory usage):

Method Categories Mean Rank Gen0 Gen1 Gen2 Allocated
Markdig Bigger 222.117 us 2 - - - 37256 B
Beskar.Markdown Bigger 95.211 us 1 - - - 5288 B
CommonMark.Net Bigger 86.785 us 1 - - - 66024 B
MarkdownSharp Bigger 429.370 us 3 - - - 273996 B
Markdig Full Spec 1,919.994 us 3 125.0000 125.0000 125.0000 2077013 B
Beskar.Markdown Full Spec 1,302.109 us 1 31.2500 31.2500 31.2500 437967 B
CommonMark.Net Full Spec 1,577.537 us 2 125.0000 125.0000 125.0000 3153930 B
MarkdownSharp Full Spec 346,914.621 us 4 1656.2500 1625.0000 1468.7500 15909676 B
Markdig Small 5.425 us 3 - - - 1144 B
Beskar.Markdown Small 2.736 us 2 - - - 504 B
CommonMark.Net Small 1.768 us 1 - - - 11488 B
MarkdownSharp Small 5.056 us 3 - - - 2752 B
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 98 5/26/2026
1.0.3 77 5/19/2026
1.0.2 79 5/18/2026
1.0.1 75 5/16/2026
1.0.0 75 5/16/2026