using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Spectre.Console.Internal; namespace Spectre.Console.Rendering { /// /// Represents a renderable segment. /// [DebuggerDisplay("{Text,nq}")] public class Segment { /// /// Gets the segment text. /// public string Text { get; private set; } /// /// Gets a value indicating whether or not this is an expicit line break /// that should be preserved. /// public bool IsLineBreak { get; } /// /// Gets a value indicating whether or not this is a whitespace /// that should be preserved but not taken into account when /// layouting text. /// public bool IsWhiteSpace { get; } /// /// Gets the segment style. /// public Style Style { get; } /// /// Gets a segment representing a line break. /// public static Segment LineBreak => new Segment(Environment.NewLine, Style.Plain, true); /// /// Gets an empty segment. /// public static Segment Empty => new Segment(string.Empty, Style.Plain, false); /// /// Initializes a new instance of the class. /// /// The segment text. public Segment(string text) : this(text, Style.Plain) { } /// /// Initializes a new instance of the class. /// /// The segment text. /// The segment style. public Segment(string text, Style style) : this(text, style, false) { } private Segment(string text, Style style, bool lineBreak) { if (text is null) { throw new ArgumentNullException(nameof(text)); } Text = text.NormalizeLineEndings(); Style = style; IsLineBreak = lineBreak; IsWhiteSpace = string.IsNullOrWhiteSpace(text); } /// /// Gets the number of cells that this segment /// occupies in the console. /// /// The encoding to use. /// The number of cells that this segment occupies in the console. public int CellLength(Encoding encoding) { return Text.CellLength(encoding); } /// /// Returns a new segment without any trailing line endings. /// /// A new segment without any trailing line endings. public Segment StripLineEndings() { return new Segment(Text.TrimEnd('\n').TrimEnd('\r'), Style); } /// /// Splits the segment at the offset. /// /// The offset where to split the segment. /// One or two new segments representing the split. public (Segment First, Segment? Second) Split(int offset) { if (offset < 0) { return (this, null); } if (offset >= Text.Length) { return (this, null); } return ( new Segment(Text.Substring(0, offset), Style), new Segment(Text.Substring(offset, Text.Length - offset), Style)); } /// /// Splits the provided segments into lines. /// /// The segments to split. /// A collection of lines. public static List SplitLines(IEnumerable segments) { return SplitLines(segments, int.MaxValue); } /// /// Splits the provided segments into lines with a maximum width. /// /// The segments to split into lines. /// The maximum width. /// A list of lines. public static List SplitLines(IEnumerable segments, int maxWidth) { if (segments is null) { throw new ArgumentNullException(nameof(segments)); } var lines = new List(); var line = new SegmentLine(); var stack = new Stack(segments.Reverse()); while (stack.Count > 0) { var segment = stack.Pop(); if (line.Width + segment.Text.Length > maxWidth) { var diff = -(maxWidth - (line.Width + segment.Text.Length)); var offset = segment.Text.Length - diff; var (first, second) = segment.Split(offset); line.Add(first); lines.Add(line); line = new SegmentLine(); if (second != null) { stack.Push(second); } continue; } if (segment.Text.Contains("\n")) { if (segment.Text == "\n") { if (line.Width > 0 || segment.IsLineBreak) { lines.Add(line); line = new SegmentLine(); } continue; } var text = segment.Text; while (text != null) { var parts = text.SplitLines(); if (parts.Length > 0) { if (parts[0].Length > 0) { line.Add(new Segment(parts[0], segment.Style)); } } if (parts.Length > 1) { if (line.Width > 0) { lines.Add(line); line = new SegmentLine(); } text = string.Concat(parts.Skip(1).Take(parts.Length - 1)); } else { text = null; } } } else { line.Add(segment); } } if (line.Count > 0) { lines.Add(line); } return lines; } internal static IEnumerable Merge(IEnumerable segments) { var result = new List(); var previous = (Segment?)null; foreach (var segment in segments) { if (previous == null) { previous = segment; continue; } // Same style? if (previous.Style.Equals(segment.Style)) { // Modify the content of the previous segment previous.Text += segment.Text; } else { // Push the current one to the results. result.Add(previous); previous = segment; } } if (previous != null) { result.Add(previous); } return result; } /// /// Splits an overflowing segment into several new segments. /// /// The segment to split. /// The overflow strategy to use. /// The encodign to use. /// The maxiumum width. /// A list of segments that has been split. public static List SplitOverflow(Segment segment, Overflow? overflow, Encoding encoding, int width) { if (segment is null) { throw new ArgumentNullException(nameof(segment)); } if (segment.CellLength(encoding) <= width) { return new List(1) { segment }; } // Default to folding overflow ??= Overflow.Fold; var result = new List(); if (overflow == Overflow.Fold) { var totalLength = segment.Text.CellLength(encoding); var lengthLeft = totalLength; while (lengthLeft > 0) { var index = totalLength - lengthLeft; var take = Math.Min(width, totalLength - index); result.Add(new Segment(segment.Text.Substring(index, take), segment.Style)); lengthLeft -= take; } } else if (overflow == Overflow.Crop) { result.Add(new Segment(segment.Text.Substring(0, width), segment.Style)); } else if (overflow == Overflow.Ellipsis) { result.Add(new Segment(segment.Text.Substring(0, width - 1) + "…", segment.Style)); } return result; } internal static Segment TruncateWithEllipsis(string text, Style style, Encoding encoding, int maxWidth) { return SplitOverflow( new Segment(text, style), Overflow.Ellipsis, encoding, maxWidth).First(); } internal static List> MakeSameHeight(int cellHeight, List> cells) { foreach (var cell in cells) { if (cell.Count < cellHeight) { while (cell.Count != cellHeight) { cell.Add(new SegmentLine()); } } } return cells; } } }