Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public DetectionRuleOverviewFile(IFileInfo sourceFile, IDirectoryInfo rootPath,

public RuleReference[] Rules { get; set; } = [];

private Dictionary<string, DetectionRuleFile> Files { get; } = [];

public void AddDetectionRuleFile(DetectionRuleFile df, RuleReference ruleReference) => Files[ruleReference.Path] = df;

protected override Task<MarkdownDocument> GetMinimalParseDocumentAsync(Cancel ctx)
{
Title = "Detection Rules Overview";
Expand Down Expand Up @@ -57,9 +61,10 @@ private string GetMarkdown()
""";
foreach (var r in group.OrderBy(r => r.Rule.Name))
{
var url = Files[r.Path].Url;
markdown +=
$"""
[{r.Rule.Name}]({r.Path}) <br>
[{r.Rule.Name}](!{url}) <br>
""";

}
Expand All @@ -69,6 +74,7 @@ private string GetMarkdown()

return markdown;
}

}

public record DetectionRuleFile : MarkdownFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,23 @@ public void CreateNavigationItem(

}

private DetectionRuleOverviewFile? _overviewFile;
public void Visit(DocumentationFile file, ITocItem tocItem)
{
// TODO the parsing of rules should not happen at ITocItem reading time.
// ensure the file has an instance of the rule the reference parsed.
if (file is DetectionRuleFile df && tocItem is RuleReference r)
{
df.Rule = r.Rule;
_overviewFile?.AddDetectionRuleFile(df, r);

}

if (file is DetectionRuleOverviewFile of && tocItem is RuleOverviewReference or)
{
var rules = or.Children.OfType<RuleReference>().ToArray();
of.Rules = rules;
_overviewFile = of;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public record RuleOverviewReference(
IReadOnlyCollection<ITocItem> Children,
IReadOnlyCollection<string> DetectionRuleFolders
)
: FileReference(TableOfContentsScope, Path, Found, true, Children);
: FileReference(TableOfContentsScope, Path, Found, false, Children);

public record RuleReference(
ITableOfContentsScope TableOfContentsScope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ public static class ContentSourceMoniker

public static string CreateString(string repo, string? path)
{
if (string.IsNullOrWhiteSpace(path))
path = path?.Replace("\\", "/").Trim('/');
if (string.IsNullOrWhiteSpace(path) || path == ".")
return $"{repo}://";
return $"{repo}://{path.Replace("\\", "/").Trim('/')}/";
return $"{repo}://{path}/";
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ public TableOfContentsTree(
Source = source;
TreeCollector.Collect(source, this);
DocumentationSet = documentationSet;

//edge case if tree only holds a single group ensure we collapse it down to the root (this)
if (NavigationItems.Count == 1 && NavigationItems.First() is GroupNavigationItem { Group.NavigationItems.Count: 0 })
NavigationItems = [];

}

internal TableOfContentsTree(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,11 +189,30 @@ private static void ProcessCrossLink(LinkInline link, InlineProcessor processor,

private static void ProcessInternalLink(LinkInline link, InlineProcessor processor, ParserContext context)
{
if (link.Url != null && link.Url.StartsWith('!'))
{
// [](!/already/resolved/url) internal syntax to allow markdown embedding already resolved links
var verbatimUrl = link.Url[1..];
link.Url = verbatimUrl;
var md = ResolveFile(context, verbatimUrl);
_ = SetLinkData(link, processor, context, md, verbatimUrl);
return;
}

var (url, anchor) = SplitUrlAndAnchor(link.Url ?? string.Empty);
var includeFrom = GetIncludeFromPath(url, context);
var file = ResolveFile(context, url);
ValidateInternalUrl(processor, url, includeFrom, link, context);

var linkMarkdown = SetLinkData(link, processor, context, file, url);

ProcessLinkText(processor, link, linkMarkdown, anchor, url, file);
UpdateLinkUrl(link, url, context, anchor);
}

private static MarkdownFile? SetLinkData(LinkInline link, InlineProcessor processor, ParserContext context,
IFileInfo file, string url)
{
if (context.DocumentationFileLookup(context.MarkdownSourcePath) is MarkdownFile currentMarkdown)
{
link.SetData(nameof(currentMarkdown.NavigationRoot), currentMarkdown.NavigationRoot);
Expand All @@ -210,15 +229,13 @@ private static void ProcessInternalLink(LinkInline link, InlineProcessor process
var linkMarkdown = context.DocumentationFileLookup(file) as MarkdownFile;
if (linkMarkdown is not null)
link.SetData($"Target{nameof(currentMarkdown.NavigationRoot)}", linkMarkdown.NavigationRoot);

ProcessLinkText(processor, link, linkMarkdown, anchor, url, file);
UpdateLinkUrl(link, url, context, anchor);
return linkMarkdown;
}

private static (string url, string? anchor) SplitUrlAndAnchor(string fullUrl)
{
var parts = fullUrl.Split('#');
return (parts[0], parts.Length > 1 ? parts[1].Trim() : null);
return (parts[0].TrimStart('!'), parts.Length > 1 ? parts[1].Trim() : null);
}

private static string GetIncludeFromPath(string url, ParserContext context) =>
Expand Down
14 changes: 14 additions & 0 deletions src/Elastic.Markdown/Slices/Layout/_TocTreeNav.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@
</a>
</li>
}
else if (item is GroupNavigationItem { Group: { NavigationItems.Count: 0, Index: not null } } group)
{
var f = group.Group.Index;
<li class="flex group/li pr-4 @(isTopLevel ? "font-semibold py-8 pr-4 not-last:border-b-1 border-grey-20" : "ml-5 lg:ml-4 mt-4 lg:mt-3")">
<a
href="@f.Url"
@Htmx.GetHxAttributes(f.Url, Model.IsPrimaryNavEnabled && f.NavigationRoot.Id == Model.RootNavigationId || true)
class="sidebar-link group-[.current]/li:text-blue-elastic!"
id="page-@id"
>
@f.NavigationTitle
</a>
</li>
}
else if (item is GroupNavigationItem folder)
{
var g = folder.Group;
Expand Down
3 changes: 2 additions & 1 deletion src/docs-assembler/AssembleSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ static void ReadBlock(
if (source is null)
return;

if (!Uri.TryCreate(source.TrimEnd('/') + '/', UriKind.Absolute, out var sourceUri))
source = source.EndsWith("://") ? source : source.TrimEnd('/') + "/";
if (!Uri.TryCreate(source, UriKind.Absolute, out var sourceUri))
{
reader.EmitError($"Source toc entry is not a valid uri: {source}", tocEntry);
return;
Expand Down
11 changes: 10 additions & 1 deletion src/docs-assembler/Building/PublishEnvironmentUriResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public PublishEnvironmentUriResolver(FrozenDictionary<Uri, TocTopLevelMapping> t

TableOfContentsPrefixes = [..topLevelMappings
.Values
.Select(v => v.Source.ToString())
.Select(p =>
{
var source = p.Source.ToString();
return source.EndsWith(":///") ? source[..^1] : source;
})
.OrderByDescending(v => v.Length)
];

Expand All @@ -36,6 +40,11 @@ public PublishEnvironmentUriResolver(FrozenDictionary<Uri, TocTopLevelMapping> t

public Uri Resolve(Uri crossLinkUri, string path)
{
if (crossLinkUri.Scheme == "detection-rules")
{

}

var subPath = GetSubPathPrefix(crossLinkUri, ref path);

var fullPath = (PublishEnvironment.PathPrefix, subPath) switch
Expand Down
3 changes: 2 additions & 1 deletion src/docs-assembler/Navigation/GlobalNavigationFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ private IReadOnlyCollection<TocReference> ReadChildren(string key, YamlStreamRea
if (source is null)
return pathPrefix;

if (!Uri.TryCreate(source.TrimEnd('/') + "/", UriKind.Absolute, out sourceUri))
source = source.EndsWith("://") ? source : source.TrimEnd('/') + "/";
if (!Uri.TryCreate(source, UriKind.Absolute, out sourceUri))
{
reader.EmitError($"Source toc entry is not a valid uri: {source}", tocEntry);
return pathPrefix;
Expand Down
27 changes: 23 additions & 4 deletions src/docs-assembler/Navigation/GlobalNavigationPathProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.IO.Abstractions;
using Elastic.Markdown;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Extensions.DetectionRules;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.Configuration;

Expand All @@ -16,7 +17,7 @@ public record GlobalNavigationPathProvider : IDocumentationFileOutputProvider
private readonly AssembleSources _assembleSources;
private readonly AssembleContext _context;

private ImmutableSortedSet<string> TableOfContentsPrefixes { get; }
public ImmutableSortedSet<string> TableOfContentsPrefixes { get; }
private ImmutableSortedSet<string> PhantomPrefixes { get; }

public GlobalNavigationPathProvider(GlobalNavigationFile navigationFile, AssembleSources assembleSources, AssembleContext context)
Expand All @@ -26,26 +27,44 @@ public GlobalNavigationPathProvider(GlobalNavigationFile navigationFile, Assembl

TableOfContentsPrefixes = [..assembleSources.TocTopLevelMappings
.Values
.Select(v => v.Source.ToString())
.Select(p =>
{
var source = p.Source.ToString();
return source.EndsWith(":///") ? source[..^1] : source;
})
.OrderByDescending(v => v.Length)
];

PhantomPrefixes = [..navigationFile.Phantoms
.Select(p => p.Source.ToString())
.Select(p =>
{
var source = p.Source.ToString();
return source.EndsWith(":///") ? source[..^1] : source;
})
.OrderByDescending(v => v.Length)
.ToArray()
];
}

public IFileInfo? OutputFile(DocumentationSet documentationSet, IFileInfo defaultOutputFile, string relativePath)
{

if (relativePath.StartsWith("_static/", StringComparison.Ordinal))
return defaultOutputFile;



var repositoryName = documentationSet.Build.Git.RepositoryName;
var outputDirectory = documentationSet.OutputDirectory;
var fs = defaultOutputFile.FileSystem;

var repositoryName = documentationSet.Build.Git.RepositoryName;
if (repositoryName == "detection-rules")
{
var output = DetectionRuleFile.OutputPath(defaultOutputFile, documentationSet.Build);
var md = fs.FileInfo.New(Path.ChangeExtension(output.FullName, "md"));
relativePath = Path.GetRelativePath(documentationSet.OutputDirectory.FullName, md.FullName);
}


var l = ContentSourceMoniker.CreateString(repositoryName, relativePath).TrimEnd('/');
var lookup = l.AsSpan();
Expand Down
5 changes: 2 additions & 3 deletions src/docs-assembler/assembler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ references:
current: master
curator:
current: master
detection-rules:
checkout_strategy: full
ecctl:
current: master
ecs-dotnet:
Expand Down Expand Up @@ -73,6 +75,3 @@ references:
logstash:
search-ui:
integration-docs:
# security-docs:
# # wait for move to https://github.com/elastic/detection-rules/pull/4507/files
# skip: true
9 changes: 3 additions & 6 deletions src/docs-assembler/navigation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,9 @@ toc:
path_prefix: reference/security
# Children include: Endpoint command reference, Elastic Defend,
# Fields and object schemas
#children:
# 📝 TO DO: Update when rules are moved to elastic/detection-rules
# This makes sense for now. I'm not sure when these files will be
# moved to https://github.com/elastic/detection-rules.
# - toc: security-docs://reference/prebuilt-rules
# path_prefix: reference/security/prebuilt-rules
children:
- toc: detection-rules://
path_prefix: reference/security/prebuilt-rules

# Observability
# ✅ https://github.com/elastic/docs-content/blob/main/reference/observability/toc.yml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,21 @@ public async Task ReadAllPathPrefixes()
pathPrefixes.Should().Contain(new Uri("eland://reference/elasticsearch/clients/eland/"));
}

[Fact]
public async Task PathProvider()
{
Assert.SkipUnless(HasCheckouts(), $"Requires local checkout folder: {CheckoutDirectory.FullName}");

var assembleSources = await Setup();

var navigationFile = new GlobalNavigationFile(Context, assembleSources);
var pathProvider = new GlobalNavigationPathProvider(navigationFile, assembleSources, Context);

assembleSources.TocTopLevelMappings.Should().NotBeEmpty().And.ContainKey(new Uri("detection-rules://"));
pathProvider.TableOfContentsPrefixes.Should().Contain("detection-rules://");
}


[Fact]
public async Task ParsesReferences()
{
Expand All @@ -89,6 +104,8 @@ public async Task ParsesReferences()
assembleSources.TocTopLevelMappings.Should().NotBeEmpty().And.ContainKey(expectedRoot);
assembleSources.TocTopLevelMappings[sut].ParentSource.Should().Be(expectedParent);

assembleSources.TocTopLevelMappings.Should().NotBeEmpty().And.ContainKey(new Uri("detection-rules://"));

var navigationFile = new GlobalNavigationFile(Context, assembleSources);
var referenceToc = navigationFile.TableOfContents.FirstOrDefault(t => t.Source == expectedRoot);
referenceToc.Should().NotBeNull();
Expand All @@ -110,9 +127,6 @@ public async Task ParsesReferences()
var referenceNav = navigation.NavigationLookup[expectedRoot];
navigation.NavigationItems.Should().HaveSameCount(navigation.NavigationLookup);

var referenceOrder = referenceNav.Group.NavigationItems.OfType<TocNavigationItem>()
.Last().Source.Should().Be(new Uri("docs-content://reference/glossary/"));

referenceNav.Should().NotBeNull();
referenceNav.NavigationLookup.Should().NotContainKey(clients);
referenceNav.Group.NavigationItems.OfType<TocNavigationItem>()
Expand Down
Loading