diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8de6b2d..c8115bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 1.1.3.10 -
+- Fixed a misplaced function causing single-file HTML output to be written without a header or styling.
+- \/me messages are now italicized according to the native client's renderer rather than left plain.
+- Under-the-hood changes to error message selection which are less efficient but also much cleaner.
+- Further optimizations to Regex pattern matching, including compile-time pattern functions.
+
# 1.1.3.9 - 25/03/2024
- Removing leftover BBCode tags after we're done translating to HTML has been massively optimized. When I say massive, I mean a literal 250% speedup in pessimistic cases. I'm ashamed of myself for not doing this sooner.
- Private channel logs are now formatted with their IDX name (if it exists) followed by the user's supplied file name in parentheses.
diff --git a/Common.cs b/Common.cs
index 2af10c0..3a3d699 100644
--- a/Common.cs
+++ b/Common.cs
@@ -7,7 +7,7 @@ namespace FLogS
///
/// Static helper functions serving purely logical purposes in either the front- or backend.
///
- internal class Common
+ internal static class Common
{
public readonly static string dateFormat = "yyyy-MM-dd HH:mm:ss"; // ISO 8601.
private readonly static DateTime epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
diff --git a/FLogS.csproj b/FLogS.csproj
index 252c059..94d0792 100644
--- a/FLogS.csproj
+++ b/FLogS.csproj
@@ -2,12 +2,12 @@
WinExe
- net6.0-windows7.0
+ net7.0-windows7.0
enable
true
False
FLogS
- 1.1.3.9
+ 1.1.3.10
True
none
True
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 12df068..04bba58 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -94,7 +94,7 @@
-
+
@@ -184,7 +184,7 @@
-
+
@@ -279,7 +279,7 @@
-
+
@@ -335,7 +335,7 @@
-
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 457ccc7..af7721a 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -23,42 +23,49 @@ public partial class MainWindow : Window
{ // 0 = Dark mode, 1 = Light mode.
new SolidColorBrush[] { Brushes.Black, Brushes.White }, // Textboxes
new SolidColorBrush[] { Brushes.LightBlue, Brushes.Beige }, // Buttons
- new SolidColorBrush[] { new(new Color() { A = 0xFF, R = 0x33, G = 0x33, B = 0x33 }), Brushes.LightGray }, // Borders
+ new SolidColorBrush[] { new(new() { A = 0xFF, R = 0x33, G = 0x33, B = 0x33 }), Brushes.LightGray }, // Borders
new SolidColorBrush[] { Brushes.Pink, Brushes.Red }, // Error messages (And the ADL warning)
new SolidColorBrush[] { Brushes.Yellow, Brushes.DarkRed }, // Warning messages
- new SolidColorBrush[] { new(new Color() { A = 0xFF, R = 0x4C, G = 0x4C, B = 0x4C }), Brushes.DarkGray }, // TabControl
+ new SolidColorBrush[] { new(new() { A = 0xFF, R = 0x4C, G = 0x4C, B = 0x4C }), Brushes.DarkGray }, // TabControl
new SolidColorBrush[] { Brushes.Transparent, new(new Color() { A = 0xFF, R = 0x33, G = 0x33, B = 0x33 }) }, // DatePicker borders
new SolidColorBrush[] { Brushes.DimGray, Brushes.Beige }, // PanelGrids
new SolidColorBrush[] { Brushes.LightBlue, Brushes.DarkBlue }, // Hyperlinks
};
private static int brushPalette = 1;
- private static uint directoryReadyToRun = 1;
- private static uint fileReadyToRun = 1;
+ private static FLogS_ERROR directoryError;
+ private static FLogS_WARNING directoryWarning;
+ private static FLogS_ERROR fileError;
+ private static FLogS_WARNING fileWarning;
private static int filesProcessed;
private static bool overrideFormat = false;
- private static uint phraseReadyToRun = 1;
+ private static FLogS_ERROR phraseError;
+ private static FLogS_WARNING phraseWarning;
private static int reversePalette = 0;
- private readonly static string[] warnings =
+
+ private enum FLogS_ERROR
{
- string.Empty,
- "No source log files selected.",
- "No destination directory selected.",
- "Destination is not a directory.",
- "Destination directory does not exist.",
- "One or more source files do not exist.",
- "One or more source files exist in the destination.",
- "No source log file selected.",
- "Source log file does not exist.",
- "No destination file selected.",
- "Destination is not a file.",
- "Source and destination files are identical.",
- "No search text entered.",
- "Search text contains an invalid RegEx pattern.",
- string.Empty,
- string.Empty,
- "Destination file will be overwritten.",
- "One or more files will be overwritten.",
- };
+ NONE,
+ NO_SOURCES,
+ NO_DEST_DIR,
+ DEST_NOT_DIRECTORY,
+ DEST_NOT_FOUND,
+ SOURCES_NOT_FOUND,
+ SOURCE_CONFLICT,
+ NO_SOURCE,
+ SOURCE_NOT_FOUND,
+ NO_DEST,
+ DEST_NOT_FILE,
+ SOURCE_EQUALS_DEST,
+ NO_REGEX,
+ BAD_REGEX,
+ }
+
+ private enum FLogS_WARNING
+ {
+ NONE,
+ SINGLE_OVERWRITE,
+ MULTI_OVERWRITE,
+ }
public MainWindow()
{
@@ -258,6 +265,30 @@ private void FormatOverride(object? sender, RoutedEventArgs e)
}
}
+ private static string GetErrorMessage(FLogS_ERROR eCode, FLogS_WARNING wCode) => (eCode, wCode) switch
+ {
+ (FLogS_ERROR.BAD_REGEX, _) => "Search text contains an invalid RegEx pattern.",
+ (FLogS_ERROR.DEST_NOT_DIRECTORY, _) => "Destination is not a directory.",
+ (FLogS_ERROR.DEST_NOT_FILE, _) => "Destination is not a file.",
+ (FLogS_ERROR.DEST_NOT_FOUND, _) => "Destination directory does not exist.",
+ (FLogS_ERROR.NO_DEST, _) => "No destination file selected.",
+ (FLogS_ERROR.NO_DEST_DIR, _) => "No destination directory selected.",
+ (FLogS_ERROR.NO_REGEX, _) => "No search text entered.",
+ (FLogS_ERROR.NO_SOURCE, _) => "No source log file selected.",
+ (FLogS_ERROR.NO_SOURCES, _) => "No source log files selected.",
+ (FLogS_ERROR.SOURCE_CONFLICT, _) => "One or more source files exist in the destination.",
+ (FLogS_ERROR.SOURCE_EQUALS_DEST, _) => "Source and destination files are identical.",
+ (FLogS_ERROR.SOURCE_NOT_FOUND, _) => "Source log file does not exist.",
+ (FLogS_ERROR.SOURCES_NOT_FOUND, _) => "One or more source files do not exist.",
+
+ (FLogS_ERROR.NONE, FLogS_WARNING.MULTI_OVERWRITE) => "One or more files will be overwritten.",
+ (FLogS_ERROR.NONE, FLogS_WARNING.SINGLE_OVERWRITE) => "Destination file will be overwritten.",
+ (FLogS_ERROR.NONE, _) => "",
+
+ (_, FLogS_WARNING.NONE) => "An unknown error has occurred.",
+ (_, _) => "An unknown error has occurred.",
+ };
+
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
@@ -434,12 +465,12 @@ private void ThemeSelector_Click(object? sender, RoutedEventArgs e)
MainGrid.Background = brushCombos[5][brushPalette];
RegexCheckBox.Background = brushCombos[1][brushPalette];
- if (directoryReadyToRun > 0xF)
+ if (fileError == FLogS_ERROR.NONE)
+ WarningLabel.Foreground = brushCombos[4][brushPalette];
+ if (directoryError == FLogS_ERROR.NONE)
DirectoryWarningLabel.Foreground = brushCombos[4][brushPalette];
- if (phraseReadyToRun > 0xF)
+ if (phraseError == FLogS_ERROR.NONE)
PhraseWarningLabel.Foreground = brushCombos[4][brushPalette];
- if (fileReadyToRun > 0xF)
- WarningLabel.Foreground = brushCombos[4][brushPalette];
}
catch (Exception ex)
{
@@ -453,65 +484,68 @@ private void TextboxUpdated(object? sender, EventArgs e)
try
{
MessagePool.regex = RegexCheckBox.IsVisible && (RegexCheckBox.IsChecked ?? false);
- fileReadyToRun = directoryReadyToRun = phraseReadyToRun = 0;
+ fileError = directoryError = phraseError = FLogS_ERROR.NONE;
+ fileWarning = directoryWarning = phraseWarning = FLogS_WARNING.NONE;
PhraseSearchLabel.Content = MessagePool.regex ? "Target Pattern" : "Target Word or Phrase";
RunButton.IsEnabled = DirectoryRunButton.IsEnabled = PhraseRunButton.IsEnabled = true;
WarningLabel.Content = DirectoryWarningLabel.Content = PhraseWarningLabel.Content = string.Empty;
WarningLabel.Foreground = DirectoryWarningLabel.Foreground = PhraseWarningLabel.Foreground = brushCombos[3][brushPalette];
if (DirectorySource.Text.Length == 0)
- directoryReadyToRun = 1;
+ directoryError = FLogS_ERROR.NO_SOURCES;
else if (DirectoryOutput.Text.Length == 0)
- directoryReadyToRun = 2;
+ directoryError = FLogS_ERROR.NO_DEST_DIR;
else if (File.Exists(DirectoryOutput.Text))
- directoryReadyToRun = 3;
+ directoryError = FLogS_ERROR.DEST_NOT_DIRECTORY;
else if (!Directory.Exists(DirectoryOutput.Text))
- directoryReadyToRun = 4;
+ directoryError = FLogS_ERROR.DEST_NOT_FOUND;
else
{
foreach (string file in DirectorySource.Text.Split(';'))
{
string outFile = Path.Join(DirectoryOutput.Text, Path.GetFileNameWithoutExtension(file));
+
if (!Common.plaintext)
outFile += ".html";
else
outFile += ".txt";
+
if (!File.Exists(file))
- directoryReadyToRun = 5;
+ directoryError = FLogS_ERROR.SOURCES_NOT_FOUND;
else if (file.Equals(outFile))
- directoryReadyToRun = 6;
- else if (directoryReadyToRun == 0 && File.Exists(outFile))
- directoryReadyToRun = 0x11;
+ directoryError = FLogS_ERROR.SOURCE_CONFLICT;
+ else if (directoryError == FLogS_ERROR.NONE && File.Exists(outFile))
+ directoryWarning = FLogS_WARNING.MULTI_OVERWRITE;
}
}
if (FileSource.Text.Length == 0)
- fileReadyToRun = 7;
+ fileError = FLogS_ERROR.NO_SOURCE;
else if (!File.Exists(FileSource.Text))
- fileReadyToRun = 8;
+ fileError = FLogS_ERROR.SOURCE_NOT_FOUND;
else if (FileOutput.Text.Length == 0)
- fileReadyToRun = 9;
+ fileError = FLogS_ERROR.NO_DEST;
else if (Directory.Exists(FileOutput.Text))
- fileReadyToRun = 0xA;
+ fileError = FLogS_ERROR.DEST_NOT_FILE;
else if (!Directory.Exists(Path.GetDirectoryName(FileOutput.Text)))
- fileReadyToRun = 4;
+ fileError = FLogS_ERROR.DEST_NOT_FOUND;
else if (FileSource.Text.Equals(FileOutput.Text))
- fileReadyToRun = 0xB;
+ fileError = FLogS_ERROR.SOURCE_EQUALS_DEST;
else if (File.Exists(FileOutput.Text))
- fileReadyToRun = 0x10;
+ fileWarning = FLogS_WARNING.SINGLE_OVERWRITE;
if (PhraseSource.Text.Length == 0)
- phraseReadyToRun = 1;
+ phraseError = FLogS_ERROR.NO_SOURCES;
else if (PhraseOutput.Text.Length == 0)
- phraseReadyToRun = 2;
+ phraseError = FLogS_ERROR.NO_DEST_DIR;
else if (File.Exists(PhraseOutput.Text))
- phraseReadyToRun = 3;
+ phraseError = FLogS_ERROR.DEST_NOT_DIRECTORY;
else if (!Directory.Exists(PhraseOutput.Text))
- phraseReadyToRun = 4;
+ phraseError = FLogS_ERROR.DEST_NOT_FOUND;
else if (PhraseSearch.Text.Length == 0)
- phraseReadyToRun = 0xC;
+ phraseError = FLogS_ERROR.NO_REGEX;
else if (RegexCheckBox.IsChecked == true && !Common.IsValidPattern(PhraseSearch.Text))
- phraseReadyToRun = 0xD;
+ phraseError = FLogS_ERROR.BAD_REGEX;
else
{
foreach (string file in PhraseSource.Text.Split(';'))
@@ -522,11 +556,11 @@ private void TextboxUpdated(object? sender, EventArgs e)
else
outFile += ".txt";
if (!File.Exists(file))
- phraseReadyToRun = 5;
+ phraseError = FLogS_ERROR.SOURCES_NOT_FOUND;
else if (file.Equals(outFile))
- phraseReadyToRun = 6;
- else if (phraseReadyToRun == 0 && File.Exists(outFile))
- phraseReadyToRun = 0x11;
+ phraseError = FLogS_ERROR.SOURCE_CONFLICT;
+ else if (phraseError == FLogS_ERROR.NONE && File.Exists(outFile))
+ phraseWarning = FLogS_WARNING.MULTI_OVERWRITE;
}
}
}
@@ -536,19 +570,19 @@ private void TextboxUpdated(object? sender, EventArgs e)
return;
}
- DirectoryWarningLabel.Content = warnings[directoryReadyToRun];
- DirectoryRunButton.IsEnabled = directoryReadyToRun == 0 || directoryReadyToRun > 0xF;
- PhraseWarningLabel.Content = warnings[phraseReadyToRun];
- PhraseRunButton.IsEnabled = phraseReadyToRun == 0 || phraseReadyToRun > 0xF;
- WarningLabel.Content = warnings[fileReadyToRun];
- RunButton.IsEnabled = fileReadyToRun == 0 || fileReadyToRun > 0xF;
+ DirectoryWarningLabel.Content = GetErrorMessage(directoryError, directoryWarning);
+ DirectoryRunButton.IsEnabled = directoryError == FLogS_ERROR.NONE;
+ PhraseWarningLabel.Content = GetErrorMessage(phraseError, phraseWarning);
+ PhraseRunButton.IsEnabled = phraseError == FLogS_ERROR.NONE;
+ WarningLabel.Content = GetErrorMessage(fileError, fileWarning);
+ RunButton.IsEnabled = fileError == FLogS_ERROR.NONE;
- if (directoryReadyToRun > 0xF)
+ if (fileError == FLogS_ERROR.NONE)
+ WarningLabel.Foreground = brushCombos[4][brushPalette];
+ if (directoryError == FLogS_ERROR.NONE)
DirectoryWarningLabel.Foreground = brushCombos[4][brushPalette];
- if (phraseReadyToRun > 0xF)
+ if (phraseError == FLogS_ERROR.NONE)
PhraseWarningLabel.Foreground = brushCombos[4][brushPalette];
- if (fileReadyToRun > 0xF)
- WarningLabel.Foreground = brushCombos[4][brushPalette];
}
private static void TransitionEnableables(DependencyObject sender, bool enabled)
diff --git a/MessagePool.cs b/MessagePool.cs
index da602e2..1499056 100644
--- a/MessagePool.cs
+++ b/MessagePool.cs
@@ -12,9 +12,9 @@ namespace FLogS
///
/// Static functions serving most of the needs of the actual translation routine. Everything not strictly WPF but still integral to the BackgroundWorker threads goes here.
///
- internal class MessagePool
+ internal partial class MessagePool
{
- public static ByteCount bytesRead;
+ private static ByteCount bytesRead;
public static uint corruptTimestamps = 0U;
public static string? destDir;
public static string? destFile;
@@ -121,6 +121,7 @@ internal class MessagePool
{ "session", 0 },
{ "color", 0 },
};
+ private static Stack tagHistory;
private static uint thisDate = 1U;
public static ByteCount totalSize;
public static ByteCount truncatedBytes;
@@ -244,6 +245,7 @@ public static void BeginRoutine(object? sender, DoWorkEventArgs e)
using (StreamWriter dstFS = divide ? StreamWriter.Null : new(destFile, true))
{
dstSB = new();
+ headerWritten = false;
lastDiscrepancy = 0;
lastPosition = 0U;
Common.lastTimestamp = 0U;
@@ -346,11 +348,11 @@ private static bool TranslateIDX(FileStream srcFS)
{
nameLength = srcFS.ReadByte();
if (nameLength < 1
- || (!Path.GetFileNameWithoutExtension(srcFS.Name).Contains('#') && nameLength > 20)) // F-List profile names cannot be greater than 20 characters in length.
+ || !Path.GetFileNameWithoutExtension(srcFS.Name).Contains('#') && nameLength > 20) // F-List profile names cannot be greater than 20 characters in length.
return false;
streamBuffer = new byte[nameLength];
- if ((result = srcFS.Read(streamBuffer, 0, (int)nameLength)) < nameLength)
+ if ((result = srcFS.Read(streamBuffer, 0, nameLength)) < nameLength)
return false;
nameString = new string(Encoding.UTF8.GetString(streamBuffer).Where(c => !Path.GetInvalidFileNameChars().Contains(c)).ToArray()).ToLower();
@@ -414,6 +416,7 @@ private static bool TranslateMessage(FileStream srcFS, StreamWriter dstFS)
string profileName = string.Empty;
int result;
byte[]? streamBuffer;
+ tagHistory = new();
DateTime thisDT = new();
uint timestamp;
bool withinRange = true;
@@ -460,7 +463,7 @@ private static bool TranslateMessage(FileStream srcFS, StreamWriter dstFS)
if (divide)
{
- thisDate = timestamp - (timestamp % 86400);
+ thisDate = timestamp - timestamp % 86400;
if (thisDate != lastDate && intact)
{
if (lastDate != 0U)
@@ -600,28 +603,27 @@ private static bool TranslateMessage(FileStream srcFS, StreamWriter dstFS)
messageOut = Regex.Replace(messageOut, entity.Key, entity.Value);
if (msId == MessageType.Me || msId == MessageType.DiceRoll)
+ {
+ messageOut = "" + messageOut;
+ tagHistory.Push("i");
+ tagCounts["i"] += 1;
messageOut = messageOut.TrimStart();
+ }
messageData.Add(messageOut);
}
}
messageOut = string.Join(' ', messageData.ToArray());
- messageOut = Regex.Replace(messageOut, @"\p{Co}+", string.Empty); // Remove everything that's not a printable, newline, or format character.
+ messageOut = ControlCharacters().Replace(messageOut, string.Empty); // Remove everything that's not a printable, newline, or format character.
if (phrase is null
- || (!regex && messageOut.Contains(phrase, StringComparison.OrdinalIgnoreCase)) // Either the profile name or the message body can contain our search text.
- || (regex && Regex.IsMatch(messageOut, phrase)))
+ || !regex && messageOut.Contains(phrase, StringComparison.OrdinalIgnoreCase) // Either the profile name or the message body can contain our search text.
+ || regex && Regex.IsMatch(messageOut, phrase))
matchPhrase = true;
if (matchPhrase && withinRange && (intact || saveTruncated))
{
- if (!Common.plaintext && !headerWritten)
- {
- dstSB.Insert(0, htmlHeader);
- headerWritten = true;
- }
-
if (intact)
{
intactMessages++;
@@ -642,7 +644,7 @@ private static bool TranslateMessage(FileStream srcFS, StreamWriter dstFS)
messageOut = TranslateTags(messageOut); // If we're saving to HTML, it's time to convert from BBCode to HTML-style tags.
messageOut = Encoding.UTF8.GetString(Encoding.UTF8.GetBytes(messageOut)); // There's an odd quirk with East Asian printable characters that requires us to reformat them once.
- messageOut = Regex.Replace(messageOut, @"\p{Co}+", string.Empty); // Once more, remove everything that's not a printable, newline, or format character.
+ messageOut = ControlCharacters().Replace(messageOut, string.Empty); // Once more, remove everything that's not a printable, newline, or format character.
if (!Common.plaintext && !opposingProfile.Equals(string.Empty) && !profileName.ToLower().Equals(opposingProfile.ToLower())) // If this is the local user, close the highlight tag from before.
messageOut += "";
@@ -654,6 +656,12 @@ private static bool TranslateMessage(FileStream srcFS, StreamWriter dstFS)
if (!divide)
{
+ if (!Common.plaintext && !headerWritten)
+ {
+ dstSB.Insert(0, htmlHeader);
+ headerWritten = true;
+ }
+
dstFS.Write(dstSB.ToString());
dstSB.Clear();
}
@@ -720,8 +728,7 @@ private static string TranslateTags(string message)
bool noParse = false;
string partialParse = string.Empty;
string tag = string.Empty;
- Stack tagHistory = new();
- MatchCollection tags = Regex.Matches(messageOut, @"\[/*(\p{L}+)(?:=+([^\p{Co}\]]*))*?\]");
+ MatchCollection tags = BBCodeTags().Matches(messageOut);
string URL = string.Empty;
// The best practice is to avoid sub-routines like these where possible.
@@ -761,7 +768,7 @@ bool AdjustHistory(int index)
continue;
}
- if (isClosing && tagCounts.ContainsKey(tag) && tagCounts[tag] % 2 == 0)
+ if (isClosing && tagCounts.TryGetValue(tag, out int tagCount) && tagCount % 2 == 0)
continue;
switch (tag)
@@ -1005,9 +1012,15 @@ bool AdjustHistory(int index)
}
// Finish things off by removing the BBCode tags, leaving only our fresh HTML behind.
- messageOut = Regex.Replace(messageOut, @"\[/*(\p{L}+)(?:=+([^\p{Co}\]]*))*?\]", string.Empty);
+ messageOut = BBCodeTags().Replace(messageOut, string.Empty);
return messageOut;
}
+
+ [GeneratedRegex(@"\[/*(\p{L}+)(?:=+([^\p{Co}\]]*))*?\]")]
+ private static partial Regex BBCodeTags();
+
+ [GeneratedRegex(@"\p{Co}+")]
+ private static partial Regex ControlCharacters();
}
}
\ No newline at end of file