Compare commits

...

7 Commits

Author SHA1 Message Date
Vitalii Mikhailov 43b4ef364e
Added original repo 2 years ago
Vitalii Mikhailov b988194972
Fix 2 years ago
Vitalii Mikhailov 86f3c3a5ec
Fix 2 years ago
Vitalii Mikhailov ef330a3672
Fix 2 years ago
Vitalii Mikhailov c74958f1b8
Fix 2 years ago
Vitalii Mikhailov 16bcbb313d
Fix 2 years ago
Vitalii Mikhailov afdac21450
Updated DocFX 2 years ago

@ -11,7 +11,7 @@ inputs:
docfx-version:
description: ''
default: '2.66.1'
default: '2.74.1'
newtonsoftjson-version:
description: ''
@ -26,10 +26,10 @@ runs:
submodules: recursive
fetch-depth: 0
- name: Setup .NET 7
- name: Setup .NET 8
uses: actions/setup-dotnet@master
with:
dotnet-version: 7.x.x
dotnet-version: 8.x.x
- name: Setup BUTR GPR
uses: actions/setup-dotnet@master
@ -47,28 +47,28 @@ runs:
shell: pwsh
- name: Build DocFx.Plugin.LastModified
run: dotnet build ${{ github.action_path }}/build/DocFx.Plugin.LastModified --configuration Release --output ${{inputs.docs-directory}}/_template/last-modified/plugins;
run: dotnet publish ${{ github.action_path }}/build/DocFx.Plugin.LastModified --configuration Release --output ${{inputs.docs-directory}}/_template/last-modified/plugins;
shell: pwsh
# When a custom fork is needed
- uses: actions/checkout@v3
with:
repository: BUTR/docfx
path: _actions_docfx
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install
working-directory: _actions_docfx/templates
shell: pwsh
- run: npm run build
working-directory: _actions_docfx/templates
shell: pwsh
- run: dotnet pack _actions_docfx/src/docfx -c Release -o _actions_docfx/drop/nuget
shell: pwsh
- run: dotnet tool update --global docfx
working-directory: _actions_docfx
shell: pwsh
# - name: Install DocFX
# run: dotnet tool update -g docfx
# - uses: actions/checkout@v3
# with:
# repository: BUTR/docfx
# path: _actions_docfx
# - uses: actions/setup-node@v4
# with:
# node-version: 16
# - run: npm install
# working-directory: _actions_docfx/templates
# shell: pwsh
# - run: npm run build
# working-directory: _actions_docfx/templates
# shell: pwsh
# - run: dotnet pack _actions_docfx/src/docfx -c Release -o _actions_docfx/drop/nuget
# shell: pwsh
# - run: dotnet tool update --global docfx
# working-directory: _actions_docfx
# shell: pwsh
- name: Install DocFX
run: dotnet tool update -g docfx
shell: pwsh

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

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>LastModifiedPostProcessor</AssemblyName>
<RootNamespace>DocFx.Plugin.LastModified</RootNamespace>
<Version>1.2.4</Version>
<Version>1.2.5</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>
@ -15,14 +15,16 @@
<Authors>Still Hsu</Authors>
<Product />
<PackageVersion>1.2.5</PackageVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="htmlagilitypack" Version="1.11.46" />
<PackageReference Include="libgit2sharp" Version="0.27.2" />
<PackageReference Include="Docfx.Common" Version="2.74.1" />
<PackageReference Include="Docfx.Plugins" Version="2.74.1" />
<PackageReference Include="htmlagilitypack" Version="1.11.54" />
<PackageReference Include="libgit2sharp" Version="0.29.0" />
<PackageReference Include="Microsoft.Composition" Version="1.0.31" />
<PackageReference Include="Microsoft.DocAsCode.Common" Version="2.66.1" />
<PackageReference Include="Microsoft.DocAsCode.Plugins" Version="2.66.1" />
<PackageReference Include="YamlDotNet" Version="13.7.1" />
</ItemGroup>
</Project>

@ -0,0 +1,45 @@
namespace DocFx.Plugin.LastModified.Files;
/// <summary>
/// A managed reference YAML document.
/// </summary>
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; }
}
}

@ -1,47 +1,42 @@
using System;
namespace DocFx.Plugin.LastModified.Helpers;
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>
/// Provides methods for repository-related operations.
/// Returns the commit information for the specified file.
/// </summary>
public static class RepositoryHelper
/// <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)
{
/// <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)
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;
}
}
}

@ -1,42 +1,54 @@
using System.Linq;
namespace DocFx.Plugin.LastModified.Helpers;
namespace DocFx.Plugin.LastModified.Helpers
using System.Linq;
/// <summary>
/// Extensions for <see cref="string"/>.
/// </summary>
public static class StringHelper
{
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)
{
/// <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 = "...";
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;
}
}
}

