From f45b93e5ca5ad8f85d41289e630b20a409d845e8 Mon Sep 17 00:00:00 2001 From: Martin Tirion Date: Wed, 18 Jan 2023 16:26:48 +0100 Subject: [PATCH 1/2] Added flag for handling index.md generation with 1 file in folder --- .../DocFxTocGenerator.csproj | 1 + .../Domain/CommandlineOptions.cs | 7 + src/DocFxTocGenerator/TocGenerator.cs | 176 +++++++++++------- 3 files changed, 114 insertions(+), 70 deletions(-) diff --git a/src/DocFxTocGenerator/DocFxTocGenerator.csproj b/src/DocFxTocGenerator/DocFxTocGenerator.csproj index 7295da6..ba2dff5 100644 --- a/src/DocFxTocGenerator/DocFxTocGenerator.csproj +++ b/src/DocFxTocGenerator/DocFxTocGenerator.csproj @@ -13,5 +13,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/DocFxTocGenerator/Domain/CommandlineOptions.cs b/src/DocFxTocGenerator/Domain/CommandlineOptions.cs index 0f851b1..c0ec724 100644 --- a/src/DocFxTocGenerator/Domain/CommandlineOptions.cs +++ b/src/DocFxTocGenerator/Domain/CommandlineOptions.cs @@ -51,5 +51,12 @@ public class CommandlineOptions /// [Option('i', "index", Required = false, HelpText = "Auto-generate a file index in each folder.")] public bool AutoIndex { get; set; } + + /// + /// Gets or sets a value indicating whether an to now generate an index in a folder with 1 file. + /// Is supplementary to the -i option and doesn't work without that flag. + /// + [Option('n', "notwithone", Required = false, HelpText = "Do not auto-generate a file index when only contains 1 file. Additional to -i flag.")] + public bool NoAutoIndexWithOneFile { get; set; } } } diff --git a/src/DocFxTocGenerator/TocGenerator.cs b/src/DocFxTocGenerator/TocGenerator.cs index 2db0cc5..06ba931 100644 --- a/src/DocFxTocGenerator/TocGenerator.cs +++ b/src/DocFxTocGenerator/TocGenerator.cs @@ -13,15 +13,20 @@ namespace DocFxTocGenerator using CommandLine; using DocFxTocGenerator.Domain; using DocFxTocGenerator.Helpers; + using Microsoft.OpenApi.Readers; /// /// Toc generator. /// internal class TocGenerator { - private static CommandlineOptions options; - private static int returnvalue; - private static MessageHelper message; + private static readonly string[] _filePatternsForToc = { "*.md", "*.swagger.json" }; + private static readonly string _filePatternsForTocJoined = string.Join(", ", _filePatternsForToc); + private static readonly EnumerationOptions _caseSetting = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; + + private static CommandlineOptions _options; + private static int _returnvalue; + private static MessageHelper _message; /// /// Main entry point. @@ -34,9 +39,9 @@ private static int Main(string[] args) .WithParsed(RunLogic) .WithNotParsed(HandleErrors); - Console.WriteLine($"Exit with return code {returnvalue}"); + Console.WriteLine($"Exit with return code {_returnvalue}"); - return returnvalue; + return _returnvalue; } /// @@ -46,39 +51,39 @@ private static int Main(string[] args) /// Parsed commandline options. private static void RunLogic(CommandlineOptions o) { - options = o; - message = new MessageHelper(options); + _options = o; + _message = new MessageHelper(_options); - if (string.IsNullOrEmpty(options.OutputFolder)) + if (string.IsNullOrEmpty(_options.OutputFolder)) { - options.OutputFolder = options.DocFolder; + _options.OutputFolder = _options.DocFolder; } - message.Verbose($"Documentation folder: {options.DocFolder}"); - message.Verbose($"Output folder : {options.OutputFolder}"); - message.Verbose($"Verbose : {options.Verbose}"); - message.Verbose($"Use .order : {options.UseOrder}"); - message.Verbose($"Use .override : {options.UseOverride}"); - message.Verbose($"Use .ignore : {options.UseIgnore}"); - message.Verbose($"Auto index : {options.AutoIndex}\n"); + _message.Verbose($"Documentation folder: {_options.DocFolder}"); + _message.Verbose($"Output folder : {_options.OutputFolder}"); + _message.Verbose($"Verbose : {_options.Verbose}"); + _message.Verbose($"Use .order : {_options.UseOrder}"); + _message.Verbose($"Use .override : {_options.UseOverride}"); + _message.Verbose($"Use .ignore : {_options.UseIgnore}"); + _message.Verbose($"Auto index : {_options.AutoIndex}\n"); - if (!Directory.Exists(options.DocFolder)) + if (!Directory.Exists(_options.DocFolder)) { - message.Error($"ERROR: Documentation folder '{options.DocFolder}' doesn't exist."); - returnvalue = 1; + _message.Error($"ERROR: Documentation folder '{_options.DocFolder}' doesn't exist."); + _returnvalue = 1; return; } - if (!Directory.Exists(options.OutputFolder)) + if (!Directory.Exists(_options.OutputFolder)) { - message.Error($"ERROR: Destination folder '{options.OutputFolder}' doesn't exist."); - returnvalue = 1; + _message.Error($"ERROR: Destination folder '{_options.OutputFolder}' doesn't exist."); + _returnvalue = 1; return; } // we start at the root to generate the TOC items TocItem tocRootItems = new TocItem(); - DirectoryInfo rootDir = new DirectoryInfo(options.DocFolder); + DirectoryInfo rootDir = new DirectoryInfo(_options.DocFolder); WalkDirectoryTree(rootDir, tocRootItems); // we have the TOC, so serialize to a string @@ -91,10 +96,10 @@ private static void RunLogic(CommandlineOptions o) } // now write the TOC to disc - File.WriteAllText(Path.Combine(options.OutputFolder, "toc.yml"), sw.ToString()); + File.WriteAllText(Path.Combine(_options.OutputFolder, "toc.yml"), sw.ToString()); } - message.Verbose($"{Path.Combine(options.OutputFolder, "toc.yml")} created."); + _message.Verbose($"{Path.Combine(_options.OutputFolder, "toc.yml")} created."); } /// @@ -103,7 +108,7 @@ private static void RunLogic(CommandlineOptions o) /// List or errors (ignored). private static void HandleErrors(IEnumerable errors) { - returnvalue = 1; + _returnvalue = 1; } /// @@ -114,10 +119,10 @@ private static void HandleErrors(IEnumerable errors) /// Full path of the entry-file of this folder. private static string WalkDirectoryTree(DirectoryInfo folder, TocItem yamlNode) { - message.Verbose($"Processing folder {folder.FullName}"); + _message.Verbose($"Processing folder {folder.FullName}"); List order = GetOrderList(folder); - Dictionary overrides = options.UseOverride ? GetOverrides(folder) : new Dictionary(); + Dictionary overrides = _options.UseOverride ? GetOverrides(folder) : new Dictionary(); List ignore = GetIgnore(folder); // add doc files to the node @@ -130,25 +135,25 @@ private static string WalkDirectoryTree(DirectoryInfo folder, TocItem yamlNode) { // now sort the files and directories with the order-list and further alphabetically yamlNode.Items = new Collection(yamlNode.Items.OrderBy(x => x.Sequence).ThenBy(x => x.SortableTitle).ToList()); - message.Verbose($"Items ordered in {folder.FullName}"); + _message.Verbose($"Items ordered in {folder.FullName}"); } if (!string.IsNullOrWhiteSpace(yamlNode.Filename)) { // if indicated, add a folder index - but not for the root folder. - if (options.AutoIndex) + if (_options.AutoIndex) { string indexFile = AddIndex(folder, yamlNode, GetOverrides(folder.Parent)); if (!string.IsNullOrEmpty(indexFile)) { - yamlNode.Href = GetRelativePath(indexFile, options.DocFolder); + yamlNode.Href = GetRelativePath(indexFile, _options.DocFolder); } } else { if (yamlNode.Items != null && yamlNode.Items.Any()) { - yamlNode.Href = GetRelativePath(yamlNode.Items.First().Filename, options.DocFolder); + yamlNode.Href = GetRelativePath(yamlNode.Items.First().Filename, _options.DocFolder); } } } @@ -160,12 +165,12 @@ private static List GetIgnore(DirectoryInfo folder) { // see if we have an .order file List ignore = new List(); - if (options.UseIgnore) + if (_options.UseIgnore) { string orderFile = Path.Combine(folder.FullName, ".ignore"); if (File.Exists(orderFile)) { - message.Verbose($"Read existing order file {orderFile}"); + _message.Verbose($"Read existing order file {orderFile}"); ignore = File.ReadAllLines(orderFile).ToList(); } } @@ -182,16 +187,17 @@ private static List GetIgnore(DirectoryInfo folder) /// The overrides. private static void GetFiles(DirectoryInfo folder, List order, TocItem yamlNode, Dictionary overrides) { - message.Verbose($"Process {folder.FullName} for files."); + _message.Verbose($"Process {folder.FullName} for files."); - List files = folder - .GetFiles("*.md") + List files = + _filePatternsForToc + .SelectMany(pattern => folder.GetFiles(pattern, _caseSetting)) .OrderBy(f => f.Name) + .Where(f => f.Name.ToUpperInvariant() != "INDEX.MD") .ToList(); - - if (files == null) + if (!files.Any()) { - message.Verbose($"No MD files found in {folder.FullName}."); + _message.Verbose($"No {_filePatternsForTocJoined} files found in {folder.FullName}."); return; } @@ -210,7 +216,7 @@ private static void GetFiles(DirectoryInfo folder, List order, TocItem y } string title = string.Empty; - if (options.UseOverride && (overrides.Count > 0)) + if (_options.UseOverride && (overrides.Count > 0)) { // get possible title override from the .override file var key = fi.Name.Substring(0, fi.Name.Length - 3); @@ -227,10 +233,10 @@ private static void GetFiles(DirectoryInfo folder, List order, TocItem y Sequence = sequence, Filename = fi.FullName, Title = title, - Href = GetRelativePath(fi.FullName, options.DocFolder), + Href = GetRelativePath(fi.FullName, _options.DocFolder), }); - message.Verbose($"Add file seq={sequence} title={title} href={GetRelativePath(fi.FullName, options.DocFolder)}"); + _message.Verbose($"Add file seq={sequence} title={title} href={GetRelativePath(fi.FullName, _options.DocFolder)}"); } } @@ -247,7 +253,7 @@ private static Dictionary GetOverrides(DirectoryInfo folder) string overrideFile = Path.Combine(folder.FullName, ".override"); if (File.Exists(overrideFile)) { - message.Verbose($"Read existing overrideFile file {overrideFile}"); + _message.Verbose($"Read existing overrideFile file {overrideFile}"); foreach (var over in File.ReadAllLines(overrideFile)) { var overSplit = over.Split(';'); @@ -258,7 +264,7 @@ private static Dictionary GetOverrides(DirectoryInfo folder) } } - message.Verbose($"Found {overrides.Count} for folder {folder.FullName}"); + _message.Verbose($"Found {overrides.Count} for folder {folder.FullName}"); return overrides; } @@ -272,7 +278,7 @@ private static Dictionary GetOverrides(DirectoryInfo folder) /// The ignore. private static void GetDirectories(DirectoryInfo folder, List order, TocItem yamlNode, Dictionary overrides, List ignore) { - message.Verbose($"Process {folder.FullName} for sub-directories."); + _message.Verbose($"Process {folder.FullName} for sub-directories."); // Now find all the subdirectories under this directory. DirectoryInfo[] subDirs = folder.GetDirectories(); @@ -291,10 +297,12 @@ private static void GetDirectories(DirectoryInfo folder, List order, Toc } // Get all the md files only - FileInfo[] subFiles = dirInfo.GetFiles("*.md"); + FileInfo[] subFiles = _filePatternsForToc + .SelectMany(pattern => dirInfo.GetFiles(pattern, _caseSetting)) + .ToArray(); if (subFiles.Any() == false) { - message.Warning($"WARNING: Folder {dirInfo.FullName} skipped as it doesn't contain MD files. This might skip further sub-folders. Solve this by adding a README.md or INDEX.md in the folder."); + _message.Warning($"WARNING: Folder {dirInfo.FullName} skipped as it doesn't contain {_filePatternsForTocJoined} files. This might skip further sub-folders. Solve this by adding a README.md or INDEX.md in the folder."); continue; } @@ -307,7 +315,7 @@ private static void GetDirectories(DirectoryInfo folder, List order, Toc } string title = string.Empty; - if (options.UseOverride) + if (_options.UseOverride) { // if in the .override file, override the title with it if (overrides.ContainsKey(dirInfo.Name)) @@ -320,17 +328,31 @@ private static void GetDirectories(DirectoryInfo folder, List order, Toc title = title.Length == 0 ? ToTitleCase(dirInfo.Name) : title; newTocItem.Filename = dirInfo.FullName; newTocItem.Title = title; - string entryFile = WalkDirectoryTree(dirInfo, newTocItem); + string entryFile = string.Empty; + if (!_options.NoAutoIndexWithOneFile) + { + // when no extra indication, this will ALWAYS generate an INDEX.md + // if no index or readme exists. Independent of the number of files in the folder. + entryFile = WalkDirectoryTree(dirInfo, newTocItem); + } + if (subFiles.Length == 1 && dirInfo.GetDirectories().Length == 0) { - newTocItem.Href = GetRelativePath(subFiles[0].FullName, options.DocFolder); + newTocItem.Href = GetRelativePath(subFiles[0].FullName, _options.DocFolder); } else { - newTocItem.Href = GetRelativePath(entryFile, options.DocFolder); + if (_options.NoAutoIndexWithOneFile) + { + // when this extra indication set, this will ONLY generate an INDEX.md + // if no index or readme exists and if the folder contains more than 1 file. + entryFile = WalkDirectoryTree(dirInfo, newTocItem); + } + + newTocItem.Href = GetRelativePath(entryFile, _options.DocFolder); } - message.Verbose($"Add directory seq={newTocItem.Sequence} title={newTocItem.Title} href={newTocItem.Href}"); + _message.Verbose($"Add directory seq={newTocItem.Sequence} title={newTocItem.Title} href={newTocItem.Href}"); yamlNode.AddItem(newTocItem); } @@ -348,12 +370,12 @@ private static List GetOrderList(DirectoryInfo folder) { // see if we have an .order file List order = new List(); - if (options.UseOrder) + if (_options.UseOrder) { string orderFile = Path.Combine(folder.FullName, ".order"); if (File.Exists(orderFile)) { - message.Verbose($"Read existing order file {orderFile}"); + _message.Verbose($"Read existing order file {orderFile}"); order = File.ReadAllLines(orderFile).ToList(); } } @@ -363,7 +385,7 @@ private static List GetOrderList(DirectoryInfo folder) if (string.IsNullOrEmpty(readmeEntry)) { order.Add("README"); - message.Verbose($"'README' added to order-list"); + _message.Verbose($"'README' added to order-list"); } // we always want to order INDEX.md as well. So add it if not in list yet @@ -371,7 +393,7 @@ private static List GetOrderList(DirectoryInfo folder) if (string.IsNullOrEmpty(indexEntry)) { order.Add("index"); - message.Verbose($"'index' added to order-list"); + _message.Verbose($"'index' added to order-list"); } return order; @@ -408,7 +430,7 @@ private static string AddIndex(DirectoryInfo folder, TocItem yamlNode, Dictionar // if a new index has been created, add that to the TOC (top of list) FileInfo fi = folder.GetFiles().FirstOrDefault(x => string.Equals(x.Name, Path.GetFileName(indexFile), StringComparison.OrdinalIgnoreCase)); string title = string.Empty; - if (options.UseOverride && (overrides.Count > 0)) + if (_options.UseOverride && (overrides.Count > 0)) { string key = folder.Name; if (overrides.ContainsKey(key)) @@ -422,12 +444,12 @@ private static string AddIndex(DirectoryInfo folder, TocItem yamlNode, Dictionar Sequence = -1, Filename = indexFile, Title = title.Length == 0 ? GetCleanedFileName(fi) : title, - Href = GetRelativePath(indexFile, options.DocFolder), + Href = GetRelativePath(indexFile, _options.DocFolder), }; // insert index item at the top yamlNode.AddItem(newItem, true); - message.Verbose($"Added index.md to top of list of files."); + _message.Verbose($"Added index.md to top of list of files."); } return indexFile; @@ -446,7 +468,7 @@ private static bool WriteIndex(string outputFile, TocItem yamlNode) return false; } - message.Verbose($"Index will be written to {outputFile}"); + _message.Verbose($"Index will be written to {outputFile}"); // read lines if existing file. List lines = new List(); @@ -459,7 +481,7 @@ private static bool WriteIndex(string outputFile, TocItem yamlNode) } File.WriteAllLines(outputFile, lines); - message.Verbose($"Written {lines.Count} lines to {outputFile} (index)."); + _message.Verbose($"Written {lines.Count} lines to {outputFile} (index)."); return true; } @@ -523,20 +545,34 @@ private static void Serialize(IndentedTextWriter writer, TocItem tocItem, bool i private static string GetCleanedFileName(FileInfo fi) { string cleanedName = fi.Name; - - // Open the file, read the line up to the first #, extract the tile - using (StreamReader toRead = File.OpenText(fi.FullName)) + if (string.Equals(fi.Name, "INDEX.MD", StringComparison.OrdinalIgnoreCase)) + { + // if this is the index doc, give it the name of the folder. + cleanedName = Path.GetFileName(fi.DirectoryName); + } + else if (fi.Name.EndsWith(".md", StringComparison.OrdinalIgnoreCase)) { - while (!toRead.EndOfStream) + // For markdownfile, open the file, read the line up to the first #, extract the tile + using (StreamReader toRead = File.OpenText(fi.FullName)) { - string strTitle = toRead.ReadLine(); - if (strTitle.TrimStart(' ').StartsWith("# ", StringComparison.OrdinalIgnoreCase)) + while (!toRead.EndOfStream) { - cleanedName = strTitle.Substring(2); - break; + string strTitle = toRead.ReadLine(); + if (strTitle.TrimStart(' ').StartsWith("# ", StringComparison.OrdinalIgnoreCase)) + { + cleanedName = strTitle.Substring(2); + break; + } } } } + else if (fi.Name.EndsWith(".swagger.json", StringComparison.OrdinalIgnoreCase)) + { + // for open api swagger file, read the title from the data. + using var stream = File.OpenRead(fi.FullName); + var document = new OpenApiStreamReader().Read(stream, out _); + cleanedName = $"{document.Info.Title} {document.Info.Version}"; + } return ToTitleCase(cleanedName); } From 6eaa6d3ce67016b83fd9877a4ffec81411805288 Mon Sep 17 00:00:00 2001 From: Martin Tirion Date: Wed, 18 Jan 2023 16:45:58 +0100 Subject: [PATCH 2/2] Fixed comment --- src/DocFxTocGenerator/Domain/CommandlineOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DocFxTocGenerator/Domain/CommandlineOptions.cs b/src/DocFxTocGenerator/Domain/CommandlineOptions.cs index c0ec724..5e30dd1 100644 --- a/src/DocFxTocGenerator/Domain/CommandlineOptions.cs +++ b/src/DocFxTocGenerator/Domain/CommandlineOptions.cs @@ -53,7 +53,7 @@ public class CommandlineOptions public bool AutoIndex { get; set; } /// - /// Gets or sets a value indicating whether an to now generate an index in a folder with 1 file. + /// Gets or sets a value indicating whether NOT to generate an index in a folder with 1 file. /// Is supplementary to the -i option and doesn't work without that flag. /// [Option('n', "notwithone", Required = false, HelpText = "Do not auto-generate a file index when only contains 1 file. Additional to -i flag.")]