如何编写自定义Markdig扩展以修改图片路径
自定义Markdig扩展修改图片路径的正确实现方法
需求
希望将Markdown中的图片路径自动添加前缀,例如:
原Markdown:

转换后的HTML应为:
<img src="images/test.gif" alt="text">
而非默认的:
<img src="test.gif" alt="text">
当前可行方案(手动遍历文档)
可以通过解析后遍历文档节点实现需求,代码如下:
string markdown = ""; 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 = ""; 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 = ""; 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
相关产品推荐
相关产品推荐

