StringEnricher 0.0.1-rc.3

This is a prerelease version of StringEnricher.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package StringEnricher --version 0.0.1-rc.3
                    
NuGet\Install-Package StringEnricher -Version 0.0.1-rc.3
                    
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="StringEnricher" Version="0.0.1-rc.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="StringEnricher" Version="0.0.1-rc.3" />
                    
Directory.Packages.props
<PackageReference Include="StringEnricher" />
                    
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 StringEnricher --version 0.0.1-rc.3
                    
#r "nuget: StringEnricher, 0.0.1-rc.3"
                    
#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 StringEnricher@0.0.1-rc.3
                    
#: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=StringEnricher&version=0.0.1-rc.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=StringEnricher&version=0.0.1-rc.3&prerelease
                    
Install as a Cake Tool

StringEnricher

Build Status NuGet Version NuGet Downloads License: MIT .NET Version GitHub Stars GitHub Issues GitHub Last Commit

StringEnricher is a powerful and extensible C# library for building and enriching strings with rich text styles, supporting formats such as HTML and MarkdownV2. It is designed for scenarios where you need to dynamically compose styled messages, such as chatbots, messaging apps, or document generators.

Features

  • High performance: Optimized for minimal allocations and fast execution.
  • Rich style system: Apply styles like bold, italic, underline, strikethrough, code blocks, blockquotes, spoilers, links, and more.
  • Multi-format support: Easily switch between HTML and MarkdownV2.
  • Composable styles: Nest and combine styles for complex formatting.
  • Well-tested: Comprehensive unit tests for all styles and formats.

Getting Started

Requirements

  • .NET 9.0 or later

Installation

Execute the following command in your project directory:

dotnet add package StringEnricher

Usage

Basic Example (HTML Bold)

using StringEnricher.Node.Html;

var styledBold = BoldHtml.Apply("bold text"); // 0 heap allocations here
var styledBoldString = styledBold.ToString(); // 1 final string heap allocation here
// styledBold == "<b>bold text</b>"

Applying Multiple Node

using StringEnricher.Node.Html;

var styled = BoldHtml.Apply(
    ItalicHtml.Apply("important text") // 0 heap allocations here
); // 0 heap allocations here
var styledString = styled.ToString(); // 1 final string heap allocation here
// styled == "<b><i>important text</i></b>"

MarkdownV2 Example

using StringEnricher.Node.MarkdownV2;

var boldMd = BoldMarkdownV2.Apply("bold text"); // 0 heap allocations here
var boldMdString = boldMd.ToString(); // 1 final string heap allocation here
// boldMd == "*bold text*"

The .CopyTo() method for Zero Allocations

using StringEnricher.Node.Html;

var styled = BoldHtml.Apply("bold text");
Span<char> buffer = stackalloc char[styled.TotalLength]; // allocate buffer on stack
int written = styled.CopyTo(buffer); // 0 heap allocations here
var result = new string(buffer.Slice(0, written)); // 1 final string heap
// result == "<b>bold text</b>"

Note: This approach is OK for small strings that fit on the stack (up to 1-2 KB). For larger strings, use ToString().

.ToString() method for Final String Creation

The ToString() method is used to create the final styled string. It performs a single heap allocation for the resulting string. Use it only when you finished building the entire styled string.

.TryGetChar() method for Single Character Access

The TryGetChar(int index, out char value) method allows you to access individual characters in the styled string without creating the entire string. It returns true if the character at the specified index exists, otherwise false.

using StringEnricher.Node.Html;
var styled = BoldHtml.Apply("bold text");
if (styled.TryGetChar(0, out char character))
{
    // character == '*'
}
if (styled.TryGetChar(11, out char character))
{
    // character == '*'
}
if (styled.TryGetChar(12, out char character))
{
    // this is out of bounds
}
else {
    // character == '\0'
}

.CombineWith() method for Merging Nodes

The CombineWith(INode other) method allows you to merge two nodes into a single node. This is useful for building complex styled strings from multiple parts.

using StringEnricher.Node.Html;
var part1 = BoldHtml.Apply("bold text");
var part2 = ItalicHtml.Apply(" and italic text");
var combined = part1.CombineWith(part2); // 0 heap allocations here
var combinedString = combined.ToString(); // 1 final string heap allocation here
// combinedString == "<b>bold text</b><i> and italic text</i

MessageBuilder for Fluent API

