66using System . Text ;
77using System . Text . Json ;
88using System . Text . RegularExpressions ;
9+ using Docfx . Build ;
910using Docfx . Plugins ;
1011using Microsoft . AspNetCore . Builder ;
1112using Microsoft . AspNetCore . Hosting ;
13+ using Microsoft . AspNetCore . Http ;
1214using Microsoft . Extensions . Logging ;
1315using Microsoft . Playwright ;
1416using Spectre . Console ;
1921using UglyToad . PdfPig . Outline . Destinations ;
2022using UglyToad . PdfPig . Writer ;
2123
24+ using static Docfx . Build . HtmlTemplate ;
25+
2226#nullable enable
2327
2428namespace 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.
0 commit comments