Markout — structured output from objects
Package Markout (the source generator ships in it — no extra package). Default output is
Markdown. Reach for Markout whenever a tool would otherwise build strings with
Console.WriteLine / StringBuilder.
Everything you need is in these skills. Do NOT web_search / web_fetch for Markout usage —
this base skill plus the domain skills below are authoritative and version-matched to the package.
If an idiom isn't here, pull the matching domain skill (listed at the end); don't go to the web.
The required pattern (3 parts — all mandatory)
using Markout;
// 1. Annotate every model type. List<T> -> table, scalar -> field.
[MarkoutSerializable(TitleProperty = nameof(Title))] // TitleProperty -> the H1 heading
public class Report
{
public string Title { get; set; } = "";
public int Count { get; set; } // scalar -> "Count | 3" field row
[MarkoutSection(Name = "Items")] // -> "## Items" heading
public List<Row>? Items { get; set; } // List<T> -> a table
}
[MarkoutSerializable]
public class Row { public string Name { get; set; } = ""; public string Value { get; set; } = ""; }
// 2. Register EVERY type on a partial context (the source generator fills it in).
[MarkoutContext(typeof(Report))]
[MarkoutContext(typeof(Row))]
public partial class ReportContext : MarkoutSerializerContext { }
// 3. Serialize THROUGH the context.
MarkoutSerializer.Serialize(report, Console.Out, ReportContext.Default);
Scalar field shaping (title, description, per-value formatting)
Shape scalar properties with attributes — never pre-format strings in the model or hand-write rows:
[MarkoutSerializable(
TitleProperty = nameof(Name), // -> the H1 heading
DescriptionProperty = nameof(Summary), // -> a paragraph under the H1 (NOT a Field | Value row)
FieldLayout = FieldLayout.Inline)] // Table (default) | Inline | Bulleted | Numbered | Plain
public class Component
{
public string Name { get; set; } = "";
public string Summary { get; set; } = "";
[MarkoutDisplayFormat("{0:N0} downloads")] // 5100000 -> "5,100,000 downloads"
public long Downloads { get; set; }
[MarkoutDisplayFormat("{0:yyyy-MM-dd}")] // DateTime -> "2024-06-01"
public DateTime Published { get; set; }
[MarkoutBoolFormat("Yes", "No")] // true -> "Yes", false -> "No"
public bool Verified { get; set; }
}
DescriptionProperty renders a property as a description paragraph, not a table row.
FieldLayout.Inline puts the scalar fields on one line (Owner: … | Status: …) instead of a table.
[MarkoutDisplayFormat("{0:…}")] / [MarkoutBoolFormat(t,f)] format a value in place — do not bake
the formatting into the getter or build the cell string yourself.
Gotchas (where System.Text.Json intuition is wrong)
- No reflection fallback. There is no
Serialize(obj) overload. EVERY Serialize call takes a
MarkoutSerializerContext. Omitting it does not compile — the #1 mistake.
- Register every type. A model missing
[MarkoutSerializable] + [MarkoutContext(typeof(T))]
won't serialize. The context class MUST be partial.
- Markout attributes, not Json:
[MarkoutSerializable] (not [JsonSerializable]),
[MarkoutContext], [MarkoutSection(Name=...)], [MarkoutPropertyName], [MarkoutIgnore].
- Type drives rendering, not markup:
List<T> -> table; scalar -> Field | Value row;
[MarkoutSection(Name="X")] -> a ## X heading above the property.
[MarkoutIgnoreInTable] on non-tabular list properties (List<Metric>, List<Breakdown>,
List<TreeNode>, List<Description>, Callout) or they get mistreated as table columns.
Most common workflow: JSON API → model → report
Fetch JSON, project to a Markout model (a plain data model is fine — no separate visual layer),
serialize. Keep the JSON DTO and the Markout model separate; project between them with LINQ.
Which skill for what
Author declaratively — describe the data, let the type/attributes drive output. Do NOT hand-roll
if/StringBuilder for things below. Pull the matching skill:
- conditional-composition — show/hide sections & columns from the data; filter to sections;
one model, many shapes (
ShowWhenProperty, IgnoreColumnWhen, IncludeSections, same-name sections).
- output-formats — plain text, ANSI/Spectre, pretty tables, TSV/JSONL, multi-format dispatch.
- built-in-shapes —
Metric, Breakdown, Callout, TreeNode, Description, CodeSection.
- composite-cells-cards — dense-Markdown-cell ↔ decomposed-column data (
Change<V>, Fraction,
Share, Percent, Segments, Delta/Goal) and metric/role/verdict cards.
1---2name: markout3description: Use when generating Markdown or other structured output (plain text, ANSI, pretty tables, TSV/JSONL) from C# objects instead of hand-built strings — CLIs, tools, reports, agent output. Markout is a source-generated .NET serializer: it looks like System.Text.Json source-gen but the rules differ (NO reflection fallback), so it needs a generated MarkoutSerializerContext and Markout-specific attributes. Start here for the required pattern; branch to the domain skills for conditional reports, multi-view/verbosity, output formats, built-in shapes, and composite cells/cards. Don't decompile the Markout assembly or web-search its API — every idiom you need is in these skills.4---56# Markout — structured output from objects78Package `Markout` (the source generator ships in it — no extra package). Default output is9Markdown. Reach for Markout whenever a tool would otherwise build strings with10`Console.WriteLine` / `StringBuilder`.1112> **Everything you need is in these skills.** Do NOT `web_search` / `web_fetch` for Markout usage —13> this base skill plus the domain skills below are authoritative and version-matched to the package.14> If an idiom isn't here, pull the matching domain skill (listed at the end); don't go to the web.1516## The required pattern (3 parts — all mandatory)1718```csharp19using Markout;2021// 1. Annotate every model type. List<T> -> table, scalar -> field.22[MarkoutSerializable(TitleProperty = nameof(Title))] // TitleProperty -> the H1 heading23public class Report24{25 public string Title { get; set; } = "";26 public int Count { get; set; } // scalar -> "Count | 3" field row27 [MarkoutSection(Name = "Items")] // -> "## Items" heading28 public List<Row>? Items { get; set; } // List<T> -> a table29}3031[MarkoutSerializable]32public class Row { public string Name { get; set; } = ""; public string Value { get; set; } = ""; }3334// 2. Register EVERY type on a partial context (the source generator fills it in).35[MarkoutContext(typeof(Report))]36[MarkoutContext(typeof(Row))]37public partial class ReportContext : MarkoutSerializerContext { }3839// 3. Serialize THROUGH the context.40MarkoutSerializer.Serialize(report, Console.Out, ReportContext.Default);41```4243## Scalar field shaping (title, description, per-value formatting)4445Shape scalar properties with attributes — never pre-format strings in the model or hand-write rows:4647```csharp48[MarkoutSerializable(49 TitleProperty = nameof(Name), // -> the H1 heading50 DescriptionProperty = nameof(Summary), // -> a paragraph under the H1 (NOT a Field | Value row)51 FieldLayout = FieldLayout.Inline)] // Table (default) | Inline | Bulleted | Numbered | Plain52public class Component53{54 public string Name { get; set; } = "";55 public string Summary { get; set; } = "";5657 [MarkoutDisplayFormat("{0:N0} downloads")] // 5100000 -> "5,100,000 downloads"58 public long Downloads { get; set; }5960 [MarkoutDisplayFormat("{0:yyyy-MM-dd}")] // DateTime -> "2024-06-01"61 public DateTime Published { get; set; }6263 [MarkoutBoolFormat("Yes", "No")] // true -> "Yes", false -> "No"64 public bool Verified { get; set; }65}66```6768- `DescriptionProperty` renders a property as a description paragraph, not a table row.69- `FieldLayout.Inline` puts the scalar fields on one line (`Owner: … | Status: …`) instead of a table.70- `[MarkoutDisplayFormat("{0:…}")]` / `[MarkoutBoolFormat(t,f)]` format a value **in place** — do not bake71 the formatting into the getter or build the cell string yourself.7273## Gotchas (where System.Text.Json intuition is wrong)7475- **No reflection fallback.** There is no `Serialize(obj)` overload. EVERY `Serialize` call takes a76 `MarkoutSerializerContext`. Omitting it does not compile — the #1 mistake.77- **Register every type.** A model missing `[MarkoutSerializable]` + `[MarkoutContext(typeof(T))]`78 won't serialize. The context class MUST be `partial`.79- **Markout attributes, not Json:** `[MarkoutSerializable]` (not `[JsonSerializable]`),80 `[MarkoutContext]`, `[MarkoutSection(Name=...)]`, `[MarkoutPropertyName]`, `[MarkoutIgnore]`.81- **Type drives rendering, not markup:** `List<T>` -> table; scalar -> `Field | Value` row;82 `[MarkoutSection(Name="X")]` -> a `## X` heading above the property.83- **`[MarkoutIgnoreInTable]` on non-tabular list properties** (`List<Metric>`, `List<Breakdown>`,84 `List<TreeNode>`, `List<Description>`, `Callout`) or they get mistreated as table columns.8586## Most common workflow: JSON API → model → report8788Fetch JSON, project to a Markout model (a plain data model is fine — no separate visual layer),89serialize. Keep the JSON DTO and the Markout model separate; project between them with LINQ.9091## Which skill for what9293Author declaratively — describe the data, let the type/attributes drive output. Do NOT hand-roll94`if`/`StringBuilder` for things below. Pull the matching skill:9596- **conditional-composition** — show/hide sections & columns from the data; filter to sections;97 one model, many shapes (`ShowWhenProperty`, `IgnoreColumnWhen`, `IncludeSections`, same-name sections).98- **output-formats** — plain text, ANSI/Spectre, pretty tables, TSV/JSONL, multi-format dispatch.99- **built-in-shapes** — `Metric`, `Breakdown`, `Callout`, `TreeNode`, `Description`, `CodeSection`.100- **composite-cells-cards** — dense-Markdown-cell ↔ decomposed-column data (`Change<V>`, `Fraction`,101 `Share`, `Percent`, `Segments`, `Delta`/`Goal`) and metric/role/verdict cards.