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
1 change: 1 addition & 0 deletions docs/source/docset.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ toc:
- file: index.md
- file: req.md
- folder: nested
- file: cross-links.md
9 changes: 9 additions & 0 deletions docs/source/testing/cross-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: Cross Links
---

[Elasticsearch](elasticsearch://index.md)

[Kibana][1]

[1]: kibana://index.md
4 changes: 4 additions & 0 deletions src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Concurrent;
using System.Threading.Channels;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -86,6 +87,8 @@ public class DiagnosticsCollector(ILoggerFactory loggerFactory, IReadOnlyCollect

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

public ConcurrentBag<string> CrossLinks { get; } = new();

public Task StartAsync(Cancel ctx)
{
if (_started is not null)
Expand Down Expand Up @@ -140,6 +143,7 @@ public virtual async Task StopAsync(CancellationToken cancellationToken)
await Channel.Reader.Completion;
}

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

public void EmitError(string file, string message, Exception? e = null)
{
Expand Down
7 changes: 6 additions & 1 deletion src/Elastic.Markdown/IO/LinkReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,21 @@ public record LinkReference
[JsonPropertyName("links")]
public required string[] Links { get; init; } = [];

[JsonPropertyName("cross_links")]
public required string[] CrossLinks { get; init; } = [];

public static LinkReference Create(DocumentationSet set)
{
var crossLinks = set.Context.Collector.CrossLinks.ToHashSet().ToArray();
var links = set.FlatMappedFiles.Values
.OfType<MarkdownFile>()
.Select(m => m.RelativePath).ToArray();
return new LinkReference
{
UrlPathPrefix = set.Context.UrlPathPrefix,
Origin = set.Context.Git,
Links = links
Links = links,
CrossLinks = crossLinks
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using System.Collections.Immutable;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.IO;
using Elastic.Markdown.Myst.Directives;
using Markdig;
using Markdig.Helpers;
using Markdig.Parsers;
using Markdig.Parsers.Inlines;
using Markdig.Renderers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;

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

public class DiagnosticLinkInlineParser : LinkInlineParser
{
// See https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for a list of URI schemes
// We can add more schemes as needed
private static readonly ImmutableHashSet<string> ExcludedSchemes = ["http", "https", "tel", "jdbc"];

public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
var match = base.Match(processor, ref slice);
Expand All @@ -48,6 +51,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
var column = link.Column;
var length = url?.Length ?? 1;


var context = processor.GetContext();
if (processor.GetContext().SkipValidation)
return match;
Expand All @@ -58,7 +62,12 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
return match;
}

if (Uri.TryCreate(url, UriKind.Absolute, out var uri) && uri.Scheme.StartsWith("http"))
var uri = Uri.TryCreate(url, UriKind.Absolute, out var u) ? u : null;

if (IsCrossLink(uri))
processor.GetContext().Build.Collector.EmitCrossLink(url!);

if (uri != null && uri.Scheme.StartsWith("http"))
{
var baseDomain = uri.Host == "localhost" ? "localhost" : string.Join('.', uri.Host.Split('.')[^2..]);
if (!context.Configuration.ExternalLinkHosts.Contains(baseDomain))
Expand All @@ -82,15 +91,11 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
var anchor = anchors.Length > 1 ? anchors[1].Trim() : null;
url = anchors[0];

if (!string.IsNullOrWhiteSpace(url))
if (!string.IsNullOrWhiteSpace(url) && uri != null)
{
var pathOnDisk = Path.Combine(includeFrom, url.TrimStart('/'));
if (!context.Build.ReadFileSystem.File.Exists(pathOnDisk))
if (uri.IsFile && !context.Build.ReadFileSystem.File.Exists(pathOnDisk))
processor.EmitError(line, column, length, $"`{url}` does not exist. resolved to `{pathOnDisk}");
else
{

}
}
else
link.Url = "";
Expand Down Expand Up @@ -128,8 +133,11 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
link.Url += $"#{anchor}";

return match;



}

private static bool IsCrossLink(Uri? uri) =>
uri != null
&& !ExcludedSchemes.Contains(uri.Scheme)
&& !uri.IsFile
&& Path.GetExtension(uri.OriginalString) == ".md";
}
90 changes: 90 additions & 0 deletions tests/Elastic.Markdown.Tests/Inline/InlineLinkTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ public void GeneratesHtml() =>

[Fact]
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);

[Fact]
public void EmitsCrossLink()
{
Collector.CrossLinks.Should().HaveCount(0);
}
}

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

[Fact]
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);

[Fact]
public void EmitsCrossLink()
{
Collector.CrossLinks.Should().HaveCount(0);
}
}

public class LinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output,
"""
[test][test]

[test]: testing/req.md
"""
)
{
[Fact]
public void GeneratesHtml() =>
// language=html
Html.Should().Contain(
"""<p><a href="testing/req.html">test</a></p>"""
);

[Fact]
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);

[Fact]
public void EmitsCrossLink()
{
Collector.CrossLinks.Should().HaveCount(0);
}
}

public class CrossLinkReferenceTest(ITestOutputHelper output) : LinkTestBase(output,
"""
[test][test]

[test]: kibana://index.md
"""
)
{
[Fact]
public void GeneratesHtml() =>
// language=html
Html.Should().Contain(
// TODO: The link is not rendered correctly yet, will be fixed in a follow-up
"""<p><a href="kibana://index.html">test</a></p>"""
);

[Fact]
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);

[Fact]
public void EmitsCrossLink()
{
Collector.CrossLinks.Should().HaveCount(1);
Collector.CrossLinks.Should().Contain("kibana://index.md");
}
}

public class CrossLinkTest(ITestOutputHelper output) : LinkTestBase(output,
"""

Go to [test](kibana://index.md)
"""
)
{
[Fact]
public void GeneratesHtml() =>
// language=html
Html.Should().Contain(
// TODO: The link is not rendered correctly yet, will be fixed in a follow-up
"""<p>Go to <a href="kibana://index.html">test</a></p>"""
);

[Fact]
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);

[Fact]
public void EmitsCrossLink()
{
Collector.CrossLinks.Should().HaveCount(1);
Collector.CrossLinks.Should().Contain("kibana://index.md");
}
}
Loading