This document specifies the Markdown rendering integration for XenoAtom.Terminal.UI as a separate NuGet package:
XenoAtom.Terminal.UI.Extensions.MarkdownMarkdig NuGet package, 1.0.*)DocumentFlow (virtualized block feed)The core UI library MUST NOT take a Markdig dependency; Markdown support lives entirely in the extension package.
This is a contributor-facing spec. End-user documentation (under site/docs/controls) will be added with the first
implementation and demo screenshots.
[!NOTE], [!TIP], …; see “Alert blocks”).DocumentFlow block virtualization.Paragraph for rich wrapped text,Table for tables (no new table implementation),LogControl for large preformatted/code blocks (with optional height caps),Rule, Group, Padder, VStack, etc. as building blocks.MarkdownControl,IDocumentFlowContent for DocumentFlow.TextArea/editors).MarkdownControlThe primary “easy” entry point to render a single Markdown document.
Namespace: XenoAtom.Terminal.UI.Controls (control discoverability stays consistent with other controls).
namespace XenoAtom.Terminal.UI.Controls;
public sealed partial class MarkdownControl : Visual, IScrollable
{
public MarkdownControl();
public MarkdownControl(string markdown);
public MarkdownControl(Func<string> markdown);
public MarkdownControl(Binding<string?> markdown);
[Bindable] public partial string? Markdown { get; set; }
// Optional: provide a custom Markdig pipeline; when null, a default pipeline is used.
[Bindable] public partial Markdig.MarkdownPipeline? Pipeline { get; set; }
// Optional: used to resolve relative links (e.g. "docs/readme.md") to absolute URIs.
[Bindable] public partial Uri? BaseUri { get; set; }
// Optional knobs for block rendering decisions.
[Bindable] public partial MarkdownRenderOptions Options { get; set; }
// Expose scrolling (forwarded to the internal DocumentFlow).
public ScrollModel Scroll { get; }
}
Design notes:
MarkdownControl is a composite control that internally hosts a DocumentFlow with a single item.Markdown changes, it re-parses (or re-renders from a cached AST if available) and updates the underlying
IDocumentFlowContent.MarkdownDocumentContentTo support conversation feeds (many Markdown “documents”), the extension package SHOULD provide a reusable
IDocumentFlowContent implementation that can be appended to DocumentFlow.Items.
namespace XenoAtom.Terminal.UI.Extensions.Markdown;
public sealed class MarkdownDocumentContent : IDocumentFlowContent
{
public MarkdownDocumentContent(
string markdown,
Markdig.MarkdownPipeline? pipeline = null,
Uri? baseUri = null,
MarkdownRenderOptions? options = null);
public int Version { get; }
public int BlockCount { get; }
public DocumentFlowBlock GetBlock(int index);
}
This type enables:
MarkdownDocumentContent per chat message,DocumentFlow append-only semantics,The extension package MUST provide a default pipeline that:
Suggested approach (implementation detail):
MarkdownPipelineBuilder.Configure(...) so it is explicit and easy to audit."common+pipetables+alerts"Optional tokens (if deemed useful in v1, but not required by this spec):
autolinks (turns bare URLs into links),tasklists (GitHub task list items),emphasisextras (strikethrough/sub/sup),smartypants (typographic punctuation).The control MUST accept a user-provided MarkdownPipeline to enable/disable extensions as needed.
Markdown rendering MUST be styleable via a single style key so callers can theme Markdown without custom renderers.
MarkdownStyleProposed style record:
namespace XenoAtom.Terminal.UI.Extensions.Markdown.Styling;
public sealed record MarkdownStyle : IStyle<MarkdownStyle>
{
public static MarkdownStyle Default { get; }
public static StyleKey<MarkdownStyle> Key { get; }
// Block roles
public MarkdownHeadingStyle Heading1 { get; init; }
public MarkdownHeadingStyle Heading2 { get; init; }
public MarkdownHeadingStyle Heading3 { get; init; }
public MarkdownHeadingStyle Heading4 { get; init; }
public MarkdownHeadingStyle Heading5 { get; init; }
public MarkdownHeadingStyle Heading6 { get; init; }
public MarkdownParagraphStyle Paragraph { get; init; }
public MarkdownQuoteStyle Quote { get; init; }
public MarkdownCodeBlockStyle CodeBlock { get; init; }
public MarkdownTableStyle Table { get; init; }
public MarkdownListStyle List { get; init; }
public MarkdownAlertStyles Alerts { get; init; }
public Style ResolveLinkStyle(Theme theme);
public Style ResolveInlineCodeStyle(Theme theme);
}
Guidance:
TableStyle preset selection),Markdown alert blocks and (optionally) quotes SHOULD use existing chrome controls (Group, Border, Padder) to avoid
new bespoke rendering code:
Group with a label (e.g. NOTE) + background style, containing a VStack of rendered child blocks.Paragraph.LinePrefix/ContinuationPrefix), orBorder/Padder) when hosting non-paragraph child blocks.Markdown is rendered into a flat list of DocumentFlowBlock descriptors (one list per “document”).
General rule:
DocumentFlowBlock per top-level Markdown block.DocumentFlow
can virtualize at block granularity without deep visual trees.ParagraphDocumentFlowBlock
Wrap = trueRuns and Hyperlinks populated from inline renderingHeadingBlock level 1–6 → ParagraphDocumentFlowBlock (or VisualDocumentFlowBlock hosting Paragraph)
Inline spans map to StyledRun segments:
TextStyle.ItalicTextStyle.BoldTextStyle.StrikethroughRuns MUST be merged when adjacent and identical to reduce allocations.
HyperlinkRun(start, length, uri) on the rendered text span.BaseUri when provided.Images:
), with the URL as a hyperlink.\n) so Paragraph creates a hard line break.Markdown quote blocks can contain nested blocks. The renderer should:
LinePrefix = "│ ", ContinuationPrefix = "│ "Indent increased for nested quote depthPadder(left: quoteIndent) and optionally
render an accent bar via an adjacent VStack/Grid pattern if needed.Lists MUST render without creating a new dedicated list control.
Preferred rendering strategy:
LinePrefix = bulletOrNumber + " "HangingIndent so wrapped lines align under the item content.LinePrefix = new string(' ', bulletWidth + 1) (continuation alignment)Nested lists increase Indent accordingly.
Task lists (optional extension):
"[ ] " / "[x] " prefixes (styleable).Code blocks can be large; they should not expand the overall document height unboundedly by default.
Default rendering:
LogControl hosted as a VisualDocumentFlowBlock:
WrapText = false by default (preserve lines),Focusable = false by default (outer scroll remains primary),MaxHeight configurable (e.g. 12–20 rows) so code blocks remain scannable.Paragraph block.Markdown tables MUST be rendered by hosting the existing Table control.
Baseline requirement:
UsePipeTables).Mapping (v1):
Table.HeaderCellsTable.RowCellsParagraph so inline formatting (emphasis/code/links) is preserved.Column alignment:
Paragraph.TextAlignment for the cell visual.Grid tables:
UseGridTables) may include spanning and multi-block cell content.--- / *** → Rule control.CommonMark includes raw HTML. Terminal rendering MUST be safe:
MarkdownRenderOptions).No HTML execution exists; this is purely a display choice.
The extension MUST be efficient under repeated updates and large feeds:
DocumentFlowBlock.ReuseKey SHOULD be stable per block (e.g. based on Markdig source span) so DocumentFlow can recycle
visuals when the document re-renders with small changes.Add tests in a dedicated test project (or in XenoAtom.Terminal.UI.Tests with a project reference) covering:
StyledRun[] and HyperlinkRun[] boundaries.Table (header + rows) and preserve inline styles in cells.[!NOTE], [!WARNING], etc. render with correct title + styles and nested content.DocumentFlow extent.Add a ControlsDemo page: