public class CrossLinkResolver()

in src/Elastic.Markdown/Links/CrossLinks/CrossLinkResolver.cs [19:215]


public class CrossLinkResolver(CrossLinkFetcher fetcher, IUriEnvironmentResolver? uriResolver = null) : ICrossLinkResolver
{
	private FetchedCrossLinks _crossLinks = FetchedCrossLinks.Empty;
	public IUriEnvironmentResolver UriResolver { get; } = uriResolver ?? new IsolatedBuildEnvironmentUriResolver();

	public async Task<FetchedCrossLinks> FetchLinks(Cancel ctx)
	{
		_crossLinks = await fetcher.Fetch(ctx);
		return _crossLinks;
	}

	public bool TryResolve(Action<string> errorEmitter, Action<string> warningEmitter, Uri crossLinkUri, [NotNullWhen(true)] out Uri? resolvedUri) =>
		TryResolve(errorEmitter, warningEmitter, _crossLinks, UriResolver, crossLinkUri, out resolvedUri);

	public FetchedCrossLinks UpdateLinkReference(string repository, LinkReference linkReference)
	{
		var dictionary = _crossLinks.LinkReferences.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
		dictionary[repository] = linkReference;
		_crossLinks = _crossLinks with
		{
			LinkReferences = dictionary.ToFrozenDictionary()
		};
		return _crossLinks;
	}

	public static bool TryResolve(
		Action<string> errorEmitter,
		Action<string> warningEmitter,
		FetchedCrossLinks fetchedCrossLinks,
		IUriEnvironmentResolver uriResolver,
		Uri crossLinkUri,
		[NotNullWhen(true)] out Uri? resolvedUri
	)
	{
		resolvedUri = null;
		var lookup = fetchedCrossLinks.LinkReferences;
		if (crossLinkUri.Scheme != "asciidocalypse" && lookup.TryGetValue(crossLinkUri.Scheme, out var linkReference))
			return TryFullyValidate(errorEmitter, uriResolver, fetchedCrossLinks, linkReference, crossLinkUri, out resolvedUri);

		// TODO this is temporary while we wait for all links.json to be published
		// Here we just silently rewrite the cross_link to the url

		var declaredRepositories = fetchedCrossLinks.DeclaredRepositories;
		if (!declaredRepositories.Contains(crossLinkUri.Scheme))
		{
			if (fetchedCrossLinks.FromConfiguration)
				errorEmitter($"'{crossLinkUri.Scheme}' is not declared as valid cross link repository in docset.yml under cross_links: '{crossLinkUri}'");
			else
				warningEmitter($"'{crossLinkUri.Scheme}' is not yet publishing to the links registry: '{crossLinkUri}'");
			return false;
		}

		var lookupPath = (crossLinkUri.Host + '/' + crossLinkUri.AbsolutePath.TrimStart('/')).Trim('/');
		var path = ToTargetUrlPath(lookupPath);
		if (!string.IsNullOrEmpty(crossLinkUri.Fragment))
			path += crossLinkUri.Fragment;

		resolvedUri = uriResolver.Resolve(crossLinkUri, path);
		return true;
	}

	private static bool TryFullyValidate(Action<string> errorEmitter,
		IUriEnvironmentResolver uriResolver,
		FetchedCrossLinks fetchedCrossLinks,
		LinkReference linkReference,
		Uri crossLinkUri,
		[NotNullWhen(true)] out Uri? resolvedUri)
	{
		resolvedUri = null;
		var lookupPath = (crossLinkUri.Host + '/' + crossLinkUri.AbsolutePath.TrimStart('/')).Trim('/');
		if (string.IsNullOrEmpty(lookupPath) && crossLinkUri.Host.EndsWith(".md"))
			lookupPath = crossLinkUri.Host;

		if (!LookupLink(errorEmitter, fetchedCrossLinks, linkReference, crossLinkUri, ref lookupPath, out var link, out var lookupFragment))
			return false;

		var path = ToTargetUrlPath(lookupPath);

		if (!string.IsNullOrEmpty(lookupFragment))
		{
			if (link.Anchors is null)
			{
				errorEmitter($"'{lookupPath}' does not have any anchors so linking to '{crossLinkUri.Fragment}' is impossible.");
				return false;
			}

			if (!link.Anchors.Contains(lookupFragment.TrimStart('#')))
			{
				errorEmitter($"'{lookupPath}' has no anchor named: '{lookupFragment}'.");
				return false;
			}

			path += "#" + lookupFragment.TrimStart('#');
		}

		resolvedUri = uriResolver.Resolve(crossLinkUri, path);
		return true;
	}

