diff --git a/docs/source/docset.yml b/docs/source/docset.yml index f4f7e75b0..3ce5c474b 100644 --- a/docs/source/docset.yml +++ b/docs/source/docset.yml @@ -74,3 +74,4 @@ toc: - file: index.md - file: req.md - folder: nested + - file: external-links.md diff --git a/docs/source/testing/external-links.md b/docs/source/testing/external-links.md new file mode 100644 index 000000000..863e00c54 --- /dev/null +++ b/docs/source/testing/external-links.md @@ -0,0 +1,9 @@ +--- +title: "External Links" +--- + +[inline external link](kibana://path/to/file.md) + +[reference external link][1] + +[1]: kibana://path/to/another/file.md \ No newline at end of file diff --git a/src/Elastic.Markdown/IO/LinkReference.cs b/src/Elastic.Markdown/IO/LinkReference.cs index da42a7964..084e3c940 100644 --- a/src/Elastic.Markdown/IO/LinkReference.cs +++ b/src/Elastic.Markdown/IO/LinkReference.cs @@ -14,19 +14,24 @@ public record LinkReference [JsonPropertyName("url_path_prefix")] public required string? UrlPathPrefix { get; init; } - [JsonPropertyName("links")] - public required string[] Links { get; init; } = []; + [JsonPropertyName("internal_links")] + public required string[] InternalLinks { get; init; } = []; + + [JsonPropertyName("external_links")] + public required string[] ExternalLinks { get; init; } = []; public static LinkReference Create(DocumentationSet set) { - var links = set.FlatMappedFiles.Values - .OfType() - .Select(m => m.RelativePath).ToArray(); + var markdownFiles = set.FlatMappedFiles.Values.OfType().ToArray(); + var internalLinks = markdownFiles.Select(m => m.RelativePath).ToArray(); + var externalLinks = markdownFiles.SelectMany(m => m.Links).ToHashSet().ToArray(); return new LinkReference { UrlPathPrefix = set.Context.UrlPathPrefix, Origin = set.Context.Git, - Links = links + InternalLinks = internalLinks, + ExternalLinks = externalLinks }; } + } diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 0e287bb27..4af4dcfd4 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -10,6 +10,7 @@ using Markdig; using Markdig.Extensions.Yaml; using Markdig.Syntax; +using Markdig.Syntax.Inlines; using Slugify; namespace Elastic.Markdown.IO; @@ -42,6 +43,7 @@ public string? NavigationTitle get => !string.IsNullOrEmpty(_navigationTitle) ? _navigationTitle : Title; private set => _navigationTitle = value; } + public IReadOnlySet Links { get; private set; } = new HashSet(); //indexed by slug private readonly Dictionary _tableOfContent = new(); @@ -56,6 +58,8 @@ public string? NavigationTitle private bool _instructionsParsed; + public static readonly HashSet ExcludedExternalLinkSchemes = ["http", "https", "mailto", "tel", "file"]; + public MarkdownFile[] YieldParents() { var parents = new List(); @@ -82,11 +86,22 @@ public async Task ParseFullAsync(Cancel ctx) await MinimalParse(ctx); var document = await MarkdownParser.ParseAsync(SourceFile, YamlFrontMatter, ctx); + ReadLinks(document); if (Title == RelativePath) Collector.EmitWarning(SourceFile.FullName, "Missing yaml front-matter block defining a title or a level 1 header"); return document; } + private void ReadLinks(MarkdownDocument document) + { + var links = document.Descendants() + .Select(l => l.Url) + .Where(l => !string.IsNullOrEmpty(l) && !l.StartsWith('/')) + .Where(url => Uri.TryCreate(url, UriKind.Absolute, out var uri) && !ExcludedExternalLinkSchemes.Contains(uri.Scheme)); + + Links = new HashSet(links!); + } + private void ReadDocumentInstructions(MarkdownDocument document) { if (document.FirstOrDefault() is YamlFrontMatterBlock yaml) diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index d04c0ac17..cff478be9 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -44,6 +44,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) return match; var url = link.Url; + var uri = Uri.TryCreate(url, UriKind.Absolute, out var u) ? u : null; var line = link.Line + 1; var column = link.Column; var length = url?.Length ?? 1; @@ -58,7 +59,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")) + if (uri != null && !MarkdownFile.ExcludedExternalLinkSchemes.Contains(uri.Scheme)) + { + return match; + } + + 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)) @@ -128,8 +134,5 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) link.Url += $"#{anchor}"; return match; - - - } }