< Summary

Information
Class: Allyaria.Theming.StyleTypes.StyleLength
Assembly: Allyaria.Theming
File(s): /home/runner/work/allyaria/allyaria/src/Allyaria.Theming/StyleTypes/StyleLength.cs
Line coverage
100%
Covered lines: 59
Uncovered lines: 0
Coverable lines: 59
Total lines: 214
Line coverage: 100%
Branch coverage
100%
Covered branches: 24
Total branches: 24
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%22100%
get_LengthUnit()100%11100%
get_Number()100%11100%
BuildUnitMap()100%44100%
Parse(...)100%22100%
TryMapUnit(...)100%22100%
TryNormalizeLength(...)100%1212100%
TryParse(...)100%11100%
op_Implicit(...)100%11100%
op_Implicit(...)100%22100%

File(s)

/home/runner/work/allyaria/allyaria/src/Allyaria.Theming/StyleTypes/StyleLength.cs

#LineLine coverage
 1namespace Allyaria.Theming.StyleTypes;
 2
 3/// <summary>
 4/// Represents a CSS length value within the Allyaria theming system. Supports parsing numeric values with optional unit
 5/// and exposes the normalized number and unit, while retaining the original string via <see cref="StyleValueBase.Value"
 6/// .
 7/// </summary>
 8public sealed record StyleLength : StyleValueBase
 9{
 10    /// <summary>
 11    /// Regular expression used to parse numeric length values with optional units from an input string. Captures a sign
 12    /// floating-point number and an optional alphabetic or percent unit token.
 13    /// </summary>
 114    private static readonly Regex LengthWithUnitRegex = new(
 115        pattern: @"^\s*(?<num>[+\-]?(?:\d+(?:\.\d+)?|\.\d+))\s*(?<unit>[A-Za-z%]+)?\s*$",
 116        options: RegexOptions.Compiled | RegexOptions.CultureInvariant,
 117        matchTimeout: TimeSpan.FromMilliseconds(value: 250)
 118    );
 19
 20    /// <summary>
 21    /// Lookup table mapping textual unit tokens (e.g., <c>px</c>, <c>em</c>, <c>%</c>) to <see cref="LengthUnits" /> va
 22    /// The dictionary is built once from the <see cref="LengthUnits" /> descriptions and reused for parsing.
 23    /// </summary>
 124    private static readonly Dictionary<string, LengthUnits> UnitByToken = BuildUnitMap();
 25
 26    /// <summary>
 27    /// Initializes a new instance of the <see cref="StyleLength" /> record from the specified string value.
 28    /// </summary>
 29    /// <param name="value">
 30    /// The raw CSS length value to parse, which may contain a numeric component and an optional unit token.
 31    /// </param>
 32    /// <exception cref="ArgumentException">
 33    /// Thrown when the provided <paramref name="value" /> is non-empty and cannot be parsed as a valid length.
 34    /// </exception>
 35    public StyleLength(string value)
 107736        : base(value: value)
 37    {
 107738        if (string.IsNullOrWhiteSpace(value: Value))
 39        {
 440            return;
 41        }
 42
 107343        AryGuard.Check(
 107344            condition: TryNormalizeLength(input: Value, number: out var number, unit: out var unit),
 107345            argName: nameof(value),
 107346            message: $"Invalid length: {Value}"
 107347        );
 48
 106749        LengthUnit = unit;
 106750        Number = number;
 106751    }
 52
 53    /// <summary>
 54    /// Gets the parsed length unit, if one was specified and successfully mapped; otherwise <see langword="null" />.
 55    /// </summary>
 1656    public LengthUnits? LengthUnit { get; }
 57
 58    /// <summary>
 59    /// Gets the parsed numeric value of the length when parsing succeeds; otherwise the default value <c>0.0</c>.
 60    /// </summary>
 1561    public double Number { get; }
 62
 63    /// <summary>
 64    /// Builds a mapping from unit description strings to their corresponding <see cref="LengthUnits" /> values. The
 65    /// descriptions are obtained from the <see cref="LengthUnits" /> enumeration via its metadata.
 66    /// </summary>
 67    /// <returns>
 68    /// A case-insensitive dictionary keyed by unit token (e.g., <c>"px"</c>) with <see cref="LengthUnits" /> values.
 69    /// </returns>
 70    private static Dictionary<string, LengthUnits> BuildUnitMap()
 71    {
 172        var dict = new Dictionary<string, LengthUnits>(comparer: StringComparer.OrdinalIgnoreCase);
 73
 5674        foreach (var unit in Enum.GetValues<LengthUnits>())
 75        {
 2776            var desc = unit.GetDescription();
 77
 2778            if (!string.IsNullOrWhiteSpace(value: desc))
 79            {
 2780                var key = desc.ToLowerInvariant();
 2781                dict.TryAdd(key: key, value: unit);
 82            }
 83        }
 84
 185        return dict;
 86    }
 87
 88    /// <summary>Parses the specified string into a <see cref="StyleLength" /> instance.</summary>
 89    /// <param name="value">The string to parse into a length value. If <see langword="null" />, an empty string is used
 90    /// <returns>A new <see cref="StyleLength" /> instance representing the parsed value.</returns>
 91    /// <exception cref="ArgumentException">
 92    /// Thrown when the provided <paramref name="value" /> cannot be parsed as a valid
 93    /// length.
 94    /// </exception>
 995    public static StyleLength Parse(string? value) => new(value: value ?? string.Empty);
 96
 97    /// <summary>Attempts to map a textual unit token to a <see cref="LengthUnits" /> value.</summary>
 98    /// <param name="token">The textual unit token to map (for example, <c>px</c>, <c>em</c>, or <c>%</c>).</param>
 99    /// <param name="unit">
 100    /// When this method returns, contains the mapped <see cref="LengthUnits" /> value if the token is recognized; other
 101    /// the default <see cref="LengthUnits" /> value.
 102    /// </param>
 103    /// <returns><see langword="true" /> if the token was successfully mapped; otherwise, <see langword="false" />.</ret
 104    private static bool TryMapUnit(string token, out LengthUnits unit)
 105    {
 921106        if (UnitByToken.TryGetValue(key: token.Trim().ToLowerInvariant(), value: out var unitParse))
 107        {
 919108            unit = unitParse;
 109
 919110            return true;
 111        }
 112
 2113        unit = default(LengthUnits);
 114
 2115        return false;
 116    }
 117
 118    /// <summary>
 119    /// Attempts to normalize an input length string into a numeric value and optional <see cref="LengthUnits" /> value.
 120    /// </summary>
 121    /// <param name="input">The input string containing a numeric length value and optional unit token.</param>
 122    /// <param name="number">
 123    /// When this method returns, contains the parsed numeric value if parsing succeeds; otherwise <c>0.0</c>.
 124    /// </param>
 125    /// <param name="unit">
 126    /// When this method returns, contains the parsed <see cref="LengthUnits" /> value if one was specified and recogniz
 127    /// otherwise <see langword="null" />.
 128    /// </param>
 129    /// <returns>
 130    /// <see langword="true" /> if the input could be successfully parsed into a number (and optional unit); otherwise,
 131    /// <see langword="false" />.
 132    /// </returns>
 133    private static bool TryNormalizeLength(string input, out double number, out LengthUnits? unit)
 134    {
 1073135        number = 0.0;
 1073136        unit = null;
 137
 1073138        var match = LengthWithUnitRegex.Match(input: input);
 139
 1073140        if (!match.Success)
 141        {
 3142            return false;
 143        }
 144
 1070145        var numText = match.Groups[groupname: "num"].Value;
 146
 1070147        var unitText = match.Groups[groupname: "unit"].Success
 1070148            ? match.Groups[groupname: "unit"].Value
 1070149            : null;
 150
 1070151        if (!double.TryParse(
 1070152            s: numText, style: NumberStyles.Float, provider: CultureInfo.InvariantCulture, result: out number
 1070153        ) || double.IsInfinity(d: number))
 154        {
 1155            return false;
 156        }
 157
 1069158        unit = null;
 159
 1069160        if (!string.IsNullOrEmpty(value: unitText))
 161        {
 921162            if (!TryMapUnit(token: unitText, unit: out var lengthUnit))
 163            {
 2164                return false;
 165            }
 166
 919167            unit = lengthUnit;
 168        }
 169
 1067170        return true;
 171    }
 172
 173    /// <summary>Attempts to parse the specified string into a <see cref="StyleLength" /> instance.</summary>
 174    /// <param name="value">The string to parse into a length value.</param>
 175    /// <param name="result">
 176    /// When this method returns, contains the parsed <see cref="StyleLength" /> instance or <see langword="null" /> if 
 177    /// failed.
 178    /// </param>
 179    /// <returns><see langword="true" /> if the value was successfully parsed; otherwise, <see langword="false" />.</ret
 180    public static bool TryParse(string? value, out StyleLength? result)
 181    {
 182        try
 183        {
 4184            result = Parse(value: value);
 185
 2186            return true;
 187        }
 2188        catch
 189        {
 2190            result = null;
 191
 2192            return false;
 193        }
 4194    }
 195
 196    /// <summary>Implicitly converts a string into a <see cref="StyleLength" /> instance.</summary>
 197    /// <param name="value">
 198    /// The string representation of the length value to convert. If <see langword="null" />, an empty string is used.
 199    /// </param>
 200    /// <returns>A <see cref="StyleLength" /> instance representing the provided value.</returns>
 201    /// <exception cref="ArgumentException">
 202    /// Thrown when the provided <paramref name="value" /> cannot be parsed as a valid
 203    /// length.
 204    /// </exception>
 2205    public static implicit operator StyleLength(string? value) => Parse(value: value);
 206
 207    /// <summary>Implicitly converts a <see cref="StyleLength" /> instance to its underlying string representation.</sum
 208    /// <param name="value">The <see cref="StyleLength" /> instance to convert.</param>
 209    /// <returns>
 210    /// The original CSS length string stored in <see cref="StyleValueBase.Value" />, or an empty string if
 211    /// <paramref name="value" /> is <see langword="null" />.
 212    /// </returns>
 2213    public static implicit operator string(StyleLength? value) => (value?.Value).OrDefaultIfEmpty();
 214}