using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; using Spectre.Console.Internal; namespace Spectre.Console.Composition { /// /// Represents a renderable segment. /// [DebuggerDisplay("{Text,nq}")] public class Segment { /// /// Gets the segment text. /// public string Text { get; } /// /// Gets a value indicating whether or not this is an expicit line break /// that should be preserved. /// public bool IsLineBreak { get; } /// /// Gets the segment style. /// public Style Style { get; } /// /// 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) { Text = text?.NormalizeLineEndings() ?? throw new ArgumentNullException(nameof(text)); Style = style; IsLineBreak = lineBreak; } /// /// Creates a segment that represents an implicit line break. /// /// A segment that represents an implicit line break. public static Segment LineBreak() { return new Segment("\n", Style.Plain, true); } /// /// 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'), 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.Length + segment.Text.Length > maxWidth) { var diff = -(maxWidth - (line.Length + 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.Length > 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.Length > 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; } } }