From ed26d246f1f32142f4eddc52711869d5f54747cf Mon Sep 17 00:00:00 2001 From: Aragas Date: Sun, 17 Oct 2021 02:01:21 +0300 Subject: [PATCH] Test --- .gitmodules | 4 - build/DocFx.Plugin.LastModified | 1 - .../CommitDataType.cs | 8 + .../DocFx.Plugin.LastModified.csproj | 28 +++ .../Helpers/RepositoryHelper.cs | 47 +++++ .../Helpers/StringHelper.cs | 42 +++++ .../LastModifiedPostProcessor.cs | 171 ++++++++++++++++++ 7 files changed, 296 insertions(+), 5 deletions(-) delete mode 100644 .gitmodules delete mode 160000 build/DocFx.Plugin.LastModified create mode 100644 build/DocFx.Plugin.LastModified/CommitDataType.cs create mode 100644 build/DocFx.Plugin.LastModified/DocFx.Plugin.LastModified.csproj create mode 100644 build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs create mode 100644 build/DocFx.Plugin.LastModified/Helpers/StringHelper.cs create mode 100644 build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 75865c7..0000000 --- a/.gitmodules +++ /dev/null @@ -1,4 +0,0 @@ -[submodule "build/DocFx.Plugin.LastModified"] - path = build/DocFx.Plugin.LastModified - url = https://github.com/Still34/DocFx.Plugin.LastModified.git - branch = master diff --git a/build/DocFx.Plugin.LastModified b/build/DocFx.Plugin.LastModified deleted file mode 160000 index 073ba66..0000000 --- a/build/DocFx.Plugin.LastModified +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 073ba660e88509980835219ae92c17a0da8c2bb7 diff --git a/build/DocFx.Plugin.LastModified/CommitDataType.cs b/build/DocFx.Plugin.LastModified/CommitDataType.cs new file mode 100644 index 0000000..9080cea --- /dev/null +++ b/build/DocFx.Plugin.LastModified/CommitDataType.cs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000..99636fc --- /dev/null +++ b/build/DocFx.Plugin.LastModified/DocFx.Plugin.LastModified.csproj @@ -0,0 +1,28 @@ + + + + net461 + LastModifiedPostProcessor + DocFx.Plugin.LastModified + 1.2.4 + https://github.com/Still34/DocFx.Plugin.LastModified + https://github.com/Still34/DocFx.Plugin.LastModified/blob/master/LICENSE + GitHub + DocFX + A post-processor plugin for DocFX v2 that adds the last commit date of the conceptual document to the end of the page. + Still Hsu © 2021 + + Still Hsu + + 1.2.5 + + + + + + + + + + + diff --git a/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs b/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs new file mode 100644 index 0000000..c4d3aee --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Helpers/RepositoryHelper.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Linq; +using LibGit2Sharp; +using Microsoft.DocAsCode.Common; + +namespace DocFx.Plugin.LastModified.Helpers +{ + /// + /// Provides methods for repository-related operations. + /// + public static class RepositoryHelper + { + /// + /// 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) + { + if (repo == null) throw new ArgumentNullException(nameof(repo)); + if (srcPath == null) throw new ArgumentNullException(nameof(srcPath)); + + // 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}"); + + // 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(sourcePath, new CommitFilter {SortBy = CommitSortStrategies.Topological}) + .FirstOrDefault(); + Logger.LogVerbose($"Finished query for {sourcePath}."); + 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 new file mode 100644 index 0000000..2f7b043 --- /dev/null +++ b/build/DocFx.Plugin.LastModified/Helpers/StringHelper.cs @@ -0,0 +1,42 @@ +using System.Linq; + +namespace DocFx.Plugin.LastModified.Helpers +{ + 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) + { + var truncationString = "..."; + + if (value == null) return null; + if (value.Length == 0) return value; + + var alphaNumericalCharactersProcessed = 0; + + if (value.ToCharArray().Count(char.IsLetterOrDigit) <= length) return value; + + for (var i = 0; i < value.Length - truncationString.Length; i++) + { + if (char.IsLetterOrDigit(value[i])) alphaNumericalCharactersProcessed++; + + if (alphaNumericalCharactersProcessed + truncationString.Length == length) + return value.Substring(0, i + 1) + truncationString; + } + + return value; + } + } +} \ No newline at end of file diff --git a/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs b/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs new file mode 100644 index 0000000..67f6630 --- /dev/null +++ b/build/DocFx.Plugin.LastModified/LastModifiedPostProcessor.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Immutable; +using System.Composition; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using DocFx.Plugin.LastModified.Helpers; +using HtmlAgilityPack; +using LibGit2Sharp; +using Microsoft.DocAsCode.Common; +using Microsoft.DocAsCode.Plugins; + +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 + { + 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); + + foreach (var manifestItem in manifest.Files.Where(x => x.DocumentType == "Conceptual")) + foreach (var manifestItemOutputFile in manifestItem.OutputFiles) + { + 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; + } + } + + var fileLastModified = File.GetLastWriteTimeUtc(sourcePath); + Logger.LogVerbose($"Writing {fileLastModified} for {outputPath}..."); + WriteModifiedDate(outputPath, fileLastModified); + } + + // dispose repo after usage + _repo?.Dispose(); + + Logger.LogInfo($"Added modification date to {_addedFiles} conceptual articles."); + return manifest; + } + + private void WriteModifiedDate(string outputPath, DateTimeOffset modifiedDate, string commitHeader = null, + string commitBody = null) + { + 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) + { + Logger.LogDiagnostic("ArticleNode not found, returning."); + return; + } + + 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)) + { + // 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); + } + + 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); + } + } +} \ No newline at end of file