Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - Autotoc generation #10574

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
17 changes: 16 additions & 1 deletion src/Docfx.Build/TableOfContents/BuildTocDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Docfx.Common;
using Docfx.DataContracts.Common;
using Docfx.Plugins;

using YamlDotNet.Core.Tokens;
namespace Docfx.Build.TableOfContents;

[Export(nameof(TocDocumentProcessor), typeof(IDocumentBuildStep))]
Expand All @@ -24,6 +24,21 @@ class BuildTocDocument : BaseDocumentBuildStep
/// </summary>
public override IEnumerable<FileModel> Prebuild(ImmutableList<FileModel> models, IHostService host)
{

if (!models.Any())
{
return TocHelper.ResolveToc(models.ToImmutableList());
}

// Keep auto toc agnostic to the toc file naming convention.
var tocFileName = models.First().Key.Split('/').Last();
var tocModels = models.OrderBy(f => f.File.Split('/').Count());
var tocCache = new Dictionary<string, TocItemViewModel>();
models.ForEach(model =>
{
tocCache.Add(model.Key.Replace("\\", "/").Replace("/" + tocFileName, string.Empty), (TocItemViewModel)model.Content);
});
TocHelper.RecursivelyPopulateTocs(tocFileName, host.SourceFiles.Keys, tocCache);
return TocHelper.ResolveToc(models);
}

Expand Down
136 changes: 135 additions & 1 deletion src/Docfx.Build/TableOfContents/TocHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;

using System.Globalization;
using System.Web;
using Docfx.Common;
using Docfx.DataContracts.Common;
using Docfx.Plugins;
Expand All @@ -11,6 +12,8 @@ namespace Docfx.Build.TableOfContents;

public static class TocHelper
{
private static TextInfo TextInfo = new CultureInfo("en-US", false).TextInfo;

private static readonly YamlDeserializerWithFallback _deserializer =
YamlDeserializerWithFallback.Create<List<TocItemViewModel>>()
.WithFallback<TocItemViewModel>();
Expand Down Expand Up @@ -82,4 +85,135 @@ public static TocItemViewModel LoadSingleToc(string file)

throw new NotSupportedException($"{file} is not a valid TOC file, supported TOC files should be either \"{Constants.TableOfContents.MarkdownTocFileName}\" or \"{Constants.TableOfContents.YamlTocFileName}\".");
}

private static (bool, TocItemViewModel) TryGetOrCreateToc(Dictionary<string, TocItemViewModel> pathToToc, string currentFolderPath, HashSet<string> virtualTocPaths)
{
bool folderHasToc = false;
TocItemViewModel tocItem;
if (pathToToc.TryGetValue(currentFolderPath, out tocItem))
{
folderHasToc = true;
}
else
{
var idx = currentFolderPath.LastIndexOf('/');
if (idx != -1)
{
tocItem = new TocItemViewModel
{
Name = currentFolderPath.Substring(idx + 1),
Auto = true
};
pathToToc[currentFolderPath] = tocItem;
virtualTocPaths.Add(currentFolderPath);

}
else
{
tocItem = new TocItemViewModel();
}
}
return (folderHasToc, tocItem);
}

private static void LinkToParentToc(Dictionary<string, TocItemViewModel> tocCache, string currentFolderPath, TocItemViewModel tocItem, HashSet<string> virtualTocPaths, bool folderHasToc)
{
int idx = currentFolderPath.LastIndexOf('/');
if (idx != -1 && !currentFolderPath.EndsWith(".."))
{
// This is an existing behavior, href: ~/foldername/ doesnot work, but href: ./foldername/ does.
// var folderToProcessSanitized = currentFolderPath.Replace("~", ".") + "/";
// validate this behavior with yuefi
var parentTocFolder = currentFolderPath.Substring(0, idx);
TocItemViewModel parentToc = null;
while (idx != -1 && !tocCache.TryGetValue(parentTocFolder, out parentToc))
{
idx = parentTocFolder.LastIndexOf('/');
if (idx != -1)
{
parentTocFolder = currentFolderPath.Substring(0, idx);
}
}


if (parentToc != null)
{
var folderToProcessSanitized = currentFolderPath.Replace(parentTocFolder, ".") + "/";
if (parentToc.Items == null)
{
parentToc.Items = new List<TocItemViewModel>();
}

// Only link to parent rootToc if the auto is enabled.
if (!folderHasToc &&
parentToc.Auto.HasValue &&
parentToc.Auto.Value)
{
parentToc.Items.Add(tocItem);
}
else if (folderHasToc &&
parentToc.Auto.HasValue &&
parentToc.Auto.Value &&
!virtualTocPaths.Contains(currentFolderPath) &&
!parentToc.Items.Any(i => i.Href != null && Path.GetRelativePath(i.Href.Replace('~', '.'), folderToProcessSanitized) == "."))
{
var tocToLinkFrom = new TocItemViewModel();
tocToLinkFrom.Name = StandarizeName(Path.GetFileNameWithoutExtension(currentFolderPath));
tocToLinkFrom.Href = folderToProcessSanitized;
parentToc.Items.Add(tocToLinkFrom);
}
}
}
}

