mirror of
https://github.com/nsnail/spectre.console.git
synced 2025-04-24 12:12:51 +08:00
226 lines
7.2 KiB
C#
226 lines
7.2 KiB
C#
namespace Spectre.Console;
|
|
|
|
internal static class StyleParser
|
|
{
|
|
public static Style Parse(string text)
|
|
{
|
|
var style = Parse(text, out var error);
|
|
if (error != null)
|
|
{
|
|
throw new InvalidOperationException(error);
|
|
}
|
|
|
|
if (style == null)
|
|
{
|
|
// This should not happen, but we need to please the compiler
|
|
// which cannot know that style isn't null here.
|
|
throw new InvalidOperationException("Could not parse style.");
|
|
}
|
|
|
|
return style;
|
|
}
|
|
|
|
public static bool TryParse(string text, out Style? style)
|
|
{
|
|
style = Parse(text, out var error);
|
|
return error == null;
|
|
}
|
|
|
|
private static Style? Parse(string text, out string? error)
|
|
{
|
|
var effectiveDecoration = (Decoration?)null;
|
|
var effectiveForeground = (Color?)null;
|
|
var effectiveBackground = (Color?)null;
|
|
var effectiveLink = (string?)null;
|
|
|
|
var parts = text.Split(new[] { ' ' });
|
|
var foreground = true;
|
|
foreach (var part in parts)
|
|
{
|
|
if (part.Equals("default", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (part.Equals("on", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
foreground = false;
|
|
continue;
|
|
}
|
|
|
|
if (part.StartsWith("link=", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (effectiveLink != null)
|
|
{
|
|
error = "A link has already been set.";
|
|
return null;
|
|
}
|
|
|
|
effectiveLink = part.Substring(5);
|
|
continue;
|
|
}
|
|
else if (part.StartsWith("link", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
effectiveLink = Constants.EmptyLink;
|
|
continue;
|
|
}
|
|
|
|
var decoration = DecorationTable.GetDecoration(part);
|
|
if (decoration != null)
|
|
{
|
|
effectiveDecoration ??= Decoration.None;
|
|
|
|
effectiveDecoration |= decoration.Value;
|
|
}
|
|
else
|
|
{
|
|
var color = ColorTable.GetColor(part);
|
|
if (color == null)
|
|
{
|
|
if (part.StartsWith("#", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
color = ParseHexColor(part, out error);
|
|
if (!string.IsNullOrWhiteSpace(error))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
else if (part.StartsWith("rgb", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
color = ParseRgbColor(part, out error);
|
|
if (!string.IsNullOrWhiteSpace(error))
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
else if (int.TryParse(part, out var number))
|
|
{
|
|
if (number < 0)
|
|
{
|
|
error = $"Color number must be greater than or equal to 0 (was {number})";
|
|
return null;
|
|
}
|
|
else if (number > 255)
|
|
{
|
|
error = $"Color number must be less than or equal to 255 (was {number})";
|
|
return null;
|
|
}
|
|
|
|
color = number;
|
|
}
|
|
else
|
|
{
|
|
error = !foreground
|
|
? $"Could not find color '{part}'."
|
|
: $"Could not find color or style '{part}'.";
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (foreground)
|
|
{
|
|
if (effectiveForeground != null)
|
|
{
|
|
error = "A foreground color has already been set.";
|
|
return null;
|
|
}
|
|
|
|
effectiveForeground = color;
|
|
}
|
|
else
|
|
{
|
|
if (effectiveBackground != null)
|
|
{
|
|
error = "A background color has already been set.";
|
|
return null;
|
|
}
|
|
|
|
effectiveBackground = color;
|
|
}
|
|
}
|
|
}
|
|
|
|
error = null;
|
|
return new Style(
|
|
effectiveForeground,
|
|
effectiveBackground,
|
|
effectiveDecoration,
|
|
effectiveLink);
|
|
}
|
|
|
|
private static Color? ParseHexColor(string hex, out string? error)
|
|
{
|
|
error = null;
|
|
|
|
hex ??= string.Empty;
|
|
hex = hex.ReplaceExact("#", string.Empty).Trim();
|
|
|
|
try
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(hex))
|
|
{
|
|
if (hex.Length == 6)
|
|
{
|
|
return new Color(
|
|
(byte)Convert.ToUInt32(hex.Substring(0, 2), 16),
|
|
(byte)Convert.ToUInt32(hex.Substring(2, 2), 16),
|
|
(byte)Convert.ToUInt32(hex.Substring(4, 2), 16));
|
|
}
|
|
else if (hex.Length == 3)
|
|
{
|
|
return new Color(
|
|
(byte)Convert.ToUInt32(new string(hex[0], 2), 16),
|
|
(byte)Convert.ToUInt32(new string(hex[1], 2), 16),
|
|
(byte)Convert.ToUInt32(new string(hex[2], 2), 16));
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Invalid hex color '#{hex}'. {ex.Message}";
|
|
return null;
|
|
}
|
|
|
|
error = $"Invalid hex color '#{hex}'.";
|
|
return null;
|
|
}
|
|
|
|
private static Color? ParseRgbColor(string rgb, out string? error)
|
|
{
|
|
try
|
|
{
|
|
error = null;
|
|
|
|
var normalized = rgb ?? string.Empty;
|
|
if (normalized.Length >= 3)
|
|
{
|
|
// Trim parentheses
|
|
normalized = normalized.Substring(3).Trim();
|
|
|
|
if (normalized.StartsWith("(", StringComparison.OrdinalIgnoreCase) &&
|
|
normalized.EndsWith(")", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
normalized = normalized.Trim('(').Trim(')');
|
|
|
|
var parts = normalized.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length == 3)
|
|
{
|
|
return new Color(
|
|
(byte)Convert.ToInt32(parts[0], CultureInfo.InvariantCulture),
|
|
(byte)Convert.ToInt32(parts[1], CultureInfo.InvariantCulture),
|
|
(byte)Convert.ToInt32(parts[2], CultureInfo.InvariantCulture));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error = $"Invalid RGB color '{rgb}'. {ex.Message}";
|
|
return null;
|
|
}
|
|
|
|
error = $"Invalid RGB color '{rgb}'.";
|
|
return null;
|
|
}
|
|
} |