From d77b0d4ae1fc47d872b27068a28be67776b15c60 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 2 Apr 2025 10:35:51 +0200 Subject: [PATCH 1/3] Resolve links in link parser using MarkdownFile.Url if available --- .../DiagnosticLinkInlineParser.cs | 88 ++++++++++--------- src/docs-assembler/AssembleContext.cs | 2 +- .../Sourcing/RepositorySourcesFetcher.cs | 2 +- 3 files changed, 50 insertions(+), 42 deletions(-) diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index 7947f6b6e..d082f5099 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -207,7 +207,7 @@ private static void ProcessInternalLink(LinkInline link, InlineProcessor process var linkMarkdown = SetLinkData(link, processor, context, file, url); ProcessLinkText(processor, link, linkMarkdown, anchor, url, file); - UpdateLinkUrl(link, url, context, anchor); + UpdateLinkUrl(link, linkMarkdown, url, context, anchor); } private static MarkdownFile? SetLinkData(LinkInline link, InlineProcessor processor, ParserContext context, @@ -292,61 +292,69 @@ private static void ValidateAnchor(InlineProcessor processor, MarkdownFile markd processor.EmitError(link, $"`{anchor}` does not exist in {markdown.RelativePath}."); } - private static void UpdateLinkUrl(LinkInline link, string url, ParserContext context, string? anchor) + private static void UpdateLinkUrl(LinkInline link, MarkdownFile? linkMarkdown, string url, ParserContext context, string? anchor) { - // TODO revisit when we refactor our documentation set graph - // This method grew too complex, we need to revisit our documentation set graph generation so we can ask these questions - // on `DocumentationFile` that are mostly precomputed - var urlPathPrefix = context.Build.UrlPathPrefix ?? string.Empty; + var newUrl = url; + if (linkMarkdown is not null) + newUrl = linkMarkdown.Url; - if (!url.StartsWith('/') && !string.IsNullOrEmpty(url)) + else { - // eat overall path prefix since its gets appended later - var subPrefix = context.CurrentUrlPath.Length >= urlPathPrefix.Length - ? context.CurrentUrlPath[urlPathPrefix.Length..] - : urlPathPrefix; - - // if we are trying to resolve a relative url from a _snippet folder ensure we eat the _snippet folder - // as it's not part of url by chopping of the extra parent navigation - if (url.StartsWith("../") && context.DocumentationFileLookup(context.MarkdownSourcePath) is SnippetFile snippetFile) - url = url.Substring(3); - - // TODO check through context.DocumentationFileLookup if file is index vs "index.md" check - var markdownPath = context.MarkdownSourcePath; - // if the current path is an index e.g /reference/cloud-k8s/ - // './' current path lookups should be relative to sub-path. - // If it's not e.g /reference/cloud-k8s/api-docs/ these links should resolve on folder up. - var lastIndexPath = subPrefix.LastIndexOf('/'); - if (lastIndexPath >= 0 && markdownPath.Name != "index.md") - subPrefix = subPrefix[..lastIndexPath]; - var combined = '/' + Path.Combine(subPrefix, url).TrimStart('/'); - url = Path.GetFullPath(combined); + // TODO revisit when we refactor our documentation set graph + // This method grew too complex, we need to revisit our documentation set graph generation so we can ask these questions + // on `DocumentationFile` that are mostly precomputed + var urlPathPrefix = context.Build.UrlPathPrefix ?? string.Empty; + + if (!newUrl.StartsWith('/') && !string.IsNullOrEmpty(newUrl)) + { + // eat overall path prefix since its gets appended later + var subPrefix = context.CurrentUrlPath.Length >= urlPathPrefix.Length + ? context.CurrentUrlPath[urlPathPrefix.Length..] + : urlPathPrefix; + + // if we are trying to resolve a relative url from a _snippet folder ensure we eat the _snippet folder + // as it's not part of url by chopping of the extra parent navigation + if (newUrl.StartsWith("../") && context.DocumentationFileLookup(context.MarkdownSourcePath) is SnippetFile snippetFile) + newUrl = url.Substring(3); + + // TODO check through context.DocumentationFileLookup if file is index vs "index.md" check + var markdownPath = context.MarkdownSourcePath; + // if the current path is an index e.g /reference/cloud-k8s/ + // './' current path lookups should be relative to sub-path. + // If it's not e.g /reference/cloud-k8s/api-docs/ these links should resolve on folder up. + var lastIndexPath = subPrefix.LastIndexOf('/'); + if (lastIndexPath >= 0 && markdownPath.Name != "index.md") + subPrefix = subPrefix[..lastIndexPath]; + var combined = '/' + Path.Combine(subPrefix, newUrl).TrimStart('/'); + newUrl = Path.GetFullPath(combined); + } + + if (!string.IsNullOrWhiteSpace(newUrl) && !string.IsNullOrWhiteSpace(urlPathPrefix)) + newUrl = $"{newUrl.TrimEnd('/')}{newUrl}"; + } - if (url.EndsWith(".md")) + if (newUrl.EndsWith(".md")) { - url = url.EndsWith($"{Path.DirectorySeparatorChar}index.md") - ? url.Remove(url.LastIndexOf("index.md", StringComparison.Ordinal), "index.md".Length) - : url.Remove(url.LastIndexOf(".md", StringComparison.Ordinal), ".md".Length); + newUrl = newUrl.EndsWith($"{Path.DirectorySeparatorChar}index.md") + ? newUrl.Remove(newUrl.LastIndexOf("index.md", StringComparison.Ordinal), "index.md".Length) + : newUrl.Remove(url.LastIndexOf(".md", StringComparison.Ordinal), ".md".Length); } // When running on Windows, path traversal results must be normalized prior to being used in a URL // Path.GetFullPath() will result in the drive letter being appended to the path, which needs to be pruned back. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - url = url.Replace('\\', '/'); - if (url.Length > 2 && url[1] == ':') - url = url[2..]; + newUrl = newUrl.Replace('\\', '/'); + if (newUrl.Length > 2 && newUrl[1] == ':') + newUrl = newUrl[2..]; } - if (!string.IsNullOrWhiteSpace(url) && !string.IsNullOrWhiteSpace(urlPathPrefix)) - url = $"{urlPathPrefix.TrimEnd('/')}{url}"; - // TODO this is hardcoded should be part of extension system - if (url.EndsWith(".toml")) - url = url[..^5]; + if (newUrl.EndsWith(".toml")) + newUrl = url[..^5]; - link.Url = string.IsNullOrEmpty(anchor) ? url : $"{url}#{anchor}"; + link.Url = string.IsNullOrEmpty(anchor) ? newUrl : $"{newUrl}#{anchor}"; } diff --git a/src/docs-assembler/AssembleContext.cs b/src/docs-assembler/AssembleContext.cs index ab3aff075..5ffe1079c 100644 --- a/src/docs-assembler/AssembleContext.cs +++ b/src/docs-assembler/AssembleContext.cs @@ -71,7 +71,7 @@ public AssembleContext( throw new Exception($"Could not find environment {environment}"); Environment = env; - var contentSource = Environment.ContentSource.ToStringFast(); + var contentSource = Environment.ContentSource.ToStringFast(true); CheckoutDirectory = ReadFileSystem.DirectoryInfo.New(checkoutDirectory ?? Path.Combine(".artifacts", "checkouts", contentSource)); OutputDirectory = ReadFileSystem.DirectoryInfo.New(output ?? Path.Combine(".artifacts", "assembly")); diff --git a/src/docs-assembler/Sourcing/RepositorySourcesFetcher.cs b/src/docs-assembler/Sourcing/RepositorySourcesFetcher.cs index 35c0216c9..84de2c886 100644 --- a/src/docs-assembler/Sourcing/RepositorySourcesFetcher.cs +++ b/src/docs-assembler/Sourcing/RepositorySourcesFetcher.cs @@ -48,7 +48,7 @@ public async Task> AcquireAllLatest(Cancel ctx = d _logger.LogInformation( "Cloning all repositories for environment {EnvironmentName} using '{ContentSourceStrategy}' content sourcing strategy", PublishEnvironment.Name, - PublishEnvironment.ContentSource.ToStringFast() + PublishEnvironment.ContentSource.ToStringFast(true) ); var dict = new ConcurrentDictionary(); From d75fee28b91faf47d54392d412c66a1d1aa9612d Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 2 Apr 2025 11:16:33 +0200 Subject: [PATCH 2/3] Fix anchor only edgecase --- .../Myst/InlineParsers/DiagnosticLinkInlineParser.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index d082f5099..908df5dee 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -296,8 +296,11 @@ private static void UpdateLinkUrl(LinkInline link, MarkdownFile? linkMarkdown, s { var newUrl = url; if (linkMarkdown is not null) - newUrl = linkMarkdown.Url; - + { + // if url is null it's an anchor link + if (!string.IsNullOrEmpty(url)) + newUrl = linkMarkdown.Url; + } else { // TODO revisit when we refactor our documentation set graph @@ -330,7 +333,7 @@ private static void UpdateLinkUrl(LinkInline link, MarkdownFile? linkMarkdown, s } if (!string.IsNullOrWhiteSpace(newUrl) && !string.IsNullOrWhiteSpace(urlPathPrefix)) - newUrl = $"{newUrl.TrimEnd('/')}{newUrl}"; + newUrl = $"{urlPathPrefix.TrimEnd('/')}{newUrl}"; } From cfbd99702a4bb4c35d0121a18c51e9a4943ebca4 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Wed, 2 Apr 2025 11:23:09 +0200 Subject: [PATCH 3/3] bump windows filepath fix above joining with urlPathPrefix again --- .../DiagnosticLinkInlineParser.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs index 908df5dee..0a5208a26 100644 --- a/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs +++ b/src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs @@ -332,6 +332,15 @@ private static void UpdateLinkUrl(LinkInline link, MarkdownFile? linkMarkdown, s newUrl = Path.GetFullPath(combined); } + // When running on Windows, path traversal results must be normalized prior to being used in a URL + // Path.GetFullPath() will result in the drive letter being appended to the path, which needs to be pruned back. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + newUrl = newUrl.Replace('\\', '/'); + if (newUrl.Length > 2 && newUrl[1] == ':') + newUrl = newUrl[2..]; + } + if (!string.IsNullOrWhiteSpace(newUrl) && !string.IsNullOrWhiteSpace(urlPathPrefix)) newUrl = $"{urlPathPrefix.TrimEnd('/')}{newUrl}"; @@ -344,15 +353,6 @@ private static void UpdateLinkUrl(LinkInline link, MarkdownFile? linkMarkdown, s : newUrl.Remove(url.LastIndexOf(".md", StringComparison.Ordinal), ".md".Length); } - // When running on Windows, path traversal results must be normalized prior to being used in a URL - // Path.GetFullPath() will result in the drive letter being appended to the path, which needs to be pruned back. - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - newUrl = newUrl.Replace('\\', '/'); - if (newUrl.Length > 2 && newUrl[1] == ':') - newUrl = newUrl[2..]; - } - // TODO this is hardcoded should be part of extension system if (newUrl.EndsWith(".toml")) newUrl = url[..^5];