MultiStandardColorNameResolver.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. #nullable enable
  2. using System.Collections.Frozen;
  3. using System.Collections.Immutable;
  4. using System.Diagnostics.CodeAnalysis;
  5. namespace Terminal.Gui;
  6. /// <summary>
  7. /// Backwards compatible(-ish) color name resolver prioritizing ANSI 4-bit (16) colors with fallback to W3C colors.
  8. /// </summary>
  9. public class MultiStandardColorNameResolver : IColorNameResolver
  10. {
  11. private static readonly AnsiColorNameResolver Ansi = new();
  12. private static readonly W3cColorNameResolver W3c = new();
  13. private static readonly FrozenSet<Color> W3cBlockedColors;
  14. private static readonly ImmutableArray<string> CombinedColorNames;
  15. private static readonly FrozenDictionary<int, (string Name, Color Color)> W3cSubstituteColors;
  16. static MultiStandardColorNameResolver ()
  17. {
  18. HashSet<string> combinedNames = new(Ansi.GetColorNames());
  19. HashSet<Color> w3cInconsistentColors = new();
  20. Dictionary<string, Color> w3cSubstituteColors = new(StringComparer.OrdinalIgnoreCase);
  21. IEnumerable<string> enumerableW3cNames = W3c.GetColorNames ();
  22. IReadOnlyList<string> w3cNames = enumerableW3cNames is IReadOnlyList<string> alreadyReadOnlyList
  23. ? alreadyReadOnlyList
  24. : [.. enumerableW3cNames];
  25. Dictionary<Color, HashSet<string>> w3cColorsWithAlternativeNames = w3cNames
  26. .GroupBy(w3cName =>
  27. {
  28. if (!W3c.TryParseColor(w3cName, out Color w3cColor))
  29. {
  30. throw new InvalidOperationException ($"W3C color name '{w3cName}' does not resolve to any W3C color.");
  31. }
  32. return w3cColor;
  33. })
  34. .Where(g => g.Count() > 1)
  35. .ToDictionary(g => g.Key, g => g.ToHashSet());
  36. // Gather inconsistencies between ANSI and W3C, filter out or substitute problematic W3C colors and names,
  37. // and create additional blocklist for W3C colors.
  38. // Blocking and filtering is only applied to W3C because this resolver prioritizes ANSI for backwards compatibility.
  39. // It would be a lot simpler to just prioritize W3C colors and names.
  40. foreach (string w3cName in w3cNames)
  41. {
  42. if (w3cSubstituteColors.ContainsKey (w3cName))
  43. {
  44. // Already dealt with alternative name.
  45. continue;
  46. }
  47. if (!W3c.TryParseColor (w3cName, out Color w3cColor))
  48. {
  49. // This condition is just inverted to reduce indentation.
  50. // Also it should practically never happen if the W3C color name resolver is properly implemented.
  51. throw new InvalidOperationException ($"W3C color name '{w3cName}' does not resolve to any color.");
  52. }
  53. if (w3cColorsWithAlternativeNames.TryGetValue (w3cColor, out var names))
  54. {
  55. bool substituted = false;
  56. // Alternative names cause issues with ColorPicker etc. when combined with ANSI and prioritizing ANSI resolver.
  57. // For example Aqua is not in ColorName16 but the actual color value resolves to ANSI Cyan
  58. // so autocomplete for Aqua suddenly changes to Cyan because they happen to have same color value in both color scheme.
  59. // Also DarkGrey would cause inconsistencies because the alternative DarkGray exists in ANSI and has different color value.
  60. foreach (string name in names)
  61. {
  62. if (Ansi.TryParseColor (name, out Color substituteColor))
  63. {
  64. // Block the W3C color when it is inconsistent with the substitute color
  65. // so there is no situation where W3C color -> color name -> ANSI color.
  66. if (w3cColor != substituteColor)
  67. {
  68. w3cInconsistentColors.Add (w3cColor);
  69. }
  70. // Substitute all W3C alternatives to match with the ANSI color to keep colors consistent.
  71. foreach (string alternativeName in names)
  72. {
  73. w3cSubstituteColors.Add (alternativeName, substituteColor);
  74. combinedNames.Add (alternativeName);
  75. }
  76. substituted = true;
  77. break;
  78. }
  79. }
  80. if (substituted)
  81. {
  82. // Already dealt with, continue to next W3C color name.
  83. continue;
  84. }
  85. }
  86. // Same name, different ANSI value.
  87. // For example both #767676 (ColorName16) and #A9A9A9 (W3C) resolve to DarkGray,
  88. // although a bad example because it is already substituted due to also having alternative names.
  89. if (Ansi.TryParseColor (w3cName, out Color ansiColor) && w3cColor != ansiColor)
  90. {
  91. w3cInconsistentColors.Add (w3cColor);
  92. continue;
  93. }
  94. combinedNames.Add (w3cName);
  95. }
  96. // TODO: Utilize .NET 9 and later alternative lookup for matching ReadOnlySpan<char> with string.
  97. W3cSubstituteColors = w3cSubstituteColors.ToFrozenDictionary (
  98. // Workaround for alternative lookup not being available in .NET 8 by matching ReadOnlySpan<char> hash code to string hash code.
  99. keySelector: kvp => string.GetHashCode (kvp.Key, StringComparison.OrdinalIgnoreCase),
  100. // The string element is for detecting hash collision.
  101. elementSelector: kvp => (kvp.Key, kvp.Value));
  102. W3cBlockedColors = w3cInconsistentColors.ToFrozenSet ();
  103. CombinedColorNames = combinedNames.Order ().ToImmutableArray ();
  104. }
  105. /// <inheritdoc/>
  106. public IEnumerable<string> GetColorNames ()
  107. {
  108. return CombinedColorNames;
  109. }
  110. /// <inheritdoc/>
  111. public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name)
  112. {
  113. if (Ansi.TryNameColor (color, out string? ansiName))
  114. {
  115. name = ansiName;
  116. return true;
  117. }
  118. if (!IsBlockedW3cColor (color) &&
  119. W3c.TryNameColor (color, out string? w3cName))
  120. {
  121. name = w3cName;
  122. return true;
  123. }
  124. name = null;
  125. return false;
  126. }
  127. /// <inheritdoc/>
  128. public bool TryParseColor (ReadOnlySpan<char> name, out Color color)
  129. {
  130. if (Ansi.TryParseColor (name, out color))
  131. {
  132. return true;
  133. }
  134. if (GetSubstituteW3cColor (name, out color))
  135. {
  136. return true;
  137. }
  138. if (W3c.TryParseColor (name, out color) &&
  139. !IsBlockedW3cColor (color))
  140. {
  141. return true;
  142. }
  143. color = default;
  144. return false;
  145. }
  146. private static bool GetSubstituteW3cColor (ReadOnlySpan<char> name, out Color substituteColor)
  147. {
  148. int nameHashCode = string.GetHashCode(name, StringComparison.OrdinalIgnoreCase);
  149. if (W3cSubstituteColors.TryGetValue (nameHashCode, out var match) &&
  150. match is (string matchName, Color matchColor) &&
  151. name.Equals (matchName, StringComparison.OrdinalIgnoreCase))
  152. {
  153. substituteColor = matchColor;
  154. return true;
  155. }
  156. substituteColor = default;
  157. return false;
  158. }
  159. private static bool IsBlockedW3cColor (Color color)
  160. {
  161. return W3cBlockedColors.Contains (color);
  162. }
  163. }