| 1 | #nullable enable |
| 2 | |
| 3 | using System.Globalization; |
| 4 | using CascadeIDE.Services; |
| 5 | using CascadeIDE.Services.Intercom; |
| 6 | |
| 7 | namespace CascadeIDE.Features.Chat.AnchorPeek; |
| 8 | |
| 9 | internal static class AnchorPeekTargetParser |
| 10 | { |
| 11 | public const string EmptyTargetError = |
| 12 | "Укажи № якоря (1…) или 8 hex: /anchor peek 1 или /anchor peek abcd1234."; |
| 13 | |
| 14 | public const string InvalidHexError = |
| 15 | "Id якоря — 8 hex-символов (как в маркере ⟦a:abcd1234⟧)."; |
| 16 | |
| 17 | public static bool TryParse(string? raw, out AnchorPeekTarget target, out string error) |
| 18 | { |
| 19 | target = default; |
| 20 | error = ""; |
| 21 | var t = (raw ?? "").Trim(); |
| 22 | if (t.Length == 0) |
| 23 | { |
| 24 | error = EmptyTargetError; |
| 25 | return false; |
| 26 | } |
| 27 | |
| 28 | if (TryParseOrdinalToken(t, out var ordinal)) |
| 29 | { |
| 30 | target = AnchorPeekTarget.FromOrdinal(ordinal); |
| 31 | return true; |
| 32 | } |
| 33 | |
| 34 | if (TryParseHexToken(t, out var hexId, out error)) |
| 35 | { |
| 36 | target = AnchorPeekTarget.FromHexId(hexId); |
| 37 | return true; |
| 38 | } |
| 39 | |
| 40 | return false; |
| 41 | } |
| 42 | |
| 43 | public static bool IsPartialHex(string raw) |
| 44 | { |
| 45 | var t = raw; |
| 46 | if (t.StartsWith("a:", StringComparison.OrdinalIgnoreCase)) |
| 47 | t = t[2..].Trim(); |
| 48 | |
| 49 | return t.Length > 0 |
| 50 | && t.Length < 8 |
| 51 | && t.All(static c => c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F'); |
| 52 | } |
| 53 | |
| 54 | public static bool LooksLikeHexEntry(string token) |
| 55 | { |
| 56 | if (token.StartsWith("a:", StringComparison.OrdinalIgnoreCase)) |
| 57 | return true; |
| 58 | |
| 59 | if (token.Any(static c => c is >= 'a' and <= 'f' or >= 'A' and <= 'F')) |
| 60 | return true; |
| 61 | |
| 62 | return token.Length >= 8 |
| 63 | && token.All(static c => c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F'); |
| 64 | } |
| 65 | |
| 66 | private static bool TryParseOrdinalToken(string t, out int ordinal) |
| 67 | { |
| 68 | ordinal = 0; |
| 69 | if (t.StartsWith("a:", StringComparison.OrdinalIgnoreCase)) |
| 70 | return false; |
| 71 | |
| 72 | if (!t.All(static c => c is >= '0' and <= '9')) |
| 73 | return false; |
| 74 | |
| 75 | return int.TryParse(t, NumberStyles.None, CultureInfo.InvariantCulture, out ordinal) && ordinal >= 1; |
| 76 | } |
| 77 | |
| 78 | private static bool TryParseHexToken(string t, out string hexId, out string error) |
| 79 | { |
| 80 | hexId = ""; |
| 81 | error = ""; |
| 82 | if (t.StartsWith("a:", StringComparison.OrdinalIgnoreCase)) |
| 83 | t = t[2..].Trim(); |
| 84 | |
| 85 | var wirePrefix = $"{IntercomAttachmentMarkers.MarkerOpen}a:"; |
| 86 | if (t.StartsWith(wirePrefix, StringComparison.Ordinal)) |
| 87 | { |
| 88 | var end = t.IndexOf(IntercomAttachmentMarkers.MarkerClose); |
| 89 | t = end >= wirePrefix.Length ? t[wirePrefix.Length..end] : t[wirePrefix.Length..]; |
| 90 | } |
| 91 | |
| 92 | if (t.Length != 8 || !t.All(static c => c is >= '0' and <= '9' or >= 'a' and <= 'f' or >= 'A' and <= 'F')) |
| 93 | { |
| 94 | error = InvalidHexError; |
| 95 | return false; |
| 96 | } |
| 97 | |
| 98 | hexId = t.ToLowerInvariant(); |
| 99 | return true; |
| 100 | } |
| 101 | } |
| 102 | |