Compare commits

...

7 Commits

Author SHA1 Message Date
Patrik Svensson
3c3afe7439 Add support for rendering exceptions 2020-10-05 07:03:02 +02:00
Gary Ewan Park
971f9032ba (doc) Dynamically generate sections
Rather than hard-coding them.  Then, as the number of sections increase,
no additional changes will be required here.
2020-10-03 16:30:21 +02:00
Gary Ewan Park
5149557560 (doc) Remove hard-coded link
Rather than specifying spectre.console directly in the layout, figure out
the URL using GetLink method.
2020-10-03 16:30:21 +02:00
Khalid Abuhakmeh
e0947708c9 Generalized Search
Generalized the search to work with any table
with some basic classes and some updated
JavaScript, we can now search colors and emojis.
2020-10-02 19:39:02 +02:00
Patrik Svensson
b1db8a9403 Clean up border code
Removed caching that really didn't do anything anymore.
2020-10-01 23:05:02 +02:00
Khalid Abuhakmeh
a2f8652575 Add Search To Emoji Page
This adds a typeahead search feature
for the very large emoji table. It filters as you type to find if an emoji exists or not.

The JavaScript could be adapted to work on all tables in the future.
2020-10-01 22:00:39 +02:00
Khalid Abuhakmeh
672faa131f Update Docs Readme
Added some more context to the docs readme for
folks looking to contribute. Also added the license
so its clear what the license of the docs site is. As
a small tweak I removed a duplicate string value and
use the Constant.
2020-10-01 20:51:32 +02:00
73 changed files with 1112 additions and 234 deletions

View File

@@ -70,6 +70,7 @@ jobs:
dotnet example panels
dotnet example colors
dotnet example emojis
dotnet example exceptions
- name: Build
shell: bash

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@
[Pp]ackages/
/.artifacts/
/[Tt]ools/
.idea
.DS_Store
# Cakeup

View File

