master
Aragas 4 years ago
parent e388c154e6
commit ed26d246f1
No known key found for this signature in database
GPG Key ID: BE336CD7B5B763A7

4
.gitmodules vendored

@ -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

@ -1 +0,0 @@
Subproject commit 073ba660e88509980835219ae92c17a0da8c2bb7

@ -0,0 +1,8 @@
namespace DocFx.Plugin.LastModified
{
public enum CommitDataType
{
Date,
Body
}
}

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net461</TargetFramework>
<AssemblyName>LastModifiedPostProcessor</AssemblyName>
<RootNamespace>DocFx.Plugin.LastModified</RootNamespace>
<Version>1.2.4</Version>
<RepositoryUrl>https://github.com/Still34/DocFx.Plugin.LastModified</RepositoryUrl>
<PackageLicenseUrl>https://github.com/Still34/DocFx.Plugin.LastModified/blob/master/LICENSE</PackageLicenseUrl>
<RepositoryType>GitHub</RepositoryType>
<PackageTags>DocFX</PackageTags>
<Description>A post-processor plugin for DocFX v2 that adds the last commit date of the conceptual document to the end of the page.</Description>
<Copyright>Still Hsu © 2021</Copyright>
<Company />
<Authors>Still Hsu</Authors>
<Product />
<PackageVersion>1.2.5</PackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="htmlagilitypack" Version="1.4.9" />
<PackageReference Include="libgit2sharp" Version="0.27.0-preview-0096" />
<PackageReference Include="Microsoft.Composition" Version="1.0.31" />
<PackageReference Include="Microsoft.DocAsCode.Common" Version="2.57.2" />
<PackageReference Include="Microsoft.DocAsCode.Plugins" Version="2.57.2" />
</ItemGroup>
</Project>

@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Linq;
using LibGit2Sharp;
using Microsoft.DocAsCode.Common;
namespace DocFx.Plugin.LastModified.Helpers
{
/// <summary>
/// Provides methods for repository-related operations.
/// </summary>
public static class RepositoryHelper
{
/// <summary>
/// Returns the commit information for the specified file.
/// </summary>
/// <param name="repo">The repository to query against.</param>
/// <param name="srcPath">The path of the file.</param>
/// <returns>
/// A <see cref="Commit"/> object containing the information of the commit.
/// </returns>
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;
}
}
}

@ -0,0 +1,42 @@
using System.Linq;
namespace DocFx.Plugin.LastModified.Helpers
{
public static class StringHelper
{
/// <summary>
/// Truncates the string to match the specified <paramref name="length"/>.
/// </summary>
/// <remarks>
/// <para>This method is retrieved and modified from Humanizr/Humanizer.</para>
/// <para>MIT License (c)</para>
/// <para>Copyright (c) .NET Foundation and Contributors</para>
/// </remarks>
/// <param name="value">The string to truncate.</param>
/// <param name="length">The target maximum length of the string.</param>
/// <returns>
/// A truncated string based on the <paramref name="length" /> specified.
/// </returns>
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;
}
}
}

@ -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
{
/// <summary>
/// Post-processor responsible for injecting last modified date according to commit or file modified date.
/// </summary>
[Export(nameof(LastModifiedPostProcessor), typeof(IPostProcessor))]
public class LastModifiedPostProcessor : IPostProcessor
{
private int _addedFiles;
private Repository _repo;
public ImmutableDictionary<string, object> PrepareMetadata(ImmutableDictionary<string, object> metadata)
=> metadata;
public Manifest Process(Manifest manifest, string outputFolder)
{
var versionInfo = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.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 = "<span class=\"arrow-r\"></span>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++;
}
/// <summary>
/// Injects script required for collapsible dropdown menu.
/// </summary>
/// <seealso cref="!:https://github.com/jordnkr/collapsible" />
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);
}
}
}
Loading…
Cancel
Save