Compare commits

...

14 Commits

Author SHA1 Message Date
Patrik Svensson
7ef1ac483a Fix overflow splitting bug
Closes #93
2020-10-17 12:37:20 +02:00
Takahito Yamatoya
c0875c912a fix incorrect link 2020-10-17 12:16:18 +02:00
Patrik Svensson
3f2ca49071 Add calendar control
Closes #101
2020-10-16 23:02:53 +02:00
Khalid Abuhakmeh
0a0380ae0a Don't throw when console is small
this just returns an empty collection when
take is 0. It leads to some strange output, but
it doesn't blow up.

 #93
2020-10-08 22:17:12 +02:00
Patrik Svensson
ae92c606bb Add support for remapping emojis
Closes #94
2020-10-07 17:33:05 +02:00
Patrik Svensson
68e92f3365 Add various exception improvements 2020-10-07 11:57:28 +02:00
Patrik Svensson
39a8588dc3 Rename ExceptionFormats.None 2020-10-07 11:57:28 +02:00
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
95 changed files with 2393 additions and 240 deletions

View File

@@ -70,6 +70,8 @@ jobs:
dotnet example panels
dotnet example colors
dotnet example emojis
dotnet example exceptions
dotnet example calendars
- 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>

View File

@@ -13,4 +13,19 @@ in markup text such as `AnsiConsole.Markup("[maroon on blue]Hello[/]")`.
# Standard colors
<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