using StringEnricher;
using StringEnricher.Node.Html;
// Pre-calculate total length to avoid over-allocation
var messageBuilder = new MessageBuilder(totalLength);
var state = ["Hello, ", "World! ", "Every ", "word ", "is ", "in ", "different ", "style&"];
var string result = messageBuilder.Create(state, static (state, writer) => 
{
    writer.Append(BoldHtml.Apply(state[0])); // "*Hello, *" - 0 heap allocations here
    writer.Append(ItalicHtml.Apply(state[1])); // "_World! _" - 0 heap allocations here
    writer.Append(UnderlineHtml.Apply(state[2])); // "__Every __" - 0 heap allocations here
    writer.Append(StrikethroughHtml.Apply(state[3])); // "~word ~" - 0 heap allocations here
    writer.Append(CodeHtml.Apply(state[4])); // "`is`" - 0 heap allocations here
    writer.Append(BlockquoteHtml.Apply(state[5])); // "> different " - 0 heap allocations here
    writer.Append(SpoilerHtml.Apply(state[6])); // "||style||" - 0 heap allocations here
    writer.Append(EscapeHtml.Apply(state[7])); // "style&amp;" - 0 heap allocations here
}); // 1 final string allocated in heap without any intermediate allocations

Using Aliases for Node via GlobalUsings.cs

To simplify switching between HTML and MarkdownV2 styles across your project, you can use C# using aliases in a GlobalUsings.cs file. This allows you to reference style helpers (like Bold, Italic, etc.) generically, and change the underlying format by updating just one file.

Example: GlobalUsings.cs

// GlobalUsings.cs
// Place this file in your project root or any folder included in compilation.

// HTML formatting nodes
global using Bold = StringEnricher.Nodes.Html.Formatting.BoldHtml;
global using Italic = StringEnricher.Nodes.Html.Formatting.ItalicHtml;
global using Underline = StringEnricher.Nodes.Html.Formatting.UnderlineHtml;
global using Strikethrough = StringEnricher.Nodes.Html.Formatting.StrikethroughHtml;
global using Spoiler = StringEnricher.Nodes.Html.Formatting.SpoilerHtml;
global using InlineLink = StringEnricher.Nodes.Html.Formatting.InlineLinkHtml;
global using Blockquote = StringEnricher.Nodes.Html.Formatting.BlockquoteHtml;
global using ExpandableBlockquote = StringEnricher.Nodes.Html.Formatting.ExpandableBlockquoteHtml;
global using CodeBlock = StringEnricher.Nodes.Html.Formatting.CodeBlockHtml;
global using SpecificCodeBlock = StringEnricher.Nodes.Html.Formatting.SpecificCodeBlockHtml;
global using InlineCode = StringEnricher.Nodes.Html.Formatting.InlineCodeHtml;
global using TgEmoji = StringEnricher.Nodes.Html.Formatting.TgEmojiHtml;
global using Escape = StringEnricher.Nodes.Html.EscapeHtml;

To switch to MarkdownV2, simply update the aliases:

// GlobalUsings.cs

global using Bold = StringEnricher.Nodes.MarkdownV2.Formatting.BoldMarkdownV2;
global using Italic = StringEnricher.Nodes.MarkdownV2.Formatting.ItalicMarkdownV2;
global using Underline = StringEnricher.Nodes.MarkdownV2.Formatting.UnderlineMarkdownV2;
global using Strikethrough = StringEnricher.Nodes.MarkdownV2.Formatting.StrikethroughMarkdownV2;
global using Spoiler = StringEnricher.Nodes.MarkdownV2.Formatting.SpoilerMarkdownV2;
global using InlineLink = StringEnricher.Nodes.MarkdownV2.Formatting.InlineLinkMarkdownV2;
global using Blockquote = StringEnricher.Nodes.MarkdownV2.Formatting.BlockquoteMarkdownV2;
global using ExpandableBlockquote = StringEnricher.Nodes.MarkdownV2.Formatting.ExpandableBlockquoteMarkdownV2;
global using CodeBlock = StringEnricher.Nodes.MarkdownV2.Formatting.CodeBlockMarkdownV2;
global using SpecificCodeBlock = StringEnricher.Nodes.MarkdownV2.Formatting.SpecificCodeBlockMarkdownV2;
global using InlineCode = StringEnricher.Nodes.MarkdownV2.Formatting.InlineCodeMarkdownV2;
global using TgEmoji = StringEnricher.Nodes.MarkdownV2.Formatting.TgEmojiMarkdownV2;
global using Escape = StringEnricher.Nodes.MarkdownV2.EscapeMarkdownV2;

Usage in Your Code

var styled = Bold.Apply(
    Italic.Apply("important text") // 0 heap allocations here
); // 0 heap allocations here
var styledString = styled.ToString(); // 1 final string heap allocation here
// styled == "<b><i>important text</i></b>" (HTML)
// styled == "* _important text_ *" (MarkdownV2)

