diff --git a/ImageDescriber.Test/ScanTests.cs b/ImageDescriber.Test/ScanTests.cs index 6fae3a9..af77c35 100644 --- a/ImageDescriber.Test/ScanTests.cs +++ b/ImageDescriber.Test/ScanTests.cs @@ -87,6 +87,62 @@ public void SanitizeFileNameHandlesSimpleName() Assert.AreEqual("sunset-over-ocean.webp", result.WeakString); } + [TestMethod] + public void SanitizeFileNameTakesNameFromInsideCodeFence() + { + FileName result = Scan.SanitizeFileName("```\nsunset-over-lake\n```", ".jpg".As()); + + Assert.AreEqual("sunset-over-lake.jpg", result.WeakString); + } + + [TestMethod] + public void SanitizeFileNameTakesNameFromInsideCodeFenceWithLanguage() + { + FileName result = Scan.SanitizeFileName("```text\r\nsunset-over-lake\r\n```", ".jpg".As()); + + Assert.AreEqual("sunset-over-lake.jpg", result.WeakString); + } + + [TestMethod] + public void SanitizeFileNameTakesNameFromInlineCodeFence() + { + FileName result = Scan.SanitizeFileName("```sunset-over-lake```", ".jpg".As()); + + Assert.AreEqual("sunset-over-lake.jpg", result.WeakString); + } + + [TestMethod] + public void SanitizeFileNameSkipsLeadingBlankLines() + { + FileName result = Scan.SanitizeFileName("\nsunset-over-lake\n", ".jpg".As()); + + Assert.AreEqual("sunset-over-lake.jpg", result.WeakString); + } + + [TestMethod] + public void SanitizeFileNameKeepsDotFollowedByText() + { + FileName result = Scan.SanitizeFileName("st.-louis-arch-at-dusk", ".jpg".As()); + + Assert.AreEqual("st.-louis-arch-at-dusk.jpg", result.WeakString); + } + + [TestMethod] + public void SanitizeFileNameKeepsDotInTime() + { + FileName result = Scan.SanitizeFileName("sunset-at-5.30pm", ".jpg".As()); + + Assert.AreEqual("sunset-at-5.30pm.jpg", result.WeakString); + } + + [TestMethod] + public void SanitizeFileNameStripsUpperCaseImageExtension() + { + FileName result = Scan.SanitizeFileName("photo.JPEG", ".png".As()); + + Assert.AreEqual("photo.png", result.WeakString); + } + [TestMethod] public void HashFilesSkipsFilesThatCannotBeRead() { diff --git a/ImageDescriber/ImageScanner.cs b/ImageDescriber/ImageScanner.cs index b9740de..5e39b50 100644 --- a/ImageDescriber/ImageScanner.cs +++ b/ImageDescriber/ImageScanner.cs @@ -33,15 +33,7 @@ internal static IReadOnlyList ScanForImages(AbsoluteDirectoryP List imageFiles = []; foreach (string file in Directory.EnumerateFiles(path.WeakString, "*", SearchOption.AllDirectories)) { - string ext = Path.GetExtension(file); - if (string.IsNullOrEmpty(ext)) - { - continue; - } - - // FileExtension compares ordinally, and cameras and Windows tools write .JPG/.PNG, - // so fold to the lower-case form the set holds before looking it up. - if (ImageExtensions.Contains(ext.ToLowerInvariant().As())) + if (IsImageExtension(Path.GetExtension(file))) { imageFiles.Add(file.As()); } @@ -49,4 +41,16 @@ internal static IReadOnlyList ScanForImages(AbsoluteDirectoryP return imageFiles; } + + internal static bool IsImageExtension(string extension) + { + if (string.IsNullOrEmpty(extension)) + { + return false; + } + + // FileExtension compares ordinally, and cameras and Windows tools write .JPG/.PNG, + // so fold to the lower-case form the set holds before looking it up. + return ImageExtensions.Contains(extension.ToLowerInvariant().As()); + } } diff --git a/ImageDescriber/Verbs/Scan.cs b/ImageDescriber/Verbs/Scan.cs index 2241803..3cfaafa 100644 --- a/ImageDescriber/Verbs/Scan.cs +++ b/ImageDescriber/Verbs/Scan.cs @@ -246,18 +246,17 @@ internal static IReadOnlyList DescribeImages( internal static FileName SanitizeFileName(string rawSuggestion, FileExtension extension) { - string name = rawSuggestion.Trim().Trim('"', '\'', '`'); - - // Take only the first line if the model returned multiple lines - int newlineIndex = name.IndexOf('\n', StringComparison.Ordinal); - if (newlineIndex >= 0) - { - name = name[..newlineIndex].Trim(); - } - - // Strip any extension the model may have included - string existingExt = System.IO.Path.GetExtension(name); - if (!string.IsNullOrEmpty(existingExt)) + // Take the first line with content, so a name wrapped in a code fence or preceded by a + // blank line is kept, and only then trim the quotes and backticks around it + string name = rawSuggestion + .Split('\n') + .Select(line => line.Trim()) + .FirstOrDefault(line => line.Length > 0 && !IsCodeFence(line)) ?? string.Empty; + name = name.Trim('"', '\'', '`').Trim(); + + // Strip an image extension the model may have included. A suggestion normally has no + // extension, so any other dot is part of the name ("st.-louis-arch", "sunset-at-5.30pm") + if (ImageScanner.IsImageExtension(System.IO.Path.GetExtension(name))) { name = System.IO.Path.GetFileNameWithoutExtension(name); } @@ -283,4 +282,10 @@ internal static FileName SanitizeFileName(string rawSuggestion, FileExtension ex return $"{name}{extension}".As(); } + + // An opening or closing fence ("```" or "```text"), as opposed to a name written inline + // between two fences on one line ("```sunset```") + private static bool IsCodeFence(string line) => + line.StartsWith("```", StringComparison.Ordinal) + && (line.Length < 6 || !line.EndsWith("```", StringComparison.Ordinal)); }