Skip to content

Commit 4f9979c

Browse files
committed
ensure anchor lookups work and urls uses extensionless paths
1 parent 1504f6e commit 4f9979c

File tree

8 files changed

+94
-30
lines changed

8 files changed

+94
-30
lines changed

src/Elastic.Markdown/CrossLinks/ExternalLinksResolver.cs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Elastic.Markdown.CrossLinks;
1414
public interface ICrossLinkResolver
1515
{
1616
Task FetchLinks();
17-
bool TryResolve(Uri crosslinkUri, [NotNullWhen(true)]out Uri? resolvedUri);
17+
bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)]out Uri? resolvedUri);
1818
}
1919

2020
public class CrossLinkResolver(ConfigurationFile configuration, ILoggerFactory logger) : ICrossLinkResolver
@@ -41,29 +41,53 @@ public async Task FetchLinks()
4141
_linkReferences = dictionary.ToFrozenDictionary();
4242
}
4343

44-
public bool TryResolve(Uri crosslinkUri, [NotNullWhen(true)]out Uri? resolvedUri) =>
45-
TryResolve(_linkReferences, crosslinkUri, out resolvedUri);
44+
public bool TryResolve(Action<string> errorEmitter, Uri crossLinkUri, [NotNullWhen(true)]out Uri? resolvedUri) =>
45+
TryResolve(errorEmitter, _linkReferences, crossLinkUri, out resolvedUri);
4646

47-
public static bool TryResolve(IDictionary<string, LinkReference> lookup, Uri crosslinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
47+
private static Uri BaseUri { get; } = new Uri("https://docs-v3-preview.elastic.dev");
48+
49+
public static bool TryResolve(Action<string> errorEmitter, IDictionary<string, LinkReference> lookup, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri)
4850
{
4951
resolvedUri = null;
50-
if (!lookup.TryGetValue(crosslinkUri.Scheme, out var linkReference))
52+
if (!lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference))
5153
{
52-
//TODO emit error
54+
errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links");
5355
return false;
5456
}
55-
var lookupPath = crosslinkUri.AbsolutePath.TrimStart('/');
57+
var lookupPath = crossLinkUri.AbsolutePath.TrimStart('/');
58+
if (string.IsNullOrEmpty(lookupPath) && crossLinkUri.Host.EndsWith(".md"))
59+
lookupPath = crossLinkUri.Host;
5660

5761
if (!linkReference.Links.TryGetValue(lookupPath, out var link))
5862
{
59-
//TODO emit error
63+
errorEmitter($"'{lookupPath}' is not a valid link in the '{crossLinkUri.Scheme}' cross link repository.");
6064
return false;
6165
}
6266

6367
//https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password
6468
var path = lookupPath.Replace(".md", "");
65-
var baseUri = new Uri("https://docs-v3-preview.elastic.dev");
66-
resolvedUri = new Uri(baseUri, $"elastic/{crosslinkUri.Scheme}/tree/main/{path}");
69+
if (path.EndsWith("/index"))
70+
path = path.Substring(0, path.Length - 6);
71+
if (path == "index")
72+
path = string.Empty;
73+
74+
if (!string.IsNullOrEmpty(crossLinkUri.Fragment))
75+
{
76+
if (link.Anchors is null)
77+
{
78+
errorEmitter($"'{lookupPath}' does not have any anchors so linking to '{crossLinkUri.Fragment}' is impossible.");
79+
return false;
80+
}
81+
82+
if (!link.Anchors.Contains(crossLinkUri.Fragment.TrimStart('#')))
83+
{
84+
errorEmitter($"'{lookupPath}' has no anchor named: '{crossLinkUri.Fragment}'.");
85+
return false;
86+
}
87+
path += crossLinkUri.Fragment;
88+
}
89+
90+
resolvedUri = new Uri(BaseUri, $"elastic/{crossLinkUri.Scheme}/tree/main/{path}");
6791
return true;
6892
}
6993
}

