This document captures the design and implementation details of Markup (the control, not the markup language).
CellBuffer.TextBlock nodes.Markup as a small, fast, allocation-conscious visual that participates in the binding dirty model.TextBox/TextArea/TextEditorBase-derived controls).Markup is a Visual with these bindable properties:
Text : string? - Markup source text. null is treated as empty.Wrap : bool - When true, wraps hard lines to the available width.TextAlignment : TextAlignment - Left/Center/Right/Justify (see behavior notes; justify is not implemented).Trimming : TextTrimming - Clip/EndEllipsis/StartEllipsis (applies only when Wrap is false).Constructors:
Markup()Markup(string markup)Markup(ref AnsiMarkupInterpolatedStringHandler handler)Markup(Func<string> markup) (binds Text)Markup(Func<AnsiMarkupInterpolatedStringHandler> handler) (binds Text)Markup parses Text into:
_plainText (markup stripped)_runs : StyledRun[] (spans in _plainText associated with a Style)The parser is MarkupTextParser (src/XenoAtom.Terminal.UI/Text/MarkupTextParser.cs), which uses XenoAtom.Ansi (AnsiMarkup)
to process markup tags and convert ANSI styles into Terminal.UI Style values.
Markup uses GetTheme().GetMarkupStyles() as the dictionary of custom style tokens (e.g. [primary]...[/]).
This lets themes define additional tag names besides the built-in XenoAtom.Ansi set.
Markup caches parsing using reference equality:
Text instance reference changes, or when the GetMarkupStyles() dictionary instance reference changes.This cache is an internal detail and can change, but it is important to understand when analyzing performance.
MeasureCore computes desired size from _plainText:
Wrap is false, height is the number of hard lines (clamped to available height).Wrap is true, height is the number of wrapped lines (clamped), and width is the wrapping width chosen by measure.Notes:
\n, \r\n, and \r.TerminalTextUtility.GetWidth(...) to account for terminal cell width.Markup has no custom ArrangeCore; it renders in its Bounds and does not allocate child visuals.
Markup does not fill its background. It only writes glyphs (with styles) into the CellBuffer.Style.None (transparent, no override).When Wrap is true:
Bounds.Width.TextAlignment.Left, Center, and Right align each rendered line within Bounds.Width.TextAlignment.Justify is treated as Left (there is no justification implementation).Trimming applies only when Wrap is false and the hard line exceeds Bounds.Width:
Clip: render only the prefix that fits.EndEllipsis: render the prefix and then an ellipsis in the last cell.StartEllipsis: render an ellipsis and then the suffix that fits.Implementation note: the ellipsis cell is currently written using Style.None (not the style of the adjacent run).
Markup is display-only and does not expose commands.
Tests:
src/XenoAtom.Terminal.UI.Tests/MarkupTextParserTests.cs (plain text and style runs)src/XenoAtom.Terminal.UI.Tests/MarkupMeasureTests.cs (measurement and wrapping)src/XenoAtom.Terminal.UI.Tests/MarkupRenderingTests.cs (hard line breaks)Demo:
samples/ControlsDemo/Demos/MarkupDemo.csUser documentation:
site/docs/controls/markup.mdStyle.None).