@@ -1,8 +1,50 @@
# Documentation
Preview the documentation locally by running the following
from your favourite shell:
To start contributing to the [Spectre.Console](https://github.com/spectresystems/spectre.console) documentation, you will need the [.NET Core SDK](https://dot.net) 3.1 or higher.
## Running Preview Site
The documentation site uses [Statiq](https://statiq.dev), a static site generator. To build the documentation site run the following in a command-line terminal.
```
> dotnet run -- preview --virtual-dir "spectre.console"
```
> dotnet run preview --virtual-dir "spectre.console"
```
After the build is complete, you can navigate to [http://localhost:5080/spectre.consle](http://localhost:5080/spectre.console).
**Note that the site runs under a virtual directory.**
## Editing Content
The documentation is written using [Markdown](https://www.markdownguide.org/basic-syntax/).
Markdown files can be found under the following directories:
- [/input](./input)
- [/appendix](./input/appendix)
## Editing Layout
Layout and styling can also be found in the [input](./input) directory. Look for Sass, Css, and Images under the [assets](./input/assets) directory.
## Custom Build Features
The documentation site has custom enhancements to Statiq located under the [./src](./src) directory. Enhancements to the build process include:
- [Extension Methods](./src/Extensions)
- [Models](./src/Models)
- [Pipelines](./src/Pipelines)
- [Shortcodes](./src/Shortcodes)
- [Utilities](./src/Utilities)
## License
MIT License
Copyright (c) 2020 Spectre Systems AB
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -5,8 +5,8 @@
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/spectre.console/assets/bootstrap/bootstrap.css" rel="stylesheet" />
<link href="/spectre.console/assets/css/styles.css" rel="stylesheet" />
<link href="@Context.GetLink("/assets/bootstrap/bootstrap.css")" rel="stylesheet" />
<link href="@Context.GetLink("/assets/css/styles.css")" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&family=Roboto+Slab:wght@400;700&family=Roboto:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" rel="stylesheet" data-no-mirror>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/themes/prism.css">
@@ -29,7 +29,7 @@
<nav id="topnav" class="navbar navbar-expand-lg navbar-light">
<div class="container py-3">
<a class="navbar-brand" href="/spectre.console"><img id="logo" src="/spectre.console/assets/logo.svg" alt="Spectre.Console"> Spectre.Console</a>
<a class="navbar-brand" href="@Context.GetLink("/")"><img id="logo" src="@Context.GetLink("/assets/logo.svg")" alt="Spectre.Console"> Spectre.Console</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarText" aria-controls="navbarText" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
@@ -135,7 +135,7 @@
else
{
@root.GetTitle()
}
}
</div>
@foreach (IDocument document in OutputPages.GetChildrenOf(root).OnlyVisible())
@@ -149,7 +149,7 @@
else
{
@document.GetTitle()
}
}
</div>
@if (documentChildren.OnlyVisible().Any())

View File

@@ -13,4 +13,19 @@ in markup text such as `AnsiConsole.Markup("[maroon on blue]Hello[/]")`.
# Standard colors
<?# ColorTable /?>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">
<i class="fas fa-search" aria-hidden="true"></i>
</span>
</div>
<input
class="form-control w-100 filter"
data-table="color-results"
type="text" placeholder="Search Colors..." autocomplete="off"
aria-label="Search Colors">
</div>
<?# ColorTable /?>
<script type="text/javascript" src="../assets/js/table-search.js"></script>

View File

@@ -35,4 +35,19 @@ var rendered = Emoji.Replace(phrase);
_The images in the table below might not render correctly in your
browser for the same reasons mentioned in the `Compatibility` section._
<?# EmojiTable /?>
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">
<i class="fas fa-search" aria-hidden="true"></i>
</span>
</div>
<input
class="form-control w-100 filter"
data-table="emoji-results"
type="text" placeholder="Search Emojis..." autocomplete="off"
aria-label="Search Emojis">
</div>
<?# EmojiTable /?>
<script type="text/javascript" src="../assets/js/table-search.js"></script>

View File

@@ -0,0 +1,12 @@
Title: Appendix
Order: 10
---
<h1>Sections</h1>
<ul>
@foreach (IDocument child in OutputPages.GetChildrenOf(Document))
{
<li>@Html.DocumentLink(child)</li>
}
</ul>

View File

@@ -1,10 +0,0 @@
Title: Appendix
Order: 10
---
# Sections
* [Styles](xref:styles)
* [Colors](xref:colors)
* [Borders](xref:borders)
* [Emojis](xref:emojis)

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

View File

@@ -0,0 +1,35 @@
$(document).ready(function () {
$('.filter').each(function () {
let input = this;
let table = document.getElementById(input.dataset.table);
input.onkeyup = function (event) {
if (event.key === "Enter") {
event.preventDefault();
event.stopPropagation();
return false;
}
let value = input.value.toUpperCase();
let rows = table.getElementsByClassName('search-row');
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
let match =
new RegExp(value, "i").test(row.textContent) ||
value === '';
if (match) {
row.style.display = 'table-row';
} else {
row.style.display = 'none';
}
}
}; // keyup
})
}); // ready

26
docs/input/exceptions.md Normal file
View File

@@ -0,0 +1,26 @@
Title: Exceptions
Order: 3
---
Exceptions isn't always readable when viewed in the terminal.
You can make exception a bit more readable by using the `WriteException` method.
```csharp
AnsiConsole.WriteException(ex);
```
<img src="assets/images/exception.png" style="max-width: 100%; margin-bottom: 20px">
You can also shorten specific parts of the exception to make it even
more readable, and make paths clickable hyperlinks. Whether or not
the hyperlinks are clickable is up to the terminal.
```csharp
AnsiConsole.WriteException(ex,
ExceptionFormat.ShortenPaths | ExceptionFormat.ShortenTypes |
ExceptionFormat.ShortenMethods | ExceptionFormat.ShowLinks);
```
<img src="assets/images/compact_exception.png" style="max-width: 100%;">

View File

@@ -65,6 +65,15 @@ For a list of emoji, see the [Emojis](xref:styles) appendix section.
# Colors
In the examples above, all colors was referenced by their name,
but you can also use the hex or rgb representation for colors in markdown.
```csharp
AnsiConsole.Markup("[red]Foo[/] ");
AnsiConsole.Markup("[#ff0000]Bar[/] ");
AnsiConsole.Markup("[rgb(255,0,0)]Baz[/] ");
```
For a list of colors, see the [Colors](xref:colors) appendix section.
# Styles

View File

@@ -20,10 +20,7 @@ namespace Docs
public static Bootstrapper ConfigureDeployment(this Bootstrapper bootstrapper, string deployBranch)
{
if (bootstrapper != null)
{
bootstrapper.AddSetting(Constants.Deployment.TargetBranch, deployBranch);
}
bootstrapper?.AddSetting(Constants.Deployment.TargetBranch, deployBranch);
return bootstrapper;
}
}

View File

@@ -9,15 +9,13 @@ namespace Docs.Pipelines
{
public class ColorsPipeline : Pipeline
{
public const string Url = "https://raw.githubusercontent.com/spectresystems/spectre.console/main/resources/scripts/Generator/Data/colors.json";
public ColorsPipeline()
{
InputModules = new ModuleList
{
new ExecuteConfig(
Config.FromContext(ctx => {
return new ReadWeb(Url);
return new ReadWeb(Constants.Colors.Url);
}))
};
@@ -32,4 +30,4 @@ namespace Docs.Pipelines
};
}
}
}
}

View File

@@ -21,7 +21,7 @@ namespace Docs.Shortcodes
.First().Object;
// Headers
var table = new XElement("table", new XAttribute("class", "table"));
var table = new XElement("table", new XAttribute("class", "table"), new XAttribute("id", "color-results"));
var header = new XElement("tr", new XAttribute("class", "color-row"));
header.Add(new XElement("th", ""));
header.Add(new XElement("th", "#"));
@@ -33,8 +33,8 @@ namespace Docs.Shortcodes
foreach (var color in colors)
{
var rep = new XElement("td",
new XElement("span",
var rep = new XElement("td",
new XElement("span",
new XAttribute("class", "color-representation"),
new XAttribute("style", $"background-color:{color.Hex};")));
var name = new XElement("td", new XElement("code", color.Number.ToString()));
@@ -44,7 +44,7 @@ namespace Docs.Shortcodes
var clr = new XElement("td", new XElement("code", color.ClrName));
// Create row
var row = new XElement("tr");
var row = new XElement("tr", new XAttribute("class", "search-row"));
row.Add(rep);
row.Add(name);
row.Add(number);

View File

@@ -18,8 +18,8 @@ namespace Docs.Shortcodes
.First().Object;
// Headers
var table = new XElement("table", new XAttribute("class", "table"));
var header = new XElement("tr", new XAttribute("class", "emoji-row"));
var table = new XElement("table", new XAttribute("class", "table"), new XAttribute("id", "emoji-results"));
var header = new XElement("tr", new XAttribute("class", "emoji-row-header"));
header.Add(new XElement("th", ""));
header.Add(new XElement("th", "Markup"));
header.Add(new XElement("th", "Constant"));
@@ -28,9 +28,9 @@ namespace Docs.Shortcodes
foreach (var emoji in emojis)
{
var code = emoji.Code.Replace("U+0000", "U+").Replace("U+000", "U+");
var icon = string.Format("&#x{0};", emoji.Code.Replace("U+", string.Empty));
var icon = $"&#x{emoji.Code.Replace("U+", string.Empty)};";
var row = new XElement("tr");
var row = new XElement("tr", new XAttribute("class", "search-row"));
row.Add(new XElement("td", icon));
row.Add(new XElement("td", new XElement("code", $":{emoji.Id}:")));
row.Add(new XElement("td", new XElement("code", emoji.Name)));

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
<Title>Exceptions</Title>
<Description>Demonstrates how to render formatted exceptions.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,42 @@
using System;
using System.Security.Authentication;
using Spectre.Console;
namespace Exceptions
{
public static class Program
{
public static void Main(string[] args)
{
try
{
DoMagic(42, null);
}
catch (Exception ex)
{
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex);
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks);
}
}
private static void DoMagic(int foo, string[,] bar)
{
try
{
CheckCredentials(foo, bar);
}
catch(Exception ex)
{
throw new InvalidOperationException("Whaaat?", ex);
}
}
private static void CheckCredentials(int qux, string[,] corgi)
{
throw new InvalidCredentialException("The credentials are invalid.");
}
}
}

View File

@@ -24,3 +24,6 @@ dotnet_diagnostic.CA2000.severity = none
# SA1118: Parameter should not span multiple lines
dotnet_diagnostic.SA1118.severity = none
# CA1031: Do not catch general exception types
dotnet_diagnostic.CA1031.severity = none

View File

@@ -0,0 +1,23 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace Spectre.Console.Tests.Data
{
public static class TestExceptions
{
[SuppressMessage("Usage", "CA1801:Review unused parameters", Justification = "<Pending>")]
public static bool MethodThatThrows(int? number) => throw new InvalidOperationException("Throwing!");
public static void ThrowWithInnerException()
{
try
{
MethodThatThrows(null);
}
catch (Exception ex)
{
throw new InvalidOperationException("Something threw!", ex);
}
}
}
}

View File

@@ -1,13 +1,34 @@
using System;
using System.Text.RegularExpressions;
namespace Spectre.Console.Tests
{
public static class StringExtensions
{
private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline);
private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline);
public static string NormalizeLineEndings(this string text)
{
return text?.Replace("\r\n", "\n", StringComparison.OrdinalIgnoreCase)
?.Replace("\r", string.Empty, StringComparison.OrdinalIgnoreCase);
}
public static string NormalizeStackTrace(this string text)
{
text = _lineNumberRegex.Replace(text, match =>
{
return ":nn";
});
return _filenameRegex.Replace(text, match =>
{
var value = match.Value;
var index = value.LastIndexOfAny(new[] { '\\', '/' });
var filename = value.Substring(index + 1, value.Length - index - 1);
return $" in /xyz/{filename}";
});
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Text;
using Spectre.Console.Rendering;
namespace Spectre.Console.Tests.Tools
{
public sealed class MarkupConsoleFixture : IDisposable, IAnsiConsole
{
private readonly StringWriter _writer;
private readonly IAnsiConsole _console;
public string Output => _writer.ToString().TrimEnd('\n');
public Capabilities Capabilities => _console.Capabilities;
public Encoding Encoding => _console.Encoding;
public int Width { get; }
public int Height => _console.Height;
public MarkupConsoleFixture(ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, int width = 80)
{
_writer = new StringWriter();
_console = AnsiConsole.Create(new AnsiConsoleSettings
{
Ansi = ansi,
ColorSystem = (ColorSystemSupport)system,
Out = _writer,
LinkIdentityGenerator = new TestLinkIdentityGenerator(),
});
Width = width;
}
public void Dispose()
{
_writer?.Dispose();
}
public void Write(Segment segment)
{
_console.Write(segment);
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Spectre.Console.Rendering;
@@ -50,5 +51,16 @@ namespace Spectre.Console.Tests
Writer.Write(segment.Text);
}
public string[] WriteExceptionAndGetLines(Exception ex, ExceptionFormats formats = ExceptionFormats.None)
{
this.WriteException(ex, formats);
return Output.NormalizeStackTrace()
.NormalizeLineEndings()
.Split(new char[] { '\n' })
.Select(line => line.TrimEnd())
.ToArray();
}
}
}

View File

@@ -0,0 +1,99 @@
using System;
using Shouldly;
using Spectre.Console.Tests.Data;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class ExceptionTests
{
[Fact]
public void Should_Write_Exception()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.MethodThatThrows(null));
// When
var result = console.WriteExceptionAndGetLines(dex);
// Then
result.Length.ShouldBe(4);
result[0].ShouldBe("System.InvalidOperationException: Throwing!");
result[1].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[2].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception>b__0_0() in /xyz/ExceptionTests.cs:nn");
result[3].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
[Fact]
public void Should_Write_Exception_With_Shortened_Types()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.MethodThatThrows(null));
// When
var result = console.WriteExceptionAndGetLines(dex, ExceptionFormats.ShortenTypes);
// Then
result.Length.ShouldBe(4);
result[0].ShouldBe("InvalidOperationException: Throwing!");
result[1].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[2].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Shortened_Types>b__1_0() in /xyz/ExceptionTests.cs:nn");
result[3].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
[Fact]
public void Should_Write_Exception_With_Shortened_Methods()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.MethodThatThrows(null));
// When
var result = console.WriteExceptionAndGetLines(dex, ExceptionFormats.ShortenMethods);
// Then
result.Length.ShouldBe(4);
result[0].ShouldBe("System.InvalidOperationException: Throwing!");
result[1].ShouldBe(" at MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[2].ShouldBe(" at <Should_Write_Exception_With_Shortened_Methods>b__2_0() in /xyz/ExceptionTests.cs:nn");
result[3].ShouldBe(" at GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
[Fact]
public void Should_Write_Exception_With_Inner_Exception()
{
// Given
var console = new PlainConsole(width: 1024);
var dex = GetException(() => TestExceptions.ThrowWithInnerException());
// When
var result = console.WriteExceptionAndGetLines(dex);
// Then
result.Length.ShouldBe(7);
result[0].ShouldBe("System.InvalidOperationException: Something threw!");
result[1].ShouldBe(" System.InvalidOperationException: Throwing!");
result[2].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.MethodThatThrows(Nullable`1 number) in /xyz/Exceptions.cs:nn");
result[3].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn");
result[4].ShouldBe(" at Spectre.Console.Tests.Data.TestExceptions.ThrowWithInnerException() in /xyz/Exceptions.cs:nn");
result[5].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.<>c.<Should_Write_Exception_With_Inner_Exception>b__3_0() in /xyz/ExceptionTests.cs:nn");
result[6].ShouldBe(" at Spectre.Console.Tests.Unit.ExceptionTests.GetException(Action action) in /xyz/ExceptionTests.cs:nn");
}
public static Exception GetException(Action action)
{
try
{
action?.Invoke();
}
catch (Exception e)
{
return e;
}
throw new InvalidOperationException("Exception harness failed");
}
}
}

