Preparations for the 1.0 release

* Less cluttered solution layout.
* Move examples to a repository of its own.
* Move Roslyn analyzer to a repository of its own.
* Enable central package management.
* Clean up csproj files.
* Add README file to NuGet packages.
This commit is contained in:
Patrik Svensson
2024-08-05 20:41:45 +02:00
committed by Patrik Svensson
parent bb72b44d60
commit 42fd801876
677 changed files with 272 additions and 6214 deletions

View File

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Transforms;
using Spectre.Console.Rendering;
namespace Spectre.Console;
/// <summary>
/// Represents a renderable image.
/// </summary>
public sealed class CanvasImage : Renderable
{
private static readonly IResampler _defaultResampler = KnownResamplers.Bicubic;
/// <summary>
/// Gets the image width.
/// </summary>
public int Width => Image.Width;
/// <summary>
/// Gets the image height.
/// </summary>
public int Height => Image.Height;
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int? MaxWidth { get; set; }
/// <summary>
/// Gets or sets the render width of the canvas.
/// </summary>
public int PixelWidth { get; set; } = 2;
/// <summary>
/// Gets or sets the <see cref="IResampler"/> that should
/// be used when scaling the image. Defaults to bicubic sampling.
/// </summary>
public IResampler? Resampler { get; set; }
internal SixLabors.ImageSharp.Image<Rgba32> Image { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CanvasImage"/> class.
/// </summary>
/// <param name="filename">The image filename.</param>
public CanvasImage(string filename)
{
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(filename);
}
/// <summary>
/// Initializes a new instance of the <see cref="CanvasImage"/> class.
/// </summary>
/// <param name="data">Buffer containing an image.</param>
public CanvasImage(ReadOnlySpan<byte> data)
{
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(data);
}
/// <summary>
/// Initializes a new instance of the <see cref="CanvasImage"/> class.
/// </summary>
/// <param name="data">Stream containing an image.</param>
public CanvasImage(Stream data)
{
Image = SixLabors.ImageSharp.Image.Load<Rgba32>(data);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderOptions options, int maxWidth)
{
if (PixelWidth < 0)
{
throw new InvalidOperationException("Pixel width must be greater than zero.");
}
var width = MaxWidth ?? Width;
if (maxWidth < width * PixelWidth)
{
return new Measurement(maxWidth, maxWidth);
}
return new Measurement(width * PixelWidth, width * PixelWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderOptions options, int maxWidth)
{
var image = Image;
var width = Width;
var height = Height;
// Got a max width?
if (MaxWidth != null)
{
height = (int)(height * ((float)MaxWidth.Value) / Width);
width = MaxWidth.Value;
}
// Exceed the max width when we take pixel width into account?
if (width * PixelWidth > maxWidth)
{
height = (int)(height * (maxWidth / (float)(width * PixelWidth)));
width = maxWidth / PixelWidth;
}
// Need to rescale the pixel buffer?
if (width != Width || height != Height)
{
var resampler = Resampler ?? _defaultResampler;
image = image.Clone(); // Clone the original image
image.Mutate(i => i.Resize(width, height, resampler));
}
var canvas = new Canvas(width, height)
{
MaxWidth = MaxWidth,
PixelWidth = PixelWidth,
Scale = false,
};
for (var y = 0; y < image.Height; y++)
{
for (var x = 0; x < image.Width; x++)
{
if (image[x, y].A == 0)
{
continue;
}
canvas.SetPixel(x, y, new Color(
image[x, y].R, image[x, y].G, image[x, y].B));
}
}
return ((IRenderable)canvas).Render(options, maxWidth);
}
}

View File

@ -0,0 +1,134 @@
using System;
using SixLabors.ImageSharp.Processing;
namespace Spectre.Console;
/// <summary>
/// Contains extension methods for <see cref="CanvasImage"/>.
/// </summary>
public static class CanvasImageExtensions
{
/// <summary>
/// Sets the maximum width of the rendered image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage MaxWidth(this CanvasImage image, int? maxWidth)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.MaxWidth = maxWidth;
return image;
}
/// <summary>
/// Disables the maximum width of the rendered image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage NoMaxWidth(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.MaxWidth = null;
return image;
}
/// <summary>
/// Sets the pixel width.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="width">The pixel width.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage PixelWidth(this CanvasImage image, int width)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.PixelWidth = width;
return image;
}
/// <summary>
/// Mutates the underlying image.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <param name="action">The action that mutates the underlying image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage Mutate(this CanvasImage image, Action<IImageProcessingContext> action)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
image.Image.Mutate(action);
return image;
}
/// <summary>
/// Uses a bicubic sampler that implements the bicubic kernel algorithm W(x).
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage BicubicResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.Bicubic;
return image;
}
/// <summary>
/// Uses a bilinear sampler. This interpolation algorithm
/// can be used where perfect image transformation with pixel matching is impossible,
/// so that one can calculate and assign appropriate intensity values to pixels.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage BilinearResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.Triangle;
return image;
}
/// <summary>
/// Uses a Nearest-Neighbour sampler that implements the nearest neighbor algorithm.
/// This uses a very fast, unscaled filter which will select the closest pixel to
/// the new pixels position.
/// </summary>
/// <param name="image">The canvas image.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static CanvasImage NearestNeighborResampler(this CanvasImage image)
{
if (image is null)
{
throw new ArgumentNullException(nameof(image));
}
image.Resampler = KnownResamplers.NearestNeighbor;
return image;
}
}

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net7.0;net6.0</TargetFrameworks>
<IsPackable>true</IsPackable>
<Description>A library that extends Spectre.Console with ImageSharp superpowers.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
namespace Spectre.Console.Json;
/// <summary>
/// Represents a JSON parser.
/// </summary>
public interface IJsonParser
{
/// <summary>
/// Parses the provided JSON into an abstract syntax tree.
/// </summary>
/// <param name="json">The JSON to parse.</param>
/// <returns>An <see cref="JsonSyntax"/> instance.</returns>
JsonSyntax Parse(string json);
}