	private static bool LookupLink(Action<string> errorEmitter,
		FetchedCrossLinks crossLinks,
		LinkReference linkReference,
		Uri crossLinkUri,
		ref string lookupPath,
		[NotNullWhen(true)] out LinkMetadata? link,
		[NotNullWhen(true)] out string? lookupFragment)
	{
		lookupFragment = null;

		if (linkReference.Redirects is not null && linkReference.Redirects.TryGetValue(lookupPath, out var redirect))
		{
			var targets = (redirect.Many ?? [])
				.Select(r => r)
				.Concat([redirect])
				.Where(s => !string.IsNullOrEmpty(s.To))
				.ToArray();

			return ResolveLinkRedirect(targets, errorEmitter, linkReference, crossLinkUri, ref lookupPath, out link, ref lookupFragment);
		}

		if (linkReference.Links.TryGetValue(lookupPath, out link))
		{
			lookupFragment = crossLinkUri.Fragment;
			return true;
		}

		var linksJson = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/elastic/{crossLinkUri.Scheme}/main/links.json";
		if (crossLinks.LinkIndexEntries.TryGetValue(crossLinkUri.Scheme, out var linkIndexEntry))
			linksJson = $"https://elastic-docs-link-index.s3.us-east-2.amazonaws.com/{linkIndexEntry.Path}";

		errorEmitter($"'{lookupPath}' is not a valid link in the '{crossLinkUri.Scheme}' cross link index: {linksJson}");
		return false;
	}

	private static bool ResolveLinkRedirect(
		LinkSingleRedirect[] redirects,
		Action<string> errorEmitter,
		LinkReference linkReference,
		Uri crossLinkUri,
		ref string lookupPath, out LinkMetadata? link, ref string? lookupFragment)
	{
		var fragment = crossLinkUri.Fragment.TrimStart('#');
		link = null;
		foreach (var redirect in redirects)
		{
			if (string.IsNullOrEmpty(redirect.To))
				continue;
			if (!linkReference.Links.TryGetValue(redirect.To, out link))
				continue;

			if (string.IsNullOrEmpty(fragment))
			{
				lookupPath = redirect.To;
				return true;
			}

			if (redirect.Anchors is null || redirect.Anchors.Count == 0)
			{
				if (redirects.Length > 1)
					continue;
				lookupPath = redirect.To;
				lookupFragment = crossLinkUri.Fragment;
				return true;
			}

			if (redirect.Anchors.TryGetValue("!", out _))
			{
				lookupPath = redirect.To;
				lookupFragment = null;
				return true;
			}

			if (!redirect.Anchors.TryGetValue(crossLinkUri.Fragment.TrimStart('#'), out var newFragment))
				continue;

			lookupPath = redirect.To;
			lookupFragment = newFragment;
			return true;
		}

		var targets = string.Join(", ", redirects.Select(r => r.To));
		var failedLookup = lookupFragment is null ? lookupPath : $"{lookupPath}#{lookupFragment.TrimStart('#')}";
		errorEmitter($"'{failedLookup}' is set a redirect but none of redirect '{targets}' match or exist in links.json.");
		return false;
	}

	private static string ToTargetUrlPath(string lookupPath)
	{
		//https://docs-v3-preview.elastic.dev/elastic/docs-content/tree/main/cloud-account/change-your-password
		var path = lookupPath.Replace(".md", "");
		if (path.EndsWith("/index"))
			path = path[..^6];
		if (path == "index")
			path = string.Empty;
		return path;
	}
}