22// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33// See the LICENSE file in the project root for more information
44
5- using System . IO . Abstractions ;
6- using System . Runtime . InteropServices ;
75using DotNet . Globbing ;
86using Elastic . Markdown . Diagnostics ;
97using Elastic . Markdown . Extensions ;
108using Elastic . Markdown . Extensions . DetectionRules ;
119using Elastic . Markdown . IO . State ;
12- using YamlDotNet . RepresentationModel ;
1310
1411namespace Elastic . Markdown . IO . Configuration ;
1512
1613public record ConfigurationFile : DocumentationFile
1714{
18- private readonly IDirectoryInfo _rootPath ;
1915 private readonly BuildContext _context ;
20- private readonly int _depth ;
16+
2117 public string ? Project { get ; }
18+
2219 public Glob [ ] Exclude { get ; } = [ ] ;
23- public bool SoftLineEndings { get ; }
2420
2521 public string [ ] CrossLinkRepositories { get ; } = [ ] ;
2622
23+ /// The maximum depth `toc.yml` files may appear
24+ public int MaxTocDepth { get ; } = 1 ;
25+
2726 public EnabledExtensions Extensions { get ; } = new ( [ ] ) ;
27+
2828 public IReadOnlyCollection < IDocsBuilderExtension > EnabledExtensions { get ; } = [ ] ;
2929
3030 public IReadOnlyCollection < ITocItem > TableOfContents { get ; } = [ ] ;
3131
32+ public HashSet < string > Files { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
33+
3234 public Dictionary < string , LinkRedirect > ? Redirects { get ; }
3335
34- public HashSet < string > Files { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
3536 public HashSet < string > ImplicitFolders { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
37+
3638 public Glob [ ] Globs { get ; } = [ ] ;
3739
3840 private readonly Dictionary < string , string > _substitutions = new ( StringComparer . OrdinalIgnoreCase ) ;
@@ -42,19 +44,18 @@ public record ConfigurationFile : DocumentationFile
4244 private FeatureFlags ? _featureFlags ;
4345 public FeatureFlags Features => _featureFlags ??= new FeatureFlags ( _features ) ;
4446
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 )
4749 {
48- _rootPath = rootPath ;
4950 _context = context ;
50- _depth = depth ;
51- if ( ! sourceFile . Exists )
51+ if ( ! context . ConfigurationPath . Exists )
5252 {
5353 Project = "unknown" ;
54- context . EmitWarning ( sourceFile , "No configuration file found" ) ;
54+ context . EmitWarning ( context . ConfigurationPath , "No configuration file found" ) ;
5555 return ;
5656 }
5757
58+ var sourceFile = context . ConfigurationPath ;
5859 var redirectFileName = sourceFile . Name . StartsWith ( '_' ) ? "_redirects.yml" : "redirects.yml" ;
5960 var redirectFileInfo = sourceFile . FileSystem . FileInfo . New ( Path . Combine ( sourceFile . Directory ! . FullName , redirectFileName ) ) ;
6061 var redirectFile = new RedirectFile ( redirectFileInfo , _context ) ;
@@ -70,11 +71,12 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
7071 case "project" :
7172 Project = reader . ReadString ( entry . Entry ) ;
7273 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 ;
7576 break ;
7677 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 ) ] ;
7880 break ;
7981 case "cross_links" :
8082 CrossLinkRepositories = [ .. YamlStreamReader . ReadStringArray ( entry . Entry ) ] ;
@@ -87,15 +89,7 @@ public ConfigurationFile(IFileInfo sourceFile, IDirectoryInfo rootPath, BuildCon
8789 _substitutions = reader . ReadDictionary ( entry . Entry ) ;
8890 break ;
8991 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
9993 break ;
10094 case "features" :
10195 _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
108102 break ;
109103 }
110104 }
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+ }
111120 }
112121 catch ( Exception e )
113122 {
@@ -135,195 +144,4 @@ private IReadOnlyCollection<IDocsBuilderExtension> InstantiateExtensions()
135144 }
136145
137146
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- }
329147}
0 commit comments