{"id":50556958,"url":"https://github.com/marvindrude/beskar.markdown","last_synced_at":"2026-06-04T08:01:19.320Z","repository":{"id":356810561,"uuid":"1227495199","full_name":"MarvinDrude/Beskar.Markdown","owner":"MarvinDrude","description":"Simple, low-memory, extensible markdown parser and html renderer.","archived":false,"fork":false,"pushed_at":"2026-05-26T21:59:22.000Z","size":364,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-26T23:24:49.730Z","etag":null,"topics":["allocation","csharp","markdown","markdown-to-html","performance","stackalloc"],"latest_commit_sha":null,"homepage":"https://marvindrude.com/","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MarvinDrude.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-02T19:04:38.000Z","updated_at":"2026-05-26T21:59:24.000Z","dependencies_parsed_at":null,"dependency_job_id":"022ef3e9-cc39-461c-90c1-03d9bf7f71eb","html_url":"https://github.com/MarvinDrude/Beskar.Markdown","commit_stats":null,"previous_names":["marvindrude/beskar.markdown"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/MarvinDrude/Beskar.Markdown","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarvinDrude%2FBeskar.Markdown","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarvinDrude%2FBeskar.Markdown/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarvinDrude%2FBeskar.Markdown/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarvinDrude%2FBeskar.Markdown/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MarvinDrude","download_url":"https://codeload.github.com/MarvinDrude/Beskar.Markdown/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MarvinDrude%2FBeskar.Markdown/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33895175,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["allocation","csharp","markdown","markdown-to-html","performance","stackalloc"],"created_at":"2026-06-04T08:01:18.130Z","updated_at":"2026-06-04T08:01:19.235Z","avatar_url":"https://github.com/MarvinDrude.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Beskar.Markdown\n\n[![NuGet](https://img.shields.io/nuget/v/Beskar.Markdown)](https://www.nuget.org/packages/Beskar.Markdown)\n\nBeskar.Markdown is a high-performance, low-allocation Markdown parser and HTML renderer \nfor .NET. It is built from the ground up to leverage modern C# features like `Span\u003cT\u003e`, \n`ReadOnlySequence\u003cT\u003e`, and efficient memory management to provide a blazing-fast and lean experience.\n\n## Table of Contents\n\n- [Why use this library?](#why-use-this-library)\n- [Motivation](#motivation)\n- [Getting Started](#getting-started)\n- [Features](#features)\n  - [Main Features](#main-features)\n  - [Currently Supported Blocks \u0026 Inlines](#currently-supported-blocks--inlines)\n  - [Future Plans](#future-plans)\n- [Frontmatter Parsing](#frontmatter-parsing)\n- [Sluggable Headers](#sluggable-headers)\n- [Code Block Intercept](#code-block-intercept)\n- [⚠️ Security Warning](#%EF%B8%8F-security-warning)\n- [Simple custom markdown extensions](#simple-custom-markdown-extensions)\n  - [Simple inline extension](#simple-inline-extension)\n  - [Simple block extension](#simple-block-extension)\n- [Benchmark Results](#benchmark-results)\n\n---\n\n\u003e Disclaimer: This is just my fun project i do on the side. I do not want to replace any of the major\n\u003e markdown solutions for csharp, neither could I do that even if i wanted. It's just for me internally\n\u003e to use.\n\n## Why use this library?\n\n- **Performance First**: Designed for scenarios where every microsecond and every byte counts.\n- **Low Allocation**: Minimizes pressure on the Garbage Collector by using stack-allocated buffers and pooling where possible.\n- **Modern C#**: Built for modern .NET, taking advantage of the latest language and runtime optimizations.\n- **Simplicity**: A clean, easy-to-use API that gets the job done without unnecessary complexity.\n- **Tests**: 1,009 passing tests (652 **CommonMark** Spec Tests)\n\n## Motivation\n\nI created this library because I love to learn about deep-level code topics. \nBuilding a Markdown parser is a fantastic way to explore memory layout optimization, \nand the intricacies of text processing at scale. It's a passion project aimed at \nachieving technical excellence and pushing the boundaries of what's possible in .NET.\n\n## Getting Started\n\nGetting started with Beskar.Markdown is easy. Call the static `ToHtml` method:\n\n```csharp\nusing Beskar.Markdown;\n\nstring markdown = \"# Hello World\\nThis is a **bold** statement.\";\nstring html = BeMarkdown.ToHtml(markdown);\n\nConsole.WriteLine(html);\n// Output: \u003ch1\u003eHello World\u003c/h1\u003e\u003cp\u003eThis is a \u003cstrong\u003ebold\u003c/strong\u003e statement.\u003c/p\u003e\n```\n\n## Features\n\n### Main Features\n- **Fast**: Beskar.Markdown is designed to be fast and leaner than existing solutions.\n- **Modern**: Built for modern .NET, taking advantage of the latest language and runtime optimizations.\n- **Easy to Use**: A clean, intuitive API that makes Markdown processing straightforward.\n- **Extensible**: Easily add support for new Markdown features or extensions.\n- **Frontmatter**: Built-in support for parsing document frontmatter.\n- **Sluggable Headers**: Automatically generate `id` attributes for headers.\n- **Advanced**: Supports contextual rendering\n- **Tests**: 1,009 passing tests (652 **CommonMark** Spec Tests)\n\n### Currently Supported Blocks \u0026 Inlines\n- **Blocks**:\n    - Headers (ATX \u0026 Setext)\n    - Paragraphs\n    - Blockquotes\n    - Lists (Ordered \u0026 Unordered)\n    - Task list items (like GitHub)\n    - Fenced Code Blocks\n    - Indented Code Blocks\n    - Thematic Breaks (Horizontal Rules)\n    - HTML Blocks\n    - GitHub like Tables\n    - Full reference links\n- **Inlines**:\n    - Emphasis (Bold, Italic)\n    - Links\n    - Autolinks\n    - Inline Code\n    - Inline HTML\n    - Line Breaks\n    - Strikethrough\n    - Images\n\n### Future Plans\n- [ ] In memory assembly baking\n\n## ⚠️ Security Warning\n\n**Important**: Beskar.Markdown does not perform HTML sanitization by default.\nIf you are processing Markdown input from untrusted users, you **MUST** sanitize\nthe resulting HTML to prevent Cross-Site Scripting (XSS) attacks.\n\nExample:\n```csharp\nvar rawHtml = BeMarkdown.ToHtml(userContent);\nvar sanitizer = new HtmlSanitizer();\nvar safeHtml = sanitizer.Sanitize(rawHtml);\n```\n\nIf your sanitizer supports spans, you can use the following to prevent double allocation unlike above:\n```csharp\nvar options = RenderOptions.HtmlDefault;\noptions.SanitizerFunc = (span) =\u003e HtmlSanitizer.Sanitize(span);\n\nvar safeHtml = BeMarkdown.ToHtml(userContent, renderOptions: options);\n```\n\n## Frontmatter Parsing\n\nBeskar.Markdown can automatically parse YAML-like frontmatter into key-value pairs.\nTo enable this, use the `WithFrontMatter()` option and the `Parse` method:\n\n```csharp\nvar options = MarkdownOptionBuilder.Create()\n    .WithFrontMatter()\n    .Build();\n\nvar markdown = \"\"\"\n               ---\n               title: My Awesome Page\n               author: Marvin\n               ---\n               # Content\n               \"\"\";\n\nvar result = BeMarkdown.Parse(markdown, options);\n\nConsole.WriteLine(result.Context.FrontMatter[\"title\"]); // My Awesome Page\nConsole.WriteLine(result.Html); // \u003ch1\u003eContent\u003c/h1\u003e\n```\n\n## Sluggable Headers\n\nBeskar.Markdown can automatically generate `id` attributes for headers based on their text content.\nTo enable this, use the `WithSluggableHeaders()` option:\n\n```csharp\nvar options = MarkdownOptionBuilder.Create()\n    .WithSluggableHeaders()\n    .Build();\n\nvar markdown = \"# My Header Text\";\nvar html = BeMarkdown.ToHtml(markdown, options);\n\nConsole.WriteLine(html);\n// Output: \u003ch1 id=\"my-header-text\"\u003eMy Header Text\u003c/h1\u003e\n```\n\nIf you need a table of contents or anchor list, use `Parse` instead of `ToHtml`.\nThe returned context exposes headers in document order through `Context.Headers`.\nEach item contains the generated slug, the plain text, and the heading level:\n\n```csharp\nvar markdown = \"\"\"\n               # My Header Text\n               ## Details\n               \"\"\";\n\nvar result = BeMarkdown.Parse(markdown, options);\n\nforeach (var header in result.Context.Headers)\n{\n    Console.WriteLine($\"{header.Level}: {header.PlainText} -\u003e #{header.Slug}\");\n}\n\n// Output:\n// 1: My Header Text -\u003e #my-header-text\n// 2: Details -\u003e #details\n```\n\n## Code Block Intercept\n\nYou can intercept the rendering of code blocks (both fenced and indented) to provide your own \nHTML output. This is particularly useful for server-side syntax highlighting (e.g., using \nlibraries like `ColorCode` or calling a highlighting service).\n\nTo use this, implement the `ICodeBlockRenderer` interface and register it via `WithCodeBlockRenderer`:\n\n```csharp\npublic sealed class MySyntaxHighlighter : ICodeBlockRenderer\n{\n    public bool TryRender\u003cTData\u003e(\n        MarkdownContext\u003cTData\u003e context,\n        ref TextWriterIndentSlim writer,\n        ReadOnlySpan\u003cchar\u003e code,\n        ReadOnlySpan\u003cchar\u003e language)\n    {\n        // Intercept only C# blocks\n        if (language.Equals(\"csharp\", StringComparison.OrdinalIgnoreCase))\n        {\n            writer.Write(\"\u003cdiv class=\\\"highlight\\\"\u003e\");\n            writer.WriteHtmlDecodedAndEncoded(language);\n            writer.Write(\":\");\n            \n            // Your custom highlighting logic here\n            writer.Write(code); \n            \n            writer.Write(\"\u003c/div\u003e\");\n            return true; // Successfully intercepted\n        }\n\n        return false; // Fallback to default renderer\n    }\n}\n\n// Usage:\nvar options = MarkdownOptionBuilder.Create()\n    .WithCodeBlockRenderer(new MySyntaxHighlighter())\n    .Build();\n\nvar html = BeMarkdown.ToHtml(\"```csharp\\nvar x = 1;\\n```\", options);\n```\n\n\u003e **Note**: When writing the `language` span to the output, always use `writer.WriteHtmlDecodedAndEncoded(language)` \n\u003e to ensure proper HTML encoding.\n\n\n## Simple custom markdown extensions\n\n### Simple inline extension\n\nThis example inline extension adds a random emoji if you use `.RandomEmoji.`:\n\n```csharp\nvar options = MarkdownOptionBuilder.Create()\n   .WithExtension(new EmojiInlineExtension())\n   .Build();\n\nconst string markdown = \n   \"\"\"\n   Hello, World! .RandomEmoji.\n   \"\"\";\nvar result = BeMarkdown.ToHtml(markdown, options);\nConsole.WriteLine(result); // \u003cp\u003eHello, World! \u003cspan class=\"emoji\"\u003e💻\u003c/span\u003e\u003c/p\u003e\n```\n\nImplementation:\n```csharp\npublic sealed class EmojiInlineExtension : BaseInlineExtension\n{\n   private const int _targetTypeValue = BeMarkdown.BuiltInNodeTypeValueOffset + 4;\n   private static readonly ImmutableArray\u003cstring\u003e _emojis = ImmutableArray.CreateRange([\n      \"😀\", \"🎉\", \"🚀\", \"🌟\", \"🔥\", \"🐱\", \"🍕\", \"💻\", \"☕\"]);\n\n   public EmojiInlineExtension()\n   {\n      Parsers = [new EmojiInlineParser()];\n      Renderers = [new HtmlEmojiInlineRenderer()];\n   }\n\n   private sealed class HtmlEmojiInlineRenderer : INodeRenderer\n   {\n      public int TargetTypeValue =\u003e _targetTypeValue;\n\n      public void Render\u003cTData\u003e(\n         MarkdownContext\u003cTData\u003e context,\n         ReadOnlySpan\u003cchar\u003e rawText, \n         ref TextWriterIndentSlim writer, \n         in MarkdownNode current, \n         ReadOnlySpan\u003cMarkdownNode\u003e nodes,\n         RenderOptions options)\n      {\n         writer.Write(\"\u003cspan class=\\\"emoji\\\"\u003e\");\n         writer.Write(_emojis[Random.Shared.Next(0, _emojis.Length)]);\n         writer.Write(\"\u003c/span\u003e\");\n      }\n   }\n   \n   private sealed class EmojiInlineParser : IInlineParser\n   {\n      private const string _identifier = \".RandomEmoji.\";\n      \n      public int Priority =\u003e 8_000;\n      public int SupportedTypeValue =\u003e _targetTypeValue;\n\n      public char TriggerChar =\u003e '.';\n      public char TriggerAltChar =\u003e '.';\n\n      public bool TryMatch\u003cTData\u003e(\n         ref InlineState\u003cTData\u003e state, \n         int parentIndex, \n         ref BufferWriter\u003cMarkdownNode\u003e writer, \n         scoped ref InlineParser\u003cTData\u003e parser,\n         ParserOptions options)\n      {\n         if (state.RemainingText.Length \u003c _identifier.Length) \n            return false;\n         \n         if (!state.RemainingText.StartsWith(_identifier))\n            return false;\n\n         var nodeIndex = writer.WrittenSpan.Length;\n         writer.Add(new MarkdownNode()\n         {\n            Type = (NodeType)SupportedTypeValue,\n            TextSpan = new TextSpan(state.GlobalOffset, _identifier.Length),\n            \n            NextSiblingIndex = -1,\n            FirstChildIndex = -1,\n            LastChildIndex = -1\n         });\n         \n         parser.LinkInlineNode(ref writer, parentIndex, nodeIndex);\n         state.Advance(_identifier.Length);\n         return true;\n      }\n   }\n}\n```\n\n### Simple block extension\n\nThis example block extension adds a basic parsing for a div that is red:\n\n```csharp\nvar options = MarkdownOptionBuilder.Create()\n   .WithExtension(new RedBlockExtension())\n   .WithMaxBlockDepth(16)\n   .Build();\n\nconst string markdown = \n   \"\"\"\n   +red block+\n   inside of `code` inline\n   red\n   \u003e blockquote\n\n   \"\"\";\nvar result = BeMarkdown.ToHtml(markdown, options);\nConsole.WriteLine(result); // \u003cdiv class=\"red-block\"\u003e\u003cp\u003einside of \u003ccode\u003ecode\u003c/code\u003e inline\\nred\u003c/p\u003e\u003cblockquote\u003e\u003cp\u003eblockquote\u003c/p\u003e\u003c/blockquote\u003e\u003c/div\u003e\n```\n\nImplementation:\n```csharp\npublic sealed class RedBlockExtension : BaseBlockExtension\n{\n   private const int _targetTypeValue = BeMarkdown.BuiltInNodeTypeValueOffset + 5;\n   \n   public RedBlockExtension()\n   {\n      Parsers = [new RedBlockParser()];\n      Renderers = [new HtmlRedBlockRenderer()];\n   }\n\n   private sealed class HtmlRedBlockRenderer : INodeRenderer\n   {\n      public int TargetTypeValue =\u003e _targetTypeValue;\n\n      public void Render\u003cTData\u003e(\n         MarkdownContext\u003cTData\u003e context,\n         ReadOnlySpan\u003cchar\u003e rawText, \n         ref TextWriterIndentSlim writer, \n         in MarkdownNode current, \n         ReadOnlySpan\u003cMarkdownNode\u003e nodes,\n         RenderOptions options)\n      {\n         writer.Write(\"\u003cdiv class=\\\"red-block\\\"\u003e\");\n         current.RenderChildren(context, rawText, nodes, ref writer, options);\n         writer.Write(\"\u003c/div\u003e\");\n      }\n   }\n\n   private sealed class RedBlockParser : IBlockParser\n   {\n      private const string _identifier = \"+red block+\";\n      \n      public int Priority =\u003e 10; // low priority\n      public int SupportedTypeValue =\u003e _targetTypeValue;\n      \n      public int TryMatch\u003cTData\u003e(ref LineState\u003cTData\u003e state, int parentIndex, ref BufferWriter\u003cMarkdownNode\u003e writer)\n      {\n         if (state.IsBlank || state.LeadingSpaces \u003e 0)\n         {\n            return -1;\n         }\n         \n         if (state.FirstChar != '+' \u0026\u0026 state.RawLine.Length \u003c _identifier.Length)\n         {\n            return -1;\n         }\n         \n         if (!state.RawLine.StartsWith(_identifier))\n         {\n            return -1;\n         }\n         \n         var nodeIndex = writer.WrittenSpan.Length;\n         writer.Add(new MarkdownNode()\n         {\n            Type = (NodeType)SupportedTypeValue,\n            TextSpan = new TextSpan(-1, 0),\n            \n            FirstChildIndex = -1,\n            NextSiblingIndex = -1,\n            LastChildIndex = -1\n         });\n\n         state.ConsumeRest();\n         return nodeIndex;\n      }\n\n      public bool CanContinue\u003cTData\u003e(ref MarkdownNode node, ref LineState\u003cTData\u003e state, ref BufferWriter\u003cMarkdownNode\u003e writer)\n      {\n         // simple example only an empty line can stop the block\n         if (state.IsBlank)\n         {\n            return false;\n         }\n         \n         if (node.TextSpan.Start == -1)\n         {\n            node.TextSpan = new TextSpan(state.GlobalOffset, state.RawLine.Length);\n         }\n         else\n         {\n            var newLength = (state.GlobalOffset - node.TextSpan.Start) + state.RawLine.Length;\n            node.TextSpan = node.TextSpan with { Length = newLength };\n         }\n\n         return true;\n      }\n   }\n}\n```\n\n## Benchmark Results\n\nBeskar.Markdown is designed to be fast and leaner than existing solutions. \nHere is a comparison with some other libraries (especially when it comes to memory usage):\n\n| Method          | Categories | Mean           | Rank | Gen0      | Gen1      | Gen2      | Allocated  |\n|---------------- |----------- |---------------:|-----:|----------:|----------:|----------:|-----------:|\n| Markdig         | Bigger     |     222.117 us |    2 |         - |         - |         - |    37256 B |\n| Beskar.Markdown | Bigger     |      95.211 us |    1 |         - |         - |         - |     5288 B |\n| CommonMark.Net  | Bigger     |      86.785 us |    1 |         - |         - |         - |    66024 B |\n| MarkdownSharp   | Bigger     |     429.370 us |    3 |         - |         - |         - |   273996 B |\n|                 |            |                |      |           |           |           |            |\n| Markdig         | Full Spec  |   1,919.994 us |    3 |  125.0000 |  125.0000 |  125.0000 |  2077013 B |\n| Beskar.Markdown | Full Spec  |   1,302.109 us |    1 |   31.2500 |   31.2500 |   31.2500 |   437967 B |\n| CommonMark.Net  | Full Spec  |   1,577.537 us |    2 |  125.0000 |  125.0000 |  125.0000 |  3153930 B |\n| MarkdownSharp   | Full Spec  | 346,914.621 us |    4 | 1656.2500 | 1625.0000 | 1468.7500 | 15909676 B |\n|                 |            |                |      |           |           |           |            |\n| Markdig         | Small      |       5.425 us |    3 |         - |         - |         - |     1144 B |\n| Beskar.Markdown | Small      |       2.736 us |    2 |         - |         - |         - |      504 B |\n| CommonMark.Net  | Small      |       1.768 us |    1 |         - |         - |         - |    11488 B |\n| MarkdownSharp   | Small      |       5.056 us |    3 |         - |         - |         - |     2752 B |\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarvindrude%2Fbeskar.markdown","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarvindrude%2Fbeskar.markdown","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarvindrude%2Fbeskar.markdown/lists"}