@@ -30,9 +30,43 @@ var phrase = "Mmmm :birthday_cake:";
var rendered = Emoji.Replace(phrase);
```
# Remapping or adding an emoji
Sometimes you want to remap an existing emoji, or
add a completely new one. For this you can use the
`Emoji.Remap` method. This approach works both with
markup strings and `Emoji.Replace`.
```csharp
// Remap the emoji
Emoji.Remap("globe_showing_europe_africa", "😄");
// Render markup
AnsiConsole.MarkupLine("Hello :globe_showing_europe_africa:!");
// Replace emojis in string
var phrase = "Hello :globe_showing_europe_africa:!";
var rendered = Emoji.Replace(phrase);
```
# Emojis
_The images in the table below might not render correctly in your
browser for the same reasons mentioned in the `Compatibility` section._
<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: 159 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

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

@@ -0,0 +1,52 @@
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%;">
## Shortening parts
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%;">
## Customizing exception output
In addition to shorten specific part of the exception, you can
also override the default styling.
```csharp
AnsiConsole.WriteException(ex, new ExceptionSettings
{
Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
Style = new ExceptionStyle
{
Exception = Style.WithForeground(Color.Grey),
Message = Style.WithForeground(Color.White),
NonEmphasized = Style.WithForeground(Color.Cornsilk1),
Parenthesis = Style.WithForeground(Color.Cornsilk1),
Method = Style.WithForeground(Color.Red),
ParameterName = Style.WithForeground(Color.Cornsilk1),
ParameterType = Style.WithForeground(Color.Red),
Path = Style.WithForeground(Color.Red),
LineNumber = Style.WithForeground(Color.Cornsilk1),
}
});
```
<img src="assets/images/custom_exception.png" style="max-width: 100%;">

View File

@@ -43,6 +43,12 @@ AnsiConsole.Markup("[[Hello]] "); // [Hello]
AnsiConsole.Markup("[red][[World]][/]"); // [World]
```
You can also use the `SafeMarkup` extension method.
```csharp
AnsiConsole.Markup("[red]{0}[/]", "Hello [World]".SafeMarkup());
```
# Setting background color
You can set the background color in markup by prefixing the color with
@@ -61,10 +67,19 @@ To output an emoji as part of markup, you can use emoji shortcodes.
AnsiConsole.MarkupLine("Hello :globe_showing_europe_africa:!");
```
For a list of emoji, see the [Emojis](xref:styles) appendix section.
For a list of emoji, see the [Emojis](xref:emojis) 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);
}))
};

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", "#"));
@@ -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>Calendars</Title>
<Description>Demonstrates how to render calendars.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,68 @@
using System.Collections.Generic;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace Calendars
{
public static class Program
{
public static void Main(string[] args)
{
AnsiConsole.WriteLine();
AnsiConsole.Render(
new Columns(GetCalendars())
.Collapse());
}
private static IEnumerable<IRenderable> GetCalendars()
{
yield return EmbedInPanel(
"Invariant calendar",
new Calendar(2020, 10)
.SimpleHeavyBorder()
.SetHighlightStyle(Style.Parse("red"))
.AddCalendarEvent("An event", 2020, 9, 22)
.AddCalendarEvent("Another event", 2020, 10, 2)
.AddCalendarEvent("A third event", 2020, 10, 13));
yield return EmbedInPanel(
"Swedish calendar (sv-SE)",
new Calendar(2020, 10)
.RoundedBorder()
.SetHighlightStyle(Style.Parse("blue"))
.SetCulture("sv-SE")
.AddCalendarEvent("An event", 2020, 9, 22)
.AddCalendarEvent("Another event", 2020, 10, 2)
.AddCalendarEvent("A third event", 2020, 10, 13));
yield return EmbedInPanel(
"German calendar (de-DE)",
new Calendar(2020, 10)
.MarkdownBorder()
.SetHighlightStyle(Style.Parse("yellow"))
.SetCulture("de-DE")
.AddCalendarEvent("An event", 2020, 9, 22)
.AddCalendarEvent("Another event", 2020, 10, 2)
.AddCalendarEvent("A third event", 2020, 10, 13));
yield return EmbedInPanel(
"Italian calendar (de-DE)",
new Calendar(2020, 10)
.DoubleBorder()
.SetHighlightStyle(Style.Parse("green"))
.SetCulture("it-IT")
.AddCalendarEvent("An event", 2020, 9, 22)
.AddCalendarEvent("Another event", 2020, 10, 2)
.AddCalendarEvent("A third event", 2020, 10, 13));
}
private static IRenderable EmbedInPanel(string title, Calendar calendar)
{
return new Panel(calendar)
.Expand()
.RoundedBorder()
.SetBorderStyle(Style.Parse("grey"))
.SetHeader($" {title} ", Style.Parse("yellow"));
}
}
}

View File

@@ -6,11 +6,19 @@ namespace EmojiExample
{
public static void Main(string[] args)
{
// Markup
// Show a known emoji
RenderEmoji();
// Show a remapped emoji
Emoji.Remap("globe_showing_europe_africa", Emoji.Known.GrinningFaceWithSmilingEyes);
RenderEmoji();
}
private static void RenderEmoji()
{
AnsiConsole.Render(
new Panel("[yellow]Hello :globe_showing_europe_africa:![/]")
.RoundedBorder()
.SetHeader("Markup"));
.RoundedBorder());
}
}
}

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,66 @@
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.Render(new Panel("[u]Default[/]").Expand());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Compact[/]").Expand());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks);
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel("[u]Custom colors[/]").Expand());
AnsiConsole.WriteLine();
AnsiConsole.WriteException(ex, new ExceptionSettings
{
Format = ExceptionFormats.ShortenEverything | ExceptionFormats.ShowLinks,
Style = new ExceptionStyle
{
Exception = Style.WithForeground(Color.Grey),
Message = Style.WithForeground(Color.White),
NonEmphasized = Style.WithForeground(Color.Cornsilk1),
Parenthesis = Style.WithForeground(Color.Cornsilk1),
Method = Style.WithForeground(Color.Red),
ParameterName = Style.WithForeground(Color.Cornsilk1),
ParameterType = Style.WithForeground(Color.Red),
Path = Style.WithForeground(Color.Red),
LineNumber = Style.WithForeground(Color.Cornsilk1),
}
});
}
}
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

@@ -1,3 +1,4 @@
using System.Diagnostics;
using Spectre.Console;
namespace TableExample

View File

@@ -87,3 +87,6 @@ dotnet_diagnostic.RCS1227.severity = none
# IDE0004: Remove Unnecessary Cast
dotnet_diagnostic.IDE0004.severity = warning
# CA1810: Initialize reference type static fields inline
dotnet_diagnostic.CA1810.severity = none

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.Default)
{
this.WriteException(ex, formats);
return Output.NormalizeStackTrace()
.NormalizeLineEndings()
.Split(new char[] { '\n' })
.Select(line => line.TrimEnd())
.ToArray();
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using Shouldly;
using Xunit;
namespace Spectre.Console.Tests.Unit
{
public sealed class CalendarTests
{
[Fact]
public void Should_Render_Calendar_Correctly()
{
// Given
var console = new PlainConsole(width: 80);
var calendar = new Calendar(2020, 10)
.AddCalendarEvent(new DateTime(2020, 9, 1))
.AddCalendarEvent(new DateTime(2020, 10, 3))
.AddCalendarEvent(new DateTime(2020, 10, 12));
// When
console.Render(calendar);
// Then
console.Lines.Count.ShouldBe(10);
console.Lines[0].ShouldBe("┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐");
console.Lines[1].ShouldBe("│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │");
console.Lines[2].ShouldBe("├─────┼─────┼─────┼─────┼─────┼─────┼─────┤");
console.Lines[3].ShouldBe("│ │ │ │ │ 1 │ 2 │ 3* │");
console.Lines[4].ShouldBe("│ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │");
console.Lines[5].ShouldBe("│ 11 │ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │");
console.Lines[6].ShouldBe("│ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │");
console.Lines[7].ShouldBe("│ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │");
console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
console.Lines[9].ShouldBe("└─────┴─────┴─────┴─────┴─────┴─────┴─────┘");
}
[Fact]
public void Should_Render_Calendar_Correctly_For_Specific_Culture()
{
// Given
var console = new PlainConsole(width: 80);
var calendar = new Calendar(2020, 10, 15)
.SetCulture("de-DE")
.AddCalendarEvent(new DateTime(2020, 9, 1))
.AddCalendarEvent(new DateTime(2020, 10, 3))
.AddCalendarEvent(new DateTime(2020, 10, 12));
// When
console.Render(calendar);
// Then
console.Lines.Count.ShouldBe(10);
console.Lines[0].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
console.Lines[1].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
console.Lines[2].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
console.Lines[3].ShouldBe("│ │ │ │ 1 │ 2 │ 3* │ 4 │");
console.Lines[4].ShouldBe("│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │");
console.Lines[5].ShouldBe("│ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │");
console.Lines[6].ShouldBe("│ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │");
console.Lines[7].ShouldBe("│ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │");
console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
console.Lines[9].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
}
[Fact]
public void Should_Render_List_Of_Events_If_Enabled()
{
// Given
var console = new PlainConsole(width: 80);
var calendar = new Calendar(2020, 10, 15)
.SetCulture("de-DE")
.AddCalendarEvent(new DateTime(2020, 9, 1))
.AddCalendarEvent(new DateTime(2020, 10, 3))
.AddCalendarEvent(new DateTime(2020, 10, 12));
// When
console.Render(calendar);
// Then
console.Lines.Count.ShouldBe(10);
console.Lines[0].ShouldBe("┌─────┬────┬────┬────┬────┬────┬────┐");
console.Lines[1].ShouldBe("│ Mo │ Di │ Mi │ Do │ Fr │ Sa │ So │");
console.Lines[2].ShouldBe("├─────┼────┼────┼────┼────┼────┼────┤");
console.Lines[3].ShouldBe("│ │ │ │ 1 │ 2 │ 3* │ 4 │");
console.Lines[4].ShouldBe("│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │");
console.Lines[5].ShouldBe("│ 12* │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │");
console.Lines[6].ShouldBe("│ 19 │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │");
console.Lines[7].ShouldBe("│ 26 │ 27 │ 28 │ 29 │ 30 │ 31 │ │");
console.Lines[8].ShouldBe("│ │ │ │ │ │ │ │");
console.Lines[9].ShouldBe("└─────┴────┴────┴────┴────┴────┴────┘");
}
}
}

View File

@@ -228,6 +228,39 @@ namespace Spectre.Console.Tests.Unit
}
}
public sealed class TheToMarkupMethod
{
[Fact]
public void Should_Return_Expected_Markup_For_Default_Color()
{
// Given, When
var result = Color.Default.ToMarkup();
// Then
result.ShouldBe("default");
}
[Fact]
public void Should_Return_Expected_Markup_For_Known_Color()
{
// Given, When
var result = Color.Red.ToMarkup();
// Then
result.ShouldBe("red");
}
[Fact]
public void Should_Return_Expected_Markup_For_Custom_Color()
{
// Given, When
var result = new Color(255, 1, 12).ToMarkup();
// Then
result.ShouldBe("#FF010C");
}
}
public sealed class TheToStringMethod
{
[Fact]

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

@@ -317,5 +317,60 @@ namespace Spectre.Console.Tests.Unit
result.ShouldBeFalse();
}
}
public sealed class TheToMarkupMethod
{
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Foreground_Color()
{
// Given
var style = new Style(Color.Red);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("red");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Foreground_And_Background_Color()
{
// Given
var style = new Style(Color.Red, Color.Green);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("red on green");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Foreground_And_Background_Color_And_Decoration()
{
// Given
var style = new Style(Color.Red, Color.Green, Decoration.Bold | Decoration.Underline);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("bold underline red on green");
}
[Fact]
public void Should_Return_Expected_Markup_For_Style_With_Only_Background_Color()
{
// Given
var style = new Style(background: Color.Green);
// When
var result = style.ToMarkup();
// Then
result.ShouldBe("default on green");
}
}
}
}

View File

@@ -35,6 +35,17 @@ 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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Calendars", "..\examples\Calendars\Calendars.csproj", "{57691C7D-683D-46E6-AA4F-57A8C5F65D25}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -177,6 +188,30 @@ 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
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|Any CPU.Build.0 = Debug|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x64.ActiveCfg = Debug|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x64.Build.0 = Debug|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x86.ActiveCfg = Debug|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Debug|x86.Build.0 = Debug|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|Any CPU.ActiveCfg = Release|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|Any CPU.Build.0 = Release|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x64.ActiveCfg = Release|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x64.Build.0 = Release|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x86.ActiveCfg = Release|Any CPU
{57691C7D-683D-46E6-AA4F-57A8C5F65D25}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -191,6 +226,9 @@ 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}
{57691C7D-683D-46E6-AA4F-57A8C5F65D25} = {F0575243-121F-4DEE-9F6B-246E26DC0844}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C}

View File

@@ -0,0 +1,30 @@
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.Default)
{
Console.WriteException(exception, format);
}
/// <summary>
/// Writes an exception to the console.
/// </summary>
/// <param name="exception">The exception to write to the console.</param>
/// <param name="settings">The exception settings.</param>
public static void WriteException(Exception exception, ExceptionSettings settings)
{
Console.WriteException(exception, settings);
}
}
}

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,37 @@ 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 ToMarkup()
{
if (IsDefault)
{
return "default";
}
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

@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace Spectre.Console
@@ -8,6 +10,35 @@ namespace Spectre.Console
public static partial class Emoji
{
private static readonly Regex _emojiCode = new Regex(@"(:(\S*?):)", RegexOptions.Compiled);
private static readonly Dictionary<string, string> _remappings;
static Emoji()
{
_remappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Remaps a specific emoji tag with a new emoji.
/// </summary>
/// <param name="tag">The emoji tag.</param>
/// <param name="emoji">The emoji.</param>
public static void Remap(string tag, string emoji)
{
if (tag is null)
{
throw new ArgumentNullException(nameof(tag));
}
if (emoji is null)
{
throw new ArgumentNullException(nameof(emoji));
}
tag = tag.TrimStart(':').TrimEnd(':');
emoji = emoji.TrimStart(':').TrimEnd(':');
_remappings[tag] = emoji;
}
/// <summary>
/// Replaces emoji markup with corresponding unicode characters.
@@ -16,7 +47,23 @@ 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 (_remappings.Count > 0 && _remappings.TryGetValue(key, out var remappedEmoji))
{
return remappedEmoji;
}
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>
Default = 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,27 @@
namespace Spectre.Console
{
/// <summary>
/// Exception settings.
/// </summary>
public sealed class ExceptionSettings
{
/// <summary>
/// Gets or sets the exception format.
/// </summary>
public ExceptionFormats Format { get; set; }
/// <summary>
/// Gets or sets the exception style.
/// </summary>
public ExceptionStyle Style { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ExceptionSettings"/> class.
/// </summary>
public ExceptionSettings()
{
Format = ExceptionFormats.Default;
Style = new ExceptionStyle();
}
}
}

View File

@@ -0,0 +1,58 @@
namespace Spectre.Console
{
/// <summary>
/// Represent an exception style.
/// </summary>
public sealed class ExceptionStyle
{
/// <summary>
/// Gets or sets the message color.
/// </summary>
public Style Message { get; set; } = new Style(Color.Red, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the exception color.
/// </summary>
public Style Exception { get; set; } = new Style(Color.White);
/// <summary>
/// Gets or sets the method color.
/// </summary>
public Style Method { get; set; } = new Style(Color.Yellow);
/// <summary>
/// Gets or sets the parameter type color.
/// </summary>
public Style ParameterType { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the parameter name color.
/// </summary>
public Style ParameterName { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the parenthesis color.
/// </summary>
public Style Parenthesis { get; set; } = new Style(Color.Silver);
/// <summary>
/// Gets or sets the path color.
/// </summary>
public Style Path { get; set; } = new Style(Color.Yellow, Color.Default, Decoration.Bold);
/// <summary>
/// Gets or sets the line number color.
/// </summary>
public Style LineNumber { get; set; } = new Style(Color.Blue);
/// <summary>
/// Gets or sets the color for dimmed text such as "at" or "in".
/// </summary>
public Style Dimmed { get; set; } = new Style(Color.Grey);
/// <summary>
/// Gets or sets the color for non emphasized items.
/// </summary>
public Style NonEmphasized { get; set; } = new Style(Color.Silver);
}
}

View File

@@ -0,0 +1,32 @@
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.Default)
{
Render(console, exception.GetRenderable(format));
}
/// <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="settings">The exception settings.</param>
public static void WriteException(this IAnsiConsole console, Exception exception, ExceptionSettings settings)
{
Render(console, exception.GetRenderable(settings));
}
}
}

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,83 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Calendar"/>.
/// </summary>
public static class CalendarExtensions
{
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar to add the calendar event to.</param>
/// <param name="date">The calendar event date.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, DateTime date)
{
return AddCalendarEvent(calendar, string.Empty, date.Year, date.Month, date.Day);
}
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar to add the calendar event to.</param>
/// <param name="description">The calendar event description.</param>
/// <param name="date">The calendar event date.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, string description, DateTime date)
{
return AddCalendarEvent(calendar, description, date.Year, date.Month, date.Day);
}
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar to add the calendar event to.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, int year, int month, int day)
{
return AddCalendarEvent(calendar, string.Empty, year, month, day);
}
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar.</param>
/// <param name="description">The calendar event description.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, string description, int year, int month, int day)
{
if (calendar is null)
{
throw new ArgumentNullException(nameof(calendar));
}
calendar.CalendarEvents.Add(new CalendarEvent(description, year, month, day));
return calendar;
}
/// <summary>
/// Sets the calendar's highlight <see cref="Style"/>.
/// </summary>
/// <param name="calendar">The calendar.</param>
/// <param name="style">The highlight style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar SetHighlightStyle(this Calendar calendar, Style? style)
{
if (calendar is null)
{
throw new ArgumentNullException(nameof(calendar));
}
calendar.HightlightStyle = style ?? Style.Plain;
return calendar;
}
}
}

View File

@@ -0,0 +1,51 @@
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.Default)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
return GetRenderable(exception, new ExceptionSettings
{
Format = format,
});
}
/// <summary>
/// Gets a <see cref="IRenderable"/> representation of the exception.
/// </summary>
/// <param name="exception">The exception to format.</param>
/// <param name="settings">The exception settings.</param>
/// <returns>A <see cref="IRenderable"/> representing the exception.</returns>
public static IRenderable GetRenderable(this Exception exception, ExceptionSettings settings)
{
if (exception is null)
{
throw new ArgumentNullException(nameof(exception));
}
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
return ExceptionFormatter.Format(exception, settings);
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Globalization;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IHasCulture"/>.
/// </summary>
public static class HasCultureExtensions
{
/// <summary>
/// Sets the culture.
/// </summary>
/// <typeparam name="T">An object type with a culture.</typeparam>
/// <param name="obj">The object to set the culture for.</param>
/// <param name="culture">The culture to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetCulture<T>(this T obj, CultureInfo culture)
where T : class, IHasCulture
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
if (culture is null)
{
throw new ArgumentNullException(nameof(culture));
}
obj.Culture = culture;
return obj;
}
/// <summary>
/// Sets the culture.
/// </summary>
/// <typeparam name="T">An object type with a culture.</typeparam>
/// <param name="obj">The object to set the culture for.</param>
/// <param name="name">The culture to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetCulture<T>(this T obj, string name)
where T : class, IHasCulture
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Culture = CultureInfo.GetCultureInfo(name);
return obj;
}
/// <summary>
/// Sets the culture.
/// </summary>
/// <typeparam name="T">An object type with a culture.</typeparam>
/// <param name="obj">The object to set the culture for.</param>
/// <param name="name">The culture to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetCulture<T>(this T obj, int name)
where T : class, IHasCulture
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Culture = CultureInfo.GetCultureInfo(name);
return obj;
}
}
}

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,15 @@
using System.Globalization;
namespace Spectre.Console
{
/// <summary>
/// Represents something that has a culture.
/// </summary>
public interface IHasCulture
{
/// <summary>
/// Gets or sets the culture.
/// </summary>
CultureInfo Culture { get; set; }
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Internal.Collections
{
internal sealed class ListWithCallback<T> : IList<T>
{
private readonly List<T> _list;
private readonly Action _callback;
public T this[int index]
{
get => _list[index];
set => _list[index] = value;
}
public int Count => _list.Count;
public bool IsReadOnly => false;
public ListWithCallback(Action callback)
{
_list = new List<T>();
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public void Add(T item)
{
_list.Add(item);
_callback();
}
public void Clear()
{
_list.Clear();
_callback();
}
public bool Contains(T item)
{
return _list.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_list.CopyTo(array, arrayIndex);
_callback();
}
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
public int IndexOf(T item)
{
return _list.IndexOf(item);
}
public void Insert(int index, T item)
{
_list.Insert(index, item);
_callback();
}
public bool Remove(T item)
{
var result = _list.Remove(item);
if (result)
{
_callback();
}
return result;
}
public void RemoveAt(int index)
{
_list.RemoveAt(index);
_callback();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Spectre.Console.Internal
{
internal static class DecorationTable
{
private static readonly Dictionary<string, Decoration?> _lookup;
private static readonly Dictionary<Decoration, string> _reverseLookup;
[SuppressMessage("Performance", "CA1810:Initialize reference type static fields inline")]
static DecorationTable()
@@ -28,6 +30,21 @@ namespace Spectre.Console.Internal
{ "strikethrough", Decoration.Strikethrough },
{ "s", Decoration.Strikethrough },
};
_reverseLookup = new Dictionary<Decoration, string>();
foreach (var (name, decoration) in _lookup)
{
// Cannot happen, but the compiler thinks so...
if (decoration == null)
{
continue;
}
if (!_reverseLookup.ContainsKey(decoration.Value))
{
_reverseLookup[decoration.Value] = name;
}
}
}
public static Decoration? GetDecoration(string name)
@@ -35,5 +52,23 @@ namespace Spectre.Console.Internal
_lookup.TryGetValue(name, out var result);
return result;
}
public static List<string> GetMarkupNames(Decoration decoration)
{
var result = new List<string>();
Enum.GetValues(typeof(Decoration))
.Cast<Decoration>()
.Where(flag => (decoration & flag) != 0)
.ForEach(flag =>
{
if (flag != Decoration.None && _reverseLookup.TryGetValue(flag, out var name))
{
result.Add(name);
}
});
return result;
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
internal static class ExceptionFormatter
{
public static IRenderable Format(Exception exception, ExceptionSettings settings)
{
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, settings);
}
private static IRenderable GetException(ExceptionInfo info, ExceptionSettings settings)
{
if (info is null)
{
throw new ArgumentNullException(nameof(info));
}
return new Rows(new IRenderable[]
{
GetMessage(info, settings),
GetStackFrames(info, settings),
}).Expand();
}
private static Markup GetMessage(ExceptionInfo ex, ExceptionSettings settings)
{
var shortenTypes = (settings.Format & ExceptionFormats.ShortenTypes) != 0;
var type = Emphasize(ex.Type, new[] { '.' }, settings.Style.Exception, shortenTypes, settings);
var message = $"[{settings.Style.Message.ToMarkup()}]{ex.Message.SafeMarkup()}[/]";
return new Markup(string.Concat(type, ": ", message));
}
private static Grid GetStackFrames(ExceptionInfo ex, ExceptionSettings settings)
{
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, settings));
}
// Stack frames
foreach (var frame in ex.Frames)
{
var builder = new StringBuilder();
// Method
var shortenMethods = (settings.Format & ExceptionFormats.ShortenMethods) != 0;
builder.Append(Emphasize(frame.Method, new[] { '.' }, settings.Style.Method, shortenMethods, settings));
builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append('(').Append("[/]");
AppendParameters(builder, frame, settings);
builder.Append('[').Append(settings.Style.Parenthesis.ToMarkup()).Append(']').Append(')').Append("[/]");
if (frame.Path != null)
{
builder.Append(" [").Append(settings.Style.Dimmed.ToMarkup()).Append("]in[/] ");
// Path
AppendPath(builder, frame, settings);
// Line number
if (frame.LineNumber != null)
{
builder.Append('[').Append(settings.Style.Dimmed.ToMarkup()).Append("]:[/]");
builder.Append('[').Append(settings.Style.LineNumber.ToMarkup()).Append(']').Append(frame.LineNumber).Append("[/]");
}
}
grid.AddRow(
$"[{settings.Style.Dimmed.ToMarkup()}]at[/]",
builder.ToString());
}
return grid;
}
private static void AppendParameters(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
var typeColor = settings.Style.ParameterType.ToMarkup();
var nameColor = settings.Style.ParameterName.ToMarkup();
var parameters = frame.Parameters.Select(x => $"[{typeColor}]{x.Type.SafeMarkup()}[/] [{nameColor}]{x.Name}[/]");
builder.Append(string.Join(", ", parameters));
}
private static void AppendPath(StringBuilder builder, StackFrameInfo frame, ExceptionSettings settings)
{
if (frame?.Path is null)
{
return;
}
void AppendPath()
{
var shortenPaths = (settings.Format & ExceptionFormats.ShortenPaths) != 0;
builder.Append(Emphasize(frame.Path, new[] { '/', '\\' }, settings.Style.Path, shortenPaths, settings));
}
if ((settings.Format & ExceptionFormats.ShowLinks) != 0)
{
var hasLink = frame.TryGetUri(out var uri);
if (hasLink && uri != null)
{
builder.Append("[link=").Append(uri.AbsoluteUri).Append(']');
}
AppendPath();
if (hasLink && uri != null)
{
builder.Append("[/]");
}
}
else
{
AppendPath();
}
}
private static string Emphasize(string input, char[] separators, Style color, bool compact, ExceptionSettings settings)
{
var builder = new StringBuilder();
var type = input;
var index = type.LastIndexOfAny(separators);
if (index != -1)
{
if (!compact)
{
builder.Append('[').Append(settings.Style.NonEmphasized.ToMarkup()).Append(']')
.Append(type, 0, index + 1).Append("[/]");
}
builder.Append('[').Append(color.ToMarkup()).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

@@ -0,0 +1,32 @@
using System;
using System.Globalization;
namespace Spectre.Console.Internal
{
internal static class DayOfWeekExtensions
{
public static string GetAbbreviatedDayName(this DayOfWeek day, CultureInfo culture)
{
culture ??= CultureInfo.InvariantCulture;
var name = culture.DateTimeFormat.GetAbbreviatedDayName(day);
if (name.Length > 0 && char.IsLower(name[0]))
{
name = string.Format(culture, "{0}{1}", char.ToUpper(name[0], culture), name.Substring(1));
}
return name;
}
public static DayOfWeek GetNextWeekDay(this DayOfWeek day)
{
var next = (int)day + 1;
if (next > (int)DayOfWeek.Saturday)
{
return DayOfWeek.Sunday;
}
return (DayOfWeek)next;
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Spectre.Console.Internal
{
internal static class DictionaryExtensions
{
public static void Deconstruct<T1, T2>(this KeyValuePair<T1, T2> tuple, out T1 key, out T2 value)
{
key = tuple.Key;
value = tuple.Value;
}
}
}

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...
return new List<Segment>();
}
result.Add(new Segment(segment.Text.Substring(index, take), segment.Style));
lengthLeft -= take;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Spectre.Console.Internal;
@@ -25,7 +26,7 @@ namespace Spectre.Console
public Decoration Decoration { get; }
/// <summary>
/// Gets the link.
/// Gets the link associated with the style.
/// </summary>
public string? Link { get; }
@@ -191,6 +192,41 @@ namespace Spectre.Console
}
}
/// <summary>
/// Returns the markup representation of this style.
/// </summary>
/// <returns>The markup representation of this style.</returns>
public string ToMarkup()
{
var builder = new List<string>();
if (Decoration != Decoration.None)
{
var result = DecorationTable.GetMarkupNames(Decoration);
if (result.Count != 0)
{
builder.AddRange(result);
}
}
if (Foreground != Color.Default)
{
builder.Add(Foreground.ToMarkup());
}
if (Background != Color.Default)
{
if (builder.Count == 0)
{
builder.Add("default");
}
builder.Add("on " + Background.ToMarkup());
}
return string.Join(" ", builder);
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{

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

@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Internal.Collections;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A renderable calendar.
/// </summary>
public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder
{
private const int NumberOfWeekDays = 7;
private const int ExpectedRowCount = 6;
private readonly ListWithCallback<CalendarEvent> _calendarEvents;
private int _year;
private int _month;
private int _day;
private IRenderable? _table;
private TableBorder _border;
private bool _useSafeBorder;
private Style? _borderStyle;
private bool _dirty;
private CultureInfo _culture;
private Style _highlightStyle;
/// <summary>
/// Gets or sets the calendar year.
/// </summary>
public int Year
{
get => _year;
set => MarkAsDirty(() => _year = value);
}
/// <summary>
/// Gets or sets the calendar month.
/// </summary>
public int Month
{
get => _month;
set => MarkAsDirty(() => _month = value);
}
/// <summary>
/// Gets or sets the calendar day.
/// </summary>
public int Day
{
get => _day;
set => MarkAsDirty(() => _day = value);
}
/// <inheritdoc/>
public TableBorder Border
{
get => _border;
set => MarkAsDirty(() => _border = value);
}
/// <inheritdoc/>
public bool UseSafeBorder
{
get => _useSafeBorder;
set => MarkAsDirty(() => _useSafeBorder = value);
}
/// <inheritdoc/>
public Style? BorderStyle
{
get => _borderStyle;
set => MarkAsDirty(() => _borderStyle = value);
}
/// <summary>
/// Gets or sets the calendar's <see cref="CultureInfo"/>.
/// </summary>
public CultureInfo Culture
{
get => _culture;
set => MarkAsDirty(() => _culture = value);
}
/// <summary>
/// Gets or sets the calendar's highlight <see cref="Style"/>.
/// </summary>
public Style HightlightStyle
{
get => _highlightStyle;
set => MarkAsDirty(() => _highlightStyle = value);
}
/// <summary>
/// Gets a list containing all calendar events.
/// </summary>
public IList<CalendarEvent> CalendarEvents => _calendarEvents;
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="date">The calendar date.</param>
public Calendar(DateTime date)
: this(date.Year, date.Month, date.Day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
public Calendar(int year, int month)
: this(year, month, 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
/// <param name="day">The calendar day.</param>
public Calendar(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_table = null;
_border = TableBorder.Square;
_useSafeBorder = true;
_borderStyle = null;
_dirty = true;
_culture = CultureInfo.InvariantCulture;
_highlightStyle = new Style(foreground: Color.Blue);
_calendarEvents = new ListWithCallback<CalendarEvent>(() => _dirty = true);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var table = GetTable();
return table.Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return GetTable().Render(context, maxWidth);
}
private IRenderable GetTable()
{
// Table needs to be built?
if (_dirty || _table == null)
{
_table = BuildTable();
_dirty = false;
}
return _table;
}
private IRenderable BuildTable()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var table = new Table
{
Border = _border,
UseSafeBorder = _useSafeBorder,
BorderStyle = _borderStyle,
};
// Add columns
foreach (var order in GetWeekdays())
{
table.AddColumn(new TableColumn(order.GetAbbreviatedDayName(culture)));
}
var row = new List<IRenderable>();
var currentDay = 1;
var weekday = culture.DateTimeFormat.FirstDayOfWeek;
var weekdays = BuildWeekDayTable();
var daysInMonth = DateTime.DaysInMonth(Year, Month);
while (currentDay <= daysInMonth)
{
if (weekdays[currentDay - 1] == weekday)
{
if (_calendarEvents.Any(e => e.Month == Month && e.Day == currentDay))
{
row.Add(new Markup(currentDay.ToString(CultureInfo.InvariantCulture) + "*", _highlightStyle));
}
else
{
row.Add(new Text(currentDay.ToString(CultureInfo.InvariantCulture)));
}
currentDay++;
}
else
{
// Add empty cell
row.Add(Text.Empty);
}
if (row.Count == NumberOfWeekDays)
{
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
weekday = weekday.GetNextWeekDay();
}
if (row.Count > 0)
{
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
// We want all calendars to have the same height.
if (table.RowCount < ExpectedRowCount)
{
var diff = Math.Max(0, ExpectedRowCount - table.RowCount);
for (var i = 0; i < diff; i++)
{
table.AddEmptyRow();
}
}
return table;
}
private void MarkAsDirty(Action action)
{
action();
_dirty = true;
}
private DayOfWeek[] GetWeekdays()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var days = new DayOfWeek[7];
days[0] = culture.DateTimeFormat.FirstDayOfWeek;
for (var i = 1; i < 7; i++)
{
days[i] = days[i - 1].GetNextWeekDay();
}
return days;
}
private DayOfWeek[] BuildWeekDayTable()
{
var result = new List<DayOfWeek>();
for (var day = 0; day < DateTime.DaysInMonth(Year, Month); day++)
{
result.Add(new DateTime(Year, Month, day + 1).DayOfWeek);
}
return result.ToArray();
}
}
}

View File

@@ -0,0 +1,54 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a calendar event.
/// </summary>
public sealed class CalendarEvent
{
/// <summary>
/// Gets the description of the calendar event.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the year of the calendar event.
/// </summary>
public int Year { get; }
/// <summary>
/// Gets the month of the calendar event.
/// </summary>
public int Month { get; }
/// <summary>
/// Gets the day of the calendar event.
/// </summary>
public int Day { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(int year, int month, int day)
: this(string.Empty, year, month, day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="description">The calendar event description.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(string description, int year, int month, int day)
{
Description = description ?? string.Empty;
Year = year;
Month = month;
Day = day;
}
}
}

View File

@@ -50,6 +50,29 @@ namespace Spectre.Console
_items = new List<IRenderable>(items.Select(item => new Markup(item)));
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.Left, Padding.Right);
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
var rows = _items.Count / columnCount;
var greatestWidth = 0;
for (var row = 0; row < rows; row += columnCount)
{
var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList();
var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1));
if (totalWidth > greatestWidth)
{
greatestWidth = totalWidth;
}
}
return new Measurement(greatestWidth, greatestWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{

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();