diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e11a67d95..45b4b5556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,13 @@ jobs: submodules: true fetch-depth: 0 - - name: Install .NET 6.0 + - name: Install .NET 6.0, 7.0, and 8.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: '6.0.x' + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x - name: Build, Test, Pack, Publish shell: bash diff --git a/src/Markdig.Tests/Markdig.Tests.csproj b/src/Markdig.Tests/Markdig.Tests.csproj index 88887715b..ee3e70aaa 100644 --- a/src/Markdig.Tests/Markdig.Tests.csproj +++ b/src/Markdig.Tests/Markdig.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0 Exe false enable diff --git a/src/Markdig.Tests/TestYamlFrontMatterExtension.cs b/src/Markdig.Tests/TestYamlFrontMatterExtension.cs index 9d9cefe0b..3b38e4f09 100644 --- a/src/Markdig.Tests/TestYamlFrontMatterExtension.cs +++ b/src/Markdig.Tests/TestYamlFrontMatterExtension.cs @@ -75,8 +75,11 @@ public DummyRenderer() ObjectRenderers = new ObjectRendererCollection(); } +#pragma warning disable CS0067 // ObjectWriteBefore/ObjectWriteAfter is never used public event Action ObjectWriteBefore; public event Action ObjectWriteAfter; +#pragma warning restore CS0067 + public ObjectRendererCollection ObjectRenderers { get; } public object Render(MarkdownObject markdownObject) { diff --git a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs index a7fb77dd7..dfbf73ecf 100644 --- a/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs +++ b/src/Markdig/Extensions/AutoIdentifiers/AutoIdentifierExtension.cs @@ -20,8 +20,10 @@ namespace Markdig.Extensions.AutoIdentifiers; public class AutoIdentifierExtension : IMarkdownExtension { private const string AutoIdentifierKey = "AutoIdentifier"; - private readonly AutoIdentifierOptions options; - private readonly StripRendererCache rendererCache = new StripRendererCache(); + + private static readonly StripRendererCache _rendererCache = new(); + + private readonly AutoIdentifierOptions _options; /// /// Initializes a new instance of the class. @@ -29,7 +31,7 @@ public class AutoIdentifierExtension : IMarkdownExtension /// The options. public AutoIdentifierExtension(AutoIdentifierOptions options) { - this.options = options; + _options = options; } public void Setup(MarkdownPipelineBuilder pipeline) @@ -68,7 +70,7 @@ private void HeadingBlockParser_Closed(BlockProcessor processor, Block block) } // If the AutoLink options is set, we register a LinkReferenceDefinition at the document level - if ((options & AutoIdentifierOptions.AutoLink) != 0) + if ((_options & AutoIdentifierOptions.AutoLink) != 0) { var headingLine = headingBlock.Lines.Lines[0]; @@ -157,16 +159,17 @@ private void HeadingBlock_ProcessInlinesEnd(InlineProcessor processor, Inline? i } // Use internally a HtmlRenderer to strip links from a heading - var stripRenderer = rendererCache.Get(); + var stripRenderer = _rendererCache.Get(); stripRenderer.Render(headingBlock.Inline); - var headingText = stripRenderer.Writer.ToString()!; - rendererCache.Release(stripRenderer); + ReadOnlySpan rawHeadingText = ((FastStringWriter)stripRenderer.Writer).AsSpan(); // Urilize the link - headingText = (options & AutoIdentifierOptions.GitHub) != 0 - ? LinkHelper.UrilizeAsGfm(headingText) - : LinkHelper.Urilize(headingText, (options & AutoIdentifierOptions.AllowOnlyAscii) != 0); + string headingText = (_options & AutoIdentifierOptions.GitHub) != 0 + ? LinkHelper.UrilizeAsGfm(rawHeadingText) + : LinkHelper.Urilize(rawHeadingText, (_options & AutoIdentifierOptions.AllowOnlyAscii) != 0); + + _rendererCache.Release(stripRenderer); // If the heading is empty, use the word "section" instead var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText; @@ -197,7 +200,7 @@ private sealed class StripRendererCache : ObjectCache { protected override HtmlRenderer NewInstance() { - var headingWriter = new StringWriter(); + var headingWriter = new FastStringWriter(); var stripRenderer = new HtmlRenderer(headingWriter) { // Set to false both to avoid having any HTML tags in the output @@ -209,7 +212,9 @@ protected override HtmlRenderer NewInstance() protected override void Reset(HtmlRenderer instance) { - instance.Reset(); + instance.ResetInternal(); + + ((FastStringWriter)instance.Writer).Reset(); } } } \ No newline at end of file diff --git a/src/Markdig/Helpers/BlockWrapper.cs b/src/Markdig/Helpers/BlockWrapper.cs index 38fe8992f..b5290fef2 100644 --- a/src/Markdig/Helpers/BlockWrapper.cs +++ b/src/Markdig/Helpers/BlockWrapper.cs @@ -22,7 +22,7 @@ public BlockWrapper(Block block) public bool Equals(BlockWrapper other) => ReferenceEquals(Block, other.Block); - public override bool Equals(object obj) => Block.Equals(obj); + public override bool Equals(object? obj) => Block.Equals(obj); public override int GetHashCode() => Block.GetHashCode(); } diff --git a/src/Markdig/Helpers/CharHelper.cs b/src/Markdig/Helpers/CharHelper.cs index fcf27a6dc..5d121bcc1 100644 --- a/src/Markdig/Helpers/CharHelper.cs +++ b/src/Markdig/Helpers/CharHelper.cs @@ -30,15 +30,17 @@ public static class CharHelper { 'I', 1 }, { 'V', 5 }, { 'X', 10 } }; - private static readonly char[] punctuationExceptions = { '−', '-', '†', '‡' }; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsPunctuationException(char c) => + c is '−' or '-' or '†' or '‡'; public static void CheckOpenCloseDelimiter(char pc, char c, bool enableWithinWord, out bool canOpen, out bool canClose) { pc.CheckUnicodeCategory(out bool prevIsWhiteSpace, out bool prevIsPunctuation); c.CheckUnicodeCategory(out bool nextIsWhiteSpace, out bool nextIsPunctuation); - var prevIsExcepted = prevIsPunctuation && punctuationExceptions.Contains(pc); - var nextIsExcepted = nextIsPunctuation && punctuationExceptions.Contains(c); + var prevIsExcepted = prevIsPunctuation && IsPunctuationException(pc); + var nextIsExcepted = nextIsPunctuation && IsPunctuationException(c); // A left-flanking delimiter run is a delimiter run that is // (1) not followed by Unicode whitespace, and either @@ -126,19 +128,6 @@ public static bool IsAcrossTab(int column) return (column & (TabSize - 1)) != 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool Contains(this char[] charList, char c) - { - foreach (char ch in charList) - { - if (ch == c) - { - return true; - } - } - return false; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsWhitespace(this char c) { @@ -178,7 +167,7 @@ static bool IsWhitespaceRare(char c) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsControl(this char c) { - return c < ' ' || char.IsControl(c); + return char.IsControl(c); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -211,15 +200,17 @@ public static void CheckUnicodeCategory(this char c, out bool space, out bool pu { // A Unicode punctuation character is an ASCII punctuation character // or anything in the general Unicode categories Pc, Pd, Pe, Pf, Pi, Po, or Ps. + const int PunctuationCategoryMask = + 1 << (int)UnicodeCategory.ConnectorPunctuation | + 1 << (int)UnicodeCategory.DashPunctuation | + 1 << (int)UnicodeCategory.OpenPunctuation | + 1 << (int)UnicodeCategory.ClosePunctuation | + 1 << (int)UnicodeCategory.InitialQuotePunctuation | + 1 << (int)UnicodeCategory.FinalQuotePunctuation | + 1 << (int)UnicodeCategory.OtherPunctuation; + space = false; - UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(c); - punctuation = category == UnicodeCategory.ConnectorPunctuation - || category == UnicodeCategory.DashPunctuation - || category == UnicodeCategory.OpenPunctuation - || category == UnicodeCategory.ClosePunctuation - || category == UnicodeCategory.InitialQuotePunctuation - || category == UnicodeCategory.FinalQuotePunctuation - || category == UnicodeCategory.OtherPunctuation; + punctuation = (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0; } } @@ -236,14 +227,16 @@ internal static bool IsSpaceOrPunctuation(this char c) } else { - var category = CharUnicodeInfo.GetUnicodeCategory(c); - return category == UnicodeCategory.ConnectorPunctuation - || category == UnicodeCategory.DashPunctuation - || category == UnicodeCategory.OpenPunctuation - || category == UnicodeCategory.ClosePunctuation - || category == UnicodeCategory.InitialQuotePunctuation - || category == UnicodeCategory.FinalQuotePunctuation - || category == UnicodeCategory.OtherPunctuation; + const int PunctuationCategoryMask = + 1 << (int)UnicodeCategory.ConnectorPunctuation | + 1 << (int)UnicodeCategory.DashPunctuation | + 1 << (int)UnicodeCategory.OpenPunctuation | + 1 << (int)UnicodeCategory.ClosePunctuation | + 1 << (int)UnicodeCategory.InitialQuotePunctuation | + 1 << (int)UnicodeCategory.FinalQuotePunctuation | + 1 << (int)UnicodeCategory.OtherPunctuation; + + return (PunctuationCategoryMask & (1 << (int)CharUnicodeInfo.GetUnicodeCategory(c))) != 0; } } diff --git a/src/Markdig/Helpers/CharacterMap.cs b/src/Markdig/Helpers/CharacterMap.cs index 2835131b3..33064bfc3 100644 --- a/src/Markdig/Helpers/CharacterMap.cs +++ b/src/Markdig/Helpers/CharacterMap.cs @@ -2,14 +2,10 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using System.Buffers; using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; -#if NETCOREAPP3_1_OR_GREATER -using System.Numerics; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -#endif namespace Markdig.Helpers; @@ -19,13 +15,9 @@ namespace Markdig.Helpers; /// public sealed class CharacterMap where T : class { -#if NETCOREAPP3_1_OR_GREATER - private readonly Vector128 _asciiBitmap; -#endif - - private readonly T[] asciiMap; - private readonly Dictionary? nonAsciiMap; - private readonly BoolVector128 isOpeningCharacter; + private readonly SearchValues _values; + private readonly T[] _asciiMap; + private readonly Dictionary? _nonAsciiMap; /// /// Initializes a new instance of the class. @@ -35,64 +27,38 @@ public sealed class CharacterMap where T : class public CharacterMap(IEnumerable> maps) { if (maps is null) ThrowHelper.ArgumentNullException(nameof(maps)); + var charSet = new HashSet(); - int maxChar = 0; foreach (var map in maps) { - var openingChar = map.Key; - charSet.Add(openingChar); - - if (openingChar < 128) - { - maxChar = Math.Max(maxChar, openingChar); - - if (openingChar == 0) - { - ThrowHelper.ArgumentOutOfRangeException("Null is not a valid opening character.", nameof(maps)); - } - } - else - { - nonAsciiMap ??= new Dictionary(); - } + charSet.Add(map.Key); } OpeningCharacters = charSet.ToArray(); Array.Sort(OpeningCharacters); - asciiMap = new T[maxChar + 1]; + _asciiMap = new T[128]; foreach (var state in maps) { char openingChar = state.Key; if (openingChar < 128) { - asciiMap[openingChar] ??= state.Value; - isOpeningCharacter.Set(openingChar); + _asciiMap[openingChar] ??= state.Value; } - else if (!nonAsciiMap!.ContainsKey(openingChar)) + else { - nonAsciiMap[openingChar] = state.Value; - } - } + _nonAsciiMap ??= new Dictionary(); -#if NETCOREAPP3_1_OR_GREATER - if (nonAsciiMap is null) - { - long bitmap_0_3 = 0; - long bitmap_4_7 = 0; - - foreach (char openingChar in OpeningCharacters) - { - int position = (openingChar >> 4) | ((openingChar & 0x0F) << 3); - if (position < 64) bitmap_0_3 |= 1L << position; - else bitmap_4_7 |= 1L << (position - 64); + if (!_nonAsciiMap.ContainsKey(openingChar)) + { + _nonAsciiMap[openingChar] = state.Value; + } } - - _asciiBitmap = Vector128.Create(bitmap_0_3, bitmap_4_7).AsByte(); } -#endif + + _values = SearchValues.Create(OpeningCharacters); } /// @@ -110,7 +76,7 @@ public T? this[uint openingChar] [MethodImpl(MethodImplOptions.AggressiveInlining)] get { - T[] asciiMap = this.asciiMap; + T[] asciiMap = _asciiMap; if (openingChar < (uint)asciiMap.Length) { return asciiMap[openingChar]; @@ -118,13 +84,12 @@ public T? this[uint openingChar] else { T? map = null; - nonAsciiMap?.TryGetValue(openingChar, out map); + _nonAsciiMap?.TryGetValue(openingChar, out map); return map; } } } - /// /// Searches for an opening character from a registered parser in the specified string. /// @@ -132,167 +97,20 @@ public T? this[uint openingChar] /// The start. /// The end. /// Index position within the string of the first opening character found in the specified text; if not found, returns -1 + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int IndexOfOpeningCharacter(string text, int start, int end) { Debug.Assert(text is not null); - Debug.Assert(start >= 0 && end >= 0); - Debug.Assert(end - start + 1 >= 0); - Debug.Assert(end - start + 1 <= text.Length); - - if (nonAsciiMap is null) - { -#if NETCOREAPP3_1_OR_GREATER - if (Ssse3.IsSupported && BitConverter.IsLittleEndian) - { - // Based on http://0x80.pl/articles/simd-byte-lookup.html#universal-algorithm - // Optimized for sets in the [1, 127] range - - int lengthMinusOne = end - start; - int charsToProcessVectorized = lengthMinusOne & ~(2 * Vector128.Count - 1); - int finalStart = start + charsToProcessVectorized; - - if (start < finalStart) - { - ref char textStartRef = ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start); - Vector128 bitmap = _asciiBitmap; - do - { - // Load 32 bytes (16 chars) into two Vector128s (chars) - // Drop the high byte of each char - // Pack the remaining bytes into a single Vector128 - Vector128 input = Sse2.PackUnsignedSaturate( - Unsafe.ReadUnaligned>(ref Unsafe.As(ref textStartRef)), - Unsafe.ReadUnaligned>(ref Unsafe.As(ref Unsafe.Add(ref textStartRef, Vector128.Count)))); - // Extract the higher nibble of each character ((input >> 4) & 0xF) - Vector128 higherNibbles = Sse2.And(Sse2.ShiftRightLogical(input.AsUInt16(), 4).AsByte(), Vector128.Create((byte)0xF)); + ReadOnlySpan span = text.AsSpan(start, end - start + 1); - // Lookup the matching higher nibble for each character based on the lower nibble - // PSHUFB will set the result to 0 for any non-ASCII (> 127) character - Vector128 bitsets = Ssse3.Shuffle(bitmap, input); + int index = span.IndexOfAny(_values); - // Calculate a bitmask (1 << (higherNibble % 8)) for each character - Vector128 bitmask = Ssse3.Shuffle(Vector128.Create(0x8040201008040201).AsByte(), higherNibbles); - - // Check which characters are present in the set - // We are relying on bitsets being zero for non-ASCII characters - Vector128 result = Sse2.And(bitsets, bitmask); - - if (!result.Equals(Vector128.Zero)) - { - int resultMask = ~Sse2.MoveMask(Sse2.CompareEqual(result, Vector128.Zero)); - return start + BitOperations.TrailingZeroCount((uint)resultMask); - } - - start += 2 * Vector128.Count; - textStartRef = ref Unsafe.Add(ref textStartRef, 2 * Vector128.Count); - } - while (start != finalStart); - } - } - - ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference()); - for (; start <= end; start++) - { - if (IntPtr.Size == 4) - { - uint c = Unsafe.Add(ref textRef, start); - if (c < 128 && isOpeningCharacter[c]) - { - return start; - } - } - else - { - ulong c = Unsafe.Add(ref textRef, start); - if (c < 128 && isOpeningCharacter[c]) - { - return start; - } - } - } -#else - unsafe - { - fixed (char* pText = text) - { - for (int i = start; i <= end; i++) - { - char c = pText[i]; - if (c < 128 && isOpeningCharacter[c]) - { - return i; - } - } - } - } -#endif - return -1; - } - else + if (index >= 0) { - return IndexOfOpeningCharacterNonAscii(text, start, end); + index += start; } - } - private int IndexOfOpeningCharacterNonAscii(string text, int start, int end) - { -#if NETCOREAPP3_1_OR_GREATER - ref char textRef = ref Unsafe.AsRef(in text.GetPinnableReference()); - for (int i = start; i <= end; i++) - { - char c = Unsafe.Add(ref textRef, i); - if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c)) - { - return i; - } - } -#else - unsafe - { - fixed (char* pText = text) - { - for (int i = start; i <= end; i++) - { - char c = pText[i]; - if (c < 128 ? isOpeningCharacter[c] : nonAsciiMap!.ContainsKey(c)) - { - return i; - } - } - } - } -#endif - return -1; + return index; } } - -internal unsafe struct BoolVector128 -{ - private fixed bool values[128]; - - public void Set(char c) - { - Debug.Assert(c < 128); - values[c] = true; - } - - public readonly bool this[uint c] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - Debug.Assert(c < 128); - return values[c]; - } - } - public readonly bool this[ulong c] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - Debug.Assert(c < 128 && IntPtr.Size == 8); - return values[c]; - } - } -} \ No newline at end of file diff --git a/src/Markdig/Helpers/FastStringWriter.cs b/src/Markdig/Helpers/FastStringWriter.cs index d2e731ebf..e0995f749 100644 --- a/src/Markdig/Helpers/FastStringWriter.cs +++ b/src/Markdig/Helpers/FastStringWriter.cs @@ -278,8 +278,7 @@ public void Reset() _pos = 0; } - public override string ToString() - { - return _chars.AsSpan(0, _pos).ToString(); - } + public override string ToString() => AsSpan().ToString(); + + public ReadOnlySpan AsSpan() => _chars.AsSpan(0, _pos); } diff --git a/src/Markdig/Helpers/HexConverter.cs b/src/Markdig/Helpers/HexConverter.cs new file mode 100644 index 000000000..de5ca4a9d --- /dev/null +++ b/src/Markdig/Helpers/HexConverter.cs @@ -0,0 +1,27 @@ +// 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.Runtime.CompilerServices; + +namespace Markdig.Helpers; + +// Based on https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/HexConverter.cs +internal static class HexConverter +{ + public enum Casing : uint + { + Upper = 0, + Lower = 0x2020U, + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ToCharsBuffer(byte value, Span buffer, int startingIndex = 0, Casing casing = Casing.Upper) + { + uint difference = (((uint)value & 0xF0U) << 4) + ((uint)value & 0x0FU) - 0x8989U; + uint packedResult = ((((uint)(-(int)difference) & 0x7070U) >> 4) + difference + 0xB9B9U) | (uint)casing; + + buffer[startingIndex + 1] = (char)(packedResult & 0xFF); + buffer[startingIndex] = (char)(packedResult >> 8); + } +} diff --git a/src/Markdig/Helpers/LineReader.cs b/src/Markdig/Helpers/LineReader.cs index 0b2938384..d8425c6dd 100644 --- a/src/Markdig/Helpers/LineReader.cs +++ b/src/Markdig/Helpers/LineReader.cs @@ -53,7 +53,7 @@ public StringSlice ReadLine() else { #if NETCOREAPP3_1_OR_GREATER - ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), sourcePosition), end - sourcePosition); + ReadOnlySpan span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), sourcePosition), end - sourcePosition); #else ReadOnlySpan span = text.AsSpan(sourcePosition); #endif @@ -65,7 +65,7 @@ public StringSlice ReadLine() newSourcePosition = end + 1; #if NETCOREAPP3_1_OR_GREATER - if (Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), end) == '\r') + if (Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), end) == '\r') #else if ((uint)end < (uint)text.Length && text[end] == '\r') #endif diff --git a/src/Markdig/Helpers/LinkHelper.cs b/src/Markdig/Helpers/LinkHelper.cs index 52c0c027c..a7d2ae553 100644 --- a/src/Markdig/Helpers/LinkHelper.cs +++ b/src/Markdig/Helpers/LinkHelper.cs @@ -19,6 +19,11 @@ public static bool TryParseAutolink(StringSlice text, [NotNullWhen(true)] out st } public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false) + { + return Urilize(headingText.AsSpan(), allowOnlyAscii, keepOpeningDigits); + } + + public static string Urilize(ReadOnlySpan headingText, bool allowOnlyAscii, bool keepOpeningDigits = false) { var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); bool hasLetter = keepOpeningDigits && headingText.Length > 0 && char.IsLetterOrDigit(headingText[0]); @@ -95,15 +100,24 @@ public static string Urilize(string headingText, bool allowOnlyAscii, bool keepO } public static string UrilizeAsGfm(string headingText) + { + return UrilizeAsGfm(headingText.AsSpan()); + } + + public static string UrilizeAsGfm(ReadOnlySpan headingText) { // Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); for (int i = 0; i < headingText.Length; i++) { var c = headingText[i]; - if (char.IsLetterOrDigit(c) || c == ' ' || c == '-' || c == '_') + if (char.IsLetterOrDigit(c) || c == '-' || c == '_') + { + headingBuffer.Append(char.ToLowerInvariant(c)); + } + else if (c == ' ') { - headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c)); + headingBuffer.Append('-'); } } return headingBuffer.ToString(); diff --git a/src/Markdig/Helpers/ObjectCache.cs b/src/Markdig/Helpers/ObjectCache.cs index 9710ae033..3d0ab9cc5 100644 --- a/src/Markdig/Helpers/ObjectCache.cs +++ b/src/Markdig/Helpers/ObjectCache.cs @@ -36,7 +36,7 @@ public void Clear() /// public T Get() { - if (_builders.TryDequeue(out T instance)) + if (_builders.TryDequeue(out T? instance)) { return instance; } diff --git a/src/Markdig/Helpers/StringSlice.cs b/src/Markdig/Helpers/StringSlice.cs index ad250053e..dd27e4008 100644 --- a/src/Markdig/Helpers/StringSlice.cs +++ b/src/Markdig/Helpers/StringSlice.cs @@ -475,7 +475,7 @@ public readonly ReadOnlySpan AsSpan() } #if NETCOREAPP3_1_OR_GREATER - return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), start), length); + return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(in text.GetPinnableReference()), start), length); #else return text.AsSpan(start, length); #endif diff --git a/src/Markdig/Markdig.targets b/src/Markdig/Markdig.targets index 1ceb54dd4..c2244d570 100644 --- a/src/Markdig/Markdig.targets +++ b/src/Markdig/Markdig.targets @@ -5,7 +5,7 @@ Alexandre Mutel en-US Alexandre Mutel - net462;netstandard2.0;netstandard2.1;net6.0 + net462;netstandard2.0;netstandard2.1;net6.0;net8.0 false Markdown CommonMark md html md2html https://github.com/lunet-io/markdig/blob/master/changelog.md @@ -14,7 +14,7 @@ markdig.png https://github.com/lunet-io/markdig true - 10 + 12 enable $(NoWarn);CS1591 true diff --git a/src/Markdig/Markdown.cs b/src/Markdig/Markdown.cs index a1e4661fa..53dfdb785 100644 --- a/src/Markdig/Markdown.cs +++ b/src/Markdig/Markdown.cs @@ -19,16 +19,10 @@ namespace Markdig; /// public static class Markdown { - public static string Version - { - get - { - if (_Version == null) - _Version = ((AssemblyFileVersionAttribute)typeof(Markdown).Assembly.GetCustomAttributes(typeof(AssemblyFileVersionAttribute), false).FirstOrDefault())?.Version ?? "Unknown"; - return _Version; - } - } - private static string? _Version; + public static string Version => + s_version ??= typeof(Markdown).Assembly.GetCustomAttribute()?.Version ?? "Unknown"; + + private static string? s_version; internal static readonly MarkdownPipeline DefaultPipeline = new MarkdownPipelineBuilder().Build(); private static readonly MarkdownPipeline _defaultTrackTriviaPipeline = new MarkdownPipelineBuilder().EnableTrackTrivia().Build(); diff --git a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs index e53d2b9a4..588bc5c4c 100644 --- a/src/Markdig/Parsers/Inlines/CodeInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/CodeInlineParser.cs @@ -26,22 +26,16 @@ public CodeInlineParser() public override bool Match(InlineProcessor processor, ref StringSlice slice) { - var match = slice.CurrentChar; + char match = slice.CurrentChar; if (slice.PeekCharExtra(-1) == match) { return false; } - var startPosition = slice.Start; + Debug.Assert(match is not ('\r' or '\n')); // Match the opened sticks int openSticks = slice.CountAndSkipChar(match); - int contentStart = slice.Start; - int closeSticks = 0; - - char c = slice.CurrentChar; - - var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); // A backtick string is a string of one or more backtick characters (`) that is neither preceded nor followed by a backtick. // A code span begins with a backtick string and ends with a backtick string of equal length. @@ -54,91 +48,106 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) // This allows you to include code that begins or ends with backtick characters, which must be separated by // whitespace from the opening or closing backtick strings. - bool allSpace = true; - bool containsNewLine = false; - var contentEnd = -1; + ReadOnlySpan span = slice.AsSpan(); + bool containsNewLines = false; - while (c != '\0') + while (true) { - // Transform '\n' into a single space - if (c == '\n') - { - containsNewLine = true; - c = ' '; - } - else if (c == '\r') + int i = span.IndexOfAny('\r', '\n', match); + + if ((uint)i >= (uint)span.Length) { - containsNewLine = true; - slice.SkipChar(); - c = slice.CurrentChar; - continue; + // We got to the end of the input before seeing the match character. CodeInline can't match here. + return false; } - if (c == match) + int closeSticks = 0; + + while ((uint)i < (uint)span.Length && span[i] == match) { - contentEnd = slice.Start; - closeSticks = slice.CountAndSkipChar(match); + closeSticks++; + i++; + } - if (openSticks == closeSticks) - { - break; - } + span = span.Slice(i); - allSpace = false; - builder.Append(match, closeSticks); - c = slice.CurrentChar; + if (openSticks == closeSticks) + { + break; } - else + else if (closeSticks == 0) { - builder.Append(c); - if (c != ' ') - { - allSpace = false; - } - c = slice.NextChar(); + containsNewLines = true; + span = span.Slice(1); } } - bool isMatching = false; - if (closeSticks == openSticks) + ReadOnlySpan rawContent = slice.AsSpan().Slice(0, slice.Length - span.Length - openSticks); + + var content = containsNewLines + ? new LazySubstring(ReplaceNewLines(rawContent)) // Should be the rare path. + : new LazySubstring(slice.Text, slice.Start, rawContent.Length); + + // Remove one space from front and back if the string is not all spaces + if (rawContent.Length > 2 && + rawContent[0] is ' ' or '\n' && + rawContent[rawContent.Length - 1] is ' ' or '\n' && + rawContent.ContainsAnyExcept(' ', '\r', '\n')) { - ReadOnlySpan contentSpan = builder.AsSpan(); + content.Offset++; + content.Length -= 2; + } - var content = containsNewLine - ? new LazySubstring(contentSpan.ToString()) - : new LazySubstring(slice.Text, contentStart, contentSpan.Length); + int startPosition = slice.Start; + slice.Start = startPosition + rawContent.Length + openSticks; - Debug.Assert(contentSpan.SequenceEqual(content.AsSpan())); + // We've already skipped the opening sticks. Account for that here. + startPosition -= openSticks; - // Remove one space from front and back if the string is not all spaces - if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ') + var codeInline = new CodeInline(content) + { + Delimiter = slice.Text[startPosition], + Span = new SourceSpan(processor.GetSourcePosition(startPosition, out int line, out int column), processor.GetSourcePosition(slice.Start - 1)), + Line = line, + Column = column, + DelimiterCount = openSticks, + }; + + if (processor.TrackTrivia) + { + // startPosition and slice.Start include the opening/closing sticks. + codeInline.ContentWithTrivia = new StringSlice(slice.Text, startPosition + openSticks, slice.Start - openSticks - 1); + } + + processor.Inline = codeInline; + return true; + } + + private static string ReplaceNewLines(ReadOnlySpan content) + { + var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]); + + while (true) + { + int i = content.IndexOfAny('\r', '\n'); + + if ((uint)i >= (uint)content.Length) { - content.Offset++; - content.Length -= 2; + builder.Append(content); + break; } - int delimiterCount = Math.Min(openSticks, closeSticks); - var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column); - var spanEnd = processor.GetSourcePosition(slice.Start - 1); - var codeInline = new CodeInline(content) - { - Delimiter = match, - Span = new SourceSpan(spanStart, spanEnd), - Line = line, - Column = column, - DelimiterCount = delimiterCount, - }; - - if (processor.TrackTrivia) + builder.Append(content.Slice(0, i)); + + if (content[i] == '\n') { - codeInline.ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1); + // Transform '\n' into a single space + builder.Append(' '); } - processor.Inline = codeInline; - isMatching = true; + content = content.Slice(i + 1); } - builder.Dispose(); - return isMatching; + return builder.ToString(); } } \ No newline at end of file diff --git a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs index 225b537e8..023fa8c28 100644 --- a/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs +++ b/src/Markdig/Parsers/Inlines/LiteralInlineParser.cs @@ -32,16 +32,12 @@ public LiteralInlineParser() public override bool Match(InlineProcessor processor, ref StringSlice slice) { - var text = slice.Text; + string text = slice.Text; - var startPosition = processor.GetSourcePosition(slice.Start, out int line, out int column); - - // Slightly faster to perform our own search for opening characters - var nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End); - //var nextStart = str.IndexOfAny(processor.SpecialCharacters, slice.Start + 1, slice.Length - 1); + int nextStart = processor.Parsers.IndexOfOpeningCharacter(text, slice.Start + 1, slice.End); int length; - if (nextStart < 0) + if ((uint)nextStart >= (uint)text.Length) { nextStart = slice.End + 1; length = nextStart - slice.Start; @@ -50,10 +46,10 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) { // Remove line endings if the next char is a new line length = nextStart - slice.Start; + if (!processor.TrackTrivia) { - var nextText = text[nextStart]; - if (nextText == '\n' || nextText == '\r') + if (text[nextStart] is '\n' or '\r') { int end = nextStart - 1; while (length > 0 && text[end].IsSpace()) @@ -86,7 +82,7 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice) processor.Inline = new LiteralInline { Content = length > 0 ? newSlice : StringSlice.Empty, - Span = new SourceSpan(startPosition, processor.GetSourcePosition(endPosition)), + Span = new SourceSpan(processor.GetSourcePosition(slice.Start, out int line, out int column), processor.GetSourcePosition(endPosition)), Line = line, Column = column, }; diff --git a/src/Markdig/Polyfills/Ascii.cs b/src/Markdig/Polyfills/Ascii.cs new file mode 100644 index 000000000..34841296b --- /dev/null +++ b/src/Markdig/Polyfills/Ascii.cs @@ -0,0 +1,30 @@ +// 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. + +#if !NET8_0_OR_GREATER + +namespace System.Text; + +internal static class Ascii +{ + public static bool IsValid(this string value) + { + return IsValid(value.AsSpan()); + } + + public static bool IsValid(this ReadOnlySpan value) + { + for (int i = 0; i < value.Length; i++) + { + if (value[i] > 127) + { + return false; + } + } + + return true; + } +} + +#endif \ No newline at end of file diff --git a/src/Markdig/Polyfills/EncodingExtensions.cs b/src/Markdig/Polyfills/EncodingExtensions.cs new file mode 100644 index 000000000..14d3845a3 --- /dev/null +++ b/src/Markdig/Polyfills/EncodingExtensions.cs @@ -0,0 +1,24 @@ +// 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. + +#if !NETSTANDARD2_1_OR_GREATER + +using System.Runtime.InteropServices; + +namespace System.Text; + +internal static class EncodingExtensions +{ + public static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan chars, Span bytes) + { + fixed (char* charsPtr = &MemoryMarshal.GetReference(chars)) + { + fixed (byte* bytesPtr = &MemoryMarshal.GetReference(bytes)) + { + return encoding.GetBytes(charsPtr, chars.Length, bytesPtr, bytes.Length); + } + } + } +} +#endif diff --git a/src/Markdig/Polyfills/IndexOfHelpers.cs b/src/Markdig/Polyfills/IndexOfHelpers.cs new file mode 100644 index 000000000..bca57be3e --- /dev/null +++ b/src/Markdig/Polyfills/IndexOfHelpers.cs @@ -0,0 +1,46 @@ +// 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. + +#if !NET8_0_OR_GREATER + +namespace System; + +internal static class IndexOfHelpers +{ + public static bool ContainsAnyExcept(this ReadOnlySpan span, char value0, char value1, char value2) + { + for (int i = 0; i < span.Length; i++) + { + char c = span[i]; + if (c != value0 && c != value1 && c != value2) + { + return true; + } + } + + return false; + } + +#if !NETSTANDARD2_1_OR_GREATER + public static int IndexOfAny(this ReadOnlySpan span, string values) + { + for (int i = 0; i < span.Length; i++) + { + char c = span[i]; + + foreach (char v in values) + { + if (c == v) + { + return i; + } + } + } + + return -1; + } +#endif +} + +#endif \ No newline at end of file diff --git a/src/Markdig/Polyfills/SearchValues.cs b/src/Markdig/Polyfills/SearchValues.cs new file mode 100644 index 000000000..5d19c0255 --- /dev/null +++ b/src/Markdig/Polyfills/SearchValues.cs @@ -0,0 +1,137 @@ +// 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. + +#if !NET8_0_OR_GREATER + +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Buffers; + +internal static class SearchValues +{ + public static SearchValues Create(string values) => + Create(values.AsSpan()); + + public static SearchValues Create(ReadOnlySpan values) => + new PreNet8CompatSearchValues(values); + + public static int IndexOfAny(this ReadOnlySpan span, SearchValues values) => + values.IndexOfAny(span); + + public static int IndexOfAnyExcept(this ReadOnlySpan span, SearchValues values) => + values.IndexOfAnyExcept(span); +} + +internal abstract class SearchValues +{ + public abstract int IndexOfAny(ReadOnlySpan span); + + public abstract int IndexOfAnyExcept(ReadOnlySpan span); +} + +internal sealed class PreNet8CompatSearchValues : SearchValues +{ + private readonly BoolVector128 _ascii; + private readonly HashSet? _nonAscii; + + public PreNet8CompatSearchValues(ReadOnlySpan values) + { + foreach (char c in values) + { + if (c < 128) + { + _ascii.Set(c); + } + else + { + _nonAscii ??= new HashSet(); + _nonAscii.Add(c); + } + } + } + + public override int IndexOfAny(ReadOnlySpan span) + { + if (_nonAscii is null) + { + for (int i = 0; i < span.Length; i++) + { + char c = span[i]; + + if (c < 128 && _ascii[c]) + { + return i; + } + } + } + else + { + for (int i = 0; i < span.Length; i++) + { + char c = span[i]; + + if (c < 128 ? _ascii[c] : _nonAscii.Contains(c)) + { + return i; + } + } + } + + return -1; + } + + public override int IndexOfAnyExcept(ReadOnlySpan span) + { + if (_nonAscii is null) + { + for (int i = 0; i < span.Length; i++) + { + char c = span[i]; + + if (c >= 128 || !_ascii[c]) + { + return i; + } + } + } + else + { + for (int i = 0; i < span.Length; i++) + { + char c = span[i]; + + if (c < 128 ? !_ascii[c] : !_nonAscii.Contains(c)) + { + return i; + } + } + } + + return -1; + } + + private unsafe struct BoolVector128 + { + private fixed bool _values[128]; + + public void Set(char c) + { + Debug.Assert(c < 128); + _values[c] = true; + } + + public readonly bool this[uint c] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + Debug.Assert(c < 128); + return _values[c]; + } + } + } +} + +#endif \ No newline at end of file diff --git a/src/Markdig/Renderers/HtmlRenderer.cs b/src/Markdig/Renderers/HtmlRenderer.cs index 1b41b339a..27b5d3616 100644 --- a/src/Markdig/Renderers/HtmlRenderer.cs +++ b/src/Markdig/Renderers/HtmlRenderer.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using System.Buffers; using System.Globalization; using System.IO; using System.Runtime.CompilerServices; @@ -20,7 +21,10 @@ namespace Markdig.Renderers; /// public class HtmlRenderer : TextRendererBase { - private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' }; + private static readonly IdnMapping s_idnMapping = new(); + + private static readonly SearchValues s_asciiNonEscapeChars = + SearchValues.Create("!#$%()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"); /// /// Initializes a new instance of the class. @@ -149,73 +153,38 @@ public void WriteEscape(ReadOnlySpan content, bool softEscape = false) { if (!content.IsEmpty) { - int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars); - if (nextIndex == -1) - { - Write(content); - } - else + WriteIndent(); + + while (true) { - WriteEscapeSlow(content, softEscape); - } - } - } + int indexOfCharToEscape = softEscape + ? content.IndexOfAny('<', '&') + : content.IndexOfAny("<>&\""); - private void WriteEscapeSlow(ReadOnlySpan content, bool softEscape = false) - { - WriteIndent(); + if ((uint)indexOfCharToEscape >= (uint)content.Length) + { + WriteRaw(content); + return; + } - int previousOffset = 0; - for (int i = 0; i < content.Length; i++) - { - switch (content[i]) - { - case '<': - WriteRaw(content.Slice(previousOffset, i - previousOffset)); - if (EnableHtmlEscape) - { - WriteRaw("<"); - } - previousOffset = i + 1; - break; - case '>': - if (!softEscape) - { - WriteRaw(content.Slice(previousOffset, i - previousOffset)); - if (EnableHtmlEscape) - { - WriteRaw(">"); - } - previousOffset = i + 1; - } - break; - case '&': - WriteRaw(content.Slice(previousOffset, i - previousOffset)); - if (EnableHtmlEscape) - { - WriteRaw("&"); - } - previousOffset = i + 1; - break; - case '"': - if (!softEscape) + WriteRaw(content.Slice(0, indexOfCharToEscape)); + + if (EnableHtmlEscape) + { + WriteRaw(content[indexOfCharToEscape] switch { - WriteRaw(content.Slice(previousOffset, i - previousOffset)); - if (EnableHtmlEscape) - { - WriteRaw("""); - } - previousOffset = i + 1; - } - break; + '<' => "<", + '>' => ">", + '&' => "&", + _ => """, + }); + } + + content = content.Slice(indexOfCharToEscape + 1); } } - - WriteRaw(content.Slice(previousOffset)); } - private static readonly IdnMapping IdnMapping = new IdnMapping(); - /// /// Writes the URL escaped for HTML. /// @@ -239,120 +208,107 @@ public HtmlRenderer WriteEscapeUrl(string? content) content = LinkRewriter(content); } - // a://c.d = 7 chars - int schemeOffset = content.Length < 7 ? -1 : content.IndexOf("://", StringComparison.Ordinal); - if (schemeOffset != -1) // This is an absolute URL + if (!Ascii.IsValid(content)) { - schemeOffset += 3; // skip :// - WriteEscapeUrl(content, 0, schemeOffset); - - bool idnaEncodeDomain = false; - int endOfDomain = schemeOffset; - for (; endOfDomain < content.Length; endOfDomain++) + int schemeOffset = content.IndexOf("://", StringComparison.Ordinal); + if (schemeOffset > 0) // This is an absolute URL { - char c = content[endOfDomain]; - if (c == '/' || c == '?' || c == '#' || c == ':') // End of domain part - { - break; - } - if (c > 127) + schemeOffset += 3; // skip :// + + int domainLength = content.AsSpan(schemeOffset).IndexOfAny("/?#:"); + if (domainLength < 0) { - idnaEncodeDomain = true; + domainLength = content.Length - schemeOffset; } - } - if (idnaEncodeDomain) - { - string domainName; + string? domainName = null; try { - domainName = IdnMapping.GetAscii(content, schemeOffset, endOfDomain - schemeOffset); + domainName = s_idnMapping.GetAscii(content, schemeOffset, domainLength); } - catch + catch { } + + if (domainName is not null) { - // Not a valid IDN, fallback to non-punycode encoding - WriteEscapeUrl(content, schemeOffset, content.Length); + WriteEscapeUrlCore(content.AsSpan(0, schemeOffset)); + WriteEscapeUrlCore(domainName.AsSpan()); + WriteEscapeUrlCore(content.AsSpan(schemeOffset + domainLength)); return this; } - // Escape the characters (see Commonmark example 327 and think of it with a non-ascii symbol) - int previousPosition = 0; - for (int i = 0; i < domainName.Length; i++) - { - var escape = HtmlHelper.EscapeUrlCharacter(domainName[i]); - if (escape != null) - { - Write(domainName, previousPosition, i - previousPosition); - previousPosition = i + 1; - Write(escape); - } - } - Write(domainName, previousPosition, domainName.Length - previousPosition); - WriteEscapeUrl(content, endOfDomain, content.Length); + // Not a valid IDN, fallback to non-punycode encoding } - else - { - WriteEscapeUrl(content, schemeOffset, content.Length); - } - } - else // This is a relative URL - { - WriteEscapeUrl(content, 0, content.Length); } + WriteEscapeUrlCore(content.AsSpan()); return this; } - private void WriteEscapeUrl(string content, int start, int length) + private void WriteEscapeUrlCore(ReadOnlySpan content) { - int previousPosition = start; - for (var i = previousPosition; i < length; i++) + WriteIndent(); + + while (true) { - var c = content[i]; + int i = content.IndexOfAnyExcept(s_asciiNonEscapeChars); + + if ((uint)i >= (uint)content.Length) + { + WriteRaw(content); + break; + } + + WriteRaw(content.Slice(0, i)); + + char c = content[i]; if (c < 128) { - var escape = HtmlHelper.EscapeUrlCharacter(c); - if (escape != null) - { - Write(content, previousPosition, i - previousPosition); - previousPosition = i + 1; - Write(escape); - } + WriteRaw(HtmlHelper.EscapeUrlCharacter(c)); + } + else if (UseNonAsciiNoEscape) + { + // Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working + WriteRaw(c); } else { - Write(content, previousPosition, i - previousPosition); - previousPosition = i + 1; + i = WriteEscapedUtf8Bytes(this, content, c, i); + } - // Special case for Edge/IE workaround for MarkdownEditor, don't escape non-ASCII chars to make image links working - if (UseNonAsciiNoEscape) - { - Write(c); - } - else - { - byte[] bytes; - if (c >= '\ud800' && c <= '\udfff' && previousPosition < length) - { - bytes = Encoding.UTF8.GetBytes(new[] { c, content[previousPosition] }); - // Skip next char as it is decoded above - i++; - previousPosition = i + 1; - } - else - { - bytes = Encoding.UTF8.GetBytes(new[] { c }); - } - for (var j = 0; j < bytes.Length; j++) - { - Write($"%{bytes[j]:X2}"); - } - } + content = content.Slice(i + 1); + } + + static int WriteEscapedUtf8Bytes(HtmlRenderer renderer, ReadOnlySpan content, char c, int i) + { + scoped ReadOnlySpan chars; + + if (CharHelper.IsHighSurrogate(c) && (uint)(i + 1) < (uint)content.Length) + { + chars = stackalloc char[] { c, content[i + 1] }; + i++; } + else + { + chars = stackalloc char[] { c }; + } + + Span utf8Buffer = stackalloc byte[4]; + int utf8Length = Encoding.UTF8.GetBytes(chars, utf8Buffer); + utf8Buffer = utf8Buffer.Slice(0, utf8Length); + + Span escapedBuffer = stackalloc char[3]; + escapedBuffer[0] = '%'; + + foreach (byte b in utf8Buffer) + { + HexConverter.ToCharsBuffer(b, escapedBuffer, startingIndex: 1); + renderer.WriteRaw(escapedBuffer); + } + + return i; } - Write(content, previousPosition, length - previousPosition); } /// diff --git a/src/Markdig/Syntax/Inlines/ContainerInline.cs b/src/Markdig/Syntax/Inlines/ContainerInline.cs index 3aaa1f555..57b23cda7 100644 --- a/src/Markdig/Syntax/Inlines/ContainerInline.cs +++ b/src/Markdig/Syntax/Inlines/ContainerInline.cs @@ -16,7 +16,7 @@ namespace Markdig.Syntax.Inlines; /// public class ContainerInline : Inline, IEnumerable { - public ContainerInline() + public ContainerInline() : base(dummySkipTypeKind: true) { SetTypeKind(isInline: true, isContainer: true); } diff --git a/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs b/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs index b472eb441..7365083c5 100644 --- a/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs +++ b/src/Markdig/Syntax/Inlines/EmphasisDelimiterInline.cs @@ -69,7 +69,21 @@ internal EmphasisDelimiterInline(InlineParser parser, EmphasisDescriptor descrip public override string ToLiteral() { - return DelimiterCount > 0 ? new string(DelimiterChar, DelimiterCount) : string.Empty; + if (DelimiterCount == 1) + { + return DelimiterChar switch + { + '*' => "*", + '_' => "_", + '~' => "~", + '^' => "^", + '+' => "+", + '=' => "=", + _ => DelimiterChar.ToString() + }; + } + + return new string(DelimiterChar, DelimiterCount); } public LiteralInline AsLiteralInline() diff --git a/src/Markdig/Syntax/Inlines/Inline.cs b/src/Markdig/Syntax/Inlines/Inline.cs index 785c5ee14..47f33af28 100644 --- a/src/Markdig/Syntax/Inlines/Inline.cs +++ b/src/Markdig/Syntax/Inlines/Inline.cs @@ -20,6 +20,8 @@ protected Inline() SetTypeKind(isInline: true, isContainer: false); } + private protected Inline(bool dummySkipTypeKind) { } + /// /// Gets the parent container of this inline. /// diff --git a/src/Markdig/Syntax/MarkdownObject.cs b/src/Markdig/Syntax/MarkdownObject.cs index 293697fa1..da5bfa050 100644 --- a/src/Markdig/Syntax/MarkdownObject.cs +++ b/src/Markdig/Syntax/MarkdownObject.cs @@ -2,6 +2,7 @@ // This file is licensed under the BSD-Clause 2 license. // See the license.txt file in the project root for more information. +using System.Diagnostics; using System.Runtime.CompilerServices; using Markdig.Helpers; @@ -36,7 +37,8 @@ public abstract class MarkdownObject : IMarkdownObject [MethodImpl(MethodImplOptions.AggressiveInlining)] private protected void SetTypeKind(bool isInline, bool isContainer) { - _lineBits |= (isInline ? IsInlineMask : 0) | (isContainer ? IsContainerMask : 0); + Debug.Assert(_lineBits == 0); + _lineBits = (isInline ? IsInlineMask : 0) | (isContainer ? IsContainerMask : 0); } private protected bool IsClosedInternal diff --git a/src/global.json b/src/global.json index b7e931338..72d38cd27 100644 --- a/src/global.json +++ b/src/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.100", + "version": "8.0.100", "rollForward": "latestMajor", "allowPrerelease": false }