Skip to content

Commit 4b65651

Browse files
authored
feat: support PDF toc page (#9356)
* feat: support PDF toc page * test(snapshot): update snapshots for eee69ff --------- Co-authored-by: yufeih <yufeih@users.noreply.github.com>
1 parent 0a2f278 commit 4b65651

File tree

186 files changed

+238
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

186 files changed

+238
-20
lines changed

samples/seed/docfx.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@
8181
"_appTitle": "docfx seed website",
8282
"_appName": "Seed",
8383
"_enableSearch": true,
84-
"pdf": true
84+
"pdf": true,
85+
"pdfTocPage": true
8586
},
8687
"output": "_site",
8788
"exportViewModel": true,

src/Docfx.App/PdfBuilder.cs

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
using System.Text;
77
using System.Text.Json;
88
using System.Text.RegularExpressions;
9+
using Docfx.Build;
910
using Docfx.Plugins;
1011
using Microsoft.AspNetCore.Builder;
1112
using Microsoft.AspNetCore.Hosting;
13+
using Microsoft.AspNetCore.Http;
1214
using Microsoft.Extensions.Logging;
1315
using Microsoft.Playwright;
1416
using Spectre.Console;
@@ -19,6 +21,8 @@
1921
using UglyToad.PdfPig.Outline.Destinations;
2022
using UglyToad.PdfPig.Writer;
2123

24+
using static Docfx.Build.HtmlTemplate;
25+
2226
#nullable enable
2327

2428
namespace Docfx.Pdf;
@@ -34,6 +38,7 @@ class Outline
3438
public bool pdf { get; init; }
3539
public string? pdfFileName { get; init; }
3640
public string? pdfCoverPage { get; init; }
41+
public bool pdfTocPage { get; init; }
3742
}
3843

3944
public static Task Run(BuildJsonConfig config, string configDirectory, string? outputDirectory = null)
@@ -47,8 +52,8 @@ public static Task Run(BuildJsonConfig config, string configDirectory, string? o
4752

4853
public static async Task CreatePdf(string outputFolder)
4954
{
50-
var pdfTocs = GetPdfTocs().ToArray();
51-
if (pdfTocs.Length == 0)
55+
var pdfTocs = GetPdfTocs().ToDictionary(p => p.url, p => p.toc);
56+
if (pdfTocs.Count == 0)
5257
return;
5358

5459
AnsiConsole.Status().Start("Installing Chromium...", _ => Program.Main(new[] { "install", "chromium" }));
@@ -58,10 +63,14 @@ public static async Task CreatePdf(string outputFolder)
5863
builder.Logging.ClearProviders();
5964
builder.WebHost.UseUrls("http://127.0.0.1:0");
6065

66+
Uri? baseUrl = null;
67+
6168
using var app = builder.Build();
6269
app.UseServe(outputFolder);
70+
app.MapGet("/_pdftoc/{*id}", (string id) => Results.Content(TocHtmlTemplate(new Uri(baseUrl!, id), pdfTocs[id]).ToString(), "text/html"));
6371
await app.StartAsync();
64-
var baseUrl = new Uri(app.Urls.First());
72+
73+
baseUrl = new Uri(app.Urls.First());
6574

6675
using var playwright = await Playwright.CreateAsync();
6776
var browser = await playwright.Chromium.LaunchAsync();
@@ -73,7 +82,7 @@ public static async Task CreatePdf(string outputFolder)
7382
await CreatePdf(browser, new(baseUrl, url), toc, outputPath);
7483
}
7584

76-
IEnumerable<(string, Outline)> GetPdfTocs()
85+
IEnumerable<(string url, Outline toc)> GetPdfTocs()
7786
{
7887
var manifestPath = Path.Combine(outputFolder, "manifest.json");
7988
var manifest = Newtonsoft.Json.JsonConvert.DeserializeObject<Manifest>(File.ReadAllText(manifestPath));
@@ -141,6 +150,13 @@ await Parallel.ForEachAsync(pages, async (item, CancellationToken) =>
141150
yield return (GetFilePath(url), url, new() { href = outline.pdfCoverPage });
142151
}
143152

153+
if (outline.pdfTocPage)
154+
{
155+
var href = $"/_pdftoc{outlineUrl.AbsolutePath}";
156+
var url = new Uri(outlineUrl, href);
157+
yield return (GetFilePath(url), url, new() { href = href });
158+
}
159+
144160
if (!string.IsNullOrEmpty(outline.href))
145161
{
146162
var url = new Uri(outlineUrl, outline.href);
@@ -229,7 +245,7 @@ PdfAction HandleUriAction(UriAction url)
229245
AnsiConsole.MarkupLine($"[yellow]Failed to resolve named dest: {name}[/]");
230246
}
231247

232-
return new GoToAction(new(pageNumbers[pages[0].node], ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty));
248+
return new GoToAction(new(pageNumbers[pages[0].node], ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty));
233249
}
234250
}
235251

