diff --git a/src/Spectre.Console.Rx.Json/IJsonParser.cs b/src/Spectre.Console.Rx.Json/IJsonParser.cs new file mode 100644 index 0000000..3da5431 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/IJsonParser.cs @@ -0,0 +1,17 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +/// +/// Represents a JSON parser. +/// +public interface IJsonParser +{ + /// + /// Parses the provided JSON into an abstract syntax tree. + /// + /// The JSON to parse. + /// An instance. + JsonSyntax Parse(string json); +} diff --git a/src/Spectre.Console.Rx.Json/JsonBuilder.cs b/src/Spectre.Console.Rx.Json/JsonBuilder.cs new file mode 100644 index 0000000..6aaf332 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonBuilder.cs @@ -0,0 +1,78 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal sealed class JsonBuilder : JsonSyntaxVisitor +{ + public static JsonBuilder Shared { get; } = new JsonBuilder(); + + public override void VisitObject(JsonObject syntax, JsonBuilderContext context) + { + context.Paragraph.Append("{", context.Styling.BracesStyle); + context.Paragraph.Append("\n"); + context.Indentation++; + + foreach (var (_, _, last, property) in syntax.Members.Enumerate()) + { + context.InsertIndentation(); + property.Accept(this, context); + + if (!last) + { + context.Paragraph.Append(",", context.Styling.CommaStyle); + } + + context.Paragraph.Append("\n"); + } + + context.Indentation--; + context.InsertIndentation(); + context.Paragraph.Append("}", context.Styling.BracesStyle); + } + + public override void VisitArray(JsonArray syntax, JsonBuilderContext context) + { + context.Paragraph.Append("[", context.Styling.BracketsStyle); + context.Paragraph.Append("\n"); + context.Indentation++; + + foreach (var (_, _, last, item) in syntax.Items.Enumerate()) + { + context.InsertIndentation(); + item.Accept(this, context); + + if (!last) + { + context.Paragraph.Append(",", context.Styling.CommaStyle); + } + + context.Paragraph.Append("\n"); + } + + context.Indentation--; + context.InsertIndentation(); + context.Paragraph.Append("]", context.Styling.BracketsStyle); + } + + public override void VisitMember(JsonMember syntax, JsonBuilderContext context) + { + context.Paragraph.Append(syntax.Name, context.Styling.MemberStyle); + context.Paragraph.Append(":", context.Styling.ColonStyle); + context.Paragraph.Append(" "); + + syntax.Value.Accept(this, context); + } + + public override void VisitNumber(JsonNumber syntax, JsonBuilderContext context) => + context.Paragraph.Append(syntax.Lexeme, context.Styling.NumberStyle); + + public override void VisitString(JsonString syntax, JsonBuilderContext context) => + context.Paragraph.Append(syntax.Lexeme, context.Styling.StringStyle); + + public override void VisitBoolean(JsonBoolean syntax, JsonBuilderContext context) => + context.Paragraph.Append(syntax.Lexeme, context.Styling.BooleanStyle); + + public override void VisitNull(JsonNull syntax, JsonBuilderContext context) => + context.Paragraph.Append(syntax.Lexeme, context.Styling.NullStyle); +} diff --git a/src/Spectre.Console.Rx.Json/JsonBuilderContext.cs b/src/Spectre.Console.Rx.Json/JsonBuilderContext.cs new file mode 100644 index 0000000..f491470 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonBuilderContext.cs @@ -0,0 +1,15 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal sealed class JsonBuilderContext(JsonTextStyles styling) +{ + public Paragraph Paragraph { get; } = new Paragraph(); + + public int Indentation { get; set; } + + public JsonTextStyles Styling { get; } = styling; + + public void InsertIndentation() => Paragraph.Append(new string(' ', Indentation * 3)); +} diff --git a/src/Spectre.Console.Rx.Json/JsonParser.cs b/src/Spectre.Console.Rx.Json/JsonParser.cs new file mode 100644 index 0000000..6a104b5 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonParser.cs @@ -0,0 +1,141 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal sealed class JsonParser : IJsonParser +{ + public static JsonParser Shared { get; } = new JsonParser(); + + public JsonSyntax Parse(string json) + { + try + { + var tokens = JsonTokenizer.Tokenize(json); + var reader = new JsonTokenReader(tokens); + return ParseElement(reader); + } + catch + { + throw new InvalidOperationException("Invalid JSON"); + } + } + + private static JsonSyntax ParseElement(JsonTokenReader reader) => ParseValue(reader); + + private static List ParseElements(JsonTokenReader reader) + { + var members = new List(); + + while (!reader.Eof) + { + members.Add(ParseElement(reader)); + + if (reader.Peek()?.Type != JsonTokenType.Comma) + { + break; + } + + reader.Consume(JsonTokenType.Comma); + } + + return members; + } + + private static JsonSyntax ParseValue(JsonTokenReader reader) + { + var current = reader.Peek() ?? throw new InvalidOperationException("Could not parse value (EOF)"); + if (current.Type == JsonTokenType.LeftBrace) + { + return ParseObject(reader); + } + + if (current.Type == JsonTokenType.LeftBracket) + { + return ParseArray(reader); + } + + if (current.Type == JsonTokenType.Number) + { + reader.Consume(JsonTokenType.Number); + return new JsonNumber(current.Lexeme); + } + + if (current.Type == JsonTokenType.String) + { + reader.Consume(JsonTokenType.String); + return new JsonString(current.Lexeme); + } + + if (current.Type == JsonTokenType.Boolean) + { + reader.Consume(JsonTokenType.Boolean); + return new JsonBoolean(current.Lexeme); + } + + if (current.Type == JsonTokenType.Null) + { + reader.Consume(JsonTokenType.Null); + return new JsonNull(current.Lexeme); + } + + throw new InvalidOperationException($"Unknown value token: {current.Type}"); + } + + private static JsonSyntax ParseObject(JsonTokenReader reader) + { + reader.Consume(JsonTokenType.LeftBrace); + + var result = new JsonObject(); + + if (reader.Peek()?.Type != JsonTokenType.RightBrace) + { + result.Members.AddRange(ParseMembers(reader)); + } + + reader.Consume(JsonTokenType.RightBrace); + return result; + } + + private static JsonSyntax ParseArray(JsonTokenReader reader) + { + reader.Consume(JsonTokenType.LeftBracket); + + var result = new JsonArray(); + + if (reader.Peek()?.Type != JsonTokenType.RightBracket) + { + result.Items.AddRange(ParseElements(reader)); + } + + reader.Consume(JsonTokenType.RightBracket); + return result; + } + + private static List ParseMembers(JsonTokenReader reader) + { + var members = new List(); + + while (!reader.Eof) + { + members.Add(ParseMember(reader)); + + if (reader.Peek()?.Type != JsonTokenType.Comma) + { + break; + } + + reader.Consume(JsonTokenType.Comma); + } + + return members; + } + + private static JsonMember ParseMember(JsonTokenReader reader) + { + var name = reader.Consume(JsonTokenType.String); + reader.Consume(JsonTokenType.Colon); + var value = ParseElement(reader); + return new JsonMember(name.Lexeme, value); + } +} diff --git a/src/Spectre.Console.Rx.Json/JsonText.cs b/src/Spectre.Console.Rx.Json/JsonText.cs new file mode 100644 index 0000000..3a9b4b7 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonText.cs @@ -0,0 +1,99 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +/// +/// A renderable piece of JSON text. +/// +/// +/// Initializes a new instance of the class. +/// +/// The JSON to render. +public sealed class JsonText(string json) : JustInTimeRenderable +{ + private readonly string _json = json ?? throw new ArgumentNullException(nameof(json)); + private JsonSyntax? _syntax; + private IJsonParser? _parser; + + /// + /// Gets or sets the style used for braces. + /// + public Style? BracesStyle { get; set; } + + /// + /// Gets or sets the style used for brackets. + /// + public Style? BracketsStyle { get; set; } + + /// + /// Gets or sets the style used for member names. + /// + public Style? MemberStyle { get; set; } + + /// + /// Gets or sets the style used for colons. + /// + public Style? ColonStyle { get; set; } + + /// + /// Gets or sets the style used for commas. + /// + public Style? CommaStyle { get; set; } + + /// + /// Gets or sets the style used for string literals. + /// + public Style? StringStyle { get; set; } + + /// + /// Gets or sets the style used for number literals. + /// + public Style? NumberStyle { get; set; } + + /// + /// Gets or sets the style used for boolean literals. + /// + public Style? BooleanStyle { get; set; } + + /// + /// Gets or sets the style used for null literals. + /// + public Style? NullStyle { get; set; } + + /// + /// Gets or sets the JSON parser. + /// + public IJsonParser? Parser + { + get => _parser; + + set + { + _syntax = null; + _parser = value; + } + } + + /// + protected override IRenderable Build() + { + _syntax ??= (Parser ?? JsonParser.Shared).Parse(_json); + + var context = new JsonBuilderContext(new JsonTextStyles + { + BracesStyle = BracesStyle ?? Color.Grey, + BracketsStyle = BracketsStyle ?? Color.Grey, + MemberStyle = MemberStyle ?? Color.Blue, + ColonStyle = ColonStyle ?? Color.Yellow, + CommaStyle = CommaStyle ?? Color.Grey, + StringStyle = StringStyle ?? Color.Red, + NumberStyle = NumberStyle ?? Color.Green, + BooleanStyle = BooleanStyle ?? Color.Green, + NullStyle = NullStyle ?? Color.Grey, + }); + + _syntax.Accept(JsonBuilder.Shared, context); + return context.Paragraph; + } +} diff --git a/src/Spectre.Console.Rx.Json/JsonTextExtensions.cs b/src/Spectre.Console.Rx.Json/JsonTextExtensions.cs new file mode 100644 index 0000000..4458663 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonTextExtensions.cs @@ -0,0 +1,316 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +/// +/// Contains extension methods for . +/// +public static class JsonTextExtensions +{ + /// + /// Sets the style used for braces. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText BracesStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.BracesStyle = style; + return text; + } + + /// + /// Sets the style used for brackets. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText BracketStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.BracketsStyle = style; + return text; + } + + /// + /// Sets the style used for member names. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText MemberStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.MemberStyle = style; + return text; + } + + /// + /// Sets the style used for colons. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText ColonStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.ColonStyle = style; + return text; + } + + /// + /// Sets the style used for commas. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText CommaStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.CommaStyle = style; + return text; + } + + /// + /// Sets the style used for string literals. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText StringStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.StringStyle = style; + return text; + } + + /// + /// Sets the style used for number literals. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText NumberStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.NumberStyle = style; + return text; + } + + /// + /// Sets the style used for boolean literals. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText BooleanStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.BooleanStyle = style; + return text; + } + + /// + /// Sets the style used for null literals. + /// + /// The JSON text instance. + /// The style to set. + /// The same instance so that multiple calls can be chained. + public static JsonText NullStyle(this JsonText text, Style? style) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.NullStyle = style; + return text; + } + + /// + /// Sets the color used for braces. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText BracesColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.BracesStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for brackets. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText BracketColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.BracketsStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for member names. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText MemberColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.MemberStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for colons. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText ColonColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.ColonStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for commas. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText CommaColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.CommaStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for string literals. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText StringColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.StringStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for number literals. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText NumberColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.NumberStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for boolean literals. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText BooleanColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.BooleanStyle = new Style(color); + return text; + } + + /// + /// Sets the color used for null literals. + /// + /// The JSON text instance. + /// The color to set. + /// The same instance so that multiple calls can be chained. + public static JsonText NullColor(this JsonText text, Color color) + { + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + text.NullStyle = new Style(color); + return text; + } +} diff --git a/src/Spectre.Console.Rx.Json/JsonTextStyles.cs b/src/Spectre.Console.Rx.Json/JsonTextStyles.cs new file mode 100644 index 0000000..df50d7f --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonTextStyles.cs @@ -0,0 +1,25 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal sealed class JsonTextStyles +{ + public Style BracesStyle { get; set; } = null!; + + public Style BracketsStyle { get; set; } = null!; + + public Style MemberStyle { get; set; } = null!; + + public Style ColonStyle { get; set; } = null!; + + public Style CommaStyle { get; set; } = null!; + + public Style StringStyle { get; set; } = null!; + + public Style NumberStyle { get; set; } = null!; + + public Style BooleanStyle { get; set; } = null!; + + public Style NullStyle { get; set; } = null!; +} diff --git a/src/Spectre.Console.Rx.Json/JsonToken.cs b/src/Spectre.Console.Rx.Json/JsonToken.cs new file mode 100644 index 0000000..0a6318c --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonToken.cs @@ -0,0 +1,11 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal sealed class JsonToken(JsonTokenType type, string lexeme) +{ + public JsonTokenType Type { get; } = type; + + public string Lexeme { get; } = lexeme ?? throw new ArgumentNullException(nameof(lexeme)); +} diff --git a/src/Spectre.Console.Rx.Json/JsonTokenReader.cs b/src/Spectre.Console.Rx.Json/JsonTokenReader.cs new file mode 100644 index 0000000..63f72df --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonTokenReader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal sealed class JsonTokenReader(List tokens) +{ + private readonly int _length = tokens.Count; + + public int Position { get; private set; } + + public bool Eof => Position >= _length; + + public JsonToken Consume(JsonTokenType type) + { + var read = Read() ?? throw new InvalidOperationException("Could not read token"); + if (read.Type != type) + { + throw new InvalidOperationException($"Expected '{type}' token, but found '{read.Type}'"); + } + + return read; + } + + public JsonToken? Peek() + { + if (Eof) + { + return null; + } + + return tokens[Position]; + } + + public JsonToken? Read() + { + if (Eof) + { + return null; + } + + Position++; + return tokens[Position - 1]; + } +} diff --git a/src/Spectre.Console.Rx.Json/JsonTokenType.cs b/src/Spectre.Console.Rx.Json/JsonTokenType.cs new file mode 100644 index 0000000..40d7792 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonTokenType.cs @@ -0,0 +1,18 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json; + +internal enum JsonTokenType +{ + LeftBrace, + RightBrace, + LeftBracket, + RightBracket, + Colon, + Comma, + String, + Number, + Boolean, + Null, +} diff --git a/src/Spectre.Console.Rx.Json/JsonTokenizer.cs b/src/Spectre.Console.Rx.Json/JsonTokenizer.cs new file mode 100644 index 0000000..a416a61 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/JsonTokenizer.cs @@ -0,0 +1,208 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text; + +namespace Spectre.Console.Rx.Json; + +internal static class JsonTokenizer +{ + private static readonly Dictionary _typeLookup; + private static readonly Dictionary _keywords; + private static readonly HashSet _allowedEscapedChars; + + static JsonTokenizer() + { + _typeLookup = new Dictionary + { + { '{', JsonTokenType.LeftBrace }, + { '}', JsonTokenType.RightBrace }, + { '[', JsonTokenType.LeftBracket }, + { ']', JsonTokenType.RightBracket }, + { ':', JsonTokenType.Colon }, + { ',', JsonTokenType.Comma }, + }; + + _keywords = new Dictionary + { + { "true", JsonTokenType.Boolean }, + { "false", JsonTokenType.Boolean }, + { "null", JsonTokenType.Null }, + }; + + _allowedEscapedChars = new HashSet + { + '\"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u', + }; + } + + public static List Tokenize(string text) + { + var result = new List(); + var buffer = new StringBuffer(text); + + while (!buffer.Eof) + { + var current = buffer.Peek(); + + if (_typeLookup.TryGetValue(current, out var tokenType)) + { + buffer.Read(); // Consume + result.Add(new JsonToken(tokenType, current.ToString())); + continue; + } + else if (current == '\"') + { + result.Add(ReadString(buffer)); + } + else if (current == '-' || current.IsDigit()) + { + result.Add(ReadNumber(buffer)); + } + else if (current is ' ' or '\n' or '\r' or '\t') + { + buffer.Read(); // Consume + } + else if (char.IsLetter(current)) + { + var accumulator = new StringBuilder(); + while (!buffer.Eof) + { + current = buffer.Peek(); + if (!char.IsLetter(current)) + { + break; + } + + buffer.Read(); // Consume + accumulator.Append(current); + } + + if (!_keywords.TryGetValue(accumulator.ToString(), out var keyword)) + { + throw new InvalidOperationException($"Encountered invalid keyword '{keyword}'"); + } + + result.Add(new JsonToken(keyword, accumulator.ToString())); + } + else + { + throw new InvalidOperationException("Invalid token"); + } + } + + return result; + } + + private static JsonToken ReadString(StringBuffer buffer) + { + var accumulator = new StringBuilder(); + accumulator.Append(buffer.Expect('\"')); + + while (!buffer.Eof) + { + var current = buffer.Peek(); + if (current == '\"') + { + break; + } + else if (current == '\\') + { + buffer.Expect('\\'); + + if (buffer.Eof) + { + break; + } + + current = buffer.Read(); + if (!_allowedEscapedChars.Contains(current)) + { + throw new InvalidOperationException("Invalid escape encountered"); + } + + accumulator.Append('\\').Append(current); + } + else + { + accumulator.Append(current); + buffer.Read(); + } + } + + if (buffer.Eof) + { + throw new InvalidOperationException("Unterminated string literal"); + } + + accumulator.Append(buffer.Expect('\"')); + return new JsonToken(JsonTokenType.String, accumulator.ToString()); + } + + private static JsonToken ReadNumber(StringBuffer buffer) + { + var accumulator = new StringBuilder(); + + // Minus? + if (buffer.Peek() == '-') + { + buffer.Read(); + accumulator.Append('-'); + } + + // Digits + var current = buffer.Peek(); + if (current.IsDigit(min: 1)) + { + ReadDigits(buffer, accumulator, min: 1); + } + else if (current == '0') + { + accumulator.Append(buffer.Expect('0')); + } + else + { + throw new InvalidOperationException("Invalid number"); + } + + // Fractions + current = buffer.Peek(); + if (current == '.') + { + accumulator.Append(buffer.Expect('.')); + ReadDigits(buffer, accumulator); + } + + // Exponent + current = buffer.Peek(); + if (current is 'e' or 'E') + { + accumulator.Append(buffer.Read()); + + current = buffer.Peek(); + if (current is '+' or '-') + { + accumulator.Append(buffer.Read()); + } + + ReadDigits(buffer, accumulator); + } + + return new JsonToken(JsonTokenType.Number, accumulator.ToString()); + } + + private static void ReadDigits(StringBuffer buffer, StringBuilder accumulator, int min = 0) + { + while (!buffer.Eof) + { + var current = buffer.Peek(); + if (!current.IsDigit(min)) + { + break; + } + + buffer.Read(); // Consume + accumulator.Append(current); + } + } +} diff --git a/src/Spectre.Console.Rx.Json/Properties/Usings.cs b/src/Spectre.Console.Rx.Json/Properties/Usings.cs new file mode 100644 index 0000000..fb4ee75 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Properties/Usings.cs @@ -0,0 +1,7 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using System.Text; +global using Spectre.Console.Rx.Internal; +global using Spectre.Console.Rx.Json.Syntax; +global using Spectre.Console.Rx.Rendering; diff --git a/src/Spectre.Console.Rx.Json/Spectre.Console.Rx.Json.csproj b/src/Spectre.Console.Rx.Json/Spectre.Console.Rx.Json.csproj new file mode 100644 index 0000000..5670b6c --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Spectre.Console.Rx.Json.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0;net6.0;net7.0;net8.0 + true + A library that extends Spectre.Console with JSON superpowers. + + + + + + + + + + + + + diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonArray.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonArray.cs new file mode 100644 index 0000000..6b07c12 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonArray.cs @@ -0,0 +1,23 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents an array in the JSON abstract syntax tree. +/// +public sealed class JsonArray : JsonSyntax +{ + /// + /// Initializes a new instance of the class. + /// + public JsonArray() => Items = new(); + + /// + /// Gets the array items. + /// + public List Items { get; } + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitArray(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonBoolean.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonBoolean.cs new file mode 100644 index 0000000..a92d5fc --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonBoolean.cs @@ -0,0 +1,22 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents a boolean literal in the JSON abstract syntax tree. +/// +/// +/// Initializes a new instance of the class. +/// +/// The lexeme. +public sealed class JsonBoolean(string lexeme) : JsonSyntax +{ + /// + /// Gets the lexeme. + /// + public string Lexeme { get; } = lexeme; + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitBoolean(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonMember.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonMember.cs new file mode 100644 index 0000000..d314a90 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonMember.cs @@ -0,0 +1,28 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents a member in the JSON abstract syntax tree. +/// +/// +/// Initializes a new instance of the class. +/// +/// The name. +/// The value. +public sealed class JsonMember(string name, JsonSyntax value) : JsonSyntax +{ + /// + /// Gets the member name. + /// + public string Name { get; } = name; + + /// + /// Gets the member value. + /// + public JsonSyntax Value { get; } = value; + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitMember(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonNull.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonNull.cs new file mode 100644 index 0000000..1887915 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonNull.cs @@ -0,0 +1,22 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents a null literal in the JSON abstract syntax tree. +/// +/// +/// Initializes a new instance of the class. +/// +/// The lexeme. +public sealed class JsonNull(string lexeme) : JsonSyntax +{ + /// + /// Gets the lexeme. + /// + public string Lexeme { get; } = lexeme; + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitNull(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonNumber.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonNumber.cs new file mode 100644 index 0000000..6206cc9 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonNumber.cs @@ -0,0 +1,12 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +internal sealed class JsonNumber(string lexeme) : JsonSyntax +{ + public string Lexeme { get; } = lexeme; + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitNumber(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonObject.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonObject.cs new file mode 100644 index 0000000..41676db --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonObject.cs @@ -0,0 +1,24 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents an object in the JSON abstract syntax tree. +/// +public sealed class JsonObject : JsonSyntax +{ + + /// + /// Initializes a new instance of the class. + /// + public JsonObject() => Members = new(); + + /// + /// Gets the object's members. + /// + public List Members { get; } + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitObject(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonString.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonString.cs new file mode 100644 index 0000000..2a4bbbf --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonString.cs @@ -0,0 +1,22 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents a string literal in the JSON abstract syntax tree. +/// +/// +/// Initializes a new instance of the class. +/// +/// The lexeme. +public sealed class JsonString(string lexeme) : JsonSyntax +{ + /// + /// Gets the lexeme. + /// + public string Lexeme { get; } = lexeme; + + internal override void Accept(JsonSyntaxVisitor visitor, T context) => + visitor.VisitString(this, context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonSyntax.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonSyntax.cs new file mode 100644 index 0000000..aee737c --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonSyntax.cs @@ -0,0 +1,12 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +/// +/// Represents a syntax node in the JSON abstract syntax tree. +/// +public abstract class JsonSyntax +{ + internal abstract void Accept(JsonSyntaxVisitor visitor, T context); +} diff --git a/src/Spectre.Console.Rx.Json/Syntax/JsonSyntaxVisitor.cs b/src/Spectre.Console.Rx.Json/Syntax/JsonSyntaxVisitor.cs new file mode 100644 index 0000000..ced7ba9 --- /dev/null +++ b/src/Spectre.Console.Rx.Json/Syntax/JsonSyntaxVisitor.cs @@ -0,0 +1,21 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Json.Syntax; + +internal abstract class JsonSyntaxVisitor +{ + public abstract void VisitObject(JsonObject syntax, T context); + + public abstract void VisitArray(JsonArray syntax, T context); + + public abstract void VisitMember(JsonMember syntax, T context); + + public abstract void VisitNumber(JsonNumber syntax, T context); + + public abstract void VisitString(JsonString syntax, T context); + + public abstract void VisitBoolean(JsonBoolean syntax, T context); + + public abstract void VisitNull(JsonNull syntax, T context); +} diff --git a/src/Spectre.Console.Rx.Testing/.editorconfig b/src/Spectre.Console.Rx.Testing/.editorconfig new file mode 100644 index 0000000..2adf83c --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/.editorconfig @@ -0,0 +1,3 @@ +root = false + +[*.cs] diff --git a/src/Spectre.Console.Rx.Testing/Extensions/ShouldlyExtensions.cs b/src/Spectre.Console.Rx.Testing/Extensions/ShouldlyExtensions.cs new file mode 100644 index 0000000..fee8c4c --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Extensions/ShouldlyExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx; + +internal static class ShouldlyExtensions +{ + [DebuggerStepThrough] + public static T And(this T item, Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + action(item); + return item; + } +} diff --git a/src/Spectre.Console.Rx.Testing/Extensions/StringExtensions.cs b/src/Spectre.Console.Rx.Testing/Extensions/StringExtensions.cs new file mode 100644 index 0000000..f05f78a --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Extensions/StringExtensions.cs @@ -0,0 +1,47 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +/// +/// Contains extensions for . +/// +public static class StringExtensions +{ + /// + /// Returns a new string with all lines trimmed of trailing whitespace. + /// + /// The string to trim. + /// A new string with all lines trimmed of trailing whitespace. + public static string TrimLines(this string value) + { + if (value is null) + { + return string.Empty; + } + + var result = new List(); + foreach (var line in value.NormalizeLineEndings().Split(new[] { '\n' })) + { + result.Add(line.TrimEnd()); + } + + return string.Join("\n", result); + } + + /// + /// Returns a new string with normalized line endings. + /// + /// The string to normalize line endings for. + /// A new string with normalized line endings. + public static string NormalizeLineEndings(this string value) + { + if (value != null) + { + value = value.Replace("\r\n", "\n"); + return value.Replace("\r", string.Empty); + } + + return string.Empty; + } +} diff --git a/src/Spectre.Console.Rx.Testing/Extensions/StyleExtensions.cs b/src/Spectre.Console.Rx.Testing/Extensions/StyleExtensions.cs new file mode 100644 index 0000000..771ceef --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Extensions/StyleExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +/// +/// Contains extensions for . +/// +public static class StyleExtensions +{ + /// + /// Sets the foreground or background color of the specified style. + /// + /// The style. + /// The color. + /// Whether or not to set the foreground color. + /// The same instance so that multiple calls can be chained. + public static Style SetColor(this Style style, Color color, bool foreground) + { + if (foreground) + { + return style.Foreground(color); + } + + return style.Background(color); + } +} diff --git a/src/Spectre.Console.Rx.Testing/Internal/NoopCursor.cs b/src/Spectre.Console.Rx.Testing/Internal/NoopCursor.cs new file mode 100644 index 0000000..5d16daa --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Internal/NoopCursor.cs @@ -0,0 +1,19 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +internal sealed class NoopCursor : IAnsiConsoleCursor +{ + public void Move(CursorDirection direction, int steps) + { + } + + public void SetPosition(int column, int line) + { + } + + public void Show(bool show) + { + } +} diff --git a/src/Spectre.Console.Rx.Testing/Internal/NoopExclusivityMode.cs b/src/Spectre.Console.Rx.Testing/Internal/NoopExclusivityMode.cs new file mode 100644 index 0000000..784aebc --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Internal/NoopExclusivityMode.cs @@ -0,0 +1,35 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +internal sealed class NoopExclusivityMode : IExclusivityMode +{ + private bool _disposedValue; + + public T Run(Func func) => func(); + + public async Task RunAsync(Func> func) => await func().ConfigureAwait(false); + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects) + } + + // TODO: free unmanaged resources (unmanaged objects) and override finalizer + // TODO: set large fields to null + _disposedValue = true; + } + } +} diff --git a/src/Spectre.Console.Rx.Testing/Properties/Usings.cs b/src/Spectre.Console.Rx.Testing/Properties/Usings.cs new file mode 100644 index 0000000..fd28302 --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Properties/Usings.cs @@ -0,0 +1,11 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using System; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.IO; +global using System.Linq; +global using System.Threading; +global using System.Threading.Tasks; +global using Spectre.Console.Rx.Rendering; diff --git a/src/Spectre.Console.Rx.Testing/Spectre.Console.Rx.Testing.csproj b/src/Spectre.Console.Rx.Testing/Spectre.Console.Rx.Testing.csproj new file mode 100644 index 0000000..673de64 --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/Spectre.Console.Rx.Testing.csproj @@ -0,0 +1,12 @@ + + + + net8.0;net7.0;net6.0;netstandard2.0 + false + Contains testing utilities for Spectre.Console. + + + + + + diff --git a/src/Spectre.Console.Rx.Testing/TestCapabilities.cs b/src/Spectre.Console.Rx.Testing/TestCapabilities.cs new file mode 100644 index 0000000..17c8855 --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/TestCapabilities.cs @@ -0,0 +1,46 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +/// +/// Represents fake capabilities useful in tests. +/// +public sealed class TestCapabilities : IReadOnlyCapabilities +{ + /// + public ColorSystem ColorSystem { get; set; } = ColorSystem.TrueColor; + + /// + public bool Ansi { get; set; } + + /// + public bool Links { get; set; } + + /// + public bool Legacy { get; set; } + + /// + public bool IsTerminal { get; set; } + + /// + public bool Interactive { get; set; } + + /// + public bool Unicode { get; set; } + + /// + /// Creates a with the same capabilities as this instace. + /// + /// The console. + /// A with the same capabilities as this instace. + public RenderOptions CreateRenderContext(IAnsiConsole console) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + return RenderOptions.Create(console, this); + } +} diff --git a/src/Spectre.Console.Rx.Testing/TestConsole.cs b/src/Spectre.Console.Rx.Testing/TestConsole.cs new file mode 100644 index 0000000..fe0f327 --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/TestConsole.cs @@ -0,0 +1,114 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +/// +/// A testable console. +/// +public sealed class TestConsole : IAnsiConsole +{ + private readonly IAnsiConsole _console; + private readonly StringWriter _writer; + private IAnsiConsoleCursor? _cursor; + + /// + /// Initializes a new instance of the class. + /// + public TestConsole() + { + _writer = new StringWriter(); + _cursor = new NoopCursor(); + + Input = new TestConsoleInput(); + EmitAnsiSequences = false; + + _console = AnsiConsole.Create(new AnsiConsoleSettings + { + Ansi = AnsiSupport.Yes, + ColorSystem = (ColorSystemSupport)ColorSystem.TrueColor, + Out = new AnsiConsoleOutput(_writer), + Interactive = InteractionSupport.No, + ExclusivityMode = new NoopExclusivityMode(), + Enrichment = new ProfileEnrichment + { + UseDefaultEnrichers = false, + }, + }); + + _console.Profile.Width = 80; + _console.Profile.Height = 24; + _console.Profile.Capabilities.Ansi = true; + _console.Profile.Capabilities.Unicode = true; + } + + /// + public Profile Profile => _console.Profile; + + /// + public IExclusivityMode ExclusivityMode => _console.ExclusivityMode; + + /// + /// Gets the console input. + /// + public TestConsoleInput Input { get; } + + /// + public RenderPipeline Pipeline => _console.Pipeline; + + /// + public IAnsiConsoleCursor Cursor => _cursor ?? _console.Cursor; + + /// + IAnsiConsoleInput IAnsiConsole.Input => Input; + + /// + /// Gets the console output. + /// + public string Output => _writer.ToString(); + + /// + /// Gets the console output lines. + /// + public IReadOnlyList Lines => + Output.NormalizeLineEndings().TrimEnd('\n').Split(['\n']); + + /// + /// Gets or sets a value indicating whether or not VT/ANSI sequences + /// should be emitted to the console. + /// + public bool EmitAnsiSequences { get; set; } + + /// + public void Dispose() + { + _writer.Dispose(); + _console.Dispose(); + } + + /// + public void Clear(bool home) => _console.Clear(home); + + /// + public void Write(IRenderable renderable) + { + if (EmitAnsiSequences) + { + _console.Write(renderable); + } + else + { + foreach (var segment in renderable.GetSegments(this)) + { + if (segment.IsControlCode) + { + continue; + } + + Profile.Out.Writer.Write(segment.Text); + } + } + } + + internal void SetCursor(IAnsiConsoleCursor? cursor) => _cursor = cursor; +} diff --git a/src/Spectre.Console.Rx.Testing/TestConsoleExtensions.cs b/src/Spectre.Console.Rx.Testing/TestConsoleExtensions.cs new file mode 100644 index 0000000..151a471 --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/TestConsoleExtensions.cs @@ -0,0 +1,129 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +/// +/// Contains extensions for . +/// +public static class TestConsoleExtensions +{ + /// + /// Sets the console's color system. + /// + /// The console. + /// The color system to use. + /// The same instance so that multiple calls can be chained. + public static TestConsole Colors(this TestConsole console, ColorSystem colors) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.Profile.Capabilities.ColorSystem = colors; + return console; + } + + /// + /// Sets whether or not ANSI is supported. + /// + /// The console. + /// Whether or not VT/ANSI control codes are supported. + /// The same instance so that multiple calls can be chained. + public static TestConsole SupportsAnsi(this TestConsole console, bool enable) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.Profile.Capabilities.Ansi = enable; + return console; + } + + /// + /// Makes the console interactive. + /// + /// The console. + /// The same instance so that multiple calls can be chained. + public static TestConsole Interactive(this TestConsole console) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.Profile.Capabilities.Interactive = true; + return console; + } + + /// + /// Sets the console width. + /// + /// The console. + /// The console width. + /// The same instance so that multiple calls can be chained. + public static TestConsole Width(this TestConsole console, int width) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.Profile.Width = width; + return console; + } + + /// + /// Sets the console height. + /// + /// The console. + /// The console height. + /// The same instance so that multiple calls can be chained. + public static TestConsole Height(this TestConsole console, int width) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.Profile.Height = width; + return console; + } + + /// + /// Sets the console size. + /// + /// The console. + /// The console size. + /// The same instance so that multiple calls can be chained. + public static TestConsole Size(this TestConsole console, Size size) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.Profile.Width = size.Width; + console.Profile.Height = size.Height; + return console; + } + + /// + /// Turns on emitting of VT/ANSI sequences. + /// + /// The console. + /// The same instance so that multiple calls can be chained. + public static TestConsole EmitAnsiSequences(this TestConsole console) + { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + + console.SetCursor(null); + console.EmitAnsiSequences = true; + return console; + } +} diff --git a/src/Spectre.Console.Rx.Testing/TestConsoleInput.cs b/src/Spectre.Console.Rx.Testing/TestConsoleInput.cs new file mode 100644 index 0000000..2c24792 --- /dev/null +++ b/src/Spectre.Console.Rx.Testing/TestConsoleInput.cs @@ -0,0 +1,98 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Spectre.Console.Rx.Testing; + +/// +/// Represents a testable console input mechanism. +/// +public sealed class TestConsoleInput : IAnsiConsoleInput +{ + private readonly Queue _input; + + /// + /// Initializes a new instance of the class. + /// + public TestConsoleInput() + { + _input = new Queue(); + } + + /// + /// Pushes the specified text to the input queue. + /// + /// The input string. + public void PushText(string input) + { + if (input is null) + { + throw new ArgumentNullException(nameof(input)); + } + + foreach (var character in input) + { + PushCharacter(character); + } + } + + /// + /// Pushes the specified text followed by 'Enter' to the input queue. + /// + /// The input. + public void PushTextWithEnter(string input) + { + PushText(input); + PushKey(ConsoleKey.Enter); + } + + /// + /// Pushes the specified character to the input queue. + /// + /// The input. + public void PushCharacter(char input) + { + var control = char.IsUpper(input); + _input.Enqueue(new ConsoleKeyInfo(input, (ConsoleKey)input, false, false, control)); + } + + /// + /// Pushes the specified key to the input queue. + /// + /// The input. + public void PushKey(ConsoleKey input) + { + _input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false)); + } + + /// + /// Pushes the specified key to the input queue. + /// + /// The input. + public void PushKey(ConsoleKeyInfo consoleKeyInfo) + { + _input.Enqueue(consoleKeyInfo); + } + + /// + public bool IsKeyAvailable() + { + return _input.Count > 0; + } + + /// + public ConsoleKeyInfo? ReadKey(bool intercept) + { + if (_input.Count == 0) + { + throw new InvalidOperationException("No input available."); + } + + return _input.Dequeue(); + } + + /// + public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + { + return Task.FromResult(ReadKey(intercept)); + } +} diff --git a/src/Spectre.Console.Rx.sln b/src/Spectre.Console.Rx.sln index c76f9c2..bc19d47 100644 --- a/src/Spectre.Console.Rx.sln +++ b/src/Spectre.Console.Rx.sln @@ -19,7 +19,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProgressThreadExample", "Pr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "..\build\_build.csproj", "{15A30A6D-9A94-4A7C-A74B-D7A9CB36C216}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CombinedExample", "CombinedExample\CombinedExample.csproj", "{00E6E161-7DD9-49BB-AEDC-FFFD0447368E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CombinedExample", "CombinedExample\CombinedExample.csproj", "{00E6E161-7DD9-49BB-AEDC-FFFD0447368E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UsingSpectreConsole", "UsingSpectreConsole\UsingSpectreConsole.csproj", "{57204D65-A88B-4065-B690-F9CC6334EDEE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Using.Spectre.Console.Rx.Tests", "Tests\Using.Spectre.Console.Rx.Tests.csproj", "{4DF93391-F7EA-4E36-8992-113387F4614A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Rx.Json", "Spectre.Console.Rx.Json\Spectre.Console.Rx.Json.csproj", "{D912E5E0-6701-4933-8D22-BD9681BCA71E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Rx.Testing", "Spectre.Console.Rx.Testing\Spectre.Console.Rx.Testing.csproj", "{2887DDD5-2622-487E-9378-C2539DC31C66}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -57,6 +65,22 @@ Global {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Debug|Any CPU.Build.0 = Debug|Any CPU {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Release|Any CPU.ActiveCfg = Release|Any CPU {00E6E161-7DD9-49BB-AEDC-FFFD0447368E}.Release|Any CPU.Build.0 = Release|Any CPU + {57204D65-A88B-4065-B690-F9CC6334EDEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {57204D65-A88B-4065-B690-F9CC6334EDEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {57204D65-A88B-4065-B690-F9CC6334EDEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {57204D65-A88B-4065-B690-F9CC6334EDEE}.Release|Any CPU.Build.0 = Release|Any CPU + {4DF93391-F7EA-4E36-8992-113387F4614A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DF93391-F7EA-4E36-8992-113387F4614A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DF93391-F7EA-4E36-8992-113387F4614A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DF93391-F7EA-4E36-8992-113387F4614A}.Release|Any CPU.Build.0 = Release|Any CPU + {D912E5E0-6701-4933-8D22-BD9681BCA71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D912E5E0-6701-4933-8D22-BD9681BCA71E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D912E5E0-6701-4933-8D22-BD9681BCA71E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D912E5E0-6701-4933-8D22-BD9681BCA71E}.Release|Any CPU.Build.0 = Release|Any CPU + {2887DDD5-2622-487E-9378-C2539DC31C66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2887DDD5-2622-487E-9378-C2539DC31C66}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2887DDD5-2622-487E-9378-C2539DC31C66}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2887DDD5-2622-487E-9378-C2539DC31C66}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -68,6 +92,8 @@ Global {51ACCE14-F44D-455B-A5E9-AEC71F7C6F61} = {E17E245C-D501-46B4-A804-A68241982088} {CB209C31-2CD2-4188-A088-18E68DF23788} = {E17E245C-D501-46B4-A804-A68241982088} {00E6E161-7DD9-49BB-AEDC-FFFD0447368E} = {E17E245C-D501-46B4-A804-A68241982088} + {57204D65-A88B-4065-B690-F9CC6334EDEE} = {E17E245C-D501-46B4-A804-A68241982088} + {4DF93391-F7EA-4E36-8992-113387F4614A} = {E17E245C-D501-46B4-A804-A68241982088} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2FADF055-54C6-4DE5-98D0-D6F352AB1EFA} diff --git a/src/Spectre.Console.Rx/Spectre.Console.Rx.csproj b/src/Spectre.Console.Rx/Spectre.Console.Rx.csproj index ebfd4d5..53bfe7b 100644 --- a/src/Spectre.Console.Rx/Spectre.Console.Rx.csproj +++ b/src/Spectre.Console.Rx/Spectre.Console.Rx.csproj @@ -11,6 +11,11 @@ False + + + + + @@ -19,7 +24,7 @@ - + diff --git a/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs b/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs index 1792b07..a3858c0 100644 --- a/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs +++ b/src/Spectre.Console.Rx/Spectre.Console/Internal/Extensions/EnumerableExtensions.cs @@ -89,16 +89,21 @@ public static void ForEach(this IEnumerable source, Action action) throw new ArgumentNullException(nameof(source)); } - var first = true; - var last = !source.MoveNext(); - T current; + return DoEnumeration(); - for (var index = 0; !last; index++) + IEnumerable<(int Index, bool First, bool Last, T Item)> DoEnumeration() { - current = source.Current; - last = !source.MoveNext(); - yield return (index, first, last, current); - first = false; + var first = true; + var last = !source.MoveNext(); + T current; + + for (var index = 0; !last; index++) + { + current = source.Current; + last = !source.MoveNext(); + yield return (index, first, last, current); + first = false; + } } } @@ -112,4 +117,4 @@ public static void ForEach(this IEnumerable source, Action action) public static IEnumerable<(TFirst First, TSecond Second, TThird Third)> ZipThree( this IEnumerable first, IEnumerable second, IEnumerable third) => first.Zip(second, (a, b) => (a, b)) .Zip(third, (a, b) => (a.a, a.b, b)); -} \ No newline at end of file +} diff --git a/src/Spectre.Console.Rx/Spectre.Console/Widgets/Figlet/FigletFont.cs b/src/Spectre.Console.Rx/Spectre.Console/Widgets/Figlet/FigletFont.cs index ee10db6..f5a9c95 100644 --- a/src/Spectre.Console.Rx/Spectre.Console/Widgets/Figlet/FigletFont.cs +++ b/src/Spectre.Console.Rx/Spectre.Console/Widgets/Figlet/FigletFont.cs @@ -8,7 +8,7 @@ namespace Spectre.Console.Rx; /// public sealed class FigletFont { - private const string StandardFont = "Spectre.Console/Widgets/Figlet/Fonts/Standard.flf"; + private const string StandardFont = "Spectre.Console.Rx/Spectre.Console/Widgets/Figlet/Fonts/Standard.flf"; private static readonly Lazy _standard; private readonly Dictionary _characters; diff --git a/src/Tests/BasicUsagesTest.WhenACalendarIsCreated_ThenPrettyPrintCalendar.verified.txt b/src/Tests/BasicUsagesTest.WhenACalendarIsCreated_ThenPrettyPrintCalendar.verified.txt new file mode 100644 index 0000000..b9792a7 --- /dev/null +++ b/src/Tests/BasicUsagesTest.WhenACalendarIsCreated_ThenPrettyPrintCalendar.verified.txt @@ -0,0 +1,11 @@ + 2023 November +┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐ +│ Sun │ Mon │ Tue │ Wed │ Thu │ Fri │ Sat │ +├─────┼─────┼─────┼─────┼─────┼─────┼─────┤ +│ │ │ │ 1 │ 2 │ 3 │ 4 │ +│ 5 │ 6 │ 7 │ 8 │ 9 │ 10 │ 11 │ +│ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │ 18 │ +│ 19* │ 20 │ 21 │ 22 │ 23 │ 24 │ 25 │ +│ 26 │ 27 │ 28 │ 29 │ 30 │ │ │ +│ │ │ │ │ │ │ │ +└─────┴─────┴─────┴─────┴─────┴─────┴─────┘ diff --git a/src/Tests/BasicUsagesTest.WhenJsonDataIsPassed_ThenRenderBeautifiedStudentsDataJson.verified.txt b/src/Tests/BasicUsagesTest.WhenJsonDataIsPassed_ThenRenderBeautifiedStudentsDataJson.verified.txt new file mode 100644 index 0000000..f7210c6 --- /dev/null +++ b/src/Tests/BasicUsagesTest.WhenJsonDataIsPassed_ThenRenderBeautifiedStudentsDataJson.verified.txt @@ -0,0 +1,39 @@ +┌───────────Students────────────┐ +│ [ │ +│ { │ +│ "Id": 1, │ +│ "FirstName": "Julie", │ +│ "LastName": "Matthew", │ +│ "Age": 19, │ +│ "Hostel": "Lincoln" │ +│ }, │ +│ { │ +│ "Id": 2, │ +│ "FirstName": "Michael", │ +│ "LastName": "Taylor", │ +│ "Age": 23, │ +│ "Hostel": "George" │ +│ }, │ +│ { │ +│ "Id": 3, │ +│ "FirstName": "Joe", │ +│ "LastName": "Hardy", │ +│ "Age": 27, │ +│ "Hostel": "Laurent" │ +│ }, │ +│ { │ +│ "Id": 4, │ +│ "FirstName": "Sabrina", │ +│ "LastName": "Azulon", │ +│ "Age": 18, │ +│ "Hostel": "George" │ +│ }, │ +│ { │ +│ "Id": 5, │ +│ "FirstName": "Hunter", │ +│ "LastName": "Cyril", │ +│ "Age": 19, │ +│ "Hostel": "Kennedy" │ +│ } │ +│ ] │ +└───────────────────────────────┘ diff --git a/src/Tests/BasicUsagesTest.cs b/src/Tests/BasicUsagesTest.cs new file mode 100644 index 0000000..94ce04f --- /dev/null +++ b/src/Tests/BasicUsagesTest.cs @@ -0,0 +1,50 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Tests; + +/// +/// BasicUsagesTest. +/// +[UsesVerify] +public class BasicUsagesTest +{ + private static readonly List _students = StudentsGenerator.GenerateStudents(); + + /// + /// Whens the format character is escaped then render text with square brackets. + /// + [Fact] + public void WhenFormatCharacterIsEscaped_ThenRenderTextWithSquareBrackets() + { + var console = new TestConsole(); + console.Markup($"[[{_students[3].FirstName}]][blue][[{_students[3].Hostel}]][/]"); + console.Output.ShouldBe("[Sabrina][George]"); + } + + /// + /// Whens the json data is passed then render beautified students data json. + /// + /// A Task. + [Fact] + public Task WhenJsonDataIsPassed_ThenRenderBeautifiedStudentsDataJson() + { + var console = new TestConsole().Size(new Size(100, 25)); + console.Write(SpectreConsoleBasicUsages.BeautifyStudentsDataJson()); + + return Verify(console.Output); + } + + /// + /// Whens a calendar is created then pretty print calendar. + /// + /// A Task. + [Fact] + public Task WhenACalendarIsCreated_ThenPrettyPrintCalendar() + { + var console = new TestConsole(); + console.Write(SpectreConsoleBasicUsages.PrettyPrintCalendar()); + + return Verify(console.Output); + } +} diff --git a/src/Tests/GlobalUsings.cs b/src/Tests/GlobalUsings.cs new file mode 100644 index 0000000..c47b1d0 --- /dev/null +++ b/src/Tests/GlobalUsings.cs @@ -0,0 +1,7 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +global using Shouldly; +global using Spectre.Console.Rx; +global using Spectre.Console.Rx.Testing; +global using UsingSpectreDotConsole; diff --git a/src/Tests/OtherUsagesTest.WhenDisplayFigLetIsInvoked_ThenDisplayFiglet.verified.txt b/src/Tests/OtherUsagesTest.WhenDisplayFigLetIsInvoked_ThenDisplayFiglet.verified.txt new file mode 100644 index 0000000..38f45c3 --- /dev/null +++ b/src/Tests/OtherUsagesTest.WhenDisplayFigLetIsInvoked_ThenDisplayFiglet.verified.txt @@ -0,0 +1,6 @@ + ____ _ _ __ + | __ ) ___ __ _ _ _ | |_ (_) / _| _ _ + | _ \ / _ \ / _` | | | | | | __| | | | |_ | | | | + | |_) | | __/ | (_| | | |_| | | |_ | | | _| | |_| | + |____/ \___| \__,_| \__,_| \__| |_| |_| \__, | + |___/ diff --git a/src/Tests/OtherUsagesTest.WhenPromptingStudentForAge_ThenReturnErrorIfValidationFails.verified.txt b/src/Tests/OtherUsagesTest.WhenPromptingStudentForAge_ThenReturnErrorIfValidationFails.verified.txt new file mode 100644 index 0000000..aa8f664 --- /dev/null +++ b/src/Tests/OtherUsagesTest.WhenPromptingStudentForAge_ThenReturnErrorIfValidationFails.verified.txt @@ -0,0 +1 @@ +How old are you? 102 diff --git a/src/Tests/OtherUsagesTest.WhenStudentDataIsPassed_ThenRenderStudentsTable.verified.txt b/src/Tests/OtherUsagesTest.WhenStudentDataIsPassed_ThenRenderStudentsTable.verified.txt new file mode 100644 index 0000000..a8db2de --- /dev/null +++ b/src/Tests/OtherUsagesTest.WhenStudentDataIsPassed_ThenRenderStudentsTable.verified.txt @@ -0,0 +1,10 @@ + STUDENTS +┌────┬───────────┬─────┐ +│ Id │ FirstName │ Age │ +├────┼───────────┼─────┤ +│ 1 │ Julie │ 19 │ +│ 2 │ Michael │ 23 │ +│ 3 │ Joe │ 27 │ +│ 4 │ Sabrina │ 18 │ +│ 5 │ Hunter │ 19 │ +└────┴───────────┴─────┘ diff --git a/src/Tests/OtherUsagesTest.WhenStudentsDataIsPassed_ThenDisplayBarChart.verified.txt b/src/Tests/OtherUsagesTest.WhenStudentsDataIsPassed_ThenDisplayBarChart.verified.txt new file mode 100644 index 0000000..add5a54 --- /dev/null +++ b/src/Tests/OtherUsagesTest.WhenStudentsDataIsPassed_ThenDisplayBarChart.verified.txt @@ -0,0 +1,6 @@ + Number of Students per Hostel +Lincoln ███████████████████████ 1 + Louisa 0 +Laurent ███████████████████████ 1 + George █████████████████████████████████████████████████ 2 +Kennedy ███████████████████████ 1 diff --git a/src/Tests/OtherUsagesTest.cs b/src/Tests/OtherUsagesTest.cs new file mode 100644 index 0000000..b38202b --- /dev/null +++ b/src/Tests/OtherUsagesTest.cs @@ -0,0 +1,89 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Tests; + +/// +/// OtherUsagesTest. +/// +[UsesVerify] +public class OtherUsagesTest +{ + /// + /// Whens the student data is passed then render students table. + /// + /// A representing the asynchronous unit test. + [Fact] + public Task WhenStudentDataIsPassed_ThenRenderStudentsTable() + { + var console = new TestConsole(); + console.Write(SpectreConsoleOtherUsages.CreateStudentTable()); + + return Verify(console.Output); + } + + /// + /// Whens the prompting student for age then return error if validation fails. + /// + /// A representing the asynchronous unit test. + [Fact] + public Task WhenPromptingStudentForAge_ThenReturnErrorIfValidationFails() + { + var console = new TestConsole(); + console.Input.PushTextWithEnter("102"); + console.Input.PushTextWithEnter("ABC"); + console.Input.PushTextWithEnter("99"); + console.Input.PushTextWithEnter("22"); + + console.Prompt(SpectreConsoleOtherUsages.GetAgeTextPrompt()); + + return Verify(console.Output); + } + + /// + /// Whens the prompting student for hostel then display hotel selection prompt. + /// + [Fact] + public void WhenPromptingStudentForHostel_ThenDisplayHotelSelectionPrompt() + { + var console = new TestConsole(); + console.Profile.Capabilities.Interactive = true; + console.Input.PushKey(ConsoleKey.Enter); + + var prompt = SpectreConsoleOtherUsages.GetHostelSelectionPrompt(); + prompt.Show(console); + + console.Output.ShouldContain("Choose a Hostel"); + console.Output.ShouldContain("Lincoln"); + console.Output.ShouldContain("Louisa"); + console.Output.ShouldContain("Laurent"); + console.Output.ShouldContain("George"); + console.Output.ShouldContain("Kennedy"); + } + + /// + /// Whens the students data is passed then display bar chart. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task WhenStudentsDataIsPassed_ThenDisplayBarChart() + { + var console = new TestConsole(); + console.Write(SpectreConsoleOtherUsages.CreateHostelBarChart()); + + await Verify(console.Output); + } + + /// + /// Whens the display fig let is invoked then display figlet. + /// + /// A representing the asynchronous unit test. + [Fact] + public async Task WhenDisplayFigLetIsInvoked_ThenDisplayFiglet() + { + var console = new TestConsole().Width(100); + console.Write(SpectreConsoleOtherUsages.DisplayFiglet()); + + await Verify(console.Output); + } +} diff --git a/src/Tests/Using.Spectre.Console.Rx.Tests.csproj b/src/Tests/Using.Spectre.Console.Rx.Tests.csproj new file mode 100644 index 0000000..93750db --- /dev/null +++ b/src/Tests/Using.Spectre.Console.Rx.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + false + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/UsingSpectreConsole/Program.cs b/src/UsingSpectreConsole/Program.cs new file mode 100644 index 0000000..6b6eea7 --- /dev/null +++ b/src/UsingSpectreConsole/Program.cs @@ -0,0 +1,11 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using UsingSpectreDotConsole; + +SpectreConsoleOtherUsages.CreateHostelBarChart(); +SpectreConsoleOtherUsages.GetHostelSelectionPrompt(); +SpectreConsoleOtherUsages.PromptStudent(); +SpectreConsoleOtherUsages.CreateStudentTable(); +SpectreConsoleOtherUsages.GetAgeTextPrompt(); +SpectreConsoleOtherUsages.WriteReadableException(); diff --git a/src/UsingSpectreConsole/SpectreConsoleBasicUsages.cs b/src/UsingSpectreConsole/SpectreConsoleBasicUsages.cs new file mode 100644 index 0000000..809eddb --- /dev/null +++ b/src/UsingSpectreConsole/SpectreConsoleBasicUsages.cs @@ -0,0 +1,76 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Spectre.Console.Rx; +using Spectre.Console.Rx.Json; + +namespace UsingSpectreDotConsole; + +/// +/// SpectreConsoleBasicUsages. +/// +public static class SpectreConsoleBasicUsages +{ + private static readonly List _students = StudentsGenerator.GenerateStudents(); + + /// + /// Sets the color of the text. + /// + public static void SetTextColor() + { + AnsiConsole.Markup($"[bold blue]Hello[/] [italic green]{_students[1].FirstName}[/]!"); + AnsiConsole.Write(new Markup($"[underline #800080]{_students[2].FirstName}[/]")); + AnsiConsole.MarkupLine($"[rgb(128,0,0)]{_students[3].FirstName}[/]"); + } + + /// + /// Sets the color of the background. + /// + public static void SetBackgroundColor() => + AnsiConsole.MarkupLine($"[red on yellow] Hello, {_students[4].LastName}![/]"); + + /// + /// Escapes the format characters. + /// + public static void EscapeFormatCharacters() + { + AnsiConsole.Markup($"[[{_students[3].FirstName}]]"); + AnsiConsole.MarkupLine($"[blue][[{_students[3].Hostel}]][/]"); + AnsiConsole.MarkupLine($"[{_students[3].Age}]".EscapeMarkup()); + } + + /// + /// Beautifies the students data json. + /// + /// A Panel. + public static Panel BeautifyStudentsDataJson() + { + var json = new JsonText(StudentsGenerator.ConvertStudentsToJson(_students)); + var panel = new Panel(json) + .Header("Students") + .HeaderAlignment(Justify.Center) + .SquareBorder() + .Collapse() + .BorderColor(Color.LightSkyBlue1); + + AnsiConsole.Write(panel); + + return panel; + } + + /// + /// Pretties the print calendar. + /// + /// A Calendar. + public static Calendar PrettyPrintCalendar() + { + var calendar = new Calendar(2023, 11) + .AddCalendarEvent(2023, 11, 19) + .HighlightStyle(Style.Parse("magenta bold")) + .HeaderStyle(Style.Parse("purple")); + + AnsiConsole.Write(calendar); + + return calendar; + } +} diff --git a/src/UsingSpectreConsole/SpectreConsoleOtherUsages.cs b/src/UsingSpectreConsole/SpectreConsoleOtherUsages.cs new file mode 100644 index 0000000..d0dfb6d --- /dev/null +++ b/src/UsingSpectreConsole/SpectreConsoleOtherUsages.cs @@ -0,0 +1,162 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Spectre.Console.Rx; + +namespace UsingSpectreDotConsole; + +/// +/// SpectreConsoleOtherUsages. +/// +public static class SpectreConsoleOtherUsages +{ + private static readonly List _students = StudentsGenerator.GenerateStudents(); + + /// + /// Creates the student table. + /// + /// A Table. + public static Table CreateStudentTable() + { + var table = new Table + { + Title = new TableTitle("STUDENTS", "bold green") + }; + + table.AddColumns("[yellow]Id[/]", $"[{Color.Olive}]FirstName[/]", "[Fuchsia]Age[/]"); + + foreach (var student in _students) + { + table.AddRow(student.Id.ToString(), $"[red]{student.FirstName}[/]", $"[cyan]{student.Age}[/]"); + } + + AnsiConsole.Write(table); + + return table; + } + + /// + /// Writes the readable exception. + /// + public static void WriteReadableException() + { + try + { + File.OpenRead("nofile.txt"); + } + catch (FileNotFoundException ex) + { + AnsiConsole.WriteException(ex, ExceptionFormats.ShortenPaths | ExceptionFormats.ShortenMethods); + } + } + + /// + /// Gets the age text prompt. + /// + /// A TextPrompt. + public static TextPrompt GetAgeTextPrompt() => + new TextPrompt("[green]How old are you[/]?") + .PromptStyle("green") + .ValidationErrorMessage("[red]That's not a valid age[/]") + .Validate(age => age switch + { + <= 10 => ValidationResult.Error("[red]You must be above 10 years[/]"), + >= 123 => ValidationResult.Error("[red]You must be younger than that[/]"), + _ => ValidationResult.Success(), + }); + + /// + /// Gets the hostel selection prompt. + /// + /// A SelectionPrompt. + public static SelectionPrompt GetHostelSelectionPrompt() + { + var hostels = StudentsGenerator.Hostels; + + return new SelectionPrompt() + .Title("Choose a hostel") + .AddChoices(hostels); + } + + /// + /// Prompts the student. + /// + /// A Student. + public static Student PromptStudent() + { + var student = new Student + { + FirstName = AnsiConsole.Ask("[green]What's your First Name[/]?"), + LastName = AnsiConsole.Ask("[green]What's your Last Name[/]?"), + + Age = AnsiConsole.Prompt(GetAgeTextPrompt()), + + Hostel = AnsiConsole.Prompt(GetHostelSelectionPrompt()) + }; + + AnsiConsole.MarkupLine($"Alright [yellow]{student.FirstName} {student.LastName}[/], welcome!"); + + return student; + } + + /// + /// Creates the hostel bar chart. + /// + /// A BarChart. + public static BarChart CreateHostelBarChart() + { + var barChart = new BarChart() + .Width(60) + .Label("[orange1 bold underline]Number of Students per Hostel[/]") + .CenterLabel(); + + var hostels = StudentsGenerator.Hostels; + var colors = new List { Color.Red, Color.Fuchsia, Color.Blue, Color.Yellow, Color.Magenta1 }; + + for (var i = 0; i < hostels.Length; i++) + { + var hostel = hostels[i]; + var color = colors[i]; + var count = _students.Count(s => s.Hostel == hostel); + barChart.AddItem(hostel, count, color); + } + + AnsiConsole.Write(barChart); + + return barChart; + } + + /// + /// Displays the progress. + /// + public static void DisplayProgress() + { + var incrementValue = 100 / _students.Count; + + AnsiConsoleRx.Progress() + .Subscribe(ctx => + { + var streamingTask = ctx.AddTask("Student Streaming"); + + foreach (var x in StudentsGenerator.StreamStudentsFromDatabase()) + { + streamingTask.Increment(incrementValue); + } + }); + } + + /// + /// Displays the figlet. + /// + /// A FigletText. + public static FigletText DisplayFiglet() + { + var text = new FigletText("Beautify") + .LeftJustified() + .Color(Color.Orchid); + + AnsiConsole.Write(text); + + return text; + } +} diff --git a/src/UsingSpectreConsole/Student.cs b/src/UsingSpectreConsole/Student.cs new file mode 100644 index 0000000..9209aea --- /dev/null +++ b/src/UsingSpectreConsole/Student.cs @@ -0,0 +1,50 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace UsingSpectreDotConsole; + +/// +/// Student. +/// +public class Student +{ + /// + /// Gets or sets the identifier. + /// + /// + /// The identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the first name. + /// + /// + /// The first name. + /// + public string? FirstName { get; set; } + + /// + /// Gets or sets the last name. + /// + /// + /// The last name. + /// + public string? LastName { get; set; } + + /// + /// Gets or sets the age. + /// + /// + /// The age. + /// + public int Age { get; set; } + + /// + /// Gets or sets the hostel. + /// + /// + /// The hostel. + /// + public string? Hostel { get; set; } +} diff --git a/src/UsingSpectreConsole/StudentsGenerator.cs b/src/UsingSpectreConsole/StudentsGenerator.cs new file mode 100644 index 0000000..f942732 --- /dev/null +++ b/src/UsingSpectreConsole/StudentsGenerator.cs @@ -0,0 +1,62 @@ +// Copyright (c) Chris Pulman. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text.Json; + +namespace UsingSpectreDotConsole; + +/// +/// StudentsGenerator. +/// +public static class StudentsGenerator +{ + private static readonly JsonSerializerOptions _writeOptions = new() + { + WriteIndented = true + }; + + /// + /// Gets the hostels. + /// + /// + /// The hostels. + /// + public static string[] Hostels { get; } + = ["Lincoln", "Louisa", "Laurent", "George", "Kennedy"]; + + /// + /// Generates the students. + /// + /// A collection of Student. + public static List GenerateStudents() => new() + { + new Student { Id = 1, FirstName = "Julie", LastName = "Matthew", Age = 19, Hostel = Hostels[0] }, + new Student { Id = 2, FirstName = "Michael", LastName = "Taylor", Age = 23, Hostel = Hostels[3] }, + new Student { Id = 3, FirstName = "Joe", LastName = "Hardy", Age = 27, Hostel = Hostels[2] }, + new Student { Id = 4, FirstName = "Sabrina", LastName = "Azulon", Age = 18, Hostel = Hostels[3] }, + new Student { Id = 5, FirstName = "Hunter", LastName = "Cyril", Age = 19, Hostel = Hostels[4] }, + }; + + /// + /// Converts the students to json. + /// + /// The students. + /// A string. + public static string ConvertStudentsToJson(List students) => + JsonSerializer.Serialize(students, _writeOptions); + + /// + /// Streams the students from database. + /// + /// A collection of Student. + public static IEnumerable StreamStudentsFromDatabase() + { + var students = GenerateStudents(); + + for (var i = 0; i < students.Count; i++) + { + yield return students[i]; + Thread.Sleep(1000); + } + } +} diff --git a/src/UsingSpectreConsole/UsingSpectreConsole.csproj b/src/UsingSpectreConsole/UsingSpectreConsole.csproj new file mode 100644 index 0000000..99e3c8a --- /dev/null +++ b/src/UsingSpectreConsole/UsingSpectreConsole.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + False + + + + + + +