diff --git a/source/Helpers/FileUtils.cs b/source/Helpers/FileUtils.cs index 3fbf30f45bd4b6f4af0d7fd61a427515173c3f73..963b662eca34de27caf83bd1864b4c2539705725 100644 --- a/source/Helpers/FileUtils.cs +++ b/source/Helpers/FileUtils.cs @@ -11,26 +11,6 @@ namespace SuCoS.Helpers; /// public static class FileUtils { - /// - /// Gets all Markdown files in the specified directory. - /// - /// The directory path. - /// The initial directory path. - /// The list of Markdown file paths. - public static IEnumerable GetAllMarkdownFiles(string directory, string? basePath = null) - { - basePath ??= directory; - var files = Directory.GetFiles(directory, "*.md").ToList(); - - var subdirectories = Directory.GetDirectories(directory); - foreach (var subdirectory in subdirectories) - { - files.AddRange(GetAllMarkdownFiles(subdirectory, basePath)); - } - - return files; - } - /// /// Gets the content of a template file based on the frontmatter and the theme path. /// diff --git a/source/Helpers/SiteHelper.cs b/source/Helpers/SiteHelper.cs index 0207a5f4381bd57a8ab55986089a97b66d09774b..5dcc818b3161b6903aeb750c653483fea6443f8b 100644 --- a/source/Helpers/SiteHelper.cs +++ b/source/Helpers/SiteHelper.cs @@ -36,11 +36,11 @@ public static class SiteHelper site.ResetCache(); - // Scan content files - var markdownFiles = FileUtils.GetAllMarkdownFiles(site.SourceContentPath); - site.ContentPaths.AddRange(markdownFiles); + stopwatch.Start("Parse"); - site.ParseSourceFiles(stopwatch); + site.ParseAndScanSourceFiles(site.SourceContentPath); + + stopwatch.Stop("Parse", site.filesParsedToReport); site.TemplateOptions.FileProvider = new PhysicalFileProvider(Path.GetFullPath(site.SourceThemePath)); @@ -109,7 +109,7 @@ public static class SiteHelper site.Logger = logger; site.options = options; site.SourceDirectoryPath = options.Source; - site.OutputPath = options.Output; + site.OutputPath = options.Output!; // Liquid template options, needed to theme the content // but also parse URLs diff --git a/source/Models/Frontmatter.cs b/source/Models/Frontmatter.cs index 621f4c2a80e91a7dc1695a6029fb58535fe2c8c6..378e3782f374bd3b29e11ba048949f26d86d14fd 100644 --- a/source/Models/Frontmatter.cs +++ b/source/Models/Frontmatter.cs @@ -89,6 +89,12 @@ public class Frontmatter : IBaseContent, IParams [YamlIgnore] public string? SourcePathDirectory { get; set; } + /// + /// The source directory of the file. + /// + [YamlIgnore] + public string? SourcePathLastDirectory => Path.GetDirectoryName(SourcePathDirectory ?? string.Empty); + /// /// Point to the site configuration. /// @@ -120,6 +126,13 @@ public class Frontmatter : IBaseContent, IParams [YamlIgnore] public ConcurrentBag? PagesReferences { get; set; } + /// + /// Other content that mention this content. + /// Used to create the tags list and Related Posts section. + /// + [YamlIgnore] + public Frontmatter? Parent { get; set; } + /// /// A list of tags, if any. /// @@ -315,8 +328,22 @@ public class Frontmatter : IBaseContent, IParams URLforce ??= URL ?? (isIndex - ? "{{ page.SourcePathDirectory }}" - : @"{{ page.SourcePathDirectory }}/{%- liquid + ? @"{%- liquid +if page.Parent +echo page.Parent.Permalink +echo '/' +endif +if page.Title != '' +echo page.Title +else +echo page.SourcePathLastDirectory +endif +-%}" + : @"{%- liquid +if page.Parent +echo page.Parent.Permalink +echo '/' +endif if page.Title != '' echo page.Title else diff --git a/source/Models/Site.cs b/source/Models/Site.cs index a3159fbbac33db9e2a2a580160e5caedc9c14a39..da1b57691bde36875dabdb2b287c5281e9f7b73e 100644 --- a/source/Models/Site.cs +++ b/source/Models/Site.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using System.Threading; @@ -7,7 +8,6 @@ using System.Threading.Tasks; using Fluid; using Markdig; using Serilog; -using SuCoS.Helpers; using SuCoS.Parser; using YamlDotNet.Serialization; @@ -119,12 +119,6 @@ public class Site : IParams /// public Frontmatter? Home { get; private set; } - /// - /// List of all content to be scanned and processed. - /// - [YamlIgnore] - public List ContentPaths { get; } = new(); - /// /// Command line options /// @@ -189,6 +183,11 @@ public class Site : IParams private List? regularPagesCache; + /// + /// Number of files parsed, used in the report. + /// + public int filesParsedToReport; + /// /// Markdig 20+ built-in extensions /// @@ -229,20 +228,99 @@ public class Site : IParams IgnoreCacheBefore = DateTime.Now; } + /// + /// Search recursively for all markdown files in the content folder, then + /// parse their content for front matter meta data and markdown. + /// + /// Folder to scan + /// Folder recursive level + /// Page of the upper directory + /// + public void ParseAndScanSourceFiles(string directory, int level = 0, Frontmatter? pageParent = null) + { + directory ??= SourceContentPath; + + var markdownFiles = Directory.GetFiles(directory, "*.md"); + + var indexPath = markdownFiles.FirstOrDefault(file => Path.GetFileName(file).ToUpperInvariant() == "INDEX.MD"); + if (indexPath != null) + { + markdownFiles = markdownFiles.Where(file => file != indexPath).ToArray(); + var frontmatter = ParseSourceFile(pageParent, indexPath); + if (level == 0) + { + Home = frontmatter; + frontmatter!.Permalink = "/"; + PagesDict.Remove(frontmatter.Permalink); + PagesDict.Add(frontmatter.Permalink, frontmatter); + } + else + { + pageParent = frontmatter; + } + } + else if (level == 0) + { + Home = CreateIndexPage(string.Empty); + } + else if (level == 1) + { + var section = directory; + var contentTemplate = new BasicContent( + title: section, + section: "section", + type: "section", + url: section + ); + pageParent = CreateAutomaticFrontmatter(contentTemplate, null); + } + + _ = Parallel.ForEach(markdownFiles, filePath => + { + ParseSourceFile(pageParent, filePath); + }); + + var subdirectories = Directory.GetDirectories(directory); + foreach (var subdirectory in subdirectories) + { + ParseAndScanSourceFiles(subdirectory, level + 1, pageParent); + } + } + + private Frontmatter? ParseSourceFile(Frontmatter? pageParent, string filePath) + { + Frontmatter? frontmatter = null; + try + { + frontmatter = frontmatterParser.ParseFrontmatterAndMarkdownFromFile(this, filePath, SourceContentPath) + ?? throw new FormatException($"Error parsing frontmatter for {filePath}"); + + if (frontmatter.IsValidDate(options)) + { + PostProcessFrontMatter(frontmatter, pageParent, true); + } + } + catch (Exception ex) + { + Logger?.Error(ex, "Error parsing file {file}", filePath); + } + + // Use interlocked to safely increment the counter in a multi-threaded environment + _ = Interlocked.Increment(ref filesParsedToReport); + + return frontmatter; + } + /// /// Create a page not from the content folder, but as part of the process. /// It's used to create tag pages, section list pages, etc. /// - public Frontmatter CreateAutomaticFrontmatter(BasicContent baseContent, Frontmatter originalFrontmatter) + public Frontmatter CreateAutomaticFrontmatter(BasicContent baseContent, Frontmatter? originalFrontmatter) { if (baseContent is null) { throw new ArgumentNullException(nameof(baseContent)); } - if (originalFrontmatter is null) - { - throw new ArgumentNullException(nameof(originalFrontmatter)); - } var id = baseContent.URL; Frontmatter? frontmatter; @@ -270,15 +348,17 @@ public class Site : IParams } } - if (frontmatter.Kind != Kind.index && originalFrontmatter.Permalink is not null) + if (frontmatter.Kind != Kind.index && originalFrontmatter?.Permalink is not null) { frontmatter.PagesReferences!.Add(originalFrontmatter.Permalink!); } + // TODO: still too hardcoded - if (frontmatter.Type != "tags") + if (frontmatter.Type != "tags" || originalFrontmatter is null) + { return frontmatter; - + } lock (originalFrontmatter) { originalFrontmatter.Tags ??= new(); @@ -287,52 +367,6 @@ public class Site : IParams return frontmatter; } - /// - /// Parses the source files and extracts the frontmatter. - /// - public void ParseSourceFiles(StopwatchReporter stopwatch) - { - if (stopwatch is null) - { - throw new ArgumentNullException(nameof(stopwatch)); - } - - stopwatch.Start("Parse"); - - // Process the source files, extracting the frontmatter - var filesParsed = 0; // counter to keep track of the number of files processed - _ = Parallel.ForEach(ContentPaths, filePath => - { - try - { - var frontmatter = frontmatterParser.ParseFrontmatterAndMarkdownFromFile(this, filePath, SourceContentPath) - ?? throw new FormatException($"Error parsing frontmatter for {filePath}"); - - if (frontmatter.IsValidDate(options)) - { - PostProcessFrontMatter(frontmatter, true); - } - } - catch (Exception ex) - { - Logger?.Error(ex, "Error parsing file {file}", filePath); - } - - // Use interlocked to safely increment the counter in a multi-threaded environment - _ = Interlocked.Increment(ref filesParsed); - }); - - // If the home page is not yet created, create it! - if (!PagesDict.TryGetValue("/", out var home)) - { - home = CreateIndexPage(string.Empty); - } - Home = home; - home.Kind = Kind.index; - - stopwatch.Stop("Parse", filesParsed); - } - /// /// Creates the frontmatter for the index page. /// @@ -359,15 +393,17 @@ public class Site : IParams /// /// Extra calculation and automatic data for each frontmatter. /// - /// + /// The given page to be processed + /// The parent page, if any /// - public void PostProcessFrontMatter(Frontmatter frontmatter, bool overwrite = false) + public void PostProcessFrontMatter(Frontmatter frontmatter, Frontmatter? pageParent = null, bool overwrite = false) { if (frontmatter is null) { throw new ArgumentNullException(nameof(frontmatter)); } + frontmatter.Parent = pageParent; frontmatter.Permalink = frontmatter.CreatePermalink(); lock (syncLockPostProcess) { @@ -398,20 +434,5 @@ public class Site : IParams } } } - - // Create a section page when due - if (frontmatter.Type == "section" - || string.IsNullOrEmpty(frontmatter.Permalink) - || string.IsNullOrEmpty(frontmatter.Section)) - { - return; - } - var contentTemplate = new BasicContent( - title: frontmatter.Section, - section: "section", - type: "section", - url: frontmatter.Section - ); - CreateAutomaticFrontmatter(contentTemplate, frontmatter); } -} +} \ No newline at end of file diff --git a/test/Models/FrontmatterTests.cs b/test/Models/FrontmatterTests.cs index d94e08b1dad1a8298ab59b11eee285867e18b4d4..b6dcb78c2305098cf2034c19f376e58888089546 100644 --- a/test/Models/FrontmatterTests.cs +++ b/test/Models/FrontmatterTests.cs @@ -135,8 +135,8 @@ public class FrontmatterTests } [Theory] - [InlineData("/test/path", "/test/path/test-title")] - [InlineData("/another/path", "/another/path/test-title")] + [InlineData("/test/path", "/test-title")] + [InlineData("/another/path", "/test-title")] public void CreatePermalink_ShouldReturnCorrectUrl_WhenUrlIsNull(string sourcePathDirectory, string expectedUrl) { var frontmatter = new Frontmatter(titleCONST, sourcePathCONST, site) @@ -149,7 +149,7 @@ public class FrontmatterTests } [Theory] - [InlineData(null, "/path/to/test-title")] + [InlineData(null, "/test-title")] [InlineData("{{ page.Title }}/{{ page.SourceFileNameWithoutExtension }}", "/test-title/file")] public void Permalink_CreateWithDefaultOrCustomURLTemplate(string urlTemplate, string expectedPermalink) { diff --git a/test/Models/SiteTests.cs b/test/Models/SiteTests.cs index da57db3b71a331bd52ac926a57840449b74721db..f5364b815589b7473e728c7933b739b92d45a4ad 100644 --- a/test/Models/SiteTests.cs +++ b/test/Models/SiteTests.cs @@ -31,14 +31,15 @@ public class SiteTests [InlineData("test02.md")] public void Test_ScanAllMarkdownFiles(string fileName) { + var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); var siteFullPath = Path.GetFullPath(Path.Combine(testSitesPath, testSite1PathCONST)); // Act - var ContentPaths = FileUtils.GetAllMarkdownFiles(Path.Combine(siteFullPath, "content")); - var fileFullPath = Path.Combine(siteFullPath, "content", fileName); + site.ParseAndScanSourceFiles(siteFullPath); // Assert - Assert.Contains(ContentPaths, rp => rp == fileFullPath); + Assert.Contains(site.Pages, page => page.SourcePathDirectory?.Length == 0); + Assert.Contains(site.Pages, page => page.SourceFileNameWithoutExtension == fileNameWithoutExtension); } [Theory]