Repository navigation
Warn when links to the API docs point at missing pages #4653
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b2bbe8d
c4dcaac
cb684bf
5115e19
616bd05
ecabff4
5cf92e9
4c2a6bf
ae861f3
4a67d21
25808cf
ed50b61
b115859
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // Licensed to Elasticsearch B.V under one or more agreements. | ||
| // 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.Text.RegularExpressions; | ||
|
|
||
| namespace Elastic.Documentation.Diagnostics; | ||
|
|
||
| /// <summary>A link to an API reference page, recorded during the Markdown build and checked once the API pages exist.</summary> | ||
| public readonly record struct ApiLinkReference(string Url, string File, int Line, int Column, int Length); | ||
|
|
||
| public static partial class ApiLinkMatcher | ||
| { | ||
| public const string ApiDocPrefix = "/docs/api/doc/"; | ||
|
|
||
| /// <summary> | ||
| /// Returns the normalized page path (no query, fragment or trailing slash, lowercase) when | ||
| /// <paramref name="url"/> targets an API reference page on elastic.co. | ||
| /// </summary> | ||
| public static bool TryGetApiPath(string? url, out string path) | ||
| { | ||
| path = string.Empty; | ||
| if (string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url.Trim(), UriKind.Absolute, out var uri)) | ||
| return false; | ||
| if (uri.Scheme is not ("http" or "https")) | ||
| return false; | ||
| if ( | ||
| !uri.Host.Equals("www.elastic.co", StringComparison.OrdinalIgnoreCase) | ||
| && !uri.Host.Equals("elastic.co", StringComparison.OrdinalIgnoreCase) | ||
| ) | ||
| return false; | ||
|
|
||
| var normalized = Normalize(uri.AbsolutePath); | ||
| if (!normalized.StartsWith(ApiDocPrefix, StringComparison.Ordinal)) | ||
| return false; | ||
|
|
||
| path = normalized; | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns <paramref name="url"/> as an absolute elastic.co API URL when it targets an API page, either as such or as a | ||
| /// site path under <paramref name="urlPathPrefix"/>, which is how the API Explorer writes links between its own pages. | ||
| /// </summary> | ||
| public static bool TryCanonicalize(string? url, string? urlPathPrefix, out string canonical) | ||
| { | ||
| canonical = string.Empty; | ||
| if (string.IsNullOrWhiteSpace(url)) | ||
| return false; | ||
|
|
||
| if (TryGetApiPath(url, out _)) | ||
| { | ||
| canonical = url; | ||
| return true; | ||
| } | ||
|
|
||
| var prefix = urlPathPrefix?.TrimEnd('/') ?? string.Empty; | ||
| if (!url.StartsWith($"{prefix}/api/doc/", StringComparison.OrdinalIgnoreCase)) | ||
| return false; | ||
|
|
||
| canonical = $"https://www.elastic.co/docs{url[prefix.Length..]}"; | ||
| return true; | ||
| } | ||
|
|
||
| public static string Normalize(string path) => | ||
| DuplicateSlashes().Replace(Uri.UnescapeDataString(path), "/").TrimEnd('/').ToLowerInvariant(); | ||
|
|
||
| [GeneratedRegex("/{2,}")] | ||
| private static partial Regex DuplicateSlashes(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,11 +8,13 @@ | |
| using Elastic.Documentation.Configuration.LegacyUrlMappings; | ||
| using Elastic.Documentation.Configuration.Products; | ||
| using Elastic.Documentation.Configuration.Versions; | ||
| using Elastic.Documentation.Diagnostics; | ||
| using Elastic.Documentation.Extensions; | ||
| using Elastic.Documentation.Navigation; | ||
| using Elastic.Documentation.Site; | ||
| using Elastic.Documentation.Site.FileProviders; | ||
| using Elastic.Documentation.Site.Navigation; | ||
| using Elastic.Markdown.Exporters; | ||
| using Elastic.Markdown.Extensions.DetectionRules; | ||
| using Elastic.Markdown.Helpers; | ||
| using Elastic.Markdown.IO; | ||
|
|
@@ -71,9 +73,21 @@ public string RenderApiDescription(string markdown, IFileInfo? source) | |
| { | ||
| source ??= DocumentationSet.Context.ConfigurationPath; | ||
| var parsed = DocumentationSet.MarkdownParser.ParseApiDescriptionString(markdown, source); | ||
| RecordApiLinks(parsed, source); | ||
| return MarkdownFile.CreateHtml(parsed, stripFirstHeadingLevel1: true, MarkdownParser.ApiDescriptionPipeline); | ||
| } | ||
|
|
||
| // Descriptions parse without link validation, so the links they hold between API pages are recorded here for the API link check. | ||
| private void RecordApiLinks(MarkdownDocument parsed, IFileInfo source) | ||
| { | ||
| var context = DocumentationSet.Context; | ||
| foreach (var url in OutboundLinkExtractor.Extract(parsed) ?? []) | ||
| { | ||
| if (ApiLinkMatcher.TryCanonicalize(url, context.UrlPathPrefix, out var canonical)) | ||
| context.Collector.EmitApiLink(new ApiLinkReference(canonical, source.FullName, 1, 1, canonical.Length)); | ||
| } | ||
| } | ||
|
Comment on lines
+80
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: P2 (Medium)
Generated by Libra. Specialist: libra-complexity. React with 👍 or 👎 to give feedback on this comment. |
||
|
|
||
| /// <inheritdoc /> | ||
| public IReadOnlyList<TocItem> ReadApiDescriptionHeadings(string markdown, IFileInfo? source) | ||
| { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Severity: P2 (Medium)
Include raw HTML anchor targets when recording description links.
ApiDescriptionPipelinesupports<a href="...">links, butOutboundLinkExtractoronly collects Markdown link nodes and directive targets, so an HTML link to a missing API page never reaches the validator and produces neither a warning nor a report entry.Generated by Libra. React with 👍 or 👎 to give feedback on this comment.