Conditional composition — one model, many views
The trap this skill prevents: hand-rolling if (hasErrors) sb.AppendLine("## Errors") and manual
column arithmetic. In Markout you declare the condition on the model; the generator renders the
right shape. This keeps one source of truth and makes the same model serve quiet/detail/export modes.
Conditional sections
[MarkoutSerializable(TitleProperty = nameof(Name))]
public class Inspection
{
public string Name { get; set; } = "";
// Section renders ONLY when HasFailures is true. Compute the predicate on the model.
[MarkoutSection(Name = "Failures", ShowWhenProperty = nameof(HasFailures))]
public List<FailureRow>? Failures { get; set; }
public bool HasFailures => Failures is { Count: > 0 };
// EmptyText shows a fallback paragraph when the list is non-null but empty; null omits the section.
[MarkoutSection(Name = "Warnings", EmptyText = "No warnings.")]
public List<WarnRow>? Warnings { get; set; }
}
ShowWhenProperty = nameof(Bool)gates a section on a bool property.[MarkoutShowWhen(nameof(Bool))]gates a scalar field the same way.[MarkoutSkipNull]/[MarkoutSkipDefault]drop individual fields when null/default.
Adaptive columns — hide what carries no information
// Drop the "Pattern" column when it's uniform/empty across the rows (keeps tables compact).
// The attribute goes on the SECTION list and names a static bool predicate + the column to hide.
[MarkoutSection(Name = "Matches")]
[MarkoutIgnoreColumnWhen(nameof(PatternIsUniform), "Pattern")]
public List<MatchRow>? Matches { get; set; }
public static bool PatternIsUniform(List<MatchRow>? rows)
=> rows?.Select(r => r.Pattern).Distinct().Count() <= 1;
// IgnoreProperty hides named columns unconditionally.
[MarkoutSection(Name = "Debug", IgnoreProperty = "InternalId,Debug")]
public List<MatchRow>? DebugRows { get; set; }
[MarkoutIgnoreColumnWhen(...)] is the declarative replacement for "compute distinct values, then
rebuild headers and rows." Columns hidden this way are also dropped from TSV/JSONL decomposition.
Filter to specific sections at render time
var options = new MarkoutWriterOptions { IncludeSections = new HashSet<string> { "Failures" } };
MarkoutSerializer.Serialize(report, Console.Out, new MarkdownFormatter(), ctx, options);
IncludeSections renders only the named sections — the caller-side lever for "just show me X"
without a second model, and the declarative way to drive quiet/detail/verbosity views from one model.
One model, many shapes: same-name section variants
When a mode needs a different projection of the same logical section (e.g. terse vs with-docs),
declare multiple properties with the same Name and gate them so exactly one renders:
[MarkoutSection(Name = "Members", ShowWhenProperty = nameof(Terse))]
public List<MemberRow>? MembersTerse { get; set; }
[MarkoutSection(Name = "Members", ShowWhenProperty = nameof(WithDocs))]
public List<MemberDocRow>? MembersWithDocs { get; set; }
This "same-name polymorphic section" is how a single report serves --docs, quiet, and select
modes without branching in the writer. Set the gating bools when you build the model.
Guardrails
- Prefer declaration over imperative assembly: no
if+AppendLine, no manual header/row rebuilding. - Keep predicates (
Has*,Terse) as computed properties on the model, next to the data. - Empty vs absent matters:
nulllist omits a section; empty list +EmptyTextshows the fallback.