View File

@ -0,0 +1,101 @@
namespace Spectre.Console.Json;
internal sealed class JsonBuilderContext
{
public Paragraph Paragraph { get; }
public int Indentation { get; set; }
public JsonTextStyles Styling { get; }
public JsonBuilderContext(JsonTextStyles styling)
{
Paragraph = new Paragraph();
Styling = styling;
}
public void InsertIndentation()
{
Paragraph.Append(new string(' ', Indentation * 3));
}
}
internal sealed class JsonBuilder : JsonSyntaxVisitor<JsonBuilderContext>
{
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);
}
}

View File

@ -0,0 +1,146 @@
namespace Spectre.Console.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)
{
return ParseValue(reader);
}
private static List<JsonSyntax> ParseElements(JsonTokenReader reader)
{
var members = new List<JsonSyntax>();
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();
if (current == null)
{
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<JsonMember> ParseMembers(JsonTokenReader reader)
{
var members = new List<JsonMember>();
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);
}
}

View File

@ -0,0 +1,106 @@
namespace Spectre.Console.Json;
/// <summary>
/// A renderable piece of JSON text.
/// </summary>
public sealed class JsonText : JustInTimeRenderable
{
private readonly string _json;
private JsonSyntax? _syntax;
private IJsonParser? _parser;
/// <summary>
/// Gets or sets the style used for braces.
/// </summary>
public Style? BracesStyle { get; set; }
/// <summary>
/// Gets or sets the style used for brackets.
/// </summary>
public Style? BracketsStyle { get; set; }
/// <summary>
/// Gets or sets the style used for member names.
/// </summary>
public Style? MemberStyle { get; set; }
/// <summary>
/// Gets or sets the style used for colons.
/// </summary>
public Style? ColonStyle { get; set; }
/// <summary>
/// Gets or sets the style used for commas.
/// </summary>
public Style? CommaStyle { get; set; }
/// <summary>
/// Gets or sets the style used for string literals.
/// </summary>
public Style? StringStyle { get; set; }
/// <summary>
/// Gets or sets the style used for number literals.
/// </summary>
public Style? NumberStyle { get; set; }
/// <summary>
/// Gets or sets the style used for boolean literals.
/// </summary>
public Style? BooleanStyle { get; set; }
/// <summary>
/// Gets or sets the style used for <c>null</c> literals.
/// </summary>
public Style? NullStyle { get; set; }
/// <summary>
/// Gets or sets the JSON parser.
/// </summary>
public IJsonParser? Parser
{
get
{
return _parser;
}
set
{
_syntax = null;
_parser = value;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="JsonText"/> class.
/// </summary>
/// <param name="json">The JSON to render.</param>
public JsonText(string json)
{
_json = json ?? throw new ArgumentNullException(nameof(json));
}
/// <inheritdoc/>
protected override IRenderable Build()
{
if (_syntax == null)
{
_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;
}
}

View File

@ -0,0 +1,313 @@
namespace Spectre.Console.Json;
/// <summary>
/// Contains extension methods for <see cref="JsonText"/>.
/// </summary>
public static class JsonTextExtensions
{
/// <summary>
/// Sets the style used for braces.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText BracesStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.BracesStyle = style;
return text;
}
/// <summary>
/// Sets the style used for brackets.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText BracketStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.BracketsStyle = style;
return text;
}
/// <summary>
/// Sets the style used for member names.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText MemberStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.MemberStyle = style;
return text;
}
/// <summary>
/// Sets the style used for colons.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText ColonStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.ColonStyle = style;
return text;
}
/// <summary>
/// Sets the style used for commas.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText CommaStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.CommaStyle = style;
return text;
}
/// <summary>
/// Sets the style used for string literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText StringStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.StringStyle = style;
return text;
}
/// <summary>
/// Sets the style used for number literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText NumberStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.NumberStyle = style;
return text;
}
/// <summary>
/// Sets the style used for boolean literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText BooleanStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.BooleanStyle = style;
return text;
}
/// <summary>
/// Sets the style used for <c>null</c> literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="style">The style to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText NullStyle(this JsonText text, Style? style)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.NullStyle = style;
return text;
}
/// <summary>
/// Sets the color used for braces.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText BracesColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.BracesStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for brackets.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText BracketColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.BracketsStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for member names.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText MemberColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.MemberStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for colons.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText ColonColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.ColonStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for commas.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText CommaColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.CommaStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for string literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText StringColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.StringStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for number literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText NumberColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.NumberStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for boolean literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText BooleanColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.BooleanStyle = new Style(color);
return text;
}
/// <summary>
/// Sets the color used for <c>null</c> literals.
/// </summary>
/// <param name="text">The JSON text instance.</param>
/// <param name="color">The color to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static JsonText NullColor(this JsonText text, Color color)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
text.NullStyle = new Style(color);
return text;
}
}