@ -0,0 +1,21 @@
namespace DocFx.Plugin.LastModified;
using System;
public record LastModifiedInfo
{
/// <summary>
/// Gets the last modified date of the file.
/// </summary>
public DateTimeOffset LastModified { get; init; }
/// <summary>
/// Gets the commit header, if any.
/// </summary>
public string CommitHeader { get; init; } = string.Empty;
/// <summary>
/// Gets the commit body, if any.
/// </summary>
public string CommitBody { get; init; } = string.Empty;
}

@ -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.Plugin.LastModified.Helpers;
using HtmlAgilityPack;
using LibGit2Sharp;
using Microsoft.DocAsCode.Common;
using Microsoft.DocAsCode.Plugins;
namespace DocFx.Plugin.LastModified
using Docfx.Common;
using Docfx.Plugins;
using Processors;
/// <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
{
/// <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);
private int _addedFiles;
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;
}
}
private IEnumerable<IProcessor> Processors { get; } = new List<IProcessor>
{
new ConceptualProcessor(),
new ManagedReferenceProcessor(),
};
var fileLastModified = File.GetLastWriteTimeUtc(sourcePath);
Logger.LogVerbose($"Writing {fileLastModified} for {outputPath}...");
WriteModifiedDate(outputPath, fileLastModified);
}
/// <inheritdoc />
public ImmutableDictionary<string, object> PrepareMetadata(ImmutableDictionary<string, object> metadata)
=> metadata;
// dispose repo after usage
_repo?.Dispose();
/// <inheritdoc />
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 = "<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);
_addedFiles++;
}
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);
}
Logger.LogInfo($"Added modification date to {_addedFiles} articles.");
return manifest;
}
}
}

@ -0,0 +1,116 @@
namespace DocFx.Plugin.LastModified.Processors;
using Docfx.Common;
using Docfx.Plugins;
using HtmlAgilityPack;
/// <summary>
/// An abstract processor for adding last modified date to articles, contains common methods.
/// </summary>
public abstract class AbstractProcessor : IProcessor
{
/// <summary>
/// Determines whether this processor supports the given type.
/// </summary>
/// <param name="type">The type of manifest item.</param>
/// <returns>True if this processor supports the given type, false otherwise.</returns>
public abstract bool Supports(string type);
/// <summary>
/// Processes the given manifest item.
/// </summary>
/// <param name="manifest">The <see cref="Manifest"/> that contains the manifest item.</param>
/// <param name="manifestItem">The <see cref="ManifestItem"/> to process.</param>
/// <param name="outputFolder">The output folder.</param>
public abstract void Process(Manifest manifest, ManifestItem manifestItem, string outputFolder);
/// <summary>
/// Attempts to process the given manifest item.
/// </summary>
/// <param name="manifest">The <see cref="Manifest"/> that contains the manifest item.</param>
/// <param name="manifestItem">The <see cref="ManifestItem"/> to process.</param>
/// <param name="outputFolder">The output folder.</param>
/// <returns>True if the manifest item was processed, false otherwise.</returns>
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;
}
/// <summary>
/// Builds the last modified info for the given file path.
/// </summary>
/// <param name="filePath">The file path.</param>
/// <returns>The <see cref="LastModifiedInfo"/> for the given file path.</returns>
protected abstract LastModifiedInfo GetLastModifiedInfo(string filePath);
/// <summary>
/// Modifies the given document with the given last modified info.
/// </summary>
/// <param name="filePath">The file path to the document.</param>
/// <param name="lastModifiedInfo">The <see cref="LastModifiedInfo"/> to use.</param>
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);
}
}

@ -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;
/// <summary>
/// Processor for adding last modified date to conceptual articles.
/// </summary>
public class ConceptualProcessor : AbstractProcessor
{
private Repository? _repo;
/// <inheritdoc />
public override bool Supports(string type)
{
return type == "Conceptual";
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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,
};
}
}

@ -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);
}

@ -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;
/// <summary>
/// Processor for adding last modified date to managed reference articles.
/// </summary>
public class ManagedReferenceProcessor : AbstractProcessor
{
private readonly Deserializer _deserializer = Deserializer.FromValueDeserializer(
new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.IgnoreUnmatchedProperties()
.BuildValueDeserializer());
private Repository? _repo;
private Manifest? _manifest;
/// <inheritdoc />
public override bool Supports(string type)
{
return type == "ManagedReference";
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
protected override LastModifiedInfo GetLastModifiedInfo(string filePath)
{
var yml = File.ReadAllText(filePath);
var managedReferenceDocument = _deserializer.Deserialize<ManagedReferenceDocument?>(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,
};
}
}

@ -0,0 +1 @@
Based on https://github.com/dhkatz/DocFx.Plugin.LastModified
Loading…
Cancel
Save