diff --git a/build/DocFx.Plugin.LastModified/CommitDataType.cs b/build/DocFx.Plugin.LastModified/CommitDataType.cs deleted file mode 100644 index 9080cea..0000000 --- a/build/DocFx.Plugin.LastModified/CommitDataType.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace DocFx.Plugin.LastModified -{ - public enum CommitDataType - { - Date, - Body - } -} \ No newline at end of file diff --git a/build/DocFx.Plugin.LastModified/DocFx.Plugin.LastModified.csproj b/build/DocFx.Plugin.LastModified/DocFx.Plugin.LastModified.csproj index 2724cdd..e926e92 100644 --- a/build/DocFx.Plugin.LastModified/DocFx.Plugin.LastModified.csproj +++ b/build/DocFx.Plugin.LastModified/DocFx.Plugin.LastModified.csproj @@ -15,6 +15,7 @@ Still Hsu 1.2.5 + enable @@ -23,6 +24,7 @@ + diff --git a/build/DocFx.Plugin.LastModified/Files/YamlManagedReference.cs b/build/DocFx.Plugin.LastModified/Files/YamlManagedReference.cs new file mode 100644 index 0000000..90bb59c --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Files/YamlManagedReference.cs @@ -0,0 +1,45 @@ +namespace DocFx.Plugin.LastModified.Files; + +/// +/// A managed reference YAML document. +/// +public class ManagedReferenceDocument +{ + public required ManagedReferenceItem[]? Items { get; set; } + + public class ManagedReferenceItem + { + public required string Uid { get; set; } + + public required string CommentId { get; set; } + + public required string Id { get; set; } + + public required string Name { get; set; } + + public required string FullName { get; set; } + + public required string NameWithType { get; set; } + + public string? Type { get; set; } + + public string? Parent { get; set; } + + public string[]? Children { get; set; } + + public string[]? Langs { get; set; } + + public ManagedReferenceItemSource? Source { get; set; } + + public string[]? Assemblies { get; set; } + } + + public class ManagedReferenceItemSource + { + public required string Id { get; set; } + + public required string Path { get; set; } + + public int? StartLine { get; set; } + } +} diff --git a/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs b/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs index 05168f7..66147f3 100644 --- a/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs +++ b/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs @@ -1,47 +1,42 @@ -using System; +namespace DocFx.Plugin.LastModified.Helpers; + using System.IO; using System.Linq; -using Docfx.Common; using LibGit2Sharp; -namespace DocFx.Plugin.LastModified.Helpers +/// +/// Provides methods for repository-related operations. +/// +public static class RepositoryHelper { /// - /// Provides methods for repository-related operations. + /// Returns the commit information for the specified file. /// - public static class RepositoryHelper + /// The repository to query against. + /// The path of the file. + /// + /// A object containing the information of the commit. + /// + public static Commit? GetCommitInfo(this Repository repo, string srcPath) { - /// - /// Returns the commit information for the specified file. - /// - /// The repository to query against. - /// The path of the file. - /// - /// A object containing the information of the commit. - /// - public static Commit GetCommitInfo(this Repository repo, string srcPath) + var gitDir = repo.Info.Path + .Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var repoRoot = Path.Join(gitDir, ".."); + + if (string.IsNullOrEmpty(repoRoot)) { - if (repo == null) throw new ArgumentNullException(nameof(repo)); - if (srcPath == null) throw new ArgumentNullException(nameof(srcPath)); + throw new DirectoryNotFoundException("Cannot obtain the root directory of the repository."); + } - // Hacky solution because libgit2sharp does not provide an easy way - // to get the root dir of the repo - // and for some reason does not work with forward-slash - var repoRoot = repo.Info.Path.Replace('\\', '/').Replace(".git/", ""); - if (string.IsNullOrEmpty(repoRoot)) - throw new DirectoryNotFoundException("Cannot obtain the root directory of the repository."); - Logger.LogVerbose($"Repository root: {repoRoot}"); + var relativePath = Path + .GetRelativePath(repoRoot, Path.GetFullPath(srcPath)) + .Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - // Remove root dir from absolute path to transform into relative path - var sourcePath = srcPath.Replace('\\', '/').Replace(repoRoot, ""); - Logger.LogVerbose($"Obtaining information from {sourcePath}, from repo {repo.Info.Path}..."); + // See libgit2sharp#1520 for sort issue + var logEntry = repo.Commits + .QueryBy(relativePath, new CommitFilter { SortBy = CommitSortStrategies.Topological }) + .FirstOrDefault(); - // See libgit2sharp#1520 for sort issue - var logEntry = repo.Commits - .QueryBy(sourcePath, new CommitFilter {SortBy = CommitSortStrategies.Topological}) - .FirstOrDefault(); - Logger.LogVerbose($"Finished query for {sourcePath}."); - return logEntry?.Commit; - } + return logEntry?.Commit; } -} \ No newline at end of file +} diff --git a/build/DocFx.Plugin.LastModified/Helpers/StringHelper.cs b/build/DocFx.Plugin.LastModified/Helpers/StringHelper.cs index 2f7b043..1669cce 100644 --- a/build/DocFx.Plugin.LastModified/Helpers/StringHelper.cs +++ b/build/DocFx.Plugin.LastModified/Helpers/StringHelper.cs @@ -1,42 +1,54 @@ -using System.Linq; +namespace DocFx.Plugin.LastModified.Helpers; -namespace DocFx.Plugin.LastModified.Helpers +using System.Linq; + +/// +/// Extensions for . +/// +public static class StringHelper { - public static class StringHelper + /// + /// Truncates the string to match the specified . + /// + /// + /// This method is retrieved and modified from Humanizr/Humanizer. + /// MIT License (c) + /// Copyright (c) .NET Foundation and Contributors + /// + /// The string to truncate. + /// The target maximum length of the string. + /// + /// A truncated string based on the specified. + /// + public static string Truncate(this string value, int length) { - /// - /// Truncates the string to match the specified . - /// - /// - /// This method is retrieved and modified from Humanizr/Humanizer. - /// MIT License (c) - /// Copyright (c) .NET Foundation and Contributors - /// - /// The string to truncate. - /// The target maximum length of the string. - /// - /// A truncated string based on the specified. - /// - public static string Truncate(this string value, int length) - { - var truncationString = "..."; + const string truncationString = "..."; - if (value == null) return null; - if (value.Length == 0) return value; + if (value.Length == 0) + { + return value; + } - var alphaNumericalCharactersProcessed = 0; + var alphaNumericalCharactersProcessed = 0; - if (value.ToCharArray().Count(char.IsLetterOrDigit) <= length) return value; + if (value.ToCharArray().Count(char.IsLetterOrDigit) <= length) + { + return value; + } - for (var i = 0; i < value.Length - truncationString.Length; i++) + for (var i = 0; i < value.Length - truncationString.Length; i++) + { + if (char.IsLetterOrDigit(value[i])) { - if (char.IsLetterOrDigit(value[i])) alphaNumericalCharactersProcessed++; - - if (alphaNumericalCharactersProcessed + truncationString.Length == length) - return value.Substring(0, i + 1) + truncationString; + alphaNumericalCharactersProcessed++; } - return value; + if (alphaNumericalCharactersProcessed + truncationString.Length == length) + { + return value[.. (i + 1)] + truncationString; + } } + + return value; } -} \ No newline at end of file +} diff --git a/build/DocFx.Plugin.LastModified/LastModifiedInfo.cs b/build/DocFx.Plugin.LastModified/LastModifiedInfo.cs new file mode 100644 index 0000000..0e99791 --- /dev/null +++ b/build/DocFx.Plugin.LastModified/LastModifiedInfo.cs @@ -0,0 +1,21 @@ +namespace DocFx.Plugin.LastModified; + +using System; + +public record LastModifiedInfo +{ + /// + /// Gets the last modified date of the file. + /// + public DateTimeOffset LastModified { get; init; } + + /// + /// Gets the commit header, if any. + /// + public string CommitHeader { get; init; } = string.Empty; + + /// + /// Gets the commit body, if any. + /// + public string CommitBody { get; init; } = string.Empty; +} diff --git a/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs b/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs index af6d05c..60d916f 100644 --- a/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs +++ b/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs @@ -1,171 +1,56 @@ -using System; +namespace DocFx.Plugin.LastModified; + +using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.IO; using System.Linq; using System.Reflection; -using System.Text; using Docfx.Common; -using DocFx.Plugin.LastModified.Helpers; using Docfx.Plugins; -using HtmlAgilityPack; -using LibGit2Sharp; +using Processors; -namespace DocFx.Plugin.LastModified +/// +/// Post-processor responsible for injecting last modified date according to commit or file modified date. +/// +[Export(nameof(LastModifiedPostProcessor), typeof(IPostProcessor))] +public class LastModifiedPostProcessor : IPostProcessor { - /// - /// Post-processor responsible for injecting last modified date according to commit or file modified date. - /// - [Export(nameof(LastModifiedPostProcessor), typeof(IPostProcessor))] - public class LastModifiedPostProcessor : IPostProcessor - { - private int _addedFiles; - private Repository _repo; - - public ImmutableDictionary PrepareMetadata(ImmutableDictionary metadata) - => metadata; - - public Manifest Process(Manifest manifest, string outputFolder) - { - var versionInfo = Assembly.GetExecutingAssembly() - .GetCustomAttribute() - ?.InformationalVersion ?? - Assembly.GetExecutingAssembly().GetName().Version?.ToString(); - Logger.LogInfo($"Version: {versionInfo}"); - Logger.LogInfo("Begin adding last modified date to items..."); - - // attempt to fetch git repo from the current project - var gitDirectory = Repository.Discover(manifest.SourceBasePath); - if (gitDirectory != null) _repo = new Repository(gitDirectory); + private int _addedFiles; - foreach (var manifestItem in manifest.Files.Where(x => x.Type == "Conceptual")) - foreach (var manifestItemOutputFile in manifestItem.Output) - { - var sourcePath = Path.Combine(manifest.SourceBasePath, manifestItem.SourceRelativePath); - var outputPath = Path.Combine(outputFolder, manifestItemOutputFile.Value.RelativePath); - if (_repo != null) - { - var commitInfo = _repo.GetCommitInfo(sourcePath); - if (commitInfo != null) - { - Logger.LogVerbose("Assigning commit date..."); - var lastModified = commitInfo.Author.When; - - var commitHeaderBuilder = new StringBuilder(); - Logger.LogVerbose("Appending commit author and email..."); - commitHeaderBuilder.AppendLine($"Author: {commitInfo.Author.Name}"); - Logger.LogVerbose("Appending commit SHA..."); - commitHeaderBuilder.AppendLine($"Commit: {commitInfo.Sha}"); - - var commitHeader = commitHeaderBuilder.ToString(); - // truncate to 200 in case of huge commit body - var commitBody = commitInfo.Message.Truncate(300); - Logger.LogVerbose($"Writing {lastModified} with reason for {outputPath}..."); - WriteModifiedDate(outputPath, lastModified, commitHeader, commitBody); - continue; - } - } + private IEnumerable Processors { get; } = new List + { + new ConceptualProcessor(), + new ManagedReferenceProcessor(), + }; - var fileLastModified = File.GetLastWriteTimeUtc(sourcePath); - Logger.LogVerbose($"Writing {fileLastModified} for {outputPath}..."); - WriteModifiedDate(outputPath, fileLastModified); - } + /// + public ImmutableDictionary PrepareMetadata(ImmutableDictionary metadata) + => metadata; - // dispose repo after usage - _repo?.Dispose(); + /// + public Manifest Process(Manifest manifest, string outputFolder) + { + var versionInfo = Assembly.GetExecutingAssembly().GetName().Version; - Logger.LogInfo($"Added modification date to {_addedFiles} conceptual articles."); - return manifest; - } + Logger.LogInfo($"Version: {versionInfo}"); + Logger.LogInfo("Begin adding last modified date to items..."); - private void WriteModifiedDate(string outputPath, DateTimeOffset modifiedDate, string commitHeader = null, - string commitBody = null) + foreach (var manifestItem in manifest.Files) { - if (outputPath == null) throw new ArgumentNullException(nameof(outputPath)); - - // load the document - var htmlDoc = new HtmlDocument(); - htmlDoc.Load(outputPath); - - // check for article container - var articleNode = htmlDoc.DocumentNode.SelectSingleNode("//article[contains(@class, 'content wrap')]"); - if (articleNode == null) + var processor = Processors.FirstOrDefault(p => p.Supports(manifestItem.Type)); + if (processor == null) { - Logger.LogDiagnostic("ArticleNode not found, returning."); - return; + Logger.LogDiagnostic($"No processor found for {manifestItem.Type}, skipping."); + continue; } - var paragraphNode = htmlDoc.CreateElement("p"); - paragraphNode.InnerHtml = $"This page was last modified at {modifiedDate} (UTC)."; - var separatorNode = htmlDoc.CreateElement("hr"); - articleNode.AppendChild(separatorNode); - articleNode.AppendChild(paragraphNode); - - if (!string.IsNullOrEmpty(commitHeader)) + if (processor.TryProcess(manifest, manifestItem, outputFolder)) { - // inject collapsible container script - InjectCollapseScript(htmlDoc); - - // create collapse container - var collapsibleNode = htmlDoc.CreateElement("div"); - collapsibleNode.SetAttributeValue("class", "collapse-container last-modified"); - collapsibleNode.SetAttributeValue("id", "accordion"); - var reasonHeaderNode = htmlDoc.CreateElement("span"); - reasonHeaderNode.InnerHtml = "Commit Message"; - var reasonContainerNode = htmlDoc.CreateElement("div"); - - // inject header - var preCodeBlockNode = htmlDoc.CreateElement("pre"); - var codeBlockNode = htmlDoc.CreateElement("code"); - codeBlockNode.InnerHtml = commitHeader; - preCodeBlockNode.AppendChild(codeBlockNode); - reasonContainerNode.AppendChild(preCodeBlockNode); - - // inject body - preCodeBlockNode = htmlDoc.CreateElement("pre"); - codeBlockNode = htmlDoc.CreateElement("code"); - codeBlockNode.SetAttributeValue("class", "xml"); - codeBlockNode.InnerHtml = commitBody; - preCodeBlockNode.AppendChild(codeBlockNode); - reasonContainerNode.AppendChild(preCodeBlockNode); - - // inject the entire block - collapsibleNode.AppendChild(reasonHeaderNode); - collapsibleNode.AppendChild(reasonContainerNode); - articleNode.AppendChild(collapsibleNode); + _addedFiles++; } - - htmlDoc.Save(outputPath); - _addedFiles++; } - /// - /// Injects script required for collapsible dropdown menu. - /// - /// - private static void InjectCollapseScript(HtmlDocument htmlDoc) - { - var bodyNode = htmlDoc.DocumentNode.SelectSingleNode("//body"); - - var accordionNode = htmlDoc.CreateElement("script"); - accordionNode.InnerHtml = @" - $( function() { - $( ""#accordion"" ).collapsible(); - } );"; - bodyNode.AppendChild(accordionNode); - - var collapsibleScriptNode = htmlDoc.CreateElement("script"); - collapsibleScriptNode.SetAttributeValue("type", "text/javascript"); - collapsibleScriptNode.SetAttributeValue("src", - "https://cdn.rawgit.com/jordnkr/collapsible/master/jquery.collapsible.min.js"); - bodyNode.AppendChild(collapsibleScriptNode); - - var headNode = htmlDoc.DocumentNode.SelectSingleNode("//head"); - var collapsibleCssNode = htmlDoc.CreateElement("link"); - collapsibleCssNode.SetAttributeValue("rel", "stylesheet"); - collapsibleCssNode.SetAttributeValue("href", - "https://cdn.rawgit.com/jordnkr/collapsible/master/collapsible.css"); - headNode.AppendChild(collapsibleCssNode); - } + Logger.LogInfo($"Added modification date to {_addedFiles} articles."); + return manifest; } -} \ No newline at end of file +} diff --git a/build/DocFx.Plugin.LastModified/Processors/AbstractProcessor.cs b/build/DocFx.Plugin.LastModified/Processors/AbstractProcessor.cs new file mode 100644 index 0000000..125512a --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Processors/AbstractProcessor.cs @@ -0,0 +1,116 @@ +namespace DocFx.Plugin.LastModified.Processors; + +using Docfx.Common; +using Docfx.Plugins; +using HtmlAgilityPack; + +/// +/// An abstract processor for adding last modified date to articles, contains common methods. +/// +public abstract class AbstractProcessor : IProcessor +{ + /// + /// Determines whether this processor supports the given type. + /// + /// The type of manifest item. + /// True if this processor supports the given type, false otherwise. + public abstract bool Supports(string type); + + /// + /// Processes the given manifest item. + /// + /// The that contains the manifest item. + /// The to process. + /// The output folder. + public abstract void Process(Manifest manifest, ManifestItem manifestItem, string outputFolder); + + /// + /// Attempts to process the given manifest item. + /// + /// The that contains the manifest item. + /// The to process. + /// The output folder. + /// True if the manifest item was processed, false otherwise. + public bool TryProcess(Manifest manifest, ManifestItem manifestItem, string outputFolder) + { + if (!Supports(manifestItem.Type)) + { + Logger.LogVerbose($"Skipping {manifestItem.Type}..."); + return false; + } + + Logger.LogDiagnostic($"Processing {manifestItem.Type}..."); + Process(manifest, manifestItem, outputFolder); + return true; + } + + /// + /// Builds the last modified info for the given file path. + /// + /// The file path. + /// The for the given file path. + protected abstract LastModifiedInfo GetLastModifiedInfo(string filePath); + + /// + /// Modifies the given document with the given last modified info. + /// + /// The file path to the document. + /// The to use. + protected virtual void ModifyDocument(string filePath, LastModifiedInfo lastModifiedInfo) + { + var htmlDocument = new HtmlDocument(); + htmlDocument.Load(filePath); + + var articleNode = htmlDocument.DocumentNode.SelectSingleNode("//article"); + if (articleNode == null) + { + Logger.LogWarning($"No article node found in {filePath}."); + return; + } + + var separatorNode = htmlDocument.CreateElement("hr"); + articleNode.AppendChild(separatorNode); + + var lastModifiedNode = htmlDocument.CreateElement("div"); + lastModifiedNode.SetAttributeValue("class", "last-modified"); + articleNode.AppendChild(lastModifiedNode); + + var paragraphNode = htmlDocument.CreateElement("p"); + paragraphNode.InnerHtml = $"This page was last modified at {lastModifiedInfo.LastModified} (UTC)."; + lastModifiedNode.AppendChild(paragraphNode); + + if (string.IsNullOrEmpty(lastModifiedInfo.CommitHeader)) + { + htmlDocument.Save(filePath); + return; + } + + var collapsibleNode = htmlDocument.CreateElement("details"); + lastModifiedNode.AppendChild(collapsibleNode); + + var reasonHeaderNode = htmlDocument.CreateElement("summary"); + reasonHeaderNode.SetAttributeValue("style", "display: list-item;"); + reasonHeaderNode.InnerHtml = "Commit Message"; + collapsibleNode.AppendChild(reasonHeaderNode); + + var reasonContainerNode = htmlDocument.CreateElement("div"); + collapsibleNode.AppendChild(reasonContainerNode); + + var preCodeBlockNode = htmlDocument.CreateElement("pre"); + var codeBlockNode = htmlDocument.CreateElement("code"); + codeBlockNode.InnerHtml = lastModifiedInfo.CommitHeader; + preCodeBlockNode.AppendChild(codeBlockNode); + reasonContainerNode.AppendChild(preCodeBlockNode); + + if (!string.IsNullOrEmpty(lastModifiedInfo.CommitBody)) + { + preCodeBlockNode = htmlDocument.CreateElement("pre"); + codeBlockNode = htmlDocument.CreateElement("code"); + codeBlockNode.InnerHtml = lastModifiedInfo.CommitBody; + preCodeBlockNode.AppendChild(codeBlockNode); + reasonContainerNode.AppendChild(preCodeBlockNode); + } + + htmlDocument.Save(filePath); + } +} diff --git a/build/DocFx.Plugin.LastModified/Processors/ConceptualProcessor.cs b/build/DocFx.Plugin.LastModified/Processors/ConceptualProcessor.cs new file mode 100644 index 0000000..1b4fae4 --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Processors/ConceptualProcessor.cs @@ -0,0 +1,74 @@ +namespace DocFx.Plugin.LastModified.Processors; + +using System; +using System.IO; +using System.Text; +using Docfx.Common; +using Docfx.Plugins; +using Helpers; +using LibGit2Sharp; + +/// +/// Processor for adding last modified date to conceptual articles. +/// +public class ConceptualProcessor : AbstractProcessor +{ + private Repository? _repo; + + /// + public override bool Supports(string type) + { + return type == "Conceptual"; + } + + /// + public override void Process(Manifest manifest, ManifestItem manifestItem, string outputFolder) + { + var repository = Repository.Discover(manifest.SourceBasePath); + if (repository != null) + { + _repo = new Repository(repository); + } + + var sourcePath = Path.Combine(manifest.SourceBasePath, manifestItem.SourceRelativePath); + var outputPath = Path.Combine(outputFolder, manifestItem.Output[".html"].RelativePath); + + var lastModifiedInfo = GetLastModifiedInfo(sourcePath); + + ModifyDocument(outputPath, lastModifiedInfo); + } + + /// + protected override LastModifiedInfo GetLastModifiedInfo(string filePath) + { + var lastModified = DateTimeOffset.MinValue; + var commitHeader = string.Empty; + var commitBody = string.Empty; + + if (_repo?.GetCommitInfo(filePath) is { } commitInfo) + { + lastModified = commitInfo.Author.When; + Logger.LogDiagnostic($"Last modified date: {lastModified} (UTC)"); + + var commitHeaderBuilder = new StringBuilder(); + commitHeaderBuilder.AppendLine($"Author: {commitInfo.Author.Name}"); + commitHeaderBuilder.AppendLine($"Commit: {commitInfo.Sha}"); + + commitHeader = commitHeaderBuilder.ToString(); + commitBody = commitInfo.Message.Truncate(300); + } + + if (lastModified == DateTimeOffset.MinValue) + { + lastModified = File.GetLastWriteTimeUtc(filePath); + Logger.LogVerbose($"Last modified date: {lastModified} (UTC)"); + } + + return new LastModifiedInfo + { + LastModified = lastModified, + CommitHeader = commitHeader, + CommitBody = commitBody, + }; + } +} diff --git a/build/DocFx.Plugin.LastModified/Processors/IProcessor.cs b/build/DocFx.Plugin.LastModified/Processors/IProcessor.cs new file mode 100644 index 0000000..dced165 --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Processors/IProcessor.cs @@ -0,0 +1,12 @@ +namespace DocFx.Plugin.LastModified.Processors; + +using Docfx.Plugins; + +public interface IProcessor +{ + bool Supports(string type); + + void Process(Manifest manifest, ManifestItem manifestItem, string outputFolder); + + bool TryProcess(Manifest manifest, ManifestItem manifestItem, string outputFolder); +} \ No newline at end of file diff --git a/build/DocFx.Plugin.LastModified/Processors/ManagedReferenceProcessor.cs b/build/DocFx.Plugin.LastModified/Processors/ManagedReferenceProcessor.cs new file mode 100644 index 0000000..ed5a981 --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Processors/ManagedReferenceProcessor.cs @@ -0,0 +1,93 @@ +namespace DocFx.Plugin.LastModified.Processors; + +using System; +using System.IO; +using System.Linq; +using Docfx.Common; +using Docfx.Plugins; +using Files; +using Helpers; +using LibGit2Sharp; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +/// +/// Processor for adding last modified date to managed reference articles. +/// +public class ManagedReferenceProcessor : AbstractProcessor +{ + private readonly Deserializer _deserializer = Deserializer.FromValueDeserializer( + new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .BuildValueDeserializer()); + + private Repository? _repo; + private Manifest? _manifest; + + /// + public override bool Supports(string type) + { + return type == "ManagedReference"; + } + + /// + public override void Process(Manifest manifest, ManifestItem manifestItem, string outputFolder) + { + var repository = Repository.Discover(manifest.SourceBasePath); + if (repository != null) + { + _repo = new Repository(repository); + } + + _manifest = manifest; + + var sourcePath = Path.Combine(manifest.SourceBasePath, manifestItem.SourceRelativePath); + var outputPath = Path.Combine(outputFolder, manifestItem.Output[".html"].RelativePath); + + var lastModifiedInfo = GetLastModifiedInfo(sourcePath); + + ModifyDocument(outputPath, lastModifiedInfo); + } + + /// + protected override LastModifiedInfo GetLastModifiedInfo(string filePath) + { + var yml = File.ReadAllText(filePath); + var managedReferenceDocument = _deserializer.Deserialize(yml); + var itemSource = managedReferenceDocument?.Items?.FirstOrDefault(i => !string.IsNullOrEmpty(i.Source?.Path)); + var itemSourcePath = itemSource?.Source?.Path ?? string.Empty; + + var lastModified = DateTimeOffset.MinValue; + var commitHeader = string.Empty; + var commitBody = string.Empty; + + var sourcePath = Path.Combine(_manifest?.SourceBasePath ?? string.Empty, itemSourcePath); + + if (!string.IsNullOrEmpty(itemSourcePath) && _repo?.GetCommitInfo(sourcePath) is { } commitInfo) + { + lastModified = commitInfo.Author.When; + Logger.LogDiagnostic($"Last modified date: {lastModified} (UTC)"); + + var commitHeaderBuilder = new System.Text.StringBuilder(); + commitHeaderBuilder.AppendLine($"Author: {commitInfo.Author.Name}"); + commitHeaderBuilder.AppendLine($"Commit: {commitInfo.Sha}"); + + commitHeader = commitHeaderBuilder.ToString(); + commitBody = commitInfo.Message.Truncate(300); + } + + if (lastModified == DateTimeOffset.MinValue) + { + lastModified = File.GetLastWriteTimeUtc(sourcePath); + Logger.LogVerbose($"Last modified date: {lastModified} (UTC)"); + } + + return new LastModifiedInfo + { + LastModified = lastModified, + CommitHeader = commitHeader, + CommitBody = commitBody, + }; + } +}