File: Model\Markdown\MarkdownHelpers.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
 
namespace Aspire.Dashboard.Model.Markdown;
 
public static class MarkdownHelpers
{
    // These URL schemes are considered always safe to be included in Markdown received from untrusted 3rd parties, e.g. GenAI.
    // Untrusted 3rd party Markdown should be limited to these URLs to avoid links that could trigger other programs that listen for the scheme.
    public static readonly HashSet<string> SafeUrlSchemes = new HashSet<string>(StringComparers.EndpointAnnotationUriScheme) { "http", "https", "mailto" };
 
    public static string ToHtml(string markdown, MarkdownOptions options)
    {
        var document = Markdig.Markdown.Parse(markdown, options.Pipeline);
 
        // Open absolute links in the response in a new window.
        foreach (var link in document.Descendants<LinkInline>())
        {
            switch (DetectLink(link.Url, options.AllowedUrlSchemes))
            {
                case LinkType.Absolute:
                    AddLinkAttributes(link);
                    break;
                case LinkType.Prohibited:
                    link.Url = string.Empty;
                    break;
            }
        }
        foreach (var link in document.Descendants<AutolinkInline>())
        {
            switch (DetectLink(link.Url, options.AllowedUrlSchemes))
            {
                case LinkType.Absolute:
                    AddLinkAttributes(link);
                    break;
                case LinkType.Prohibited:
                    link.Url = string.Empty;
                    break;
            }
        }
 
        // Adjust heading levels so that they are appropriate for embedding in a page.
        // Markdown can easily contain h2/h3 headings, which would be greater than the dialog title header.
        // This change ensures all headings are h4 or smaller.
        foreach (var heading in document.Descendants<HeadingBlock>())
        {
            heading.Level = heading.Level switch
            {
                1 => 4,
                2 => 4,
                3 => 5,
                4 => 5,
                5 => 6,
                _ => 6
            };
        }
 
        var writer = new StringWriter();
        var renderer = new HtmlRenderer(writer);
        if (options.SuppressSurroundingParagraph)
        {
            // markdig won't render a surrounding paragraph if HtmlRenderer.ImplicitParagraph is true.
            // The naming is odd, but I think the idea is we're telling the renderer that there is an implicit paragraph
            // around the content and so renderer doesn't need to add one.
            renderer.ImplicitParagraph = true;
        }
 
        options.Pipeline.Setup(renderer);
 
        renderer.Render(document); // using the renderer directly
        writer.Flush();
 
        return writer.ToString();
 
        static LinkType DetectLink(string? url, HashSet<string>? allowedUrlSchemes)
        {
            if (url == null || !Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
            {
                return LinkType.None;
            }
 
            if (!uri.IsAbsoluteUri)
            {
                return LinkType.Relative;
            }
 
            if (allowedUrlSchemes != null)
            {
                if (!allowedUrlSchemes.Contains(uri.Scheme))
                {
                    return LinkType.Prohibited;
                }
            }
            else
            {
                // Even if no allowed schemes are specified, always block "javascript:" links.
                if (string.Equals(uri.Scheme, "javascript", StringComparison.OrdinalIgnoreCase))
                {
                    return LinkType.Prohibited;
                }
            }
 
            return LinkType.Absolute;
        }
 
        static void AddLinkAttributes(IMarkdownObject link)
        {
            var attributes = link.GetAttributes();
 
            attributes.AddPropertyIfNotExist("target", "_blank");
            attributes.AddPropertyIfNotExist("rel", "noopener noreferrer nofollow");
        }
    }
 
    public static bool GetSuppressSurroundingParagraph(string markdown, bool suppressParagraphOnNewLines)
    {
        // Avoid adding paragraphs to HTML output from Markdown content unless there are multiple lines (aka multiple paragraphs).
        return suppressParagraphOnNewLines && !(markdown.Contains('\n') || markdown.Contains('\r'));
    }
 
    private enum LinkType
    {
        None,
        Relative,
        Absolute,
        Prohibited
    }
}