using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Spectre.Console.Internal; using Spectre.Console.Rendering; namespace Spectre.Console { /// /// A paragraph of text where different parts /// of the paragraph can have individual styling. /// [DebuggerDisplay("{_text,nq}")] public sealed class Paragraph : Renderable, IAlignable, IOverflowable { private readonly List _lines; /// /// Gets or sets the alignment of the whole paragraph. /// public Justify? Alignment { get; set; } /// /// Gets or sets the text overflow strategy. /// public Overflow? Overflow { get; set; } /// /// Initializes a new instance of the class. /// public Paragraph() { _lines = new List(); } /// /// Initializes a new instance of the class. /// /// The text. /// The style of the text. public Paragraph(string text, Style? style = null) : this() { if (text is null) { throw new ArgumentNullException(nameof(text)); } Append(text, style); } /// /// Appends some text to this paragraph. /// /// The text to append. /// The style of the appended text. /// The same instance so that multiple calls can be chained. public Paragraph Append(string text, Style? style = null) { if (text is null) { throw new ArgumentNullException(nameof(text)); } foreach (var (_, first, last, part) in text.SplitLines().Enumerate()) { var current = part; if (first) { var line = _lines.LastOrDefault(); if (line == null) { _lines.Add(new SegmentLine()); line = _lines.Last(); } if (string.IsNullOrEmpty(current)) { line.Add(Segment.Empty); } else { foreach (var span in current.SplitWords()) { line.Add(new Segment(span, style ?? Style.Plain)); } } } else { var line = new SegmentLine(); if (string.IsNullOrEmpty(current)) { line.Add(Segment.Empty); } else { foreach (var span in current.SplitWords()) { line.Add(new Segment(span, style ?? Style.Plain)); } } _lines.Add(line); } } return this; } /// protected override Measurement Measure(RenderContext context, int maxWidth) { if (_lines.Count == 0) { return new Measurement(0, 0); } var min = _lines.Max(line => line.Max(segment => segment.CellLength(context))); var max = _lines.Max(x => x.CellWidth(context)); return new Measurement(min, Math.Min(max, maxWidth)); } /// protected override IEnumerable Render(RenderContext context, int maxWidth) { if (context is null) { throw new ArgumentNullException(nameof(context)); } if (_lines.Count == 0) { return Array.Empty(); } var lines = context.SingleLine ? new List(_lines) : SplitLines(context, maxWidth); // Justify lines var justification = context.Justification ?? Alignment ?? Justify.Left; foreach (var (_, _, last, line) in lines.Enumerate()) { var length = line.Sum(l => l.StripLineEndings().CellLength(context)); if (length < maxWidth) { // Justify right side if (justification == Justify.Right) { var diff = maxWidth - length; line.Prepend(new Segment(new string(' ', diff))); } else if (justification == Justify.Center) { // Left side. var diff = (maxWidth - length) / 2; line.Prepend(new Segment(new string(' ', diff))); // Right side line.Add(new Segment(new string(' ', diff))); var remainder = (maxWidth - length) % 2; if (remainder != 0) { line.Add(new Segment(new string(' ', remainder))); } } } } if (context.SingleLine) { return lines.First().Where(segment => !segment.IsLineBreak); } return new SegmentLineEnumerator(lines); } private List Clone() { var result = new List(); foreach (var line in _lines) { var newLine = new SegmentLine(); foreach (var segment in line) { newLine.Add(segment); } result.Add(newLine); } return result; } private List SplitLines(RenderContext context, int maxWidth) { if (maxWidth <= 0) { // Nothing fits, so return an empty line. return new List(); } if (_lines.Max(x => x.CellWidth(context)) <= maxWidth) { return Clone(); } var lines = new List(); var line = new SegmentLine(); var newLine = true; using var iterator = new SegmentLineIterator(_lines); var queue = new Queue(); while (true) { var current = (Segment?)null; if (queue.Count == 0) { if (!iterator.MoveNext()) { break; } current = iterator.Current; } else { current = queue.Dequeue(); } if (current == null) { throw new InvalidOperationException("Iterator returned empty segment."); } newLine = false; if (current.IsLineBreak) { lines.Add(line); line = new SegmentLine(); newLine = true; continue; } var length = current.CellLength(context); if (length > maxWidth) { // The current segment is longer than the width of the console, // so we will need to crop it up, into new segments. var segments = Segment.SplitOverflow(current, Overflow, context, maxWidth); if (segments.Count > 0) { if (line.CellWidth(context) + segments[0].CellLength(context) > maxWidth) { lines.Add(line); line = new SegmentLine(); newLine = true; segments.ForEach(s => queue.Enqueue(s)); continue; } else { // Add the segment and push the rest of them to the queue. line.Add(segments[0]); segments.Skip(1).ForEach(s => queue.Enqueue(s)); continue; } } } else { if (line.CellWidth(context) + length > maxWidth) { line.Add(Segment.Empty); lines.Add(line); line = new SegmentLine(); newLine = true; } } if (newLine && current.IsWhiteSpace) { continue; } newLine = false; line.Add(current); } // Flush remaining. if (line.Count > 0) { lines.Add(line); } return lines; } } }