diff --git a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs index b40919e52..cf5125480 100644 --- a/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs +++ b/src/Markdig.Tests/RoundtripSpecs/Inlines/TestNullCharacterInline.cs @@ -22,16 +22,15 @@ public void Test(string value, string expected) // do not unintentionally use the expected parameter private static void RoundTrip(string markdown, string expected) { - var pipelineBuilder = new MarkdownPipelineBuilder(); - pipelineBuilder.EnableTrackTrivia(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); - MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var rr = new RoundtripRenderer(sw); + var pipeline = new MarkdownPipelineBuilder() + .EnableTrackTrivia() + .ConfigureRoundtripRenderer() + .Build(); - rr.Write(markdownDocument); + MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); + var result = Markdown.ToHtml(markdownDocument, pipeline); - Assert.AreEqual(expected, sw.ToString()); + Assert.AreEqual(expected, result); } } } diff --git a/src/Markdig.Tests/TestLinkRewriter.cs b/src/Markdig.Tests/TestLinkRewriter.cs index fa2ea1df4..dcaaaf7de 100644 --- a/src/Markdig.Tests/TestLinkRewriter.cs +++ b/src/Markdig.Tests/TestLinkRewriter.cs @@ -24,17 +24,13 @@ public void ReplacesRelativeImageSources() public static void TestSpec(Func linkRewriter, string markdown, string expectedLink) { - var pipeline = new MarkdownPipelineBuilder().Build(); - - var writer = new StringWriter(); - var renderer = new HtmlRenderer(writer); - renderer.LinkRewriter = linkRewriter; - pipeline.Setup(renderer); + var pipeline = new MarkdownPipelineBuilder() + .ConfigureHtmlRenderer(r => r.UseLinkRewriter(linkRewriter)) + .Build(); var document = MarkdownParser.Parse(markdown, pipeline); - renderer.Render(document); - writer.Flush(); + var html = Markdown.ToHtml(document, pipeline); - Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\"")); + Assert.That(html, Contains.Substring("=\"" + expectedLink + "\"")); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestRelativeUrlReplacement.cs b/src/Markdig.Tests/TestRelativeUrlReplacement.cs index c13ef18d0..43f249a38 100644 --- a/src/Markdig.Tests/TestRelativeUrlReplacement.cs +++ b/src/Markdig.Tests/TestRelativeUrlReplacement.cs @@ -27,18 +27,13 @@ public void ReplacesRelativeImageSources() public static void TestSpec(string baseUrl, string markdown, string expectedLink) { - var pipeline = new MarkdownPipelineBuilder().Build(); - - var writer = new StringWriter(); - var renderer = new HtmlRenderer(writer); - if (baseUrl != null) - renderer.BaseUrl = new Uri(baseUrl); - pipeline.Setup(renderer); + var pipeline = new MarkdownPipelineBuilder() + .ConfigureHtmlRenderer(b => b.UseBaseUrl(baseUrl)) + .Build(); var document = MarkdownParser.Parse(markdown, pipeline); - renderer.Render(document); - writer.Flush(); + var html = Markdown.ToHtml(document, pipeline); - Assert.That(writer.ToString(), Contains.Substring("=\"" + expectedLink + "\"")); + Assert.That(html, Contains.Substring("=\"" + expectedLink + "\"")); } } \ No newline at end of file diff --git a/src/Markdig.Tests/TestRoundtrip.cs b/src/Markdig.Tests/TestRoundtrip.cs index 24868b6bf..a39a733f7 100644 --- a/src/Markdig.Tests/TestRoundtrip.cs +++ b/src/Markdig.Tests/TestRoundtrip.cs @@ -12,18 +12,14 @@ internal static void TestSpec(string markdownText, string expected, string exten internal static void RoundTrip(string markdown, string context = null) { - var pipelineBuilder = new MarkdownPipelineBuilder(); - pipelineBuilder.EnableTrackTrivia(); - pipelineBuilder.UseYamlFrontMatter(); - MarkdownPipeline pipeline = pipelineBuilder.Build(); + var pipeline = new MarkdownPipelineBuilder() + .EnableTrackTrivia() + .UseYamlFrontMatter() + .ConfigureRoundtripRenderer() + .Build(); MarkdownDocument markdownDocument = Markdown.Parse(markdown, pipeline); - var sw = new StringWriter(); - var nr = new RoundtripRenderer(sw); - pipeline.Setup(nr); - nr.Write(markdownDocument); - - var result = sw.ToString(); + var result = Markdown.ToHtml(markdownDocument, pipeline); TestParser.PrintAssertExpected("", result, markdown, context); } } diff --git a/src/Markdig/Markdown.cs b/src/Markdig/Markdown.cs index 53dfdb785..4ecb2ce99 100644 --- a/src/Markdig/Markdown.cs +++ b/src/Markdig/Markdown.cs @@ -117,7 +117,7 @@ public static string ToHtml(this MarkdownDocument document, MarkdownPipeline? pi pipeline ??= DefaultPipeline; using var rentedRenderer = pipeline.RentHtmlRenderer(); - HtmlRenderer renderer = rentedRenderer.Instance; + var renderer = rentedRenderer.Instance; renderer.Render(document); renderer.Writer.Flush(); @@ -141,7 +141,7 @@ public static void ToHtml(this MarkdownDocument document, TextWriter writer, Mar pipeline ??= DefaultPipeline; using var rentedRenderer = pipeline.RentHtmlRenderer(writer); - HtmlRenderer renderer = rentedRenderer.Instance; + var renderer = rentedRenderer.Instance; renderer.Render(document); renderer.Writer.Flush(); @@ -166,7 +166,7 @@ public static MarkdownDocument ToHtml(string markdown, TextWriter writer, Markdo var document = MarkdownParser.Parse(markdown, pipeline, context); using var rentedRenderer = pipeline.RentHtmlRenderer(writer); - HtmlRenderer renderer = rentedRenderer.Instance; + var renderer = rentedRenderer.Instance; renderer.Render(document); writer.Flush(); diff --git a/src/Markdig/MarkdownExtensions.cs b/src/Markdig/MarkdownExtensions.cs index 185cb3b5f..4f6221d98 100644 --- a/src/Markdig/MarkdownExtensions.cs +++ b/src/Markdig/MarkdownExtensions.cs @@ -36,6 +36,8 @@ using Markdig.Parsers; using Markdig.Parsers.Inlines; using Markdig.Renderers; +using Markdig.Renderers.Normalize; +using Markdig.Renderers.Roundtrip; namespace Markdig; @@ -108,7 +110,7 @@ public static MarkdownPipelineBuilder UseAlertBlocks(this MarkdownPipelineBuilde pipeline.Extensions.ReplaceOrAdd(new AlertExtension() { RenderKind = renderKind }); return pipeline; } - + /// /// Uses this extension to enable autolinks from text `http://`, `https://`, `ftp://`, `mailto:`, `www.xxx.yyy` /// @@ -708,4 +710,44 @@ public static MarkdownPipelineBuilder EnableTrackTrivia(this MarkdownPipelineBui } return pipeline; } + + /// + /// Configure the pipeline with a . + /// + /// The pipeline. + /// An action which configures the HtmlRenderer. + public static MarkdownPipelineBuilder ConfigureHtmlRenderer( + this MarkdownPipelineBuilder pipeline, + Func configureRenderer) + => pipeline.UseRendererBuilder(configureRenderer(new HtmlRendererBuilder())); + + /// + /// Configure the pipeline with a . + /// + /// The pipeline. + /// An action which configures the NormalizeRenderer. + public static MarkdownPipelineBuilder ConfigureNormalizeRenderer( + this MarkdownPipelineBuilder pipeline, + Func configureRenderer) + => pipeline.UseRendererBuilder(configureRenderer(new NormalizeRendererBuilder())); + + /// + /// Configure the pipeline with a . + /// + /// The pipeline. + public static MarkdownPipelineBuilder ConfigureRoundtripRenderer(this MarkdownPipelineBuilder pipeline) + => pipeline.UseRendererBuilder(new RoundtripRendererBuilder()); + + /// + /// Configure the pipeline to use a to construct the renderer. + /// + /// The pipeline. + /// The builder for the renderer. + public static MarkdownPipelineBuilder UseRendererBuilder( + this MarkdownPipelineBuilder pipeline, + IMarkdownRendererBuilder rendererBuilder) + { + pipeline.RendererBuilder = rendererBuilder; + return pipeline; + } } diff --git a/src/Markdig/MarkdownPipeline.cs b/src/Markdig/MarkdownPipeline.cs index d96675005..43b348560 100644 --- a/src/Markdig/MarkdownPipeline.cs +++ b/src/Markdig/MarkdownPipeline.cs @@ -1,5 +1,5 @@ // Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. +// This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. using System.IO; @@ -25,7 +25,8 @@ internal MarkdownPipeline( BlockParserList blockParsers, InlineParserList inlineParsers, TextWriter? debugLog, - ProcessDocumentDelegate? documentProcessed) + ProcessDocumentDelegate? documentProcessed, + IMarkdownRendererBuilder? rendererBuilder) { if (blockParsers is null) ThrowHelper.ArgumentNullException(nameof(blockParsers)); if (inlineParsers is null) ThrowHelper.ArgumentNullException(nameof(inlineParsers)); @@ -35,6 +36,7 @@ internal MarkdownPipeline( InlineParsers = inlineParsers; DebugLog = debugLog; DocumentProcessed = documentProcessed; + RendererBuilder = rendererBuilder; SelfPipeline = Extensions.Find(); } @@ -57,6 +59,8 @@ internal MarkdownPipeline( internal SelfPipelineExtension? SelfPipeline; + internal IMarkdownRendererBuilder? RendererBuilder; + /// /// True to parse trivia such as whitespace, extra heading characters and unescaped /// string values. @@ -82,10 +86,10 @@ public void Setup(IMarkdownRenderer renderer) internal RentedHtmlRenderer RentHtmlRenderer(TextWriter? writer = null) { HtmlRendererCache cache = writer is null - ? _rendererCache ??= new HtmlRendererCache(this, customWriter: false) - : _rendererCacheForCustomWriter ??= new HtmlRendererCache(this, customWriter: true); + ? _rendererCache ??= new HtmlRendererCache(this, customWriter: false, RendererBuilder) + : _rendererCacheForCustomWriter ??= new HtmlRendererCache(this, customWriter: true, RendererBuilder); - HtmlRenderer renderer = cache.Get(); + TextRendererBase renderer = cache.Get(); if (writer is not null) { @@ -97,22 +101,26 @@ internal RentedHtmlRenderer RentHtmlRenderer(TextWriter? writer = null) internal sealed class HtmlRendererCache( MarkdownPipeline pipeline, - bool customWriter = false) : ObjectCache + bool customWriter = false, + IMarkdownRendererBuilder? rendererBuilder = null) : ObjectCache { private static readonly FastStringWriter s_dummyWriter = new(); private readonly MarkdownPipeline _pipeline = pipeline; private readonly bool _customWriter = customWriter; - protected override HtmlRenderer NewInstance() + private readonly IMarkdownRendererBuilder RendererBuilder = + rendererBuilder ?? new HtmlRendererBuilder(); + + protected override TextRendererBase NewInstance() { TextWriter writer = _customWriter ? s_dummyWriter : new FastStringWriter(); - var renderer = new HtmlRenderer(writer); + var renderer = RendererBuilder.Build(writer); _pipeline.Setup(renderer); return renderer; } - protected override void Reset(HtmlRenderer instance) + protected override void Reset(TextRendererBase instance) { instance.ResetInternal(); @@ -130,9 +138,9 @@ protected override void Reset(HtmlRenderer instance) internal readonly struct RentedHtmlRenderer : IDisposable { private readonly HtmlRendererCache _cache; - public readonly HtmlRenderer Instance; + public readonly TextRendererBase Instance; - internal RentedHtmlRenderer(HtmlRendererCache cache, HtmlRenderer renderer) + internal RentedHtmlRenderer(HtmlRendererCache cache, TextRendererBase renderer) { _cache = cache; Instance = renderer; diff --git a/src/Markdig/MarkdownPipelineBuilder.cs b/src/Markdig/MarkdownPipelineBuilder.cs index 165365d82..2108a9c3d 100644 --- a/src/Markdig/MarkdownPipelineBuilder.cs +++ b/src/Markdig/MarkdownPipelineBuilder.cs @@ -1,5 +1,5 @@ // Copyright (c) Alexandre Mutel. All rights reserved. -// This file is licensed under the BSD-Clause 2 license. +// This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. using System.IO; @@ -7,6 +7,7 @@ using Markdig.Helpers; using Markdig.Parsers; using Markdig.Parsers.Inlines; +using Markdig.Renderers; namespace Markdig; @@ -89,6 +90,8 @@ public MarkdownPipelineBuilder() internal ProcessDocumentDelegate? GetDocumentProcessed => DocumentProcessed; + internal IMarkdownRendererBuilder? RendererBuilder; + /// /// Builds a pipeline from this instance. Once the pipeline is build, it cannot be modified. /// @@ -120,11 +123,12 @@ public MarkdownPipeline Build() new BlockParserList(BlockParsers), new InlineParserList(InlineParsers), DebugLog, - GetDocumentProcessed) + GetDocumentProcessed, + RendererBuilder) { PreciseSourceLocation = PreciseSourceLocation, TrackTrivia = TrackTrivia, }; return pipeline; } -} \ No newline at end of file +} diff --git a/src/Markdig/Renderers/HtmlRenderer.cs b/src/Markdig/Renderers/HtmlRenderer.cs index 085a4e971..74bd44189 100644 --- a/src/Markdig/Renderers/HtmlRenderer.cs +++ b/src/Markdig/Renderers/HtmlRenderer.cs @@ -48,7 +48,7 @@ public HtmlRenderer(TextWriter writer) : base(writer) ObjectRenderers.Add(new EmphasisInlineRenderer()); ObjectRenderers.Add(new LineBreakInlineRenderer()); ObjectRenderers.Add(new HtmlInlineRenderer()); - ObjectRenderers.Add(new HtmlEntityInlineRenderer()); + ObjectRenderers.Add(new HtmlEntityInlineRenderer()); ObjectRenderers.Add(new LinkInlineRenderer()); ObjectRenderers.Add(new LiteralInlineRenderer()); @@ -202,7 +202,16 @@ public HtmlRenderer WriteEscapeUrl(string? content) // this is the proper cross-platform way to check whether a uri is absolute or not: && Uri.TryCreate(content, UriKind.RelativeOrAbsolute, out var contentUri) && !contentUri.IsAbsoluteUri) { - content = new Uri(BaseUrl, contentUri).AbsoluteUri; + if (BaseUrl.IsAbsoluteUri) + content = new Uri(BaseUrl, contentUri).AbsoluteUri; + else + { + var baseUrl = BaseUrl.ToString(); + content = baseUrl; + if (!baseUrl.EndsWith("/")) + content += "/"; + content += contentUri.ToString(); + } } if (LinkRewriter != null) diff --git a/src/Markdig/Renderers/HtmlRendererBuilder.cs b/src/Markdig/Renderers/HtmlRendererBuilder.cs new file mode 100644 index 000000000..2f7ea688e --- /dev/null +++ b/src/Markdig/Renderers/HtmlRendererBuilder.cs @@ -0,0 +1,86 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System.IO; + +namespace Markdig.Renderers; + +/// +/// This class is used with +/// to set up a pipeline with a html renderer. +/// +public class HtmlRendererBuilder : IMarkdownRendererBuilder +{ + private Uri? baseUrl; + private bool? enableHtmlEscape; + private bool? enableHtmlForBlock; + private bool? enableHtmlForInline; + private bool? useNonAsciiNoEscape; + public Func? linkRewriter; + + public HtmlRenderer Build(TextWriter writer) + { + HtmlRenderer htmlRenderer = new HtmlRenderer(writer); + + if (baseUrl != null) htmlRenderer.BaseUrl = baseUrl; + if (enableHtmlEscape != null) htmlRenderer.EnableHtmlEscape = enableHtmlEscape.Value; + if (enableHtmlForBlock != null) htmlRenderer.EnableHtmlForBlock = enableHtmlForBlock.Value; + if (enableHtmlForInline != null) htmlRenderer.EnableHtmlForInline = enableHtmlForInline.Value; + if (useNonAsciiNoEscape != null) htmlRenderer.UseNonAsciiNoEscape = useNonAsciiNoEscape.Value; + if (linkRewriter != null) htmlRenderer.LinkRewriter = linkRewriter; + + return htmlRenderer; + } + + TextRendererBase IMarkdownRendererBuilder.Build(TextWriter writer) => Build(writer); + + /// + public HtmlRendererBuilder UseBaseUrl(string baseUrl) + { + this.baseUrl = baseUrl != null ? new Uri(baseUrl) : null; + return this; + } + + /// + public HtmlRendererBuilder UseBaseUrl(Uri baseUrl) + { + this.baseUrl = baseUrl; + return this; + } + + /// + public HtmlRendererBuilder EnableHtmlEscape(bool enable) + { + enableHtmlEscape = enable; + return this; + } + + /// + public HtmlRendererBuilder EnableHtmlForBlock(bool enable) + { + enableHtmlForBlock = enable; + return this; + } + + /// + public HtmlRendererBuilder EnableHtmlForInline(bool enable) + { + enableHtmlForInline = enable; + return this; + } + + /// + public HtmlRendererBuilder UseNonAsciiNoEscape(bool enable) + { + useNonAsciiNoEscape = enable; + return this; + } + + /// + public HtmlRendererBuilder UseLinkRewriter(Func linkRewriter) + { + this.linkRewriter = linkRewriter; + return this; + } +} diff --git a/src/Markdig/Renderers/IMarkdownRendererBuilder.cs b/src/Markdig/Renderers/IMarkdownRendererBuilder.cs new file mode 100644 index 000000000..21db70c6a --- /dev/null +++ b/src/Markdig/Renderers/IMarkdownRendererBuilder.cs @@ -0,0 +1,22 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System.IO; + +namespace Markdig.Renderers; + +/// +/// Defines a common interface for all rendererer builders. +/// +public interface IMarkdownRendererBuilder { + /// + /// Creates a new instance of a renderer for use in a pipeline. + /// + /// + /// The renderer needs to be a subclass of + /// because it is intended to be used by which depends on + /// for re-use. + /// + TextRendererBase Build(TextWriter writer); +} diff --git a/src/Markdig/Renderers/Normalize/NormalizeRendererBuilder.cs b/src/Markdig/Renderers/Normalize/NormalizeRendererBuilder.cs new file mode 100644 index 000000000..52bf2ed0b --- /dev/null +++ b/src/Markdig/Renderers/Normalize/NormalizeRendererBuilder.cs @@ -0,0 +1,66 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System.IO; + +namespace Markdig.Renderers.Normalize; + + +/// +/// This class is used with +/// to set up a pipeline with a normalizing renderer. +/// +public class NormalizeRendererBuilder : IMarkdownRendererBuilder +{ + private readonly Lazy options = new(() => new NormalizeOptions()); + + public NormalizeRenderer Build(TextWriter writer) + { + return new NormalizeRenderer(writer, options.IsValueCreated ? options.Value : null); + } + + TextRendererBase IMarkdownRendererBuilder.Build(TextWriter writer) => Build(writer); + + /// + public NormalizeRendererBuilder UseSpaceAfterQuoteBlock(bool enable) + { + options.Value.SpaceAfterQuoteBlock = enable; + return this; + } + + /// + public NormalizeRendererBuilder UseEmptyLineAfterCodeBlock(bool enable) + { + options.Value.EmptyLineAfterCodeBlock = enable; + return this; + } + + /// + public NormalizeRendererBuilder UseEmptyLineAfterHeading(bool enable) + { + options.Value.EmptyLineAfterHeading = enable; + return this; + } + + /// + public NormalizeRendererBuilder UseEmptyLineAfterThematicBreak(bool enable) + { + options.Value.EmptyLineAfterThematicBreak = enable; + return this; + } + + /// + public NormalizeRendererBuilder UseListItemCharacter(char? character) + { + options.Value.ListItemCharacter = character; + return this; + } + + /// + public NormalizeRendererBuilder ExpandAutoLinks(bool enable) + { + options.Value.ExpandAutoLinks = enable; + return this; + } +} diff --git a/src/Markdig/Renderers/Roundtrip/RoundtripRendererBuilder.cs b/src/Markdig/Renderers/Roundtrip/RoundtripRendererBuilder.cs new file mode 100644 index 000000000..89692c651 --- /dev/null +++ b/src/Markdig/Renderers/Roundtrip/RoundtripRendererBuilder.cs @@ -0,0 +1,23 @@ +// Copyright (c) Alexandre Mutel. All rights reserved. +// This file is licensed under the BSD-Clause 2 license. +// See the license.txt file in the project root for more information. + +using System.IO; + +namespace Markdig.Renderers.Roundtrip; + +/// +/// This class is used with +/// to set up a pipeline with a markdown renderer. +/// +/// +/// This builder has no options since has none. +/// It is solely in use internally in +/// because can only use a renderer builder, not a renderer. +/// +class RoundtripRendererBuilder : IMarkdownRendererBuilder +{ + public RoundtripRenderer Build(TextWriter writer) => new(writer); + + TextRendererBase IMarkdownRendererBuilder.Build(TextWriter writer) => Build(writer); +} \ No newline at end of file diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 4d428a770..9b61c4f6b 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -61,6 +61,12 @@ public override object Render(MarkdownObject markdownObject) Write(markdownObject); return Writer; } + + internal virtual void ResetInternal() + { + + } + } /// @@ -134,7 +140,7 @@ protected internal void Reset() ResetInternal(); } - internal void ResetInternal() + internal override void ResetInternal() { _childrenDepth = 0; previousWasLine = true;