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
88 changes: 73 additions & 15 deletions src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class CrossLinkResolver(ConfigurationFile configuration, ILoggerFactory l
private readonly string[] _links = configuration.CrossLinkRepositories;
private FrozenDictionary<string, LinkReference> _linkReferences = new Dictionary<string, LinkReference>().ToFrozenDictionary();
private readonly ILogger _logger = logger.CreateLogger(nameof(CrossLinkResolver));
private readonly HashSet<string> _declaredRepositories = new();

public static LinkReference Deserialize(string json) =>
JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LinkReference)!;
Expand All @@ -32,28 +33,65 @@ public async Task FetchLinks()
var dictionary = new Dictionary<string, LinkReference>();
foreach (var link in _links)
{
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{link}/main/links.json";
_logger.LogInformation($"Fetching {url}");
var json = await client.GetStringAsync(url);
var linkReference = Deserialize(json);
dictionary.Add(link, linkReference);
_declaredRepositories.Add(link);
try
{
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{link}/main/links.json";
_logger.LogInformation($"Fetching {url}");
var json = await client.GetStringAsync(url);
var linkReference = Deserialize(json);
dictionary.Add(link, linkReference);
}
catch when (link == "docs-content")
{
throw;
}
catch when (link != "docs-content")
{
// TODO: ignored for now while we wait for all links.json files to populate
}
}
_linkReferences = dictionary.ToFrozenDictionary();
}

public bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
TryResolve(errorEmitter, _linkReferences, crossLinkUri, out resolvedUri);
TryResolve(errorEmitter, _declaredRepositories, _linkReferences, crossLinkUri, out resolvedUri);

private static Uri BaseUri { get; } = new Uri("https://docs-v3-preview.elastic.dev");

public static bool TryResolve(Action<string> errorEmitter, IDictionary<string, LinkReference> lookup, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
public static bool TryResolve(Action<string> errorEmitter, HashSet<string> declaredRepositories, IDictionary<string, LinkReference> lookup, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
{
resolvedUri = null;
if (!lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference))
if (crossLinkUri.Scheme == "docs-content")
{
if (!lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference))
{
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links");
return false;
}
return TryFullyValidate(errorEmitter, linkReference, crossLinkUri, out resolvedUri);
}

// TODO this is temporary while we wait for all links.json files to be published
if (!declaredRepositories.Contains(crossLinkUri.Scheme))
{
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links");
return false;
}

var lookupPath = crossLinkUri.AbsolutePath.TrimStart('/');
var path = ToTargetUrlPath(lookupPath);
if (!string.IsNullOrEmpty(crossLinkUri.Fragment))
path += crossLinkUri.Fragment;

var branch = GetBranch(crossLinkUri);
resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}");
return true;
}

private static bool TryFullyValidate(Action<string> errorEmitter, LinkReference linkReference, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
{
resolvedUri = null;
var lookupPath = crossLinkUri.AbsolutePath.TrimStart('/');
if (string.IsNullOrEmpty(lookupPath) && crossLinkUri.Host.EndsWith(".md"))
lookupPath = crossLinkUri.Host;
Expand All @@ -64,12 +102,7 @@ public static bool TryResolve(Action<string> errorEmitter, IDictionary<string, L
return false;
}

//https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password
var path = lookupPath.Replace(".md", "");
if (path.EndsWith("/index"))
path = path.Substring(0, path.Length - 6);
if (path == "index")
path = string.Empty;
var path = ToTargetUrlPath(lookupPath);

if (!string.IsNullOrEmpty(crossLinkUri.Fragment))
{
Expand All @@ -87,7 +120,32 @@ public static bool TryResolve(Action<string> errorEmitter, IDictionary<string, L
path += crossLinkUri.Fragment;
}

resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/main/{path}");
var branch = GetBranch(crossLinkUri);
resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}");
return true;
}

/// Hardcoding these for now, we'll have an index.json pointing to all links.json files
/// at some point from which we can query the branch soon.
private static string GetBranch(Uri crossLinkUri)
{
var branch = crossLinkUri.Scheme switch
{
"docs-content" => "main",
_ => "main"
};
return branch;
}


private static string ToTargetUrlPath(string lookupPath)
{
//https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password
var path = lookupPath.Replace(".md", "");
if (path.EndsWith("/index"))
path = path.Substring(0, path.Length - 6);
if (path == "index")
path = string.Empty;
return path;
}
}
5 changes: 4 additions & 1 deletion tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using System.Diagnostics.CodeAnalysis;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.IO.State;
using Xunit.Internal;

