Markout 0.22.0
dotnet add package Markout --version 0.22.0
NuGet\Install-Package Markout -Version 0.22.0
<PackageReference Include="Markout" Version="0.22.0" />
<PackageVersion Include="Markout" Version="0.22.0" />
<PackageReference Include="Markout" />
paket add Markout --version 0.22.0
#r "nuget: Markout, 0.22.0"
#:package Markout@0.22.0
#addin nuget:?package=Markout&version=0.22.0
#tool nuget:?package=Markout&version=0.22.0
Markout
Markup adds instructions to content. Markout removes structure from data.
Markout is a source-generated .NET serializer that projects objects into structured, human-readable documents. You annotate your models with attributes that describe data relationships — identity, enumeration, tabulation, measurement, hierarchy — and the source generator emits code that writes through an abstract renderer. The same object graph produces Markdown tables, ANSI terminal output with colored bars, plain text with aligned columns, pretty tables, or TSV rows, without the developer making visual decisions.
Quick Start
using Markout;
var artist = new Artist(
Name: "Sarah McLachlan",
Genre: "Pop / Adult Contemporary",
Origin: "Halifax, Nova Scotia",
DebutYear: 1988,
BestKnownFor: "Angel, Building a Mystery, Adia");
MarkoutSerializer.Serialize(artist, Console.Out, ArtistContext.Default);
[MarkoutSerializable(TitleProperty = nameof(Artist.Name))]
public record Artist(
string Name,
string Genre,
string Origin,
int DebutYear,
string BestKnownFor);
[MarkoutContext(typeof(Artist))]
public partial class ArtistContext : MarkoutSerializerContext { }
Output:
# Sarah McLachlan
| Field | Value |
| ----- | ----- |
| Genre | Pop / Adult Contemporary |
| Origin | Halifax, Nova Scotia |
| Debut Year | 1988 |
| Best Known For | Angel, Building a Mystery, Adia |
Three things: a record, a context, one line of serialization. The TitleProperty becomes a heading; everything else renders as a field table.
Field Layouts
The same model renders differently with FieldLayout. The default is a two-column table. Switch to Inline for a compact summary line:
[MarkoutSerializable(TitleProperty = nameof(Artist.Name), FieldLayout = FieldLayout.Inline)]
public record Artist( ... );
# Sarah McLachlan
Genre: Pop / Adult Contemporary | Origin: Halifax, Nova Scotia | Debut Year: 1988 | Best Known For: Angel, Building a Mystery, Adia
Or Bulleted for a list:
[MarkoutSerializable(TitleProperty = nameof(Artist.Name), FieldLayout = FieldLayout.Bulleted)]
public record Artist( ... );
# Sarah McLachlan
- Genre: Pop / Adult Contemporary
- Origin: Halifax, Nova Scotia
- Debut Year: 1988
- Best Known For: Angel, Building a Mystery, Adia
Or Numbered:
# Sarah McLachlan
1. Genre: Pop / Adult Contemporary
2. Origin: Halifax, Nova Scotia
3. Debut Year: 1988
4. Best Known For: Angel, Building a Mystery, Adia
Or Plain — bare lines, no markers. Each line ends with two trailing spaces, which is the markdown signal for <br>:
# Sarah McLachlan
Genre: Pop / Adult Contemporary
Origin: Halifax, Nova Scotia
Debut Year: 1988
Best Known For: Angel, Building a Mystery, Adia
The data model doesn't change — only the attribute controls the shape.
Adding Sections and Tables
Add [MarkoutSection] to group properties with headings, and use List<T> for tables:
[MarkoutSerializable(TitleProperty = nameof(Title))]
public class CityReport
{
public string Title { get; set; } = "";
public string Province { get; set; } = "";
public int Population { get; set; }
[MarkoutSection(Name = "Landmarks")]
public List<LandmarkRow>? Landmarks { get; set; }
}
[MarkoutSerializable]
public class LandmarkRow
{
public string Name { get; set; } = "";
public string Type { get; set; } = "";
public int Year { get; set; }
}
[MarkoutContext(typeof(CityReport))]
[MarkoutContext(typeof(LandmarkRow))]
public partial class ReportContext : MarkoutSerializerContext { }
MarkoutSerializer.Serialize(city, Console.Out, ReportContext.Default);
Output:
# Vancouver
| Field | Value |
| ----- | ----- |
| Province | British Columbia |
| Population | 2632000 |
## Landmarks
| Name | Type | Year |
| ----- | ----- | ----- |
| Stanley Park | Park | 1888 |
| Gastown | Historic | 1867 |
| Science World | Museum | 1989 |
Real-World Example: GitHub Repository Report
The GitHubRepo sample fetches four GitHub API endpoints in parallel and projects the combined JSON into a single report — fields, bar charts, contributor metrics, and release tables — all from one model:
[MarkoutSerializable(TitleProperty = nameof(Title), DescriptionProperty = nameof(Description))]
public class RepoInfo
{
public string Title { get; set; } = "";
[MarkoutIgnore]
public string Description { get; set; } = "";
[MarkoutDisplayFormat("{0:N0}")]
public int Stars { get; set; }
[MarkoutDisplayFormat("{0:N0}")]
public int Forks { get; set; }
[MarkoutDisplayFormat("{0:N0}")]
public int OpenIssues { get; set; }
public string Language { get; set; } = "";
public string License { get; set; } = "";
[MarkoutIgnoreInTable]
[MarkoutSkipDefault]
public Callout ArchivedWarning { get; set; }
[MarkoutSection(Name = "Languages")]
[MarkoutIgnoreInTable]
public List<Breakdown>? Languages { get; set; }
[MarkoutSection(Name = "Top Contributors")]
[MarkoutIgnoreInTable]
public List<Metric>? TopContributors { get; set; }
[MarkoutSection(Name = "Releases")]
public List<ReleaseRow>? Releases { get; set; }
}
Run it:
dotnet run samples/GitHubRepo/GitHubRepo.cs # ANSI terminal (default)
dotnet run samples/GitHubRepo/GitHubRepo.cs -- dotnet/runtime --format markdown # Markdown
dotnet run samples/GitHubRepo/GitHubRepo.cs -- dotnet/runtime --format table # Compact table
dotnet run samples/GitHubRepo/GitHubRepo.cs -- dotnet/runtime --format tsv # Stable TSV rows
dotnet run samples/GitHubRepo/GitHubRepo.cs -- dotnet/runtime --format jsonl # Stable JSONL rows
Markdown output for dotnet/runtime:
# dotnet/runtime
.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
| Field | Value |
| ----- | ----- |
| Stars | 17,703 |
| Forks | 5,353 |
| Open Issues | 8,371 |
| Language | C# |
| License | MIT License |
## Languages
| Category | Count | % |
| -------- | ----- | - |
| C# | 80 | 83 |
| C++ | 9 | 9 |
| C | 7 | 7 |
## Top Contributors
| Label | Value |
| ----- | ----- |
| vargaz | 11910 |
| stephentoub | 10418 |
| kumpera | 4074 |
| jkotas | 3245 |
## Releases
| Tag | Name | Published |
| --- | ---- | --------- |
| v8.0.24 | .NET 8.0.24 | 2026-02-10 |
| v9.0.13 | v9.0.13 | 2026-02-10 |
| v10.0.3 | .NET 10.0.3 | 2026-02-10 |
Table output — same model, --format table:
FIELD VALUE
Stars 17,703
Forks 5,353
Open Issues 8,371
Language C#
License MIT License
TAG NAME PUBLISHED
v8.0.24 .NET 8.0.24 2026-02-10
v9.0.13 v9.0.13 2026-02-10
v10.0.3 .NET 10.0.3 2026-02-10
JSONL output — same table projection, --format jsonl:
{"tag":"v8.0.24","name":".NET 8.0.24","published":"2026-02-10"}
{"tag":"v9.0.13","name":"v9.0.13","published":"2026-02-10"}
{"tag":"v10.0.3","name":".NET 10.0.3","published":"2026-02-10"}
The pattern is always the same: deserialize your API data, project to a model, serialize. Markout handles the rest.
Shape Library
Each property on a model maps to a data relationship, not a visual element. Renderers decide how to present each shape.
| Relationship | C# type | What it means | Markdown | ANSI |
|---|---|---|---|---|
| Identity | string, int, bool |
Named value | Key: value |
Bold key, value |
| Enumeration | string[] |
Sequence of items | - item |
Bullet list |
| Tabulation | List<T> |
Uniform records | \| col \| col \| |
Space-padded table |
| Section | [MarkoutSection] |
Logical grouping | ## Heading |
Colored heading |
| Description | List<Description> |
Terms with explanations | - **Term:** text |
Bold term, text |
| Measurement | List<Metric> |
Comparative quantities | Label ████░░ 45 |
Colored bars |
| Composition | List<Breakdown> |
Parts of a whole | ██▓▓▒░ stacked |
Colored segments |
| Hierarchy | List<TreeNode> |
Parent-child structure | ├── node |
Box-drawing tree |
| Quotation | CodeSection |
Verbatim content | ```code``` |
Syntax display |
| Attention | Callout |
Important messages | > [!WARNING] |
Colored label |
Plus structural shapes: Quotation (prose quotation), Matrix (2D pivot grid), Rule (section separator).
Record Types
Shapes that need structured input provide record types named for what the data is, not what it looks like:
new Metric("Build Time", 4.2) // measurement
new Description("dotnet-inspect", "API surface inspection tool") // term + explanation
new Breakdown("Jan 2025", [new("Critical", 1), new("High", 3)]) // proportional composition
new Callout(CalloutSeverity.Warning, "3 vulnerabilities found") // attention
new CodeSection("csharp", "public class Foo { }") // verbatim content
Renderers
Markout ships five formatters. The serializer writes through MarkoutWriter (the orchestrator) — swap the formatter, change the output.
| Formatter | Output | Use case |
|---|---|---|
| MarkdownFormatter | GitHub-Flavored Markdown | Documentation, LLM tool output, rendered reports |
| PlainTextFormatter | Plain text without markup | Minimal output, no syntax characters |
| UnicodeFormatter | Box-drawing characters | Rich tables with borders, no color |
| TableFormatter | Tables, lists, fields | Compact summaries, pretty tables, TSV/JSONL rows |
| DiagramFormatter | Trees and structural diagrams | Dependency graphs, file trees |
Optional packages:
| Package | Formatter | Use case |
|---|---|---|
| Markout.Ansi.Spectre | SpectreFormatter |
Rich terminal UI via Spectre.Console |
Formatters declare which shapes they support by implementing capability interfaces (ITableFormatter, IFieldFormatter, ITreeFormatter, etc.). Unsupported shapes are silently skipped — the data is never lost, only the visual sophistication changes.
Format promises
- Markdown table-cell and heading text is treated as literal data: the structural characters
&,<,>, and backtick are escaped to entities (&,<,>,`) so the value renders verbatim rather than being interpreted as HTML or a code span. Table cells additionally normalize the delimiting pipe to|. - The exception is content inside a rendered
<code>span, where those characters are literal: there, a pipe is escaped as\|(GFM unescapes it while splitting table rows, before code-span parsing) and angle brackets/ampersands need no escaping. - Semantic inline
<code>...</code>tags render as Markdown code spans and as plain text in table, TSV, and JSONL output. TableFormatterwithMarkoutTableMode.Tsvuses stable snake_case headers by default and never emits embedded tabs or newlines in table cells.TableFormatterwithMarkoutTableMode.Jsonluses stable snake_case property names by default and emits one JSON object per table row.TableFormatterwithMarkoutTableMode.Prettyrenders the same projection as TSV, with each column starting at a uniform position across rows.
Templates
Templates let you author document structure in Markdown and fill data slots at runtime. The same template renders to any format — Markdown, plain text, or ANSI terminal.
template.md:
# .NET Security Report for {{date}}
The following vulnerabilities were disclosed this month.
{{vuln-table}}
| Level | CVSS Range | Response |
| ----- | ---------- | -------- |
| Critical | 9.0–10.0 | Patch immediately |
| High | 7.0–8.9 | Patch within 30 days |
{{#if commits}}
## Commits
{{commit-table}}
{{/if}}
Program.cs:
using Markout;
using Markout.Templates;
var template = MarkoutTemplate.Load("template.md");
template.TableOptions = new TableFormatterOptions(); // smooth column widths
template.Bind("date", "February 2026");
template.Bind("vuln-table", vulnerabilityData); // IMarkoutFormattable
template.Bind("commits", hasCommits ? "yes" : null);
template.Bind("commit-table", commitData);
// Markdown output
Console.WriteLine(template.Render(new MarkoutWriterOptions { PrettyTables = true }));
// Plain text output — same template, different formatter
var plainWriter = new MarkoutWriter(Console.Out, new UnicodeFormatter());
template.Render(plainWriter);
Templates support:
{{key}}— inline substitution in headings and prose, or block-level data rendering{{#if key}}/{{/if}}— conditional sections (included when key is bound and non-null)- Pipe tables — parsed and re-rendered through the writer's table shape, with optional statistical column-width optimization
IMarkoutFormattableandMarkoutTypeInfo<T>bindings for shape-aware data rendering
dotnet add package Markout.Templates
Template Runner Tool
A basic CLI tool is included for rendering templates with string bindings:
dotnet run --project tools/markout-template -- template.md date="February 2026" title="Report"
Unbound block placeholders are skipped, so you can render partial templates. Pipe key=value lines on stdin for additional bindings.
Customization Layers
Markout provides multiple layers of control, from zero-config to full custom:
Layer 1 — Attributes (compile-time): Control what's rendered and how.
[MarkoutPropertyName("Born")] // rename a field
[MarkoutSkipNull] // hide when null
[MarkoutSection(Name = "Details")] // group into a section
[MarkoutDisplayFormat("N0")] // format numbers
[MarkoutShowWhen(nameof(HasDetails))] // conditional rendering
[MarkoutMaxItems(10)] // truncate long lists
[MarkoutSection(FieldOrder = MarkoutFieldOrder.Alphabetical)] // alpha-order field rows
[MarkoutValueMap("k=badge", ...)] // map values to badge-prefixed output
[MarkoutUnwrap] // inline collection items without section heading
[MarkoutIgnoreColumnWhen(...)] // conditionally hide table columns
Layer 2 — Writer Options (runtime): Control which sections appear.
var options = new MarkoutWriterOptions
{
IncludeSections = new HashSet<string> { "Summary", "Errors" }, // only these sections
BoldFieldNames = true
};
MarkoutSerializer.Serialize(view, Console.Out, new MarkdownFormatter(), context, options);
Layer 3 — Custom Formatter (code): Implement capability interfaces for custom rendering.
public class MyFormatter : IMarkoutFormatter, IFieldFormatter, ITableFormatter
{
// Implement only the interfaces your formatter supports
void IFieldFormatter.FormatFieldName(TextWriter w, string key, bool bold) { ... }
void ITableFormatter.FormatTable(TextWriter w, ReadOnlySpan<string> headers, IList<string[]> rows, int skippedRows, MarkoutWriterOptions options) { ... }
}
Installation
dotnet add package Markout
The package includes the source generator — no additional packages needed.
Samples
- HelloMarkout — Simplest possible example: a class, a context, one line of serialization
- RecordDemo — Records as data models
- GitHubRepo — GitHub API → fields, breakdowns, metrics, and tables with Spectre, Markdown, table, TSV, and JSONL output
- GitHubActivity — User profile and recent events from the GitHub API
- CanadianContent — Canadian actors and shows with tables, trees, metrics, and multiple renderers
- LatestCves — .NET security advisories with trees and severity breakdowns
- DotNetReleases — .NET release information from GitHub
- Serialization — Shape gallery, section filtering, and writer API examples
- TemplateDemo — Document template with inline tables, conditional sections, and multi-format rendering
Formats and Agents
Agents and LLMs are skilled at two things: using tools and reading text. No single text format is best in all cases — a tool that can produce results in multiple formats is more useful to an agent than one locked to a single output.
JSON and Markout are peer options. JSON carries data between systems — APIs, config, round-trip serialization. Markout carries data to readers — humans and agents consuming structured documents. A tool like dotnet-inspect gathers data from NuGet packages, assemblies, and APIs, projects it into Markout models, and offers both --json and Markout output. The same section metadata, schema discovery, and projection features enable control and customization upstream of either format generation.
Tools that produce a lot of text need standard controls to segment it. Verbosity levels are useful but blunt — they control how much without letting you say which parts. Markout's section selection (-s Dependencies) and column/field projection (--columns Name,Version) are sharper instruments, offering the kind of targeted trimming that Go templates provide in other ecosystems, but without requiring users to learn a template language. You say what data to keep; the renderer decides how to present it.
Tools built on a shared output scheme improve together. When eval sessions reveal that a heading level, table layout, or field ordering works better for agents, a new Markout version carries that improvement to every tool that uses it.
For Coding Agents
If you are an LLM or coding agent building a CLI tool that needs structured, readable output, see SKILL.md for step-by-step integration instructions and attribute reference.
Real-World Usage
Markout was created for dotnet-inspect, which uses all ten data relationships across 49 models to generate API inspection reports, diff analysis, dependency trees, and security summaries.
Documentation
- User Guide — Complete tutorial with attribute reference
- Shape System Design — Data projection model, shape tiers, admission criteria
- Specification — Format grammar and type inference rules
- Nested Lists Guide — Strategies for nested data structures
License
MIT
| Product | Versions 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. |
-
net10.0
- MarkdownTable.Formatting (>= 0.3.3)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Markout:
| Package | Downloads |
|---|---|
|
Markout.Templates
Template engine for Markout — compose human-authored documents with data-driven content |
|
|
Shelf.Core
Core library for shelf — personal knowledge graph with items, relationships, and seen-state |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on Markout:
| Repository | Stars |
|---|---|
|
richlander/dotnet-inspect
Tool to inspect .NET assets, like docker inspect and kubectl describe.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 0.22.0 | 773 | 7/6/2026 |
| 0.21.0 | 167 | 7/6/2026 |
| 0.20.0 | 117 | 7/6/2026 |
| 0.19.0 | 99 | 7/6/2026 |
| 0.18.0 | 107 | 7/6/2026 |
| 0.17.0 | 107 | 7/6/2026 |
| 0.16.1 | 118 | 7/5/2026 |
| 0.16.0 | 90 | 7/5/2026 |
| 0.15.0 | 234 | 7/3/2026 |
| 0.14.0 | 1,141 | 6/26/2026 |
| 0.13.9 | 115 | 6/22/2026 |
| 0.13.8 | 158 | 6/22/2026 |
| 0.13.7 | 2,420 | 6/22/2026 |
| 0.13.6 | 3,105 | 6/16/2026 |
| 0.13.5 | 2,833 | 6/10/2026 |
| 0.13.4 | 121 | 6/9/2026 |
| 0.13.3 | 354 | 6/9/2026 |
| 0.13.2 | 160 | 6/9/2026 |
| 0.13.1 | 266 | 6/8/2026 |
| 0.13.0 | 147 | 6/8/2026 |
Quality follow-ups to the 0.21.0 goal-aware annotations: (1) [#139] Composite columns of an element table (List<T> with a Change<V> or other composite property) now decompose into typed {column}_{sub} fields for structured formatters (TSV/JSONL) — score_before/score_after/score_delta_pct, bugs_direction/bugs_status, tasks_delta_count/tasks_delta_noun — so the card is reconstructable from the data; Markdown keeps the dense cell (100 → 50 (-50%)). [MarkoutDeltaNoun] now also decomposes (deltaCount/deltaNoun), and [MarkoutIgnoreColumnWhen] hidden columns drop from the decomposed output too. (2) [#140] Goal direction/status derivation uses an exact numeric path, so adjacent large integral values past 2^53 classify correctly and the noise tolerance is an exact rational comparison. (3) [#141] When a Delta.Multiple aligns with its goal, the redundant inline status word is suppressed (dense Markdown only); a conflicting status is kept. (4) [#115] MarkoutWriter field-render methods take a zero-allocation fast path when no field projection is configured.