Skip to content

Commit fbd89ea

Browse files
authored
Add cross-links to links.json (#168)
Closes #162 ## Context For our Links Validation Service, we need to generate a list of cross-links (links pointing to external documentation in our system) that will be uploaded to the Link Index. ## Changes Add external links to the links.json Resulting JSON file: ```json { "origin": { "branch": "feature/add-external-links", "remote": "elastic/docs-builder-unknown", "ref": "03c22a29e6585b72f64a0aa64f087dd5664c939f" }, "url_path_prefix": "", "internal_links": [ "index.md", "configure/index.md", "configure/page.md", "testing/external-links.md", "...", ], "cross_links": [ "kibana://index.md", "elasticsearch://index.md" ] } ``` ## Notes Changed the naming to Cross-Links, so it won't get confused with external hosts.
1 parent f0c347a commit fbd89ea

File tree

6 files changed

+130
-13
lines changed

6 files changed

+130
-13
lines changed

docs/source/docset.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ toc:
7777
- file: index.md
7878
- file: req.md
7979
- folder: nested
80+
- file: cross-links.md

docs/source/testing/cross-links.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
title: Cross Links
3+
---
4+
5+
[Elasticsearch](elasticsearch://index.md)
6+
7+
[Kibana][1]
8+
9+
[1]: kibana://index.md

src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Collections.Concurrent;
56
using System.Threading.Channels;
67
using Microsoft.Extensions.Hosting;
78
using Microsoft.Extensions.Logging;
@@ -72,6 +73,8 @@ public class DiagnosticsCollector(IReadOnlyCollection<IDiagnosticsOutput> output
7273

7374
public HashSet<string> OffendingFiles { get; } = new();
7475

76+
public ConcurrentBag<string> CrossLinks { get; } = new();
77+
7578
public Task StartAsync(Cancel ctx)
7679
{
7780
if (_started is not null)
@@ -126,6 +129,7 @@ public virtual async Task StopAsync(CancellationToken cancellationToken)
126129
await Channel.Reader.Completion;
127130
}
128131

132+
public void EmitCrossLink(string link) => CrossLinks.Add(link);
129133

130134
public void EmitError(string file, string message, Exception? e = null)
131135
{

src/Elastic.Markdown/IO/LinkReference.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,21 @@ public record LinkReference
1717
[JsonPropertyName("links")]
1818
public required string[] Links { get; init; } = [];
1919

20+
[JsonPropertyName("cross_links")]
21+
public required string[] CrossLinks { get; init; } = [];
22+
2023
public static LinkReference Create(DocumentationSet set)
2124
{
25+
var crossLinks = set.Context.Collector.CrossLinks.ToHashSet().ToArray();
2226
var links = set.FlatMappedFiles.Values
2327
.OfType<MarkdownFile>()
2428
.Select(m => m.RelativePath).ToArray();
2529
return new LinkReference
2630
{
2731
UrlPathPrefix = set.Context.UrlPathPrefix,
2832
Origin = set.Context.Git,
29-
Links = links
33+
Links = links,
34+
CrossLinks = crossLinks
3035
};
3136
}
3237
}

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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Collections.Immutable;
56
using Elastic.Markdown.Diagnostics;
67
using Elastic.Markdown.IO;
7-
using Elastic.Markdown.Myst.Directives;
88
using Markdig;
99
using Markdig.Helpers;
1010
using Markdig.Parsers;
1111
using Markdig.Parsers.Inlines;
1212
using Markdig.Renderers;
13-
using Markdig.Syntax;
1413
using Markdig.Syntax.Inlines;
1514

1615
namespace Elastic.Markdown.Myst.InlineParsers;
@@ -34,6 +33,10 @@ public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { }
3433

3534
public class DiagnosticLinkInlineParser : LinkInlineParser
3635
{
36+
// See https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for a list of URI schemes
37+
// We can add more schemes as needed
38+
private static readonly ImmutableHashSet<string> ExcludedSchemes = ["http", "https", "tel", "jdbc"];
39+
3740
public override bool Match(InlineProcessor processor, ref StringSlice slice)
3841
{
3942
var match = base.Match(processor, ref slice);
@@ -48,6 +51,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
4851
var column = link.Column;
4952
var length = url?.Length ?? 1;
5053

54+
5155
var context = processor.GetContext();
5256
if (processor.GetContext().SkipValidation)
5357
return match;
@@ -58,7 +62,12 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
5862
return match;
5963
}
6064

61-
if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme.StartsWith("http"))
65+
var uri = Uri.TryCreate(url, UriKind.Absolute, out var u) ? u : null;
66+
67+
if (IsCrossLink(uri))
68+
processor.GetContext().Build.Collector.EmitCrossLink(url!);
69+
70+
if (uri != null && uri.Scheme.StartsWith("http"))
6271
{
6372
var baseDomain = uri.Host == "localhost" ? "localhost" : string.Join('.', uri.Host.Split('.')[^2..]);
6473
if (!context.Configuration.ExternalLinkHosts.Contains(baseDomain))
@@ -82,15 +91,11 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
8291
var anchor = anchors.Length > 1 ? anchors[1].Trim() : null;
8392
url = anchors[0];
8493

85-
if (!string.IsNullOrWhiteSpace(url))
94+
if (!string.IsNullOrWhiteSpace(url) && uri != null)
8695
{
8796
var pathOnDisk = Path.Combine(includeFrom, url.TrimStart('/'));
88-
if (!context.Build.ReadFileSystem.File.Exists(pathOnDisk))
97+
if (uri.IsFile && !context.Build.ReadFileSystem.File.Exists(pathOnDisk))
8998
processor.EmitError(line, column, length, $"`{url}` does not exist. resolved to `{pathOnDisk}");
90-
else
91-
{
92-
93-
}
9499
}
95100
else
96101
link.Url = "";
@@ -128,8 +133,11 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
128133
link.Url += $"#{anchor}";
129134

130135
return match;
131-
132-
133-
134136
}
137+
138+
private static bool IsCrossLink(Uri? uri) =>
139+
uri != null
140+
&& !ExcludedSchemes.Contains(uri.Scheme)
141+
&& !uri.IsFile
142+
&& Path.GetExtension(uri.OriginalString) == ".md";
135143
}

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

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ public void GeneratesHtml() =>
6565

6666
[Fact]
6767
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
68+
69+
[Fact]
70+
public void EmitsCrossLink()
71+
{
72+
Collector.CrossLinks.Should().HaveCount(0);
73+
}
6874
}
6975

7076
public class InsertPageTitleTests(ITestOutputHelper output) : LinkTestBase(output,
@@ -82,4 +88,88 @@ public void GeneratesHtml() =>
8288

8389
[Fact]
8490
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
91+
92+
[Fact]
93+
public void EmitsCrossLink()
94+
{
95+
Collector.CrossLinks.Should().HaveCount(0);
96+
}
97+
}
98+
99+
public class LinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output,
100+
"""
101+
[test][test]
102+
103+
[test]: testing/req.md
104+
"""
105+
)
106+
{
107+
[Fact]
108+
public void GeneratesHtml() =>
109+
// language=html
110+
Html.Should().Contain(
111+
"""<p><a href="testing/req.html">test</a></p>"""
112+
);
113+
114+
[Fact]
115+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
116+
117+
[Fact]
118+
public void EmitsCrossLink()
119+
{
120+
Collector.CrossLinks.Should().HaveCount(0);
121+
}
122+
}
123+
124+
public class CrossLinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output,
125+
"""
126+
[test][test]
127+
128+
[test]: kibana://index.md
129+
"""
130+
)
131+
{
132+
[Fact]
133+
public void GeneratesHtml() =>
134+
// language=html
135+
Html.Should().Contain(
136+
// TODO: The link is not rendered correctly yet, will be fixed in a follow-up
137+
"""<p><a href="kibana://index.html">test</a></p>"""
138+
);
139+
140+
[Fact]
141+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
142+
143+
[Fact]
144+
public void EmitsCrossLink()
145+
{
146+
Collector.CrossLinks.Should().HaveCount(1);
147+
Collector.CrossLinks.Should().Contain("kibana://index.md");
148+
}
149+
}
150+
151+
public class CrossLinkTest(ITestOutputHelper output) : LinkTestBase(output,
152+
"""
153+
154+
Go to [test](kibana://index.md)
155+
"""
156+
)
157+
{
158+
[Fact]
159+
public void GeneratesHtml() =>
160+
// language=html
161+
Html.Should().Contain(
162+
// TODO: The link is not rendered correctly yet, will be fixed in a follow-up
163+
"""<p>Go to <a href="kibana://index.html">test</a></p>"""
164+
);
165+
166+
[Fact]
167+
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
168+
169+
[Fact]
170+
public void EmitsCrossLink()
171+
{
172+
Collector.CrossLinks.Should().HaveCount(1);
173+
Collector.CrossLinks.Should().Contain("kibana://index.md");
174+
}
85175
}

0 commit comments

Comments
 (0)