namespace Elastic.Markdown.Tests;

public class TestCrossLinkResolver : ICrossLinkResolver
{
public Dictionary<string, LinkReference> LinkReferences { get; } = new();
public HashSet<string> DeclaredRepositories { get; } = new();

public Task FetchLinks()
{
Expand Down Expand Up @@ -40,9 +42,10 @@ public Task FetchLinks()
var reference = CrossLinkResolver.Deserialize(json);
LinkReferences.Add("docs-content", reference);
LinkReferences.Add("kibana", reference);
DeclaredRepositories.AddRange(["docs-content", "kibana", "elasticsearch"]);
return Task.CompletedTask;
}

public bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
CrossLinkResolver.TryResolve(errorEmitter, LinkReferences, crossLinkUri, out resolvedUri);
CrossLinkResolver.TryResolve(errorEmitter, DeclaredRepositories, LinkReferences, crossLinkUri, out resolvedUri);
}
2 changes: 2 additions & 0 deletions tests/authoring/Framework/Setup.fs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type Setup =
let yaml = new StringWriter();
yaml.WriteLine("cross_links:");
yaml.WriteLine(" - docs-content");
yaml.WriteLine(" - elasticsearch");
yaml.WriteLine(" - kibana");
yaml.WriteLine("toc:");
let markdownFiles = fileSystem.Directory.EnumerateFiles(root.FullName, "*.md", SearchOption.AllDirectories)
markdownFiles
Expand Down
8 changes: 7 additions & 1 deletion tests/authoring/Framework/TestCrossLinkResolver.fs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ open Elastic.Markdown.IO.State
type TestCrossLinkResolver () =

let references = Dictionary<string, LinkReference>()
let declared = HashSet<string>()

member this.LinkReferences = references
member this.DeclaredRepositories = declared

interface ICrossLinkResolver with
member this.FetchLinks() =
Expand Down Expand Up @@ -44,9 +47,12 @@ type TestCrossLinkResolver () =
let reference = CrossLinkResolver.Deserialize json
this.LinkReferences.Add("docs-content", reference)
this.LinkReferences.Add("kibana", reference)
this.DeclaredRepositories.Add("docs-content") |> ignore;
this.DeclaredRepositories.Add("kibana") |> ignore;
this.DeclaredRepositories.Add("elasticsearch") |> ignore;
Task.CompletedTask

member this.TryResolve(errorEmitter, crossLinkUri, [<Out>]resolvedUri : byref<Uri|null>) =
CrossLinkResolver.TryResolve(errorEmitter, this.LinkReferences, crossLinkUri, &resolvedUri);
CrossLinkResolver.TryResolve(errorEmitter, this.DeclaredRepositories, this.LinkReferences, crossLinkUri, &resolvedUri);


22 changes: 22 additions & 0 deletions tests/authoring/Inline/CrossLinks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,25 @@ type ``link to valid anchor`` () =

[<Fact>]
let ``has no warning`` () = markdown |> hasNoWarnings

type ``link to repository that does not resolve yet`` () =

static let markdown = Setup.Markdown """
[Elasticsearch Documentation](elasticsearch:/index.md)
"""

[<Fact>]
let ``validate HTML`` () =
markdown |> convertsToHtml """
<p><a
href="https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/">
Elasticsearch Documentation
</a>
</p>
"""

[<Fact>]
let ``has no errors`` () = markdown |> hasNoErrors

[<Fact>]
let ``has no warning`` () = markdown |> hasNoWarnings