iOfficeAI/OfficeCLI · error · ArgumentException

Invalid color value: '{original}'. HSL saturation/lightness

Error message

Invalid color value: '{original}'. HSL saturation/lightness must be expressed as a percentage (e.g. 50%).

What it means

Thrown by ParsePercent01 when an HSL saturation or lightness token does not end with '%'. The library deliberately requires the percent sign for HSL S/L so that the meaning is unambiguous (CSS requires it too). A bare fraction like 0.5 is rejected even though it is numerically valid.

Source

Thrown at src/officecli/Core/ParseHelpers.cs:210

    {
        token = token.Trim();
        // Strip an optional `deg` suffix (CSS allows it; other angle units
        // — turn/rad/grad — are out of scope for the input vocabulary we care
        // about and would just need their own multipliers if asked for).
        if (token.EndsWith("deg", StringComparison.OrdinalIgnoreCase))
            token = token[..^3].Trim();
        if (!double.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out var h))
            throw new ArgumentException($"Invalid color value: '{original}'. Hue must be a number in degrees.");
        h %= 360;
        if (h < 0) h += 360;
        return h;
    }

    private static double ParsePercent01(string token, string original)
    {
        token = token.Trim();
        if (!token.EndsWith('%'))
            throw new ArgumentException($"Invalid color value: '{original}'. HSL saturation/lightness must be expressed as a percentage (e.g. 50%).");
        if (!double.TryParse(token[..^1], NumberStyles.Float, CultureInfo.InvariantCulture, out var pct)
            || pct < 0 || pct > 100)
            throw new ArgumentException($"Invalid color value: '{original}'. HSL saturation/lightness must be 0%-100%.");
        return pct / 100.0;
    }

    private static (byte R, byte G, byte B) HslToRgb(double h, double s, double l)
    {
        // Standard HSL → RGB (CSS Color Module Level 3).
        double c = (1 - Math.Abs(2 * l - 1)) * s;
        double hp = h / 60.0;
        double x = c * (1 - Math.Abs(hp % 2 - 1));
        double r1 = 0, g1 = 0, b1 = 0;
        if (hp < 1)      { r1 = c; g1 = x; b1 = 0; }
        else if (hp < 2) { r1 = x; g1 = c; b1 = 0; }
        else if (hp < 3) { r1 = 0; g1 = c; b1 = x; }
        else if (hp < 4) { r1 = 0; g1 = x; b1 = c; }
        else if (hp < 5) { r1 = x; g1 = 0; b1 = c; }

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Always append % to saturation and lightness: hsl(120,50%,50%).
  2. When interpolating, format with the suffix: $"hsl({h},{s}%,{l}%".
  3. If you hold values as 0-1 fractions, multiply by 100 and add %.

Example fix

// before
var c = $"hsl({h},{s},{l}")); // s,l are fractions 0..1
ParseColor(c); // throws 289

// after
var c = $"hsl({h},{s*100}%,{l*100}%");
ParseColor(c);
Defensive patterns

Strategy: validation

Validate before calling

// ensure S/L always carry '%'
static string Pct(double f01) => $"{Math.Clamp(f01,0,1)*100}%";
// build: $"hsl({h},{Pct(s)},{Pct(l)})"

Type guard

static bool IsPercentToken(string t) => t.Trim().EndsWith('%');

Try / catch

try { color = ParseColor(spec); }
catch (ArgumentException ex) when (ex.Message.Contains("expressed as a percentage"))
{ /* append % to S/L tokens, retry */ }

Prevention

When it happens

Trigger: Passing hsl(120,0.5,0.5) (omitting %); passing hsl(120,50,50) (treating S/L as 0-100 integers without a suffix).

Common situations: Programmers used to 0-1 fraction notation; building the hsl string from numeric variables without appending %; copy-paste from a source that used the fraction form.

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/71e511f90e1f808b. Report an issue: GitHub.