View File

@@ -1,4 +1,6 @@
using System.Collections.Generic;
using Shouldly;
using Spectre.Console.Rendering;
using Xunit;
namespace Spectre.Console.Tests.Unit
@@ -298,5 +300,33 @@ namespace Spectre.Console.Tests.Unit
console.Lines[3].ShouldBe("│ └─────────────┘ │");
console.Lines[4].ShouldBe("└─────────────────┘");
}
[Fact]
public void Should_Wrap_Content_Correctly()
{
// Given
var console = new PlainConsole(width: 84);
var rows = new List<IRenderable>();
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0));
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
grid.AddRow("at", "[grey]System.Runtime.CompilerServices.TaskAwaiter.[/][yellow]HandleNonSuccessAndDebuggerNotification[/]([blue]Task[/] task)");
rows.Add(grid);
var panel = new Panel(grid)
.Expand().RoundedBorder()
.SetBorderStyle(Style.WithForeground(Color.Grey))
.SetHeader("Short paths ", Style.WithForeground(Color.Grey));
// When
console.Render(panel);
// Then
console.Lines.Count.ShouldBe(4);
console.Lines[0].ShouldBe("╭─Short paths ─────────────────────────────────────────────────────────────────────╮");
console.Lines[1].ShouldBe("│ at System.Runtime.CompilerServices.TaskAwaiter. │");
console.Lines[2].ShouldBe("│ HandleNonSuccessAndDebuggerNotification(Task task) │");
console.Lines[3].ShouldBe("╰──────────────────────────────────────────────────────────────────────────────────╯");
}
}
}

