Skip to content

Commit 2a634aa

Browse files
committed
Only validate docs-content cross links for now
Introduced a `_declaredRepositories` field in `CrossLinkResolver` to track valid repositories explicitly. Updated logic to validate cross-links against declared repositories and handle errors or missing links more gracefully. Extended tests to ensure correctness and consistency with declared repository functionality. This allows us to temporarily allow cross links to repositories that do not publish a links.json file. With this PR we only validate: - docs-content links completely For all other repositories we only validate that they are declared in docset.yml
1 parent 1a2dd84 commit 2a634aa

File tree

5 files changed

+108
-17
lines changed

5 files changed

+108
-17
lines changed

src/Elastic.Markdown/CrossLinks/CrossLinkResolver.cs

Lines changed: 73 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class CrossLinkResolver(ConfigurationFile configuration, ILoggerFactory l
2222
private readonly string[] _links = configuration.CrossLinkRepositories;
2323
private FrozenDictionary<string, LinkReference> _linkReferences = new Dictionary<string, LinkReference>().ToFrozenDictionary();
2424
private readonly ILogger _logger = logger.CreateLogger(nameof(CrossLinkResolver));
25+
private readonly HashSet<string> _declaredRepositories = new();
2526

2627
public static LinkReference Deserialize(string json) =>
2728
JsonSerializer.Deserialize(json, SourceGenerationContext.Default.LinkReference)!;
@@ -32,28 +33,65 @@ public async Task FetchLinks()
3233
var dictionary = new Dictionary<string, LinkReference>();
3334
foreach (var link in _links)
3435
{
35-
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{link}/main/links.json";
36-
_logger.LogInformation($"Fetching {url}");
37-
var json = await client.GetStringAsync(url);
38-
var linkReference = Deserialize(json);
39-
dictionary.Add(link, linkReference);
36+
_declaredRepositories.Add(link);
37+
try
38+
{
39+
var url = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{link}/main/links.json";
40+
_logger.LogInformation($"Fetching {url}");
41+
var json = await client.GetStringAsync(url);
42+
var linkReference = Deserialize(json);
43+
dictionary.Add(link, linkReference);
44+
}
45+
catch when (link == "docs-content")
46+
{
47+
throw;
48+
}
49+
catch when (link != "docs-content")
50+
{
51+
// TODO: ignored for now while we wait for all links.json files to populate
52+
}
4053
}
4154
_linkReferences = dictionary.ToFrozenDictionary();
4255
}
4356

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

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

49-
public static bool TryResolve(Action<string> errorEmitter, IDictionary<string, LinkReference> lookup, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
62+
public static bool TryResolve(Action<string> errorEmitter, HashSet<string> declaredRepositories, IDictionary<string, LinkReference> lookup, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
5063
{
5164
resolvedUri = null;
52-
if (!lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference))
65+
if (crossLinkUri.Scheme == "docs-content")
66+
{
67+
if (!lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference))
68+
{
69+
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links");
70+
return false;
71+
}
72+
return TryFullyValidate(errorEmitter, linkReference, crossLinkUri, out resolvedUri);
73+
}
74+
75+
// TODO this is temporary while we wait for all links.json files to be published
76+
if (!declaredRepositories.Contains(crossLinkUri.Scheme))
5377
{
5478
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links");
5579
return false;
5680
}
81+
82+
var lookupPath = crossLinkUri.AbsolutePath.TrimStart('/');
83+
var path = ToTargetUrlPath(lookupPath);
84+
if (!string.IsNullOrEmpty(crossLinkUri.Fragment))
85+
path += crossLinkUri.Fragment;
86+
87+
var branch = GetBranch(crossLinkUri);
88+
resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}");
89+
return true;
90+
}
91+
92+
private static bool TryFullyValidate(Action<string> errorEmitter, LinkReference linkReference, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
93+
{
94+
resolvedUri = null;
5795
var lookupPath = crossLinkUri.AbsolutePath.TrimStart('/');
5896
if (string.IsNullOrEmpty(lookupPath) && crossLinkUri.Host.EndsWith(".md"))
5997
lookupPath = crossLinkUri.Host;
@@ -64,12 +102,7 @@ public static bool TryResolve(Action<string> errorEmitter, IDictionary<string, L
64102
return false;
65103
}
66104

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

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

90-
resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/main/{path}");
123+
var branch = GetBranch(crossLinkUri);
124+
resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/{branch}/{path}");
91125
return true;
92126
}
127+
128+
/// Hardcoding these for now, we'll have an index.json pointing to all links.json files
129+
/// at some point from which we can query the branch soon.
130+
private static string GetBranch(Uri crossLinkUri)
131+
{
132+
var branch = crossLinkUri.Scheme switch
133+
{
134+
"docs-content" => "main",
135+
_ => "main"
136+
};
137+
return branch;
138+
}
139+
140+
141+
private static string ToTargetUrlPath(string lookupPath)
142+
{
143+
//https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password
144+
var path = lookupPath.Replace(".md", "");
145+
if (path.EndsWith("/index"))
146+
path = path.Substring(0, path.Length - 6);
147+
if (path == "index")
148+
path = string.Empty;
149+
return path;
150+
}
93151
}

tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
using System.Diagnostics.CodeAnalysis;
66
using Elastic.Markdown.CrossLinks;
77
using Elastic.Markdown.IO.State;
8+
using Xunit.Internal;
89

910
namespace Elastic.Markdown.Tests;
1011

1112
public class TestCrossLinkResolver : ICrossLinkResolver
1213
{
1314
public Dictionary<string, LinkReference> LinkReferences { get; } = new();
15+
public HashSet<string> DeclaredRepositories { get; } = new();
1416

1517
public Task FetchLinks()
1618
{
@@ -40,9 +42,10 @@ public Task FetchLinks()
4042
var reference = CrossLinkResolver.Deserialize(json);
4143
LinkReferences.Add("docs-content", reference);
4244
LinkReferences.Add("kibana", reference);
45+
DeclaredRepositories.AddRange(["docs-content", "kibana", "elasticsearch"]);
4346
return Task.CompletedTask;
4447
}
4548

4649
public bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
47-
CrossLinkResolver.TryResolve(errorEmitter, LinkReferences, crossLinkUri, out resolvedUri);
50+
CrossLinkResolver.TryResolve(errorEmitter, DeclaredRepositories, LinkReferences, crossLinkUri, out resolvedUri);
4851
}

tests/authoring/Framework/Setup.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ type Setup =
4242
let yaml = new StringWriter();
4343
yaml.WriteLine("cross_links:");
4444
yaml.WriteLine(" - docs-content");
45+
yaml.WriteLine(" - elasticsearch");
46+
yaml.WriteLine(" - kibana");
4547
yaml.WriteLine("toc:");
4648
let markdownFiles = fileSystem.Directory.EnumerateFiles(root.FullName, "*.md", SearchOption.AllDirectories)
4749
markdownFiles

tests/authoring/Framework/TestCrossLinkResolver.fs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ open Elastic.Markdown.IO.State
1414
type TestCrossLinkResolver () =
1515

1616
let references = Dictionary<string, LinkReference>()
17+
let declared = HashSet<string>()
18+
1719
member this.LinkReferences = references
20+
member this.DeclaredRepositories = declared
1821

1922
interface ICrossLinkResolver with
2023
member this.FetchLinks() =
@@ -44,9 +47,12 @@ type TestCrossLinkResolver () =
4447
let reference = CrossLinkResolver.Deserialize json
4548
this.LinkReferences.Add("docs-content", reference)
4649
this.LinkReferences.Add("kibana", reference)
50+
this.DeclaredRepositories.Add("docs-content") |> ignore;
51+
this.DeclaredRepositories.Add("kibana") |> ignore;
52+
this.DeclaredRepositories.Add("elasticsearch") |> ignore;
4753
Task.CompletedTask
4854

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

5258

tests/authoring/Inline/CrossLinks.fs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,25 @@ type ``link to valid anchor`` () =
7878

7979
[<Fact>]
8080
let ``has no warning`` () = markdown |> hasNoWarnings
81+
82+
type ``link to repository that does not resolve yet`` () =
83+
84+
static let markdown = Setup.Markdown """
85+
[Elasticsearch Documentation](elasticsearch:/index.md)
86+
"""
87+
88+
[<Fact>]
89+
let ``validate HTML`` () =
90+
markdown |> convertsToHtml """
91+
<p><a
92+
href="https://docs-v3-preview.elastic.dev/elastic/elasticsearch/tree/main/">
93+
Elasticsearch Documentation
94+
</a>
95+
</p>
96+
"""
97+
98+
[<Fact>]
99+
let ``has no errors`` () = markdown |> hasNoErrors
100+
101+
[<Fact>]
102+
let ``has no warning`` () = markdown |> hasNoWarnings

0 commit comments

Comments
 (0)