View File

@ -0,0 +1,14 @@
namespace Spectre.Console.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!;
}

View File

@ -0,0 +1,13 @@
namespace Spectre.Console.Json;
internal sealed class JsonToken
{
public JsonTokenType Type { get; }
public string Lexeme { get; }
public JsonToken(JsonTokenType type, string lexeme)
{
Type = type;
Lexeme = lexeme ?? throw new ArgumentNullException(nameof(lexeme));
}
}

View File

@ -0,0 +1,55 @@
namespace Spectre.Console.Json;
internal sealed class JsonTokenReader
{
private readonly List<JsonToken> _reader;
private readonly int _length;
public int Position { get; private set; }
public bool Eof => Position >= _length;
public JsonTokenReader(List<JsonToken> tokens)
{
_reader = tokens;
_length = tokens.Count;
Position = 0;
}
public JsonToken Consume(JsonTokenType type)
{
var read = Read();
if (read == null)
{
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 _reader[Position];
}
public JsonToken? Read()
{
if (Eof)
{
return null;
}
Position++;
return _reader[Position - 1];
}
}

View File

@ -0,0 +1,15 @@
namespace Spectre.Console.Json;
internal enum JsonTokenType
{
LeftBrace,
RightBrace,
LeftBracket,
RightBracket,
Colon,
Comma,
String,
Number,
Boolean,
Null,
}

View File

@ -0,0 +1,205 @@
using System.Text;
namespace Spectre.Console.Json;
internal static class JsonTokenizer
{
private static readonly Dictionary<char, JsonTokenType> _typeLookup;
private static readonly Dictionary<string, JsonTokenType> _keywords;
private static readonly HashSet<char> _allowedEscapedChars;
static JsonTokenizer()
{
_typeLookup = new Dictionary<char, JsonTokenType>
{
{ '{', JsonTokenType.LeftBrace },
{ '}', JsonTokenType.RightBrace },
{ '[', JsonTokenType.LeftBracket },
{ ']', JsonTokenType.RightBracket },
{ ':', JsonTokenType.Colon },
{ ',', JsonTokenType.Comma },
};
_keywords = new Dictionary<string, JsonTokenType>
{
{ "true", JsonTokenType.Boolean },
{ "false", JsonTokenType.Boolean },
{ "null", JsonTokenType.Null },
};
_allowedEscapedChars = new HashSet<char>
{
'\"', '\\', '/', 'b', 'f', 'n', 'r', 't', 'u',
};
}
public static List<JsonToken> Tokenize(string text)
{
var result = new List<JsonToken>();
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);
}
}
}

