diff --git a/docs/docs/table-of-contents.md b/docs/docs/table-of-contents.md index f49d2bc768d..48c7a17adf0 100644 --- a/docs/docs/table-of-contents.md +++ b/docs/docs/table-of-contents.md @@ -10,19 +10,15 @@ To add a TOC, create a file named `toc.yml`. Here's the structure for a simple Y items: - name: Tutorial items: - - name: Introduction - href: tutorial.md - - name: Step 1 - href: step-1.md - - name: Step 2 - href: step-2.md - - name: Step 3 - href: step-3.md + - href: tutorial.md + - href: step-1.md + - href: step-2.md + - href: step-3.md ``` The YAML document is a tree of TOC nodes, each of which has these properties: -- `name`: The display name for the TOC node. +- `name`: An optional display name for the TOC node. When not specified, uses the `title` metadata or the first Heading 1 element from the referenced article as the display name. - `href`: The path the TOC node leads to. Optional because a node can exist just to parent other nodes. - `items`: If a node has children, they're listed in the items array. - `uid`: The uid of the article. Can be used instead of `href`. diff --git a/samples/seed/articles/toc.yml b/samples/seed/articles/toc.yml index af211de867d..2790484fcf7 100644 --- a/samples/seed/articles/toc.yml +++ b/samples/seed/articles/toc.yml @@ -1,17 +1,13 @@ pdfFileName: seed.pdf pdfCoverPage: pdf/cover.html items: -- name: Getting Started - href: docfx_getting_started.md +- href: docfx_getting_started.md - name: Engineering Docs expanded: true items: - name: Section 1 - - name: Engineering Guidelines - href: engineering_guidelines.md - - name: CSharp Coding Standards - href: csharp_coding_standards.md -- name: Markdown - href: markdown.md + - href: engineering_guidelines.md + - href: csharp_coding_standards.md +- href: markdown.md - name: Microsoft Docs href: https://docs.microsoft.com/en-us/ diff --git a/src/Docfx.Build/ManifestProcessor.cs b/src/Docfx.Build/ManifestProcessor.cs index 71457c9341b..2b025b15bd6 100644 --- a/src/Docfx.Build/ManifestProcessor.cs +++ b/src/Docfx.Build/ManifestProcessor.cs @@ -4,6 +4,7 @@ using System.Collections.Concurrent; using Docfx.Common; +using Docfx.DataContracts.Common; using Docfx.Plugins; namespace Docfx.Build.Engine; @@ -33,6 +34,8 @@ public ManifestProcessor(List manifestWithContext, Docu public void Process() { + UpdateTocName(); + UpdateContext(); // Afterwards, m.Item.Model.Content is always IDictionary @@ -53,8 +56,6 @@ public void Process() } } - #region Private - private void UpdateContext() { _context.ResolveExternalXRefSpec(); @@ -181,5 +182,49 @@ private List ProcessTemplate() return _templateProcessor.Process(_manifestWithContext.Select(s => s.Item).ToList(), _context.ApplyTemplateSettings, _globalMetadata); } - #endregion + private void UpdateTocName() + { + var titles = ( + from item in _manifestWithContext + let title = GetTitle(item) + where !string.IsNullOrEmpty(title) + group title + by item.FileModel.Key).ToDictionary(g => g.Key, g => g.First()); + + foreach (var item in _manifestWithContext) + { + if (item.FileModel.Content is not TocItemViewModel toc) + continue; + + UpdateTocNameCore(toc.Items); + } + + void UpdateTocNameCore(List items) + { + if (items is null) + return; + + foreach (var node in items) + { + if (string.IsNullOrEmpty(node.Name)) + { + if (node.Href is not null && titles.TryGetValue(UriUtility.GetPath(node.Href), out var title)) + node.Name = title; + else + Logger.LogWarning( + $"TOC item ({node}) with empty name found. Missing a name?", + code: WarningCodes.Build.EmptyTocItemName); + } + + UpdateTocNameCore(node.Items); + } + } + + string GetTitle(ManifestItemWithContext item) + { + return item.FileModel.Content is Dictionary dict && + dict.TryGetValue("title", out var title) && + title is string result ? result : null; + } + } } diff --git a/src/Docfx.Build/TableOfContents/TocResolver.cs b/src/Docfx.Build/TableOfContents/TocResolver.cs index ba3f52bd6ba..b3fcdf318ea 100644 --- a/src/Docfx.Build/TableOfContents/TocResolver.cs +++ b/src/Docfx.Build/TableOfContents/TocResolver.cs @@ -83,14 +83,6 @@ private TocItemInfo ResolveItemCore(TocItemInfo wrapper, Stack stac // validate href ValidateHref(item); - // validate if name is missing - if (!isRoot && string.IsNullOrEmpty(item.Name) && string.IsNullOrEmpty(item.TopicUid)) - { - Logger.LogWarning( - $"TOC item ({item}) with empty name found. Missing a name?", - code: WarningCodes.Build.EmptyTocItemName); - } - // TocHref supports 2 forms: absolute path and local toc file. // When TocHref is set, using TocHref as Href in output, and using Href as Homepage in output var tocHrefType = Utility.GetHrefType(item.TocHref); diff --git a/templates/modern/src/helper.ts b/templates/modern/src/helper.ts index 6c2f1a01872..7600bd64ba9 100644 --- a/templates/modern/src/helper.ts +++ b/templates/modern/src/helper.ts @@ -33,7 +33,10 @@ export function loc(id: string, args?: { [key: string]: string }): string { /** * Add into long word. */ -export function breakWord(text: string): string[] { +export function breakWord(text?: string): string[] { + if (!text) { + return [] + } const regex = /([a-z0-9])([A-Z]+[a-z])|([a-zA-Z0-9][.,/<>_])/g const result = [] let start = 0 @@ -55,7 +58,7 @@ export function breakWord(text: string): string[] { /** * Add into long word. */ -export function breakWordLit(text: string): TemplateResult { +export function breakWordLit(text?: string): TemplateResult { const result = [] breakWord(text).forEach(word => { if (result.length > 0) { diff --git a/templates/modern/src/toc.ts b/templates/modern/src/toc.ts index 19611d7aa67..6dbe86345af 100644 --- a/templates/modern/src/toc.ts +++ b/templates/modern/src/toc.ts @@ -6,7 +6,7 @@ import { classMap } from 'lit-html/directives/class-map.js' import { breakWordLit, meta, isExternalHref, loc, isSameURL } from './helper' export type TocNode = { - name: string + name?: string href?: string expanded?: boolean items?: TocNode[]