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,
+ };
+ }
+}