File: Documentation\Docs\DocsSourceConfiguration.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
 
namespace Aspire.Cli.Documentation.Docs;
 
/// <summary>
/// Resolves configuration for the Aspire llms.txt docs source.
/// </summary>
internal static partial class DocsSourceConfiguration
{
    private const string IndexCacheKeyPrefix = "index:";
 
    /// <summary>
    /// Configuration path for overriding the llms.txt source URL.
    /// </summary>
    public const string LlmsTxtUrlConfigPath = "docs:llmsTxtUrl";
 
    /// <summary>
    /// Default URL for the Aspire llms.txt documentation source.
    /// </summary>
    /// <remarks>
    /// Uses <c>llms-full.txt</c> rather than <c>llms-small.txt</c> because the
    /// abridged source collapses whitespace inside fenced code blocks, which
    /// renders code samples as flowed paragraphs in <c>aspire docs get</c>.
    /// The full source is only marginally larger and is ETag-cached, so the
    /// extra bytes are paid once per source change.
    /// </remarks>
    public const string DefaultLlmsTxtUrl = "https://aspire.dev/llms-full.txt";
 
    /// <summary>
    /// Gets the URL used to fetch the Aspire llms.txt documentation source.
    /// </summary>
    /// <param name="configuration">The configuration to read from.</param>
    /// <returns>The resolved documentation source URL.</returns>
    public static string GetLlmsTxtUrl(IConfiguration configuration)
        => configuration[LlmsTxtUrlConfigPath] ?? DefaultLlmsTxtUrl;
 
    /// <summary>
    /// Gets a source-specific cache key for the parsed llms.txt index.
    /// </summary>
    /// <param name="llmsTxtUrl">The configured documentation source URL.</param>
    /// <returns>The cache key used for the parsed documentation index.</returns>
    public static string GetIndexCacheKey(string llmsTxtUrl)
        => $"{IndexCacheKeyPrefix}{GetContentCacheKey(llmsTxtUrl)}";
 
    /// <summary>
    /// Gets the legacy raw-URL cache key for the parsed llms.txt index.
    /// </summary>
    /// <param name="llmsTxtUrl">The configured documentation source URL.</param>
    /// <returns>The legacy cache key used by earlier builds.</returns>
    public static string GetLegacyIndexCacheKey(string llmsTxtUrl)
        => $"{IndexCacheKeyPrefix}{llmsTxtUrl.Trim()}";
 
    /// <summary>
    /// Gets the cache key used for the fetched llms.txt source content.
    /// </summary>
    /// <param name="llmsTxtUrl">The configured documentation source URL.</param>
    /// <returns>The cache key used for source content and ETag persistence.</returns>
    public static string GetContentCacheKey(string llmsTxtUrl)
        => DocumentationCacheKey.FromUrl(llmsTxtUrl, "llms");
 
    /// <summary>
    /// Rewrites docs markdown links so site-relative links are clickable on the configured host
    /// and in-page bookmarks are reduced to plain text.
    /// </summary>
    /// <param name="markdown">The markdown content to normalize.</param>
    /// <param name="llmsTxtUrl">The configured llms.txt URL.</param>
    /// <returns>The markdown with rewritten link targets.</returns>
    public static string RewriteMarkdownLinks(string markdown, string llmsTxtUrl)
    {
        if (string.IsNullOrWhiteSpace(markdown))
        {
            return markdown;
        }
 
        var siteRoot = Uri.TryCreate(llmsTxtUrl, UriKind.Absolute, out var llmsUri)
            ? llmsUri.GetLeftPart(UriPartial.Authority).TrimEnd('/')
            : null;
 
        return MarkdownLinkRegex().Replace(markdown, match =>
        {
            var href = NormalizeMarkdownHref(match.Groups["href"].Value);
            var text = match.Groups["text"].Value;
 
            if (string.IsNullOrEmpty(href))
            {
                return match.Value;
            }
 
            return href[0] switch
            {
                '#' => text,
                '/' when siteRoot is not null => $"[{text}]({siteRoot}{href})",
                _ => match.Value
            };
        });
    }
 
    private static string NormalizeMarkdownHref(string href)
    {
        href = href.Trim();
        if (href.Length > 1 && href[0] is '<' && href[^1] is '>')
        {
            href = href[1..^1];
        }
 
        var titleSeparatorIndex = href.IndexOf(' ');
        return titleSeparatorIndex > 0
            ? href[..titleSeparatorIndex]
            : href;
    }
 
    [GeneratedRegex(@"(?<!!)\[(?<text>(?:[^\[\]]|\[[^\[\]]*\])+)\]\((?<href>[^)]*)\)")]
    private static partial Regex MarkdownLinkRegex();
}