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
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/DocumentationGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
using System.IO.Abstractions;
using System.Reflection;
using System.Text.Json;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.Exporters;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.State;
using Elastic.Markdown.Links.CrossLinks;
using Elastic.Markdown.Slices;
using Markdig.Syntax;
using Microsoft.Extensions.Logging;
Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/IO/DocumentationSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
using System.Collections.Frozen;
using System.IO.Abstractions;
using System.Runtime.InteropServices;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Extensions;
using Elastic.Markdown.IO.Configuration;
using Elastic.Markdown.IO.Discovery;
using Elastic.Markdown.IO.Navigation;
using Elastic.Markdown.Links.CrossLinks;
using Elastic.Markdown.Myst;
using Microsoft.Extensions.Logging;

Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/IO/MarkdownFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

using System.IO.Abstractions;
using System.Runtime.InteropServices;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.Helpers;
using Elastic.Markdown.IO.Configuration;
using Elastic.Markdown.IO.Navigation;
using Elastic.Markdown.Links.CrossLinks;
using Elastic.Markdown.Myst;
using Elastic.Markdown.Myst.Directives;
using Elastic.Markdown.Myst.FrontMatter;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
using Elastic.Markdown.IO.State;
using Microsoft.Extensions.Logging;

namespace Elastic.Markdown.CrossLinks;
namespace Elastic.Markdown.Links.CrossLinks;

public class ConfigurationCrossLinkFetcher(ConfigurationFile configuration, ILoggerFactory logger) : CrossLinkFetcher(logger)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using Elastic.Markdown.IO.State;
using Microsoft.Extensions.Logging;

namespace Elastic.Markdown.CrossLinks;
namespace Elastic.Markdown.Links.CrossLinks;

public record FetchedCrossLinks
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
using System.Text.Json.Serialization;
using Elastic.Markdown.IO.State;

namespace Elastic.Markdown.CrossLinks;
namespace Elastic.Markdown.Links.CrossLinks;

public record LinkIndex
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +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

namespace Elastic.Markdown.CrossLinks;
namespace Elastic.Markdown.Links.CrossLinks;

public interface IUriEnvironmentResolver
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
// See the LICENSE file in the project root for more information

using System.Collections.Frozen;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.IO.State;
using Elastic.Markdown.Links.CrossLinks;
using Microsoft.Extensions.Logging;

namespace Elastic.Markdown.InboundLinks;
namespace Elastic.Markdown.Links.InboundLinks;

public class LinksIndexCrossLinkFetcher(ILoggerFactory logger) : CrossLinkFetcher(logger)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
// 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 Elastic.Markdown.CrossLinks;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.State;
using Elastic.Markdown.Links.CrossLinks;
using Microsoft.Extensions.Logging;

namespace Elastic.Markdown.InboundLinks;
namespace Elastic.Markdown.Links.InboundLinks;