src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ internal partial class LinkRegexExtensions
4747
public class DiagnosticLinkInlineParser : LinkInlineParser
4848
{
4949
// See https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for a list of URI schemes
50-
private static readonly ImmutableHashSet<string> ExcludedSchemes = ["http", "https", "tel", "jdbc"];
50+
private static readonly ImmutableHashSet<string> ExcludedSchemes = ["http", "https", "tel", "jdbc", "mailto"];
5151

5252
public override bool Match(InlineProcessor processor, ref StringSlice slice)
5353
{
@@ -161,18 +161,7 @@ private static void ProcessCrossLink(LinkInline link, InlineProcessor processor,
161161
if (url != null)
162162
context.Build.Collector.EmitCrossLink(url);
163163

164-
var scheme = uri.Scheme;
165-
if (!context.Configuration.CrossLinkRepositories.Contains(scheme))
166-
{
167-
processor.EmitWarning(
168-
link,
169-
$"Cross link '{uri}' is not allowed. Add '{scheme}' to the " +
170-
$"'cross_links' list in the configuration file '{context.Configuration.SourceFile}' " +
171-
"to allow cross links to this external documentation set."
172-
);
173-
return;
174-
}
175-
if (context.LinksResolver.TryResolve(uri, out var resolvedUri))
164+
if (context.LinksResolver.TryResolve(s => processor.EmitError(link, s), uri, out var resolvedUri))
176165
link.Url = resolvedUri.ToString();
177166
}
178167

@@ -276,5 +265,5 @@ private static bool IsCrossLink([NotNullWhen(true)]Uri? uri) =>
276265
uri != null // This means it's not a local
277266
&& !ExcludedSchemes.Contains(uri.Scheme)
278267
&& !uri.IsFile
279-
&& Path.GetExtension(uri.OriginalString) == ".md";
268+
&& !string.IsNullOrEmpty(uri.Scheme);
280269
}

tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public virtual async ValueTask InitializeAsync()
8686
{
8787
_ = Collector.StartAsync(default);
8888

89+
await Set.LinkResolver.FetchLinks();
8990
Document = await File.ParseFullAsync(default);
9091
var html = File.CreateHtml(Document).AsSpan();
9192
var find = "</section>";

tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,7 @@ Go to [test](kibana://index.md)
157157
public void GeneratesHtml() =>
158158
// language=html
159159
Html.Should().Contain(
160-
// TODO: The link is not rendered correctly yet, will be fixed in a follow-up
161-
"""<p>Go to <a href="kibana://index.md">test</a></p>"""
160+
"""<p>Go to <a href="https://docs-v3-preview.elastic.dev/elastic/kibana/tree/main/">test</a></p>"""
162161
);
163162

164163
[Fact]

tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public virtual async ValueTask InitializeAsync()
128128
_ = Collector.StartAsync(default);
129129

130130
await Set.ResolveDirectoryTree(default);
131+
await Set.LinkResolver.FetchLinks();
131132

132133
Document = await File.ParseFullAsync(default);
133134
var html = File.CreateHtml(Document).AsSpan();

tests/Elastic.Markdown.Tests/TestCrossLinkResolver.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@ public Task FetchLinks()
4343
return Task.CompletedTask;
4444
}
4545

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

tests/authoring/Framework/TestCrossLinkResolver.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ type TestCrossLinkResolver () =
4646
this.LinkReferences.Add("kibana", reference)
4747
Task.CompletedTask
4848

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

5252

tests/authoring/Inline/CrossLinks.fs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,53 @@ type ``cross-link makes it into html`` () =
2828

2929
[<Fact>]
3030
let ``has no warning`` () = markdown |> hasNoWarnings
31+
32+
type ``error when using wrong scheme`` () =
33+
34+
static let markdown = Setup.Markdown """
35+
[APM Server binary](docs-x:/solutions/observability/apps/apm-server-binary.md)
36+
"""
37+
38+
[<Fact>]
39+
let ``error on bad scheme`` () =
40+
markdown
41+
|> hasError "'docs-x' is not declared as valid cross link repository in docset.yml under cross_links"
42+
43+
[<Fact>]
44+
let ``has no warning`` () = markdown |> hasNoWarnings
45+
46+
type ``error when bad anchor is used`` () =
47+
48+
static let markdown = Setup.Markdown """
49+
[APM Server binary](docs-content:/solutions/observability/apps/apm-server-binary.md#apm-deb-x)
50+
"""
51+
52+
[<Fact>]
53+
let ``error when linking to unknown anchor`` () =
54+
markdown
55+
|> hasError "'solutions/observability/apps/apm-server-binary.md' has no anchor named: '#apm-deb-x"
56+
57+
[<Fact>]
58+
let ``has no warning`` () = markdown |> hasNoWarnings
59+
60+
type ``link to valid anchor`` () =
61+
62+
static let markdown = Setup.Markdown """
63+
[APM Server binary](docs-content:/solutions/observability/apps/apm-server-binary.md#apm-deb)
64+
"""
65+
66+
[<Fact>]
67+
let ``validate HTML`` () =
68+
markdown |> convertsToHtml """
69+
<p><a
70+
href="https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/solutions/observability/apps/apm-server-binary#apm-deb">
71+
APM Server binary
72+
</a>
73+
</p>
74+
"""
75+
76+
[<Fact>]
77+
let ``has no errors`` () = markdown |> hasNoErrors
78+
79+
[<Fact>]
80+
let ``has no warning`` () = markdown |> hasNoWarnings

0 commit comments

Comments
 (0)