View File

@@ -35,6 +35,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Links", "..\examples\Links\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emojis", "..\examples\Emojis\Emojis.csproj", "{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptions", "..\examples\Exceptions\Exceptions.csproj", "{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D}"
ProjectSection(SolutionItems) = preProject
..\.github\workflows\ci.yaml = ..\.github\workflows\ci.yaml
..\.github\workflows\docs.yaml = ..\.github\workflows\docs.yaml
..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -177,6 +186,18 @@ Global
{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2}.Release|x64.Build.0 = Release|Any CPU
{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2}.Release|x86.ActiveCfg = Release|Any CPU
{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2}.Release|x86.Build.0 = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Debug|x64.ActiveCfg = Debug|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Debug|x64.Build.0 = Debug|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Debug|x86.ActiveCfg = Debug|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Debug|x86.Build.0 = Debug|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|Any CPU.Build.0 = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x64.ActiveCfg = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x64.Build.0 = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x86.ActiveCfg = Release|Any CPU
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -191,6 +212,8 @@ Global
{094245E6-4C94-485D-B5AC-3153E878B112} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{6AF8C93B-AA41-4F44-8B1B-B8D166576174} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
{C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D} = {20595AD4-8D75-4AF8-B6BC-9C38C160423F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}

View File

@@ -0,0 +1,20 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// A console capable of writing ANSI escape sequences.
/// </summary>
public static partial class AnsiConsole
{
/// <summary>
/// Writes an exception to the console.
/// </summary>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="format">The exception format options.</param>
public static void WriteException(Exception exception, ExceptionFormats format = ExceptionFormats.None)
{
Console.WriteException(exception, format);
}
}
}

View File

@@ -1,7 +1,3 @@
using System;
using System.IO;
using Spectre.Console.Internal;
namespace Spectre.Console
{
/// <summary>
@@ -9,9 +5,6 @@ namespace Spectre.Console
/// </summary>
public static partial class AnsiConsole
{
private static ConsoleColor _defaultForeground;
private static ConsoleColor _defaultBackground;
internal static Style CurrentStyle { get; private set; } = Style.Plain;
internal static bool Created { get; private set; }
@@ -42,20 +35,6 @@ namespace Spectre.Console
set => CurrentStyle = CurrentStyle.WithDecoration(value);
}
internal static void Initialize(TextWriter? @out)
{
if (@out?.IsStandardOut() ?? false)
{
Foreground = _defaultForeground = System.Console.ForegroundColor;
Background = _defaultBackground = System.Console.BackgroundColor;
}
else
{
Foreground = _defaultForeground = Color.Silver;
Background = _defaultBackground = Color.Black;
}
}
/// <summary>
/// Resets colors and text decorations.
/// </summary>
@@ -78,8 +57,7 @@ namespace Spectre.Console
/// </summary>
public static void ResetColors()
{
Foreground = _defaultForeground;
Background = _defaultBackground;
CurrentStyle = Style.Plain;
}
}
}

View File

@@ -16,7 +16,6 @@ namespace Spectre.Console
ColorSystem = ColorSystemSupport.Detect,
Out = System.Console.Out,
});
Initialize(System.Console.Out);
Created = true;
return console;
});

View File

