mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-08-04 12:57:59 +08:00
Add support for tables
This commit is contained in:

committed by
Patrik Svensson

parent
aa34c145b9
commit
a068fc68c3
82
src/Spectre.Console/Composition/Border.cs
Normal file
82
src/Spectre.Console/Composition/Border.cs
Normal file
@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a border used by tables.
|
||||
/// </summary>
|
||||
public abstract class Border
|
||||
{
|
||||
private readonly Dictionary<BorderPart, char> _lookup;
|
||||
|
||||
private static readonly Dictionary<BorderKind, Border> _borders = new Dictionary<BorderKind, Border>
|
||||
{
|
||||
{ BorderKind.Ascii, new AsciiBorder() },
|
||||
{ BorderKind.Square, new SquareBorder() },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Border"/> class.
|
||||
/// </summary>
|
||||
protected Border()
|
||||
{
|
||||
_lookup = Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="Border"/> represented by the specified <see cref="BorderKind"/>.
|
||||
/// </summary>
|
||||
/// <param name="kind">The kind of border to get.</param>
|
||||
/// <returns>A <see cref="Border"/> instance representing the specified <see cref="BorderKind"/>.</returns>
|
||||
public static Border GetBorder(BorderKind kind)
|
||||
{
|
||||
if (!_borders.TryGetValue(kind, out var border))
|
||||
{
|
||||
throw new InvalidOperationException("Unknown border kind");
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
private Dictionary<BorderPart, char> Initialize()
|
||||
{
|
||||
var lookup = new Dictionary<BorderPart, char>();
|
||||
foreach (BorderPart part in Enum.GetValues(typeof(BorderPart)))
|
||||
{
|
||||
lookup.Add(part, GetBoxPart(part));
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of a specific border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get a string representation for.</param>
|
||||
/// <param name="count">The number of repetitions.</param>
|
||||
/// <returns>A string representation of the specified border part.</returns>
|
||||
public string GetPart(BorderPart part, int count)
|
||||
{
|
||||
return new string(GetBoxPart(part), count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string representation of a specific border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get a string representation for.</param>
|
||||
/// <returns>A string representation of the specified border part.</returns>
|
||||
public string GetPart(BorderPart part)
|
||||
{
|
||||
return _lookup[part].ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the character representing the specified border part.
|
||||
/// </summary>
|
||||
/// <param name="part">The part to get the character representation for.</param>
|
||||
/// <returns>A character representation of the specified border part.</returns>
|
||||
protected abstract char GetBoxPart(BorderPart part);
|
||||
}
|
||||
}
|
18
src/Spectre.Console/Composition/BorderKind.cs
Normal file
18
src/Spectre.Console/Composition/BorderKind.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents different kinds of borders.
|
||||
/// </summary>
|
||||
public enum BorderKind
|
||||
{
|
||||
/// <summary>
|
||||
/// A square border.
|
||||
/// </summary>
|
||||
Square = 0,
|
||||
|
||||
/// <summary>
|
||||
/// An old school ASCII border.
|
||||
/// </summary>
|
||||
Ascii = 1,
|
||||
}
|
||||
}
|
98
src/Spectre.Console/Composition/BorderPart.cs
Normal file
98
src/Spectre.Console/Composition/BorderPart.cs
Normal file
@ -0,0 +1,98 @@
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the different border parts.
|
||||
/// </summary>
|
||||
public enum BorderPart
|
||||
{
|
||||
/// <summary>
|
||||
/// The top left part of a header.
|
||||
/// </summary>
|
||||
HeaderTopLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The top part of a header.
|
||||
/// </summary>
|
||||
HeaderTop,
|
||||
|
||||
/// <summary>
|
||||
/// The top separator part of a header.
|
||||
/// </summary>
|
||||
HeaderTopSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The top right part of a header.
|
||||
/// </summary>
|
||||
HeaderTopRight,
|
||||
|
||||
/// <summary>
|
||||
/// The left part of a header.
|
||||
/// </summary>
|
||||
HeaderLeft,
|
||||
|
||||
/// <summary>
|
||||
/// A header separator.
|
||||
/// </summary>
|
||||
HeaderSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The right part of a header.
|
||||
/// </summary>
|
||||
HeaderRight,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom left part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom part of a header.
|
||||
/// </summary>
|
||||
HeaderBottom,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom separator part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom right part of a header.
|
||||
/// </summary>
|
||||
HeaderBottomRight,
|
||||
|
||||
/// <summary>
|
||||
/// The left part of a cell.
|
||||
/// </summary>
|
||||
CellLeft,
|
||||
|
||||
/// <summary>
|
||||
/// A cell separator.
|
||||
/// </summary>
|
||||
CellSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The right part of a cell.
|
||||
/// </summary>
|
||||
ColumnRight,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom left part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomLeft,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom part of a footer.
|
||||
/// </summary>
|
||||
FooterBottom,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom separator part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomSeparator,
|
||||
|
||||
/// <summary>
|
||||
/// The bottom right part of a footer.
|
||||
/// </summary>
|
||||
FooterBottomRight,
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Composition/Borders/AsciiBorder.cs
Normal file
37
src/Spectre.Console/Composition/Borders/AsciiBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an old school ASCII border.
|
||||
/// </summary>
|
||||
public sealed class AsciiBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override char GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => '+',
|
||||
BorderPart.HeaderTop => '-',
|
||||
BorderPart.HeaderTopSeparator => '-',
|
||||
BorderPart.HeaderTopRight => '+',
|
||||
BorderPart.HeaderLeft => '|',
|
||||
BorderPart.HeaderSeparator => '|',
|
||||
BorderPart.HeaderRight => '|',
|
||||
BorderPart.HeaderBottomLeft => '|',
|
||||
BorderPart.HeaderBottom => '-',
|
||||
BorderPart.HeaderBottomSeparator => '+',
|
||||
BorderPart.HeaderBottomRight => '|',
|
||||
BorderPart.CellLeft => '|',
|
||||
BorderPart.CellSeparator => '|',
|
||||
BorderPart.ColumnRight => '|',
|
||||
BorderPart.FooterBottomLeft => '+',
|
||||
BorderPart.FooterBottom => '-',
|
||||
BorderPart.FooterBottomSeparator => '-',
|
||||
BorderPart.FooterBottomRight => '+',
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
37
src/Spectre.Console/Composition/Borders/SquareBorder.cs
Normal file
37
src/Spectre.Console/Composition/Borders/SquareBorder.cs
Normal file
@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace Spectre.Console.Composition
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a square border.
|
||||
/// </summary>
|
||||
public sealed class SquareBorder : Border
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
protected override char GetBoxPart(BorderPart part)
|
||||
{
|
||||
return part switch
|
||||
{
|
||||
BorderPart.HeaderTopLeft => '┌',
|
||||
BorderPart.HeaderTop => '─',
|
||||
BorderPart.HeaderTopSeparator => '┬',
|
||||
BorderPart.HeaderTopRight => '┐',
|
||||
BorderPart.HeaderLeft => '│',
|
||||
BorderPart.HeaderSeparator => '│',
|
||||
BorderPart.HeaderRight => '│',
|
||||
BorderPart.HeaderBottomLeft => '├',
|
||||
BorderPart.HeaderBottom => '─',
|
||||
BorderPart.HeaderBottomSeparator => '┼',
|
||||
BorderPart.HeaderBottomRight => '┤',
|
||||
BorderPart.CellLeft => '│',
|
||||
BorderPart.CellSeparator => '│',
|
||||
BorderPart.ColumnRight => '│',
|
||||
BorderPart.FooterBottomLeft => '└',
|
||||
BorderPart.FooterBottom => '─',
|
||||
BorderPart.FooterBottomSeparator => '┴',
|
||||
BorderPart.FooterBottomRight => '┘',
|
||||
_ => throw new InvalidOperationException("Unknown box part."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
262
src/Spectre.Console/Composition/Table.cs
Normal file
262
src/Spectre.Console/Composition/Table.cs
Normal file
@ -0,0 +1,262 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Spectre.Console.Composition;
|
||||
using Spectre.Console.Internal;
|
||||
|
||||
namespace Spectre.Console
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a table.
|
||||
/// </summary>
|
||||
public sealed class Table : IRenderable
|
||||
{
|
||||
private readonly List<Text> _columns;
|
||||
private readonly List<List<Text>> _rows;
|
||||
private readonly Border _border;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Table"/> class.
|
||||
/// </summary>
|
||||
/// <param name="border">The border to use.</param>
|
||||
public Table(BorderKind border = BorderKind.Square)
|
||||
{
|
||||
_columns = new List<Text>();
|
||||
_rows = new List<List<Text>>();
|
||||
_border = Border.GetBorder(border);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a column to the table.
|
||||
/// </summary>
|
||||
/// <param name="column">The column to add.</param>
|
||||
public void AddColumn(string column)
|
||||
{
|
||||
if (column is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(column));
|
||||
}
|
||||
|
||||
_columns.Add(Text.New(column));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple columns to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The columns to add.</param>
|
||||
public void AddColumns(params string[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
_columns.AddRange(columns.Select(column => Text.New(column)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a row to the table.
|
||||
/// </summary>
|
||||
/// <param name="columns">The row columns to add.</param>
|
||||
public void AddRow(params string[] columns)
|
||||
{
|
||||
if (columns is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(columns));
|
||||
}
|
||||
|
||||
if (columns.Length < _columns.Count)
|
||||
{
|
||||
throw new InvalidOperationException("The number of row columns are less than the number of table columns.");
|
||||
}
|
||||
|
||||
if (columns.Length > _columns.Count)
|
||||
{
|
||||
throw new InvalidOperationException("The number of row columns are greater than the number of table columns.");
|
||||
}
|
||||
|
||||
_rows.Add(columns.Select(column => Text.New(column)).ToList());
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Measure(Encoding encoding, int maxWidth)
|
||||
{
|
||||
// Calculate the max width for each column.
|
||||
var maxColumnWidth = (maxWidth - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
|
||||
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
|
||||
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
|
||||
{
|
||||
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
|
||||
{
|
||||
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
|
||||
if (columnWidth > columnWidths[columnIndex])
|
||||
{
|
||||
columnWidths[columnIndex] = columnWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We now know the max width of each column, so let's recalculate the width.
|
||||
return columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Segment> Render(Encoding encoding, int width)
|
||||
{
|
||||
// Calculate the max width for each column.
|
||||
var maxColumnWidth = (width - (2 + (_columns.Count * 2) + (_columns.Count - 1))) / _columns.Count;
|
||||
var columnWidths = _columns.Select(c => c.Measure(encoding, maxColumnWidth)).ToArray();
|
||||
for (var rowIndex = 0; rowIndex < _rows.Count; rowIndex++)
|
||||
{
|
||||
for (var columnIndex = 0; columnIndex < _rows[rowIndex].Count; columnIndex++)
|
||||
{
|
||||
var columnWidth = _rows[rowIndex][columnIndex].Measure(encoding, maxColumnWidth);
|
||||
if (columnWidth > columnWidths[columnIndex])
|
||||
{
|
||||
columnWidths[columnIndex] = columnWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We now know the max width of each column, so let's recalculate the width.
|
||||
width = columnWidths.Sum() + 2 + (_columns.Count * 2) + (_columns.Count - 1);
|
||||
|
||||
// Create the rows.
|
||||
var rows = new List<List<Text>>();
|
||||
rows.Add(new List<Text>(_columns));
|
||||
rows.AddRange(_rows);
|
||||
|
||||
// Iterate all rows.
|
||||
var result = new List<Segment>();
|
||||
foreach (var (index, firstRow, lastRow, row) in rows.Enumerate())
|
||||
{
|
||||
var cellHeight = 1;
|
||||
|
||||
// Get the list of cells for the row.
|
||||
var cells = new List<List<SegmentLine>>();
|
||||
|
||||
foreach (var (rowWidth, cell) in columnWidths.Zip(row, (f, s) => (f, s)))
|
||||
{
|
||||
var lines = Segment.SplitLines(cell.Render(encoding, rowWidth));
|
||||
cellHeight = Math.Max(cellHeight, lines.Count);
|
||||
cells.Add(lines);
|
||||
}
|
||||
|
||||
if (firstRow)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopLeft)));
|
||||
foreach (var (columnIndex, _, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Left padding
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop, columnWidth)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTop))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderTopRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
|
||||
// Iterate through each cell row
|
||||
foreach (var cellRowIndex in Enumerable.Range(0, cellHeight))
|
||||
{
|
||||
// Make cells the same shape
|
||||
MakeSameHeight(cellHeight, cells);
|
||||
|
||||
var w00t = cells.Enumerate().ToArray();
|
||||
foreach (var (cellIndex, firstCell, lastCell, cell) in w00t)
|
||||
{
|
||||
if (firstCell)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.CellLeft)));
|
||||
}
|
||||
|
||||
result.Add(new Segment(" "));
|
||||
result.AddRange(cell[cellRowIndex]);
|
||||
|
||||
// Pad cell right
|
||||
var length = cell[cellRowIndex].Sum(segment => segment.CellLength(encoding));
|
||||
if (length < columnWidths[cellIndex])
|
||||
{
|
||||
result.Add(new Segment(new string(' ', columnWidths[cellIndex] - length)));
|
||||
}
|
||||
|
||||
result.Add(new Segment(" "));
|
||||
|
||||
if (lastCell)
|
||||
{
|
||||
// Separator
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.ColumnRight)));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Separator
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.CellSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
|
||||
if (firstRow)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomLeft)));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Left padding
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom, columnWidth)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottom))); // Right padding
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.HeaderBottomRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
|
||||
if (lastRow)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomLeft)));
|
||||
foreach (var (columnIndex, first, lastColumn, columnWidth) in columnWidths.Enumerate())
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom, columnWidth)));
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottom)));
|
||||
|
||||
if (!lastColumn)
|
||||
{
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomSeparator)));
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new Segment(_border.GetPart(BorderPart.FooterBottomRight)));
|
||||
result.Add(Segment.LineBreak());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void MakeSameHeight(int cellHeight, List<List<SegmentLine>> cells)
|
||||
{
|
||||
foreach (var cell in cells)
|
||||
{
|
||||
if (cell.Count < cellHeight)
|
||||
{
|
||||
while (cell.Count != cellHeight)
|
||||
{
|
||||
cell.Add(new SegmentLine());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -12,6 +13,7 @@ namespace Spectre.Console
|
||||
/// Represents text with color and decorations.
|
||||
/// </summary>
|
||||
[SuppressMessage("Naming", "CA1724:Type names should not match namespaces")]
|
||||
[DebuggerDisplay("{_text,nq}")]
|
||||
public sealed class Text : IRenderable
|
||||
{
|
||||
private readonly List<Span> _spans;
|
||||
@ -98,15 +100,24 @@ namespace Spectre.Console
|
||||
/// <inheritdoc/>
|
||||
public int Measure(Encoding encoding, int maxWidth)
|
||||
{
|
||||
var lines = _text.SplitLines();
|
||||
return lines.Max(x => x.CellLength(encoding));
|
||||
var lines = Segment.SplitLines(Render(encoding, maxWidth));
|
||||
if (lines.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return lines.Max(x => x.Length);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<Segment> Render(Encoding encoding, int width)
|
||||
{
|
||||
var result = new List<Segment>();
|
||||
if (string.IsNullOrWhiteSpace(_text))
|
||||
{
|
||||
return Array.Empty<Segment>();
|
||||
}
|
||||
|
||||
var result = new List<Segment>();
|
||||
var segments = SplitLineBreaks(CreateSegments());
|
||||
|
||||
foreach (var (_, _, last, line) in Segment.SplitLines(segments, width).Enumerate())
|
||||
|
Reference in New Issue
Block a user