diff --git a/src/Elastic.Markdown/DocumentationGenerator.cs b/src/Elastic.Markdown/DocumentationGenerator.cs index 1adac0602..13c124395 100644 --- a/src/Elastic.Markdown/DocumentationGenerator.cs +++ b/src/Elastic.Markdown/DocumentationGenerator.cs @@ -28,6 +28,7 @@ public interface IDocumentationFileOutputProvider public class DocumentationGenerator { private readonly IDocumentationFileOutputProvider? _documentationFileOutputProvider; + private readonly IConversionCollector? _conversionCollector; private readonly ILogger _logger; private readonly IFileSystem _writeFileSystem; private readonly IDocumentationFileExporter _documentationFileExporter; @@ -46,6 +47,7 @@ public DocumentationGenerator( ) { _documentationFileOutputProvider = documentationFileOutputProvider; + _conversionCollector = conversionCollector; _writeFileSystem = docSet.Build.WriteFileSystem; _logger = logger.CreateLogger(nameof(DocumentationGenerator)); @@ -55,7 +57,8 @@ public DocumentationGenerator( HtmlWriter = new HtmlWriter(DocumentationSet, _writeFileSystem, new DescriptionGenerator()); _documentationFileExporter = documentationExporter - ?? new DocumentationFileExporter(docSet.Build.ReadFileSystem, _writeFileSystem, HtmlWriter, conversionCollector); + ?? docSet.Build.Configuration.EnabledExtensions.FirstOrDefault(e => e.FileExporter != null)?.FileExporter + ?? new DocumentationFileExporter(docSet.Build.ReadFileSystem, _writeFileSystem); _logger.LogInformation("Created documentation set for: {DocumentationSetName}", DocumentationSet.Name); _logger.LogInformation("Source directory: {SourcePath} Exists: {SourcePathExists}", docSet.SourceDirectory, docSet.SourceDirectory.Exists); @@ -205,7 +208,7 @@ private async Task ProcessFile(HashSet offendingFiles, DocumentationFile //TODO send file to OutputFile() so we can validate its scope is defined in navigation.yml var outputFile = OutputFile(file.RelativePath); if (outputFile is not null) - await _documentationFileExporter.ProcessFile(file, outputFile, token); + await _documentationFileExporter.ProcessFile(Context, file, outputFile, HtmlWriter, _conversionCollector, token); } private IFileInfo? OutputFile(string relativePath) diff --git a/src/Elastic.Markdown/Exporters/DocumentationFileExporter.cs b/src/Elastic.Markdown/Exporters/DocumentationFileExporter.cs index 482d7785e..beb73fd53 100644 --- a/src/Elastic.Markdown/Exporters/DocumentationFileExporter.cs +++ b/src/Elastic.Markdown/Exporters/DocumentationFileExporter.cs @@ -13,7 +13,8 @@ public interface IDocumentationFileExporter /// Used in documentation state to ensure we break the build cache if a different exporter is chosen string Name { get; } - Task ProcessFile(DocumentationFile file, IFileInfo outputFile, Cancel token); + Task ProcessFile(BuildContext context, DocumentationFile file, IFileInfo outputFile, HtmlWriter htmlWriter, IConversionCollector? conversionCollector, + Cancel token); Task CopyEmbeddedResource(IFileInfo outputFile, Stream resourceStream, Cancel ctx); } @@ -21,7 +22,10 @@ public interface IDocumentationFileExporter public abstract class DocumentationFileExporterBase(IFileSystem readFileSystem, IFileSystem writeFileSystem) : IDocumentationFileExporter { public abstract string Name { get; } - public abstract Task ProcessFile(DocumentationFile file, IFileInfo outputFile, Cancel token); + + public abstract Task ProcessFile(BuildContext context, DocumentationFile file, IFileInfo outputFile, HtmlWriter htmlWriter, + IConversionCollector? conversionCollector, + Cancel token); protected async Task CopyFileFsAware(DocumentationFile file, IFileInfo outputFile, Cancel ctx) { @@ -47,14 +51,16 @@ public async Task CopyEmbeddedResource(IFileInfo outputFile, Stream resourceStre public class DocumentationFileExporter( IFileSystem readFileSystem, - IFileSystem writeFileSystem, - HtmlWriter htmlWriter, - IConversionCollector? conversionCollector + IFileSystem writeFileSystem ) : DocumentationFileExporterBase(readFileSystem, writeFileSystem) { public override string Name { get; } = nameof(DocumentationFileExporter); - public override async Task ProcessFile(DocumentationFile file, IFileInfo outputFile, Cancel token) + public override async Task ProcessFile(BuildContext context, DocumentationFile file, + IFileInfo outputFile, + HtmlWriter htmlWriter, + IConversionCollector? conversionCollector, + Cancel token) { if (file is MarkdownFile markdown) await htmlWriter.WriteAsync(outputFile, markdown, conversionCollector, token); diff --git a/src/Elastic.Markdown/Exporters/NoopDocumentationFileExporter.cs b/src/Elastic.Markdown/Exporters/NoopDocumentationFileExporter.cs index 995043152..0185a0b44 100644 --- a/src/Elastic.Markdown/Exporters/NoopDocumentationFileExporter.cs +++ b/src/Elastic.Markdown/Exporters/NoopDocumentationFileExporter.cs @@ -4,12 +4,17 @@ using System.IO.Abstractions; using Elastic.Markdown.IO; +using Elastic.Markdown.Slices; namespace Elastic.Markdown.Exporters; public class NoopDocumentationFileExporter : IDocumentationFileExporter { public string Name { get; } = nameof(NoopDocumentationFileExporter); - public Task ProcessFile(DocumentationFile file, IFileInfo outputFile, Cancel token) => Task.CompletedTask; + + public Task ProcessFile(BuildContext context, DocumentationFile file, IFileInfo outputFile, HtmlWriter htmlWriter, + IConversionCollector? conversionCollector, Cancel token) => + Task.CompletedTask; + public Task CopyEmbeddedResource(IFileInfo outputFile, Stream resourceStream, Cancel ctx) => Task.CompletedTask; } diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs index a3528654f..30343db36 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRule.cs @@ -61,7 +61,7 @@ public record DetectionRule public required string Type { get; init; } public required string? Language { get; init; } public required string[]? Indices { get; init; } - public required string RunsEvery { get; init; } + public required string? RunsEvery { get; init; } public required string? IndicesFromDateMath { get; init; } public required string MaximumAlertsPerExecution { get; init; } public required string[]? References { get; init; } @@ -108,7 +108,7 @@ public static DetectionRule From(IFileInfo source) Query = TryGetString(rule, "query"), Note = TryGetString(rule, "note"), Name = rule.GetString("name"), - RunsEvery = "?", + RunsEvery = TryGetString(rule, "interval"), MaximumAlertsPerExecution = "?", Version = "?", Threats = threats diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs index 1e90e7f2c..4efe35e68 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRuleFile.cs @@ -13,6 +13,10 @@ public record DetectionRuleFile : MarkdownFile { public DetectionRule? Rule { get; set; } + public override string LinkReferenceRelativePath { get; } + + public IFileInfo RuleSourceMarkdownPath { get; } + public DetectionRuleFile( IFileInfo sourceFile, IDirectoryInfo rootPath, @@ -21,6 +25,25 @@ public DetectionRuleFile( DocumentationSet set ) : base(sourceFile, rootPath, parser, build, set) { + RuleSourceMarkdownPath = SourcePath(sourceFile, build); + LinkReferenceRelativePath = Path.GetRelativePath(build.DocumentationSourceDirectory.FullName, RuleSourceMarkdownPath.FullName); + } + + private static IFileInfo SourcePath(IFileInfo rulePath, BuildContext build) + { + var relative = Path.GetRelativePath(build.DocumentationCheckoutDirectory!.FullName, rulePath.FullName); + var newPath = Path.Combine(build.DocumentationSourceDirectory.FullName, relative); + var md = Path.ChangeExtension(newPath, ".md"); + return rulePath.FileSystem.FileInfo.New(md); + } + + public static IFileInfo OutputPath(IFileInfo rulePath, BuildContext build) + { + var relative = Path.GetRelativePath(build.DocumentationOutputDirectory.FullName, rulePath.FullName); + if (relative.StartsWith("../")) + relative = relative[3..]; + var newPath = Path.Combine(build.DocumentationOutputDirectory.FullName, relative); + return rulePath.FileSystem.FileInfo.New(newPath); } protected override string RelativePathUrl => RelativePath.AsSpan().TrimStart("../").ToString(); @@ -29,14 +52,14 @@ protected override Task GetMinimalParseDocumentAsync(Cancel ct { Title = Rule?.Name; var markdown = GetMarkdown(); - var document = MarkdownParser.MinimalParseStringAsync(markdown, SourceFile, null); + var document = MarkdownParser.MinimalParseStringAsync(markdown, RuleSourceMarkdownPath, null); return Task.FromResult(document); } protected override Task GetParseDocumentAsync(Cancel ctx) { var markdown = GetMarkdown(); - var document = MarkdownParser.ParseStringAsync(markdown, SourceFile, null); + var document = MarkdownParser.ParseStringAsync(markdown, RuleSourceMarkdownPath, null); return Task.FromResult(document); } diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs index 0dc58209d..92a52fd37 100644 --- a/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Markdown.Exporters; using Elastic.Markdown.Helpers; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Configuration; @@ -16,6 +17,8 @@ public class DetectionRulesDocsBuilderExtension(BuildContext build) : IDocsBuild public bool InjectsIntoNavigation(ITocItem tocItem) => false; + public IDocumentationFileExporter? FileExporter { get; } = new RuleDocumentationFileExporter(build.ReadFileSystem, build.WriteFileSystem); + public void CreateNavigationItem( DocumentationGroup? parent, ITocItem tocItem, diff --git a/src/Elastic.Markdown/Extensions/DetectionRules/RuleDocumentationFileExporter.cs b/src/Elastic.Markdown/Extensions/DetectionRules/RuleDocumentationFileExporter.cs new file mode 100644 index 000000000..f557297e9 --- /dev/null +++ b/src/Elastic.Markdown/Extensions/DetectionRules/RuleDocumentationFileExporter.cs @@ -0,0 +1,31 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.IO.Abstractions; +using Elastic.Markdown.Exporters; +using Elastic.Markdown.IO; +using Elastic.Markdown.Slices; + +namespace Elastic.Markdown.Extensions.DetectionRules; + +public class RuleDocumentationFileExporter(IFileSystem readFileSystem, IFileSystem writeFileSystem) + : DocumentationFileExporterBase(readFileSystem, writeFileSystem) +{ + public override string Name { get; } = nameof(RuleDocumentationFileExporter); + + public override async Task ProcessFile(BuildContext context, DocumentationFile file, IFileInfo outputFile, HtmlWriter htmlWriter, + IConversionCollector? conversionCollector, Cancel token) + { + if (file is DetectionRuleFile df) + await htmlWriter.WriteAsync(DetectionRuleFile.OutputPath(outputFile, context), df, conversionCollector, token); + else if (file is MarkdownFile markdown) + await htmlWriter.WriteAsync(outputFile, markdown, conversionCollector, token); + else + { + if (outputFile.Directory is { Exists: false }) + outputFile.Directory.Create(); + await CopyFileFsAware(file, outputFile, token); + } + } +} diff --git a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs index fef9e9129..e1b08889a 100644 --- a/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs +++ b/src/Elastic.Markdown/Extensions/IDocsBuilderExtension.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using Elastic.Markdown.Exporters; using Elastic.Markdown.IO; using Elastic.Markdown.IO.Configuration; using Elastic.Markdown.IO.Navigation; @@ -11,6 +12,8 @@ namespace Elastic.Markdown.Extensions; public interface IDocsBuilderExtension { + IDocumentationFileExporter? FileExporter { get; } + /// Inject items into the current navigation void CreateNavigationItem( DocumentationGroup? parent, diff --git a/src/Elastic.Markdown/IO/DocumentationFile.cs b/src/Elastic.Markdown/IO/DocumentationFile.cs index b4b9dd6b3..ce166b10f 100644 --- a/src/Elastic.Markdown/IO/DocumentationFile.cs +++ b/src/Elastic.Markdown/IO/DocumentationFile.cs @@ -13,6 +13,9 @@ public abstract record DocumentationFile(IFileInfo SourceFile, IDirectoryInfo Ro public string RelativePath { get; } = Path.GetRelativePath(RootPath.FullName, SourceFile.FullName); public string RelativeFolder { get; } = Path.GetRelativePath(RootPath.FullName, SourceFile.Directory!.FullName); + /// Allows documentation files of non markdown origins to advertise as their markdown equivalent in links.json + public virtual string LinkReferenceRelativePath => RelativePath; + } public record ImageFile(IFileInfo SourceFile, IDirectoryInfo RootPath, string MimeType = "image/png") diff --git a/src/Elastic.Markdown/IO/State/LinkReference.cs b/src/Elastic.Markdown/IO/State/LinkReference.cs index 96c52e4c8..3bd6f8f85 100644 --- a/src/Elastic.Markdown/IO/State/LinkReference.cs +++ b/src/Elastic.Markdown/IO/State/LinkReference.cs @@ -70,10 +70,10 @@ public static LinkReference Create(DocumentationSet set) var redirects = set.Configuration.Redirects; var crossLinks = set.Build.Collector.CrossLinks.ToHashSet().ToArray(); var links = set.MarkdownFiles.Values - .Select(m => (m.RelativePath, File: m)) + .Select(m => (m.LinkReferenceRelativePath, File: m)) .ToDictionary(k => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? k.RelativePath.Replace('\\', '/') - : k.RelativePath, v => + ? k.LinkReferenceRelativePath.Replace('\\', '/') + : k.LinkReferenceRelativePath, v => { var anchors = v.File.Anchors.Count == 0 ? null : v.File.Anchors.ToArray(); return new LinkMetadata { Anchors = anchors, Hidden = v.File.Hidden };