Skip to content

Repository files navigation

MarkupString

CI MarkupString MarkupString.Ansi MarkupString.Html License

Immutable styled text for terminals and the web. One value — a plain string plus layered markup runs over it — renders to ANSI, HTML, Pueblo, MXP, BBCode or plain text, and round-trips through JSON without losing a layer the reader does not understand.

var text = MarkupText.Concat(
  MarkupText.Plain("Hello, "),
  MarkupText.Wrap(AnsiCodeParser.Parse("hr"), "world"));

text.Render(MarkupFormat.Ansi);   // Hello, \e[1;31mworld\e[0m
text.Render(MarkupFormat.Html);   // Hello, <span style="color: #ff5555">world</span>
text.Render(MarkupFormat.Plain);  // Hello, world

Slicing, padding, wrapping, trimming and the rest of the string operations carry the markup with them, and measure in display cells — wide CJK, combining marks and emoji sequences count correctly, and no operation ever cuts a grapheme cluster in half.

Install

dotnet add package MarkupString
dotnet add package MarkupString.Ansi
dotnet add package MarkupString.Html
Package What it gives you
MarkupString The MarkupText type, runs, formats, the registry, the emitter/codec contracts, the JSON serializer, grapheme and display-width helpers. No rendering opinions.
MarkupString.Ansi Terminal styling: colours (16 / xterm-256 / truecolor), attributes, links; an ansi() code parser and an escape-sequence parser; emitters for ANSI, HTML, Pueblo, MXP and BBCode.
MarkupString.Html Raw HTML/MXP tag markup — an MXP <send>, an anchor, a <span class> — plus the stylesheet for the classes the emitters write.

The core package renders nothing on its own: emitters live in the kind packages, so a consumer that only needs one of them pays for one of them, and a kind of your own is a first-class peer rather than a fork.

Getting started

using MarkupString;
using MarkupString.Ansi;
using MarkupString.Html;

// Once, at startup. Set-once: a second, different registry throws.
MarkupRegistry.Default = MarkupRegistry.Empty.WithAnsi().WithHtml();

var prompt = MarkupText.Wrap(
  HtmlMarkup.Create("send", "href=\"north\""),
  MarkupText.Wrap(AnsiCodeParser.Parse("hc"), "Go north"));

Console.WriteLine(prompt.Render(MarkupFormat.Ansi));

ToString() is always the plain text — it is never format-specific. Rendering is explicit: Render(MarkupFormat.Ansi), RenderTo(format, bufferWriter) when you have somewhere to write.

Documentation

Guide
Getting started Install, wire up the registry, build and render your first styled text.
Text operations Slicing, padding, alignment, splitting, splicing — and the grapheme and display-width rules they obey.
Formats and rendering The six built-in formats, what each emits, framers, custom formats.
Custom markup kinds Write your own IMarkup, emitters and codec; compose with the kinds already registered.
Serialization The JSON wire format, forward compatibility, UnknownMarkup.
Releasing How a version is cut and published (maintainers).

Design

  • Runs, not a tree. Text is a string; markup is an ImmutableArray<Run> of coalesced, non-overlapping ranges, each holding a stack of layers. Identically marked neighbours merge, overlapping runs are rejected at construction, and a slice is a clip of that array. This is the model behind NSAttributedString, Swift's AttributedString and VS Code's line tokens.
  • An explicit registry, not reflection. Emitters are keyed on (markup type, format) in a FrozenDictionary built by WithAnsi()/WithHtml()/With(...). Nothing is discovered at runtime, so nothing breaks under trimming, and adding a kind is a call, not a convention.
  • Diffed output. ANSI transitions are written as the difference between the previous run's style and this one's, so nested styling does not restate what is already in effect and no run pays for a reset it does not need.
  • Unicode-correct by construction. Extractions snap inward to cluster boundaries, edits snap outward; padding and alignment measure in cells, not code units.
  • AOT and trimming clean. All three packages are IsAotCompatible with no reflection and no dynamic code, and CI publishes a native binary with every assembly rooted, failing on any IL2xxx/IL3xxx warning.

Requirements

.NET 10 or later.

Versioning

Semantic versioning, driven by MinVer: the tag v1.2.3 builds 1.2.3, and any other commit builds the next patch as a -preview.0.N prerelease. The three packages share one version and are released together. Public API changes are tracked in PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt and enforced at build time.

Contributing

dotnet build MarkupString.slnx
dotnet run --project MarkupString.Tests

The build fails with FORMAT001 if C# no longer matches .editorconfig; the error text carries the dotnet format whitespace --folder <dir> command that fixes it (run it until it reports no changes — the formatter needs two passes to converge).

Licence

Apache-2.0. Extracted from and used by SharpMUSH.

About

Immutable styled text for terminals and the web: one value renders to ANSI, HTML, Pueblo, MXP, BBCode or plain text.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages