diff --git a/docs/specs/toc/service-page.yml b/docs/specs/toc/service-page.yml index 58494113a17..fa50f476d59 100644 --- a/docs/specs/toc/service-page.yml +++ b/docs/specs/toc/service-page.yml @@ -865,3 +865,244 @@ outputs: a/api/bot/toc.json: a/result/index.json: a/result/client.json: +--- +# Generate a TOC for each moniker +repos: + https://github.com/ops/join-toc-with-monikers-in-reference-toc: + - files: + .openpublishing.publish.config.json: | + { + "docsets_to_publish": [ + { + "build_source_folder": "docs", + "SplitTOC": [ + "api/bot/legacy/toc.yml", + "api/bot/latest/toc.yml", + "api/bot/preview/toc.yml" + ], + "JoinTOCPlugin": [ + { + "ReferenceTOC": "api/bot/legacy/toc.yml", + "TopLevelTOC": "docs-ref-toc/top-level-toc-legacy.yml", + "OutputFolder": "result-legacy", + "OriginalReferenceTOC": "api/bot/toc.yml" + }, + { + "ReferenceTOC": "api/bot/latest/toc.yml", + "TopLevelTOC": "docs-ref-toc/top-level-toc-latest.yml", + "OutputFolder": "result-latest", + "OriginalReferenceTOC": "api/bot/toc.yml" + }, + { + "ReferenceTOC": "api/bot/preview/toc.yml", + "TopLevelTOC": "docs-ref-toc/top-level-toc-preview.yml", + "OutputFolder": "result-preview", + "OriginalReferenceTOC": "api/bot/toc.yml" + } + ] + } + ] + } + docs/docfx.yml: | + build: + content: + files: "**/*" + exclude: + - docs-ref-toc/top-level-toc-legacy.yml + - docs-ref-toc/top-level-toc-latest.yml + - docs-ref-toc/top-level-toc-preview.yml + docs/_themes/ContentTemplate/schemas/ReferenceContainer.schema.json: | + { + "type": "object", + "required": ["name", "pageType"], + "properties": { + "uid": {"contentType": "uid", "type": "string"}, + "name": {"type": "string"}, "fullName": {"type": "string"}, + "children": { + "items": { + "type": "object", "additionalProperties": false, + "properties": { + "href": {"contentType": "href", "type": "string"}, + "uid": {"contentType": "xref", "type": "string"}, + "name": {"type": "string"} + } + }, "type": "array", "minItems": 1}, + "pageType": { "enum": ["root", "service"], "type": "string" } + } + } + docs/overview/legacy.md: + docs/overview/latest.md: + docs/overview/preview.md: + docs/docs-auto-gen/legacy.md: | + --- + name: legacy-doc + uid: legacy + --- + docs/docs-auto-gen/latest.md: | + --- + name: latest-doc + uid: latest + --- + docs/docs-auto-gen/preview.md: | + --- + name: preview-doc + uid: preview + --- + docs/toc.yml: | + items: + - name: a + docs/api/bot/toc.yml: | + items: + - name: MS.ServicePage.legacy.test + items: + - name: legacy + uid: legacy + monikers: + - legacy + - name: MS.ServicePage.latest.test + items: + - name: latest + uid: latest + monikers: + - latest + - name: MS.ServicePage.preview.test + items: + - name: preview + uid: preview + monikers: + - preview + docs/docs-ref-toc/top-level-toc-legacy.yml: | + items: + - name: API Reference + landingPageType: Root + children: + - 'MS.ServicePage.legacy*' + docs/docs-ref-toc/top-level-toc-latest.yml: | + items: + - name: API Reference + landingPageType: Root + children: + - 'MS.ServicePage.latest*' + docs/docs-ref-toc/top-level-toc-preview.yml: | + items: + - name: API Reference + landingPageType: Root + items: + - name: App Service + landingPageType: Service + items: + - name: preview docs + children: + - 'MS.ServicePage.preview*' +outputs: + docs/toc.json: + docs/overview/legacy.json: + docs/overview/latest.json: + docs/overview/preview.json: + docs/docs-auto-gen/legacy.json: + docs/docs-auto-gen/latest.json: + docs/docs-auto-gen/preview.json: + docs/result-legacy/index.json: | + { + "children": [ { "name": "MS.ServicePage.legacy.test", "uid": "legacy" } ], + "fullName": "API Reference", + "metadata": { + "_path": "result-legacy/index.json", + "_tocRel": "../api/bot/legacy/toc.json" + }, + "name": "API Reference", + "pageType": "root", + "schema": "ReferenceContainer" + } + docs/result-latest/index.json: | + { + "children": [ { "name": "MS.ServicePage.latest.test", "uid": "latest" } ], + "fullName": "API Reference", + "metadata": { + "_path": "result-latest/index.json", + "_tocRel": "../api/bot/latest/toc.json", + }, + "name": "API Reference", + "pageType": "root", + "schema": "ReferenceContainer" + } + docs/result-preview/index.json: | + { + "children": [ { "href": "appservice", "name": "App Service" } ], + "fullName": "API Reference", + "metadata": { + "_path": "result-preview/index.json", + "_tocRel": "../api/bot/preview/toc.json" + }, + "name": "API Reference", + "pageType": "root", + "schema": "ReferenceContainer" + } + docs/result-preview/appservice.json: | + { + "children": [ { "name": "preview docs" } ], + "fullName": "App Service", + "metadata": { + "_path": "result-preview/appservice.json", + "_tocRel": "../api/bot/preview/toc.json" + }, + "name": "App Service", + "pageType": "service", + "schema": "ReferenceContainer" + } + docs/api/bot/legacy/toc.json: | + { + "items": [ + { + "items": [ + { "href": "../../../result-legacy/", "name": "Overview" }, + { "href": "../../../docs-auto-gen/legacy", "name": "MS.ServicePage.legacy.test" } + ], + "name": "API Reference" + } + ] + } + docs/api/bot/legacy/_splitted/ms.servicepage.legacy.test/toc.json: + docs/api/bot/legacy/_splitted/ms.servicepage.latest.test/toc.json: + docs/api/bot/legacy/_splitted/ms.servicepage.preview.test/toc.json: + docs/api/bot/latest/toc.json: | + { + "items": [ + { + "items": [ + { "href": "../../../result-latest/", "name": "Overview" }, + { "href": "../../../docs-auto-gen/latest", "name": "MS.ServicePage.latest.test"} + ], + "name": "API Reference" + } + ] + } + docs/api/bot/latest/_splitted/ms.servicepage.legacy.test/toc.json: + docs/api/bot/latest/_splitted/ms.servicepage.latest.test/toc.json: + docs/api/bot/latest/_splitted/ms.servicepage.preview.test/toc.json: + docs/api/bot/preview/toc.json: | + { + "items": [ + { + "items": [ + { "href": "../../../result-preview/", "name": "Overview" }, + { + "items": [ + { "href": "../../../result-preview/appservice", "name": "Overview" }, + { + "items": [ { "href": "../../../docs-auto-gen/preview", "name": "MS.ServicePage.preview.test" } ], + "name": "preview docs" + } + ], + "name": "App Service" + } + ], + "name": "API Reference" + } + ] + } + docs/api/bot/preview/_splitted/ms.servicepage.legacy.test/toc.json: + docs/api/bot/preview/_splitted/ms.servicepage.latest.test/toc.json: + docs/api/bot/preview/_splitted/ms.servicepage.preview.test/toc.json: + docs/.xrefmap.json: + docs/filemap.json: diff --git a/src/docfx/build/DocsetBuilder.cs b/src/docfx/build/DocsetBuilder.cs index 5dc4b08d2a7..57cd989a24d 100644 --- a/src/docfx/build/DocsetBuilder.cs +++ b/src/docfx/build/DocsetBuilder.cs @@ -96,7 +96,7 @@ private DocsetBuilder( _metadataValidator = new MetadataValidator(_config, _microsoftGraphAccessor, _jsonSchemaLoader, _monikerProvider, _customRuleProvider); _tocParser = new(_input, _markdownEngine); _tocLoader = new(_buildOptions, _input, _linkResolver, _xrefResolver, _tocParser, _monikerProvider, _dependencyMapBuilder, _contentValidator, _config, _errors, _buildScope); - _tocMap = new(_config, _errors, _input, _buildScope, _dependencyMapBuilder, _tocParser, _tocLoader, _documentProvider, _contentValidator, _publishUrlMap); + _tocMap = new(_sourceMap, _config, _errors, _input, _buildScope, _dependencyMapBuilder, _tocParser, _tocLoader, _documentProvider, _contentValidator, _publishUrlMap); } public static DocsetBuilder? Create( diff --git a/src/docfx/build/toc/TocMap.cs b/src/docfx/build/toc/TocMap.cs index 610d1a10712..ab0c670f734 100644 --- a/src/docfx/build/toc/TocMap.cs +++ b/src/docfx/build/toc/TocMap.cs @@ -11,6 +11,7 @@ namespace Microsoft.Docs.Build; /// internal class TocMap { + private readonly SourceMap _sourceMap; private readonly Config _config; private readonly Input _input; private readonly ErrorBuilder _errors; @@ -25,6 +26,7 @@ internal class TocMap private readonly Watch<(FilePath[] tocs, Dictionary docToTocs, List servicePages)> _tocs; public TocMap( + SourceMap sourceMap, Config config, ErrorBuilder errors, Input input, @@ -36,6 +38,7 @@ public TocMap( ContentValidator contentValidator, PublishUrlMap publishUrlMap) { + _sourceMap = sourceMap; _config = config; _errors = errors; _input = input; @@ -179,17 +182,33 @@ private static (int subDirectoryCount, int parentDirectoryCount) GetRelativeDire var allTocs = new List<(FilePath file, HashSet docs, HashSet tocs, bool shouldBuildFile)>(); var includedTocs = new HashSet(); var allServicePages = new List(); + var (originalReferenceTOCs, targetReferenceTOCs) = GetOriginalReferenceTocWithTargetReferenceToc(); // Parse and split TOC ParallelUtility.ForEach( scope, _errors, - _buildScope.GetFiles(ContentType.Toc), - file => SplitToc(file, _tocParser.Parse(file, _errors), allTocFiles)); + _buildScope.GetFiles(ContentType.Toc).Concat(targetReferenceTOCs).Except(originalReferenceTOCs), + file => + { + if (_input.Exists(file)) + { + SplitToc(file, _tocParser.Parse(file, _errors), allTocFiles); + } + else + { + var node = _tocParser.Parse(_sourceMap.GetOriginalFilePath(file)!, _errors); + SplitToc(file, node, allTocFiles); + } + }); // Load TOC ParallelUtility.ForEach(scope, _errors, allTocFiles, file => { + if (!_input.Exists(file)) + { + file = _sourceMap.GetOriginalFilePath(file)!; + } var (_, docsList, tocsList, servicePages) = _tocLoader.Load(file); var docs = docsList.ToHashSet(); var tocs = tocsList.ToHashSet(); @@ -254,6 +273,26 @@ void RemoveInvalidServicePage() } } + private (HashSet originalReferenceTOCs, List targetReferenceTOCs) GetOriginalReferenceTocWithTargetReferenceToc() + { + var originalReferenceTOCs = new HashSet(); + var targetReferenceTOCs = new List(); + + foreach (var joinTOCConfig in _config.JoinTOC) + { + if (!string.IsNullOrEmpty(joinTOCConfig.OriginalReferenceToc)) + { + var filePathForOriginalTOC = FilePath.Content(new PathString(joinTOCConfig.OriginalReferenceToc)); + originalReferenceTOCs.Add(filePathForOriginalTOC); + var referenceTocFilePath = FilePath.Content(new PathString(joinTOCConfig.ReferenceToc!)); + targetReferenceTOCs.Add(referenceTocFilePath); + _sourceMap.AddOriginalPath(referenceTocFilePath.Path, filePathForOriginalTOC.Path); + } + } + + return (originalReferenceTOCs, targetReferenceTOCs); + } + private void SplitToc(FilePath file, TocNode toc, ConcurrentHashSet result) { if (!_config.SplitTOC.Contains(file.Path) || toc.Items.Count <= 0) diff --git a/src/docfx/config/JoinTOCConfig.cs b/src/docfx/config/JoinTOCConfig.cs index 588944191df..663d276dcb9 100644 --- a/src/docfx/config/JoinTOCConfig.cs +++ b/src/docfx/config/JoinTOCConfig.cs @@ -14,4 +14,6 @@ internal class JoinTOCConfig public string? ReferenceToc { get; init; } public string? TopLevelToc { get; init; } + + public string? OriginalReferenceToc { get; init; } } diff --git a/src/docfx/config/ops/OpsConfigLoader.cs b/src/docfx/config/ops/OpsConfigLoader.cs index eb4de311e57..bb544ec0035 100644 --- a/src/docfx/config/ops/OpsConfigLoader.cs +++ b/src/docfx/config/ops/OpsConfigLoader.cs @@ -227,6 +227,10 @@ private static (JObject joinTocMetadata, JArray joinTocConfig) GenerateJoinTocMe { item["topLevelToc"] = baseDir.GetRelativePath(new PathString(config.TopLevelTOC)); } + if (!string.IsNullOrEmpty(config.OriginalReferenceTOC)) + { + item["originalReferenceToc"] = baseDir.GetRelativePath(new PathString(config.OriginalReferenceTOC)); + } joinTocConfig.Add(item); } diff --git a/src/docfx/config/ops/OpsJoinTocConfig.cs b/src/docfx/config/ops/OpsJoinTocConfig.cs index bcb7c78a612..67ab2f1f45f 100644 --- a/src/docfx/config/ops/OpsJoinTocConfig.cs +++ b/src/docfx/config/ops/OpsJoinTocConfig.cs @@ -19,5 +19,7 @@ internal class OpsJoinTocConfig public string? OutputFolder { get; init; } + public string? OriginalReferenceTOC { get; init; } + public JObject? ContainerPageMetadata { get; init; } } diff --git a/src/docfx/docfx.csproj b/src/docfx/docfx.csproj index cd143b2c77b..95e092be2a1 100644 --- a/src/docfx/docfx.csproj +++ b/src/docfx/docfx.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/docfx/lib/log/SourceMap.cs b/src/docfx/lib/log/SourceMap.cs index 8d8afb1434c..b46ebefd9a6 100644 --- a/src/docfx/lib/log/SourceMap.cs +++ b/src/docfx/lib/log/SourceMap.cs @@ -6,9 +6,11 @@ namespace Microsoft.Docs.Build; internal class SourceMap { private readonly Dictionary _map = new(); + private readonly ErrorBuilder _errors; public SourceMap(ErrorBuilder errors, PathString docsetPath, Config config, FileResolver fileResolver) { + _errors = errors; foreach (var sourceMap in config.SourceMap) { if (!string.IsNullOrEmpty(sourceMap)) @@ -38,6 +40,14 @@ public SourceMap(ErrorBuilder errors, PathString docsetPath, Config config, File } } + public void AddOriginalPath(PathString path, PathString originalPath) + { + if (!_map.TryAdd(path, originalPath)) + { + _errors.Add(Errors.SourceMap.DuplicateSourceMapItem(path, new List { _map[path], originalPath })); + } + } + public FilePath? GetOriginalFilePath(FilePath path) { if (path.Origin == FileOrigin.Main && _map.TryGetValue(path.Path, out var originalPath))