internal static void RecursivelyPopulateTocs(string tocFileName, IEnumerable<string> sourceFilePaths, Dictionary<string, TocItemViewModel> tocCache)
{
var rootToc = tocCache.GetValueOrDefault(RelativePath.WorkingFolderString);
/*if (!(rootToc != null && rootToc.Auto.HasValue && rootToc.Auto.Value))
{
Logger.LogInfo($"auto value is not set to true. skipping auto gen.");
return;
}*/
var folderPathForRootToc = RelativePath.WorkingFolderString;

// Omit the files that are outside the docfx base directory.
var fileNames = sourceFilePaths
.Where(s => !Path.GetRelativePath(folderPathForRootToc, s).Contains("..") && !s.EndsWith(tocFileName))
.Select(p => p.Replace("\\", "/"))
.OrderBy(f => f.Split('/').Count());

var virtualTocs = new HashSet<string>();
foreach (var filePath in fileNames)
{
var folderToProcess = Path.GetDirectoryName(filePath).Replace("\\", "/");

var (folderHasToc, tocToProcess) = TryGetOrCreateToc(tocCache, folderToProcess, virtualTocs);

LinkToParentToc(tocCache, folderToProcess, tocToProcess, virtualTocs, folderHasToc);

// If the rootToc we currently process didnot have auto enabled.
// There is no need to populate the rootToc, move on.
if (!tocToProcess.Auto.HasValue || (tocToProcess.Auto.HasValue && !tocToProcess.Auto.Value))
{
continue;
}

var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);

if (tocToProcess.Items == null)
{
tocToProcess.Items = new List<TocItemViewModel>();
}

if (!(tocToProcess.Items.Where(i => i.Href !=null && (i.Href.Equals(filePath) || i.Href.Equals(Path.GetFileName(filePath))))).Any())
{
var item = new TocItemViewModel();
item.Name = item.Name != null ? item.Name : StandarizeName(fileNameWithoutExtension);
item.Href = filePath;
tocToProcess.Items.Add(item);
}
}
}

internal static string StandarizeName(string name) => TextInfo.ToTitleCase(HttpUtility.UrlDecode(name)).Replace('-', ' ');
}
1 change: 1 addition & 0 deletions src/Docfx.DataContracts.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static class DocumentType

public static class PropertyName
{
public const string Auto = "auto";
public const string Uid = "uid";
public const string CommentId = "commentId";
public const string Id = "id";
Expand Down
5 changes: 5 additions & 0 deletions src/Docfx.DataContracts.Common/TocItemViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ namespace Docfx.DataContracts.Common;

public class TocItemViewModel
{
[YamlMember(Alias = Constants.PropertyName.Auto)]
[JsonProperty(Constants.PropertyName.Auto)]
[JsonPropertyName(Constants.PropertyName.Auto)]
public bool? Auto { get; set; }

[YamlMember(Alias = Constants.PropertyName.Uid)]
[JsonProperty(Constants.PropertyName.Uid)]
[JsonPropertyName(Constants.PropertyName.Uid)]
Expand Down
27 changes: 7 additions & 20 deletions test/Docfx.Build.Tests/TocDocumentProcessorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public void ProcessMarkdownTocWithComplexHrefShouldSucceed()
]
};

AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);
}

[Fact]
Expand Down Expand Up @@ -132,7 +132,7 @@ public void ProcessMarkdownTocWithAbsoluteHrefShouldSucceed()
]
};

AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);
}

[Fact]
Expand Down Expand Up @@ -204,7 +204,7 @@ public void ProcessMarkdownTocWithRelativeHrefShouldSucceed()
]
};

AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);
}

[Fact]
Expand Down Expand Up @@ -273,7 +273,7 @@ public void ProcessYamlTocWithFolderShouldSucceed()
]
};

AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);
}

[Fact]
Expand Down Expand Up @@ -335,7 +335,7 @@ public void ProcessYamlTocWithMetadataShouldSucceed()
}
]
};
AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);
}

[Fact]
Expand Down Expand Up @@ -534,7 +534,7 @@ public void ProcessYamlTocWithReferencedTocShouldSucceed()
]
};

AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);

// Referenced TOC File should exist
var referencedTocPath = Path.Combine(_outputFolder, Path.ChangeExtension(sub1tocmd, RawModelFileExtension));
Expand Down Expand Up @@ -684,7 +684,7 @@ public void ProcessYamlTocWithTocHrefShouldSucceed()
]
};

AssertTocEqual(expectedModel, model);
TocHelperTest.AssertTocEqual(expectedModel, model);
}

[Fact]
Expand Down Expand Up @@ -939,18 +939,5 @@ private void BuildDocument(FileCollection files)
builder.Build(parameters);
}

private static void AssertTocEqual(TocItemViewModel expected, TocItemViewModel actual, bool noMetadata = true)
{
using var swForExpected = new StringWriter();
YamlUtility.Serialize(swForExpected, expected);
using var swForActual = new StringWriter();
if (noMetadata)
{
actual.Metadata.Clear();
}
YamlUtility.Serialize(swForActual, actual);
Assert.Equal(swForExpected.ToString(), swForActual.ToString());
}

#endregion
}
Loading