Skip to content

Commit

Permalink
feat: add TOC order for toc_rel pick order (#9406)
Browse files Browse the repository at this point in the history
  • Loading branch information
yufeih committed Nov 13, 2023
1 parent 7d482d7 commit 8a60af9
Show file tree
Hide file tree
Showing 46 changed files with 11,586 additions and 152 deletions.
13 changes: 12 additions & 1 deletion docs/docs/pdf.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,17 @@ In case the TOC file is auto-generated, use [file metadata](./config.md#metadata
}
```

You can create a single PDF file using a dedicated PDF TOC containing all articles with [Nested TOCs](./table-of-contents.md#nested-tocs) and set `order` to a bigger value to prevent the PDF TOC from appearing on the website.

```yaml
order: 200
items:
- name: Section 1
href: section-1/toc.yml
- name: Section 2
href: section-2/toc.yml
```
## PDF Metadata
These metadata applies to TOC files that controls behaviors of PDF generation.
Expand Down Expand Up @@ -84,7 +95,7 @@ To preview PDF rendering result, print the HTML page in the web browser, or set

The site template adds a default margin and removes background graphics for pages in print mode. Use `@page { margin: 0 }` to remove the default margin and use `print-color-adjust: exact` to keep background graphics for cover pages.

See [this example](https://raw.githubusercontent.com/dotnet/docfx/main/samples/seed/pdf.md) on a PDF cover page that fills the whole page with background graphics:
See [this example](https://raw.githubusercontent.com/dotnet/docfx/main/samples/seed/pdf/cover.md) on a PDF cover page that fills the whole page with background graphics:

![Alt text](./media/pdf-cover-page.png)

Expand Down
12 changes: 12 additions & 0 deletions docs/docs/table-of-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ The YAML document is a tree of TOC nodes, each of which has these properties:
- `uid`: The uid of the article. Can be used instead of `href`.
- `expanded`: Expand children on load, only works if the template is `modern`.

When an article is referenced by a TOC through `href`, the corresponding TOC appears when viewing that article. If multiple TOCs reference the same article, or the article isn't referenced by any TOC, the nearest TOC with the least amount of directory jumps is picked.

The `order` property can customize this pick logic, TOCs with a smaller order value are picked first. The default order is 0.

```yml
order: 100
items:
- ...
```

## Nested TOCs

To nest a TOC within another TOC, set the `href` property to point to the `toc.yml` file that you want to nest. You can also use this structure as a way to reuse a TOC structure in one or more TOC files.
Expand Down Expand Up @@ -63,6 +73,8 @@ Reference
├─ System.Float
```

Nested TOCs by default have `order` set to `100` to let containing TOCs take precedence.

## Reference TOCs

To reference another TOC without embeding it to a parent TOC using nested TOCs, set the `href` property to point to the directory that you want to reference and end the string with `/`, this will generate a link pointing to the first article in the referenced TOC.
Expand Down
2 changes: 1 addition & 1 deletion samples/seed/articles/toc.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pdfFileName: seed.pdf
pdfCoverPage: pdf.html
pdfCoverPage: pdf/cover.html
items:
- name: Getting Started
href: docfx_getting_started.md
Expand Down
4 changes: 2 additions & 2 deletions samples/seed/docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@
"build": {
"content": [
{ "files": [ "**/*.yml" ], "src": "obj/api", "dest": "api" },
{ "files": [ "**/*.yml" ], "src": "obj/apipage", "dest": "apipage" },
{ "files": [ "**" ], "src": "obj/md", "dest": "md" },
{ "files": [ "**" ], "src": "obj/apipage", "dest": "apipage" },
{ "files": [ "articles/**/*.{md,yml}", "*.md", "toc.yml", "restapi/**", "md/**", "md2/**" ] }
{ "files": [ "articles/**/*.{md,yml}", "*.md", "toc.yml", "restapi/**" ] },
{ "files": [ "pdf/**" ] }
],
"resource": [
{
Expand Down
File renamed without changes.
4 changes: 3 additions & 1 deletion samples/seed/pdf/toc.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
- name: Articles
order: 200
items:
- name: Articles
href: ../articles/toc.yml
- name: API Documentation
href: ../obj/api/toc.yml
Expand Down
35 changes: 20 additions & 15 deletions src/Docfx.Build/SystemMetadataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Docfx.Build.Engine;
internal sealed class SystemMetadataGenerator
{
private readonly IDocumentBuildContext _context;
private readonly IEnumerable<FileInfo> _toc;
private readonly Dictionary<string, FileInfo> _toc;

public SystemMetadataGenerator(IDocumentBuildContext context)
{
Expand All @@ -21,9 +21,10 @@ public SystemMetadataGenerator(IDocumentBuildContext context)

// Order toc files by the output folder depth
_toc = context.GetTocInfo()
.Select(s => new FileInfo(s.TocFileKey, context.GetFilePath(s.TocFileKey)))
.Select(s => new FileInfo(s.Order, s.TocFileKey, context.GetFilePath(s.TocFileKey)))
.Where(s => s.RelativePath != null)
.OrderBy(s => s.RelativePath.SubdirectoryCount);
.OrderBy(s => s.RelativePath.SubdirectoryCount)
.ToDictionary(s => s.Key);
}

public SystemMetadata Generate(InternalManifestItem item)
Expand Down Expand Up @@ -68,7 +69,7 @@ public SystemMetadata Generate(InternalManifestItem item)
// 2. The algorithm of toc current article belongs to:
// a. If toc can be found in TocMap, return that toc
// b. Elsewise, get the nearest toc, **nearest** means nearest toc in **OUTPUT** folder
var parentTocFiles = _context.GetTocFileKeySet(key)?.Select(s => new FileInfo(s, _context.GetFilePath(s)));
var parentTocFiles = _context.GetTocFileKeySet(key)?.Select(s => _toc[s]);
var parentToc = GetNearestToc(parentTocFiles, file) ?? GetDefaultToc(key);

if (parentToc != null)
Expand All @@ -90,7 +91,7 @@ public SystemMetadata Generate(InternalManifestItem item)

private void GetRootTocFromOutputRoot(SystemMetadata attrs, RelativePath file)
{
var rootToc = _toc.FirstOrDefault();
var rootToc = _toc.Values.FirstOrDefault();
if (rootToc != null)
{
var rootTocPath = rootToc.RelativePath.RemoveWorkingFolder();
Expand All @@ -116,11 +117,13 @@ private FileInfo GetDefaultToc(string fileKey)

// MakeRelativeTo calculates how to get file "s" from "outputPath"
// The standard for being the toc of current file is: Relative directory is empty or ".."s only
var parentTocs = _toc
var parentTocs = _toc.Values
.Select(s => new { rel = s.RelativePath.MakeRelativeTo(outputPath), info = s })
.Where(s => s.rel.SubdirectoryCount == 0)
.OrderBy(s => s.rel.ParentDirectoryCount)
.OrderBy(s => s.info.Order)
.ThenBy(s => s.rel.ParentDirectoryCount)
.Select(s => s.info);

return parentTocs.FirstOrDefault();
}

Expand All @@ -139,9 +142,8 @@ private static FileInfo GetNearestToc(IEnumerable<FileInfo> tocFiles, RelativePa
return (from toc in tocFiles
where toc.RelativePath != null
let relativePath = toc.RelativePath.RemoveWorkingFolder() - file
orderby relativePath.SubdirectoryCount, relativePath.ParentDirectoryCount, toc.FilePath, toc.Key
select toc)
.FirstOrDefault();
orderby toc.Order, relativePath.SubdirectoryCount, relativePath.ParentDirectoryCount, toc.FilePath, toc.Key
select toc).FirstOrDefault();
}

private static string GetFileKey(string key)
Expand All @@ -150,16 +152,19 @@ private static string GetFileKey(string key)
return RelativePath.NormalizedWorkingFolder + key;
}

private sealed class FileInfo
class FileInfo
{
public string Key { get; set; }
public int Order { get; }

public string Key { get; }

public string FilePath { get; set; }
public string FilePath { get; }

public RelativePath RelativePath { get; set; }
public RelativePath RelativePath { get; }

public FileInfo(string key, string filePath)
public FileInfo(int order, string key, string filePath)
{
Order = order;
Key = key;
FilePath = filePath;
RelativePath = (RelativePath)filePath;
Expand Down
13 changes: 1 addition & 12 deletions src/Docfx.Build/TableOfContents/TocDocumentProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,7 @@ private void RegisterTocToContext(TocItemViewModel toc, FileModel model, IDocume
// Add current folder to the toc mapping, e.g. `a/` maps to `a/toc`
var directory = ((RelativePath)key).GetPathFromWorkingFolder().GetDirectoryPath();
context.RegisterToc(key, directory);

var tocInfo = new TocInfo(key);
if (toc.Homepage != null)
{
if (PathUtility.IsRelativePath(toc.Homepage))
{
var pathToRoot = ((RelativePath)model.File + (RelativePath)HttpUtility.UrlDecode(toc.Homepage)).GetPathFromWorkingFolder();
tocInfo.Homepage = pathToRoot;
}
}

context.RegisterTocInfo(tocInfo);
context.RegisterTocInfo(new() { TocFileKey = key, Order = toc.Order ?? 0 });
}

private void RegisterTocMapToContext(TocItemViewModel item, FileModel model, IDocumentBuildContext context)
Expand Down
63 changes: 15 additions & 48 deletions src/Docfx.Build/TableOfContents/TocHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public static class TocHelper
{
private static readonly YamlDeserializerWithFallback _deserializer =
YamlDeserializerWithFallback.Create<TocViewModel>()
.WithFallback<TocRootViewModel>();
.WithFallback<TocItemViewModel>();

public static List<FileModel> ResolveToc(ImmutableList<FileModel> models, IHostService host)
{
Expand All @@ -32,13 +32,13 @@ public static List<FileModel> ResolveToc(ImmutableList<FileModel> models, IHostS

foreach (var model in models)
{
// If the TOC file is referenced by other TOC, remove it from the collection
// If the TOC file is referenced by other TOC, decrease the score
var tocItemInfo = tocCache[model.OriginalFileAndType.FullPath];
if (!tocItemInfo.IsReferenceToc)
{
model.Content = tocItemInfo.Content;
nonReferencedTocModels.Add(model);
}
if (tocItemInfo.IsReferenceToc && tocItemInfo.Content.Order is null)
tocItemInfo.Content.Order = 100;

model.Content = tocItemInfo.Content;
nonReferencedTocModels.Add(model);
}

return nonReferencedTocModels;
Expand All @@ -65,61 +65,28 @@ public static TocItemViewModel LoadSingleToc(string file)
{
if (fileType == TocFileType.Markdown)
{
return new TocItemViewModel
return new()
{
Items = MarkdownTocReader.LoadToc(EnvironmentContext.FileAbstractLayer.ReadAllText(file), file)
};
}
else if (fileType == TocFileType.Yaml)
{
return LoadYamlToc(file);
return _deserializer.Deserialize(file) switch
{
TocViewModel vm => new() { Items = vm },
TocItemViewModel root => root,
_ => throw new NotSupportedException($"{file} is not a valid TOC file."),
};
}
}
catch (Exception e)
{
var message = $"{file} is not a valid TOC File: {e.Message}";
var message = $"{file} is not a valid TOC File: {e}";
Logger.LogError(message, code: ErrorCodes.Toc.InvalidTocFile);
throw new DocumentException(message, e);
}

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

public static TocItemViewModel LoadYamlToc(string file)
{
#if NET7_0_OR_GREATER
ArgumentException.ThrowIfNullOrEmpty(file);
#else
if (string.IsNullOrEmpty(file))
{
throw new ArgumentNullException(nameof(file));
}
#endif

object obj;
try
{
obj = _deserializer.Deserialize(file);
}
catch (Exception ex)
{
throw new NotSupportedException($"{file} is not a valid TOC file, detail: {ex}.", ex);
}
if (obj is TocViewModel vm)
{
return new TocItemViewModel
{
Items = vm,
};
}
if (obj is TocRootViewModel root)
{
return new TocItemViewModel
{
Items = root.Items,
Metadata = root.Metadata,
};
}
throw new NotSupportedException($"{file} is not a valid TOC file.");
}
}
5 changes: 5 additions & 0 deletions src/Docfx.DataContracts.Common/TocItemViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ public string NameForVB
[JsonPropertyName(Constants.PropertyName.TopicUid)]
public string TopicUid { get; set; }

[YamlMember(Alias = "order")]
[JsonProperty("order")]
[JsonPropertyName("order")]
public int? Order { get; set; }

[YamlIgnore]
[Newtonsoft.Json.JsonIgnore]
[System.Text.Json.Serialization.JsonIgnore]
Expand Down
22 changes: 0 additions & 22 deletions src/Docfx.DataContracts.Common/TocRootViewModel.cs

This file was deleted.

2 changes: 1 addition & 1 deletion src/Docfx.Dotnet/DotnetApiCatalog.ManagedReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ void ResolveAndExportYamlMetadata(
// generate toc.yml
model.TocYamlViewModel.Type = MemberType.Toc;

var tocViewModel = new TocRootViewModel
var tocViewModel = new TocItemViewModel
{
Metadata = new() { ["memberLayout"] = config.MemberLayout },
Items = model.TocYamlViewModel.ToTocViewModel(),
Expand Down
8 changes: 2 additions & 6 deletions src/Docfx.Plugins/TocInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ namespace Docfx.Plugins;

public class TocInfo
{
public string TocFileKey { get; }
public string Homepage { get; set; }
public string TocFileKey { get; init; }

public TocInfo(string tocFileKey)
{
TocFileKey = tocFileKey;
}
public int Order { get; init; }
}
2 changes: 1 addition & 1 deletion src/docfx/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// Run `docfx build` command.
"docfx build": {
"commandName": "Project",
"commandLineArgs": "build ../../samples/seed/docfx.json",
"commandLineArgs": "build ../../samples/seed/docfx.json --serve",
"workingDirectory": ".",
"environmentVariables": {
}
Expand Down
8 changes: 4 additions & 4 deletions test/Docfx.Build.Tests/TocDocumentProcessorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -529,9 +529,9 @@ public void ProcessYamlTocWithReferencedTocShouldSucceed()

AssertTocEqual(expectedModel, model);

// Referenced TOC File should not exist
// Referenced TOC File should exist
var referencedTocPath = Path.Combine(_outputFolder, Path.ChangeExtension(sub1tocmd, RawModelFileExtension));
Assert.False(File.Exists(referencedTocPath));
Assert.True(File.Exists(referencedTocPath));
}

[Fact]
Expand Down Expand Up @@ -766,7 +766,7 @@ public void LoadBadTocYamlFileShouldGiveLineNumber()
href: x2.md";
var toc = _fileCreator.CreateFile(content, FileType.YamlToc);
var ex = Assert.Throws<DocumentException>(() => TocHelper.LoadSingleToc(toc));
Assert.Equal("toc.yml is not a valid TOC File: toc.yml is not a valid TOC file, detail: (Line: 3, Col: 10, Idx: 22) - (Line: 3, Col: 10, Idx: 22): While scanning a plain scalar value, found invalid mapping..", ex.Message);
Assert.Equal("toc.yml is not a valid TOC File: (Line: 3, Col: 10, Idx: 22) - (Line: 3, Col: 10, Idx: 22): While scanning a plain scalar value, found invalid mapping.", ex.Message);
}

[Fact]
Expand All @@ -787,7 +787,7 @@ public void LoadTocYamlWithEmptyNodeShouldSucceed()
// Assert
var outputRawModelPath = Path.GetFullPath(Path.Combine(_outputFolder, Path.ChangeExtension(file, RawModelFileExtension)));
Assert.True(File.Exists(outputRawModelPath));
var model = JsonUtility.Deserialize<TocRootViewModel>(outputRawModelPath);
var model = JsonUtility.Deserialize<TocItemViewModel>(outputRawModelPath);
Assert.Single(model.Items); // empty node is removed
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"order": 100.0,
"items": [
{
"name": "BuildFromAssembly",
Expand Down
Loading

0 comments on commit 8a60af9

Please sign in to comment.