├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── Tababular.Tests ├── Bugs │ └── TestRecordsAndStuff.cs ├── Ex │ └── StringExtensions.cs ├── Examples │ └── ReadmeCode.cs ├── Extractors │ ├── TestDictionaryTableExtractor.cs │ ├── TestJsonTableExtractor.cs │ └── TestObjectTableExtractor.cs ├── Formatter │ ├── TestBreaker.cs │ ├── TestCustomNewline.cs │ └── TestTableFormatter.cs ├── Integration │ ├── CollapseTest.cs │ ├── JsonTest.cs │ └── SimpleTest.cs └── Tababular.Tests.csproj ├── Tababular.sln ├── Tababular ├── Hints.cs ├── Internals │ ├── Breaker.cs │ ├── Extensions │ │ ├── DictionaryExtensions.cs │ │ └── EnumerableExtensions.cs │ ├── Extractors │ │ ├── DictionaryTableExtractor.cs │ │ ├── JsonTableExtractor.cs │ │ └── ObjectTableExtractor.cs │ ├── ITableExtractor.cs │ ├── InternalsVisibleTo.cs │ └── TableModel │ │ ├── Cell.cs │ │ ├── Column.cs │ │ ├── Row.cs │ │ └── Table.cs ├── Tababular.csproj └── TableFormatter.cs ├── appveyor.yml ├── artwork └── tababular_logo.png ├── packages ├── NUnit.2.6.4 │ ├── NUnit.2.6.4.nupkg │ ├── lib │ │ ├── nunit.framework.dll │ │ └── nunit.framework.xml │ └── license.txt └── Newtonsoft.Json.8.0.3 │ ├── Newtonsoft.Json.8.0.3.nupkg │ ├── lib │ ├── net20 │ │ ├── Newtonsoft.Json.dll │ │ └── Newtonsoft.Json.xml │ ├── net35 │ │ ├── Newtonsoft.Json.dll │ │ └── Newtonsoft.Json.xml │ ├── net40 │ │ ├── Newtonsoft.Json.dll │ │ └── Newtonsoft.Json.xml │ ├── net45 │ │ ├── Newtonsoft.Json.dll │ │ └── Newtonsoft.Json.xml │ ├── portable-net40+sl5+wp80+win8+wpa81 │ │ ├── Newtonsoft.Json.dll │ │ └── Newtonsoft.Json.xml │ └── portable-net45+wp80+win8+wpa81+dnxcore50 │ │ ├── Newtonsoft.Json.dll │ │ └── Newtonsoft.Json.xml │ └── tools │ └── install.ps1 ├── scripts ├── build.cmd ├── push.cmd └── release.cmd └── tools └── nuget └── nuget.exe /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=crlf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj 2 | bin 3 | deploy 4 | deploy/* 5 | _ReSharper.* 6 | *.csproj.user 7 | *.resharper.user 8 | *.ReSharper.user 9 | *.teamcity.user 10 | *.TeamCity.user 11 | *.resharper 12 | *.DotSettings.user 13 | *.dotsettings.user 14 | *.ncrunchproject 15 | *.ncrunchsolution 16 | *.suo 17 | *.cache 18 | ~$* 19 | .vs 20 | .vs/* 21 | _NCrunch_* 22 | *.user 23 | *.backup 24 | 25 | AssemblyInfo_Patch.cs -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | * Made Tababular 5 | 6 | ## 1.0.1 7 | * More robustrustness 8 | 9 | ## 1.0.2 10 | * Added ability to supply hints - at the moment this is the ability to specify a max width for the table 11 | 12 | ## 1.0.3 13 | * Changed total table width constraint algorithm to cut off one fourth of the widest column's width on each iteration 14 | 15 | ## 1.0.4 16 | * Added ability to parse [JSONL](http://jsonlines.org/) too 17 | 18 | ## 1.0.5 19 | * Added ability (via a hint) to specify that the formatter can collapse the table vertically if it has no cell with multiple lines 20 | 21 | ## 1.0.6 22 | * Updated JSON.NET (which gets merged) to 8.0.3 23 | 24 | ## 2.0.0 25 | * Add .NET Core support 26 | * No longer merge JSON.NET 27 | * Skip property enumeration for primitive-like types - thanks [gary-palmer] 28 | 29 | ## 3.0.0 30 | * Change style to contain more space visually 31 | * Use FastMember for ultra-fast reflection 32 | * Add .NET Standard 2.0 as a target 33 | 34 | ## 3.0.1 35 | * When formatting objects, columns are now returned in the order the corresponding properties are defined 36 | 37 | ## 4.0.0 38 | * Better exception when value extraction fails on object 39 | 40 | ## 4.1.0 41 | * Update some packages, modernize code base 42 | 43 | ## 4.2.0 44 | * Make newline character sequence configurable - thanks [antifree] 45 | 46 | ## 4.3.0 47 | * Add logo 🤩 48 | 49 | [antifree]: https://github.com/antifree 50 | [gary-palmer]: https://github.com/gary-palmer 51 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Tababular is licensed under [The MIT License (MIT)](http://opensource.org/licenses/MIT) 2 | 3 | # The MIT License (MIT) 4 | 5 | Copyright (c) 2012-2016 Mogens Heller Grabe 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tababular 2 | 3 | A simple .NET monospace text table formatting library for .NET 4.5 and .NET Standard 1.6. 4 | 5 | You can use it if you are standing with a bunch of objects or dictionaries in your hand, and you 6 | wish for them to become as nice as this: 7 | 8 | +-------------+--------------+-------------+ 9 | | FirstColumn | SecondColumn | ThirdColumn | 10 | +-------------+--------------+-------------+ 11 | | r1 | hej | hej igen | 12 | +-------------+--------------+-------------+ 13 | | r2 | hej | hej igen | 14 | +-------------+--------------+-------------+ 15 | 16 | This can easily be achieved by newing up the `TableFormatter` like this: 17 | 18 | var formatter = new TableFormatter(); 19 | 20 | and then you call an appropriate `Format***` method on it, e.g. `FormatObjects`: 21 | 22 | var objects = new[] 23 | { 24 | new {FirstColumn = "r1", SecondColumn = "hej", ThirdColumn = "hej igen"}, 25 | new {FirstColumn = "r2", SecondColumn = "hej", ThirdColumn = "hej igen"}, 26 | }; 27 | 28 | var text = tableFormatter.FormatObjects(objects); 29 | 30 | Console.WriteLine(text); 31 | 32 | which will result in the table shown above. 33 | 34 | For now, Tababular does not support that much stuff - but one nice thing about it is that 35 | it will properly format lines in cells, so that e.g. 36 | 37 | var objects = new[] 38 | { 39 | new { MachineName = "ctxtest01", Ip = "10.0.0.10", Ports = new[] {80, 8080, 9090}}, 40 | new { MachineName = "ctxtest02", Ip = "10.0.0.11", Ports = new[] {80, 5432}} 41 | }; 42 | 43 | var text = new TableFormatter().FormatObjects(objects); 44 | 45 | Console.WriteLine(text); 46 | 47 | becomes nice like this: 48 | 49 | +-----------+-------------+-------+ 50 | | Ip | MachineName | Ports | 51 | +-----------+-------------+-------+ 52 | | 10.0.0.10 | ctxtest01 | 80 | 53 | | | | 8080 | 54 | | | | 9090 | 55 | +-----------+-------------+-------+ 56 | | 10.0.0.11 | ctxtest02 | 80 | 57 | | | | 5432 | 58 | +-----------+-------------+-------+ 59 | 60 | which looks pretty neat if you ask me. 61 | 62 | # Formatting different things 63 | 64 | Tababular can format different things, which at the moment includes: 65 | 66 | * Objects: `formatter.FormatObjects(objects)` 67 | * Dictionaries: `formatter.FormatDictionaries(dictionaries)` 68 | * JSON: `formatter.FormatJson(jsonText)` 69 | 70 | # More niceness 71 | 72 | What about longs texts? Consider this example where the "Comments" column can be used to supply arbitrarily long texts: 73 | 74 | var objects = new[] 75 | { 76 | new {MachineName = "ctxtest01", Ip = "10.0.0.10", Ports = new[] {80, 8080, 9090}, Comments = ""}, 77 | new {MachineName = "ctxtest02", Ip = "10.0.0.11", Ports = new[] {5432}, 78 | Comments = @"This bad boy hosts our database and a couple of internal jobs."} 79 | }; 80 | 81 | var text = new TableFormatter().FormatObjects(objects); 82 | 83 | In this case, the resulting table would look like this: 84 | 85 | +----------------------------------------------------------------+-----------+-------------+-------+ 86 | | Comments | Ip | MachineName | Ports | 87 | +----------------------------------------------------------------+-----------+-------------+-------+ 88 | | | 10.0.0.10 | ctxtest01 | 80 | 89 | | | | | 8080 | 90 | | | | | 9090 | 91 | +----------------------------------------------------------------+-----------+-------------+-------+ 92 | | This bad boy hosts our database and a couple of internal jobs. | 10.0.0.11 | ctxtest02 | 5432 | 93 | +----------------------------------------------------------------+-----------+-------------+-------+ 94 | 95 | which might be fine, but since the texts can be even longer than this, it might end up being a problem. 96 | 97 | Fear not! We can supply a small hint to the table formatter like this: 98 | 99 | var hints = new Hints { MaxTableWidth = 80 }; 100 | var formatter = new TableFormatter(hints); 101 | 102 | and then when we 103 | 104 | var text = formatter.FormatObjects(objects); 105 | 106 | it will look like this: 107 | 108 | +------------------------------------------------+-----------+-------------+-------+ 109 | | Comments | Ip | MachineName | Ports | 110 | +------------------------------------------------+-----------+-------------+-------+ 111 | | | 10.0.0.10 | ctxtest01 | 80 | 112 | | | | | 8080 | 113 | | | | | 9090 | 114 | +------------------------------------------------+-----------+-------------+-------+ 115 | | This bad boy hosts our database and a couple | 10.0.0.11 | ctxtest02 | 5432 | 116 | | of internal jobs. | | | | 117 | +------------------------------------------------+-----------+-------------+-------+ 118 | 119 | and the table will never become wider than at most 80 characters. Objectively speaking, this is actually freaking awesome. 120 | 121 | # License 122 | 123 | [The MIT License (MIT)](http://opensource.org/licenses/MIT) -------------------------------------------------------------------------------- /Tababular.Tests/Bugs/TestRecordsAndStuff.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | // ReSharper disable NotAccessedPositionalProperty.Local 4 | // ReSharper disable ArgumentsStyleStringLiteral 5 | // ReSharper disable ArgumentsStyleLiteral 6 | 7 | namespace Tababular.Tests.Bugs; 8 | 9 | [TestFixture] 10 | public class TestRecordsAndStuff 11 | { 12 | [Test] 13 | public void CanDoIt() 14 | { 15 | var formatter = new TableFormatter(); 16 | 17 | var records = new[] 18 | { 19 | new SomeRecordThing(Number: 21, Text: "Big Number"), 20 | new SomeRecordThing(Number: 5, Text: "Small Number"), 21 | new SomeRecordThing(Number: 12, Text: "Medium Number") 22 | }; 23 | 24 | var text = formatter.FormatObjects(records); 25 | 26 | Console.WriteLine(text); 27 | } 28 | 29 | record SomeRecordThing(int Number, string Text); 30 | } -------------------------------------------------------------------------------- /Tababular.Tests/Ex/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Tababular.Tests.Ex; 5 | 6 | public static class StringExtensions 7 | { 8 | public static string Normalized(this string str) 9 | { 10 | var lines = str.Split(new[] {Environment.NewLine, "\r", "\n"}, StringSplitOptions.None) 11 | .SkipWhile(string.IsNullOrWhiteSpace) 12 | .Reverse() 13 | .SkipWhile(string.IsNullOrWhiteSpace) 14 | .Reverse(); 15 | 16 | return string.Join(Environment.NewLine, lines.Select(l => l.TrimEnd())); 17 | } 18 | } -------------------------------------------------------------------------------- /Tababular.Tests/Examples/ReadmeCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace Tababular.Tests.Examples; 5 | 6 | [TestFixture] 7 | public class ReadmeCode 8 | { 9 | [Test] 10 | public void Example1() 11 | { 12 | var tableFormatter = new TableFormatter(); 13 | 14 | var objects = new[] 15 | { 16 | new {FirstColumn = "r1", SecondColumn = "hej", ThirdColumn = "hej igen"}, 17 | new {FirstColumn = "r2", SecondColumn = "hej", ThirdColumn = "hej igen"}, 18 | }; 19 | 20 | var text = tableFormatter.FormatObjects(objects); 21 | 22 | Console.WriteLine(text); 23 | } 24 | 25 | [Test] 26 | public void Example2() 27 | { 28 | var objects = new[] 29 | { 30 | new {MachineName = "ctxtest01", Ip = "10.0.0.10", Ports = new[] {80, 8080, 9090}}, 31 | new {MachineName = "ctxtest02", Ip = "10.0.0.11", Ports = new[] {80, 5432}} 32 | }; 33 | 34 | var text = new TableFormatter().FormatObjects(objects); 35 | 36 | Console.WriteLine(text); 37 | } 38 | 39 | [Test] 40 | public void Example3_WithoutHint() 41 | { 42 | var objects = new[] 43 | { 44 | new {MachineName = "ctxtest01", Ip = "10.0.0.10", Ports = new[] {80, 8080, 9090}, Comments = ""}, 45 | new {MachineName = "ctxtest02", Ip = "10.0.0.11", Ports = new[] {5432}, 46 | Comments = @"This bad boy hosts our database and a couple of internal jobs."} 47 | }; 48 | 49 | var formatter = new TableFormatter(); 50 | 51 | var text = formatter.FormatObjects(objects); 52 | 53 | Console.WriteLine(text); 54 | 55 | /* 56 | 57 | +----------------------------------------------------------------+-----------+-------------+-------+ 58 | | Comments | Ip | MachineName | Ports | 59 | +----------------------------------------------------------------+-----------+-------------+-------+ 60 | | | 10.0.0.10 | ctxtest01 | 80 | 61 | | | | | 8080 | 62 | | | | | 9090 | 63 | +----------------------------------------------------------------+-----------+-------------+-------+ 64 | | This bad boy hosts our database and a couple of internal jobs. | 10.0.0.11 | ctxtest02 | 5432 | 65 | +----------------------------------------------------------------+-----------+-------------+-------+ 66 | 67 | */ 68 | } 69 | 70 | [Test] 71 | public void Example3_WithHint() 72 | { 73 | var objects = new[] 74 | { 75 | new {MachineName = "ctxtest01", Ip = "10.0.0.10", Ports = new[] {80, 8080, 9090}, Comments = ""}, 76 | new {MachineName = "ctxtest02", Ip = "10.0.0.11", Ports = new[] {5432}, 77 | Comments = @"This bad boy hosts our database and a couple of internal jobs."} 78 | }; 79 | 80 | var hints = new Hints { MaxTableWidth = 80 }; 81 | var formatter = new TableFormatter(hints); 82 | 83 | var text = formatter.FormatObjects(objects); 84 | 85 | Console.WriteLine(text); 86 | 87 | /* 88 | 89 | +------------------------------------------------+-----------+-------------+-------+ 90 | | Comments | Ip | MachineName | Ports | 91 | +------------------------------------------------+-----------+-------------+-------+ 92 | | | 10.0.0.10 | ctxtest01 | 80 | 93 | | | | | 8080 | 94 | | | | | 9090 | 95 | +------------------------------------------------+-----------+-------------+-------+ 96 | | This bad boy hosts our database and a couple | 10.0.0.11 | ctxtest02 | 5432 | 97 | | of internal jobs. | | | | 98 | +------------------------------------------------+-----------+-------------+-------+ 99 | 100 | */ 101 | } 102 | } -------------------------------------------------------------------------------- /Tababular.Tests/Extractors/TestDictionaryTableExtractor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using NUnit.Framework; 4 | using Tababular.Internals.Extractors; 5 | 6 | namespace Tababular.Tests.Extractors; 7 | 8 | [TestFixture] 9 | public class TestDictionaryTableExtractor 10 | { 11 | [Test] 12 | public void DoesNotDieOnEmptyArray() 13 | { 14 | var table = new DictionaryTableExtractor(Enumerable.Empty>()).GetTable(); 15 | 16 | Assert.That(table.Rows.Count, Is.EqualTo(0)); 17 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 18 | } 19 | 20 | [Test] 21 | public void DoesNotDieOnArrayWithEmptyObject() 22 | { 23 | var table = new DictionaryTableExtractor(new[] 24 | { 25 | new Dictionary(), 26 | new Dictionary() 27 | }).GetTable(); 28 | 29 | Assert.That(table.Rows.Count, Is.EqualTo(2)); 30 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 31 | } 32 | 33 | [Test] 34 | public void GetsColumnsAsExpected() 35 | { 36 | var dictionaries = new[] 37 | { 38 | new Dictionary 39 | { 40 | { "First column", "Value 1" }, 41 | { "Second column", "Value 1" }, 42 | }, 43 | new Dictionary 44 | { 45 | { "First column", "Value 1" }, 46 | { "Second column", "Value 1" }, 47 | } 48 | }; 49 | 50 | var table = new DictionaryTableExtractor(dictionaries).GetTable(); 51 | 52 | Assert.That(table.Columns.Select(c => c.Label), Is.EqualTo(new[] { "First column", "Second column" })); 53 | } 54 | 55 | [Test] 56 | public void CanExtractValues_Strings() 57 | { 58 | var dictionaries = new[] 59 | { 60 | new Dictionary 61 | { 62 | { "col1", "v1" }, 63 | { "col2", "v2" }, 64 | { "col3", "v3" } 65 | } 66 | }; 67 | 68 | var table = new DictionaryTableExtractor(dictionaries).GetTable(); 69 | 70 | var row = table.Rows.Single(); 71 | 72 | var cellTexts = row.GetAllCells().OrderBy(c => c.TextValue).Select(c => c.TextValue); 73 | 74 | Assert.That(cellTexts, Is.EqualTo(new[] {"v1", "v2", "v3"})); 75 | 76 | } 77 | 78 | [Test] 79 | public void CanExtractValues_Multiple() 80 | { 81 | var dictionaries = new[] 82 | { 83 | new Dictionary 84 | { 85 | { "col1", new[] {"line1", "line2", "line3"} } 86 | } 87 | }; 88 | 89 | var table = new DictionaryTableExtractor(dictionaries).GetTable(); 90 | 91 | var row = table.Rows.Single(); 92 | 93 | var cellLines = row.GetAllCells().Single().Lines; 94 | 95 | Assert.That(cellLines, Is.EqualTo(new[] {"line1", "line2", "line3"})); 96 | 97 | } 98 | } -------------------------------------------------------------------------------- /Tababular.Tests/Extractors/TestJsonTableExtractor.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using NUnit.Framework; 3 | using Tababular.Internals.Extractors; 4 | 5 | namespace Tababular.Tests.Extractors; 6 | 7 | [TestFixture] 8 | public class TestJsonTableExtractor 9 | { 10 | [Test] 11 | public void CanExtractFromJsonlToo() 12 | { 13 | var jsonObjects = @"{""First column"": ""Value 1"", ""Second column"": ""Value 1""} 14 | {""First column"": ""Value 1"", ""Second column"": ""Value 1""}"; 15 | 16 | var table = new JsonTableExtractor(jsonObjects).GetTable(); 17 | 18 | Assert.That(table.Rows.Count, Is.EqualTo(2)); 19 | Assert.That(table.Columns.Count, Is.EqualTo(2)); 20 | } 21 | 22 | [Test] 23 | public void DoesNotDieOnEmptyObject() 24 | { 25 | var table = new JsonTableExtractor("{}").GetTable(); 26 | 27 | Assert.That(table.Rows.Count, Is.EqualTo(1)); 28 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 29 | } 30 | 31 | [Test] 32 | public void DoesNotDieOnEmptyArray() 33 | { 34 | var table = new JsonTableExtractor("[]").GetTable(); 35 | 36 | Assert.That(table.Rows.Count, Is.EqualTo(0)); 37 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 38 | } 39 | 40 | [Test] 41 | public void DoesNotDieOnArrayWithEmptyObject() 42 | { 43 | var table = new JsonTableExtractor("[{}, {}]").GetTable(); 44 | 45 | Assert.That(table.Rows.Count, Is.EqualTo(2)); 46 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 47 | } 48 | 49 | [Test] 50 | public void GetsColumnsAsExpected() 51 | { 52 | var jsonObjects = @" 53 | [ 54 | { 55 | ""First column"": ""Value 1"", 56 | ""Second column"": ""Value 1"" 57 | }, 58 | { 59 | ""First column"": ""Value 1"", 60 | ""Second column"": ""Value 1"" 61 | } 62 | ] 63 | 64 | "; 65 | 66 | var table = new JsonTableExtractor(jsonObjects).GetTable(); 67 | 68 | Assert.That(table.Columns.Select(c => c.Label), Is.EqualTo(new[] { "First column", "Second column" })); 69 | } 70 | 71 | [Test] 72 | public void CanExtractValues_Strings() 73 | { 74 | var json = @"[{""col1"": ""v1"", ""col2"": ""v2"", ""col3"": ""v3""}]"; 75 | 76 | var table = new JsonTableExtractor(json).GetTable(); 77 | 78 | var row = table.Rows.Single(); 79 | 80 | var cellTexts = row.GetAllCells().OrderBy(c => c.TextValue).Select(c => c.TextValue); 81 | 82 | Assert.That(cellTexts, Is.EqualTo(new[] { "v1", "v2", "v3" })); 83 | 84 | } 85 | 86 | [Test] 87 | public void CanExtractValues_Multiple() 88 | { 89 | var json = @"{""col1"": [""line1"", ""line2"", ""line3""]}"; 90 | 91 | var table = new JsonTableExtractor(json).GetTable(); 92 | 93 | var row = table.Rows.Single(); 94 | 95 | var cellLines = row.GetAllCells().Single().Lines; 96 | 97 | Assert.That(cellLines, Is.EqualTo(new[] { "line1", "line2", "line3" })); 98 | 99 | } 100 | } -------------------------------------------------------------------------------- /Tababular.Tests/Extractors/TestObjectTableExtractor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using NUnit.Framework; 4 | using Tababular.Internals.Extractors; 5 | using Tababular.Internals.TableModel; 6 | 7 | namespace Tababular.Tests.Extractors; 8 | 9 | [TestFixture] 10 | public class TestObjectTableExtractor 11 | { 12 | [Test] 13 | public void DoesNotDieOnEmptyArray() 14 | { 15 | var table = new ObjectTableExtractor(new object[0]).GetTable(); 16 | 17 | Assert.That(table.Rows.Count, Is.EqualTo(0)); 18 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 19 | } 20 | 21 | [Test] 22 | public void DoesNotDieOnArrayWithEmptyObject() 23 | { 24 | var table = new ObjectTableExtractor(new[] 25 | { 26 | new {}, 27 | new {} 28 | }).GetTable(); 29 | 30 | Assert.That(table.Rows.Count, Is.EqualTo(2)); 31 | Assert.That(table.Columns.Count, Is.EqualTo(0)); 32 | } 33 | 34 | [Test] 35 | public void GetsColumnsAsExpected() 36 | { 37 | var objects = new[] 38 | { 39 | new 40 | { 41 | FirstColumn = "Value 1", 42 | SecondColumn = "Value 1" 43 | }, 44 | new 45 | { 46 | FirstColumn = "Value 1", 47 | SecondColumn = "Value 1" 48 | } 49 | }; 50 | 51 | var table = new ObjectTableExtractor(objects).GetTable(); 52 | 53 | Assert.That(table.Columns.Select(c => c.Label), Is.EqualTo(new[] { "FirstColumn", "SecondColumn" })); 54 | } 55 | 56 | [Test] 57 | public void CanExtractValues_Strings() 58 | { 59 | var objects = new[] 60 | { 61 | new 62 | { 63 | col1 = "v1", 64 | col2 = "v2", 65 | col3 = "v3", 66 | } 67 | }; 68 | 69 | var table = new ObjectTableExtractor(objects).GetTable(); 70 | 71 | var row = table.Rows.Single(); 72 | 73 | var cellTexts = row.GetAllCells().OrderBy(c => c.TextValue).Select(c => c.TextValue); 74 | 75 | Assert.That(cellTexts, Is.EqualTo(new[] { "v1", "v2", "v3" })); 76 | 77 | } 78 | 79 | [Test] 80 | public void CanExtractValues_Multiple() 81 | { 82 | var objects = new[] 83 | { 84 | new {col1 = new[] {"line1", "line2", "line3"}} 85 | }; 86 | 87 | var table = new ObjectTableExtractor(objects).GetTable(); 88 | 89 | var row = table.Rows.Single(); 90 | 91 | var cellLines = row.GetAllCells().Single().Lines; 92 | 93 | Assert.That(cellLines, Is.EqualTo(new[] { "line1", "line2", "line3" })); 94 | 95 | } 96 | 97 | 98 | [Test] 99 | public void CanExtractListOfPrimitiveObjects() 100 | { 101 | var objects = new List { "line1", "line2", "line3" }; 102 | 103 | var table = new ObjectTableExtractor(objects).GetTable(); 104 | 105 | Assert.That(table.Rows.Count, Is.EqualTo(3)); 106 | Assert.That(table.Rows[0].GetAllCells().Single().Lines, Is.EqualTo(new[] { "line1" })); 107 | Assert.That(table.Rows[1].GetAllCells().Single().Lines, Is.EqualTo(new[] { "line2" })); 108 | Assert.That(table.Rows[2].GetAllCells().Single().Lines, Is.EqualTo(new[] { "line3" })); 109 | } 110 | 111 | [Test] 112 | public void ExtractsColumnsOrderedByHowCodeIsWritten() 113 | { 114 | var objects = new List 115 | { 116 | new SampleRow("1", "2", "3", "4"), 117 | new SampleRow("10", "20", "30", "40"), 118 | new SampleRow("100", "200", "300", "400"), 119 | }; 120 | 121 | var table = new ObjectTableExtractor(objects).GetTable(); 122 | 123 | string[] GetCells(Row row) => row.GetAllCells().Select(cell => cell.TextValue).ToArray(); 124 | 125 | Assert.That(table.Rows.Count, Is.EqualTo(3)); 126 | Assert.That(GetCells(table.Rows[0]), Is.EqualTo(new[] { "1", "2", "3", "4" })); 127 | Assert.That(GetCells(table.Rows[1]), Is.EqualTo(new[] { "10", "20", "30", "40" })); 128 | Assert.That(GetCells(table.Rows[2]), Is.EqualTo(new[] { "100", "200", "300", "400" })); 129 | } 130 | 131 | class SampleRow 132 | { 133 | public SampleRow(string firstColumn, string secondColumn, string thirdColumn, string fourthColumn) 134 | { 135 | FirstColumn = firstColumn; 136 | SecondColumn = secondColumn; 137 | ThirdColumn = thirdColumn; 138 | FourthColumn = fourthColumn; 139 | } 140 | 141 | public string FirstColumn { get; } 142 | public string SecondColumn { get; } 143 | public string ThirdColumn { get; } 144 | public string FourthColumn { get; } 145 | } 146 | } -------------------------------------------------------------------------------- /Tababular.Tests/Formatter/TestBreaker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Tababular.Internals; 6 | 7 | namespace Tababular.Tests.Formatter; 8 | 9 | [TestFixture] 10 | public class TestBreaker 11 | { 12 | [Test] 13 | public void DoesNotBreakWhenThereIsNoNeed() 14 | { 15 | var breaker = new Breaker("this is a short line"); 16 | 17 | var lines = breaker.GetLines(100).ToList(); 18 | 19 | Assert.That(lines, Is.EqualTo(new[] { "this is a short line" })); 20 | } 21 | 22 | [Test] 23 | public void CanBreakBetweenWords() 24 | { 25 | // 0 10 20 26 | var breaker = new Breaker("this is a short line"); 27 | 28 | var lines = breaker.GetLines(10).ToList(); 29 | 30 | Assert.That(lines, Is.EqualTo(new[] 31 | { 32 | "this is a", 33 | "short line" 34 | })); 35 | } 36 | 37 | [Test] 38 | public void BreaksTheRightWayWithMultipleParagraphs() 39 | { 40 | var breaker = new Breaker(@"This is the first line. 41 | 42 | This is the second line, which is actually a paragraph, which happens to be pretty long, although it might/might not have correct punctuation. 43 | 44 | The third line is also fairly long, and it has a bunch of CAPITALIZED FILL WORDS: BLA BLA BLA BLA BLA."); 45 | 46 | const int maxWidth = 50; 47 | var lines = breaker.GetLines(maxWidth).ToList(); 48 | 49 | PrintLines(lines, maxWidth); 50 | 51 | Assert.That(lines, Is.EqualTo(new[] 52 | { 53 | "This is the first line.", 54 | "", 55 | "This is the second line, which is actually a", 56 | "paragraph, which happens to be pretty long,", 57 | "although it might/might not have correct", 58 | "punctuation.", 59 | "", 60 | "The third line is also fairly long, and it has a", 61 | "bunch of CAPITALIZED FILL WORDS: BLA BLA BLA BLA", 62 | "BLA." 63 | })); 64 | } 65 | 66 | [Test] 67 | public void BreaksTheRightWayWhenWordIsJustTooLong() 68 | { 69 | var breaker = new Breaker(@"This is the first line. 70 | 71 | This is a line with an artificial too-long word: huifhueihfwiufewlfehwfeliwhfuilewhfuilehwfuilehwfulehwuflewufilehwufilehwuilfhewuifhwe. 72 | 73 | This is the third line."); 74 | 75 | const int maxWidth = 60; 76 | var lines = breaker.GetLines(maxWidth).ToList(); 77 | 78 | PrintLines(lines, maxWidth); 79 | 80 | Assert.That(lines, Is.EqualTo(new[] 81 | { 82 | "This is the first line.", 83 | "", 84 | "This is a line with an artificial too-long word:", 85 | "huifhueihfwiufewlfehwfeliwhfuilewhfuilehwfuilehwfulehwuflewu", 86 | "filehwufilehwuilfhewuifhwe.", 87 | "", 88 | "This is the third line." 89 | })); 90 | } 91 | 92 | [Test] 93 | public void WorksWhenTooLongWordIsFirst() 94 | { 95 | var breaker = new Breaker("THISISJUSTTOOLONGTOFITONALINE"); 96 | 97 | const int maxWidth = 10; 98 | var lines = breaker.GetLines(maxWidth).ToList(); 99 | 100 | PrintLines(lines, maxWidth); 101 | 102 | Assert.That(lines, Is.EqualTo(new[] 103 | { 104 | "THISISJUST", 105 | "TOOLONGTOF", 106 | "ITONALINE" 107 | })); 108 | } 109 | 110 | [Test] 111 | public void WorksWhenTooLongWordIsFirstAndItIsFollowedByAnotherWord() 112 | { 113 | var breaker = new Breaker("THISISJUSTTOOLONGTOFITONA LINE"); 114 | 115 | const int maxWidth = 20; 116 | var lines = breaker.GetLines(maxWidth).ToList(); 117 | 118 | PrintLines(lines, maxWidth); 119 | 120 | Assert.That(lines, Is.EqualTo(new[] 121 | { 122 | "THISISJUSTTOOLONGTOF", 123 | "ITONA LINE" 124 | })); 125 | } 126 | 127 | static void PrintLines(IEnumerable lines, int maxWidth) 128 | { 129 | Console.WriteLine(new string('*', maxWidth)); 130 | Console.WriteLine(string.Join(Environment.NewLine, lines)); 131 | Console.WriteLine(new string('*', maxWidth)); 132 | } 133 | } -------------------------------------------------------------------------------- /Tababular.Tests/Formatter/TestCustomNewline.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace Tababular.Tests.Formatter; 5 | 6 | [TestFixture] 7 | public class TestCustomNewline 8 | { 9 | [TestCase(true)] 10 | [TestCase(false)] 11 | public void ItWorks(bool customize) 12 | { 13 | var rows = new[] 14 | { 15 | new {Name = "Mogens", Role = "Salesperson"}, 16 | new {Name = "Michael", Role = "Terraformer"}, 17 | }; 18 | 19 | var formatter = new TableFormatter(new() { NewLineSeparator = customize ? $"🙂{Environment.NewLine}" : Environment.NewLine }); 20 | 21 | var output = formatter.FormatObjects(rows); 22 | 23 | Console.WriteLine(output); 24 | 25 | if (customize) 26 | { 27 | Assert.That(output, Is.EqualTo(@"+---------+-------------+🙂 28 | | Name | Role |🙂 29 | +---------+-------------+🙂 30 | | Mogens | Salesperson |🙂 31 | +---------+-------------+🙂 32 | | Michael | Terraformer |🙂 33 | +---------+-------------+🙂 34 | ")); 35 | } 36 | else 37 | { 38 | Assert.That(output, Is.EqualTo(@"+---------+-------------+ 39 | | Name | Role | 40 | +---------+-------------+ 41 | | Mogens | Salesperson | 42 | +---------+-------------+ 43 | | Michael | Terraformer | 44 | +---------+-------------+ 45 | ")); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Tababular.Tests/Formatter/TestTableFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using Tababular.Internals.TableModel; 5 | using Tababular.Tests.Ex; 6 | 7 | namespace Tababular.Tests.Formatter; 8 | 9 | [TestFixture] 10 | public class TestTableFormatter 11 | { 12 | [Test] 13 | public void CanTakeAHint() 14 | { 15 | var column = new Column("Just a colum"); 16 | var row = new Row(); 17 | row.AddCell(column, new Cell(new string('*', 80))); 18 | var table = new Table(new List {column}, new List {row}); 19 | var text = TableFormatter.FormatTable(table, new Hints { MaxTableWidth = 50 }); 20 | 21 | Console.WriteLine(text); 22 | 23 | Assert.That(text.Normalized(), Is.EqualTo(@" 24 | 25 | +---------------------------------------------+ 26 | | Just a colum | 27 | +---------------------------------------------+ 28 | | ******************************************* | 29 | | ************************************* | 30 | +---------------------------------------------+ 31 | 32 | ".Normalized())); 33 | 34 | // Assert.That(text.Normalized(), Is.EqualTo(@" 35 | 36 | //=============================================== 37 | //| Just a colum | 38 | //=============================================== 39 | //| ******************************************* | 40 | //| ************************************* | 41 | //=============================================== 42 | 43 | //".Normalized())); 44 | 45 | } 46 | 47 | [Test] 48 | public void NoColumnsAndNoRows_EmptyResult() 49 | { 50 | var text = TableFormatter.FormatTable(new Table(new List(), new List())); 51 | 52 | Console.WriteLine(text); 53 | 54 | Assert.That(text.Normalized(), Is.EqualTo("".Normalized())); 55 | } 56 | 57 | [Test] 58 | public void NoRows_JustColumnHeaders() 59 | { 60 | var columns = new List 61 | { 62 | new Column("Bimse"), 63 | new Column("Hej"), 64 | }; 65 | var text = TableFormatter.FormatTable(new Table(columns, new List())); 66 | 67 | Console.WriteLine(text); 68 | 69 | Assert.That(text.Normalized(), Is.EqualTo(@" 70 | +-------+-----+ 71 | | Bimse | Hej | 72 | +-------+-----+ 73 | ".Normalized())); 74 | 75 | } 76 | 77 | [Test] 78 | public void NoColumnsAndEmptyRows_EmptyResult() 79 | { 80 | var rows = new List 81 | { 82 | new Row(), 83 | new Row() 84 | }; 85 | var text = TableFormatter.FormatTable(new Table(new List(), rows)); 86 | 87 | Console.WriteLine(text); 88 | 89 | Assert.That(text.Normalized(), Is.EqualTo("".Normalized())); 90 | 91 | } 92 | } -------------------------------------------------------------------------------- /Tababular.Tests/Integration/CollapseTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using Tababular.Tests.Ex; 4 | 5 | namespace Tababular.Tests.Integration; 6 | 7 | [TestFixture] 8 | public class CollapseTest 9 | { 10 | [Test] 11 | public void CanCollapseTable() 12 | { 13 | var objects = new[] 14 | { 15 | new {Id = "whatever01", Value = "jigeojgieojw"}, 16 | new {Id = "whatever02", Value = "huiehguiw"}, 17 | new {Id = "whatever03", Value = "nvnjkdnjkdsjkvds"}, 18 | new {Id = "whatever04", Value = "fjeiufhweui"} 19 | }; 20 | 21 | var formatter = new TableFormatter(new Hints { CollapseVerticallyWhenSingleLine = true }); 22 | 23 | var text = formatter.FormatObjects(objects); 24 | 25 | Console.WriteLine(text); 26 | 27 | Assert.That(text.Normalized(), Is.EqualTo(@" 28 | 29 | +------------+------------------+ 30 | | Id | Value | 31 | +------------+------------------+ 32 | | whatever01 | jigeojgieojw | 33 | | whatever02 | huiehguiw | 34 | | whatever03 | nvnjkdnjkdsjkvds | 35 | | whatever04 | fjeiufhweui | 36 | +------------+------------------+ 37 | 38 | ".Normalized())); 39 | } 40 | 41 | [Test] 42 | public void DoesNotCollapseWhenCellHasMoreLines() 43 | { 44 | var objects = new[] 45 | { 46 | new {Id = "whatever01", Value = "jigeojgieojw"}, 47 | new {Id = "whatever02", Value = @"huiehguiw 48 | ruined the party!"}, 49 | }; 50 | 51 | 52 | var formatter = new TableFormatter(new Hints { CollapseVerticallyWhenSingleLine = true }); 53 | 54 | var text = formatter.FormatObjects(objects); 55 | 56 | Console.WriteLine(text); 57 | 58 | Assert.That(text.Normalized(), Is.EqualTo(@" 59 | 60 | +------------+-------------------+ 61 | | Id | Value | 62 | +------------+-------------------+ 63 | | whatever01 | jigeojgieojw | 64 | +------------+-------------------+ 65 | | whatever02 | huiehguiw | 66 | | | ruined the party! | 67 | +------------+-------------------+ 68 | 69 | ".Normalized())); 70 | } 71 | } -------------------------------------------------------------------------------- /Tababular.Tests/Integration/JsonTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using Tababular.Tests.Ex; 4 | 5 | namespace Tababular.Tests.Integration; 6 | 7 | [TestFixture] 8 | public class JsonTest 9 | { 10 | [Test] 11 | public void CanGenerateTableFromJsonArray() 12 | { 13 | var text = new TableFormatter().FormatJson(@" 14 | [ 15 | {""A property"": ""A value"", ""Another property"": 123}, 16 | {""A property"": ""Another value"", ""Another property"": 2567} 17 | ]"); 18 | 19 | Console.WriteLine(text); 20 | 21 | Assert.That(text.Normalized(), Is.EqualTo(@" 22 | +---------------+------------------+ 23 | | A property | Another property | 24 | +---------------+------------------+ 25 | | A value | 123 | 26 | +---------------+------------------+ 27 | | Another value | 2567 | 28 | +---------------+------------------+ 29 | ".Normalized())); 30 | 31 | } 32 | 33 | [Test] 34 | public void CanGenerateTableFromSingleJsonObject() 35 | { 36 | var text = new TableFormatter().FormatJson(@"{""A property"": ""A value"", ""Another property"": 123}"); 37 | 38 | Console.WriteLine(text); 39 | 40 | Assert.That(text.Normalized(), Is.EqualTo(@" 41 | +------------+------------------+ 42 | | A property | Another property | 43 | +------------+------------------+ 44 | | A value | 123 | 45 | +------------+------------------+ 46 | ".Normalized())); 47 | } 48 | } -------------------------------------------------------------------------------- /Tababular.Tests/Integration/SimpleTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NUnit.Framework; 4 | using Tababular.Tests.Ex; 5 | 6 | namespace Tababular.Tests.Integration; 7 | 8 | [TestFixture] 9 | public class SimpleTest 10 | { 11 | [Test] 12 | public void CanFormatAsTableWithTwoColumns() 13 | { 14 | var tableFormatter = new TableFormatter(); 15 | 16 | var text = tableFormatter.FormatObjects(new[] 17 | { 18 | new {FirstColumn = "r1", SecondColumn = "hej"}, 19 | new {FirstColumn = "r2", SecondColumn = "hej"}, 20 | new {FirstColumn = "r3", SecondColumn = "hej"}, 21 | new {FirstColumn = "r4", SecondColumn = "hej"} 22 | }); 23 | 24 | Console.WriteLine(text); 25 | 26 | const string expected = @" 27 | +-------------+--------------+ 28 | | FirstColumn | SecondColumn | 29 | +-------------+--------------+ 30 | | r1 | hej | 31 | +-------------+--------------+ 32 | | r2 | hej | 33 | +-------------+--------------+ 34 | | r3 | hej | 35 | +-------------+--------------+ 36 | | r4 | hej | 37 | +-------------+--------------+ 38 | "; 39 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 40 | } 41 | 42 | [Test] 43 | public void CanFormatAsTableWithThreeColumns() 44 | { 45 | var tableFormatter = new TableFormatter(); 46 | 47 | var objects = new[] 48 | { 49 | new {FirstColumn = "r1", SecondColumn = "hej", ThirdColumn = "hej igen"}, 50 | new {FirstColumn = "r2", SecondColumn = "hej", ThirdColumn = "hej igen"}, 51 | }; 52 | 53 | var text = tableFormatter.FormatObjects(objects); 54 | 55 | Console.WriteLine(text); 56 | 57 | const string expected = @" 58 | 59 | +-------------+--------------+-------------+ 60 | | FirstColumn | SecondColumn | ThirdColumn | 61 | +-------------+--------------+-------------+ 62 | | r1 | hej | hej igen | 63 | +-------------+--------------+-------------+ 64 | | r2 | hej | hej igen | 65 | +-------------+--------------+-------------+ 66 | 67 | "; 68 | 69 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 70 | } 71 | 72 | [Test] 73 | public void SimplePaddingTest_OneColumn() 74 | { 75 | var tableFormatter = new TableFormatter(); 76 | 77 | var objects = new[] 78 | { 79 | new {A = "A"} 80 | }; 81 | 82 | var text = tableFormatter.FormatObjects(objects); 83 | 84 | Console.WriteLine(text); 85 | 86 | const string expected = @" 87 | 88 | +---+ 89 | | A | 90 | +---+ 91 | | A | 92 | +---+ 93 | "; 94 | 95 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 96 | } 97 | 98 | [Test] 99 | public void SimplePaddingTest_TwoColumns() 100 | { 101 | var tableFormatter = new TableFormatter(); 102 | 103 | var objects = new[] 104 | { 105 | new {A = "A", B = "B"} 106 | }; 107 | 108 | var text = tableFormatter.FormatObjects(objects); 109 | 110 | Console.WriteLine(text); 111 | 112 | const string expected = @" 113 | 114 | +---+---+ 115 | | A | B | 116 | +---+---+ 117 | | A | B | 118 | +---+---+ 119 | "; 120 | 121 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 122 | } 123 | 124 | [Test] 125 | public void CanFormatAsTableWithCellWiderThanColumnLabel() 126 | { 127 | var tableFormatter = new TableFormatter(); 128 | 129 | var text = tableFormatter.FormatObjects(new[] 130 | { 131 | new {FirstColumn = "This is a fairly long text"} 132 | }); 133 | 134 | Console.WriteLine(text); 135 | 136 | const string expected = @" 137 | +----------------------------+ 138 | | FirstColumn | 139 | +----------------------------+ 140 | | This is a fairly long text | 141 | +----------------------------+ 142 | "; 143 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 144 | } 145 | 146 | [Test] 147 | public void CanUseDictionaryAsInput() 148 | { 149 | var tableFormatter = new TableFormatter(); 150 | 151 | var text = tableFormatter.FormatDictionaries(new[] 152 | { 153 | new Dictionary { {"Headline with space", "Some value"} }, 154 | new Dictionary { {"Headline with space", "Another value"} }, 155 | new Dictionary { {"Another headline with space", "Third value"} }, 156 | new Dictionary { {"Yet another headline with space", "Fourth value"} }, 157 | }); 158 | 159 | Console.WriteLine(text); 160 | 161 | const string expected = @" 162 | 163 | 164 | +---------------------+-----------------------------+---------------------------------+ 165 | | Headline with space | Another headline with space | Yet another headline with space | 166 | +---------------------+-----------------------------+---------------------------------+ 167 | | Some value | | | 168 | +---------------------+-----------------------------+---------------------------------+ 169 | | Another value | | | 170 | +---------------------+-----------------------------+---------------------------------+ 171 | | | Third value | | 172 | +---------------------+-----------------------------+---------------------------------+ 173 | | | | Fourth value | 174 | +---------------------+-----------------------------+---------------------------------+ 175 | 176 | "; 177 | 178 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 179 | } 180 | 181 | [Test] 182 | public void CanFormatAsTableWithCellWithMultipleLines() 183 | { 184 | var tableFormatter = new TableFormatter(); 185 | 186 | var text = tableFormatter.FormatObjects(new[] 187 | { 188 | new {FirstColumn = @"This is the first line 189 | This is the second line 190 | And this is the third and last line"} 191 | }); 192 | 193 | Console.WriteLine(text); 194 | 195 | const string expected = @" 196 | 197 | +-------------------------------------+ 198 | | FirstColumn | 199 | +-------------------------------------+ 200 | | This is the first line | 201 | | This is the second line | 202 | | And this is the third and last line | 203 | +-------------------------------------+ 204 | "; 205 | 206 | Assert.That(text.Normalized(), Is.EqualTo(expected.Normalized())); 207 | } 208 | 209 | [Test] 210 | public void MultipleCellsWithMultipleLines() 211 | { 212 | var objects = new[] 213 | { 214 | new { MachineName = "ctxtest01", Ip = "10.0.0.10", Ports = new[] {80, 8080, 9090}}, 215 | new { MachineName = "ctxtest02", Ip = "10.0.0.11", Ports = new[] {80, 5432}} 216 | }; 217 | 218 | var text = new TableFormatter().FormatObjects(objects); 219 | 220 | Console.WriteLine(text); 221 | } 222 | } -------------------------------------------------------------------------------- /Tababular.Tests/Tababular.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Tababular.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.29709.97 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tababular", "Tababular\Tababular.csproj", "{94EDA166-33F2-4D88-B5B4-268429254205}" 6 | EndProject 7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tababular.Tests", "Tababular.Tests\Tababular.Tests.csproj", "{512230FE-18E5-4C59-9AE8-6432B0A6143A}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stuff", "stuff", "{D6C5120D-809A-41A0-9B0C-4149C88C56E1}" 10 | ProjectSection(SolutionItems) = preProject 11 | appveyor.yml = appveyor.yml 12 | CHANGELOG.md = CHANGELOG.md 13 | LICENSE.md = LICENSE.md 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{BB6EFF8C-E12C-4B6A-AB53-6BFEE4A5CC86}" 18 | ProjectSection(SolutionItems) = preProject 19 | scripts\build.cmd = scripts\build.cmd 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {94EDA166-33F2-4D88-B5B4-268429254205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {94EDA166-33F2-4D88-B5B4-268429254205}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {94EDA166-33F2-4D88-B5B4-268429254205}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {94EDA166-33F2-4D88-B5B4-268429254205}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {512230FE-18E5-4C59-9AE8-6432B0A6143A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {512230FE-18E5-4C59-9AE8-6432B0A6143A}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {512230FE-18E5-4C59-9AE8-6432B0A6143A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {512230FE-18E5-4C59-9AE8-6432B0A6143A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {FC8EE34B-6F42-4342-BDE1-B52E5407D906} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /Tababular/Hints.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Tababular; 4 | 5 | /// 6 | /// Represents some hints on how to format things 7 | /// 8 | public class Hints 9 | { 10 | /// 11 | /// Can be used to specify the max width that the table should try to fit within 12 | /// 13 | public int? MaxTableWidth { get; set; } 14 | 15 | /// 16 | /// Can be used to specify that the table formatter can make the table vertically more compact 17 | /// when no cell has more than one line 18 | /// 19 | public bool CollapseVerticallyWhenSingleLine { get; set; } 20 | 21 | /// 22 | /// Can be used to specify the separator that the table formatter will use for new lines. 23 | /// By default has value of 24 | /// 25 | public string NewLineSeparator { get; set; } = Environment.NewLine; 26 | } -------------------------------------------------------------------------------- /Tababular/Internals/Breaker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Tababular.Internals.Extensions; 6 | 7 | namespace Tababular.Internals; 8 | 9 | class Breaker 10 | { 11 | readonly string _text; 12 | 13 | public Breaker(string text) 14 | { 15 | _text = text; 16 | } 17 | 18 | public IEnumerable GetLines(int maxWidth) 19 | { 20 | var lines = _text.Split(new[] { Environment.NewLine, "\r", "\n" }, StringSplitOptions.None); 21 | 22 | return lines 23 | .SelectMany(line => BreakLines(line, maxWidth) 24 | .ToList()); 25 | } 26 | 27 | static IEnumerable BreakLines(string line, int maxWidth) 28 | { 29 | if (line.Length <= maxWidth) 30 | { 31 | yield return line; 32 | yield break; 33 | } 34 | 35 | var words = line.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); 36 | var builder = new StringBuilder(); 37 | 38 | foreach (var word in words) 39 | { 40 | if (builder.Length == 0) 41 | { 42 | if (word.Length > maxWidth) 43 | { 44 | var forceBrokenWords = word 45 | .Batch(maxWidth) 46 | .Select(characters => new string(characters.ToArray())) 47 | .ToList(); 48 | 49 | foreach (var forceBrokenWord in forceBrokenWords.Take(forceBrokenWords.Count - 1)) 50 | { 51 | yield return forceBrokenWord; 52 | } 53 | 54 | builder.Append(forceBrokenWords.Last()); 55 | } 56 | else 57 | { 58 | builder.Append(word); 59 | } 60 | } 61 | else if (builder.Length + word.Length < maxWidth) 62 | { 63 | builder.Append(" " + word); 64 | } 65 | else 66 | { 67 | yield return builder.ToString(); 68 | 69 | if (word.Length > maxWidth) 70 | { 71 | var forceBrokenWords = word 72 | .Batch(maxWidth) 73 | .Select(characters => new string(characters.ToArray())) 74 | .ToList(); 75 | 76 | foreach (var forceBrokenWord in forceBrokenWords.Take(forceBrokenWords.Count - 1)) 77 | { 78 | yield return forceBrokenWord; 79 | } 80 | 81 | builder = new StringBuilder(forceBrokenWords.Last()); 82 | } 83 | else 84 | { 85 | builder = new StringBuilder(word); 86 | } 87 | } 88 | } 89 | 90 | if (builder.Length > 0) 91 | { 92 | yield return builder.ToString(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Tababular/Internals/Extensions/DictionaryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Tababular.Internals.Extensions; 5 | 6 | static class DictionaryExtensions 7 | { 8 | public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func newValueFactory) 9 | { 10 | if (dictionary.TryGetValue(key, out var existing)) return existing; 11 | 12 | var newValue = newValueFactory(key); 13 | 14 | dictionary[key] = newValue; 15 | 16 | return newValue; 17 | } 18 | } -------------------------------------------------------------------------------- /Tababular/Internals/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Tababular.Internals.Extensions; 5 | 6 | static class EnumerableExtensions 7 | { 8 | public static IEnumerable> Batch(this IEnumerable items, int maxBatchSize) 9 | { 10 | var list = new List(); 11 | 12 | foreach (var item in items) 13 | { 14 | list.Add(item); 15 | 16 | if (list.Count < maxBatchSize) continue; 17 | 18 | yield return list; 19 | list = new List(); 20 | } 21 | 22 | if (list.Any()) 23 | { 24 | yield return list; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Tababular/Internals/Extractors/DictionaryTableExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Tababular.Internals.Extensions; 5 | using Tababular.Internals.TableModel; 6 | 7 | namespace Tababular.Internals.Extractors; 8 | 9 | class DictionaryTableExtractor : ITableExtractor 10 | { 11 | readonly List> _rows; 12 | 13 | public DictionaryTableExtractor(IEnumerable> rows) 14 | { 15 | if (rows == null) throw new ArgumentNullException(nameof(rows)); 16 | _rows = rows.ToList(); 17 | } 18 | 19 | public Table GetTable() 20 | { 21 | var columns = new Dictionary(); 22 | var rows = new List(); 23 | 24 | foreach (var dictRow in _rows) 25 | { 26 | var row = new Row(); 27 | 28 | foreach (var name in dictRow.Keys) 29 | { 30 | var column = columns.GetOrAdd(name, _ => new Column(name)); 31 | var value = dictRow[name]; 32 | 33 | row.AddCell(column, new Cell(value)); 34 | } 35 | 36 | rows.Add(row); 37 | } 38 | 39 | return new Table(columns.Values.ToList(), rows); 40 | } 41 | } -------------------------------------------------------------------------------- /Tababular/Internals/Extractors/JsonTableExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json.Linq; 5 | using Tababular.Internals.Extensions; 6 | using Tababular.Internals.TableModel; 7 | 8 | namespace Tababular.Internals.Extractors 9 | { 10 | class JsonTableExtractor : ITableExtractor 11 | { 12 | readonly string _jsonText; 13 | 14 | public JsonTableExtractor(string jsonText) 15 | { 16 | _jsonText = jsonText ?? throw new ArgumentNullException(nameof(jsonText)); 17 | } 18 | 19 | public Table GetTable() 20 | { 21 | var table = ParseArrayOrNull() 22 | ?? ParseJsonlOrNull() 23 | ?? ParseObjectOrNull(); 24 | 25 | if (table == null) 26 | { 27 | throw new FormatException($"Could not interpret this as JSON: '{_jsonText}'"); 28 | } 29 | 30 | return table; 31 | } 32 | 33 | Table ParseJsonlOrNull() 34 | { 35 | try 36 | { 37 | var lines = _jsonText 38 | .Split(new[] {Environment.NewLine, "\r", "\n"}, StringSplitOptions.RemoveEmptyEntries) 39 | .Select(line => line.Trim()) 40 | .ToList(); 41 | 42 | var looksLikeJsonLines = lines 43 | .All(line => line.StartsWith("{") && line.EndsWith("}")); 44 | 45 | if (!looksLikeJsonLines) 46 | { 47 | return null; 48 | } 49 | 50 | var jsonObjects = lines.Select(JObject.Parse); 51 | 52 | var jsonArray = new JArray(jsonObjects); 53 | 54 | return GetTableFromJsonArray(jsonArray); 55 | } 56 | catch 57 | { 58 | return null; 59 | } 60 | } 61 | 62 | Table ParseObjectOrNull() 63 | { 64 | try 65 | { 66 | var singleObject = JObject.Parse(_jsonText); 67 | 68 | var jsonArray = new JArray(singleObject); 69 | 70 | return GetTableFromJsonArray(jsonArray); 71 | } 72 | catch 73 | { 74 | return null; 75 | } 76 | } 77 | 78 | Table ParseArrayOrNull() 79 | { 80 | try 81 | { 82 | var jsonArray = JArray.Parse(_jsonText); 83 | 84 | return GetTableFromJsonArray(jsonArray); 85 | } 86 | catch 87 | { 88 | return null; 89 | } 90 | } 91 | 92 | static Table GetTableFromJsonArray(JArray jsonArray) 93 | { 94 | var columns = new Dictionary(); 95 | var rows = new List(); 96 | 97 | foreach (var jsonToken in jsonArray) 98 | { 99 | var row = new Row(); 100 | 101 | var jsonObject = jsonToken.Value(); 102 | 103 | foreach (var property in jsonObject.Properties()) 104 | { 105 | var name = property.Name; 106 | var column = columns.GetOrAdd(name, _ => new Column(name)); 107 | 108 | var value = property.Value; 109 | 110 | var stringValue = value.Type == JTokenType.Array 111 | ? GetAsLines(value) 112 | : value.ToString(); 113 | 114 | row.AddCell(column, new Cell(stringValue)); 115 | } 116 | 117 | rows.Add(row); 118 | } 119 | 120 | return new Table(columns.Values.ToList(), rows); 121 | } 122 | 123 | static string GetAsLines(JToken value) 124 | { 125 | var lines = value.Values(); 126 | 127 | return string.Join(Environment.NewLine, lines); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /Tababular/Internals/Extractors/ObjectTableExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using FastMember; 6 | using Tababular.Internals.Extensions; 7 | using Tababular.Internals.TableModel; 8 | 9 | namespace Tababular.Internals.Extractors; 10 | 11 | class ObjectTableExtractor : ITableExtractor 12 | { 13 | readonly List _objectRows; 14 | 15 | public ObjectTableExtractor(IEnumerable objectRows) 16 | { 17 | if (objectRows == null) throw new ArgumentNullException(nameof(objectRows)); 18 | _objectRows = objectRows.Cast().ToList(); 19 | } 20 | 21 | public Table GetTable() 22 | { 23 | var columns = new Dictionary(); 24 | var rows = new List(); 25 | var typeAccessors = new Dictionary(); 26 | var memberLists = new Dictionary(); 27 | 28 | foreach (var objectRow in _objectRows) 29 | { 30 | var row = new Row(); 31 | var rowType = objectRow.GetType(); 32 | 33 | if (Convert.GetTypeCode(objectRow) == TypeCode.Object) 34 | { 35 | var accessor = typeAccessors.GetOrAdd(rowType, TypeAccessor.Create); 36 | var members = memberLists.GetOrAdd(rowType, type => GetMemberAccessors(accessor, type)); 37 | 38 | foreach (var member in members) 39 | { 40 | var name = member.Name; 41 | var column = columns.GetOrAdd(name, _ => new Column(name)); 42 | 43 | object GetValue() 44 | { 45 | try 46 | { 47 | return accessor[objectRow, name]; 48 | } 49 | catch (Exception exception) 50 | { 51 | throw new ArgumentException($"Could not get value from property '{name}' of {rowType}", exception); 52 | } 53 | } 54 | 55 | var value = GetValue(); 56 | 57 | row.AddCell(column, new Cell(value)); 58 | } 59 | } 60 | else 61 | { 62 | var name = rowType.Name; 63 | var column = columns.GetOrAdd(name, _ => new Column(name)); 64 | row.AddCell(column, new Cell(objectRow)); 65 | } 66 | 67 | rows.Add(row); 68 | } 69 | 70 | return new Table(columns.Values.ToList(), rows); 71 | } 72 | 73 | static Member[] GetMemberAccessors(TypeAccessor accessor, Type type) 74 | { 75 | var memberSet = accessor.GetMembers(); 76 | 77 | try 78 | { 79 | var orderDictionary = type.GetProperties() 80 | .Select((property, index) => new {property, index}) 81 | .ToDictionary(a => a.property.Name, a => a.index); 82 | 83 | return memberSet 84 | .OrderBy(m => orderDictionary[m.Name]) 85 | .ToArray(); 86 | } 87 | catch 88 | { 89 | return memberSet.ToArray(); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /Tababular/Internals/ITableExtractor.cs: -------------------------------------------------------------------------------- 1 | using Tababular.Internals.TableModel; 2 | 3 | namespace Tababular.Internals; 4 | 5 | interface ITableExtractor 6 | { 7 | Table GetTable(); 8 | } -------------------------------------------------------------------------------- /Tababular/Internals/InternalsVisibleTo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Tababular.Tests")] 4 | -------------------------------------------------------------------------------- /Tababular/Internals/TableModel/Cell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Tababular.Internals.TableModel; 7 | 8 | class Cell 9 | { 10 | public Cell(object value) 11 | { 12 | if (value is string) 13 | { 14 | TextValue = value?.ToString() ?? ""; 15 | } 16 | else if (value is IEnumerable) 17 | { 18 | TextValue = string.Join(Environment.NewLine, ((IEnumerable)value).Cast()); 19 | } 20 | else 21 | { 22 | TextValue = value?.ToString() ?? ""; 23 | } 24 | 25 | Lines = GetLines(); 26 | } 27 | 28 | string[] GetLines() 29 | { 30 | return TextValue 31 | .Split(new[] { Environment.NewLine }, StringSplitOptions.None); 32 | } 33 | 34 | public string[] Lines { get; private set; } 35 | 36 | public string TextValue { get; } 37 | 38 | public int GetWidth() 39 | { 40 | return Lines.Any() 41 | ? Lines.Max(l => l.Length) 42 | : 0; 43 | } 44 | 45 | public int GetHeight() 46 | { 47 | return Lines.Length; 48 | } 49 | 50 | public void Rearrange(Column column) 51 | { 52 | var maxLineLength = column.Width - 2 * column.Padding; 53 | 54 | Lines = Lines 55 | .SelectMany(line => 56 | { 57 | var breaker = new Breaker(line); 58 | 59 | return breaker.GetLines(maxLineLength); 60 | }) 61 | .ToArray(); 62 | } 63 | } -------------------------------------------------------------------------------- /Tababular/Internals/TableModel/Column.cs: -------------------------------------------------------------------------------- 1 | namespace Tababular.Internals.TableModel; 2 | 3 | class Column 4 | { 5 | public Column(string label) 6 | { 7 | Label = label ?? ""; 8 | 9 | Padding = 1; 10 | 11 | AdjustForWidth(Label.Length); 12 | } 13 | 14 | public string Label { get; } 15 | 16 | public int Width { get; private set; } 17 | 18 | public int Padding { get; } 19 | 20 | public void AdjustWidth(Cell cell) 21 | { 22 | var cellWidth = cell.GetWidth(); 23 | 24 | AdjustForWidth(cellWidth); 25 | } 26 | 27 | void AdjustForWidth(int cellWidth) 28 | { 29 | var width = cellWidth + 2 * Padding; 30 | 31 | if (width < Width) return; 32 | 33 | Width = width; 34 | } 35 | 36 | public void ConstrainWidth(int newWidth) 37 | { 38 | Width = newWidth; 39 | } 40 | } -------------------------------------------------------------------------------- /Tababular/Internals/TableModel/Row.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Tababular.Internals.TableModel; 5 | 6 | class Row 7 | { 8 | readonly Dictionary _cells = new(); 9 | 10 | public void AddCell(Column column, Cell cell) 11 | { 12 | try 13 | { 14 | _cells.Add(column, cell); 15 | 16 | AdjustHeight(cell); 17 | } 18 | catch (Exception exception) 19 | { 20 | throw new InvalidOperationException($"Tried to add cell '{cell}' to row as column '{column.Label}', but the row had this cell already: '{_cells[column]}'", exception); 21 | } 22 | } 23 | 24 | public Cell GetCellOrNull(Column column) 25 | { 26 | Cell cell; 27 | 28 | return _cells.TryGetValue(column, out cell) 29 | ? cell 30 | : null; 31 | } 32 | 33 | void AdjustHeight(Cell cell) 34 | { 35 | var numberOfLines = cell.GetHeight(); 36 | 37 | if (numberOfLines < Height) return; 38 | 39 | Height = numberOfLines; 40 | } 41 | 42 | public int Height { get; private set; } 43 | 44 | public IEnumerable GetAllCells() 45 | { 46 | return _cells.Values; 47 | } 48 | } -------------------------------------------------------------------------------- /Tababular/Internals/TableModel/Table.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Tababular.Internals.TableModel; 6 | 7 | class Table 8 | { 9 | public Table(List columns, List rows) 10 | { 11 | Rows = rows ?? throw new ArgumentNullException(nameof(rows)); 12 | Columns = columns ?? throw new ArgumentNullException(nameof(columns)); 13 | 14 | foreach (var column in Columns) 15 | { 16 | foreach (var row in Rows) 17 | { 18 | var cellOrNull = row.GetCellOrNull(column); 19 | 20 | if (cellOrNull == null) continue; 21 | 22 | column.AdjustWidth(cellOrNull); 23 | } 24 | } 25 | 26 | } 27 | 28 | public List Columns { get; } 29 | public List Rows { get; } 30 | 31 | public bool HasCellWith(Func cellPredicate) 32 | { 33 | return Rows.SelectMany(r => r.GetAllCells()).Any(cellPredicate); 34 | } 35 | } -------------------------------------------------------------------------------- /Tababular/Tababular.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Library 5 | netstandard2.0 6 | 10 7 | mookid8000 8 | https://github.com/rebus-org/Tababular 9 | Copyright 2016 10 | console 11 | https://github.com/rebus-org/Tabababular 12 | git 13 | MIT 14 | tababular_logo.png 15 | README.md 16 | 17 | 18 | 19 | True 20 | \ 21 | 22 | 23 | True 24 | \ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Tababular/TableFormatter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Tababular.Internals; 6 | using Tababular.Internals.Extractors; 7 | using Tababular.Internals.TableModel; 8 | 9 | namespace Tababular 10 | { 11 | /// 12 | /// This is the table formatter. New it up and call one of the Format*** methods on it, e.g. 13 | /// with a sequence of objects whose properties will become columns, 14 | /// or with a sequence of dictionaries whose keys will become columns. 15 | /// 16 | public class TableFormatter 17 | { 18 | readonly Hints _hints; 19 | 20 | /// 21 | /// Creates the table formatter possibly using the given to do its thing 22 | /// 23 | public TableFormatter(Hints hints = null) 24 | { 25 | _hints = hints ?? new Hints(); 26 | } 27 | 28 | /// 29 | /// Formats a sequence of objects as rows of a table, using the property names as column names 30 | /// 31 | public string FormatObjects(IEnumerable rows) 32 | { 33 | var extractor = new ObjectTableExtractor(rows); 34 | 35 | return UseExtractor(extractor); 36 | } 37 | 38 | /// 39 | /// Formats a sequence of dictionaries as rows of a table, using the dictionary keys as column names 40 | /// 41 | public string FormatDictionaries(IEnumerable> rows) 42 | { 43 | var extractor = new DictionaryTableExtractor(rows.Select(r => r.ToDictionary(d => d.Key, d => (object)d.Value))); 44 | 45 | return UseExtractor(extractor); 46 | } 47 | 48 | /// 49 | /// Formats the given JSON object or array of objects as a table 50 | /// 51 | public string FormatJson(string jsonText) 52 | { 53 | var extractor = new JsonTableExtractor(jsonText); 54 | 55 | return UseExtractor(extractor); 56 | } 57 | 58 | string UseExtractor(ITableExtractor extractor) 59 | { 60 | var table = extractor.GetTable(); 61 | 62 | return FormatTable(table, _hints); 63 | } 64 | 65 | internal static string FormatTable(Table table, Hints hints = null) => InnerFormatTable(table, hints ?? new Hints()); 66 | 67 | static string InnerFormatTable(Table table, Hints hints) 68 | { 69 | if (!table.Columns.Any()) return ""; 70 | 71 | if (hints.MaxTableWidth.HasValue) 72 | { 73 | EnforceMaxWidth(table, hints.MaxTableWidth.Value); 74 | } 75 | 76 | const char horizontalLineChar = '-'; 77 | const char verticalLineChar = '|'; 78 | const char cornerChar = '+'; 79 | 80 | var skipHorizontalLines = hints.CollapseVerticallyWhenSingleLine 81 | && !table.HasCellWith(c => c.Lines.Length > 1); 82 | 83 | var builder = new StringBuilder(); 84 | 85 | BuildHorizontalLine(table, builder, horizontalLineChar, cornerChar, hints.NewLineSeparator); 86 | 87 | BuildColumnLabels(table, builder, verticalLineChar, hints.NewLineSeparator); 88 | 89 | if (skipHorizontalLines) 90 | { 91 | BuildHorizontalLine(table, builder, horizontalLineChar, cornerChar, hints.NewLineSeparator); 92 | } 93 | 94 | if (table.Rows.Any()) 95 | { 96 | foreach (var row in table.Rows) 97 | { 98 | if (!skipHorizontalLines) 99 | { 100 | BuildHorizontalLine(table, builder, horizontalLineChar, cornerChar, hints.NewLineSeparator); 101 | } 102 | 103 | BuildTableRow(row, table, builder, verticalLineChar, hints.NewLineSeparator); 104 | } 105 | } 106 | 107 | BuildHorizontalLine(table, builder, horizontalLineChar, cornerChar, hints.NewLineSeparator); 108 | 109 | return builder.ToString(); 110 | 111 | } 112 | 113 | static void EnforceMaxWidth(Table table, int maxWidth) 114 | { 115 | if (!table.Columns.Any()) return; 116 | 117 | while (true) 118 | { 119 | var totalWidth = table.Columns.Sum(c => c.Width); 120 | 121 | if (totalWidth < maxWidth) break; 122 | 123 | var widestColumn = table.Columns 124 | .OrderByDescending(c => c.Width) 125 | .First(); 126 | 127 | widestColumn.ConstrainWidth(3 * widestColumn.Width / 4); 128 | } 129 | 130 | foreach (var column in table.Columns) 131 | { 132 | foreach (var row in table.Rows) 133 | { 134 | var cell = row.GetCellOrNull(column); 135 | if (cell == null) continue; 136 | 137 | cell.Rearrange(column); 138 | } 139 | } 140 | } 141 | 142 | static void BuildColumnLabels(Table table, StringBuilder builder, char verticalLineChar, string newLineChar) 143 | { 144 | var texts = table.Columns 145 | .Select(column => new 146 | { 147 | Column = column, 148 | Text = new[] { column.Label } 149 | }) 150 | .ToDictionary(a => a.Column, a => a.Text); 151 | 152 | BuildRow(texts, table, builder, verticalLineChar, newLineChar); 153 | } 154 | 155 | static void BuildTableRow(Row row, Table table, StringBuilder builder, char verticalLineChar, string newLineChar) 156 | { 157 | var texts = table.Columns 158 | .Select(column => new 159 | { 160 | Column = column, 161 | Text = row.GetCellOrNull(column)?.Lines ?? new string[0] 162 | }) 163 | .ToDictionary(a => a.Column, a => a.Text); 164 | 165 | BuildRow(texts, table, builder, verticalLineChar, newLineChar); 166 | } 167 | 168 | static void BuildRow(Dictionary texts, Table table, StringBuilder builder, char verticalLineChar, string newLineChar) 169 | { 170 | for (var index = 0; index < texts.Values.Max(l => l.Length); index++) 171 | { 172 | builder.Append(verticalLineChar); 173 | 174 | foreach (var colum in table.Columns) 175 | { 176 | var lines = texts[colum]; 177 | var text = index < lines.Length ? lines[index] : ""; 178 | 179 | builder.Append(new string(' ', colum.Padding)); 180 | builder.Append(text.PadRight(colum.Width - colum.Padding)); 181 | builder.Append(verticalLineChar); 182 | } 183 | 184 | builder.Append(newLineChar); 185 | } 186 | } 187 | 188 | static void BuildHorizontalLine(Table table, StringBuilder builder, char lineCharacter, char cornerCharacter, string newLineChar) 189 | { 190 | var columns = table.Columns; 191 | 192 | if (!columns.Any()) 193 | { 194 | return; 195 | } 196 | 197 | 198 | foreach (var column in columns) 199 | { 200 | builder.Append(cornerCharacter); 201 | builder.Append(new string(lineCharacter, column.Width + column.Padding - 1)); 202 | } 203 | 204 | builder.Append(cornerCharacter); 205 | 206 | builder.Append(newLineChar); 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | 3 | before_build: 4 | - appveyor-retry dotnet restore -v Minimal 5 | 6 | build_script: 7 | - dotnet build Tababular -c Release 8 | - dotnet build Tababular.Tests -c Release 9 | -------------------------------------------------------------------------------- /artwork/tababular_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/artwork/tababular_logo.png -------------------------------------------------------------------------------- /packages/NUnit.2.6.4/NUnit.2.6.4.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/NUnit.2.6.4/NUnit.2.6.4.nupkg -------------------------------------------------------------------------------- /packages/NUnit.2.6.4/lib/nunit.framework.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/NUnit.2.6.4/lib/nunit.framework.dll -------------------------------------------------------------------------------- /packages/NUnit.2.6.4/license.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/NUnit.2.6.4/license.txt -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/Newtonsoft.Json.8.0.3.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/Newtonsoft.Json.8.0.3.nupkg -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/lib/net20/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/lib/net20/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/lib/net35/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/lib/net35/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/lib/net40/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/lib/net40/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/lib/net45/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/lib/net45/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/lib/portable-net45+wp80+win8+wpa81+dnxcore50/Newtonsoft.Json.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/packages/Newtonsoft.Json.8.0.3/lib/portable-net45+wp80+win8+wpa81+dnxcore50/Newtonsoft.Json.dll -------------------------------------------------------------------------------- /packages/Newtonsoft.Json.8.0.3/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | # open json.net splash page on package install 4 | # don't open if json.net is installed as a dependency 5 | 6 | try 7 | { 8 | $url = "http://www.newtonsoft.com/json/install?version=" + $package.Version 9 | $dte2 = Get-Interface $dte ([EnvDTE80.DTE2]) 10 | 11 | if ($dte2.ActiveWindow.Caption -eq "Package Manager Console") 12 | { 13 | # user is installing from VS NuGet console 14 | # get reference to the window, the console host and the input history 15 | # show webpage if "install-package newtonsoft.json" was last input 16 | 17 | $consoleWindow = $(Get-VSComponentModel).GetService([NuGetConsole.IPowerConsoleWindow]) 18 | 19 | $props = $consoleWindow.GetType().GetProperties([System.Reflection.BindingFlags]::Instance -bor ` 20 | [System.Reflection.BindingFlags]::NonPublic) 21 | 22 | $prop = $props | ? { $_.Name -eq "ActiveHostInfo" } | select -first 1 23 | if ($prop -eq $null) { return } 24 | 25 | $hostInfo = $prop.GetValue($consoleWindow) 26 | if ($hostInfo -eq $null) { return } 27 | 28 | $history = $hostInfo.WpfConsole.InputHistory.History 29 | 30 | $lastCommand = $history | select -last 1 31 | 32 | if ($lastCommand) 33 | { 34 | $lastCommand = $lastCommand.Trim().ToLower() 35 | if ($lastCommand.StartsWith("install-package") -and $lastCommand.Contains("newtonsoft.json")) 36 | { 37 | $dte2.ItemOperations.Navigate($url) | Out-Null 38 | } 39 | } 40 | } 41 | else 42 | { 43 | # user is installing from VS NuGet dialog 44 | # get reference to the window, then smart output console provider 45 | # show webpage if messages in buffered console contains "installing...newtonsoft.json" in last operation 46 | 47 | $instanceField = [NuGet.Dialog.PackageManagerWindow].GetField("CurrentInstance", [System.Reflection.BindingFlags]::Static -bor ` 48 | [System.Reflection.BindingFlags]::NonPublic) 49 | 50 | $consoleField = [NuGet.Dialog.PackageManagerWindow].GetField("_smartOutputConsoleProvider", [System.Reflection.BindingFlags]::Instance -bor ` 51 | [System.Reflection.BindingFlags]::NonPublic) 52 | 53 | if ($instanceField -eq $null -or $consoleField -eq $null) { return } 54 | 55 | $instance = $instanceField.GetValue($null) 56 | 57 | if ($instance -eq $null) { return } 58 | 59 | $consoleProvider = $consoleField.GetValue($instance) 60 | if ($consoleProvider -eq $null) { return } 61 | 62 | $console = $consoleProvider.CreateOutputConsole($false) 63 | 64 | $messagesField = $console.GetType().GetField("_messages", [System.Reflection.BindingFlags]::Instance -bor ` 65 | [System.Reflection.BindingFlags]::NonPublic) 66 | if ($messagesField -eq $null) { return } 67 | 68 | $messages = $messagesField.GetValue($console) 69 | if ($messages -eq $null) { return } 70 | 71 | $operations = $messages -split "==============================" 72 | 73 | $lastOperation = $operations | select -last 1 74 | 75 | if ($lastOperation) 76 | { 77 | $lastOperation = $lastOperation.ToLower() 78 | 79 | $lines = $lastOperation -split "`r`n" 80 | 81 | $installMatch = $lines | ? { $_.StartsWith("------- installing...newtonsoft.json ") } | select -first 1 82 | 83 | if ($installMatch) 84 | { 85 | $dte2.ItemOperations.Navigate($url) | Out-Null 86 | } 87 | } 88 | } 89 | } 90 | catch 91 | { 92 | try 93 | { 94 | $pmPane = $dte2.ToolWindows.OutputWindow.OutputWindowPanes.Item("Package Manager") 95 | 96 | $selection = $pmPane.TextDocument.Selection 97 | $selection.StartOfDocument($false) 98 | $selection.EndOfDocument($true) 99 | 100 | if ($selection.Text.StartsWith("Attempting to gather dependencies information for package 'Newtonsoft.Json." + $package.Version + "'")) 101 | { 102 | # don't show on upgrade 103 | if (!$selection.Text.Contains("Removed package")) 104 | { 105 | $dte2.ItemOperations.Navigate($url) | Out-Null 106 | } 107 | } 108 | } 109 | catch 110 | { 111 | # stop potential errors from bubbling up 112 | # worst case the splash page won't open 113 | } 114 | } 115 | 116 | # still yolo -------------------------------------------------------------------------------- /scripts/build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set scriptsdir=%~dp0 4 | set root=%scriptsdir%\.. 5 | set project=%1 6 | set version=%2 7 | 8 | if "%project%"=="" ( 9 | echo Please invoke the build script with a project name as its first argument. 10 | echo. 11 | goto exit_fail 12 | ) 13 | 14 | if "%version%"=="" ( 15 | echo Please invoke the build script with a version as its second argument. 16 | echo. 17 | goto exit_fail 18 | ) 19 | 20 | set Version=%version% 21 | 22 | pushd %root% 23 | 24 | dotnet restore 25 | if %ERRORLEVEL% neq 0 ( 26 | popd 27 | goto exit_fail 28 | ) 29 | 30 | dotnet build "%root%\%project%" -c Release 31 | if %ERRORLEVEL% neq 0 ( 32 | popd 33 | goto exit_fail 34 | ) 35 | 36 | popd 37 | 38 | 39 | 40 | 41 | 42 | 43 | goto exit_success 44 | :exit_fail 45 | exit /b 1 46 | :exit_success -------------------------------------------------------------------------------- /scripts/push.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set version=%1 4 | 5 | if "%version%"=="" ( 6 | echo Please remember to specify which version to push as an argument. 7 | goto exit_fail 8 | ) 9 | 10 | set reporoot=%~dp0\.. 11 | set destination=%reporoot%\deploy 12 | 13 | if not exist "%destination%" ( 14 | echo Could not find %destination% 15 | echo. 16 | echo Did you remember to build the packages before running this script? 17 | ) 18 | 19 | set nuget=%reporoot%\tools\NuGet\NuGet.exe 20 | 21 | if not exist "%nuget%" ( 22 | echo Could not find NuGet here: 23 | echo. 24 | echo "%nuget%" 25 | echo. 26 | goto exit_fail 27 | ) 28 | 29 | 30 | "%nuget%" push "%destination%\*.%version%.nupkg" -Source https://nuget.org 31 | if %ERRORLEVEL% neq 0 ( 32 | echo NuGet push failed. 33 | goto exit_fail 34 | ) 35 | 36 | 37 | 38 | 39 | 40 | 41 | goto exit_success 42 | :exit_fail 43 | exit /b 1 44 | :exit_success 45 | -------------------------------------------------------------------------------- /scripts/release.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set scriptsdir=%~dp0 4 | set root=%scriptsdir%\.. 5 | set deploydir=%root%\deploy 6 | set project=%1 7 | set version=%2 8 | 9 | if "%project%"=="" ( 10 | echo Please invoke the build script with a project name as its first argument. 11 | echo. 12 | goto exit_fail 13 | ) 14 | 15 | if "%version%"=="" ( 16 | echo Please invoke the build script with a version as its second argument. 17 | echo. 18 | goto exit_fail 19 | ) 20 | 21 | set Version=%version% 22 | 23 | if exist "%deploydir%" ( 24 | rd "%deploydir%" /s/q 25 | ) 26 | 27 | pushd %root% 28 | 29 | dotnet restore 30 | if %ERRORLEVEL% neq 0 ( 31 | popd 32 | goto exit_fail 33 | ) 34 | 35 | dotnet pack "%root%/%project%" -c Release -o "%deploydir%" /p:PackageVersion=%version% 36 | if %ERRORLEVEL% neq 0 ( 37 | popd 38 | goto exit_fail 39 | ) 40 | 41 | call scripts\push.cmd "%version%" 42 | 43 | popd 44 | 45 | 46 | 47 | 48 | 49 | 50 | goto exit_success 51 | :exit_fail 52 | exit /b 1 53 | :exit_success -------------------------------------------------------------------------------- /tools/nuget/nuget.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rebus-org/Tababular/9074afe32917ac17bee9506b6887b13e1fe0c392/tools/nuget/nuget.exe --------------------------------------------------------------------------------