You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写自定义Markdig扩展以修改图片路径

自定义Markdig扩展修改图片路径的正确实现方法

需求

希望将Markdown中的图片路径自动添加前缀,例如:
原Markdown:

![text](test.gif)

转换后的HTML应为:

<img src="images/test.gif" alt="text">

而非默认的:

<img src="test.gif" alt="text">

当前可行方案(手动遍历文档)

可以通过解析后遍历文档节点实现需求,代码如下:

string markdown = "![text](test.gif)";

MarkdownPipeline pipeline = new MarkdownPipelineBuilder().Build();

MarkdownDocument document = Markdown.Parse(markdown, pipeline);

foreach (LinkInline link in document.Descendants<LinkInline>())
{
    if (link.IsImage)
    {
        link.Url = "images/" + link.Url;
    }                
}

string html = document.ToHtml(pipeline);

问题:如何通过Markdig扩展实现?

尝试编写自定义扩展但无法正常工作,以下是尝试的代码:

public class CustomImagePath : LeafInline
{
    public string? OriginalPath { get; set; }
}

public class CustomImagePathExtension : IMarkdownExtension
{
    private readonly CustomImagePathOptions _options;

    public CustomImagePathExtension(CustomImagePathOptions options)
    {
        _options = options;
    }

    public void Setup(MarkdownPipelineBuilder pipeline)
    {
        OrderedList<InlineParser> parsers;

        parsers = pipeline.InlineParsers;

        if (!parsers.Contains<CustomImagePathInlineParser>())
        {
            parsers.Add(new CustomImagePathInlineParser());
        }
    }

    public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
    {
        HtmlRenderer? htmlRenderer;
        ObjectRendererCollection? renderers;

        htmlRenderer = renderer as HtmlRenderer;
        renderers = htmlRenderer?.ObjectRenderers;

        if (renderers != null && !renderers.Contains<CustomImagePathRenderer>())
        {
            renderers.Add(new CustomImagePathRenderer(_options));
        }
    }
}

public static class CustomImagePathExtensionFunctions
{
    public static MarkdownPipelineBuilder UseCustomImagePath(this MarkdownPipelineBuilder pipeline, CustomImagePathOptions options)
    {
        OrderedList<IMarkdownExtension> extensions;

        extensions = pipeline.Extensions;

        if (!extensions.Contains<CustomImagePathExtension>())
        {
            extensions.Add(new CustomImagePathExtension(options));
        }

        return pipeline;
    }
}

public class CustomImagePathInlineParser : InlineParser
{
    private static readonly char[] _openingCharacters =
    {
        '!'
    };

    public CustomImagePathInlineParser()
    {
        OpeningCharacters = _openingCharacters;
    }

    public override bool Match(InlineProcessor processor, ref StringSlice slice)
    {
        // Check if the current character is the start of an image
        if (slice.CurrentChar == '!' && !char.IsLetterOrDigit(slice.PeekCharExtra(-1)))
        {                
            // Move the slice position to skip the '!'
            slice.NextChar();

            // Delegate the parsing to the base LinkInlineParser
            var result = Match(processor, ref slice);
            if (result && processor.Inline is LinkInline linkInline)
            {
                // Check if it's an image link
                if (linkInline.IsImage)
                {
                    // Modify the image path
                    var imagePath = "images/" + linkInline.Url;

                    // Update the link inline with the modified path
                    linkInline.Url = imagePath;
                }
            }

            return result;
        }

        return false;
    }
}

public class CustomImagePathRenderer : HtmlObjectRenderer<LinkInline>
{
    private readonly CustomImagePathOptions _options;

    public CustomImagePathRenderer(CustomImagePathOptions options)
    {
        _options = options;
    }

    protected override void Write(HtmlRenderer renderer, LinkInline link)
    {
        if (link.IsImage)
        {
            // Check if the image inline is our custom inline
            if (link.FirstChild is CustomImagePath customImage)
            {
                // Modify the image path before rendering
                var imagePath = "images/" + customImage.OriginalPath;

                // Render the modified image path
                renderer.Write("<img src=\"");
                renderer.WriteEscapeUrl(imagePath);
                renderer.Write("\"");

                renderer.WriteAttributes(link);

                renderer.Write("/>");
                return;
            }
        }
        
        renderer.Write(link);
    }
}

// 使用示例
string markdown = "![text](test.gif)";

MarkdownPipeline pipeline = new MarkdownPipelineBuilder()
  .UseCustomImagePath(new CustomImagePathOptions())
  .Build();

string html = Markdown.ToHtml(markdown, pipeline);

正确实现方案

不需要自定义新的LeafInline类型或重写解析器,通过以下两种简洁方式即可实现需求:

方式1:通过文档处理器在解析后修改节点

逻辑与手动遍历一致,但集成到扩展中,无需手动处理文档节点:

// 配置选项类
public class CustomImagePathOptions
{
    public string ImagePrefix { get; set; } = "images/";
}

