79527995

Date: 2025-03-22 19:31:39
Score: 0.5
Natty:
Report link

I want to write a custom markdig extension to change the path of the images. ... I'm curious how to do it via an extension.

There are three places where you can insert code that executes during the Markdig process of converting a Markdown string into an HTML string:

  1. During the process of parsing characters in the markdown text.

  2. After the whole Markdown text has been parsed into an AST node tree (represented by the MarkdownDocument object), but before rendering into HTML. (You may see this referred to as post-processing.)

  3. During the process of converting AST nodes into HTML.

The code in the original post along with @Jinjinov's answer is #2: post-processing. document.Descendants<LinkInline>() returns an IEnumerable of AST nodes of type LinkInline.

The CodeProject article referenced in the original post is inserting code into 1. Character processing and 3. Rendering nodes as HTML. As @Jinjinov points out, for what you're attempting to do ("change the path of images"), his answer for creating an extension is best solution. It's quick and simple.

If/When your need goes beyond changing paths to include things like adding/modifying HTML attributes of the <img> HTML, then adding an extension that executes during the rendering process (#3 above) is the solution.

The MediaLinks extension in the Markdig GitHub project provides a great and simple starting point. The extension executes during the rendering process and adds HTML attributes, among other things.

The following class adapts methods in the MediaLinkExtension.cs class file to process image nodes. It does two things:

  1. It modifies the URL.
  2. If the URL is an absolute path -- that is, starts with http: -- then the target and rel attributes are added. The use case scenario for this allows for a CSS style rule that renders an external link icon after absolute URL paths versus relative URLs that point to the Web app's storage service.
a[target=_blank] {
    display: grid;
    grid-auto-flow: column;
    justify-content: start;
    gap: .5rem;
}
    /* New tab icon */
    a[target=_blank]::after {
        content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M 19 6.41 L 8.7 16.71 a 1 1 0 1 1 -1.4 -1.42 L 17.58 5 H 14 a 1 1 0 0 1 0 -2 h 6 a 1 1 0 0 1 1 1 v 6 a 1 1 0 0 1 -2 0 V 6.41 Z M 17 14 a 1 1 0 0 1 2 0 v 5 a 2 2 0 0 1 -2 2 H 5 a 2 2 0 0 1 -2 -2 V 7 c 0 -1.1 0.9 -2 2 -2 h 5 a 1 1 0 0 1 0 2 H 5 v 12 h 12 v -5 Z" /></svg>');
        display: grid;
        width: 1rem;
    }

Image extension class

using Markdig;
using Markdig.Helpers;
using Markdig.Renderers;
using Markdig.Renderers.Html;
using Markdig.Renderers.Html.Inlines;
using Markdig.Syntax.Inlines;

namespace markdown.Image
{
    public class ImageOptions
    {
        public string? BlobPath { get; set; }
        public bool? OpenExternalLinksInNewTab { get; set; } = false;
    }

    public static class ImagePipelineBuilder
    {
        public static MarkdownPipelineBuilder UseImageBuilder(
            this MarkdownPipelineBuilder pipeline,
            ImageOptions? imageOptions = null)
        {
            OrderedList<IMarkdownExtension> extensions = pipeline.Extensions;

            if (!extensions.Contains<ImageExtension>())
            {
                extensions.Add(new ImageExtension(imageOptions));
            }

            return pipeline;
        }
    }

    public class ImageExtension(ImageOptions? options) : IMarkdownExtension
    {
        public void Setup(MarkdownPipelineBuilder pipeline)
        {
            // Not needed
        }

        // 'Setup()' method adapted from:
        // github.com/xoofx/markdig/blob/master/src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs
        public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
        {
            if (renderer is HtmlRenderer htmlRenderer)
            {
                LinkInlineRenderer? inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();

                if (inlineRenderer != null)
                {
                    inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
                    inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
                }
            }
        }

        // `TryLinkInlineRenderer()` method adapted from the method with the
        // same name in the C# class file for the Markdig media link extension:
        // github.com/xoofx/markdig/blob/master/src/Markdig/Extensions/MediaLinks/MediaLinkExtension.cs
        private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
        {
            // If the link is `null`, then return
            if (linkInline.Url is null) { return false; }

            // If the link is an absolute path and the option of opening
            // absolute paths in a new window is set to `true`, then append
            // the `target` and `rel` attributes.
            bool openInNewWindow = options?.OpenExternalLinksInNewTab ?? false;
            if (linkInline.Url.StartsWith("http") && openInNewWindow)
            {
                HtmlAttributes attributes = linkInline.TryGetAttributes() ?? new HtmlAttributes();
                attributes.AddPropertyIfNotExist("target", "_blank");
                // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
                attributes.AddPropertyIfNotExist("rel", "external nofollow");
                linkInline.SetAttributes(attributes);
                return false;
            }

            // If the link is for an image, then append the blob path
            // to the `Url` property. Example:
            // ![Alt text](hero.jpg "Title")
            // ![Alt text](https://127.0.0.1:10000/devstoreaccount1/images/hero.jpg "Title")
            if (linkInline.IsImage)
            {
                // If the blob path is empty or null, then return
                if (string.IsNullOrWhiteSpace(options?.BlobPath)) { return false; }

                // Assign the modified link to the 'Url' property
                linkInline.Url = HtmlHelper.Unescape($"{options?.BlobPath}/{linkInline.Url}");
            }

            return false;
        }
    }
}
Reasons:
  • RegEx Blacklisted phrase (1): I want
  • Long answer (-1):
  • Has code block (-0.5):
  • User mentioned (1): @Jinjinov's
  • User mentioned (0): @Jinjinov
Posted by: Dave B