11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4+ using System . Collections . Immutable ;
45using System . Composition ;
56using System . Web ;
6-
7+ using Docfx . Build . Common ;
78using Docfx . Common ;
89using Docfx . DataContracts . Common ;
910using Docfx . Plugins ;
1011
1112namespace Docfx . Build . TableOfContents ;
1213
1314[ Export ( typeof ( IDocumentProcessor ) ) ]
14- public class TocDocumentProcessor : TocDocumentProcessorBase
15+ public class TocDocumentProcessor : DisposableDocumentProcessor
1516{
17+ private static readonly char [ ] QueryStringOrAnchor = new [ ] { '#' , '?' } ;
18+
1619 public override string Name => nameof ( TocDocumentProcessor ) ;
1720
1821 [ ImportMany ( nameof ( TocDocumentProcessor ) ) ]
@@ -28,7 +31,155 @@ public override ProcessingPriority GetProcessingPriority(FileAndType file)
2831 return ProcessingPriority . NotSupported ;
2932 }
3033
31- protected override void RegisterTocToContext ( TocItemViewModel toc , FileModel model , IDocumentBuildContext context )
34+ public override FileModel Load ( FileAndType file , ImmutableDictionary < string , object > metadata )
35+ {
36+ var filePath = file . FullPath ;
37+ var toc = TocHelper . LoadSingleToc ( filePath ) ;
38+
39+ var displayLocalPath = PathUtility . MakeRelativePath ( EnvironmentContext . BaseDirectory , file . FullPath ) ;
40+
41+ // Apply metadata to TOC
42+ foreach ( var ( key , value ) in metadata . OrderBy ( item => item . Key ) )
43+ {
44+ toc . Metadata [ key ] = value ;
45+ }
46+
47+ return new FileModel ( file , toc )
48+ {
49+ LocalPathFromRoot = displayLocalPath
50+ } ;
51+ }
52+
53+ public override SaveResult Save ( FileModel model )
54+ {
55+ return new SaveResult
56+ {
57+ DocumentType = Constants . DocumentType . Toc ,
58+ FileWithoutExtension = Path . ChangeExtension ( model . File , null ) ,
59+ LinkToFiles = model . LinkToFiles . ToImmutableArray ( ) ,
60+ LinkToUids = model . LinkToUids ,
61+ FileLinkSources = model . FileLinkSources ,
62+ UidLinkSources = model . UidLinkSources ,
63+ } ;
64+ }
65+
66+ public override void UpdateHref ( FileModel model , IDocumentBuildContext context )
67+ {
68+ var toc = ( TocItemViewModel ) model . Content ;
69+ UpdateTocItemHref ( toc , model , context ) ;
70+
71+ RegisterTocToContext ( toc , model , context ) ;
72+ model . Content = toc ;
73+ }
74+
75+ private void UpdateTocItemHref ( TocItemViewModel toc , FileModel model , IDocumentBuildContext context , string includedFrom = null )
76+ {
77+ if ( toc . IsHrefUpdated ) return ;
78+
79+ ResolveUid ( toc , model , context , includedFrom ) ;
80+
81+ // Have to register TocMap after uid is resolved
82+ RegisterTocMapToContext ( toc , model , context ) ;
83+
84+ toc . Homepage = ResolveHref ( toc . Homepage , toc . OriginalHomepage , model , context , nameof ( toc . Homepage ) ) ;
85+ toc . OriginalHomepage = null ;
86+ toc . Href = ResolveHref ( toc . Href , toc . OriginalHref , model , context , nameof ( toc . Href ) ) ;
87+ toc . OriginalHref = null ;
88+ toc . TocHref = ResolveHref ( toc . TocHref , toc . OriginalTocHref , model , context , nameof ( toc . TocHref ) ) ;
89+ toc . OriginalTocHref = null ;
90+ toc . TopicHref = ResolveHref ( toc . TopicHref , toc . OriginalTopicHref , model , context , nameof ( toc . TopicHref ) ) ;
91+ toc . OriginalTopicHref = null ;
92+
93+ includedFrom = toc . IncludedFrom ?? includedFrom ;
94+ if ( toc . Items != null && toc . Items . Count > 0 )
95+ {
96+ foreach ( var item in toc . Items )
97+ {
98+ UpdateTocItemHref ( item , model , context , includedFrom ) ;
99+ }
100+ }
101+
102+ toc . IsHrefUpdated = true ;
103+ }
104+
105+ private static void ResolveUid ( TocItemViewModel item , FileModel model , IDocumentBuildContext context , string includedFrom )
106+ {
107+ if ( item . TopicUid != null )
108+ {
109+ var xref = GetXrefFromUid ( item . TopicUid , model , context , includedFrom ) ;
110+ if ( xref != null )
111+ {
112+ item . Href = item . TopicHref = xref . Href ;
113+ if ( string . IsNullOrEmpty ( item . Name ) )
114+ {
115+ item . Name = xref . Name ;
116+ }
117+
118+ if ( string . IsNullOrEmpty ( item . NameForCSharp ) && xref . TryGetXrefStringValue ( "name.csharp" , out var nameForCSharp ) )
119+ {
120+ item . NameForCSharp = nameForCSharp ;
121+ }
122+ if ( string . IsNullOrEmpty ( item . NameForVB ) && xref . TryGetXrefStringValue ( "name.vb" , out var nameForVB ) )
123+ {
124+ item . NameForVB = nameForVB ;
125+ }
126+ }
127+ }
128+ }
129+
130+ private static XRefSpec GetXrefFromUid ( string uid , FileModel model , IDocumentBuildContext context , string includedFrom )
131+ {
132+ var xref = context . GetXrefSpec ( uid ) ;
133+ if ( xref == null )
134+ {
135+ Logger . LogWarning (
136+ $ "Unable to find file with uid \" { uid } \" referenced by TOC file \" { includedFrom ?? model . LocalPathFromRoot } \" ",
137+ code : WarningCodes . Build . UidNotFound ,
138+ file : includedFrom ) ;
139+ }
140+ return xref ;
141+ }
142+
143+ private static string ResolveHref ( string pathToFile , string originalPathToFile , FileModel model , IDocumentBuildContext context , string propertyName )
144+ {
145+ if ( ! Utility . IsSupportedRelativeHref ( pathToFile ) )
146+ {
147+ return pathToFile ;
148+ }
149+
150+ var index = pathToFile . IndexOfAny ( QueryStringOrAnchor ) ;
151+ if ( index == 0 )
152+ {
153+ var message = $ "Invalid toc link for { propertyName } : { originalPathToFile } .";
154+ Logger . LogError ( message , code : ErrorCodes . Toc . InvalidTocLink ) ;
155+ throw new DocumentException ( message ) ;
156+ }
157+
158+ var path = UriUtility . GetPath ( pathToFile ) ;
159+ var segments = UriUtility . GetQueryStringAndFragment ( pathToFile ) ;
160+
161+ var fli = new FileLinkInfo ( model . LocalPathFromRoot , model . File , path , context ) ;
162+ var href = context . HrefGenerator ? . GenerateHref ( fli ) ;
163+
164+ // Check href is modified by HrefGenerator or not.
165+ if ( href != null && href != fli . Href )
166+ {
167+ return UriUtility . MergeHref ( href , segments ) ;
168+ }
169+
170+ if ( fli . ToFileInDest == null )
171+ {
172+ // original path to file can be null for files generated by docfx in PreBuild
173+ var displayFilePath = string . IsNullOrEmpty ( originalPathToFile ) ? pathToFile : originalPathToFile ;
174+ Logger . LogInfo ( $ "Unable to find file \" { displayFilePath } \" for { propertyName } referenced by TOC file \" { model . LocalPathFromRoot } \" ") ;
175+ return originalPathToFile ;
176+ }
177+
178+ // fragment and query in original href takes precedence over the one from hrefGenerator
179+ return fli . Href + segments ;
180+ }
181+
182+ private void RegisterTocToContext ( TocItemViewModel toc , FileModel model , IDocumentBuildContext context )
32183 {
33184 var key = model . Key ;
34185
@@ -49,7 +200,7 @@ protected override void RegisterTocToContext(TocItemViewModel toc, FileModel mod
49200 context . RegisterTocInfo ( tocInfo ) ;
50201 }
51202
52- protected override void RegisterTocMapToContext ( TocItemViewModel item , FileModel model , IDocumentBuildContext context )
203+ private void RegisterTocMapToContext ( TocItemViewModel item , FileModel model , IDocumentBuildContext context )
53204 {
54205 var key = model . Key ;
55206 // If tocHref is set, href is originally RelativeFolder type, and href is set to the homepage of TocHref,
0 commit comments