@@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Spectre.Console.Rendering;
namespace Spectre.Console
@@ -11,70 +7,16 @@ namespace Spectre.Console
/// </summary>
public abstract partial class BoxBorder
{
private readonly Dictionary<BoxBorderPart, string> _lookup;
/// <summary>
/// Gets the safe border for this border or <c>null</c> if none exist.
/// </summary>
public virtual BoxBorder? SafeBorder { get; }
/// <summary>
/// Initializes a new instance of the <see cref="BoxBorder"/> class.
/// </summary>
protected BoxBorder()
{
_lookup = Initialize();
}
private Dictionary<BoxBorderPart, string> Initialize()
{
var lookup = new Dictionary<BoxBorderPart, string>();
foreach (BoxBorderPart? part in Enum.GetValues(typeof(BoxBorderPart)))
{
if (part == null)
{
continue;
}
var text = GetBorderPart(part.Value);
if (text.Length > 1)
{
throw new InvalidOperationException("A box part cannot contain more than one character.");
}
lookup.Add(part.Value, GetBorderPart(part.Value));
}
return lookup;
}
/// <summary>
/// Gets the string representation of a specific border part.
/// </summary>
/// <param name="part">The part to get a string representation for.</param>
/// <param name="count">The number of repetitions.</param>
/// <returns>A string representation of the specified border part.</returns>
public string GetPart(BoxBorderPart part, int count)
{
// TODO: This need some optimization...
return string.Join(string.Empty, Enumerable.Repeat(GetBorderPart(part)[0], count));
}
/// <summary>
/// Gets the string representation of a specific border part.
/// </summary>
/// <param name="part">The part to get a string representation for.</param>
/// <returns>A string representation of the specified border part.</returns>
public string GetPart(BoxBorderPart part)
{
return _lookup[part].ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// Gets the character representing the specified border part.
/// Gets the string representation of the specified border part.
/// </summary>
/// <param name="part">The part to get the character representation for.</param>
/// <returns>A character representation of the specified border part.</returns>
protected abstract string GetBorderPart(BoxBorderPart part);
public abstract string GetPart(BoxBorderPart part);
}
}

View File

@@ -245,9 +245,32 @@ namespace Spectre.Console
};
}
/// <summary>
/// Converts the color to a markup string.
/// </summary>
/// <returns>A <see cref="string"/> representing the color as markup.</returns>
public string ToMarkupString()
{
if (Number != null)
{
var name = ColorTable.GetName(Number.Value);
if (!string.IsNullOrWhiteSpace(name))
{
return name;
}
}
return string.Format(CultureInfo.InvariantCulture, "#{0:X2}{1:X2}{2:X2}", R, G, B);
}
/// <inheritdoc/>
public override string ToString()
{
if (IsDefault)
{
return "default";
}
if (Number != null)
{
var name = ColorTable.GetName(Number.Value);

View File

@@ -16,7 +16,17 @@ namespace Spectre.Console
/// <returns>A string with emoji codes replaced with actual emoji.</returns>
public static string Replace(string value)
{
static string ReplaceEmoji(Match match) => _emojis[match.Groups[2].Value];
static string ReplaceEmoji(Match match)
{
var key = match.Groups[2].Value;
if (_emojis.TryGetValue(key, out var emoji))
{
return emoji;
}
return match.Value;
}
return _emojiCode.Replace(value, ReplaceEmoji);
}
}

View File

@@ -0,0 +1,41 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents how an exception is formatted.
/// </summary>
[Flags]
public enum ExceptionFormats
{
/// <summary>
/// The default formatting.
/// </summary>
None = 0,
/// <summary>
/// Whether or not paths should be shortened.
/// </summary>
ShortenPaths = 1,
/// <summary>
/// Whether or not types should be shortened.
/// </summary>
ShortenTypes = 2,
/// <summary>
/// Whether or not methods should be shortened.
/// </summary>
ShortenMethods = 4,
/// <summary>
/// Whether or not to show paths as links in the terminal.
/// </summary>
ShowLinks = 8,
/// <summary>
/// Shortens everything that can be shortened.
/// </summary>
ShortenEverything = ShortenMethods | ShortenTypes | ShortenPaths,
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAnsiConsole"/>.
/// </summary>
public static partial class AnsiConsoleExtensions
{
/// <summary>
/// Writes an exception to the console.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="format">The exception format options.</param>
public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionFormats format = ExceptionFormats.None)
{
Render(console, exception.GetRenderable(format));
}
}
}

View File

@@ -27,10 +27,9 @@ namespace Spectre.Console
}
var options = new RenderContext(console.Encoding, console.Capabilities.LegacyConsole);
var segments = renderable.Render(options, console.Width).Where(x => !(x.Text.Length == 0 && !x.IsLineBreak)).ToArray();
var segments = renderable.Render(options, console.Width).ToArray();
segments = Segment.Merge(segments).ToArray();
var current = Style.Plain;
foreach (var segment in segments)
{
if (string.IsNullOrEmpty(segment.Text))

View File

@@ -0,0 +1,22 @@
using System;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Exception"/>.
/// </summary>
public static class ExceptionExtensions
{
/// <summary>
/// Gets a <see cref="IRenderable"/> representation of the exception.
/// </summary>
/// <param name="exception">The exception to format.</param>
/// <param name="format">The exception format options.</param>
/// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
public static IRenderable GetRenderable(this Exception exception, ExceptionFormats format = ExceptionFormats.None)
{
return ExceptionFormatter.Format(exception, format);
}
}
}

View File

