From e36d4564f18579407c6e5bb3d81620c10a085663 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 01:53:20 +0100 Subject: [PATCH 01/11] Remove NormalizeAutoLinkRenderer --- .../Extensions/AutoLinks/AutoLinkExtension.cs | 6 ---- .../AutoLinks/NormalizeAutoLinkRenderer.cs | 33 ------------------- .../Normalize/Inlines/LinkInlineRenderer.cs | 6 ++++ 3 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 src/Markdig/Extensions/AutoLinks/NormalizeAutoLinkRenderer.cs diff --git a/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs b/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs index b06a510e1..7e70ba1a2 100644 --- a/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs +++ b/src/Markdig/Extensions/AutoLinks/AutoLinkExtension.cs @@ -3,8 +3,6 @@ // See the license.txt file in the project root for more information. using Markdig.Renderers; -using Markdig.Renderers.Normalize; -using Markdig.Renderers.Normalize.Inlines; using Markdig.Syntax.Inlines; namespace Markdig.Extensions.AutoLinks @@ -33,10 +31,6 @@ public void Setup(MarkdownPipelineBuilder pipeline) public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer) { - if (renderer is NormalizeRenderer normalizeRenderer && !normalizeRenderer.ObjectRenderers.Contains()) - { - normalizeRenderer.ObjectRenderers.InsertBefore(new NormalizeAutoLinkRenderer()); - } } } } \ No newline at end of file diff --git a/src/Markdig/Extensions/AutoLinks/NormalizeAutoLinkRenderer.cs b/src/Markdig/Extensions/AutoLinks/NormalizeAutoLinkRenderer.cs deleted file mode 100644 index 93afc995f..000000000 --- a/src/Markdig/Extensions/AutoLinks/NormalizeAutoLinkRenderer.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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 Markdig.Renderers; -using Markdig.Renderers.Normalize; -using Markdig.Syntax; -using Markdig.Syntax.Inlines; - -namespace Markdig.Extensions.AutoLinks -{ - public class NormalizeAutoLinkRenderer : NormalizeObjectRenderer - { - public override bool Accept(RendererBase renderer, MarkdownObject obj) - { - if (base.Accept(renderer, obj)) - { - return renderer is NormalizeRenderer normalizeRenderer - && obj is LinkInline link - && !normalizeRenderer.Options.ExpandAutoLinks - && link.IsAutoLink; - } - else - { - return false; - } - } - protected override void Write(NormalizeRenderer renderer, LinkInline obj) - { - renderer.Write(obj.Url); - } - } -} diff --git a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs index 9ca45d9ec..adfdb1879 100644 --- a/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Normalize/Inlines/LinkInlineRenderer.cs @@ -14,6 +14,12 @@ public class LinkInlineRenderer : NormalizeObjectRenderer { protected override void Write(NormalizeRenderer renderer, LinkInline link) { + if (link.IsAutoLink && !renderer.Options.ExpandAutoLinks) + { + renderer.Write(link.Url); + return; + } + if (link.IsImage) { renderer.Write('!'); From 14ab45cf8f7bf854c109376b5d47610ea134e6d0 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 01:56:48 +0100 Subject: [PATCH 02/11] Move TryWriters to cold path --- .../Renderers/MarkdownObjectRenderer.cs | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Markdig/Renderers/MarkdownObjectRenderer.cs b/src/Markdig/Renderers/MarkdownObjectRenderer.cs index 55579eda2..471fd9a85 100644 --- a/src/Markdig/Renderers/MarkdownObjectRenderer.cs +++ b/src/Markdig/Renderers/MarkdownObjectRenderer.cs @@ -15,10 +15,9 @@ namespace Markdig.Renderers /// public abstract class MarkdownObjectRenderer : IMarkdownObjectRenderer where TRenderer : RendererBase where TObject : MarkdownObject { - protected MarkdownObjectRenderer() - { - TryWriters = new OrderedList(); - } + private OrderedList? _tryWriters; + + protected MarkdownObjectRenderer() { } public delegate bool TryWriteDelegate(TRenderer renderer, TObject obj); @@ -32,23 +31,31 @@ public virtual void Write(RendererBase renderer, MarkdownObject obj) var htmlRenderer = (TRenderer)renderer; var typedObj = (TObject)obj; - // Try processing - for (int i = 0; i < TryWriters.Count; i++) + if (_tryWriters is not null && TryWrite(htmlRenderer, typedObj)) { - var tryWriter = TryWriters[i]; - if (tryWriter(htmlRenderer, typedObj)) - { - return; - } + return; } Write(htmlRenderer, typedObj); } + private bool TryWrite(TRenderer renderer, TObject obj) + { + for (int i = 0; i < _tryWriters!.Count; i++) + { + var tryWriter = _tryWriters[i]; + if (tryWriter(renderer, obj)) + { + return true; + } + } + return false; + } + /// /// Gets the optional writers attached to this instance. /// - public OrderedList TryWriters { get; } + public OrderedList TryWriters => _tryWriters ??= new(); /// /// Writes the specified Markdown object to the renderer. From cc04208b95f149af3f5d40ab6942e29bb4b25150 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 02:06:08 +0100 Subject: [PATCH 03/11] Change IMarkdownObjectRenderer.Accept to take a Type instead of instance --- src/Markdig/Renderers/IMarkdownObjectRenderer.cs | 5 +++-- src/Markdig/Renderers/MarkdownObjectRenderer.cs | 5 +++-- src/Markdig/Renderers/RendererBase.cs | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Markdig/Renderers/IMarkdownObjectRenderer.cs b/src/Markdig/Renderers/IMarkdownObjectRenderer.cs index 70eb8375d..8ecd1a356 100644 --- a/src/Markdig/Renderers/IMarkdownObjectRenderer.cs +++ b/src/Markdig/Renderers/IMarkdownObjectRenderer.cs @@ -3,6 +3,7 @@ // See the license.txt file in the project root for more information. using Markdig.Syntax; +using System; namespace Markdig.Renderers { @@ -15,9 +16,9 @@ public interface IMarkdownObjectRenderer /// Accepts the specified . /// /// The renderer. - /// The Markdown object. + /// The of the Markdown object. /// true If this renderer is accepting to render the specified Markdown object - bool Accept(RendererBase renderer, MarkdownObject obj); + bool Accept(RendererBase renderer, Type objectType); /// /// Writes the specified to the . diff --git a/src/Markdig/Renderers/MarkdownObjectRenderer.cs b/src/Markdig/Renderers/MarkdownObjectRenderer.cs index 471fd9a85..638d05c74 100644 --- a/src/Markdig/Renderers/MarkdownObjectRenderer.cs +++ b/src/Markdig/Renderers/MarkdownObjectRenderer.cs @@ -4,6 +4,7 @@ using Markdig.Helpers; using Markdig.Syntax; +using System; namespace Markdig.Renderers { @@ -21,9 +22,9 @@ public abstract class MarkdownObjectRenderer : IMarkdownObje public delegate bool TryWriteDelegate(TRenderer renderer, TObject obj); - public virtual bool Accept(RendererBase renderer, MarkdownObject obj) + public bool Accept(RendererBase renderer, Type objectType) { - return obj is TObject; + return typeof(TObject).IsAssignableFrom(objectType); } public virtual void Write(RendererBase renderer, MarkdownObject obj) diff --git a/src/Markdig/Renderers/RendererBase.cs b/src/Markdig/Renderers/RendererBase.cs index 35520a0d1..871afc702 100644 --- a/src/Markdig/Renderers/RendererBase.cs +++ b/src/Markdig/Renderers/RendererBase.cs @@ -141,7 +141,7 @@ public void Write(MarkdownObject obj) for (int i = 0; i < ObjectRenderers.Count; i++) { var testRenderer = ObjectRenderers[i]; - if (testRenderer.Accept(this, obj)) + if (testRenderer.Accept(this, objectType)) { renderersPerType[objectType] = renderer = testRenderer; break; From 260423976431a24e15d3862d4f2aba8c45c51b2b Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 02:12:35 +0100 Subject: [PATCH 04/11] Move TryGetRenderer to cold path --- src/Markdig/Renderers/RendererBase.cs | 54 +++++++++++++---------- src/Markdig/Renderers/TextRendererBase.cs | 2 +- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/Markdig/Renderers/RendererBase.cs b/src/Markdig/Renderers/RendererBase.cs index 871afc702..09d9c0ba7 100644 --- a/src/Markdig/Renderers/RendererBase.cs +++ b/src/Markdig/Renderers/RendererBase.cs @@ -16,10 +16,10 @@ namespace Markdig.Renderers /// public abstract class RendererBase : IMarkdownRenderer { - private readonly Dictionary renderersPerType; - private IMarkdownObjectRenderer? previousRenderer; - private Type? previousObjectType; - internal int childrenDepth = 0; + private readonly Dictionary _renderersPerType; + private IMarkdownObjectRenderer? _previousRenderer; + private Type? _previousObjectType; + internal int _childrenDepth = 0; /// /// Initializes a new instance of the class. @@ -27,7 +27,24 @@ public abstract class RendererBase : IMarkdownRenderer protected RendererBase() { ObjectRenderers = new ObjectRendererCollection(); - renderersPerType = new Dictionary(); + _renderersPerType = new Dictionary(); + } + + private IMarkdownObjectRenderer? TryGetRenderer(Type objectType) + { + for (int i = 0; i < ObjectRenderers.Count; i++) + { + var renderer = ObjectRenderers[i]; + if (renderer.Accept(this, objectType)) + { + _renderersPerType[objectType] = renderer; + _previousObjectType = objectType; + _previousRenderer = renderer; + return renderer; + } + } + + return null; } public ObjectRendererCollection ObjectRenderers { get; } @@ -59,7 +76,7 @@ public void WriteChildren(ContainerBlock containerBlock) return; } - ThrowHelper.CheckDepthLimit(childrenDepth++); + ThrowHelper.CheckDepthLimit(_childrenDepth++); bool saveIsFirstInContainer = IsFirstInContainer; bool saveIsLastInContainer = IsLastInContainer; @@ -75,7 +92,7 @@ public void WriteChildren(ContainerBlock containerBlock) IsFirstInContainer = saveIsFirstInContainer; IsLastInContainer = saveIsLastInContainer; - childrenDepth--; + _childrenDepth--; } /// @@ -89,7 +106,7 @@ public void WriteChildren(ContainerInline containerInline) return; } - ThrowHelper.CheckDepthLimit(childrenDepth++); + ThrowHelper.CheckDepthLimit(_childrenDepth++); bool saveIsFirstInContainer = IsFirstInContainer; bool saveIsLastInContainer = IsLastInContainer; @@ -110,7 +127,7 @@ public void WriteChildren(ContainerInline containerInline) IsFirstInContainer = saveIsFirstInContainer; IsLastInContainer = saveIsLastInContainer; - childrenDepth--; + _childrenDepth--; } /// @@ -132,29 +149,18 @@ public void Write(MarkdownObject obj) IMarkdownObjectRenderer? renderer; // Handle regular renderers - if (objectType == previousObjectType) + if (objectType == _previousObjectType) { - renderer = previousRenderer; + renderer = _previousRenderer; } - else if (!renderersPerType.TryGetValue(objectType, out renderer)) + else if (!_renderersPerType.TryGetValue(objectType, out renderer)) { - for (int i = 0; i < ObjectRenderers.Count; i++) - { - var testRenderer = ObjectRenderers[i]; - if (testRenderer.Accept(this, objectType)) - { - renderersPerType[objectType] = renderer = testRenderer; - break; - } - } + renderer = TryGetRenderer(objectType); } if (renderer != null) { renderer.Write(this, obj); - - previousObjectType = objectType; - previousRenderer = renderer; } else if (obj is ContainerBlock containerBlock) { diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 0ac1c2407..95755e562 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -137,7 +137,7 @@ protected internal void Reset() internal void ResetInternal() { - childrenDepth = 0; + _childrenDepth = 0; previousWasLine = true; indents.Clear(); } From 202ac1e4f919331dcae9c2ef5f1ab22f118b2916 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 02:36:02 +0100 Subject: [PATCH 05/11] Simplify RendererBase ctor --- src/Markdig/Renderers/TextRendererBase.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 95755e562..434aaf25d 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.CompilerServices; using Markdig.Helpers; @@ -18,7 +19,7 @@ namespace Markdig.Renderers /// public abstract class TextRendererBase : RendererBase { - private TextWriter writer; + private TextWriter _writer; /// /// Initializes a new instance of the class. @@ -27,9 +28,7 @@ public abstract class TextRendererBase : RendererBase /// protected TextRendererBase(TextWriter writer) { - if (writer is null) ThrowHelper.ArgumentNullException_writer(); - this.writer = writer; - this.writer.NewLine = "\n"; + Writer = writer; } /// @@ -38,8 +37,8 @@ protected TextRendererBase(TextWriter writer) /// if the value is null public TextWriter Writer { - get { return writer; } - set + get => _writer; + [MemberNotNull(nameof(_writer))] set { if (value is null) { @@ -48,7 +47,7 @@ public TextWriter Writer // By default we output a newline with '\n' only even on Windows platforms value.NewLine = "\n"; - writer = value; + _writer = value; } } From bb6ace15b7e179c57fffa03b77c1b60092ed2aa6 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 03:39:47 +0100 Subject: [PATCH 06/11] Optimize RendererBase.Write --- src/Markdig/Renderers/RendererBase.cs | 45 ++++++++++----------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/Markdig/Renderers/RendererBase.cs b/src/Markdig/Renderers/RendererBase.cs index 09d9c0ba7..c11733765 100644 --- a/src/Markdig/Renderers/RendererBase.cs +++ b/src/Markdig/Renderers/RendererBase.cs @@ -16,38 +16,34 @@ namespace Markdig.Renderers /// public abstract class RendererBase : IMarkdownRenderer { - private readonly Dictionary _renderersPerType; - private IMarkdownObjectRenderer? _previousRenderer; - private Type? _previousObjectType; + private readonly Dictionary _renderersPerType = new(); internal int _childrenDepth = 0; /// /// Initializes a new instance of the class. /// - protected RendererBase() - { - ObjectRenderers = new ObjectRendererCollection(); - _renderersPerType = new Dictionary(); - } + protected RendererBase() { } - private IMarkdownObjectRenderer? TryGetRenderer(Type objectType) + private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj) { + RuntimeTypeHandle typeHandle = Type.GetTypeHandle(obj); + Type objectType = obj.GetType(); + for (int i = 0; i < ObjectRenderers.Count; i++) { var renderer = ObjectRenderers[i]; if (renderer.Accept(this, objectType)) { - _renderersPerType[objectType] = renderer; - _previousObjectType = objectType; - _previousRenderer = renderer; + _renderersPerType[typeHandle] = renderer; return renderer; } } + _renderersPerType[typeHandle] = null; return null; } - public ObjectRendererCollection ObjectRenderers { get; } + public ObjectRendererCollection ObjectRenderers { get; } = new(); public abstract object Render(MarkdownObject markdownObject); @@ -144,32 +140,23 @@ public void Write(MarkdownObject obj) // Calls before writing an object ObjectWriteBefore?.Invoke(this, obj); - var objectType = obj.GetType(); - - IMarkdownObjectRenderer? renderer; - - // Handle regular renderers - if (objectType == _previousObjectType) - { - renderer = _previousRenderer; - } - else if (!_renderersPerType.TryGetValue(objectType, out renderer)) + if (!_renderersPerType.TryGetValue(Type.GetTypeHandle(obj), out IMarkdownObjectRenderer? renderer)) { - renderer = TryGetRenderer(objectType); + renderer = GetRendererInstance(obj); } - if (renderer != null) + if (renderer is not null) { renderer.Write(this, obj); } - else if (obj is ContainerBlock containerBlock) - { - WriteChildren(containerBlock); - } else if (obj is ContainerInline containerInline) { WriteChildren(containerInline); } + else if (obj is ContainerBlock containerBlock) + { + WriteChildren(containerBlock); + } // Calls after writing an object ObjectWriteAfter?.Invoke(this, obj); From f3d6c2775bcc2aebabc334d122151744df79cc1f Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 04:01:25 +0100 Subject: [PATCH 07/11] Add Unsafe.As polyfill for NETSTANDARD 2.1 --- src/Markdig/Parsers/BlockProcessor.cs | 28 +++---------------- src/Markdig/Parsers/InlineProcessor.cs | 4 --- .../Parsers/Inlines/EmphasisInlineParser.cs | 4 --- src/Markdig/Parsers/MarkdownParser.cs | 9 ------ src/Markdig/Polyfills/Unsafe.cs | 17 +++++++++++ 5 files changed, 21 insertions(+), 41 deletions(-) create mode 100644 src/Markdig/Polyfills/Unsafe.cs diff --git a/src/Markdig/Parsers/BlockProcessor.cs b/src/Markdig/Parsers/BlockProcessor.cs index 472525864..9d46ac42b 100644 --- a/src/Markdig/Parsers/BlockProcessor.cs +++ b/src/Markdig/Parsers/BlockProcessor.cs @@ -604,12 +604,7 @@ private void UpdateLastBlockAndContainer(int stackIndex = -1) if (block.IsContainerBlock) { - var currentContainer = -#if NETSTANDARD2_1 - (ContainerBlock)block; -#else - Unsafe.As(block); -#endif + var currentContainer = Unsafe.As(block); CurrentContainer = currentContainer; LastBlock = currentContainer.LastChild; CurrentBlock = currentBlock; @@ -699,12 +694,7 @@ private void TryContinueBlocks() } } -#if NETSTANDARD2_1 - ((LeafBlock)block) -#else - Unsafe.As(block) -#endif - .AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); + Unsafe.As(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } } @@ -851,12 +841,7 @@ private bool TryOpenBlocks(BlockParser[] parsers) UnwindAllIndents(); } -#if NETSTANDARD2_1 - ((ParagraphBlock)currentBlock) -#else - Unsafe.As(currentBlock) -#endif - .AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); + Unsafe.As(currentBlock).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } if (TrackTrivia) { @@ -926,12 +911,7 @@ private void ProcessNewBlocks(BlockState result, bool allowClosing) } } -#if NETSTANDARD2_1 - ((LeafBlock)block) -#else - Unsafe.As(block) -#endif - .AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); + Unsafe.As(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia); } if (newBlocks.Count > 0) diff --git a/src/Markdig/Parsers/InlineProcessor.cs b/src/Markdig/Parsers/InlineProcessor.cs index 98bfe1a81..e821a2fe5 100644 --- a/src/Markdig/Parsers/InlineProcessor.cs +++ b/src/Markdig/Parsers/InlineProcessor.cs @@ -325,11 +325,7 @@ private ContainerInline FindLastContainer() Inline? lastChild = container.LastChild; if (lastChild is not null && lastChild.IsContainerInline && !lastChild.IsClosed) { -#if NETSTANDARD2_1 - container = ((ContainerInline)lastChild); -#else container = Unsafe.As(lastChild); -#endif } else { diff --git a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs index 1b1e3342c..d53e050c9 100644 --- a/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/EmphasisInlineParser.cs @@ -97,11 +97,7 @@ public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, return true; } -#if NETSTANDARD2_1 - ContainerInline container = (ContainerInline)root; -#else ContainerInline container = Unsafe.As(root); -#endif List? delimiters = null; if (container is EmphasisDelimiterInline emphasisDelimiter) diff --git a/src/Markdig/Parsers/MarkdownParser.cs b/src/Markdig/Parsers/MarkdownParser.cs index aeff7a988..b1255c951 100644 --- a/src/Markdig/Parsers/MarkdownParser.cs +++ b/src/Markdig/Parsers/MarkdownParser.cs @@ -152,12 +152,7 @@ private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocu var block = container[item.Index]; if (block.IsLeafBlock) { -#if NETSTANDARD2_1 - LeafBlock leafBlock = (LeafBlock)block; -#else LeafBlock leafBlock = Unsafe.As(block); -#endif - leafBlock.OnProcessInlinesBegin(inlineProcessor); if (leafBlock.ProcessInlines) { @@ -192,11 +187,7 @@ private static void ProcessInlines(InlineProcessor inlineProcessor, MarkdownDocu Array.Resize(ref blocks, blockCount * 2); ThrowHelper.CheckDepthLimit(blocks.Length); } -#if NETSTANDARD2_1 - blocks[blockCount++] = new ContainerItem((ContainerBlock)block); -#else blocks[blockCount++] = new ContainerItem(Unsafe.As(block)); -#endif block.OnProcessInlinesBegin(inlineProcessor); goto process_new_block; } diff --git a/src/Markdig/Polyfills/Unsafe.cs b/src/Markdig/Polyfills/Unsafe.cs new file mode 100644 index 000000000..5fc650b83 --- /dev/null +++ b/src/Markdig/Polyfills/Unsafe.cs @@ -0,0 +1,17 @@ +// 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. + +namespace System.Runtime.CompilerServices +{ +#if NETSTANDARD2_1 + internal static class Unsafe + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T As(object o) where T : class + { + return (T)o; + } + } +#endif +} From 3f3b3c46b6a53eb737d24cd4b9d22958b03899e8 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 08:24:29 +0100 Subject: [PATCH 08/11] Optimize renderers --- .../Renderers/Html/CodeBlockRenderer.cs | 19 ++- src/Markdig/Renderers/Html/HeadingRenderer.cs | 17 ++- .../Html/Inlines/AutolinkInlineRenderer.cs | 16 ++- .../Html/Inlines/CodeInlineRenderer.cs | 6 +- .../Html/Inlines/EmphasisInlineRenderer.cs | 11 +- .../Html/Inlines/LinkInlineRenderer.cs | 18 +-- src/Markdig/Renderers/Html/ListRenderer.cs | 14 ++- .../Renderers/Html/ParagraphRenderer.cs | 6 +- .../Renderers/Html/QuoteBlockRenderer.cs | 4 +- .../Renderers/Html/ThematicBreakRenderer.cs | 4 +- src/Markdig/Renderers/HtmlRenderer.cs | 112 +++++++++++------- src/Markdig/Renderers/TextRendererBase.cs | 102 +++++++++------- 12 files changed, 200 insertions(+), 129 deletions(-) diff --git a/src/Markdig/Renderers/Html/CodeBlockRenderer.cs b/src/Markdig/Renderers/Html/CodeBlockRenderer.cs index 339bb2bb8..c1599c29e 100644 --- a/src/Markdig/Renderers/Html/CodeBlockRenderer.cs +++ b/src/Markdig/Renderers/Html/CodeBlockRenderer.cs @@ -15,27 +15,25 @@ namespace Markdig.Renderers.Html /// public class CodeBlockRenderer : HtmlObjectRenderer { + private HashSet? _blocksAsDiv; + /// /// Initializes a new instance of the class. /// - public CodeBlockRenderer() - { - BlocksAsDiv = new HashSet(StringComparer.OrdinalIgnoreCase); - } + public CodeBlockRenderer() { } public bool OutputAttributesOnPre { get; set; } /// /// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks. /// - public HashSet BlocksAsDiv { get; } + public HashSet BlocksAsDiv => _blocksAsDiv ??= new HashSet(StringComparer.OrdinalIgnoreCase); protected override void Write(HtmlRenderer renderer, CodeBlock obj) { renderer.EnsureLine(); - var fencedCodeBlock = obj as FencedCodeBlock; - if (fencedCodeBlock?.Info != null && BlocksAsDiv.Contains(fencedCodeBlock.Info)) + if (_blocksAsDiv is not null && (obj as FencedCodeBlock)?.Info is string info && _blocksAsDiv.Contains(info)) { var infoPrefix = (obj.Parser as FencedCodeBlockParser)?.InfoPrefix ?? FencedCodeBlockParser.DefaultInfoPrefix; @@ -48,7 +46,7 @@ protected override void Write(HtmlRenderer renderer, CodeBlock obj) renderer.Write(" cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls) - .Write('>'); + .WriteRaw('>'); } renderer.WriteLeafRawLines(obj, true, true, true); @@ -57,7 +55,6 @@ protected override void Write(HtmlRenderer renderer, CodeBlock obj) { renderer.WriteLine(""); } - } else { @@ -70,14 +67,14 @@ protected override void Write(HtmlRenderer renderer, CodeBlock obj) renderer.WriteAttributes(obj); } - renderer.Write(">'); + renderer.WriteRaw('>'); } renderer.WriteLeafRawLines(obj, true, true); diff --git a/src/Markdig/Renderers/Html/HeadingRenderer.cs b/src/Markdig/Renderers/Html/HeadingRenderer.cs index cc9c25525..d19e64b3a 100644 --- a/src/Markdig/Renderers/Html/HeadingRenderer.cs +++ b/src/Markdig/Renderers/Html/HeadingRenderer.cs @@ -2,7 +2,6 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. -using System.Globalization; using Markdig.Syntax; namespace Markdig.Renderers.Html @@ -25,20 +24,26 @@ public class HeadingRenderer : HtmlObjectRenderer protected override void Write(HtmlRenderer renderer, HeadingBlock obj) { int index = obj.Level - 1; - string headingText = ((uint)index < (uint)HeadingTexts.Length) - ? HeadingTexts[index] - : "h" + obj.Level.ToString(CultureInfo.InvariantCulture); + string[] headings = HeadingTexts; + string headingText = ((uint)index < (uint)headings.Length) + ? headings[index] + : $"h{obj.Level}"; if (renderer.EnableHtmlForBlock) { - renderer.Write("<").Write(headingText).WriteAttributes(obj).Write('>'); + renderer.Write('<'); + renderer.WriteRaw(headingText); + renderer.WriteAttributes(obj); + renderer.WriteRaw('>'); } renderer.WriteLeafInline(obj); if (renderer.EnableHtmlForBlock) { - renderer.Write(""); + renderer.Write("'); } renderer.EnsureLine(); diff --git a/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs index b879bdfd8..817d4842f 100644 --- a/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/AutolinkInlineRenderer.cs @@ -54,28 +54,26 @@ protected override void Write(HtmlRenderer renderer, AutolinkInline obj) { if (renderer.EnableHtmlForInline) { - renderer.Write("'); + renderer.WriteRaw('>'); } renderer.WriteEscape(obj.Url); if (renderer.EnableHtmlForInline) { - renderer.Write(""); + renderer.WriteRaw(""); } } } diff --git a/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs index 9e60651f1..696d1514c 100644 --- a/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/CodeInlineRenderer.cs @@ -16,7 +16,9 @@ protected override void Write(HtmlRenderer renderer, CodeInline obj) { if (renderer.EnableHtmlForInline) { - renderer.Write("'); + renderer.Write("'); } if (renderer.EnableHtmlEscape) { @@ -28,7 +30,7 @@ protected override void Write(HtmlRenderer renderer, CodeInline obj) } if (renderer.EnableHtmlForInline) { - renderer.Write(""); + renderer.WriteRaw(""); } } } diff --git a/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs index 7f5b8452b..50c1e9dfc 100644 --- a/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/EmphasisInlineRenderer.cs @@ -39,12 +39,17 @@ protected override void Write(HtmlRenderer renderer, EmphasisInline obj) if (renderer.EnableHtmlForInline) { tag = GetTag(obj); - renderer.Write("<").Write(tag).WriteAttributes(obj).Write('>'); + renderer.Write('<'); + renderer.WriteRaw(tag); + renderer.WriteAttributes(obj); + renderer.WriteRaw('>'); } renderer.WriteChildren(obj); if (renderer.EnableHtmlForInline) { - renderer.Write("'); + renderer.Write("'); } } @@ -53,7 +58,7 @@ protected override void Write(HtmlRenderer renderer, EmphasisInline obj) /// /// The object. /// - public string? GetDefaultTag(EmphasisInline obj) + public static string? GetDefaultTag(EmphasisInline obj) { if (obj.DelimiterChar is '*' or '_') { diff --git a/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs b/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs index a1b178579..7b7e06fc5 100644 --- a/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs +++ b/src/Markdig/Renderers/Html/Inlines/LinkInlineRenderer.cs @@ -51,14 +51,14 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) { renderer.Write(link.IsImage ? "\"");"); + renderer.WriteRaw(" />"); } } else @@ -90,9 +90,11 @@ protected override void Write(HtmlRenderer renderer, LinkInline link) { if (!string.IsNullOrWhiteSpace(Rel)) { - renderer.Write($" rel=\"{Rel}\""); + renderer.WriteRaw(" rel=\""); + renderer.WriteRaw(Rel); + renderer.WriteRaw('"'); } - renderer.Write('>'); + renderer.WriteRaw('>'); } renderer.WriteChildren(link); if (renderer.EnableHtmlForInline) diff --git a/src/Markdig/Renderers/Html/ListRenderer.cs b/src/Markdig/Renderers/Html/ListRenderer.cs index 543208441..931c0e939 100644 --- a/src/Markdig/Renderers/Html/ListRenderer.cs +++ b/src/Markdig/Renderers/Html/ListRenderer.cs @@ -22,12 +22,16 @@ protected override void Write(HtmlRenderer renderer, ListBlock listBlock) renderer.Write("'); @@ -49,7 +53,9 @@ protected override void Write(HtmlRenderer renderer, ListBlock listBlock) renderer.EnsureLine(); if (renderer.EnableHtmlForBlock) { - renderer.Write("'); + renderer.Write("'); } renderer.WriteChildren(listItem); diff --git a/src/Markdig/Renderers/Html/ParagraphRenderer.cs b/src/Markdig/Renderers/Html/ParagraphRenderer.cs index 703dfe2d6..5630db47b 100644 --- a/src/Markdig/Renderers/Html/ParagraphRenderer.cs +++ b/src/Markdig/Renderers/Html/ParagraphRenderer.cs @@ -21,12 +21,14 @@ protected override void Write(HtmlRenderer renderer, ParagraphBlock obj) renderer.EnsureLine(); } - renderer.Write(""); + renderer.Write("'); } renderer.WriteLeafInline(obj); if (!renderer.ImplicitParagraph) { - if(renderer.EnableHtmlForBlock) + if (renderer.EnableHtmlForBlock) { renderer.WriteLine("

