4
4
5
5
using System . Collections . Frozen ;
6
6
using System . Diagnostics . CodeAnalysis ;
7
- using Elastic . Documentation ;
8
7
using Elastic . Documentation . Links ;
9
8
10
9
namespace Elastic . Markdown . Links . CrossLinks ;
11
10
12
11
public interface ICrossLinkResolver
13
12
{
14
13
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 ) ;
16
15
IUriEnvironmentResolver UriResolver { get ; }
17
16
}
18
17
@@ -27,8 +26,8 @@ public async Task<FetchedCrossLinks> FetchLinks(Cancel ctx)
27
26
return _crossLinks ;
28
27
}
29
28
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 ) ;
32
31
33
32
public FetchedCrossLinks UpdateLinkReference ( string repository , RepositoryLinks repositoryLinks )
34
33
{
@@ -43,163 +42,161 @@ public FetchedCrossLinks UpdateLinkReference(string repository, RepositoryLinks
43
42
44
43
public static bool TryResolve (
45
44
Action < string > errorEmitter ,
46
- Action < string > warningEmitter ,
47
45
FetchedCrossLinks fetchedCrossLinks ,
48
46
IUriEnvironmentResolver uriResolver ,
49
47
Uri crossLinkUri ,
50
48
[ NotNullWhen ( true ) ] out Uri ? resolvedUri
51
49
)
52
50
{
53
51
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 ) ;
57
52
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 ) )
63
54
{
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") ;
68
56
return false ;
69
57
}
70
58
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 ;
75
62
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 ;
78
77
}
79
78
80
- private static bool TryFullyValidate ( Action < string > errorEmitter ,
79
+ private static bool ResolveDirectLink ( Action < string > errorEmitter ,
81
80
IUriEnvironmentResolver uriResolver ,
82
- FetchedCrossLinks fetchedCrossLinks ,
83
- RepositoryLinks repositoryLinks ,
84
81
Uri crossLinkUri ,
82
+ string lookupPath ,
83
+ LinkMetadata linkMetadata ,
85
84
[ NotNullWhen ( true ) ] out Uri ? resolvedUri )
86
85
{
87
86
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 ) ;
96
89
97
90
if ( ! string . IsNullOrEmpty ( lookupFragment ) )
98
91
{
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 ) )
106
94
{
107
95
errorEmitter ( $ "'{ lookupPath } ' has no anchor named: '{ lookupFragment } '.") ;
108
96
return false ;
109
97
}
110
98
111
- path += "#" + lookupFragment . TrimStart ( '#' ) ;
99
+ targetUrlPath += lookupFragment ;
112
100
}
113
101
114
- resolvedUri = uriResolver . Resolve ( crossLinkUri , path ) ;
102
+ resolvedUri = uriResolver . Resolve ( crossLinkUri , targetUrlPath ) ;
115
103
return true ;
116
104
}
117
105
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 )
125
114
{
126
- lookupFragment = null ;
115
+ resolvedUri = null ;
116
+ var originalFragment = originalCrossLinkUri . Fragment . TrimStart ( '#' ) ;
127
117
128
- if ( repositoryLinks . Redirects is not null && repositoryLinks . Redirects . TryGetValue ( lookupPath , out var redirect ) )
118
+ if ( ! string . IsNullOrEmpty ( originalFragment ) && redirectRule . Many is { Length : > 0 } )
129
119
{
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 ;
135
124
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
+ }
137
133
}
138
134
139
- if ( repositoryLinks . Links . TryGetValue ( lookupPath , out link ) )
135
+ string ? finalTargetFragment = null ;
136
+
137
+ if ( ! string . IsNullOrEmpty ( originalFragment ) )
140
138
{
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
+ }
143
150
}
144
151
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 ) ;
151
155
}
152
156
153
- private static bool ResolveLinkRedirect (
154
- LinkSingleRedirect [ ] redirects ,
157
+ private static bool FinalizeRedirect (
155
158
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 )
159
165
{
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" )
163
170
{
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 ) ;
168
173
169
- if ( string . IsNullOrEmpty ( fragment ) )
170
- {
171
- lookupPath = redirect . To ;
172
- return true ;
173
- }
174
+ if ( ! string . IsNullOrEmpty ( targetFragment ) && targetFragment != "!" )
175
+ finalPathForResolver += $ "#{ targetFragment } ";
174
176
175
- if ( redirect . Anchors is null || redirect . Anchors . Count == 0 )
177
+ if ( ! fetchedCrossLinks . LinkReferences . TryGetValue ( targetCrossUri . Scheme , out var targetLinkReference ) )
176
178
{
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 ;
182
181
}
183
182
184
- if ( redirect . Anchors . TryGetValue ( "!" , out _ ) )
183
+ if ( ! targetLinkReference . Links . ContainsKey ( lookupPath ) )
185
184
{
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 ;
189
187
}
190
188
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
197
190
}
191
+ else
192
+ {
193
+ finalPathForResolver = ToTargetUrlPath ( redirectToPath ) ;
194
+ if ( ! string . IsNullOrEmpty ( targetFragment ) && targetFragment != "!" )
195
+ finalPathForResolver += $ "#{ targetFragment } ";
198
196
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 ;
203
200
}
204
201
205
202
private static string ToTargetUrlPath ( string lookupPath )
0 commit comments