This approach centralizes format selection, making it easy to switch formats for the entire project by editing only GlobalUsings.cs.

Notes

  • Prefer .CopyTo() for zero allocations.
  • Use .ToString() for final string creation.
  • .TryGetChar() for random character access.
    • DO NOT USE IT in loops or performance-critical paths as it is O(n) operation (in the worst case).
    • The only purpose is to get a single character without creating the entire string.
    • If you need to iterate over all characters, use .ToString() and then iterate over the resulting string. OR use .CopyTo() to copy to a buffer (you can use stack allocated buffer) and then iterate over the buffer.
  • .CombineWith() for merging nodes at a compile time.
  • MessageBuilder for fluent API and complex message construction at runtime.
    • Comprehensive support for primitive types and INode - see MessageBuilder.Append() overloads. Means you can append any primitive type directly without converting to string first.
    • Using MessageBuilder requires pre-calculation of the total length for the final string. This allows to build the entire message in a single final string allocation without intermediate allocations.
    • If you cannot pre-calculate the total length, use StringBuilder or other approaches to build the message in multiple steps. This library is optimized for zero allocations only when the total length is known in advance.
  • Use using aliases in a GlobalUsings.cs file to easily switch between HTML and MarkdownV2 formats across your project.
  • Escape special characters using EscapeHtml or EscapeMarkdownV2 nodes.
    • Also, available as static methods: HtmlEscaper.Escape(string) and MarkdownV2Escaper.Escape(string). But these create returns strings as the result, while nodes provides lazy evaluation and zero allocations until the final string is created. So prefer nodes over static methods when possible.
  • It is recommended to use Html format for better performance and stability by format consumers (like TG) unless MarkdownV2 is specifically required.
    • Html by its nature is more robust and less error-prone than MarkdownV2.
    • MarkdownV2 has many edge cases and limitations that can lead to formatting issues.
    • Html-related code paths are generally faster and more memory efficient than MarkdownV2 paths.
  • Despite the fact that every node implements INode interface, avoid using INode directly in performance-critical paths to prevent boxing allocations. Use concrete node types instead.
    • The INode interface is used in this library only for generic definitions as a constraint. The library itself never uses INode directly as it will cause boxing/unboxing.
  • Check existing benchmarks in the benchmarks folder. You can run them using BenchmarkDotNet.
    • You can find interesting results there, including comparisons of different string building approaches.
    • I strongly recommend to review all benchmarks if you want to write the most performant code using this library. You will get understanding how the library works under the hood and how to use it in the most efficient way.
    • Also, I recommend to write your custom benchmarks for your specific use cases to check performance and memory allocations. Sometimes the most optimal approach is not obvious and depends on the specific scenario.
    • Also, I recommend to check existing unit tests in the tests/StringEnricher.Tests folder. They cover all styles and formats and can be a good reference for usage examples.

Project Structure

  • src/StringEnricher/: Core library
    • Nodes/: Nodes definitions for HTML, MarkdownV2, PlainText, etc.
  • tests/StringEnricher.Tests/: Unit tests for all styles and formats

Facts

  • Designed for high performance and composability
  • Easily extendable for new formats and styles
  • Suitable for chatbots, messaging apps, and document generation

License

MIT

Benchmarks

Benchmarks are available in the benchmarks folder. You can run them using BenchmarkDotNet.


Feel free to contribute or open issues for feature requests and bug reports!


TODOs

  • Consider adding support for more primitive types in MessageBuilder.Append() overloads and Node types if needed.
    • short, byte, sbyte, ushort, uint, ulong, object
    • Add support for enums?
    • Are there any other types that are commonly used and should be supported directly?
  • Think about the possibility to add support for custom user-defined types in MessageBuilder.Append() and Node types.
    • Make a guide on how to implement INode for custom types.
    • Make a guide on how to extend MessageBuilder to support custom types.
  • Add more benchmarks for different scenarios and use cases.
    • Consider extending the ci-cd pipeline to run benchmarks and update results.
  • Add more test cases for nodes.
Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  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.
  • net9.0

    • No dependencies.

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
0.0.1-rc.9 40 9/15/2025
0.0.1-rc.8 34 9/15/2025
0.0.1-rc.7 129 9/11/2025
0.0.1-rc.6 121 9/10/2025
0.0.1-rc.5 120 9/10/2025
0.0.1-rc.4 116 9/9/2025
0.0.1-rc.3 130 9/7/2025
0.0.1-rc.2 128 9/7/2025
0.0.1-rc.1 91 9/7/2025
0.0.1-rc.0 126 8/31/2025

Inline links for HTML and MarkdownV2 were fixed - the URL parameter is string right now