"); } diff --git a/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs b/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs index f2faeab8c..dbc44c7b9 100644 --- a/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs +++ b/src/Markdig/Renderers/Html/QuoteBlockRenderer.cs @@ -17,7 +17,9 @@ protected override void Write(HtmlRenderer renderer, QuoteBlock obj) renderer.EnsureLine(); if (renderer.EnableHtmlForBlock) { - renderer.Write(""); + renderer.Write("'); } var savedImplicitParagraph = renderer.ImplicitParagraph; renderer.ImplicitParagraph = false; diff --git a/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs b/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs index 5409c6ead..f824fd463 100644 --- a/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs +++ b/src/Markdig/Renderers/Html/ThematicBreakRenderer.cs @@ -16,7 +16,9 @@ protected override void Write(HtmlRenderer renderer, ThematicBreakBlock obj) { if (renderer.EnableHtmlForBlock) { - renderer.Write(""); + renderer.Write(""); } } } diff --git a/src/Markdig/Renderers/HtmlRenderer.cs b/src/Markdig/Renderers/HtmlRenderer.cs index 33f9e47db..5ed7a14d0 100644 --- a/src/Markdig/Renderers/HtmlRenderer.cs +++ b/src/Markdig/Renderers/HtmlRenderer.cs @@ -20,6 +20,8 @@ namespace Markdig.Renderers /// public class HtmlRenderer : TextRendererBase { + private static ReadOnlySpan WriteEscapeIndexOfAnyChars => new[] { '<', '>', '&', '"' }; + /// /// Initializes a new instance of the class. /// @@ -94,10 +96,7 @@ public HtmlRenderer(TextWriter writer) : base(writer) [MethodImpl(MethodImplOptions.AggressiveInlining)] public HtmlRenderer WriteEscape(string? content) { - if (content is { Length: > 0 }) - { - WriteEscape(content, 0, content.Length); - } + WriteEscape(content.AsSpan()); return this; } @@ -110,11 +109,8 @@ public HtmlRenderer WriteEscape(string? content) [MethodImpl(MethodImplOptions.AggressiveInlining)] public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false) { - if (slice.Start > slice.End) - { - return this; - } - return WriteEscape(slice.Text, slice.Start, slice.Length, softEscape); + WriteEscape(slice.AsSpan(), softEscape); + return this; } /// @@ -126,7 +122,8 @@ public HtmlRenderer WriteEscape(ref StringSlice slice, bool softEscape = false) [MethodImpl(MethodImplOptions.AggressiveInlining)] public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false) { - return WriteEscape(ref slice, softEscape); + WriteEscape(slice.AsSpan(), softEscape); + return this; } /// @@ -139,58 +136,80 @@ public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false) /// This instance public HtmlRenderer WriteEscape(string content, int offset, int length, bool softEscape = false) { - if (string.IsNullOrEmpty(content) || length == 0) - return this; + WriteEscape(content.AsSpan(offset, length), softEscape); + return this; + } + + /// + /// Writes the content escaped for HTML. + /// + /// The content. + /// Only escape < and & + public void WriteEscape(ReadOnlySpan content, bool softEscape = false) + { + if (!content.IsEmpty) + { + int nextIndex = content.IndexOfAny(WriteEscapeIndexOfAnyChars); + if (nextIndex == -1) + { + Write(content); + } + else + { + WriteEscapeSlow(content, softEscape); + } + } + } - var end = offset + length; - int previousOffset = offset; - for (;offset < end; offset++) + private void WriteEscapeSlow(ReadOnlySpan content, bool softEscape = false) + { + int previousOffset = 0; + for (int i = 0; i < content.Length; i++) { - switch (content[offset]) + switch (content[i]) { case '<': - Write(content, previousOffset, offset - previousOffset); + Write(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { Write("<"); } - previousOffset = offset + 1; + previousOffset = i + 1; break; case '>': if (!softEscape) { - Write(content, previousOffset, offset - previousOffset); + Write(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { Write(">"); } - previousOffset = offset + 1; + previousOffset = i + 1; } break; case '&': - Write(content, previousOffset, offset - previousOffset); + Write(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { Write("&"); } - previousOffset = offset + 1; + previousOffset = i + 1; break; case '"': if (!softEscape) { - Write(content, previousOffset, offset - previousOffset); + Write(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { Write("""); } - previousOffset = offset + 1; + previousOffset = i + 1; } break; } } - Write(content, previousOffset, end - previousOffset); - return this; + Write(content.Slice(previousOffset)); } private static readonly IdnMapping IdnMapping = new IdnMapping(); @@ -218,8 +237,8 @@ public HtmlRenderer WriteEscapeUrl(string? content) content = LinkRewriter(content); } - // ab://c.d = 8 chars - int schemeOffset = content.Length < 8 ? -1 : content.IndexOf("://", 2, StringComparison.Ordinal); + // a://c.d = 7 chars + int schemeOffset = content.Length < 7 ? -1 : content.IndexOf("://", StringComparison.Ordinal); if (schemeOffset != -1) // This is an absolute URL { schemeOffset += 3; // skip :// @@ -360,7 +379,9 @@ public HtmlRenderer WriteAttributes(HtmlAttributes? attributes, Func 0 }) @@ -371,21 +392,22 @@ public HtmlRenderer WriteAttributes(HtmlAttributes? attributes, Func 0) { - Write(' '); + WriteRaw(' '); } WriteEscape(classFilter != null ? classFilter(cssClass) : cssClass); } - Write('"'); + WriteRaw('"'); } if (attributes.Properties is { Count: > 0 }) { foreach (var property in attributes.Properties) { - Write(' ').Write(property.Key); - Write("=\""); + Write(' '); + WriteRaw(property.Key); + WriteRaw("=\""); WriteEscape(property.Value ?? ""); - Write('"'); + WriteRaw('"'); } } @@ -403,30 +425,40 @@ public HtmlRenderer WriteAttributes(HtmlAttributes? attributes, Func 0) { WriteLine(); } + + ReadOnlySpan span = slice.AsSpan(); if (escape) { - WriteEscape(ref slices[i].Slice, softEscape); + WriteEscape(span, softEscape); } else { - Write(ref slices[i].Slice); + Write(span); } + if (writeEndOfLines) { WriteLine(); } } } + return this; } } diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 434aaf25d..5ae839fa4 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -38,7 +38,8 @@ protected TextRendererBase(TextWriter writer) public TextWriter Writer { get => _writer; - [MemberNotNull(nameof(_writer))] set + [MemberNotNull(nameof(_writer))] + set { if (value is null) { @@ -145,11 +146,13 @@ internal void ResetInternal() /// Ensures a newline. /// /// This instance + [MethodImpl(MethodImplOptions.AggressiveInlining)] public T EnsureLine() { if (!previousWasLine) { - WriteLine(); + previousWasLine = true; + Writer.WriteLine(); } return (T)this; } @@ -176,20 +179,25 @@ public void PopIndent() indents.RemoveAt(indents.Count - 1); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void WriteIndent() { if (previousWasLine) { - previousWasLine = false; - for (int i = 0; i < indents.Count; i++) - { - var indent = indents[i]; - var indentText = indent.Next(); - Writer.Write(indentText); - } + WriteIndentCore(); } } + private void WriteIndentCore() + { + previousWasLine = false; + for (int i = 0; i < indents.Count; i++) + { + var indent = indents[i]; + var indentText = indent.Next(); + Writer.Write(indentText); + } + } /// /// Writes the specified content. @@ -200,9 +208,8 @@ private void WriteIndent() public T Write(string? content) { WriteIndent(); - previousWasLine = false; Writer.Write(content); - return (T) this; + return (T)this; } /// @@ -213,11 +220,8 @@ public T Write(string? content) [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Write(ref StringSlice slice) { - if (slice.Start > slice.End) - { - return (T) this; - } - return Write(slice.Text, slice.Start, slice.Length); + Write(slice.AsSpan()); + return (T)this; } /// @@ -228,7 +232,8 @@ public T Write(ref StringSlice slice) [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Write(StringSlice slice) { - return Write(ref slice); + Write(slice.AsSpan()); + return (T)this; } /// @@ -240,9 +245,12 @@ public T Write(StringSlice slice) public T Write(char content) { WriteIndent(); - previousWasLine = content == '\n'; + if (content == '\n') + { + previousWasLine = true; + } Writer.Write(content); - return (T) this; + return (T)this; } /// @@ -254,38 +262,48 @@ public T Write(char content) /// This instance public T Write(string content, int offset, int length) { - if (content is null) + if (content is not null) + { + Write(content.AsSpan(offset, length)); + } + return (T)this; + } + + /// + /// Writes the specified content. + /// + /// The content. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Write(ReadOnlySpan content) + { + if (content.IsEmpty) { - return (T) this; + return; } WriteIndent(); - previousWasLine = false; #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER - Writer.Write(content.AsSpan(offset, length)); + Writer.Write(content); #else - if (offset == 0 && content.Length == length) + if (content.Length > buffer.Length) { - Writer.Write(content); + buffer = content.ToArray(); } else { - if (length > buffer.Length) - { - buffer = content.ToCharArray(); - Writer.Write(buffer, offset, length); - } - else - { - content.CopyTo(offset, buffer, 0, length); - Writer.Write(buffer, 0, length); - } + content.CopyTo(buffer); } + Writer.Write(buffer, 0, content.Length); #endif - return (T) this; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteRaw(char content) => Writer.Write(content); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteRaw(string? content) => Writer.Write(content); + /// /// Writes a newline. /// @@ -296,7 +314,7 @@ public T WriteLine() WriteIndent(); Writer.WriteLine(); previousWasLine = true; - return (T) this; + return (T)this; } /// @@ -323,7 +341,7 @@ public T WriteLine(string content) WriteIndent(); previousWasLine = true; Writer.WriteLine(content); - return (T) this; + return (T)this; } /// @@ -349,15 +367,15 @@ public T WriteLine(char content) public T WriteLeafInline(LeafBlock leafBlock) { if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock(); - var inline = (Inline) leafBlock.Inline!; - + Inline? inline = leafBlock.Inline; + while (inline != null) { Write(inline); inline = inline.NextSibling; } - - return (T) this; + + return (T)this; } } } \ No newline at end of file From 31904f6c53220a99eef553340fafb5ed6cd8dba9 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 10:54:09 +0100 Subject: [PATCH 09/11] Avoid allocating WriteEscapeIndexOfAnyChars Roslyn doesn't support static char arrays yet --- src/Markdig/Renderers/HtmlRenderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markdig/Renderers/HtmlRenderer.cs b/src/Markdig/Renderers/HtmlRenderer.cs index 5ed7a14d0..217af4672 100644 --- a/src/Markdig/Renderers/HtmlRenderer.cs +++ b/src/Markdig/Renderers/HtmlRenderer.cs @@ -20,7 +20,7 @@ namespace Markdig.Renderers /// public class HtmlRenderer : TextRendererBase { - private static ReadOnlySpan WriteEscapeIndexOfAnyChars => new[] { '<', '>', '&', '"' }; + private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' }; /// /// Initializes a new instance of the class. @@ -149,7 +149,7 @@ public void WriteEscape(ReadOnlySpan content, bool softEscape = false) { if (!content.IsEmpty) { - int nextIndex = content.IndexOfAny(WriteEscapeIndexOfAnyChars); + int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars); if (nextIndex == -1) { Write(content); From 9adf60116b0d637b6527d17401d6099c176742a8 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 11:24:05 +0100 Subject: [PATCH 10/11] More WriteRaw --- src/Markdig/Renderers/HtmlRenderer.cs | 20 +++++++++++--------- src/Markdig/Renderers/TextRendererBase.cs | 23 +++++++++++++---------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Markdig/Renderers/HtmlRenderer.cs b/src/Markdig/Renderers/HtmlRenderer.cs index 217af4672..29fbe1b28 100644 --- a/src/Markdig/Renderers/HtmlRenderer.cs +++ b/src/Markdig/Renderers/HtmlRenderer.cs @@ -163,45 +163,47 @@ public void WriteEscape(ReadOnlySpan content, bool softEscape = false) private void WriteEscapeSlow(ReadOnlySpan content, bool softEscape = false) { + WriteIndent(); + int previousOffset = 0; for (int i = 0; i < content.Length; i++) { switch (content[i]) { case '<': - Write(content.Slice(previousOffset, i - previousOffset)); + WriteRaw(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { - Write("<"); + WriteRaw("<"); } previousOffset = i + 1; break; case '>': if (!softEscape) { - Write(content.Slice(previousOffset, i - previousOffset)); + WriteRaw(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { - Write(">"); + WriteRaw(">"); } previousOffset = i + 1; } break; case '&': - Write(content.Slice(previousOffset, i - previousOffset)); + WriteRaw(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { - Write("&"); + WriteRaw("&"); } previousOffset = i + 1; break; case '"': if (!softEscape) { - Write(content.Slice(previousOffset, i - previousOffset)); + WriteRaw(content.Slice(previousOffset, i - previousOffset)); if (EnableHtmlEscape) { - Write("""); + WriteRaw("""); } previousOffset = i + 1; } @@ -209,7 +211,7 @@ private void WriteEscapeSlow(ReadOnlySpan content, bool softEscape = false } } - Write(content.Slice(previousOffset)); + WriteRaw(content.Slice(previousOffset)); } private static readonly IdnMapping IdnMapping = new IdnMapping(); diff --git a/src/Markdig/Renderers/TextRendererBase.cs b/src/Markdig/Renderers/TextRendererBase.cs index 5ae839fa4..c5c8c2cc1 100644 --- a/src/Markdig/Renderers/TextRendererBase.cs +++ b/src/Markdig/Renderers/TextRendererBase.cs @@ -180,7 +180,7 @@ public void PopIndent() } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void WriteIndent() + private protected void WriteIndent() { if (previousWasLine) { @@ -276,13 +276,22 @@ public T Write(string content, int offset, int length) [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ReadOnlySpan content) { - if (content.IsEmpty) + if (!content.IsEmpty) { - return; + WriteIndent(); + WriteRaw(content); } + } - WriteIndent(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteRaw(char content) => Writer.Write(content); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteRaw(string? content) => Writer.Write(content); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void WriteRaw(ReadOnlySpan content) + { #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER Writer.Write(content); #else @@ -298,12 +307,6 @@ public void Write(ReadOnlySpan content) #endif } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void WriteRaw(char content) => Writer.Write(content); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void WriteRaw(string? content) => Writer.Write(content); - /// /// Writes a newline. /// From ed83943ba58eb2e148d5f459144348c40b0ec197 Mon Sep 17 00:00:00 2001 From: Miha Zupan Date: Sun, 20 Mar 2022 13:49:38 +0100 Subject: [PATCH 11/11] Use custom StringWriter for rendering internally --- src/Markdig.Tests/TestFastStringWriter.cs | 189 +++++++++++++ src/Markdig/Helpers/FastStringWriter.cs | 293 ++++++++++++++++++++ src/Markdig/MarkdownPipeline.cs | 10 +- src/Markdig/Polyfills/NullableAttributes.cs | 3 + 4 files changed, 489 insertions(+), 6 deletions(-) create mode 100644 src/Markdig.Tests/TestFastStringWriter.cs create mode 100644 src/Markdig/Helpers/FastStringWriter.cs diff --git a/src/Markdig.Tests/TestFastStringWriter.cs b/src/Markdig.Tests/TestFastStringWriter.cs new file mode 100644 index 000000000..d43dd3a3d --- /dev/null +++ b/src/Markdig.Tests/TestFastStringWriter.cs @@ -0,0 +1,189 @@ +using Markdig.Helpers; +using NUnit.Framework; +using System; +using System.Text; +using System.Threading.Tasks; + +namespace Markdig.Tests +{ + [TestFixture] + public class TestFastStringWriter + { + private const string NewLineReplacement = "~~NEW_LINE~~"; + + private FastStringWriter _writer = new(); + + [SetUp] + public void Setup() + { + _writer = new FastStringWriter + { + NewLine = NewLineReplacement + }; + } + + public void AssertToString(string value) + { + value = value.Replace("\n", NewLineReplacement); + Assert.AreEqual(value, _writer.ToString()); + Assert.AreEqual(value, _writer.ToString()); + } + + [Test] + public async Task NewLine() + { + Assert.AreEqual("\n", new FastStringWriter().NewLine); + + _writer.NewLine = "\r"; + Assert.AreEqual("\r", _writer.NewLine); + + _writer.NewLine = "foo"; + Assert.AreEqual("foo", _writer.NewLine); + + _writer.WriteLine(); + await _writer.WriteLineAsync(); + _writer.WriteLine("bar"); + Assert.AreEqual("foofoobarfoo", _writer.ToString()); + } + + [Test] + public async Task FlushCloseDispose() + { + _writer.Write('a'); + + // Nops + _writer.Close(); + _writer.Dispose(); + await _writer.DisposeAsync(); + _writer.Flush(); + await _writer.FlushAsync(); + + _writer.Write('b'); + AssertToString("ab"); + } + + [Test] + public async Task Write_Char() + { + _writer.Write('a'); + AssertToString("a"); + + _writer.Write('b'); + AssertToString("ab"); + + _writer.Write('\0'); + _writer.Write('\r'); + _writer.Write('\u1234'); + AssertToString("ab\0\r\u1234"); + + _writer.Reset(); + AssertToString(""); + + _writer.Write('a'); + _writer.WriteLine('b'); + _writer.Write('c'); + _writer.Write('d'); + _writer.WriteLine('e'); + AssertToString("ab\ncde\n"); + + await _writer.WriteAsync('f'); + await _writer.WriteLineAsync('g'); + AssertToString("ab\ncde\nfg\n"); + + _writer.Reset(); + + for (int i = 0; i < 2050; i++) + { + _writer.Write('a'); + AssertToString(new string('a', i + 1)); + } + } + + [Test] + public async Task Write_String() + { + _writer.Write("foo"); + AssertToString("foo"); + + _writer.WriteLine("bar"); + AssertToString("foobar\n"); + + await _writer.WriteAsync("baz"); + await _writer.WriteLineAsync("foo"); + AssertToString("foobar\nbazfoo\n"); + + _writer.Write(new string('a', 1050)); + AssertToString("foobar\nbazfoo\n" + new string('a', 1050)); + } + + [Test] + public async Task Write_Span() + { + _writer.Write("foo".AsSpan()); + AssertToString("foo"); + + _writer.WriteLine("bar".AsSpan()); + AssertToString("foobar\n"); + + await _writer.WriteAsync("baz".AsMemory()); + await _writer.WriteLineAsync("foo".AsMemory()); + AssertToString("foobar\nbazfoo\n"); + + _writer.Write(new string('a', 1050).AsSpan()); + AssertToString("foobar\nbazfoo\n" + new string('a', 1050)); + } + + [Test] + public async Task Write_CharArray() + { + _writer.Write("foo".ToCharArray()); + AssertToString("foo"); + + _writer.WriteLine("bar".ToCharArray()); + AssertToString("foobar\n"); + + await _writer.WriteAsync("baz".ToCharArray()); + await _writer.WriteLineAsync("foo".ToCharArray()); + AssertToString("foobar\nbazfoo\n"); + + _writer.Write(new string('a', 1050).ToCharArray()); + AssertToString("foobar\nbazfoo\n" + new string('a', 1050)); + } + + [Test] + public async Task Write_CharArrayWithIndexes() + { + _writer.Write("foo".ToCharArray(), 1, 1); + AssertToString("o"); + + _writer.WriteLine("bar".ToCharArray(), 0, 2); + AssertToString("oba\n"); + + await _writer.WriteAsync("baz".ToCharArray(), 0, 1); + await _writer.WriteLineAsync("foo".ToCharArray(), 0, 3); + AssertToString("oba\nbfoo\n"); + + _writer.Write(new string('a', 1050).ToCharArray(), 10, 1035); + AssertToString("oba\nbfoo\n" + new string('a', 1035)); + } + + [Test] + public async Task Write_StringBuilder() + { + _writer.Write(new StringBuilder("foo")); + AssertToString("foo"); + + _writer.WriteLine(new StringBuilder("bar")); + AssertToString("foobar\n"); + + await _writer.WriteAsync(new StringBuilder("baz")); + await _writer.WriteLineAsync(new StringBuilder("foo")); + AssertToString("foobar\nbazfoo\n"); + + var sb = new StringBuilder("foo"); + sb.Append('a', 1050); + _writer.Write(sb); + AssertToString("foobar\nbazfoo\nfoo" + new string('a', 1050)); + } + } +} diff --git a/src/Markdig/Helpers/FastStringWriter.cs b/src/Markdig/Helpers/FastStringWriter.cs new file mode 100644 index 000000000..8be2307f3 --- /dev/null +++ b/src/Markdig/Helpers/FastStringWriter.cs @@ -0,0 +1,293 @@ +// 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; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Markdig.Helpers +{ + internal sealed class FastStringWriter : TextWriter + { +#if NET452 + private static Task CompletedTask => Task.FromResult(0); +#else + private static Task CompletedTask => Task.CompletedTask; +#endif + + public override Encoding Encoding => Encoding.Unicode; + + private char[] _chars; + private int _pos; + private string _newLine; + + public FastStringWriter() + { + _chars = new char[1024]; + _newLine = "\n"; + } + + [AllowNull] + public override string NewLine + { + get => _newLine; + set => _newLine = value ?? Environment.NewLine; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(char value) + { + char[] chars = _chars; + int pos = _pos; + if ((uint)pos < (uint)chars.Length) + { + chars[pos] = value; + _pos = pos + 1; + } + else + { + GrowAndAppend(value); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(char value) + { + Write(value); + WriteLine(); + } + + public override Task WriteAsync(char value) + { + Write(value); + return CompletedTask; + } + + public override Task WriteLineAsync(char value) + { + WriteLine(value); + return CompletedTask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(string? value) + { + if (value is not null) + { + if (_pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.AsSpan().CopyTo(_chars.AsSpan(_pos)); + _pos += value.Length; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(string? value) + { + Write(value); + WriteLine(); + } + + public override Task WriteAsync(string? value) + { + Write(value); + return CompletedTask; + } + + public override Task WriteLineAsync(string? value) + { + WriteLine(value); + return CompletedTask; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(char[]? buffer) + { + if (buffer is not null) + { + if (_pos > _chars.Length - buffer.Length) + { + Grow(buffer.Length); + } + + buffer.CopyTo(_chars.AsSpan(_pos)); + _pos += buffer.Length; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(char[]? buffer) + { + Write(buffer); + WriteLine(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(char[] buffer, int index, int count) + { + if (buffer is not null) + { + if (_pos > _chars.Length - count) + { + Grow(buffer.Length); + } + + buffer.AsSpan(index, count).CopyTo(_chars.AsSpan(_pos)); + _pos += count; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(char[] buffer, int index, int count) + { + Write(buffer, index, count); + WriteLine(); + } + + public override Task WriteAsync(char[] buffer, int index, int count) + { + Write(buffer, index, count); + return CompletedTask; + } + + public override Task WriteLineAsync(char[] buffer, int index, int count) + { + WriteLine(buffer, index, count); + return CompletedTask; + } + +#if !(NET452 || NETSTANDARD2_0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(ReadOnlySpan value) + { + if (_pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.AsSpan(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(ReadOnlySpan buffer) + { + Write(buffer); + WriteLine(); + } + + public override Task WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + Write(buffer.Span); + return CompletedTask; + } + + public override Task WriteLineAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + WriteLine(buffer.Span); + return CompletedTask; + } +#endif + +#if !(NET452 || NETSTANDARD2_0 || NETSTANDARD2_1) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Write(StringBuilder? value) + { + if (value is not null) + { + int length = value.Length; + if (_pos > _chars.Length - length) + { + Grow(length); + } + + value.CopyTo(0, _chars.AsSpan(_pos), length); + _pos += length; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine(StringBuilder? value) + { + Write(value); + WriteLine(); + } + + public override Task WriteAsync(StringBuilder? value, CancellationToken cancellationToken = default) + { + Write(value); + return CompletedTask; + } + + public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default) + { + WriteLine(value); + return CompletedTask; + } +#endif + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void WriteLine() + { + foreach (char c in _newLine) + { + Write(c); + } + } + + public override Task WriteLineAsync() + { + WriteLine(); + return CompletedTask; + } + + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char value) + { + Grow(1); + Write(value); + } + + private void Grow(int additionalCapacityBeyondPos) + { + Debug.Assert(additionalCapacityBeyondPos > 0); + Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "No resize is needed."); + + char[] newArray = new char[(int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2)]; + _chars.AsSpan(0, _pos).CopyTo(newArray); + _chars = newArray; + } + + + public override void Flush() { } + + public override void Close() { } + + public override Task FlushAsync() => CompletedTask; + +#if !(NET452 || NETSTANDARD2_0) + public override ValueTask DisposeAsync() => default; +#endif + + + public void Reset() + { + _pos = 0; + } + + public override string ToString() + { + return _chars.AsSpan(0, _pos).ToString(); + } + } +} diff --git a/src/Markdig/MarkdownPipeline.cs b/src/Markdig/MarkdownPipeline.cs index ca4d7e370..17ce8f6ec 100644 --- a/src/Markdig/MarkdownPipeline.cs +++ b/src/Markdig/MarkdownPipeline.cs @@ -94,9 +94,7 @@ internal RentedHtmlRenderer RentHtmlRenderer(TextWriter? writer = null) internal sealed class HtmlRendererCache : ObjectCache { - private const int InitialCapacity = 1024; - - private static readonly StringWriter _dummyWriter = new(); + private static readonly TextWriter s_dummyWriter = new StringWriter(); private readonly MarkdownPipeline _pipeline; private readonly bool _customWriter; @@ -109,7 +107,7 @@ public HtmlRendererCache(MarkdownPipeline pipeline, bool customWriter = false) protected override HtmlRenderer NewInstance() { - var writer = _customWriter ? _dummyWriter : new StringWriter(new StringBuilder(InitialCapacity)); + TextWriter writer = _customWriter ? s_dummyWriter : new FastStringWriter(); var renderer = new HtmlRenderer(writer); _pipeline.Setup(renderer); return renderer; @@ -121,11 +119,11 @@ protected override void Reset(HtmlRenderer instance) if (_customWriter) { - instance.Writer = _dummyWriter; + instance.Writer = s_dummyWriter; } else { - ((StringWriter)instance.Writer).GetStringBuilder().Length = 0; + ((FastStringWriter)instance.Writer).Reset(); } } } diff --git a/src/Markdig/Polyfills/NullableAttributes.cs b/src/Markdig/Polyfills/NullableAttributes.cs index 8655646be..632ceb7c4 100644 --- a/src/Markdig/Polyfills/NullableAttributes.cs +++ b/src/Markdig/Polyfills/NullableAttributes.cs @@ -15,6 +15,9 @@ internal sealed class NotNullWhenAttribute : Attribute public bool ReturnValue { get; } } + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)] + public sealed class AllowNullAttribute : Attribute { } #endif #if !NET5_0_OR_GREATER