| | | 1 | | namespace 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> |
| | | 8 | | public 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> |
| | 1 | 14 | | private static readonly Regex LengthWithUnitRegex = new( |
| | 1 | 15 | | pattern: @"^\s*(?<num>[+\-]?(?:\d+(?:\.\d+)?|\.\d+))\s*(?<unit>[A-Za-z%]+)?\s*$", |
| | 1 | 16 | | options: RegexOptions.Compiled | RegexOptions.CultureInvariant, |
| | 1 | 17 | | matchTimeout: TimeSpan.FromMilliseconds(value: 250) |
| | 1 | 18 | | ); |
| | | 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> |
| | 1 | 24 | | 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) |
| | 1077 | 36 | | : base(value: value) |
| | | 37 | | { |
| | 1077 | 38 | | if (string.IsNullOrWhiteSpace(value: Value)) |
| | | 39 | | { |
| | 4 | 40 | | return; |
| | | 41 | | } |
| | | 42 | | |
| | 1073 | 43 | | AryGuard.Check( |
| | 1073 | 44 | | condition: TryNormalizeLength(input: Value, number: out var number, unit: out var unit), |
| | 1073 | 45 | | argName: nameof(value), |
| | 1073 | 46 | | message: $"Invalid length: {Value}" |
| | 1073 | 47 | | ); |
| | | 48 | | |
| | 1067 | 49 | | LengthUnit = unit; |
| | 1067 | 50 | | Number = number; |
| | 1067 | 51 | | } |
| | | 52 | | |
| | | 53 | | /// <summary> |
| | | 54 | | /// Gets the parsed length unit, if one was specified and successfully mapped; otherwise <see langword="null" />. |
| | | 55 | | /// </summary> |
| | 16 | 56 | | 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> |
| | 15 | 61 | | 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 | | { |
| | 1 | 72 | | var dict = new Dictionary<string, LengthUnits>(comparer: StringComparer.OrdinalIgnoreCase); |
| | | 73 | | |
| | 56 | 74 | | foreach (var unit in Enum.GetValues<LengthUnits>()) |
| | | 75 | | { |
| | 27 | 76 | | var desc = unit.GetDescription(); |
| | | 77 | | |
| | 27 | 78 | | if (!string.IsNullOrWhiteSpace(value: desc)) |
| | | 79 | | { |
| | 27 | 80 | | var key = desc.ToLowerInvariant(); |
| | 27 | 81 | | dict.TryAdd(key: key, value: unit); |
| | | 82 | | } |
| | | 83 | | } |
| | | 84 | | |
| | 1 | 85 | | 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> |
| | 9 | 95 | | 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 | | { |
| | 921 | 106 | | if (UnitByToken.TryGetValue(key: token.Trim().ToLowerInvariant(), value: out var unitParse)) |
| | | 107 | | { |
| | 919 | 108 | | unit = unitParse; |
| | | 109 | | |
| | 919 | 110 | | return true; |
| | | 111 | | } |
| | | 112 | | |
| | 2 | 113 | | unit = default(LengthUnits); |
| | | 114 | | |
| | 2 | 115 | | 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 | | { |
| | 1073 | 135 | | number = 0.0; |
| | 1073 | 136 | | unit = null; |
| | | 137 | | |
| | 1073 | 138 | | var match = LengthWithUnitRegex.Match(input: input); |
| | | 139 | | |
| | 1073 | 140 | | if (!match.Success) |
| | | 141 | | { |
| | 3 | 142 | | return false; |
| | | 143 | | } |
| | | 144 | | |
| | 1070 | 145 | | var numText = match.Groups[groupname: "num"].Value; |
| | | 146 | | |
| | 1070 | 147 | | var unitText = match.Groups[groupname: "unit"].Success |
| | 1070 | 148 | | ? match.Groups[groupname: "unit"].Value |
| | 1070 | 149 | | : null; |
| | | 150 | | |
| | 1070 | 151 | | if (!double.TryParse( |
| | 1070 | 152 | | s: numText, style: NumberStyles.Float, provider: CultureInfo.InvariantCulture, result: out number |
| | 1070 | 153 | | ) || double.IsInfinity(d: number)) |
| | | 154 | | { |
| | 1 | 155 | | return false; |
| | | 156 | | } |
| | | 157 | | |
| | 1069 | 158 | | unit = null; |
| | | 159 | | |
| | 1069 | 160 | | if (!string.IsNullOrEmpty(value: unitText)) |
| | | 161 | | { |
| | 921 | 162 | | if (!TryMapUnit(token: unitText, unit: out var lengthUnit)) |
| | | 163 | | { |
| | 2 | 164 | | return false; |
| | | 165 | | } |
| | | 166 | | |
| | 919 | 167 | | unit = lengthUnit; |
| | | 168 | | } |
| | | 169 | | |
| | 1067 | 170 | | 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 | | { |
| | 4 | 184 | | result = Parse(value: value); |
| | | 185 | | |
| | 2 | 186 | | return true; |
| | | 187 | | } |
| | 2 | 188 | | catch |
| | | 189 | | { |
| | 2 | 190 | | result = null; |
| | | 191 | | |
| | 2 | 192 | | return false; |
| | | 193 | | } |
| | 4 | 194 | | } |
| | | 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> |
| | 2 | 205 | | 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> |
| | 2 | 213 | | public static implicit operator string(StyleLength? value) => (value?.Value).OrDefaultIfEmpty(); |
| | | 214 | | } |