-
Notifications
You must be signed in to change notification settings - Fork 884
Feature - Autotoc generation #10574
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
base: main
Are you sure you want to change the base?
Feature - Autotoc generation #10574
Conversation
|
What naming conventions are you following for generating the human-readable names in the TOC? I assume you'd want to use something similar to what ADO Wiki follows or give the end-user options for detecting camel case, snake case, etc. Also, should there be an option to output toc.yml to the end user so they could use autotoc generation for the first pass, then easily switch to providing manually updated toc.yml files? |
|
Here are some rules I was following (in Powershell) to convert the folder name to human readable to handle these scenarios: hello-world = Hello World |
i see this an extension and not a part of the auto toc feature. so the auto toc does the job of generating toc using the underlying file system. you can apply an extension to massage those titles to fit whatever need. we can easily write one i think ryan. @yufeih can weigh in . Found one, we already have a contribution for this. |
@dotnet-policy-service agree company="Microsoft" |
|
It makes sense to have a default naming convention for folders. Typically, folder names for URL segments uses either hyphens (-) or underscores (_) for spaces. We could follow the ADO wiki convention. |
done ! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Adds support for auto-generating TOC entries from the filesystem when auto: true is set in a toc.yml, plus tests and a runnable sample demonstrating the behavior.
Changes:
- Introduces an
autoproperty on TOC models and constants to enable auto-population. - Implements auto-population logic in the TOC build step (recursive folder traversal, boundary at folders with their own TOC).
- Adds unit tests and a full sample site under
samples/autotoc/.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/Docfx.Build.Tests/TocHelperTest.cs | Adds tests for YAML TOC loading including the new auto root property format and shared TOC equality helper. |
| test/Docfx.Build.Tests/TocDocumentProcessorTest.cs | Reuses shared TOC comparison helper and adds end-to-end tests covering auto-populated TOCs. |
| src/Docfx.DataContracts.Common/TocItemViewModel.cs | Adds Auto property to the TOC view model for YAML/JSON serialization. |
| src/Docfx.DataContracts.Common/Constants.cs | Adds Constants.PropertyName.Auto for consistent serialization keys. |
| src/Docfx.Build/TableOfContents/TocHelper.cs | Minor formatting-only change. |
| src/Docfx.Build/TableOfContents/BuildTocDocument.cs | Implements auto-population logic during Prebuild for TOC models with auto: true. |
| samples/autotoc/docfx.json | Adds a runnable sample config to demonstrate auto TOC generation. |
| samples/autotoc/toc.yml | Root sample TOC enabling auto: true with a manual “Home” entry. |
| samples/autotoc/index.md | Sample homepage explaining the feature and folder structure. |
| samples/autotoc/getting-started.md | Sample page intended to be auto-added to the root TOC. |
| samples/autotoc/guides/installation.md | Sample nested content auto-included under a generated “Guides” node. |
| samples/autotoc/guides/configuration.md | Additional sample nested content for auto-inclusion. |
| samples/autotoc/tutorials/toc.yml | Sample subfolder TOC demonstrating “boundary” behavior with its own toc.yml. |
| samples/autotoc/tutorials/beginner.md | Sample content for the tutorials TOC auto-population. |
| samples/autotoc/tutorials/advanced.md | Additional tutorials content for auto-population. |
| samples/autotoc/reference/toc.yml | Sample subfolder TOC with auto: false demonstrating opt-out/manual control. |
| samples/autotoc/reference/api.md | Manually-listed page under the reference TOC. |
| samples/autotoc/reference/unlisted.md | Demonstrates a file intentionally excluded when auto: false. |
| samples/autotoc/README.md | Documentation for running the sample and expected TOC results. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // Skip if already in TOC | ||
| if (toc.Items.Any(i => i.Name?.Equals(subfolderName, StringComparison.OrdinalIgnoreCase) == true)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var subItem = new TocItemViewModel | ||
| { | ||
| Name = StandardizeName(subfolderName) |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Subfolder duplicate detection compares existing item Name to the raw folder name (subfolderName). For folders with separators (e.g., getting-started), auto-generated names are standardized (spaces/title case) so this check can miss an existing manually-added item and create duplicates. Consider comparing against the standardized folder name (and/or checking existing items’ hrefs) instead of the raw folder name.
| // Skip if already in TOC | |
| if (toc.Items.Any(i => i.Name?.Equals(subfolderName, StringComparison.OrdinalIgnoreCase) == true)) | |
| { | |
| continue; | |
| } | |
| var subItem = new TocItemViewModel | |
| { | |
| Name = StandardizeName(subfolderName) | |
| var standardizedSubfolderName = StandardizeName(subfolderName); | |
| // Skip if already in TOC: compare against standardized name and existing items' hrefs | |
| if (toc.Items.Any(i => | |
| (i.Name != null && i.Name.Equals(standardizedSubfolderName, StringComparison.OrdinalIgnoreCase)) || | |
| (i.Href != null && | |
| NormalizePath(Path.GetDirectoryName(StripTildePrefix(i.Href))) == subfolder))) | |
| { | |
| continue; | |
| } | |
| var subItem = new TocItemViewModel | |
| { | |
| Name = standardizedSubfolderName |
| _inputFolder); | ||
|
|
||
| // Act & Assert - Empty YAML file should throw DocumentException | ||
| Assert.ThrowsAny<Exception>(() => TocHelper.LoadSingleToc(tocFile)); |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test comment says an empty YAML TOC should throw DocumentException, but the assertion currently allows any exception type. Since TocHelper.LoadSingleToc wraps YAML parsing errors into DocumentException, asserting DocumentException here would be more precise and would catch regressions in error handling.
| Assert.ThrowsAny<Exception>(() => TocHelper.LoadSingleToc(tocFile)); | |
| Assert.Throws<DocumentException>(() => TocHelper.LoadSingleToc(tocFile)); |
| private readonly string _templateFolder; | ||
| private TestLoggerListener Listener { get; set; } | ||
|
|
||
| public TocHelperTest() | ||
| { | ||
| _inputFolder = GetRandomFolder(); | ||
| _outputFolder = GetRandomFolder(); | ||
| _templateFolder = GetRandomFolder(); |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_templateFolder and Listener are declared but not used in this test class. Removing them will reduce noise and avoid unused-member warnings.
| private readonly string _templateFolder; | |
| private TestLoggerListener Listener { get; set; } | |
| public TocHelperTest() | |
| { | |
| _inputFolder = GetRandomFolder(); | |
| _outputFolder = GetRandomFolder(); | |
| _templateFolder = GetRandomFolder(); | |
| public TocHelperTest() | |
| { | |
| _inputFolder = GetRandomFolder(); | |
| _outputFolder = GetRandomFolder(); |
| [YamlMember(Alias = Constants.PropertyName.Auto)] | ||
| [JsonProperty(Constants.PropertyName.Auto)] | ||
| [JsonPropertyName(Constants.PropertyName.Auto)] | ||
| public bool? Auto { get; set; } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the new auto property to TocItemViewModel likely also needs a corresponding update to schemas/toc.schema.json so YAML/JSON schema validation and editor IntelliSense recognize it (currently the schema does not include auto).
| var fileName = Path.GetFileName(file); | ||
| // Skip if already in TOC (strip ~/ prefix for comparison) | ||
| var existingItem = toc.Items.FirstOrDefault(i => i.Href != null && | ||
| (StripTildePrefix(i.Href).Equals(file, StringComparison.OrdinalIgnoreCase) || | ||
| StripTildePrefix(i.Href).Equals(fileName, StringComparison.OrdinalIgnoreCase) || | ||
| Path.GetFileName(StripTildePrefix(i.Href)).Equals(fileName, StringComparison.OrdinalIgnoreCase))); | ||
| if (existingItem != null) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| // Compute href relative to the TOC file's folder | ||
| var href = GetRelativeHref(file, tocRootFolder); |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate detection for auto-generated file items can produce false positives because it treats any existing TOC item with the same file name (even if it points to a different folder) as a duplicate. This can cause legitimate files (e.g., root index.md) to be skipped when the TOC already references sub/index.md. Consider comparing against the normalized relative href for the file (e.g., GetRelativeHref(file, tocRootFolder)), rather than Path.GetFileName(...)/bare filename matching, and also account for existing topicHref values if applicable.
| var fileName = Path.GetFileName(file); | |
| // Skip if already in TOC (strip ~/ prefix for comparison) | |
| var existingItem = toc.Items.FirstOrDefault(i => i.Href != null && | |
| (StripTildePrefix(i.Href).Equals(file, StringComparison.OrdinalIgnoreCase) || | |
| StripTildePrefix(i.Href).Equals(fileName, StringComparison.OrdinalIgnoreCase) || | |
| Path.GetFileName(StripTildePrefix(i.Href)).Equals(fileName, StringComparison.OrdinalIgnoreCase))); | |
| if (existingItem != null) | |
| { | |
| continue; | |
| } | |
| // Compute href relative to the TOC file's folder | |
| var href = GetRelativeHref(file, tocRootFolder); | |
| // Compute href relative to the TOC file's folder | |
| var href = GetRelativeHref(file, tocRootFolder); | |
| var normalizedHref = NormalizePath(StripTildePrefix(href)); | |
| // Skip if already in TOC by comparing normalized relative hrefs | |
| var existingItem = toc.Items.FirstOrDefault(i => | |
| { | |
| var itemHref = i.Href ?? i.TopicHref; | |
| if (string.IsNullOrEmpty(itemHref)) | |
| { | |
| return false; | |
| } | |
| var normalizedItemHref = NormalizePath(StripTildePrefix(itemHref)); | |
| return normalizedItemHref.Equals(normalizedHref, StringComparison.OrdinalIgnoreCase); | |
| }); | |
| if (existingItem != null) | |
| { | |
| continue; | |
| } |
| // Get files directly in this folder (excluding TOC files) | ||
| var filesInFolder = allFiles | ||
| .Where(f => NormalizePath(Path.GetDirectoryName(f)) == folder) | ||
| .Where(f => !IsTocFile(f)) | ||
| .OrderBy(f => f); | ||
|
|
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The implementation sorts discovered files lexicographically (OrderBy(f => f)), which does not match the PR description of using the underlying file system order. Either adjust the implementation to preserve filesystem enumeration order (or explicitly enumerate directories/files) or update the PR description/spec to reflect deterministic sorting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wow ! pretty cool observation. scary.
This pull request introduces the ability to generate a Table of Contents (TOC) based on the underlying file system. The feature works as follows:
When auto TOC generation is enabled:
auto: trueto thetoc.ymllocated alongside thedocfx.json.toc.ymlin the order specified by the file system.auto: falseis encountered in any TOC, the auto TOC generation for that TOC will be skipped.hrefbefore adding one, thus avoiding any duplicates.