diff --git a/AppScaffolding/AppScaffolding.csproj b/AppScaffolding/AppScaffolding.csproj
index 831d139f..b23e4756 100644
--- a/AppScaffolding/AppScaffolding.csproj
+++ b/AppScaffolding/AppScaffolding.csproj
@@ -3,7 +3,7 @@
net5.0
- 6.2.8.3
+ 6.2.9.1
diff --git a/AppScaffolding/LibationScaffolding.cs b/AppScaffolding/LibationScaffolding.cs
index 53927b24..e988165b 100644
--- a/AppScaffolding/LibationScaffolding.cs
+++ b/AppScaffolding/LibationScaffolding.cs
@@ -59,6 +59,7 @@ namespace AppScaffolding
Migrations.migrate_to_v5_7_1(config);
Migrations.migrate_to_v6_1_2(config);
Migrations.migrate_to_v6_2_0(config);
+ Migrations.migrate_to_v6_2_9(config);
}
/// Initialize logging. Run after migration
@@ -345,5 +346,18 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
}
+
+ // add file naming templates
+ public static void migrate_to_v6_2_9(Configuration config)
+ {
+ if (!config.Exists(nameof(config.FolderTemplate)))
+ config.FolderTemplate = Templates.Folder.DefaultTemplate;
+
+ if (!config.Exists(nameof(config.FileTemplate)))
+ config.FileTemplate = Templates.File.DefaultTemplate;
+
+ if (!config.Exists(nameof(config.ChapterFileTemplate)))
+ config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
+ }
}
}
diff --git a/FileLiberator/AudioFileStorageExt.cs b/FileLiberator/AudioFileStorageExt.cs
index 7d4f4ffd..d799d547 100644
--- a/FileLiberator/AudioFileStorageExt.cs
+++ b/FileLiberator/AudioFileStorageExt.cs
@@ -11,9 +11,8 @@ namespace FileLiberator
{
public static class AudioFileStorageExt
{
- private static string TEMP_SINGLE_TEMPLATE { get; } = " []";
- private static string TEMP_DIR_TEMPLATE { get; } = " []";
- private static string TEMP_MULTI_TEMPLATE { get; } = " [] - - ";
+ private static void AddParameterReplacement(this FileTemplate fileTemplate, TemplateTags templateTags, object value)
+ => fileTemplate.AddParameterReplacement(templateTags.TagName, value);
internal class MultipartRenamer
{
@@ -22,16 +21,16 @@ namespace FileLiberator
public MultipartRenamer(LibraryBook libraryBook) => this.libraryBook = libraryBook;
internal string MultipartFilename(string outputFileName, int partsPosition, int partsTotal, AAXClean.NewSplitCallback newSplitCallback)
- => MultipartFilename(TEMP_MULTI_TEMPLATE, AudibleFileStorage.DecryptInProgressDirectory, Path.GetExtension(outputFileName), partsPosition, partsTotal, newSplitCallback?.Chapter?.Title ?? "");
+ => MultipartFilename(Configuration.Instance.ChapterFileTemplate, AudibleFileStorage.DecryptInProgressDirectory, Path.GetExtension(outputFileName), partsPosition, partsTotal, newSplitCallback?.Chapter?.Title ?? "");
internal string MultipartFilename(string template, string fullDirPath, string extension, int partsPosition, int partsTotal, string chapterTitle)
{
var fileTemplate = GetFileTemplateSingle(template, libraryBook, fullDirPath, extension);
- fileTemplate.AddParameterReplacement("ch count", partsTotal.ToString());
- fileTemplate.AddParameterReplacement("ch#", partsPosition.ToString());
- fileTemplate.AddParameterReplacement("ch# 0", FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
- fileTemplate.AddParameterReplacement("ch title", chapterTitle);
+ fileTemplate.AddParameterReplacement(TemplateTags.ChCount, partsTotal);
+ fileTemplate.AddParameterReplacement(TemplateTags.ChNumber, partsPosition);
+ fileTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(partsPosition, partsTotal));
+ fileTemplate.AddParameterReplacement(TemplateTags.ChTitle, chapterTitle);
return fileTemplate.GetFilePath();
}
@@ -47,11 +46,11 @@ namespace FileLiberator
=> GetCustomDirFilename(_, libraryBook, AudibleFileStorage.BooksDirectory, extension);
public static string GetDestinationDirectory(this AudioFileStorage _, LibraryBook libraryBook)
- => GetFileTemplateSingle(TEMP_DIR_TEMPLATE, libraryBook, AudibleFileStorage.BooksDirectory, null)
+ => GetFileTemplateSingle(Configuration.Instance.FolderTemplate, libraryBook, AudibleFileStorage.BooksDirectory, null)
.GetFilePath();
public static string GetCustomDirFilename(this AudioFileStorage _, LibraryBook libraryBook, string dirFullPath, string extension)
- => GetFileTemplateSingle(TEMP_SINGLE_TEMPLATE, libraryBook, dirFullPath, extension)
+ => GetFileTemplateSingle(Configuration.Instance.FolderTemplate, libraryBook, dirFullPath, extension)
.GetFilePath();
internal static FileTemplate GetFileTemplateSingle(string template, LibraryBook libraryBook, string dirFullPath, string extension)
@@ -65,9 +64,17 @@ namespace FileLiberator
var title = libraryBook.Book.Title ?? "";
- fileTemplate.AddParameterReplacement("title", title);
- fileTemplate.AddParameterReplacement("title short", title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')));
- fileTemplate.AddParameterReplacement("id", libraryBook.Book.AudibleProductId);
+ fileTemplate.AddParameterReplacement(TemplateTags.Id, libraryBook.Book.AudibleProductId);
+ fileTemplate.AddParameterReplacement(TemplateTags.Title, title);
+ fileTemplate.AddParameterReplacement(TemplateTags.TitleShort, title.IndexOf(':') < 1 ? title : title.Substring(0, title.IndexOf(':')));
+ fileTemplate.AddParameterReplacement(TemplateTags.Author, libraryBook.Book.AuthorNames);
+ fileTemplate.AddParameterReplacement(TemplateTags.FirstAuthor, libraryBook.Book.Authors.FirstOrDefault()?.Name);
+ fileTemplate.AddParameterReplacement(TemplateTags.Narrator, libraryBook.Book.NarratorNames);
+ fileTemplate.AddParameterReplacement(TemplateTags.FirstNarrator, libraryBook.Book.Narrators.FirstOrDefault()?.Name);
+
+ var seriesLink = libraryBook.Book.SeriesLink.FirstOrDefault();
+ fileTemplate.AddParameterReplacement(TemplateTags.SeriesName, seriesLink?.Series.Name);
+ fileTemplate.AddParameterReplacement(TemplateTags.SeriesNumber, seriesLink?.Order);
return fileTemplate;
}
diff --git a/FileManager/FileTemplate.cs b/FileManager/FileTemplate.cs
index 138bbfc5..8fab2de0 100644
--- a/FileManager/FileTemplate.cs
+++ b/FileManager/FileTemplate.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using Dinah.Core;
@@ -16,10 +15,12 @@ namespace FileManager
public FileTemplate(string template) => Template = ArgumentValidator.EnsureNotNullOrWhiteSpace(template, nameof(template));
/// Optional step 1: Replace html-styled template tags with parameters. Eg {"name", "Bill Gates"} => /<name>/ => /Bill Gates/
- public Dictionary ParameterReplacements { get; } = new Dictionary();
+ public Dictionary ParameterReplacements { get; } = new Dictionary();
/// Convenience method
- public void AddParameterReplacement(string key ,string value) => ParameterReplacements.Add(key, value);
+ public void AddParameterReplacement(string key, object value)
+ // using .Add() instead of "[key] = value" will make unintended overwriting throw exception
+ => ParameterReplacements.Add(key, value);
/// If set, truncate each parameter replacement to this many characters. Default 50
public int? ParameterMaxSize { get; set; } = 50;
@@ -38,14 +39,14 @@ namespace FileManager
return FileUtility.GetValidFilename(filename, IllegalCharacterReplacements);
}
- private string formatKey(string key)
+ private static string formatKey(string key)
=> key
.Replace("<", "")
.Replace(">", "");
- private string formatValue(string value)
+ private string formatValue(object value)
=> ParameterMaxSize.HasValue && ParameterMaxSize.Value > 0
- ? value?.Truncate(ParameterMaxSize.Value)
- : value;
+ ? value?.ToString().Truncate(ParameterMaxSize.Value)
+ : value?.ToString();
}
}
diff --git a/Libation.sln b/Libation.sln
index 6729b1cd..fe88ed24 100644
--- a/Libation.sln
+++ b/Libation.sln
@@ -62,6 +62,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileLiberator.Tests", "_Tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileManager.Tests", "_Tests\FileManager.Tests\FileManager.Tests.csproj", "{F2E04270-4551-41C4-99FF-E7125BED708C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibationFileManager.Tests", "_Tests\LibationFileManager.Tests\LibationFileManager.Tests.csproj", "{EB781571-8548-477E-82AD-FB9FAB548D2F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -136,6 +138,10 @@ Global
{F2E04270-4551-41C4-99FF-E7125BED708C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F2E04270-4551-41C4-99FF-E7125BED708C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EB781571-8548-477E-82AD-FB9FAB548D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EB781571-8548-477E-82AD-FB9FAB548D2F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -158,6 +164,7 @@ Global
{788294BE-0D8E-40D4-9CEE-67896FBB52CE} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{5B8FC827-BF58-4CB1-A59E-BDEB9C62A05E} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
{F2E04270-4551-41C4-99FF-E7125BED708C} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
+ {EB781571-8548-477E-82AD-FB9FAB548D2F} = {67E66E82-5532-4440-AFB3-9FB1DF9DEF53}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}
diff --git a/LibationFileManager/Configuration.cs b/LibationFileManager/Configuration.cs
index 3e701084..2105ed81 100644
--- a/LibationFileManager/Configuration.cs
+++ b/LibationFileManager/Configuration.cs
@@ -145,8 +145,40 @@ namespace LibationFileManager
{
get => persistentDictionary.GetNonString(nameof(DownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(DownloadEpisodes), value);
+ }
+
+ [Description("How to format the folders in which files will be saved")]
+ public string FolderTemplate
+ {
+ get => getTemplate(nameof(FolderTemplate), Templates.Folder);
+ set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
}
+ [Description("How to format the saved pdf and audio files")]
+ public string FileTemplate
+ {
+ get => getTemplate(nameof(FileTemplate), Templates.File);
+ set => setTemplate(nameof(FileTemplate), Templates.File, value);
+ }
+
+ [Description("How to format the saved audio files which are split by chapters")]
+ public string ChapterFileTemplate
+ {
+ get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
+ set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
+ }
+
+ private string getTemplate(string settingName, Templates templ)
+ {
+ var value = persistentDictionary.GetString(settingName).Trim();
+ return templ.IsValid(value) ? value : templ.DefaultTemplate;
+ }
+ private void setTemplate(string settingName, Templates templ, string newValue)
+ {
+ var template = newValue.Trim();
+ if (templ.IsValid(template))
+ persistentDictionary.SetString(settingName, template);
+ }
#endregion
#region known directories
diff --git a/LibationFileManager/TemplateTags.cs b/LibationFileManager/TemplateTags.cs
new file mode 100644
index 00000000..262cb853
--- /dev/null
+++ b/LibationFileManager/TemplateTags.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dinah.Core;
+
+namespace LibationFileManager
+{
+ public sealed class TemplateTags : Enumeration
+ {
+ public string TagName => DisplayName;
+ public string Description { get; }
+ public bool IsChapterOnly { get; }
+
+ private static int value = 0;
+ private TemplateTags(string tagName, string description, bool isChapterOnly = false) : base(value++, tagName)
+ {
+ Description = description;
+ IsChapterOnly = isChapterOnly;
+ }
+
+ public static TemplateTags Id { get; } = new TemplateTags("id", "Audible ID");
+ public static TemplateTags Title { get; } = new TemplateTags("title", "Full title");
+ public static TemplateTags TitleShort { get; } = new TemplateTags("title short", "Title. Stop at first colon");
+ public static TemplateTags Author { get; } = new TemplateTags("author", "Author(s)");
+ public static TemplateTags FirstAuthor { get; } = new TemplateTags("first author", "First author");
+ public static TemplateTags Narrator { get; } = new TemplateTags("narrator", "Narrator(s)");
+ public static TemplateTags FirstNarrator { get; } = new TemplateTags("first narrator", "First narrator");
+ public static TemplateTags SeriesName { get; } = new TemplateTags("series name", "Name of series");
+ // can't also have a leading zeros version. Too many weird edge cases. Eg: "1-4"
+ public static TemplateTags SeriesNumber { get; } = new TemplateTags("series#", "Number order in series");
+
+ public static TemplateTags ChCount { get; } = new TemplateTags("ch count", "Number of chapters", true);
+ public static TemplateTags ChTitle { get; } = new TemplateTags("ch title", "Chapter title", true);
+ public static TemplateTags ChNumber { get; } = new TemplateTags("ch#", "Chapter number", true);
+ public static TemplateTags ChNumber0 { get; } = new TemplateTags("ch# 0", "Chapter number with leading zeros", true);
+ }
+}
diff --git a/LibationFileManager/Templates.cs b/LibationFileManager/Templates.cs
new file mode 100644
index 00000000..c51b27dc
--- /dev/null
+++ b/LibationFileManager/Templates.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LibationFileManager
+{
+ public abstract class Templates
+ {
+ public static Templates Folder { get; } = new FolderTemplate();
+ public static Templates File { get; } = new FileTemplate();
+ public static Templates ChapterFile { get; } = new ChapterFileTemplate();
+
+ public abstract string DefaultTemplate { get; }
+
+ public abstract bool IsValid(string template);
+ public abstract bool IsRecommended(string template);
+ public abstract int TagCount(string template);
+
+ public static bool ContainsChapterOnlyTags(string template)
+ => TemplateTags.GetAll()
+ .Where(t => t.IsChapterOnly)
+ .Any(t => ContainsTag(template, t.TagName));
+
+ public static bool ContainsTag(string template, string tag) => template.Contains($"<{tag}>");
+
+ protected static bool fileIsValid(string template)
+ // File name only; not path. all other path chars are valid enough to pass this check and will be handled on final save.
+ // null is invalid. whitespace is valid but not recommended
+ => template is not null
+ && !template.Contains(':')
+ && !template.Contains(System.IO.Path.DirectorySeparatorChar)
+ && !template.Contains(System.IO.Path.AltDirectorySeparatorChar);
+
+ protected bool isRecommended(string template, bool isChapter)
+ => IsValid(template)
+ && !string.IsNullOrWhiteSpace(template)
+ && TagCount(template) > 0
+ && ContainsChapterOnlyTags(template) == isChapter;
+
+ protected static int tagCount(string template, Func func)
+ => TemplateTags.GetAll()
+ .Where(func)
+ // for == 1, use:
+ // .Count(t => template.Contains($"<{t.TagName}>"))
+ // .Sum() impl: == 2
+ .Sum(t => template.Split($"<{t.TagName}>").Length - 1);
+
+ private class FolderTemplate : Templates
+ {
+ public override string DefaultTemplate { get; } = " []";
+
+ public override bool IsValid(string template)
+ // must be relative. no colons. all other path chars are valid enough to pass this check and will be handled on final save.
+ // null is invalid. whitespace is valid but not recommended
+ => template is not null
+ && !template.Contains(':');
+
+ public override bool IsRecommended(string template) => isRecommended(template, false);
+
+ public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly);
+ }
+
+ private class FileTemplate : Templates
+ {
+ public override string DefaultTemplate { get; } = " []";
+
+ public override bool IsValid(string template) => fileIsValid(template);
+
+ public override bool IsRecommended(string template) => isRecommended(template, false);
+
+ public override int TagCount(string template) => tagCount(template, t => !t.IsChapterOnly);
+ }
+
+ private class ChapterFileTemplate : Templates
+ {
+ public override string DefaultTemplate { get; } = " [] - - ";
+
+ public override bool IsValid(string template) => fileIsValid(template);
+
+ public override bool IsRecommended(string template)
+ => isRecommended(template, true)
+ // recommended to incl. or
+ && (ContainsTag(template, TemplateTags.ChNumber.TagName) || ContainsTag(template, TemplateTags.ChNumber0.TagName));
+
+ public override int TagCount(string template) => tagCount(template, t => true);
+ }
+ }
+}
diff --git a/LibationFileManager/_InternalsVisible.cs b/LibationFileManager/_InternalsVisible.cs
new file mode 100644
index 00000000..d0bcf4c8
--- /dev/null
+++ b/LibationFileManager/_InternalsVisible.cs
@@ -0,0 +1 @@
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(nameof(LibationFileManager) + ".Tests")]
\ No newline at end of file
diff --git a/_Tests/FileManager.Tests/FileUtilityTests.cs b/_Tests/FileManager.Tests/FileUtilityTests.cs
index d9371c15..49ef3a2f 100644
--- a/_Tests/FileManager.Tests/FileUtilityTests.cs
+++ b/_Tests/FileManager.Tests/FileUtilityTests.cs
@@ -35,6 +35,7 @@ namespace FileUtilityTests
[DataRow(@"a\b:c\d.txt", "ZZZ", @"a\bZZZc\d.txt")]
// remove empty directories
[DataRow(@"C:\a\\\b\c\\\d.txt", "ZZZ", @"C:\a\b\c\d.txt")]
+ [DataRow(@"C:\""foo\", "ZZZ", @"C:\ZZZfoo\ZZZidZZZ")]
public void Tests(string inStr, string replacement, string outStr) => Assert.AreEqual(outStr, FileUtility.GetSafePath(inStr, replacement));
}
diff --git a/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj b/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj
new file mode 100644
index 00000000..0aaf853d
--- /dev/null
+++ b/_Tests/LibationFileManager.Tests/LibationFileManager.Tests.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/_Tests/LibationFileManager.Tests/TemplatesTests.cs b/_Tests/LibationFileManager.Tests/TemplatesTests.cs
new file mode 100644
index 00000000..9010744a
--- /dev/null
+++ b/_Tests/LibationFileManager.Tests/TemplatesTests.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Dinah.Core;
+using FluentAssertions;
+using LibationFileManager;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace TemplatesTests
+{
+ [TestClass]
+ public class ContainsChapterOnlyTags
+ {
+ [TestMethod]
+ [DataRow("", false)]
+ [DataRow("", true)]
+ [DataRow("", false)]
+ [DataRow("", true)]
+ public void Tests(string template, bool expected) => Templates.ContainsChapterOnlyTags(template).Should().Be(expected);
+ }
+
+ [TestClass]
+ public class ContainsTag
+ {
+ [TestMethod]
+ [DataRow("", "ch#", true)]
+ [DataRow("", "ch#", false)]
+ [DataRow("", "ch#", true)]
+ public void Tests(string template, string tag, bool expected) => Templates.ContainsTag(template, tag).Should().Be(expected);
+ }
+}
+
+namespace Templates_Folder_Tests
+{
+ [TestClass]
+ public class IsValid
+ {
+ [TestMethod]
+ public void null_is_invalid() => Tests(null, false);
+
+ [TestMethod]
+ public void empty_is_valid() => Tests("", true);
+
+ [TestMethod]
+ public void whitespace_is_valid() => Tests(" ", true);
+
+ [TestMethod]
+ [DataRow(@"C:\", false)]
+ [DataRow(@"foo", true)]
+ [DataRow(@"\foo", true)]
+ [DataRow(@"foo\", true)]
+ [DataRow(@"\foo\", true)]
+ [DataRow(@"foo\bar", true)]
+ [DataRow(@"", true)]
+ [DataRow(@"\", true)]
+ public void Tests(string template, bool expected) => Templates.Folder.IsValid(template).Should().Be(expected);
+ }
+
+ [TestClass]
+ public class IsRecommended
+ {
+ [TestMethod]
+ public void null_is_not_recommended() => Tests(null, false);
+
+ [TestMethod]
+ public void empty_is_not_recommended() => Tests("", false);
+
+ [TestMethod]
+ public void whitespace_is_not_recommended() => Tests(" ", false);
+
+ [TestMethod]
+ [DataRow(@"no tags", false)]
+ [DataRow(@"\foo\bar", true)]
+ [DataRow(" ", false)]
+ [DataRow(" chapter tag", false)]
+ public void Tests(string template, bool expected) => Templates.Folder.IsRecommended(template).Should().Be(expected);
+ }
+
+ [TestClass]
+ public class TagCount
+ {
+ [TestMethod]
+ public void null_is_not_recommended() => Assert.ThrowsException(() => Tests(null, -1));
+
+ [TestMethod]
+ public void empty_is_not_recommended() => Tests("", 0);
+
+ [TestMethod]
+ public void whitespace_is_not_recommended() => Tests(" ", 0);
+
+ [TestMethod]
+ [DataRow("no tags", 0)]
+ [DataRow(@"\foo\bar", 1)]
+ [DataRow(" ", 2)]
+ [DataRow(" >", 1)]
+ [DataRow(" ", 2)]
+ [DataRow("id> ", 0)]
+ [DataRow(" non-folder tag", 0)]
+ [DataRow(" case specific", 0)]
+ public void Tests(string template, int expected) => Templates.Folder.TagCount(template).Should().Be(expected);
+ }
+}
+
+namespace Templates_File_Tests
+{
+ [TestClass]
+ public class IsValid
+ {
+ [TestMethod]
+ public void null_is_invalid() => Tests(null, false);
+
+ [TestMethod]
+ public void empty_is_valid() => Tests("", true);
+
+ [TestMethod]
+ public void whitespace_is_valid() => Tests(" ", true);
+
+ [TestMethod]
+ [DataRow(@"C:\", false)]
+ [DataRow(@"foo", true)]
+ [DataRow(@"\foo", false)]
+ [DataRow(@"/foo", false)]
+ [DataRow(@"", true)]
+ public void Tests(string template, bool expected) => Templates.File.IsValid(template).Should().Be(expected);
+ }
+
+ // same as Templates.Folder.IsRecommended
+ //[TestClass]
+ //public class IsRecommended { }
+
+ // same as Templates.Folder.TagCount
+ //[TestClass]
+ //public class TagCount { }
+}
+
+namespace Templates_ChapterFile_Tests
+{
+ // same as Templates.File.IsValid
+ //[TestClass]
+ //public class IsValid { }
+
+ [TestClass]
+ public class IsRecommended
+ {
+ [TestMethod]
+ public void null_is_not_recommended() => Tests(null, false);
+
+ [TestMethod]
+ public void empty_is_not_recommended() => Tests("", false);
+
+ [TestMethod]
+ public void whitespace_is_not_recommended() => Tests(" ", false);
+
+ [TestMethod]
+ [DataRow(@"no tags", false)]
+ [DataRow(@"\foo\bar", false)]
+ [DataRow(" ", true)]
+ [DataRow(" -- chapter tag", true)]
+ [DataRow(" -- chapter tag but not ch# or ch_#", false)]
+ public void Tests(string template, bool expected) => Templates.ChapterFile.IsRecommended(template).Should().Be(expected);
+ }
+
+ [TestClass]
+ public class TagCount
+ {
+ [TestMethod]
+ public void null_is_not_recommended() => Assert.ThrowsException(() => Tests(null, -1));
+
+ [TestMethod]
+ public void empty_is_not_recommended() => Tests("", 0);
+
+ [TestMethod]
+ public void whitespace_is_not_recommended() => Tests(" ", 0);
+
+ [TestMethod]
+ [DataRow("no tags", 0)]
+ [DataRow(@"\foo\bar", 1)]
+ [DataRow(" ", 2)]
+ [DataRow(" >", 1)]
+ [DataRow(" ", 2)]
+ [DataRow("id> ", 0)]
+ [DataRow(" non-folder tag", 1)]
+ [DataRow(" case specific", 0)]
+ public void Tests(string template, int expected) => Templates.ChapterFile.TagCount(template).Should().Be(expected);
+ }
+}