2
2
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3
3
// See the LICENSE file in the project root for more information
4
4
5
- using System . IO . Abstractions ;
6
- using System . Runtime . InteropServices ;
7
5
using DotNet . Globbing ;
8
6
using Elastic . Markdown . Diagnostics ;
9
7
using Elastic . Markdown . Extensions ;
10
8
using Elastic . Markdown . Extensions . DetectionRules ;
11
9
using Elastic . Markdown . IO . State ;
12
- using YamlDotNet . RepresentationModel ;
13
10
14
11
namespace Elastic . Markdown . IO . Configuration ;
15
12
16
13
public record ConfigurationFile : DocumentationFile
17
14
{
18
- private readonly IDirectoryInfo _rootPath ;
19
15
private readonly BuildContext _context ;
20
- private readonly int _depth ;
16
+
21
17
public string ? Project { get ; }
18
+
22
19
public Glob [ ] Exclude { get ; } = [ ] ;
23
- public bool SoftLineEndings { get ; }
24
20
25
21
public string [ ] CrossLinkRepositories { get ; } = [ ] ;
26
22
23
+ /// The maximum depth `toc.yml` files may appear
24
+ public int MaxTocDepth { get ; } = 1 ;
25
+
27
26
public EnabledExtensions Extensions { get ; } = new ( [ ] ) ;
27
+
28
28
public IReadOnlyCollection < IDocsBuilderExtension > EnabledExtensions { get ; } = [ ] ;
29
29
30
30
public IReadOnlyCollection < ITocItem > TableOfContents { get ; } = [ ] ;
31
31
32
+ public HashSet < string > Files { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
33
+
32
34
public Dictionary < string , LinkRedirect > ? Redirects { get ; }
33
35
34
- public HashSet < string > Files { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
35
36
public HashSet < string > ImplicitFolders { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
37
+
36
38
public Glob [ ] Globs { get ; } = [ ] ;
37
39
38
40
private readonly Dictionary < string , string > _substitutions = new ( StringComparer . OrdinalIgnoreCase ) ;
@@ -42,19 +44,18 @@ public record ConfigurationFile : DocumentationFile
42
44
private FeatureFlags ? _featureFlags ;
43
45
public FeatureFlags Features => _featureFlags ??= new FeatureFlags ( _features ) ;
44
46
45
- public ConfigurationFile ( IFileInfo sourceFile , IDirectoryInfo rootPath , BuildContext context , int depth = 0 , string parentPath = "" )
46
- : base ( sourceFile , rootPath )
47
+ public ConfigurationFile ( BuildContext context )
48
+ : base ( context . ConfigurationPath , context . DocumentationSourceDirectory )
47
49
{
48
- _rootPath = rootPath ;
49
50
_context = context ;
50
- _depth = depth ;
51
- if ( ! sourceFile . Exists )
51
+ if ( ! context . ConfigurationPath . Exists )
52
52
{
53
53
Project = "unknown" ;
54
- context . EmitWarning ( sourceFile , "No configuration file found" ) ;
54
+ context . EmitWarning ( context . ConfigurationPath , "No configuration file found" ) ;
55
55
return ;
56
56
}
57
57
58
+ var sourceFile = context . ConfigurationPath ;
58
59
var redirectFileName = sourceFile . Name . StartsWith ( '_' ) ? "_redirects.yml" : "redirects.yml" ;
59
60
var redirectFileInfo = sourceFile . FileSystem . FileInfo . New ( Path . Combine ( sourceFile . Directory ! . FullName , redirectFileName ) ) ;
60
61
var redirectFile = new RedirectFile ( redirectFileInfo , _context ) ;
@@ -70,11 +71,12 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
70
71
case "project" :
71
72
Project = reader . ReadString ( entry . Entry ) ;
72
73
break ;
73
- case "soft_line_endings " :
74
- SoftLineEndings = bool . TryParse ( reader . ReadString ( entry . Entry ) , out var softLineEndings ) && softLineEndings ;
74
+ case "max_toc_depth " :
75
+ MaxTocDepth = int . TryParse ( reader . ReadString ( entry . Entry ) , out var maxTocDepth ) ? maxTocDepth : 1 ;
75
76
break ;
76
77
case "exclude" :
77
- Exclude = [ .. YamlStreamReader . ReadStringArray ( entry . Entry ) . Select ( Glob . Parse ) ] ;
78
+ var excludes = YamlStreamReader . ReadStringArray ( entry . Entry ) ;
79
+ Exclude = [ .. excludes . Where ( s => ! string . IsNullOrEmpty ( s ) ) . Select ( Glob . Parse ) ] ;
78
80
break ;
79
81
case "cross_links" :
80
82
CrossLinkRepositories = [ .. YamlStreamReader . ReadStringArray ( entry . Entry ) ] ;
@@ -87,15 +89,7 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
87
89
_substitutions = reader . ReadDictionary ( entry . Entry ) ;
88
90
break ;
89
91
case "toc" :
90
- if ( depth > 1 )
91
- {
92
- reader . EmitError ( $ "toc.yml files may only be linked from docset.yml", entry . Key ) ;
93
- break ;
94
- }
95
-
96
- var entries = ReadChildren ( reader , entry . Entry , parentPath ) ;
97
-
98
- TableOfContents = entries ;
92
+ // read this later
99
93
break ;
100
94
case "features" :
101
95
_features = reader . ReadDictionary ( entry . Entry ) . ToDictionary ( k => k . Key , v => bool . Parse ( v . Value ) , StringComparer . OrdinalIgnoreCase ) ;
@@ -108,6 +102,21 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
108
102
break ;
109
103
}
110
104
}
105
+
106
+ //we read it twice to ensure we read 'toc' last
107
+ reader = new YamlStreamReader ( sourceFile , _context ) ;
108
+ foreach ( var entry in reader . Read ( ) )
109
+ {
110
+ switch ( entry . Key )
111
+ {
112
+ case "toc" :
113
+ var toc = new TableOfContentsConfiguration ( this , _context , 0 , "" ) ;
114
+ var entries = toc . ReadChildren ( reader , entry . Entry ) ;
115
+ TableOfContents = entries ;
116
+ Files = toc . Files ; //side-effect ripe for refactor
117
+ break ;
118
+ }
119
+ }
111
120
}
112
121
catch ( Exception e )
113
122
{
@@ -135,195 +144,4 @@ private IReadOnlyCollection<IDocsBuilderExtension> InstantiateExtensions()
135
144
}
136
145
137
146
138
- private List < ITocItem > ReadChildren ( YamlStreamReader reader , KeyValuePair < YamlNode , YamlNode > entry , string parentPath )
139
- {
140
- var entries = new List < ITocItem > ( ) ;
141
- if ( entry . Value is not YamlSequenceNode sequence )
142
- {
143
- if ( entry . Key is YamlScalarNode scalarKey )
144
- {
145
- var key = scalarKey . Value ;
146
- reader . EmitWarning ( $ "'{ key } ' is not an array") ;
147
- }
148
- else
149
- reader . EmitWarning ( $ "'{ entry . Key } ' is not an array") ;
150
-
151
- return entries ;
152
- }
153
-
154
- entries . AddRange (
155
- sequence . Children . OfType < YamlMappingNode > ( )
156
- . SelectMany ( tocEntry => ReadChild ( reader , tocEntry , parentPath ) ?? [ ] )
157
- ) ;
158
-
159
- return entries ;
160
- }
161
-
162
- private IEnumerable < ITocItem > ? ReadChild ( YamlStreamReader reader , YamlMappingNode tocEntry , string parentPath )
163
- {
164
- string ? file = null ;
165
- string ? folder = null ;
166
- string ? detectionRules = null ;
167
- ConfigurationFile ? toc = null ;
168
- var fileFound = false ;
169
- var folderFound = false ;
170
- var detectionRulesFound = false ;
171
- var hiddenFile = false ;
172
- var inNav = false ;
173
- IReadOnlyCollection < ITocItem > ? children = null ;
174
- foreach ( var entry in tocEntry . Children )
175
- {
176
- var key = ( ( YamlScalarNode ) entry . Key ) . Value ;
177
- switch ( key )
178
- {
179
- case "toc" :
180
- toc = ReadNestedToc ( reader , entry , out fileFound ) ;
181
- break ;
182
- case "in_nav" :
183
- if ( ! bool . TryParse ( reader . ReadString ( entry ) , out inNav ) )
184
- throw new ArgumentException ( "in_nav must be a boolean" ) ;
185
- break ;
186
- case "hidden" :
187
- case "file" :
188
- hiddenFile = key == "hidden" ;
189
- file = ReadFile ( reader , entry , parentPath , out fileFound ) ;
190
- break ;
191
- case "folder" :
192
- folder = ReadFolder ( reader , entry , parentPath , out folderFound ) ;
193
- parentPath += $ "{ Path . DirectorySeparatorChar } { folder } ";
194
- break ;
195
- case "detection_rules" :
196
- if ( Extensions . IsDetectionRulesEnabled )
197
- {
198
- detectionRules = ReadDetectionRules ( reader , entry , parentPath , out detectionRulesFound ) ;
199
- parentPath += $ "{ Path . DirectorySeparatorChar } { folder } ";
200
- }
201
- break ;
202
- case "children" :
203
- children = ReadChildren ( reader , entry , parentPath ) ;
204
- break ;
205
- }
206
- }
207
-
208
- if ( toc is not null )
209
- {
210
- foreach ( var f in toc . Files )
211
- _ = Files . Add ( f ) ;
212
-
213
- return [ new FolderReference ( $ "{ parentPath } ". TrimStart ( Path . DirectorySeparatorChar ) , folderFound , inNav , toc . TableOfContents ) ] ;
214
- }
215
-
216
- if ( file is not null )
217
- {
218
- if ( detectionRules is not null )
219
- {
220
- if ( children is not null )
221
- reader . EmitError ( $ "'detection_rules' is not allowed to have 'children'", tocEntry ) ;
222
-
223
- if ( ! detectionRulesFound )
224
- {
225
- reader . EmitError ( $ "'detection_rules' folder { parentPath } is not found, skipping'", tocEntry ) ;
226
- children = [ ] ;
227
- }
228
- else
229
- {
230
- var extension = EnabledExtensions . OfType < DetectionRulesDocsBuilderExtension > ( ) . First ( ) ;
231
- children = extension . CreateTableOfContentItems ( parentPath , detectionRules , Files ) ;
232
- }
233
- }
234
- return [ new FileReference ( $ "{ parentPath } { Path . DirectorySeparatorChar } { file } ". TrimStart ( Path . DirectorySeparatorChar ) , fileFound , hiddenFile , children ?? [ ] ) ] ;
235
- }
236
-
237
- if ( folder is not null )
238
- {
239
- if ( children is null )
240
- _ = ImplicitFolders . Add ( parentPath . TrimStart ( Path . DirectorySeparatorChar ) ) ;
241
-
242
- return [ new FolderReference ( $ "{ parentPath } ". TrimStart ( Path . DirectorySeparatorChar ) , folderFound , inNav , children ?? [ ] ) ] ;
243
- }
244
-
245
- return null ;
246
- }
247
-
248
- private string ? ReadFolder ( YamlStreamReader reader , KeyValuePair < YamlNode , YamlNode > entry , string parentPath , out bool found )
249
- {
250
- found = false ;
251
- var folder = reader . ReadString ( entry ) ;
252
- if ( folder is not null )
253
- {
254
- var path = Path . Combine ( _rootPath . FullName , parentPath . TrimStart ( Path . DirectorySeparatorChar ) , folder ) ;
255
- if ( ! _context . ReadFileSystem . DirectoryInfo . New ( path ) . Exists )
256
- reader . EmitError ( $ "Directory '{ path } ' does not exist", entry . Key ) ;
257
- else
258
- found = true ;
259
- }
260
-
261
- return folder ;
262
- }
263
-
264
- private string ? ReadDetectionRules ( YamlStreamReader reader , KeyValuePair < YamlNode , YamlNode > entry , string parentPath , out bool found )
265
- {
266
- found = false ;
267
- var folder = reader . ReadString ( entry ) ;
268
- if ( folder is not null )
269
- {
270
- var path = Path . Combine ( _rootPath . FullName , parentPath . TrimStart ( Path . DirectorySeparatorChar ) , folder ) ;
271
- if ( ! _context . ReadFileSystem . DirectoryInfo . New ( path ) . Exists )
272
- reader . EmitError ( $ "Directory '{ path } ' does not exist", entry . Key ) ;
273
- else
274
- found = true ;
275
- }
276
-
277
- return folder ;
278
- }
279
-
280
- private string ? ReadFile ( YamlStreamReader reader , KeyValuePair < YamlNode , YamlNode > entry , string parentPath , out bool found )
281
- {
282
- found = false ;
283
- var file = reader . ReadString ( entry ) ;
284
- if ( file is null )
285
- return null ;
286
- if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
287
- file = file . Replace ( '/' , Path . DirectorySeparatorChar ) ;
288
-
289
- var path = Path . Combine ( _rootPath . FullName , parentPath . TrimStart ( Path . DirectorySeparatorChar ) , file ) ;
290
- if ( ! _context . ReadFileSystem . FileInfo . New ( path ) . Exists )
291
- reader . EmitError ( $ "File '{ path } ' does not exist", entry . Key ) ;
292
- else
293
- found = true ;
294
- _ = Files . Add ( ( parentPath + Path . DirectorySeparatorChar + file ) . TrimStart ( Path . DirectorySeparatorChar ) ) ;
295
-
296
- return file ;
297
- }
298
-
299
- private ConfigurationFile ? ReadNestedToc ( YamlStreamReader reader , KeyValuePair < YamlNode , YamlNode > entry , out bool found )
300
- {
301
- found = false ;
302
- var tocPath = reader . ReadString ( entry ) ;
303
- if ( tocPath is null )
304
- {
305
- reader . EmitError ( $ "Empty toc: reference", entry . Key ) ;
306
- return null ;
307
- }
308
-
309
- var rootPath = _context . ReadFileSystem . DirectoryInfo . New ( Path . Combine ( _rootPath . FullName , tocPath ) ) ;
310
- var path = Path . Combine ( rootPath . FullName , "toc.yml" ) ;
311
- var source = _context . ReadFileSystem . FileInfo . New ( path ) ;
312
-
313
- var errorMessage = $ "Nested toc: '{ source . Directory } ' directory has no toc.yml or _toc.yml file";
314
-
315
- if ( ! source . Exists )
316
- {
317
- path = Path . Combine ( rootPath . FullName , "_toc.yml" ) ;
318
- source = _context . ReadFileSystem . FileInfo . New ( path ) ;
319
- }
320
-
321
- if ( ! source . Exists )
322
- reader . EmitError ( errorMessage , entry . Key ) ;
323
- else
324
- found = true ;
325
-
326
- var nestedConfiguration = new ConfigurationFile ( source , _rootPath , _context , _depth + 1 , tocPath ) ;
327
- return nestedConfiguration ;
328
- }
329
147
}
0 commit comments