@@ -249,7 +265,7 @@ IEnumerable<BookmarkNode> CreateBookmarks(Outline[]? items, int level = 0)
249265
{
250266
yield return new DocumentBookmarkNode(
251267
item.name, level,
252-
new(nextPageNumber, ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty),
268+
new(nextPageNumber, ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty),
253269
CreateBookmarks(item.items, level + 1).ToArray());
254270
continue;
255271
}
@@ -268,13 +284,33 @@ IEnumerable<BookmarkNode> CreateBookmarks(Outline[]? items, int level = 0)
268284
nextPageNumber = nextPageNumbers[item];
269285
yield return new DocumentBookmarkNode(
270286
item.name, level,
271-
new(pageNumbers[item], ExplicitDestinationType.XyzCoordinates, ExplicitDestinationCoordinates.Empty),
287+
new(pageNumbers[item], ExplicitDestinationType.FitHorizontally, ExplicitDestinationCoordinates.Empty),
272288
CreateBookmarks(item.items, level + 1).ToArray());
273289
}
274290
}
275291
}
276292
}
277293

294+
static HtmlTemplate TocHtmlTemplate(Uri baseUrl, Outline node)
295+
{
296+
return Html($"""
297+
<!DOCTYPE html>
298+
<html>
299+
<head>
300+
<link rel="stylesheet" href="/public/docfx.min.css">
301+
<link rel="stylesheet" href="/public/main.css">
302+
</head>
303+
<body class="pdftoc">
304+
<h1>Table of Contents</h1>
305+
<ul>{node.items?.Select(TocNode)}</ul>
306+
</body>
307+
</html>
308+
""");
309+
310+
HtmlTemplate TocNode(Outline node) => string.IsNullOrEmpty(node.name) ? default :
311+
Html($"<li><a href='{(string.IsNullOrEmpty(node.href) ? null : new Uri(baseUrl, node.href))}'>{node.name}</a>{(node.items?.Length > 0 ? Html($"<ul>{node.items.Select(TocNode)}</ul>") : null)}</li>");
312+
}
313+
278314
/// <summary>
279315
/// Adds hidden links to headings to ensure Chromium saves heading anchors to named dests
280316
/// for cross page bookmark reference.

templates/modern/src/docfx.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,7 @@ article {
7979
margin: .4in;
8080
}
8181
}
82+
83+
.pdftoc ul {
84+
list-style: none;
85+
}

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromAssembly.Class1.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,7 @@
825825
"_appTitle": "docfx seed website",
826826
"_enableSearch": true,
827827
"pdf": true,
828+
"pdfTocPage": true,
828829
"_key": "obj/api/BuildFromAssembly.Class1.yml",
829830
"_navKey": "~/toc.yml",
830831
"_navPath": "toc.html",

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromAssembly.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"_appTitle": "docfx seed website",
117117
"_enableSearch": true,
118118
"pdf": true,
119+
"pdfTocPage": true,
119120
"_key": "obj/api/BuildFromAssembly.yml",
120121
"_navKey": "~/toc.yml",
121122
"_navPath": "toc.html",

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromCSharpSourceCode.CSharp.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@
742742
"_appTitle": "docfx seed website",
743743
"_enableSearch": true,
744744
"pdf": true,
745+
"pdfTocPage": true,
745746
"_key": "obj/api/BuildFromCSharpSourceCode.CSharp.yml",
746747
"_navKey": "~/toc.yml",
747748
"_navPath": "toc.html",

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromCSharpSourceCode.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"_appTitle": "docfx seed website",
113113
"_enableSearch": true,
114114
"pdf": true,
115+
"pdfTocPage": true,
115116
"_key": "obj/api/BuildFromCSharpSourceCode.yml",
116117
"_navKey": "~/toc.yml",
117118
"_navPath": "toc.html",

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromProject.Class1.IIssue8948.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@
309309
"_appTitle": "docfx seed website",
310310
"_enableSearch": true,
311311
"pdf": true,
312+
"pdfTocPage": true,
312313
"_key": "obj/api/BuildFromProject.Class1.IIssue8948.yml",
313314
"_navKey": "~/toc.yml",
314315
"_navPath": "toc.html",

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromProject.Class1.Issue8665.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,6 +1922,7 @@
19221922
"_appTitle": "docfx seed website",
19231923
"_enableSearch": true,
19241924
"pdf": true,
1925+
"pdfTocPage": true,
19251926
"_key": "obj/api/BuildFromProject.Class1.Issue8665.yml",
19261927
"_navKey": "~/toc.yml",
19271928
"_navPath": "toc.html",

test/docfx.Snapshot.Tests/SamplesTest.Seed/api/BuildFromProject.Class1.Issue8696Attribute.html.view.verified.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2743,6 +2743,7 @@
27432743
"_appTitle": "docfx seed website",
27442744
"_enableSearch": true,
27452745
"pdf": true,
2746+
"pdfTocPage": true,
27462747
"_key": "obj/api/BuildFromProject.Class1.Issue8696Attribute.yml",
27472748
"_navKey": "~/toc.yml",
27482749
"_navPath": "toc.html",

0 commit comments

Comments
 (0)