44
55using System . Collections . Frozen ;
66using System . Diagnostics . CodeAnalysis ;
7- using Elastic . Documentation ;
87using Elastic . Documentation . Links ;
98
109namespace Elastic . Markdown . Links . CrossLinks ;
1110
1211public interface ICrossLinkResolver
1312{
1413 Task < FetchedCrossLinks > FetchLinks ( Cancel ctx ) ;
15- bool TryResolve ( Action < string > errorEmitter , Action < string > warningEmitter , Uri crossLinkUri , [ NotNullWhen ( true ) ] out Uri ? resolvedUri ) ;
14+ bool TryResolve ( Action < string > errorEmitter , Uri crossLinkUri , [ NotNullWhen ( true ) ] out Uri ? resolvedUri ) ;
1615 IUriEnvironmentResolver UriResolver { get ; }
1716}
1817
@@ -27,8 +26,8 @@ public async Task<FetchedCrossLinks> FetchLinks(Cancel ctx)
2726 return _crossLinks ;
2827 }
2928
30- public bool TryResolve ( Action < string > errorEmitter , Action < string > warningEmitter , Uri crossLinkUri , [ NotNullWhen ( true ) ] out Uri ? resolvedUri ) =>
31- TryResolve ( errorEmitter , warningEmitter , _crossLinks , UriResolver , crossLinkUri , out resolvedUri ) ;
29+ public bool TryResolve ( Action < string > errorEmitter , Uri crossLinkUri , [ NotNullWhen ( true ) ] out Uri ? resolvedUri ) =>
30+ TryResolve ( errorEmitter , _crossLinks , UriResolver , crossLinkUri , out resolvedUri ) ;
3231
3332 public FetchedCrossLinks UpdateLinkReference ( string repository , RepositoryLinks repositoryLinks )
3433 {
@@ -43,163 +42,161 @@ public FetchedCrossLinks UpdateLinkReference(string repository, RepositoryLinks
4342
4443 public static bool TryResolve (
4544 Action < string > errorEmitter ,
46- Action < string > warningEmitter ,
4745 FetchedCrossLinks fetchedCrossLinks ,
4846 IUriEnvironmentResolver uriResolver ,
4947 Uri crossLinkUri ,
5048 [ NotNullWhen ( true ) ] out Uri ? resolvedUri
5149 )
5250 {
5351 resolvedUri = null ;
54- var lookup = fetchedCrossLinks . LinkReferences ;
55- if ( crossLinkUri . Scheme != "asciidocalypse" && lookup . TryGetValue ( crossLinkUri . Scheme , out var linkReference ) )
56- return TryFullyValidate ( errorEmitter , uriResolver , fetchedCrossLinks , linkReference , crossLinkUri , out resolvedUri ) ;
5752
58- // TODO this is temporary while we wait for all links.json to be published
59- // Here we just silently rewrite the cross_link to the url
60-
61- var declaredRepositories = fetchedCrossLinks . DeclaredRepositories ;
62- if ( ! declaredRepositories . Contains ( crossLinkUri . Scheme ) )
53+ if ( ! fetchedCrossLinks . LinkReferences . TryGetValue ( crossLinkUri . Scheme , out var sourceLinkReference ) )
6354 {
64- if ( fetchedCrossLinks . FromConfiguration )
65- errorEmitter ( $ "'{ crossLinkUri . Scheme } ' is not declared as valid cross link repository in docset.yml under cross_links: '{ crossLinkUri } '") ;
66- else
67- warningEmitter ( $ "'{ crossLinkUri . Scheme } ' is not yet publishing to the links registry: '{ crossLinkUri } '") ;
55+ errorEmitter ( $ "'{ crossLinkUri . Scheme } ' was not found in the cross link index") ;
6856 return false ;
6957 }
7058
71- var lookupPath = ( crossLinkUri . Host + '/' + crossLinkUri . AbsolutePath . TrimStart ( '/' ) ) . Trim ( '/' ) ;
72- var path = ToTargetUrlPath ( lookupPath ) ;
73- if ( ! string . IsNullOrEmpty ( crossLinkUri . Fragment ) )
74- path += crossLinkUri . Fragment ;
59+ var originalLookupPath = ( crossLinkUri . Host + '/' + crossLinkUri . AbsolutePath . TrimStart ( '/' ) ) . Trim ( '/' ) ;
60+ if ( string . IsNullOrEmpty ( originalLookupPath ) && crossLinkUri . Host . EndsWith ( ".md" ) )
61+ originalLookupPath = crossLinkUri . Host ;
7562
76- resolvedUri = uriResolver . Resolve ( crossLinkUri , path ) ;
77- return true ;
63+ if ( sourceLinkReference . Redirects is not null && sourceLinkReference . Redirects . TryGetValue ( originalLookupPath , out var redirectRule ) )
64+ return ResolveRedirect ( errorEmitter , uriResolver , crossLinkUri , redirectRule , originalLookupPath , fetchedCrossLinks , out resolvedUri ) ;
65+
66+ if ( sourceLinkReference . Links . TryGetValue ( originalLookupPath , out var directLinkMetadata ) )
67+ return ResolveDirectLink ( errorEmitter , uriResolver , crossLinkUri , originalLookupPath , directLinkMetadata , out resolvedUri ) ;
68+
69+
70+ var linksJson = $ "https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{ crossLinkUri . Scheme } /main/links.json";
71+ if ( fetchedCrossLinks . LinkIndexEntries . TryGetValue ( crossLinkUri . Scheme , out var indexEntry ) )
72+ linksJson = $ "https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{ indexEntry . Path } ";
73+
74+ errorEmitter ( $ "'{ originalLookupPath } ' is not a valid link in the '{ crossLinkUri . Scheme } ' cross link index: { linksJson } ") ;
75+ resolvedUri = null ;
76+ return false ;
7877 }
7978
80- private static bool TryFullyValidate ( Action < string > errorEmitter ,
79+ private static bool ResolveDirectLink ( Action < string > errorEmitter ,
8180 IUriEnvironmentResolver uriResolver ,
82- FetchedCrossLinks fetchedCrossLinks ,
83- RepositoryLinks repositoryLinks ,
8481 Uri crossLinkUri ,
82+ string lookupPath ,
83+ LinkMetadata linkMetadata ,
8584 [ NotNullWhen ( true ) ] out Uri ? resolvedUri )
8685 {
8786 resolvedUri = null ;
88- var lookupPath = ( crossLinkUri . Host + '/' + crossLinkUri . AbsolutePath . TrimStart ( '/' ) ) . Trim ( '/' ) ;
89- if ( string . IsNullOrEmpty ( lookupPath ) && crossLinkUri . Host . EndsWith ( ".md" ) )
90- lookupPath = crossLinkUri . Host ;
91-
92- if ( ! LookupLink ( errorEmitter , fetchedCrossLinks , repositoryLinks , crossLinkUri , ref lookupPath , out var link , out var lookupFragment ) )
93- return false ;
94-
95- var path = ToTargetUrlPath ( lookupPath ) ;
87+ var lookupFragment = crossLinkUri . Fragment ;
88+ var targetUrlPath = ToTargetUrlPath ( lookupPath ) ;
9689
9790 if ( ! string . IsNullOrEmpty ( lookupFragment ) )
9891 {
99- if ( link . Anchors is null )
100- {
101- errorEmitter ( $ "'{ lookupPath } ' does not have any anchors so linking to '{ crossLinkUri . Fragment } ' is impossible.") ;
102- return false ;
103- }
104-
105- if ( ! link . Anchors . Contains ( lookupFragment . TrimStart ( '#' ) ) )
92+ var anchor = lookupFragment . TrimStart ( '#' ) ;
93+ if ( linkMetadata . Anchors is null || ! linkMetadata . Anchors . Contains ( anchor ) )
10694 {
10795 errorEmitter ( $ "'{ lookupPath } ' has no anchor named: '{ lookupFragment } '.") ;
10896 return false ;
10997 }
11098
111- path += "#" + lookupFragment . TrimStart ( '#' ) ;
99+ targetUrlPath += lookupFragment ;
112100 }
113101
114- resolvedUri = uriResolver . Resolve ( crossLinkUri , path ) ;
102+ resolvedUri = uriResolver . Resolve ( crossLinkUri , targetUrlPath ) ;
115103 return true ;
116104 }
117105
118- private static bool LookupLink ( Action < string > errorEmitter ,
119- FetchedCrossLinks crossLinks ,
120- RepositoryLinks repositoryLinks ,
121- Uri crossLinkUri ,
122- ref string lookupPath ,
123- [ NotNullWhen ( true ) ] out LinkMetadata ? link ,
124- [ NotNullWhen ( true ) ] out string ? lookupFragment )
106+ private static bool ResolveRedirect (
107+ Action < string > errorEmitter ,
108+ IUriEnvironmentResolver uriResolver ,
109+ Uri originalCrossLinkUri ,
110+ LinkRedirect redirectRule ,
111+ string originalLookupPath ,
112+ FetchedCrossLinks fetchedCrossLinks ,
113+ [ NotNullWhen ( true ) ] out Uri ? resolvedUri )
125114 {
126- lookupFragment = null ;
115+ resolvedUri = null ;
116+ var originalFragment = originalCrossLinkUri . Fragment . TrimStart ( '#' ) ;
127117
128- if ( repositoryLinks . Redirects is not null && repositoryLinks . Redirects . TryGetValue ( lookupPath , out var redirect ) )
118+ if ( ! string . IsNullOrEmpty ( originalFragment ) && redirectRule . Many is { Length : > 0 } )
129119 {
130- var targets = ( redirect . Many ?? [ ] )
131- . Select ( r => r )
132- . Concat ( [ redirect ] )
133- . Where ( s => ! string . IsNullOrEmpty ( s . To ) )
134- . ToArray ( ) ;
120+ foreach ( var subRule in redirectRule . Many )
121+ {
122+ if ( string . IsNullOrEmpty ( subRule . To ) )
123+ continue ;
135124
136- return ResolveLinkRedirect ( targets , errorEmitter , repositoryLinks , crossLinkUri , ref lookupPath , out link , ref lookupFragment ) ;
125+ if ( subRule . Anchors is null || subRule . Anchors . Count == 0 )
126+ continue ;
127+
128+ if ( subRule . Anchors . TryGetValue ( "!" , out _ ) )
129+ return FinalizeRedirect ( errorEmitter , uriResolver , originalCrossLinkUri , subRule . To , null , fetchedCrossLinks , out resolvedUri ) ;
130+ if ( subRule . Anchors . TryGetValue ( originalFragment , out var mappedAnchor ) )
131+ return FinalizeRedirect ( errorEmitter , uriResolver , originalCrossLinkUri , subRule . To , mappedAnchor , fetchedCrossLinks , out resolvedUri ) ;
132+ }
137133 }
138134
139- if ( repositoryLinks . Links . TryGetValue ( lookupPath , out link ) )
135+ string ? finalTargetFragment = null ;
136+
137+ if ( ! string . IsNullOrEmpty ( originalFragment ) )
140138 {
141- lookupFragment = crossLinkUri . Fragment ;
142- return true ;
139+ if ( redirectRule . Anchors ? . TryGetValue ( "!" , out _ ) ?? false )
140+ finalTargetFragment = null ;
141+ else if ( redirectRule . Anchors ? . TryGetValue ( originalFragment , out var mappedAnchor ) ?? false )
142+ finalTargetFragment = mappedAnchor ;
143+ else if ( redirectRule . Anchors is null || redirectRule . Anchors . Count == 0 )
144+ finalTargetFragment = originalFragment ;
145+ else
146+ {
147+ errorEmitter ( $ "Redirect rule for '{ originalLookupPath } ' in '{ originalCrossLinkUri . Scheme } ' found, but top-level rule did not handle anchor '#{ originalFragment } '.") ;
148+ return false ;
149+ }
143150 }
144151
145- var linksJson = $ "https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{ crossLinkUri . Scheme } /main/links.json";
146- if ( crossLinks . LinkIndexEntries . TryGetValue ( crossLinkUri . Scheme , out var linkIndexEntry ) )
147- linksJson = $ "https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{ linkIndexEntry . Path } ";
148-
149- errorEmitter ( $ "'{ lookupPath } ' is not a valid link in the '{ crossLinkUri . Scheme } ' cross link index: { linksJson } ") ;
150- return false ;
152+ return string . IsNullOrEmpty ( redirectRule . To )
153+ ? FinalizeRedirect ( errorEmitter , uriResolver , originalCrossLinkUri , originalLookupPath , finalTargetFragment , fetchedCrossLinks , out resolvedUri )
154+ : FinalizeRedirect ( errorEmitter , uriResolver , originalCrossLinkUri , redirectRule . To , finalTargetFragment , fetchedCrossLinks , out resolvedUri ) ;
151155 }
152156
153- private static bool ResolveLinkRedirect (
154- LinkSingleRedirect [ ] redirects ,
157+ private static bool FinalizeRedirect (
155158 Action < string > errorEmitter ,
156- RepositoryLinks repositoryLinks ,
157- Uri crossLinkUri ,
158- ref string lookupPath , out LinkMetadata ? link , ref string ? lookupFragment )
159+ IUriEnvironmentResolver uriResolver ,
160+ Uri originalProcessingUri ,
161+ string redirectToPath ,
162+ string ? targetFragment ,
163+ FetchedCrossLinks fetchedCrossLinks ,
164+ [ NotNullWhen ( true ) ] out Uri ? resolvedUri )
159165 {
160- var fragment = crossLinkUri . Fragment . TrimStart ( '#' ) ;
161- link = null ;
162- foreach ( var redirect in redirects )
166+ resolvedUri = null ;
167+ string finalPathForResolver ;
168+
169+ if ( Uri . TryCreate ( redirectToPath , UriKind . Absolute , out var targetCrossUri ) && targetCrossUri . Scheme != "http" && targetCrossUri . Scheme != "https" )
163170 {
164- if ( string . IsNullOrEmpty ( redirect . To ) )
165- continue ;
166- if ( ! repositoryLinks . Links . TryGetValue ( redirect . To , out link ) )
167- continue ;
171+ var lookupPath = $ "{ targetCrossUri . Host } /{ targetCrossUri . AbsolutePath . TrimStart ( '/' ) } ";
172+ finalPathForResolver = ToTargetUrlPath ( lookupPath ) ;
168173
169- if ( string . IsNullOrEmpty ( fragment ) )
170- {
171- lookupPath = redirect . To ;
172- return true ;
173- }
174+ if ( ! string . IsNullOrEmpty ( targetFragment ) && targetFragment != "!" )
175+ finalPathForResolver += $ "#{ targetFragment } ";
174176
175- if ( redirect . Anchors is null || redirect . Anchors . Count == 0 )
177+ if ( ! fetchedCrossLinks . LinkReferences . TryGetValue ( targetCrossUri . Scheme , out var targetLinkReference ) )
176178 {
177- if ( redirects . Length > 1 )
178- continue ;
179- lookupPath = redirect . To ;
180- lookupFragment = crossLinkUri . Fragment ;
181- return true ;
179+ errorEmitter ( $ "Redirect target '{ redirectToPath } ' points to repository '{ targetCrossUri . Scheme } ' for which no links.json was found.") ;
180+ return false ;
182181 }
183182
184- if ( redirect . Anchors . TryGetValue ( "!" , out _ ) )
183+ if ( ! targetLinkReference . Links . ContainsKey ( lookupPath ) )
185184 {
186- lookupPath = redirect . To ;
187- lookupFragment = null ;
188- return true ;
185+ errorEmitter ( $ "Redirect target '{ redirectToPath } ' points to file '{ lookupPath } ' which was not found in repository '{ targetCrossUri . Scheme } 's links.json.") ;
186+ return false ;
189187 }
190188
191- if ( ! redirect . Anchors . TryGetValue ( crossLinkUri . Fragment . TrimStart ( '#' ) , out var newFragment ) )
192- continue ;
193-
194- lookupPath = redirect . To ;
195- lookupFragment = newFragment ;
196- return true ;
189+ resolvedUri = uriResolver . Resolve ( targetCrossUri , finalPathForResolver ) ; // Use targetUri for scheme and base
197190 }
191+ else
192+ {
193+ finalPathForResolver = ToTargetUrlPath ( redirectToPath ) ;
194+ if ( ! string . IsNullOrEmpty ( targetFragment ) && targetFragment != "!" )
195+ finalPathForResolver += $ "#{ targetFragment } ";
198196
199- var targets = string . Join ( ", " , redirects . Select ( r => r . To ) ) ;
200- var failedLookup = lookupFragment is null ? lookupPath : $ "{ lookupPath } #{ lookupFragment . TrimStart ( '#' ) } ";
201- errorEmitter ( $ "'{ failedLookup } ' is set a redirect but none of redirect '{ targets } ' match or exist in links.json.") ;
202- return false ;
197+ resolvedUri = uriResolver . Resolve ( originalProcessingUri , finalPathForResolver ) ; // Use original URI's scheme
198+ }
199+ return true ;
203200 }
204201
205202 private static string ToTargetUrlPath ( string lookupPath )
0 commit comments