@@ -0,0 +1,26 @@
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="string"/>.
/// </summary>
public static class StringExtensions
{
/// <summary>
/// Converts the string to something that is safe to
/// use in a markup string.
/// </summary>
/// <param name="text">The text to convert.</param>
/// <returns>A string that is safe to use in a markup string.</returns>
public static string SafeMarkup(this string text)
{
if (text == null)
{
return string.Empty;
}
return text
.Replace("[", "[[")
.Replace("]", "]]");
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal static class ExceptionFormatter
{
private static readonly Color _typeColor = Color.White;
private static readonly Color _methodColor = Color.Yellow;
private static readonly Color _parameterColor = Color.Blue;
private static readonly Color _pathColor = Color.Yellow;
private static readonly Color _dimmedColor = Color.Grey;
public static IRenderable Format(Exception exception, ExceptionFormats format)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var info = ExceptionParser.Parse(exception.ToString());
if (info == null)
{
return new Text(exception.ToString());
}
return GetException(info, format);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionFormats format)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
return new Rows(new IRenderable[]
{
GetMessage(info, format),
GetStackFrames(info, format),
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionFormats format)
{
var shortenTypes = (format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, _typeColor.ToMarkupString(), shortenTypes);
var message = $"[b red]{ex.Message.SafeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionFormats format)
{
var grid = new Grid();
grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0).NoWrap());
grid.AddColumn(new GridColumn().PadLeft(1).PadRight(0));
// Inner
if (ex.Inner != null)
{
grid.AddRow(
Text.Empty,
GetException(ex.Inner, format));
}
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
// Method
var shortenMethods = (format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, _methodColor.ToMarkupString(), shortenMethods));
builder.Append('(');
builder.Append(string.Join(", ", frame.Parameters.Select(x => $"[{_parameterColor.ToMarkupString()}]{x.Type.SafeMarkup()}[/] {x.Name}")));
builder.Append(')');
if (frame.Path != null)
{
builder.Append(" [").Append(_dimmedColor.ToMarkupString()).Append("]in[/] ");
// Path
AppendPath(builder, frame, format);
// Line number
if (frame.LineNumber != null)
{
builder.Append(':');
builder.Append('[').Append(_parameterColor.ToMarkupString()).Append(']').Append(frame.LineNumber).Append("[/]");
}
}
grid.AddRow($"[{_dimmedColor.ToMarkupString()}]at[/]", builder.ToString());
}
return grid;
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionFormats format)
{
if (frame?.Path is null)
{
return;
}
void RenderLink()
{
var shortenPaths = (format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, $"b {_pathColor.ToMarkupString()}", shortenPaths));
}
if ((format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
RenderLink();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
RenderLink();
}
}
private static string Emphasize(string input, char[] separators, string color, bool compact)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.Append("[silver]").Append(type, 0, index + 1).Append("[/]");
}
builder.Append('[').Append(color).Append(']').Append(type, index + 1, type.Length - index - 1).Append("[/]");
}
else
{
builder.Append(type);
}
return builder.ToString();
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal sealed class ExceptionInfo
{
public string Type { get; }
public string Message { get; }
public List<StackFrameInfo> Frames { get; }
public ExceptionInfo? Inner { get; }
public ExceptionInfo(
string type, string message,
List<StackFrameInfo> frames,
ExceptionInfo? inner)
{
Type = type ?? string.Empty;
Message = message ?? string.Empty;
Frames = frames ?? new List<StackFrameInfo>();
Inner = inner;
}
}
}

View File

@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
namespace Spectre.Console.Internal
{
internal static class ExceptionParser
{
private static readonly Regex _messageRegex = new Regex(@"^(?'type'.*):\s(?'message'.*)$");
private static readonly Regex _stackFrameRegex = new Regex(@"^\s*\w*\s(?'method'.*)\((?'params'.*)\)");
private static readonly Regex _fullStackFrameRegex = new Regex(@"^\s*(?'at'\w*)\s(?'method'.*)\((?'params'.*)\)\s(?'in'\w*)\s(?'path'.*)\:(?'line'\w*)\s(?'linenumber'\d*)$");
public static ExceptionInfo? Parse(string exception)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
var lines = exception.SplitLines();
return Parse(new Queue<string>(lines));
}
private static ExceptionInfo? Parse(Queue<string> lines)
{
if (lines.Count == 0)
{
// Error: No lines to parse
return null;
}
var line = lines.Dequeue();
line = line.Replace(" ---> ", string.Empty);
var match = _messageRegex.Match(line);
if (!match.Success)
{
return null;
}
var inner = (ExceptionInfo?)null;
// Stack frames
var frames = new List<StackFrameInfo>();
while (lines.Count > 0)
{
if (lines.Peek().TrimStart().StartsWith("---> ", StringComparison.OrdinalIgnoreCase))
{
inner = Parse(lines);
if (inner == null)
{
// Error: Could not parse inner exception
return null;
}
continue;
}
line = lines.Dequeue();
if (string.IsNullOrWhiteSpace(line))
{
// Empty line
continue;
}
if (line.TrimStart().StartsWith("--- ", StringComparison.OrdinalIgnoreCase))
{
// End of inner exception
break;
}
var stackFrame = ParseStackFrame(line);
if (stackFrame == null)
{
// Error: Could not parse stack frame
return null;
}
frames.Add(stackFrame);
}
return new ExceptionInfo(
match.Groups["type"].Value,
match.Groups["message"].Value,
frames, inner);
}
private static StackFrameInfo? ParseStackFrame(string frame)
{
var match = _fullStackFrameRegex.Match(frame);
if (match?.Success != true)
{
match = _stackFrameRegex.Match(frame);
if (match?.Success != true)
{
return null;
}
}
var parameters = ParseMethodParameters(match.Groups["params"].Value);
if (parameters == null)
{
// Error: Could not parse parameters
return null;
}
var method = match.Groups["method"].Value;
var path = match.Groups["path"].Success ? match.Groups["path"].Value : null;
var lineNumber = (int?)null;
if (!string.IsNullOrWhiteSpace(match.Groups["linenumber"].Value))
{
lineNumber = int.Parse(match.Groups["linenumber"].Value, CultureInfo.InvariantCulture);
}
return new StackFrameInfo(method, parameters, path, lineNumber);
}
private static List<(string Type, string Name)>? ParseMethodParameters(string parameters)
{
var result = new List<(string Type, string Name)>();
foreach (var parameterPart in parameters.Split(new[] { ", " }, StringSplitOptions.RemoveEmptyEntries))
{
var parameterNameIndex = parameterPart.LastIndexOf(' ');
if (parameterNameIndex == -1)
{
// Error: Could not parse parameter
return null;
}
var type = parameterPart.Substring(0, parameterNameIndex);
var name = parameterPart.Substring(parameterNameIndex + 1, parameterPart.Length - parameterNameIndex - 1);
result.Add((type, name));
}
return result;
}
}
}

