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));
}
}
}