View File

@ -0,0 +1,4 @@
global using System.Text;
global using Spectre.Console.Internal;
global using Spectre.Console.Json.Syntax;
global using Spectre.Console.Rendering;

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net7.0;net6.0;netstandard2.0</TargetFrameworks>
<ImplicitUsings>true</ImplicitUsings>
<IsPackable>true</IsPackable>
<Description>A library that extends Spectre.Console with JSON superpowers.</Description>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\Spectre.Console\Internal\Extensions\CharExtensions.cs" Link="Internal\CharExtensions.cs" />
<Compile Include="..\..\Spectre.Console\Internal\Extensions\EnumerableExtensions.cs" Link="Internal\EnumerableExtensions.cs" />
<Compile Include="..\..\Spectre.Console\Internal\Text\StringBuffer.cs" Link="Internal\StringBuffer.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Spectre.Console\Spectre.Console.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents an array in the JSON abstract syntax tree.
/// </summary>
public sealed class JsonArray : JsonSyntax
{
/// <summary>
/// Gets the array items.
/// </summary>
public List<JsonSyntax> Items { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonArray"/> class.
/// </summary>
public JsonArray()
{
Items = new List<JsonSyntax>();
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitArray(this, context);
}
}

View File

@ -0,0 +1,26 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents a boolean literal in the JSON abstract syntax tree.
/// </summary>
public sealed class JsonBoolean : JsonSyntax
{
/// <summary>
/// Gets the lexeme.
/// </summary>
public string Lexeme { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonBoolean"/> class.
/// </summary>
/// <param name="lexeme">The lexeme.</param>
public JsonBoolean(string lexeme)
{
Lexeme = lexeme;
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitBoolean(this, context);
}
}

View File

@ -0,0 +1,33 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents a member in the JSON abstract syntax tree.
/// </summary>
public sealed class JsonMember : JsonSyntax
{
/// <summary>
/// Gets the member name.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the member value.
/// </summary>
public JsonSyntax Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonMember"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public JsonMember(string name, JsonSyntax value)
{
Name = name;
Value = value;
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitMember(this, context);
}
}

View File

@ -0,0 +1,26 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents a null literal in the JSON abstract syntax tree.
/// </summary>
public sealed class JsonNull : JsonSyntax
{
/// <summary>
/// Gets the lexeme.
/// </summary>
public string Lexeme { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonNull"/> class.
/// </summary>
/// <param name="lexeme">The lexeme.</param>
public JsonNull(string lexeme)
{
Lexeme = lexeme;
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitNull(this, context);
}
}

View File

@ -0,0 +1,16 @@
namespace Spectre.Console.Json.Syntax;
internal sealed class JsonNumber : JsonSyntax
{
public string Lexeme { get; }
public JsonNumber(string lexeme)
{
Lexeme = lexeme;
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitNumber(this, context);
}
}

View File

@ -0,0 +1,25 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents an object in the JSON abstract syntax tree.
/// </summary>
public sealed class JsonObject : JsonSyntax
{
/// <summary>
/// Gets the object's members.
/// </summary>
public List<JsonMember> Members { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonObject"/> class.
/// </summary>
public JsonObject()
{
Members = new List<JsonMember>();
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitObject(this, context);
}
}

View File

@ -0,0 +1,26 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents a string literal in the JSON abstract syntax tree.
/// </summary>
public sealed class JsonString : JsonSyntax
{
/// <summary>
/// Gets the lexeme.
/// </summary>
public string Lexeme { get; }
/// <summary>
/// Initializes a new instance of the <see cref="JsonString"/> class.
/// </summary>
/// <param name="lexeme">The lexeme.</param>
public JsonString(string lexeme)
{
Lexeme = lexeme;
}
internal override void Accept<T>(JsonSyntaxVisitor<T> visitor, T context)
{
visitor.VisitString(this, context);
}
}

View File

@ -0,0 +1,9 @@
namespace Spectre.Console.Json.Syntax;
/// <summary>
/// Represents a syntax node in the JSON abstract syntax tree.
/// </summary>
public abstract class JsonSyntax
{
internal abstract void Accept<T>(JsonSyntaxVisitor<T> visitor, T context);
}

View File

@ -0,0 +1,12 @@
namespace Spectre.Console.Json.Syntax;
internal abstract class JsonSyntaxVisitor<T>
{
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);
}