View File

@@ -81,7 +81,7 @@ namespace Spectre.Console.Internal
return result.ToArray();
}
public static string Multiply(this string text, int count)
public static string Repeat(this string text, int count)
{
if (text is null)
{

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
namespace Spectre.Console.Internal
{
internal sealed class StackFrameInfo
{
public string Method { get; }
public List<(string Type, string Name)> Parameters { get; }
public string? Path { get; }
public int? LineNumber { get; }
public StackFrameInfo(
string method, List<(string Type, string Name)> parameters,
string? path, int? lineNumber)
{
Method = method ?? throw new System.ArgumentNullException(nameof(method));
Parameters = parameters ?? throw new System.ArgumentNullException(nameof(parameters));
Path = path;
LineNumber = lineNumber;
}
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
public bool TryGetUri([NotNullWhen(true)] out Uri? result)
{
try
{
if (Path == null)
{
result = null;
return false;
}
if (!Uri.TryCreate(Path, UriKind.Absolute, out var uri))
{
result = null;
return false;
}
if (uri.Scheme == "file")
{
// For local files, we need to append
// the host name. Otherwise the terminal
// will most probably not allow it.
var builder = new UriBuilder(uri)
{
Host = Dns.GetHostName(),
};
uri = builder.Uri;
}
result = uri;
return true;
}
catch
{
result = null;
return false;
}
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class AsciiBoxBorder : BoxBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(BoxBorderPart part)
public override string GetPart(BoxBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class DoubleBoxBorder : BoxBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(BoxBorderPart part)
public override string GetPart(BoxBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override BoxBorder? SafeBorder => BoxBorder.Square;
/// <inheritdoc/>
protected override string GetBorderPart(BoxBorderPart part)
public override string GetPart(BoxBorderPart part)
{
return part switch
{

View File

@@ -6,7 +6,7 @@ namespace Spectre.Console.Rendering
public sealed class NoBoxBorder : BoxBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(BoxBorderPart part)
public override string GetPart(BoxBorderPart part)
{
return " ";
}

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override BoxBorder? SafeBorder => BoxBorder.Square;
/// <inheritdoc/>
protected override string GetBorderPart(BoxBorderPart part)
public override string GetPart(BoxBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class SquareBoxBorder : BoxBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(BoxBorderPart part)
public override string GetPart(BoxBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class Ascii2TableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class AsciiDoubleHeadTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class AsciiTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class DoubleEdgeTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class DoubleTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override TableBorder? SafeBorder => TableBorder.Square;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override TableBorder? SafeBorder => TableBorder.Square;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override TableBorder? SafeBorder => TableBorder.Square;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class HorizontalTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public sealed class MarkdownTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{
@@ -57,39 +57,39 @@ namespace Spectre.Console.Rendering
if (padding.Left > 0)
{
// Left padding
builder.Append(" ".Multiply(padding.Left));
builder.Append(" ".Repeat(padding.Left));
}
var justification = columns[columnIndex].Alignment;
if (justification == null)
{
// No alignment
builder.Append(center.Multiply(columnWidth));
builder.Append(center.Repeat(columnWidth));
}
else if (justification.Value == Justify.Left)
{
// Left
builder.Append(':');
builder.Append(center.Multiply(columnWidth - 1));
builder.Append(center.Repeat(columnWidth - 1));
}
else if (justification.Value == Justify.Center)
{
// Centered
builder.Append(':');
builder.Append(center.Multiply(columnWidth - 2));
builder.Append(center.Repeat(columnWidth - 2));
builder.Append(':');
}
else if (justification.Value == Justify.Right)
{
// Right
builder.Append(center.Multiply(columnWidth - 1));
builder.Append(center.Repeat(columnWidth - 1));
builder.Append(':');
}
// Right padding
if (padding.Right > 0)
{
builder.Append(" ".Multiply(padding.Right));
builder.Append(" ".Repeat(padding.Right));
}
if (!lastColumn)

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class MinimalDoubleHeadTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override TableBorder? SafeBorder => TableBorder.Minimal;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class MinimalTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -9,7 +9,7 @@ namespace Spectre.Console.Rendering
public override bool Visible => false;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return " ";
}

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override TableBorder? SafeBorder => TableBorder.Square;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -11,7 +11,7 @@ namespace Spectre.Console.Rendering
public override TableBorder? SafeBorder => TableBorder.Simple;
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class SimpleTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -8,7 +8,7 @@ namespace Spectre.Console.Rendering
public sealed class SquareTableBorder : TableBorder
{
/// <inheritdoc/>
protected override string GetBorderPart(TableBorderPart part)
public override string GetPart(TableBorderPart part)
{
return part switch
{

View File

@@ -239,7 +239,7 @@ namespace Spectre.Console.Rendering
}
// Same style?
if (previous.Style.Equals(segment.Style))
if (previous.Style.Equals(segment.Style) && !previous.IsLineBreak)
{
previous = new Segment(previous.Text + segment.Text, previous.Style);
}
@@ -299,7 +299,15 @@ namespace Spectre.Console.Rendering
while (lengthLeft > 0)
{
var index = totalLength - lengthLeft;
// How many characters should we take?
var take = Math.Min(width, totalLength - index);
if (take == 0)
{
// This shouldn't really occur, but I don't like
// never ending loops if it does...
throw new InvalidOperationException("Text folding failed since 'take' was zero.");
}
result.Add(new Segment(segment.Text.Substring(index, take), segment.Style));
lengthLeft -= take;

View File

@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
@@ -13,8 +11,6 @@ namespace Spectre.Console
/// </summary>
public abstract partial class TableBorder
{
private readonly Dictionary<TableBorderPart, string> _lookup;
/// <summary>
/// Gets a value indicating whether or not the border is visible.
/// </summary>
@@ -26,46 +22,11 @@ namespace Spectre.Console
public virtual TableBorder? SafeBorder { get; }
/// <summary>
/// Initializes a new instance of the <see cref="TableBorder"/> class.
/// Gets the string representation of a specified table border part.
/// </summary>
protected TableBorder()
{
_lookup = Initialize();
}
private Dictionary<TableBorderPart, string> Initialize()
{
var lookup = new Dictionary<TableBorderPart, string>();
foreach (TableBorderPart? part in Enum.GetValues(typeof(TableBorderPart)))
{
if (part == null)
{
continue;
}
var text = GetBorderPart(part.Value);
if (text.Length > 1)
{
throw new InvalidOperationException("A box part cannot contain more than one character.");
}
lookup.Add(part.Value, GetBorderPart(part.Value));
}
return lookup;
}
/// <summary>
/// Gets the string representation of a specific border part.
/// </summary>
/// <param name="part">The part to get a string representation for.</param>
/// <param name="count">The number of repetitions.</param>
/// <returns>A string representation of the specified border part.</returns>
public string GetPart(TableBorderPart part, int count)
{
// TODO: This need some optimization...
return string.Join(string.Empty, Enumerable.Repeat(GetBorderPart(part)[0], count));
}
/// <param name="part">The part to get the character representation for.</param>
/// <returns>A character representation of the specified border part.</returns>
public abstract string GetPart(TableBorderPart part);
/// <summary>
/// Gets a whole column row for the specific column row part.
@@ -95,7 +56,7 @@ namespace Spectre.Console
{
var padding = columns[columnIndex].Padding;
var centerWidth = padding.Left + columnWidth + padding.Right;
builder.Append(center.Multiply(centerWidth));
builder.Append(center.Repeat(centerWidth));
if (!lastColumn)
{
@@ -107,30 +68,12 @@ namespace Spectre.Console
return builder.ToString();
}
/// <summary>
/// Gets the string representation of a specific border part.
/// </summary>
/// <param name="part">The part to get a string representation for.</param>
/// <returns>A string representation of the specified border part.</returns>
public string GetPart(TableBorderPart part)
{
return _lookup[part].ToString(CultureInfo.InvariantCulture);
}
/// <summary>
/// Gets the character representing the specified border part.
/// </summary>
/// <param name="part">The part to get the character representation for.</param>
/// <returns>A character representation of the specified border part.</returns>
protected abstract string GetBorderPart(TableBorderPart part);
/// <summary>
/// Gets the table parts used to render a specific table row.
/// </summary>
/// <param name="part">The table part.</param>
/// <returns>The table parts used to render the specific table row.</returns>
protected (string Left, string Center, string Separator, string Right)
GetTableParts(TablePart part)
protected (string Left, string Center, string Separator, string Right) GetTableParts(TablePart part)
{
return part switch
{

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
@@ -93,8 +94,15 @@ namespace Spectre.Console
// Split the child segments into lines.
var childSegments = ((IRenderable)child).Render(context, childWidth);
foreach (var line in Segment.SplitLines(childSegments, panelWidth))
foreach (var line in Segment.SplitLines(childSegments, childWidth))
{
if (line.Count == 1 && line[0].IsWhiteSpace)
{
// NOTE: This check might impact other things.
// Hopefully not, but there is a chance.
continue;
}
result.Add(new Segment(border.GetPart(BoxBorderPart.Left), borderStyle));
var content = new List<Segment>();
@@ -123,7 +131,7 @@ namespace Spectre.Console
private static void AddBottomBorder(List<Segment> result, BoxBorder border, Style borderStyle, int panelWidth)
{
result.Add(new Segment(border.GetPart(BoxBorderPart.BottomLeft), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom, panelWidth - EdgeWidth), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.Bottom).Repeat(panelWidth - EdgeWidth), borderStyle));
result.Add(new Segment(border.GetPart(BoxBorderPart.BottomRight), borderStyle));
result.Add(Segment.LineBreak);
}
@@ -160,13 +168,13 @@ namespace Spectre.Console
}
}
segments.Add(new Segment(border.GetPart(BoxBorderPart.Top, leftSpacing + 1), borderStyle));
segments.Add(new Segment(border.GetPart(BoxBorderPart.Top).Repeat(leftSpacing + 1), borderStyle));
segments.Add(header);
segments.Add(new Segment(border.GetPart(BoxBorderPart.Top, rightSpacing + 1), borderStyle));
segments.Add(new Segment(border.GetPart(BoxBorderPart.Top).Repeat(rightSpacing + 1), borderStyle));
}
else
{
segments.Add(new Segment(border.GetPart(BoxBorderPart.Top, panelWidth - EdgeWidth), borderStyle));
segments.Add(new Segment(border.GetPart(BoxBorderPart.Top).Repeat(panelWidth - EdgeWidth), borderStyle));
}
segments.Add(new Segment(border.GetPart(BoxBorderPart.TopRight), borderStyle));

View File

@@ -227,17 +227,10 @@ namespace Spectre.Console
throw new InvalidOperationException("Iterator returned empty segment.");
}
if (newLine && current.IsWhiteSpace && !current.IsLineBreak)
{
newLine = false;
continue;
}
newLine = false;
if (current.IsLineBreak)
{
line.Add(current);
lines.Add(line);
line = new SegmentLine();
newLine = true;

View File

@@ -44,22 +44,26 @@ namespace Spectre.Console
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
var result = new List<Segment>();
foreach (var child in _children)
{
var segments = child.Render(context, maxWidth);
foreach (var (_, _, last, segment) in segments.Enumerate())
{
yield return segment;
result.Add(segment);
if (last)
{
if (!segment.IsLineBreak)
{
yield return Segment.LineBreak;
result.Add(Segment.LineBreak);
}
}
}
}
return result;
}
}
}

View File

@@ -298,7 +298,6 @@ namespace Spectre.Console
var widths = width_ranges.Select(range => range.Max).ToList();
var tableWidth = widths.Sum();
if (tableWidth > maxWidth)
{
var wrappable = _columns.Select(c => !c.NoWrap).ToList();