diff --git a/ChangeLogs/2.2.0-ChangeLog.md b/ChangeLogs/2.2.0-ChangeLog.md new file mode 100644 index 0000000..432797b --- /dev/null +++ b/ChangeLogs/2.2.0-ChangeLog.md @@ -0,0 +1,57 @@ +# V2.2.0 + +## Multi-line support +Cell content can contain newlines and the table will adjust row height accordingly, in headers, rows and footers. + +## Bug fix +- Edge case drawing issues of cell borders miss alligned when having empty rows or empty cell content fixed +- Performance improvements + + +```csharp +using ConsoleTable.Text; + + // Setup the table +var table = new Table +{ + RowTextAlignmentRight = false, + HeaderTextAlignmentRight = false, + FooterTextAlignmentRight = false, + Padding = 2, + Headers = new string[] { "Name", $"Age{Environment.NewLine}&{Environment.NewLine}Birthyear", "City" }, + Rows = new List + { + new string[] { "Alice Cooper", $"30{Environment.NewLine}1995", "New York" }, + new string[] { "Bob", $"25{Environment.NewLine}2000", "Los Angeles" }, + new string[] { "Charlie Brown", $"67{Environment.NewLine}1958", "Chicago" }, + new string[] { "Gloria", $"40{Environment.NewLine}1985", $"Chicago{Environment.NewLine}Originally from Bogota, Colombia" } + }, + Footers = new string[] { $"Total: 4{Environment.NewLine}3 Male - 1 Female", "Total Age: 162" } +}; + +// Display the table +Console.WriteLine(table.ToTable()); +``` + +Output: +``` +┌─────────────────────┬──────────────────┬────────────────────────────────────┐ +│ Name │ Age │ City │ +│ │ & │ │ +│ │ Birthyear │ │ +├═════════════════════┼══════════════════┼════════════════════════════════════┤ +│ Alice Cooper │ 30 │ New York │ +│ │ 1995 │ │ +├─────────────────────┼──────────────────┼────────────────────────────────────┤ +│ Bob │ 25 │ Los Angeles │ +│ │ 2000 │ │ +├─────────────────────┼──────────────────┼────────────────────────────────────┤ +│ Charlie Brown │ 67 │ Chicago │ +│ │ 1958 │ │ +├─────────────────────┼──────────────────┼────────────────────────────────────┤ +│ Gloria │ 40 │ Chicago │ +│ │ 1985 │ Originally from Bogota, Colombia │ +└─────────────────────┴──────────────────┴────────────────────────────────────┘ + Total: 4 Total Age: 162 + 3 Male - 1 Female +``` diff --git a/ConsoleTable.Text.Examples/Program.cs b/ConsoleTable.Text.Examples/Program.cs index 80c0249..8217953 100644 --- a/ConsoleTable.Text.Examples/Program.cs +++ b/ConsoleTable.Text.Examples/Program.cs @@ -38,6 +38,10 @@ static void Main(string[] args) WriteTableFluent(); + WriteMultiLineTable(false); + + WriteMultiLineTable(true); + WriteTableWithoutBorders(); //WriteBigTable(); @@ -74,6 +78,35 @@ private static void WriteDefaultTable() Console.WriteLine(); } + private static void WriteMultiLineTable(bool textAlignRight) + { + Console.WriteLine(); + Console.WriteLine("Multi line table:"); + Console.WriteLine("Text align " + (textAlignRight ? "right" : "left")); + + // Setup the table + var table = new Table + { + RowTextAlignmentRight = textAlignRight, + HeaderTextAlignmentRight = textAlignRight, + FooterTextAlignmentRight = textAlignRight, + Padding = 2, + Headers = new string[] { "Name", $"Age{Environment.NewLine}&{Environment.NewLine}Birthyear", "City" }, + Rows = new List + { + new string[] { "Alice Cooper", $"30{Environment.NewLine}1995", "New York" }, + new string[] { "Bob", $"25{Environment.NewLine}2000", "Los Angeles" }, + new string[] { "Charlie Brown", $"67{Environment.NewLine}1958", "Chicago" }, + new string[] { "Gloria", $"40{Environment.NewLine}1985", $"Chicago{Environment.NewLine}Originally from Bogota, Colombia" } + }, + Footers = new string[] { $"Total: 4{Environment.NewLine}3 Male - 1 Female", "Total Age: 162" } + }; + + // Display the table + Console.WriteLine(table.ToTable()); + Console.WriteLine(); + } + private static void WriteDefaultTableWithProperties() { Console.WriteLine(); @@ -187,12 +220,12 @@ private static void WriteTableEachRowRandom() table.AddRow("Bob", "25", "Antwerp", "Belgium"); table.AddRow("Charlie", "47", "Chicago"); table.AddRow("Karina", "33", "Lima", "Peru", "South-America"); - table.AddRow("Jenny", "43"); + table.AddRow("Jenny", $"43{Environment.NewLine}1982"); table.AddRow("John"); table.AddRow("Johny"); table.AddRow(); table.AddRow(null!); - table.AddRow("Thomas", "33", "Brussels", "Belgium", "Europe", "Earth", "Solar System"); + table.AddRow("Thomas", "33", "Brussels", $"Belgium{Environment.NewLine}BE", "Europe", "Earth", "Solar System"); table.AddRow("Nathalie", "29", "Paris", "France", "Europe", "Earth", "Solar System"); table.AddRow("Mathias", "37", "Oslo", "Norway", "Europe", "Earth", "Solar System"); table.AddRow("Kenny", "55", "Tokyo"); @@ -303,7 +336,10 @@ private static void WriteBigTable() var headers = new List(); for (var columnPos = 1; columnPos <= columnCount; columnPos++) { - headers.Add($"Header {columnPos}"); + if (columnPos % 2 == 0) + headers.Add($"Header {columnPos}"); + else + headers.Add($"MultiLine{Environment.NewLine}Header {columnPos}"); } table.Headers = headers.ToArray(); @@ -313,7 +349,10 @@ private static void WriteBigTable() var row = new string[columnCount]; for (var columnPos = 1; columnPos <= columnCount; columnPos++) { - row[columnPos - 1] = $"Row {rowPos} -> Column {columnPos}"; + if (columnPos % 2 == 0) + row[columnPos - 1] = $"Row {rowPos} -> Column {columnPos}"; + else + row[columnPos - 1] = $"Row {rowPos}{Environment.NewLine}Column {columnPos}"; } rows.Add(row); } @@ -322,7 +361,10 @@ private static void WriteBigTable() var footers = new List(); for (var columnPos = 1; columnPos <= columnCount; columnPos++) { - footers.Add($"Footer {columnPos}"); + if (columnPos % 2 == 0) + footers.Add($"Footer {columnPos}"); + else + footers.Add($"Footer{Environment.NewLine}{columnPos}"); } table.Footers = footers.ToArray(); diff --git a/ConsoleTable.Text/ConsoleTable.Text.csproj b/ConsoleTable.Text/ConsoleTable.Text.csproj index e2c001c..f2ebfaa 100644 --- a/ConsoleTable.Text/ConsoleTable.Text.csproj +++ b/ConsoleTable.Text/ConsoleTable.Text.csproj @@ -5,7 +5,7 @@ ConsoleTable.Text - 2.1.0 + 2.2.0 Bruno Van Thournout A library for creating a formatted string table with customizable headers, footers, rows and easy to use styling options. https://github.com/BrunoVT1992/ConsoleTable diff --git a/ConsoleTable.Text/Table.cs b/ConsoleTable.Text/Table.cs index 6e26810..3127180 100644 --- a/ConsoleTable.Text/Table.cs +++ b/ConsoleTable.Text/Table.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -245,35 +245,37 @@ public string ToTable() int[] maximumCellWidths = GetMaxCellWidths(); var topLineCreated = false; - var previousRow = Array.Empty(); - var nextRow = Array.Empty(); + var previousRowColumnCount = 0; + var nextRowColumnCount = 0; if (Headers?.Any() == true) { + var headerColumnCount = GetColumnCount(Headers); + if (ShowBorders) { - formattedTable = CreateTopLine(maximumCellWidths, Headers.Count(), formattedTable); + formattedTable = CreateTopLine(maximumCellWidths, headerColumnCount, formattedTable); topLineCreated = true; } formattedTable = CreateValueLine(maximumCellWidths, Headers, HeaderTextAlignmentRight, ShowBorders ? TableDrawing.VerticalLine : TableDrawing.EmptySpace, formattedTable); - previousRow = Headers; + previousRowColumnCount = headerColumnCount; - //When there are no rows immediatly draw the bottom line after the header + //When there are no rows immediately draw the bottom line after the header if (Rows?.Any() == true) { - nextRow = Rows.First(); + nextRowColumnCount = GetColumnCount(Rows.First()); if (ShowBorders) { - formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), TableDrawing.HorizontalHeaderLine, formattedTable); + formattedTable = CreateSeperatorLine(maximumCellWidths, previousRowColumnCount, nextRowColumnCount, TableDrawing.HorizontalHeaderLine, formattedTable); } } else { if (ShowBorders) { - formattedTable = CreateBottomLine(maximumCellWidths, Headers.Count(), TableDrawing.HorizontalHeaderLine, formattedTable); + formattedTable = CreateBottomLine(maximumCellWidths, headerColumnCount, TableDrawing.HorizontalHeaderLine, formattedTable); } } } @@ -282,7 +284,7 @@ public string ToTable() { if (!topLineCreated && ShowBorders) { - formattedTable = CreateTopLine(maximumCellWidths, Rows.First().Count(), formattedTable); + formattedTable = CreateTopLine(maximumCellWidths, GetColumnCount(Rows.First()), formattedTable); topLineCreated = true; } @@ -291,19 +293,19 @@ public string ToTable() for (int i = 0; i < Rows.Count; i++) { - var row = CleanupRow(Rows[i]); + var row = Rows[i]; formattedTable = CreateValueLine(maximumCellWidths, row, RowTextAlignmentRight, ShowBorders ? TableDrawing.VerticalLine : TableDrawing.EmptySpace, formattedTable); - previousRow = row; + previousRowColumnCount = GetColumnCount(row); if (rowIndex != lastRowIndex) { - nextRow = CleanupRow(Rows[rowIndex + 1]); + nextRowColumnCount = GetColumnCount(Rows[rowIndex + 1]); if (ShowBorders) { - formattedTable = CreateSeperatorLine(maximumCellWidths, previousRow.Count(), nextRow.Count(), TableDrawing.HorizontalLine, formattedTable); + formattedTable = CreateSeperatorLine(maximumCellWidths, previousRowColumnCount, nextRowColumnCount, TableDrawing.HorizontalLine, formattedTable); } } @@ -312,7 +314,7 @@ public string ToTable() if (ShowBorders) { - formattedTable = CreateBottomLine(maximumCellWidths, previousRow.Count(), TableDrawing.HorizontalLine, formattedTable); + formattedTable = CreateBottomLine(maximumCellWidths, previousRowColumnCount, TableDrawing.HorizontalLine, formattedTable); } } @@ -334,13 +336,20 @@ public override string ToString() return ToTable(); } - private static string[] CleanupRow(string[] row) + private static int GetColumnCount(string[] row) { - //Weird behaviour with empty rows. So we create 1 column with a space inside. if (row == null || row.Length <= 0) - return new string[] { " " }; + return 1; + + return row.Length; + } - return row; + private static string[] GetCellLines(string cellValue) + { + if (string.IsNullOrEmpty(cellValue)) + return new string[] { string.Empty }; + + return cellValue.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); } private int[] GetMaxCellWidths() @@ -365,8 +374,9 @@ private int[] GetMaxCellWidths() var maximumColumns = 0; foreach (var row in table) { - if (row != null && row.Length > maximumColumns) - maximumColumns = row.Length; + var colCount = GetColumnCount(row); + if (colCount > maximumColumns) + maximumColumns = colCount; } var maximumCellWidths = new int[maximumColumns]; @@ -385,13 +395,28 @@ private int[] GetMaxCellWidths() for (int i = 0; i < row.Length; i++) { - var maxWidth = row[i].Length + paddingCount; + var lines = GetCellLines(row[i]); + var maxLineWidth = 0; + for (int l = 0; l < lines.Length; l++) + { + if (lines[l].Length > maxLineWidth) + maxLineWidth = lines[l].Length; + } + + var maxWidth = maxLineWidth + paddingCount; if (maxWidth > maximumCellWidths[i]) maximumCellWidths[i] = maxWidth; } } + //Ensure every column has at least the padding width + for (int i = 0; i < maximumCellWidths.Length; i++) + { + if (maximumCellWidths[i] < paddingCount) + maximumCellWidths[i] = paddingCount; + } + return maximumCellWidths; } @@ -431,39 +456,51 @@ private StringBuilder CreateBottomLine(int[] maximumCellWidths, int rowColumnCou private StringBuilder CreateValueLine(int[] maximumCellWidths, string[] row, bool alignRight, string verticalLine, StringBuilder formattedTable) { - int cellIndex = 0; - int lastCellIndex = row.Length - 1; + int columnCount = GetColumnCount(row); + + var cellLines = new string[columnCount][]; + int maxLineCount = 1; + for (int i = 0; i < columnCount; i++) + { + var cellValue = (row != null && i < row.Length) ? row[i] : null; + cellLines[i] = GetCellLines(cellValue); + if (cellLines[i].Length > maxLineCount) + maxLineCount = cellLines[i].Length; + } + + int lastCellIndex = columnCount - 1; var paddingString = string.Empty; if (Padding > 0) paddingString = string.Concat(Enumerable.Repeat(' ', Padding)); - for (int i = 0; i < row.Length; i++) + for (int lineIndex = 0; lineIndex < maxLineCount; lineIndex++) { - var column = row[i]; - - var leftVerticalLine = verticalLine; - if (i == 0 && !ShowBorders) + for (int i = 0; i < columnCount; i++) { - leftVerticalLine = TableDrawing.Empty; - } + var column = lineIndex < cellLines[i].Length ? cellLines[i][lineIndex] : string.Empty; - var restWidth = maximumCellWidths[cellIndex]; - if (Padding > 0) - restWidth -= Padding * 2; + var leftVerticalLine = verticalLine; + if (i == 0 && !ShowBorders) + { + leftVerticalLine = TableDrawing.Empty; + } - var cellValue = alignRight ? column.PadLeft(restWidth, ' ') : column.PadRight(restWidth, ' '); + var restWidth = maximumCellWidths[i]; + if (Padding > 0) + restWidth -= Padding * 2; - if (cellIndex == 0 && cellIndex == lastCellIndex) - formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine)); - else if (cellIndex == 0) - formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString)); - else if (cellIndex == lastCellIndex) - formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine)); - else - formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString)); + var cellValue = alignRight ? column.PadLeft(restWidth, ' ') : column.PadRight(restWidth, ' '); - cellIndex++; + if (i == 0 && i == lastCellIndex) + formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine)); + else if (i == 0) + formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString)); + else if (i == lastCellIndex) + formattedTable.AppendLine(string.Format("{0}{1}{2}{3}{4}", leftVerticalLine, paddingString, cellValue, paddingString, verticalLine)); + else + formattedTable.Append(string.Format("{0}{1}{2}{3}", leftVerticalLine, paddingString, cellValue, paddingString)); + } } return formattedTable; diff --git a/ConsoleTable.slnx b/ConsoleTable.slnx index 7b05515..141fbd9 100644 --- a/ConsoleTable.slnx +++ b/ConsoleTable.slnx @@ -10,6 +10,7 @@ + diff --git a/README.md b/README.md index 71bbbaf..83d1011 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A lightweight .NET library for creating beautifully formatted console tables wit - Simple and intuitive API - Optimized for performance - Support for varying column counts across rows (each row can have its own number of cells) +- Multi line support (cell content can contain newlines and the table will adjust row height accordingly) in headers, rows and footers ## Releases Check releases for the changelog here [https://github.com/BrunoVT1992/ConsoleTable/releases/](https://github.com/BrunoVT1992/ConsoleTable/releases/) @@ -43,22 +44,22 @@ Download this nuget package from [https://www.nuget.org/packages/ConsoleTable.Te using ConsoleTable.Text; // Setup the table -var table = new Table(); - -// Set headers -table.SetHeaders("Name", "Age", "City"); - -// Add rows -table.AddRow("Alice Cooper", "30", "New York"); - -table.AddRows(new string[][] +var table = new Table { - new string[] { "Bob", "25", "Los Angeles" }, - new string[] { "Charlie Brown", "47", "Chicago" } -}); - -//Set footers -table.SetFooters("Total: 3", "Total Age: 102"); + RowTextAlignmentRight = false, + HeaderTextAlignmentRight = false, + FooterTextAlignmentRight = false, + Padding = 2, + Headers = new string[] { "Name", $"Age{Environment.NewLine}&{Environment.NewLine}Birthyear", "City" }, + Rows = new List + { + new string[] { "Alice Cooper", $"30{Environment.NewLine}1995", "New York" }, + new string[] { "Bob", $"25{Environment.NewLine}2000", "Los Angeles" }, + new string[] { "Charlie Brown", $"67{Environment.NewLine}1958", "Chicago" }, + new string[] { "Gloria", $"40{Environment.NewLine}1985", $"Chicago{Environment.NewLine}Originally from Bogota, Colombia" } + }, + Footers = new string[] { $"Total: 4{Environment.NewLine}3 Male - 1 Female", "Total Age: 162" } +}; // Display the table Console.WriteLine(table.ToTable()); @@ -66,16 +67,25 @@ Console.WriteLine(table.ToTable()); Output: ``` -┌───────────────┬────────────────┬─────────────┐ -│ Name │ Age │ City │ -├═══════════════┼════════════════┼═════════════┤ -│ Alice Cooper │ 30 │ New York │ -├───────────────┼────────────────┼─────────────┤ -│ Bob │ 25 │ Los Angeles │ -├───────────────┼────────────────┼─────────────┤ -│ Charlie Brown │ 47 │ Chicago │ -└───────────────┴────────────────┴─────────────┘ - Total: 3 Total Age: 102 +┌─────────────────────┬──────────────────┬────────────────────────────────────┐ +│ Name │ Age │ City │ +│ │ & │ │ +│ │ Birthyear │ │ +├═════════════════════┼══════════════════┼════════════════════════════════════┤ +│ Alice Cooper │ 30 │ New York │ +│ │ 1995 │ │ +├─────────────────────┼──────────────────┼────────────────────────────────────┤ +│ Bob │ 25 │ Los Angeles │ +│ │ 2000 │ │ +├─────────────────────┼──────────────────┼────────────────────────────────────┤ +│ Charlie Brown │ 67 │ Chicago │ +│ │ 1958 │ │ +├─────────────────────┼──────────────────┼────────────────────────────────────┤ +│ Gloria │ 40 │ Chicago │ +│ │ 1985 │ Originally from Bogota, Colombia │ +└─────────────────────┴──────────────────┴────────────────────────────────────┘ + Total: 4 Total Age: 162 + 3 Male - 1 Female ``` ## API Reference diff --git a/Tests/ConsoleTable.Text.Tests/TableTests.cs b/Tests/ConsoleTable.Text.Tests/TableTests.cs index 2462fb6..5350b68 100644 --- a/Tests/ConsoleTable.Text.Tests/TableTests.cs +++ b/Tests/ConsoleTable.Text.Tests/TableTests.cs @@ -17,6 +17,59 @@ public void SetHeaders_Success() Assert.Contains("Age", result); } + [Fact] + public void Empty() + { + var table = new Table(); + + var result = table.ToTable(); + + Assert.Equal(string.Empty, result); + } + + [Fact] + public void OnlyEmptyRows() + { + var table = new Table + { + Rows = new List + { + new[] { "" }, + Array.Empty() + } + }; + + var result = table.ToTable(); + + Assert.NotEqual(string.Empty, result); + } + + [Fact] + public void OnlyEmptyFooters() + { + var table = new Table + { + Footers = new string[] { "", "" } + }; + + var result = table.ToTable(); + + Assert.NotEqual(string.Empty, result); + } + + [Fact] + public void OnlyEmptyHeaders() + { + var table = new Table + { + Headers = new string[] { "", "" } + }; + + var result = table.ToTable(); + + Assert.NotEqual(string.Empty, result); + } + [Fact] public void SetHeaders_OverwritesPreviousHeaders() { @@ -845,4 +898,320 @@ public void Clear_IsEmpty() Assert.Empty(result); } #endregion + + #region MultiLine + [Fact] + public void MultiLineRow_RendersAllLines() + { + var table = new Table(); + table.SetHeaders("Name", "City"); + table.AddRow("Alice", "Chicago\nUSA"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // Both physical lines of the multi-line cell should appear between separators + Assert.Contains(outputLines, l => l.Contains("Chicago") && l.Contains("│")); + Assert.Contains(outputLines, l => l.Contains("USA") && l.Contains("│")); + } + + [Fact] + public void MultiLineHeader_RendersAllLines() + { + var table = new Table(); + table.SetHeaders("Name", "Age\n(years)"); + table.AddRow("Alice", "30"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + Assert.Contains(outputLines, l => l.Contains("Age") && l.Contains("│")); + Assert.Contains(outputLines, l => l.Contains("(years)") && l.Contains("│")); + } + + [Fact] + public void MultiLineFooter_RendersAllLines() + { + var table = new Table(); + table.AddRow("Alice", "30"); + table.SetFooters("Total: 1\nAll Female", "Sum: 30"); + + var result = table.ToTable(); + + Assert.Contains("Total: 1", result); + Assert.Contains("All Female", result); + Assert.Contains("Sum: 30", result); + } + + [Fact] + public void MultiLineRow_ColumnWidthBasedOnWidestLine() + { + var table = new Table { Padding = 0 }; + // "Short" is 5 chars, "VeryLongSecondLine" is 18 chars + table.AddRow("Short\nVeryLongSecondLine"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // The top border should be sized for the longest line, not the short one + var topLine = outputLines.First(l => l.Contains("┌")); + // Width = 18 chars of content (no padding) + 2 border chars + Assert.Equal(20, topLine.Length); + } + + [Fact] + public void MultiLineRow_ShorterCellsPaddedWithBlanks_LeftAligned() + { + var table = new Table { Padding = 0 }; + table.AddRow("Single", "Line1\nLine2"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // The second physical line should have an empty first column + var secondValueLine = outputLines.First(l => l.Contains("Line2")); + Assert.Contains("│", secondValueLine); + Assert.DoesNotContain("Single", secondValueLine); + } + + [Fact] + public void MultiLineRow_LeftAligned_PadsCorrectly() + { + var table = new Table(); + table.AddRow("A\nLonger"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // "A" line should be left-aligned and padded to the width of "Longer" + var lineWithA = outputLines.First(l => l.Contains("A") && !l.Contains("Longer") && !l.Contains("─")); + var lineWithLonger = outputLines.First(l => l.Contains("Longer")); + + // Both value lines must be the same total length (borders + padding + content) + Assert.Equal(lineWithA.Length, lineWithLonger.Length); + } + + [Fact] + public void MultiLineRow_RightAligned_PadsCorrectly() + { + var table = new Table { RowTextAlignmentRight = true }; + table.AddRow("A\nLonger"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + var lineWithA = outputLines.First(l => l.Contains("A") && !l.Contains("Longer") && !l.Contains("─")); + var lineWithLonger = outputLines.First(l => l.Contains("Longer")); + + // Both value lines must be the same total length + Assert.Equal(lineWithA.Length, lineWithLonger.Length); + + // "A" should be right-aligned: preceded by spaces + var contentA = lineWithA.Split('│')[1]; + Assert.True(contentA.TrimEnd().Length > contentA.TrimStart().Length, + "Right-aligned 'A' should have leading spaces"); + } + + [Fact] + public void MultiLineHeader_RightAligned() + { + var table = new Table { HeaderTextAlignmentRight = true }; + table.SetHeaders("H\nLongHeader"); + table.AddRow("Value"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + var lineWithH = outputLines.First(l => l.Contains("H") && !l.Contains("Long") && !l.Contains("─")); + var contentH = lineWithH.Split('│')[1]; + + // "H" should be right-aligned: more leading spaces than trailing + Assert.True(contentH.TrimEnd().Length > contentH.TrimStart().Length, + "Right-aligned 'H' should have leading spaces"); + } + + [Fact] + public void MultiLineFooter_RightAligned() + { + var table = new Table { FooterTextAlignmentRight = true }; + table.AddRow("VeryLongValue"); + table.SetFooters("F\nLong"); + + var result = table.ToTable(); + + // Verify both footer lines are present in the rendered output. + Assert.Contains("F", result); + Assert.Contains("Long", result); + + var outputLines = GetOutputLines(result); + // Find the footer line with just "F" (not "Long") + var lineWithF = outputLines.First(l => + !l.Contains("│") && !l.Contains("─") && + l.Contains("F") && !l.Contains("Long") && !l.Contains("VeryLongValue")); + // Right-aligned means leading spaces + Assert.True(lineWithF.TrimEnd().Length > lineWithF.TrimStart().Length, + "Right-aligned 'F' should have leading spaces"); + } + + [Fact] + public void MultiLineRow_WithCustomPadding() + { + var table = new Table { Padding = 3 }; + table.AddRow("A\nB"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + var lineWithA = outputLines.First(l => l.Contains("A") && !l.Contains("─")); + // With padding 3, there should be 3 spaces between the border and the content on each side + Assert.Contains("│ A", lineWithA); + } + + [Fact] + public void MultiLineRow_MixedSingleAndMultiLine() + { + var table = new Table(); + table.SetHeaders("Name", "City"); + table.AddRow("Alice", "New York"); + table.AddRow("Bob", "Chicago\nUSA"); + table.AddRow("Charlie", "London"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // All content should be present + Assert.Contains("Alice", result); + Assert.Contains("New York", result); + Assert.Contains("Bob", result); + Assert.Contains("Chicago", result); + Assert.Contains("USA", result); + Assert.Contains("Charlie", result); + Assert.Contains("London", result); + + // The "USA" line should have an empty name column + var usaLine = outputLines.First(l => l.Contains("USA")); + Assert.DoesNotContain("Bob", usaLine); + } + + [Fact] + public void MultiLineRow_WithoutBorders() + { + var table = new Table { ShowBorders = false }; + table.AddRow("A\nB", "C"); + + var result = table.ToTable(); + + Assert.Contains("A", result); + Assert.Contains("B", result); + Assert.Contains("C", result); + Assert.DoesNotContain("│", result); + Assert.DoesNotContain("─", result); + } + + [Fact] + public void MultiLineRow_CrLf_HandledCorrectly() + { + var table = new Table(); + table.AddRow("Line1\r\nLine2"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + Assert.Contains(outputLines, l => l.Contains("Line1")); + Assert.Contains(outputLines, l => l.Contains("Line2")); + } + + [Fact] + public void MultiLineRow_Lf_HandledCorrectly() + { + var table = new Table(); + table.AddRow("Line1\nLine2"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + Assert.Contains(outputLines, l => l.Contains("Line1")); + Assert.Contains(outputLines, l => l.Contains("Line2")); + } + + [Fact] + public void MultiLineRow_EmptyLinesPreserved() + { + var table = new Table { Padding = 0 }; + table.AddRow("A\n\nB"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // Should produce 3 value lines between borders + var valueLines = outputLines.Where(l => l.StartsWith("│") && !l.Contains("─")).ToList(); + Assert.Equal(3, valueLines.Count); + Assert.Contains(valueLines, l => l.Contains("A")); + Assert.Contains(valueLines, l => l.Contains("B")); + } + + [Fact] + public void MultiLineRow_AllLinesSameWidth() + { + var table = new Table(); + table.SetHeaders("Name", "Info"); + table.AddRow("Alice", "Line1\nLongerLine2\nL3"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // All value lines for the multi-line row should have the same total width + var rowValueLines = outputLines + .Where(l => l.Contains("│") && (l.Contains("Line1") || l.Contains("LongerLine2") || l.Contains("L3"))) + .ToList(); + + Assert.True(rowValueLines.Count >= 3); + var expectedWidth = rowValueLines[0].Length; + Assert.All(rowValueLines, l => Assert.Equal(expectedWidth, l.Length)); + } + + [Fact] + public void MultiLineRow_HeadersRowsAndFooters_AllMultiLine() + { + var table = new Table(); + table.SetHeaders("Name", "Age\n(years)"); + table.AddRow("Alice", "30"); + table.AddRow("Bob", "25\nyears old"); + table.SetFooters("Total\n2 people", "Sum\n55"); + + var result = table.ToTable(); + + Assert.Contains("Age", result); + Assert.Contains("(years)", result); + Assert.Contains("25", result); + Assert.Contains("years old", result); + Assert.Contains("Total", result); + Assert.Contains("2 people", result); + Assert.Contains("Sum", result); + Assert.Contains("55", result); + } + + [Fact] + public void MultiLineRow_SingleLineContent_UnchangedBehavior() + { + // Ensure single-line content still works exactly as before + var table = new Table(); + table.SetHeaders("Name", "Age"); + table.AddRow("Alice", "30"); + + var result = table.ToTable(); + var outputLines = GetOutputLines(result); + + // Should have exactly: top border, header, separator, row, bottom border = 5 lines + Assert.Equal(5, outputLines.Length); + } + + private static string[] GetOutputLines(string tableOutput) + { + return tableOutput + .Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None) + .Where(l => !string.IsNullOrWhiteSpace(l)) + .ToArray(); + } + #endregion }