// 扩展类
public class CustomImagePathExtension : IMarkdownExtension
{
    private readonly CustomImagePathOptions _options;

    public CustomImagePathExtension(CustomImagePathOptions options)
    {
        _options = options;
    }

    public void Setup(MarkdownPipelineBuilder pipeline)
    {
        // 添加文档处理器,解析完成后自动处理节点
        pipeline.DocumentProcessors.Add(new CustomImagePathProcessor(_options));
    }

    public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
    {
        // 此方式不需要修改渲染器,留空即可
    }
}

// 文档处理器
public class CustomImagePathProcessor : IDocumentProcessor
{
    private readonly CustomImagePathOptions _options;

    public CustomImagePathProcessor(CustomImagePathOptions options)
    {
        _options = options;
    }

    public void Process(MarkdownDocument document, IMarkdownRenderer renderer)
    {
        // 遍历所有图片链接节点
        foreach (var link in document.Descendants<LinkInline>().Where(l => l.IsImage))
        {
            // 避免重复添加前缀
            if (!link.Url.StartsWith(_options.ImagePrefix, StringComparison.OrdinalIgnoreCase))
            {
                link.Url = _options.ImagePrefix + link.Url;
            }
        }
    }

    public void Setup(MarkdownPipeline pipeline, MarkdownDocument document)
    {
        // 无需额外初始化
    }
}

// 扩展方法
public static class CustomImagePathExtensionExtensions
{
    public static MarkdownPipelineBuilder UseCustomImagePath(this MarkdownPipelineBuilder pipeline, CustomImagePathOptions options = null)
    {
        options ??= new CustomImagePathOptions();
        pipeline.Extensions.Add(new CustomImagePathExtension(options));
        return pipeline;
    }
}

方式2:自定义渲染器修改输出HTML

如果不想修改文档节点,可在渲染阶段直接修改HTML输出:

// 配置选项类
public class CustomImagePathOptions
{
    public string ImagePrefix { get; set; } = "images/";
}

// 扩展类
public class CustomImagePathExtension : IMarkdownExtension
{
    private readonly CustomImagePathOptions _options;

    public CustomImagePathExtension(CustomImagePathOptions options)
    {
        _options = options;
    }

    public void Setup(MarkdownPipelineBuilder pipeline)
    {
        // 此方式不需要修改解析器,留空即可
    }

    public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
    {
        if (renderer is HtmlRenderer htmlRenderer)
        {
            // 移除默认的LinkInline渲染器,添加自定义渲染器
            var defaultRenderer = htmlRenderer.ObjectRenderers.FindExact<HtmlObjectRenderer<LinkInline>>();
            if (defaultRenderer != null)
            {
                htmlRenderer.ObjectRenderers.Remove(defaultRenderer);
            }
            htmlRenderer.ObjectRenderers.Add(new CustomImageLinkRenderer(_options));
        }
    }
}

// 自定义LinkInline渲染器
public class CustomImageLinkRenderer : HtmlObjectRenderer<LinkInline>
{
    private readonly CustomImagePathOptions _options;

    public CustomImageLinkRenderer(CustomImagePathOptions options)
    {
        _options = options;
    }

    protected override void Write(HtmlRenderer renderer, LinkInline link)
    {
        if (link.IsImage)
        {
            // 处理图片链接,添加前缀
            string imageUrl = link.Url;
            if (!imageUrl.StartsWith(_options.ImagePrefix, StringComparison.OrdinalIgnoreCase))
            {
                imageUrl = _options.ImagePrefix + imageUrl;
            }

            // 渲染img标签
            renderer.Write("<img src=\"");
            renderer.WriteEscapeUrl(imageUrl);
            renderer.Write("\" alt=\"");
            renderer.WriteEscape(link.FirstChild?.ToString() ?? string.Empty);
            renderer.WriteAttributes(link);
            renderer.Write("/>");
        }
        else
        {
            // 非图片链接使用默认渲染逻辑
            renderer.Write("<a href=\"");
            renderer.WriteEscapeUrl(link.Url);
            renderer.Write("\"");
            renderer.WriteAttributes(link);
            renderer.Write(">");
            renderer.WriteChildren(link);
            renderer.Write("</a>");
        }
    }
}

// 扩展方法
public static class CustomImagePathExtensionExtensions
{
    public static MarkdownPipelineBuilder UseCustomImagePath(this MarkdownPipelineBuilder pipeline, CustomImagePathOptions options = null)
    {
        options ??= new CustomImagePathOptions();
        pipeline.Extensions.Add(new CustomImagePathExtension(options));
        return pipeline;
    }
}

使用示例

两种方式的使用方法完全一致:

string markdown = "![text](test.gif)";

var pipeline = new MarkdownPipelineBuilder()
    .UseCustomImagePath(new CustomImagePathOptions { ImagePrefix = "images/" })
    .Build();

string html = Markdown.ToHtml(markdown, pipeline);
// 输出:<img src="images/test.gif" alt="text" />

内容的提问来源于stack exchange,提问作者pidela

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 07:54:59