using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Spectre.Console.Composition; using Spectre.Console.Internal; namespace Spectre.Console { /// /// Represents text with color and decorations. /// [SuppressMessage("Naming", "CA1724:Type names should not match namespaces")] [DebuggerDisplay("{_text,nq}")] public sealed class Text : IRenderable { private readonly List _spans; private string _text; /// /// Gets or sets the text alignment. /// public Justify Alignment { get; set; } = Justify.Left; private sealed class Span { public int Start { get; } public int End { get; } public Style Style { get; } public Span(int start, int end, Style style) { Start = start; End = end; Style = style ?? Style.Plain; } } /// /// Initializes a new instance of the class. /// /// The text. internal Text(string text) { _text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text)); _spans = new List(); } /// /// Initializes a new instance of the class. /// /// The text. /// The foreground. /// The background. /// The text decoration. /// A instance. public static Text New( string text, Color? foreground = null, Color? background = null, Decoration? decoration = null) { var result = MarkupParser.Parse(text, new Style(foreground, background, decoration)); return result; } /// /// Sets the text alignment. /// /// The text alignment. /// The same instance. public Text WithAlignment(Justify alignment) { Alignment = alignment; return this; } /// /// Appends some text with the specified color and decorations. /// /// The text to append. /// The text style. public void Append(string text, Style style) { if (text == null) { throw new ArgumentNullException(nameof(text)); } var start = _text.Length; var end = _text.Length + text.Length; _text += text; Stylize(start, end, style); } /// /// Stylizes a part of the text. /// /// The start position. /// The end position. /// The style to apply. public void Stylize(int start, int end, Style style) { if (start >= end) { throw new ArgumentOutOfRangeException(nameof(start), "Start position must be less than the end position."); } start = Math.Max(start, 0); end = Math.Min(end, _text.Length); _spans.Add(new Span(start, end, style)); } /// Measurement IRenderable.Measure(RenderContext context, int maxWidth) { if (string.IsNullOrEmpty(_text)) { return new Measurement(1, 1); } // TODO: Write some kind of tokenizer for this var min = Segment.SplitLines(((IRenderable)this).Render(context, maxWidth)) .SelectMany(line => line.Select(segment => segment.Text.Length)) .Max(); var max = _text.SplitLines().Max(x => Cell.GetCellLength(context.Encoding, x)); return new Measurement(min, max); } /// IEnumerable IRenderable.Render(RenderContext context, int width) { if (string.IsNullOrEmpty(_text)) { return Array.Empty(); } if (width == 0) { return Array.Empty(); } var result = new List(); var segments = SplitLineBreaks(CreateSegments()); var justification = context.Justification ?? Alignment; foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate()) { var length = line.Sum(l => l.StripLineEndings().CellLength(context.Encoding)); if (length < width) { // Justify right side if (justification == Justify.Right) { var diff = width - length; result.Add(new Segment(new string(' ', diff))); } else if (justification == Justify.Center) { var diff = (width - length) / 2; result.Add(new Segment(new string(' ', diff))); } } // Render the line. foreach (var segment in line) { result.Add(segment.StripLineEndings()); } // Justify left side if (length < width) { if (justification == Justify.Center) { var diff = (width - length) / 2; result.Add(new Segment(new string(' ', diff))); var remainder = (width - length) % 2; if (remainder != 0) { result.Add(new Segment(new string(' ', remainder))); } } } if (!last || line.Count == 0) { result.Add(Segment.LineBreak()); } } return result; } private static IEnumerable SplitLineBreaks(IEnumerable segments) { // Creates individual segments of line breaks. var result = new List(); var queue = new Stack(segments.Reverse()); while (queue.Count > 0) { var segment = queue.Pop(); var index = segment.Text.IndexOf("\n", StringComparison.OrdinalIgnoreCase); if (index == -1) { if (!string.IsNullOrEmpty(segment.Text)) { result.Add(segment); } } else { var (first, second) = segment.Split(index); if (!string.IsNullOrEmpty(first.Text)) { result.Add(first); } result.Add(Segment.LineBreak()); if (second != null) { queue.Push(new Segment(second.Text.Substring(1), second.Style)); } } } return result; } private IEnumerable CreateSegments() { // This excellent algorithm to sort spans was ported and adapted from // https://github.com/willmcgugan/rich/blob/eb2f0d5277c159d8693636ec60c79c5442fd2e43/rich/text.py#L492 // Create the style map. var styleMap = _spans.SelectIndex((span, index) => (span, index)).ToDictionary(x => x.index + 1, x => x.span.Style); styleMap[0] = Style.Plain; // Create a span list. var spans = new List<(int Offset, bool Leaving, int Style)>(); spans.AddRange(_spans.SelectIndex((span, index) => (span.Start, false, index + 1))); spans.AddRange(_spans.SelectIndex((span, index) => (span.End, true, index + 1))); spans = spans.OrderBy(x => x.Offset).ThenBy(x => !x.Leaving).ToList(); // Keep track of applied styles using a stack var styleStack = new Stack(); // Now build the segments. var result = new List(); foreach (var (offset, leaving, style, nextOffset) in BuildSkipList(spans)) { if (leaving) { // Leaving styleStack.Pop(); } else { // Entering styleStack.Push(style); } if (nextOffset > offset) { // Build the current style from the stack var styleIndices = styleStack.OrderBy(index => index).ToArray(); var currentStyle = Style.Plain.Combine(styleIndices.Select(index => styleMap[index])); // Create segment var text = _text.Substring(offset, Math.Min(_text.Length - offset, nextOffset - offset)); result.Add(new Segment(text, currentStyle)); } } return result; } private static IEnumerable<(int Offset, bool Leaving, int Style, int NextOffset)> BuildSkipList( List<(int Offset, bool Leaving, int Style)> spans) { return spans.Zip(spans.Skip(1), (first, second) => (first, second)).Select( x => (x.first.Offset, x.first.Leaving, x.first.Style, NextOffset: x.second.Offset)); } } }