public class LinkIndexLinkChecker(ILoggerFactory logger)
{
Expand Down Expand Up @@ -47,9 +47,7 @@ public async Task<int> CheckRepository(DiagnosticsCollector collector, string? t
return await ValidateCrossLinks(collector, crossLinks, resolver, filter, ctx);
}

public async Task<int> CheckWithLocalLinksJson(DiagnosticsCollector collector, string repository,
string localLinksJson,
Cancel ctx)
public async Task<int> CheckWithLocalLinksJson(DiagnosticsCollector collector, string repository, string localLinksJson, Cancel ctx)
{
var fetcher = new LinksIndexCrossLinkFetcher(logger);
var resolver = new CrossLinkResolver(fetcher);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.State;
using Microsoft.Extensions.Logging;

namespace Elastic.Markdown.Links.LinkNamespaces;

/// <summary>
/// Validates paths don't conflict with global navigation namespaces.
/// For example if the global navigation defines:
/// <code>
/// - toc: elasticsearch://reference/elasticsearch
/// path_prefix: reference/elasticsearch
///
/// - toc: docs-content://reference/elasticsearch/clients
/// path_prefix: reference/elasticsearch/clients
/// </code>
///
/// This will validate `elasticsearch://` does not create a `elasticsearch://reference/elasticsearch/clients` folder
/// since that is already claimed by `docs-content://reference/elasticsearch/clients`
///
/// </summary>
public class LinkGlobalNamespaceChecker(ILoggerFactory logger, ImmutableHashSet<Uri> namespaces)
{
private readonly Dictionary<string, string> _pathPrefixes = namespaces
.ToDictionary(n => $"{n.Host}/{n.AbsolutePath.Trim('/')}/", n => n.Scheme);

private readonly ILogger _logger = logger.CreateLogger<LinkGlobalNamespaceChecker>();

public async Task CheckWithLocalLinksJson(DiagnosticsCollector collector, string repository, string? localLinksJson, CancellationToken ctx)
{
if (string.IsNullOrEmpty(repository))
throw new ArgumentNullException(nameof(repository));
if (string.IsNullOrEmpty(localLinksJson))
throw new ArgumentNullException(nameof(localLinksJson));

_logger.LogInformation("Checking '{Repository}' with local '{LocalLinksJson}'", repository, localLinksJson);

if (!Path.IsPathRooted(localLinksJson))
localLinksJson = Path.Combine(Paths.WorkingDirectoryRoot.FullName, localLinksJson);

var linkReference = await ReadLocalLinksJsonAsync(localLinksJson, ctx);

foreach (var (relativeLink, _) in linkReference.Links)
{
if (!TryGetReservedPathPrefix(relativeLink, out var reservedPathPrefix, out var byRepository))
continue;
if (byRepository == repository)
continue;

collector.EmitError(repository, $"'{relativeLink}' lives in path_prefix already claimed by '{byRepository}://{reservedPathPrefix}' in global navigation.yml");
}
}

private bool TryGetReservedPathPrefix(
string path,
[NotNullWhen(true)] out string? reservedPathPrefix,
[NotNullWhen(true)] out string? reservedByRepository
)
{
reservedPathPrefix = null;
reservedByRepository = null;
foreach (var (prefix, repository) in _pathPrefixes)
{
if (!path.StartsWith(prefix))
continue;
reservedPathPrefix = prefix;
reservedByRepository = repository;
return true;
}

return false;
}

private async Task<LinkReference> ReadLocalLinksJsonAsync(string localLinksJson, CancellationToken ctx)
{
try
{
var json = await File.ReadAllTextAsync(localLinksJson, ctx);
return LinkReference.Deserialize(json);
}
catch (Exception e)
{
_logger.LogError(e, "Failed to read {LocalLinksJson}", localLinksJson);
throw;
}
}
}
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/Myst/ParserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
// See the LICENSE file in the project root for more information

using System.IO.Abstractions;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.Diagnostics;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.Configuration;
using Elastic.Markdown.Links.CrossLinks;
using Elastic.Markdown.Myst.FrontMatter;
using Markdig;
using Markdig.Parsers;
Expand Down
2 changes: 1 addition & 1 deletion src/Elastic.Markdown/SourceGenerationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
// See the LICENSE file in the project root for more information

using System.Text.Json.Serialization;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.IO.Discovery;
using Elastic.Markdown.IO.State;
using Elastic.Markdown.Links.CrossLinks;

namespace Elastic.Markdown;

Expand Down
2 changes: 1 addition & 1 deletion src/docs-assembler/AssembleSources.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
using Documentation.Assembler.Configuration;
using Documentation.Assembler.Navigation;
using Documentation.Assembler.Sourcing;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.IO.Configuration;
using Elastic.Markdown.IO.Navigation;
using Elastic.Markdown.Links.CrossLinks;
using Microsoft.Extensions.Logging.Abstractions;
using YamlDotNet.RepresentationModel;

Expand Down
2 changes: 1 addition & 1 deletion src/docs-assembler/Building/AssemblerCrossLinkFetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

using System.Collections.Frozen;
using Documentation.Assembler.Configuration;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.IO.State;
using Elastic.Markdown.Links.CrossLinks;
using Microsoft.Extensions.Logging;

namespace Documentation.Assembler.Building;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

using System.Collections.Frozen;
using Documentation.Assembler.Configuration;
using Elastic.Markdown.CrossLinks;
using Elastic.Markdown.Links.CrossLinks;

namespace Documentation.Assembler.Building;

Expand Down
2 changes: 1 addition & 1 deletion src/docs-assembler/Cli/InboundLinkCommands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
using Actions.Core.Services;
using ConsoleAppFramework;
using Elastic.Documentation.Tooling.Diagnostics.Console;
using Elastic.Markdown.InboundLinks;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.Discovery;
using Elastic.Markdown.Links.InboundLinks;
using Microsoft.Extensions.Logging;

namespace Documentation.Assembler.Cli;
Expand Down
85 changes: 85 additions & 0 deletions src/docs-assembler/Cli/NavigationCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// 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.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using Actions.Core.Services;
using ConsoleAppFramework;
using Documentation.Assembler.Navigation;
using Elastic.Documentation.Tooling.Diagnostics.Console;
using Elastic.Documentation.Tooling.Filters;
using Elastic.Markdown.IO;
using Elastic.Markdown.IO.Discovery;
using Elastic.Markdown.Links.InboundLinks;
using Elastic.Markdown.Links.LinkNamespaces;
using Microsoft.Extensions.Logging;

namespace Documentation.Assembler.Cli;

internal sealed class NavigationCommands(ILoggerFactory logger, ICoreService githubActionsService)
{
private readonly LinkIndexLinkChecker _linkIndexLinkChecker = new(logger);

[SuppressMessage("Usage", "CA2254:Template should be a static expression")]
private void AssignOutputLogger()
{
var log = logger.CreateLogger<Program>();
ConsoleApp.Log = msg => log.LogInformation(msg);
ConsoleApp.LogError = msg => log.LogError(msg);
}

/// <summary> Validates navigation.yml does not contain colliding path prefixes </summary>
/// <param name="ctx"></param>
[Command("validate")]
[ConsoleAppFilter<StopwatchFilter>]
[ConsoleAppFilter<CatchExceptionFilter>]
public async Task<int> Validate(Cancel ctx = default)
{
AssignOutputLogger();
await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService);
var assembleContext = new AssembleContext("dev", collector, new FileSystem(), new FileSystem(), null, null);
_ = collector.StartAsync(ctx);

// this validates all path prefixes are unique, early exit if duplicates are detected
if (!GlobalNavigationFile.ValidatePathPrefixes(assembleContext) || assembleContext.Collector.Errors > 0)
{
assembleContext.Collector.Channel.TryComplete();
await assembleContext.Collector.StopAsync(ctx);
return 1;
}

return 0;
}

/// <summary> Validate all published links in links.json do not collide with navigation path_prefixes. </summary>
/// <param name="file">Path to `links.json` defaults to '.artifacts/docs/html/links.json'</param>
/// <param name="ctx"></param>
[Command("validate-link-reference")]
[ConsoleAppFilter<StopwatchFilter>]
[ConsoleAppFilter<CatchExceptionFilter>]
public async Task<int> ValidateLocalLinkReference([Argument] string? file = null, Cancel ctx = default)
{
AssignOutputLogger();
file ??= ".artifacts/docs/html/links.json";

await using var collector = new ConsoleDiagnosticsCollector(logger, githubActionsService);
_ = collector.StartAsync(ctx);

var assembleContext = new AssembleContext("dev", collector, new FileSystem(), new FileSystem(), null, null);

var fs = new FileSystem();
var root = fs.DirectoryInfo.New(Paths.WorkingDirectoryRoot.FullName);
var repository = GitCheckoutInformation.Create(root, new FileSystem(), logger.CreateLogger(nameof(GitCheckoutInformation))).RepositoryName
?? throw new Exception("Unable to determine repository name");

var prefixes = GlobalNavigationFile.GetAllPathPrefixes(assembleContext);

var namespaceChecker = new LinkGlobalNamespaceChecker(logger, prefixes);

await namespaceChecker.CheckWithLocalLinksJson(assembleContext.Collector, repository, file, ctx);

return await _linkIndexLinkChecker.CheckAll(collector, ctx);
}

}
Loading
Loading