mirror of
https://github.com/spectreconsole/spectre.console.git
synced 2026-02-10 04:13:32 +01:00
* Move extension methods closer to implementations * Fix namespaces * Make structs read-only where applicable * Use ArgumentNullException.ThrowIfNull * Use collection expressions
203 lines
5.5 KiB
C#
203 lines
5.5 KiB
C#
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 =
|
|
[
|
|
'\"', '\\', '/', '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);
|
|
}
|
|
}
|
|
} |