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