From b1b6402827100c27e03485cc3a6c72f9bba03d8f Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Mon, 11 May 2026 08:11:30 +0200 Subject: [PATCH] docs: Fix the last comments i think now --- HellionChat/Code/ChatSource.cs | 22 +++++----- HellionChat/GameFunctions/Chat.cs | 39 ++++------------- HellionChat/Ui/Settings.cs | 9 ++-- HellionChat/Util/AutoTranslate.cs | 37 +++++----------- HellionChat/Util/GlobalParametersCache.cs | 12 ++---- HellionChat/Util/IconUtil.cs | 46 ++++---------------- HellionChat/Util/MathUtil.cs | 12 +----- HellionChat/Util/Payloads.cs | 51 +++++------------------ HellionChat/Util/TabsUtil.cs | 27 ++++-------- HellionChat/Util/Tokenizer.cs | 14 +------ 10 files changed, 70 insertions(+), 199 deletions(-) diff --git a/HellionChat/Code/ChatSource.cs b/HellionChat/Code/ChatSource.cs index 73e9701..3aa0d48 100755 --- a/HellionChat/Code/ChatSource.cs +++ b/HellionChat/Code/ChatSource.cs @@ -7,36 +7,36 @@ public enum ChatSource : ushort { None = 0, - /// The player currently controlled by the local client. + // The player controlled by this client LocalPlayer = 1 << XivChatRelationKind.LocalPlayer, - /// A player in the same 4-man or 8-man party as the local player. + // Member of the local party PartyMember = 1 << XivChatRelationKind.PartyMember, - /// A player in the same alliance raid. + // Member of the alliance AllianceMember = 1 << XivChatRelationKind.AllianceMember, - /// A player not in the local player's party or alliance. + // Other player OtherPlayer = 1 << XivChatRelationKind.OtherPlayer, - /// An enemy entity that is currently in combat with the player or party. + // Enemy in combat EngagedEnemy = 1 << XivChatRelationKind.EngagedEnemy, - /// An enemy entity that is not yet in combat or claimed. + // Enemy out of combat UnengagedEnemy = 1 << XivChatRelationKind.UnengagedEnemy, - /// An NPC that is friendly or neutral to the player (e.g., EventNPCs). + // Friendly NPC FriendlyNpc = 1 << XivChatRelationKind.FriendlyNpc, - /// A pet (Summoner/Scholar) or companion (Chocobo) belonging to the local player. + // Own pet or companion PetOrCompanion = 1 << XivChatRelationKind.PetOrCompanion, - /// A pet or companion belonging to a member of the local player's party. + // Pet or companion of party members PetOrCompanionParty = 1 << XivChatRelationKind.PetOrCompanionParty, - /// A pet or companion belonging to a member of the alliance. + // Pet or companion of alliance members PetOrCompanionAlliance = 1 << XivChatRelationKind.PetOrCompanionAlliance, - /// A pet or companion belonging to a player not in the party or alliance. + // Pet or companion of other players PetOrCompanionOther = 1 << XivChatRelationKind.PetOrCompanionOther, } diff --git a/HellionChat/GameFunctions/Chat.cs b/HellionChat/GameFunctions/Chat.cs index 35e328d..b4a0396 100755 --- a/HellionChat/GameFunctions/Chat.cs +++ b/HellionChat/GameFunctions/Chat.cs @@ -174,8 +174,7 @@ internal sealed unsafe class Chat : IDisposable internal static void RotateCrossLinkshellHistory(RotateMode mode) => UIModule.Instance()->RotateCrossLinkshellHistory(GetRotateIdx(mode)); - // This function looks up a channel's user-defined color. - // If this function ever returns 0, it returns null instead. + // Look up a channel's user-defined color, returns null if 0 internal uint? GetChannelColor(ChatType type) { var parent = type.Parent(); @@ -215,8 +214,7 @@ internal sealed unsafe class Chat : IDisposable if (Plugin.Functions.KeybindManager.DirectChat && LastTypedCharacter != null) { - // FIXME: this whole system sucks - // FIXME v2: I hate everything about this, but it works + // Capture the just-typed character input Plugin.Framework.RunOnTick(() => { string? input = null; @@ -255,13 +253,9 @@ internal sealed unsafe class Chat : IDisposable try { - // We already called this function once, so we skip the duplicated call - // Also return the original value here so that vanilla chat receives all information + // Prevent duplicate calls if (Plugin.ChatLogWindow.TellSpecial) - { - Plugin.Log.Information("Return early to prevent duplicated call..."); return ChatLogRefreshHook!.Original(log, eventId, value); - } Plugin.ChatLogWindow.Activated( new ChatActivatedArgs(new ChannelSwitchInfo(null)) @@ -275,8 +269,7 @@ internal sealed unsafe class Chat : IDisposable Plugin.Log.Error(ex, "Error in chat Activated event"); } - // prevent the game from focusing the chat log - return 1; + return 1; // Prevent vanilla chat log from gaining focus } private CStringPointer ChangeChannelNameDetour(AgentChatLog* agent) @@ -430,10 +423,7 @@ internal sealed unsafe class Chat : IDisposable ); } - /// - /// Returns true if the channel is any non-linkshell channel, or if the - /// linkshell actually exists. - /// + // Check if channel is valid (non-linkshell or existing linkshell) internal static bool ValidAnyLinkshell(InputChannel channel) { var idx = channel.LinkshellIndex(); @@ -477,8 +467,7 @@ internal sealed unsafe class Chat : IDisposable _ => 1, }; - // Iterate up to 8 times to find a valid linkshell. - for (var i = 0; i < 8; i++) + for (var i = 0; i < 8; i++) // Find valid linkshell within 8 iterations { currentIndex = (uint)((8 + currentIndex + delta) % 8); if (validFn(currentIndex)) @@ -524,7 +513,7 @@ internal sealed unsafe class Chat : IDisposable ); // RotateLinkshell returns null when no valid linkshell is found within 8 iterations. // Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic. - return idx is null ? null : channel + idx.Value; + return idx is null ? null : channel + idx.Value; // null if not found, otherwise new channel } default: return channel; @@ -533,11 +522,7 @@ internal sealed unsafe class Chat : IDisposable internal void SetChannel(InputChannel channel, TellTarget? tellTarget = null) { - // ExtraChat linkshells aren't supported in game so we never want to - // call the ChangeChatChannel function with them. - // - // Callers should call ChatLogWindow.SetChannel() which handles - // ExtraChat channels + // Ignore ExtraChat linkshells (use ChatLogWindow.SetChannel() instead) if (channel.IsExtraChatLinkshell()) return; @@ -565,9 +550,6 @@ internal sealed unsafe class Chat : IDisposable bool setChatType ) { - // param6 is 0 for contentId and 1 for objectId - // param7 is always 0 ? - if (!Plugin.CurrentTab.CurrentChannel.UseTempChannel) Plugin.CurrentTab.CurrentChannel.UseTempChannel = true; @@ -742,10 +724,7 @@ internal sealed unsafe class Chat : IDisposable internal bool CheckHideFlags() { - // Only hide the chat in a cutscene when the vanilla chat would've - // also been hidden. This prevents Chat 2 from hiding for a split - // second before the cutscene actually starts, because the game sets - // the cutscene conditions before processing the skip. + // Only hide chat in cutscene when vanilla chat would also be hidden var raptureAtkUnitManager = RaptureAtkUnitManager.Instance(); return raptureAtkUnitManager == null || raptureAtkUnitManager->UiFlags.HasFlag(UiFlags.Chat); diff --git a/HellionChat/Ui/Settings.cs b/HellionChat/Ui/Settings.cs index d9aad7c..a6d93a3 100755 --- a/HellionChat/Ui/Settings.cs +++ b/HellionChat/Ui/Settings.cs @@ -254,12 +254,9 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window Initialise(); } - /// - /// Returns true if any setting that influences message filtering changed - /// between Plugin.Config and the Mutable working copy. Gates the heavy - /// ClearAllTabs+FilterAllTabsAsync cycle on Save so cosmetic changes - /// don't wipe in-session chat history. - /// + // Returns true if any filter-relevant setting changed between Plugin.Config + // and the Mutable copy. Gates Clear+Refilter on Save so cosmetic changes + // don't wipe in-session chat history. private bool HasFilterRelevantChanges() { if (Mutable.PrivacyFilterEnabled != Plugin.Config.PrivacyFilterEnabled) diff --git a/HellionChat/Util/AutoTranslate.cs b/HellionChat/Util/AutoTranslate.cs index b8281c9..e0a940c 100644 --- a/HellionChat/Util/AutoTranslate.cs +++ b/HellionChat/Util/AutoTranslate.cs @@ -17,10 +17,9 @@ internal static class AutoTranslate private static readonly Dictionary> Entries = new(); private static readonly HashSet<(uint, uint)> ValidEntries = []; - // Serializes all reads and writes against Entries / ValidEntries. - // PreloadCache spawns a worker thread that fills both, while the main - // thread reads them via Matching / ReplaceWithPayload / StartsWithCommand - // — without this lock the HashSet/Dictionary access is undefined. + // Serialises all reads/writes against Entries and ValidEntries. + // PreloadCache fills both from a worker thread while the main thread + // reads via Matching/ReplaceWithPayload/StartsWithCommand. private static readonly object EntriesLock = new(); private static Parser> selector)> Parser() @@ -54,13 +53,8 @@ internal static class AutoTranslate return Map((name, sel) => (name, sel), sheetName, selector.Optional()); } - /// - /// Preloads auto-translate entries into the cache for the current game - /// language. Without this, the first message will take a long time to send - /// (which causes a hitch in the main thread). - /// - /// This spawns a new thread. - /// + // Warms the auto-translate cache on a background thread so the first + // message send doesn't hitch the main thread. internal static void PreloadCache() { new Thread(() => @@ -104,7 +98,7 @@ internal static class AutoTranslate { if (lookup is not ("" or "@")) { - // SE added whitespace to the newest additions, but ParseOrThrow doesn't see them as valid + // SE added whitespace to newer entries; strip it before parsing. lookup = lookup.Replace(" ", ""); var (sheetName, selector) = parser.ParseOrThrow(lookup); @@ -144,19 +138,13 @@ internal static class AutoTranslate columns.Add(0); if (rows.Count == 0) - // We can't use an "index from end" (like `^0`) here because - // we're iterating over integers, not an array directly. - // Previously, we were setting `0..^0` which caused these - // sheets to be completely skipped due to this bug. - // See below. + // Can't use index-from-end here because we iterate over integers, + // not an array directly. `0..^0` would silently skip the sheet. rows.Add(..Index.FromStart((int)sheet.GetRowAt(sheet.Count - 1).RowId + 1)); foreach (var range in rows) { - // We iterate over the range by numerical values here, so - // we can't use an "index from end" otherwise nothing will - // happen. - // See above. + // Integer iteration -- can't use index-from-end (see above). for (var i = range.Start.Value; i < range.End.Value; i++) { if (!sheet.TryGetRow((uint)i, out var rowParser)) @@ -261,7 +249,6 @@ internal static class AutoTranslate if (bytes.Length <= search.Length) return; - // populate the list of valid entries bool needBuild; lock (EntriesLock) needBuild = ValidEntries.Count == 0; @@ -308,9 +295,8 @@ internal static class AutoTranslate start = -1; } - // Pure managed comparison via Span avoids the msvcrt.dll P/Invoke, - // which is fragile under Wine and triggered an extra managed-to- - // unmanaged copy per check. + // Span comparison avoids the msvcrt.dll P/Invoke which is fragile + // under Wine and caused an extra managed-to-unmanaged copy per check. if ( i + search.Length < bytes.Length && bytes.AsSpan(i, search.Length).SequenceEqual(search) @@ -325,7 +311,6 @@ internal static class AutoTranslate if (bytes.Length <= search.Length) return false; - // populate the list of valid entries bool needBuild; lock (EntriesLock) needBuild = ValidEntries.Count == 0; diff --git a/HellionChat/Util/GlobalParametersCache.cs b/HellionChat/Util/GlobalParametersCache.cs index 0c53f58..beb1758 100644 --- a/HellionChat/Util/GlobalParametersCache.cs +++ b/HellionChat/Util/GlobalParametersCache.cs @@ -10,9 +10,8 @@ public static class GlobalParametersCache public static int GetValue(int index) { - // Capture the array reference once so the bounds check and the - // indexed read operate on the same instance, even if Refresh - // reassigns Cache between the two operations. + // Capture the array reference once so bounds check and read operate + // on the same instance if Refresh reassigns Cache between the two. var cache = Cache; if (index < 0 || index >= cache.Length) return 0; @@ -20,12 +19,7 @@ public static class GlobalParametersCache return cache[index]; } - /// - /// Refresh the cache of global parameters from RaptureTextModule. - /// - /// - /// This should be called in the main thread when updates are necessary. - /// + // Refreshes the cache from RaptureTextModule. Must be called on the main thread. public static unsafe void Refresh() { if (!ThreadSafety.IsMainThread) diff --git a/HellionChat/Util/IconUtil.cs b/HellionChat/Util/IconUtil.cs index b53ea60..05a4a86 100755 --- a/HellionChat/Util/IconUtil.cs +++ b/HellionChat/Util/IconUtil.cs @@ -11,8 +11,7 @@ public readonly unsafe ref struct GfdFileView private readonly ReadOnlySpan Span; private readonly bool DirectLookup; - /// Initializes a new instance of the struct. - /// The data. + // span: raw .gfd file bytes public GfdFileView(ReadOnlySpan span) { Span = span; @@ -27,18 +26,13 @@ public readonly unsafe ref struct GfdFileView DirectLookup &= i + 1 == entries[i].Id; } - /// Gets the header. private ref readonly GfdHeader Header => ref MemoryMarshal.AsRef(Span); - /// Gets the entries. private ReadOnlySpan Entries => MemoryMarshal.Cast(Span[sizeof(GfdHeader)..]); - /// Attempts to get an entry. - /// The icon ID. - /// The entry. - /// Whether to follow redirects. - /// true if found. + // Returns true if the entry was found. + // followRedirect: whether to chase redirect chains. public bool TryGetEntry(uint iconId, out GfdEntry entry, bool followRedirect = true) { if (iconId == 0) @@ -50,9 +44,8 @@ public readonly unsafe ref struct GfdFileView var entries = Entries; if (DirectLookup) { - // Resolve redirects on the direct-lookup path too — the binary-search - // path follows them, and skipping them here was inconsistent for - // contiguous ID sets. + // Follow redirects on the direct-lookup path for consistency with + // the binary-search path. var visited = 0; while (iconId <= entries.Length) { @@ -107,49 +100,28 @@ public readonly unsafe ref struct GfdFileView return false; } - /// Header of a .gfd file. + // .gfd file header [StructLayout(LayoutKind.Sequential)] public struct GfdHeader { - /// Signature: "gftd0100". - public fixed byte Signature[8]; - - /// Number of entries. + public fixed byte Signature[8]; // "gftd0100" public int Count; - - /// Unused/unknown. public fixed byte Padding[4]; } - /// An entry of a .gfd file. + // .gfd file entry -- one icon slot [StructLayout(LayoutKind.Sequential, Size = 0x10)] public struct GfdEntry { - /// ID of the entry. public ushort Id; - - /// The left offset of the entry. public ushort Left; - - /// The top offset of the entry. public ushort Top; - - /// The width of the entry. public ushort Width; - - /// The height of the entry. public ushort Height; - - /// Unknown/unused. public ushort Unk0A; - - /// The redirected entry, maybe. - public ushort Redirect; - - /// Unknown/unused. + public ushort Redirect; // non-zero = redirects to another entry public ushort Unk0E; - /// Gets a value indicating whether this entry is effectively empty. public bool IsEmpty => Width == 0 || Height == 0; } } diff --git a/HellionChat/Util/MathUtil.cs b/HellionChat/Util/MathUtil.cs index c1e0501..5f4fba4 100644 --- a/HellionChat/Util/MathUtil.cs +++ b/HellionChat/Util/MathUtil.cs @@ -31,18 +31,10 @@ public static class MathUtil public override string ToString() => $"X: {X} Y: {Y} Width: {Width} Height: {Height}"; } - /// - /// Checks if two rectangles overlap at any point. - /// - /// - /// - /// True if overlapping + // Standard AABB overlap test. Inclusive on both axes to catch shared + // edges and identical rectangles (previous ValueInRange approach missed these). public static bool HasOverlap(this Rectangle a, Rectangle b) { - // Standard AABB overlap test: two rectangles overlap iff they - // overlap on both axes. The previous nested ValueInRange approach - // used strict inequalities at both ends, which dropped identical - // rectangles and shared-edge cases as false negatives. return a.X < b.X + b.Width && a.X + a.Width > b.X && a.Y < b.Y + b.Height diff --git a/HellionChat/Util/Payloads.cs b/HellionChat/Util/Payloads.cs index 9ca6c88..682ffcd 100755 --- a/HellionChat/Util/Payloads.cs +++ b/HellionChat/Util/Payloads.cs @@ -13,15 +13,10 @@ internal class PartyFinderPayload : Payload Id = id; } - protected override byte[] EncodeImpl() - { - throw new NotImplementedException(); - } + protected override byte[] EncodeImpl() => throw new NotImplementedException(); - protected override void DecodeImpl(BinaryReader reader, long endOfStream) - { + protected override void DecodeImpl(BinaryReader reader, long endOfStream) => throw new NotImplementedException(); - } } internal class AchievementPayload : Payload @@ -35,15 +30,10 @@ internal class AchievementPayload : Payload Id = id; } - protected override byte[] EncodeImpl() - { - throw new NotImplementedException(); - } + protected override byte[] EncodeImpl() => throw new NotImplementedException(); - protected override void DecodeImpl(BinaryReader reader, long endOfStream) - { + protected override void DecodeImpl(BinaryReader reader, long endOfStream) => throw new NotImplementedException(); - } } internal class UriPayload(Uri uri) : Payload @@ -55,20 +45,14 @@ internal class UriPayload(Uri uri) : Payload private const string DefaultScheme = "https"; private static readonly string[] ExpectedSchemes = ["http", "https"]; - /// - /// Create a URIPayload from a raw URI string. If the URI does not have a - /// scheme, it will default to https://. - /// - /// - /// If the URI is invalid, or if the scheme is not supported. - /// + // Parses a raw URI string. Defaults to https:// if no scheme is present. + // Throws UriFormatException for empty input or unsupported schemes. public static UriPayload ResolveUri(string rawUri) { ArgumentNullException.ThrowIfNull(rawUri); if (string.IsNullOrWhiteSpace(rawUri)) throw new UriFormatException("URI cannot be empty or whitespace."); - // Check for an expected scheme '://', if not add 'https://' if (ExpectedSchemes.Any(scheme => rawUri.StartsWith($"{scheme}://"))) return new UriPayload(new Uri(rawUri)); @@ -78,15 +62,10 @@ internal class UriPayload(Uri uri) : Payload return new UriPayload(new Uri($"{DefaultScheme}://{rawUri}")); } - protected override void DecodeImpl(BinaryReader reader, long endOfStream) - { + protected override void DecodeImpl(BinaryReader reader, long endOfStream) => throw new NotImplementedException(); - } - protected override byte[] EncodeImpl() - { - throw new NotImplementedException(); - } + protected override byte[] EncodeImpl() => throw new NotImplementedException(); } internal class EmotePayload : Payload @@ -95,18 +74,10 @@ internal class EmotePayload : Payload public string Code = string.Empty; - public static EmotePayload ResolveEmote(string code) - { - return new EmotePayload { Code = code }; - } + public static EmotePayload ResolveEmote(string code) => new EmotePayload { Code = code }; - protected override void DecodeImpl(BinaryReader reader, long endOfStream) - { + protected override void DecodeImpl(BinaryReader reader, long endOfStream) => throw new NotImplementedException(); - } - protected override byte[] EncodeImpl() - { - throw new NotImplementedException(); - } + protected override byte[] EncodeImpl() => throw new NotImplementedException(); } diff --git a/HellionChat/Util/TabsUtil.cs b/HellionChat/Util/TabsUtil.cs index 0887ab4..264e186 100755 --- a/HellionChat/Util/TabsUtil.cs +++ b/HellionChat/Util/TabsUtil.cs @@ -14,12 +14,8 @@ public static class TabsUtil return channels; } - // Hellion-tuned General preset (v1.0.0 — sharpened defaults). - // Public-chat-only, the bare three channels you encounter in open - // world. Group/FC/Linkshell traffic moves to dedicated tabs, gameplay - // events (loot, crafting, gathering, NPC dialogue, PF pings) move to - // the System tab where they belong — keeps the General view focused - // on actual conversation in the immediate surroundings. + // Public-chat-only: Say, Yell, Shout. Group/FC/Linkshell and gameplay + // events live in their own tabs to keep General focused on open-world chat. public static Tab VanillaGeneral => new() { @@ -55,11 +51,8 @@ public static class TabsUtil AllSenderMessages = true, }; - // Hellion default-tab presets used by the v10 wipe migration. Names are - // kept in HellionStrings (EN+DE) instead of Language.* so the upstream - // resource files stay untouched. Channel selections cover the channels - // a typical Eorzea raider uses without forcing the user to hand-tick - // each box on first start. + // Hellion tab presets. Names live in HellionStrings (EN+DE) so upstream + // resource files stay untouched. public static Tab HellionFreeCompany => new() { @@ -88,10 +81,8 @@ public static class TabsUtil [ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All), }, - // No automatic input-channel switch; the Gruppe tab is a read - // surface that pulls in Party, CrossParty, Alliance and PvpTeam - // together. Auto-routing /party into this tab would surprise the - // user when they actually wanted /alliance or /pvpteam. + // No input-channel switch: Party pulls in multiple channel types + // and auto-routing /party would surprise users wanting /alliance or /pvpteam. }; public static Tab HellionBeginner => @@ -112,7 +103,7 @@ public static class TabsUtil Name = HellionStrings.Tabs_Presets_System, SelectedChannels = new Dictionary { - // Plain system noise + // System noise [ChatType.Debug] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Urgent] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.Notice] = (ChatSourceExt.All, ChatSourceExt.All), @@ -122,7 +113,7 @@ public static class TabsUtil [ChatType.GatheringSystem] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.NoviceNetworkSystem] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.BattleSystem] = (ChatSourceExt.All, ChatSourceExt.All), - // Login / logout / announcement noise + // Login/logout/announcement noise [ChatType.NpcAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.FreeCompanyAnnouncement] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.FreeCompanyLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All), @@ -130,7 +121,7 @@ public static class TabsUtil [ChatType.PvpTeamLoginLogout] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.RetainerSale] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.PeriodicRecruitmentNotification] = (ChatSourceExt.All, ChatSourceExt.All), - // Gameplay-event streams (moved out of General in v1.0.0) + // Gameplay event streams [ChatType.NpcDialogue] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.LootNotice] = (ChatSourceExt.All, ChatSourceExt.All), [ChatType.LootRoll] = (ChatSourceExt.All, ChatSourceExt.All), diff --git a/HellionChat/Util/Tokenizer.cs b/HellionChat/Util/Tokenizer.cs index c51f2a8..649f152 100644 --- a/HellionChat/Util/Tokenizer.cs +++ b/HellionChat/Util/Tokenizer.cs @@ -135,18 +135,8 @@ public static class Tokenizer public int Precedence { get; set; } } - /// - /// URLRegex returns a regex object that matches URLs like: - /// - https://example.com - /// - http://example.com - /// - www.example.com - /// - https://sub.example.com - /// - example.com - /// - sub.example.com - /// - /// It matches URLs with www. or https:// prefix, and also matches URLs - /// without a prefix on specific TLDs. - /// + // Matches URLs with http(s):// or www. prefix, and bare domains on known TLDs. + // Examples: https://example.com, www.sub.example.com, example.com private static readonly Regex UrlRegex = new( @"(?((https?:\/\/|www\.)[a-z0-9-]+(\.[a-z0-9-]+)*|([a-z0-9-]+(\.[a-z0-9-]+)*\.(com|net|org|co|io|app)))(:[\d]{1,5})?(\/[^\s]*)?)", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture