Compare commits

..

33 Commits

Author SHA1 Message Date
Alexandre Mutel
ccf455d316 Fix AssemblyVersion with MinVer (#612) 2022-03-27 10:15:16 +02:00
Alexandre Mutel
8beb096814 Fix emphasis parsing with table delimiters (#614) 2022-03-27 10:04:14 +02:00
Alexandre Mutel
6a35ec45b9 Merge pull request #611 from MihaZupan/renderer-perf
Improve rendering performance
2022-03-27 09:45:19 +02:00
Miha Zupan
ed83943ba5 Use custom StringWriter for rendering internally 2022-03-20 13:49:38 +01:00
Miha Zupan
9adf60116b More WriteRaw 2022-03-20 11:24:05 +01:00
Miha Zupan
31904f6c53 Avoid allocating WriteEscapeIndexOfAnyChars
Roslyn doesn't support static char arrays yet
2022-03-20 10:54:09 +01:00
Miha Zupan
3f3b3c46b6 Optimize renderers 2022-03-20 10:21:36 +01:00
Miha Zupan
f3d6c2775b Add Unsafe.As polyfill for NETSTANDARD 2.1 2022-03-20 04:02:16 +01:00
Miha Zupan
bb6ace15b7 Optimize RendererBase.Write 2022-03-20 03:39:47 +01:00
Miha Zupan
202ac1e4f9 Simplify RendererBase ctor 2022-03-20 02:36:02 +01:00
Miha Zupan
2604239764 Move TryGetRenderer to cold path 2022-03-20 02:12:35 +01:00
Miha Zupan
cc04208b95 Change IMarkdownObjectRenderer.Accept to take a Type instead of instance 2022-03-20 02:06:08 +01:00
Miha Zupan
14ab45cf8f Move TryWriters to cold path 2022-03-20 01:56:48 +01:00
Miha Zupan
e36d4564f1 Remove NormalizeAutoLinkRenderer 2022-03-20 01:53:20 +01:00
Alexandre Mutel
358a5f09ef Merge pull request #608 from MihaZupan/perf-march-2022-2
Some CPU improvements
2022-03-19 14:24:43 +01:00
Miha Zupan
315ffd42ab Move InternalsVisibleTo from targets to non-signed csproj 2022-03-14 11:33:04 +01:00
Miha Zupan
2675b4dd1e Fixup FencedBlockParserBase nullability 2022-03-14 11:31:42 +01:00
Miha Zupan
58d7fae12d Cleanup exit condition in Unescape 2022-03-14 11:11:35 +01:00
Miha Zupan
e16ed79dcd Optimize StringLineGroup iteration 2022-03-14 10:58:21 +01:00
Miha Zupan
9ef5171369 Reduce type check and casting overhead 2022-03-14 10:56:59 +01:00
Alexandre Mutel
0cfe6d7da4 Merge pull request #606 from MihaZupan/perf-march-2022
Improvements when TrackTrivia is disabled
2022-03-14 09:46:26 +01:00
Miha Zupan
fe65c1b187 Add & use ValueStringBuilder 2022-03-14 07:36:10 +01:00
Miha Zupan
92385ee19a Set SkipLocalsInit for Markdig 2022-03-14 05:05:35 +01:00
Miha Zupan
9f651feac0 Properly trim & cache FencedCodeBlock Info strings 2022-03-14 04:23:44 +01:00
Miha Zupan
b7d02cadbb Fix TrackTrivia /// comments 2022-03-13 01:54:44 +01:00
Miha Zupan
6f75b5156c Aggressively avoid TrackTrivia work and allocations when not requested for Inlines 2022-03-13 01:34:47 +01:00
Miha Zupan
61452c91e9 Aggressively avoid TrackTrivia work and allocations when not requested 2022-03-13 01:21:33 +01:00
Miha Zupan
b697a03c2b Lazily allocate Trivia properties on LinkReferenceDefinition 2022-03-12 23:34:30 +01:00
Miha Zupan
9f734ba3c9 Lazily allocate Trivia properties on LinkInline 2022-03-12 23:28:16 +01:00
Miha Zupan
88cdbf3a17 Lazily allocate CodeBlock.CodeBlockLines 2022-03-12 22:42:15 +01:00
Miha Zupan
fb9561cf89 Fix roughLineCountEstimate calculation (min/max order) 2022-03-12 22:26:41 +01:00
Miha Zupan
9145f47f89 Move TryParseInlineLinkTrivia to cold path 2022-03-12 22:25:52 +01:00
Miha Zupan
1862b37bbd Optimize LineReader.ReadLine 2022-03-12 02:18:50 +01:00
76 changed files with 2154 additions and 815 deletions

View File

@@ -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));
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Linq;
using Markdig.Syntax;
using NUnit.Framework;
namespace Markdig.Tests
{
public class TestFencedCodeBlocks
{
[Test]
[TestCase("c#", "c#", "")]
[TestCase("C#", "C#", "")]
[TestCase(" c#", "c#", "")]
[TestCase(" c# ", "c#", "")]
[TestCase(" \tc# ", "c#", "")]
[TestCase("\t c# \t", "c#", "")]
[TestCase(" c# ", "c#", "")]
[TestCase(" c# foo", "c#", "foo")]
[TestCase(" c# \t fOo \t", "c#", "fOo")]
[TestCase("in\\%fo arg\\%ument", "in%fo", "arg%ument")]
[TestCase("info&#9; arg&acute;ument", "info\t", "arg\u00B4ument")]
public void TestInfoAndArguments(string infoString, string expectedInfo, string expectedArguments)
{
Test('`');
Test('~');
void Test(char fencedChar)
{
const string Contents = "Foo\nBar\n";
string fence = new string(fencedChar, 3);
string markdownText = $"{fence}{infoString}\n{Contents}\n{fence}\n";
MarkdownDocument document = Markdown.Parse(markdownText);
FencedCodeBlock codeBlock = document.Descendants<FencedCodeBlock>().Single();
Assert.AreEqual(fencedChar, codeBlock.FencedChar);
Assert.AreEqual(3, codeBlock.OpeningFencedCharCount);
Assert.AreEqual(3, codeBlock.ClosingFencedCharCount);
Assert.AreEqual(expectedInfo, codeBlock.Info);
Assert.AreEqual(expectedArguments, codeBlock.Arguments);
Assert.AreEqual(Contents, codeBlock.Lines.ToString());
}
}
}
}

View File

@@ -12,6 +12,12 @@ namespace Markdig.Tests
[TestFixture]
public class TestPlayParser
{
[Test]
public void TestBugWithEmphasisAndTable()
{
TestParser.TestSpec("**basics | 8:00**", "<p><strong>basics | 8:00</strong></p>", "advanced");
}
[Test]
public void TestLinksWithCarriageReturn()
{

View File

@@ -0,0 +1,102 @@
using System;
using System.Linq;
using Markdig.Helpers;
using NUnit.Framework;
namespace Markdig.Tests
{
public class TestTransformedStringCache
{
[Test]
public void GetRunsTransformationCallback()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
Assert.AreEqual("callback-foo", cache.Get("foo"));
Assert.AreEqual("callback-bar", cache.Get("bar"));
Assert.AreEqual("callback-baz", cache.Get("baz"));
}
[Test]
public void CachesTransformedInstance()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
string transformedBar = cache.Get("bar");
Assert.AreSame(transformedBar, cache.Get("bar"));
string transformedFoo = cache.Get("foo".AsSpan());
Assert.AreSame(transformedFoo, cache.Get("foo"));
Assert.AreSame(cache.Get("baz"), cache.Get("baz".AsSpan()));
Assert.AreSame(transformedBar, cache.Get("bar"));
Assert.AreSame(transformedFoo, cache.Get("foo"));
Assert.AreSame(transformedBar, cache.Get("bar".AsSpan()));
Assert.AreSame(transformedFoo, cache.Get("foo".AsSpan()));
}
[Test]
public void DoesNotCacheEmptyInputs()
{
var cache = new TransformedStringCache(static s => new string('a', 4));
string cached = cache.Get("");
string cached2 = cache.Get("");
string cached3 = cache.Get(ReadOnlySpan<char>.Empty);
Assert.AreEqual("aaaa", cached);
Assert.AreEqual(cached, cached2);
Assert.AreEqual(cached, cached3);
Assert.AreNotSame(cached, cached2);
Assert.AreNotSame(cached, cached3);
Assert.AreNotSame(cached2, cached3);
}
[Test]
[TestCase(TransformedStringCache.InputLengthLimit, true)]
[TestCase(TransformedStringCache.InputLengthLimit + 1, false)]
public void DoesNotCacheLongInputs(int length, bool shouldBeCached)
{
var cache = new TransformedStringCache(static s => "callback-" + s);
string input = new string('a', length);
string cached = cache.Get(input);
string cached2 = cache.Get(input);
Assert.AreEqual("callback-" + input, cached);
Assert.AreEqual(cached, cached2);
if (shouldBeCached)
{
Assert.AreSame(cached, cached2);
}
else
{
Assert.AreNotSame(cached, cached2);
}
}
[Test]
public void CachesAtMostNEntriesPerCharacter()
{
var cache = new TransformedStringCache(static s => "callback-" + s);
int limit = TransformedStringCache.MaxEntriesPerCharacter;
string[] a = Enumerable.Range(1, limit + 1).Select(i => $"a{i}").ToArray();
string[] cachedAs = a.Select(a => cache.Get(a)).ToArray();
for (int i = 0; i < limit; i++)
{
Assert.AreSame(cachedAs[i], cache.Get(a[i]));
}
Assert.AreNotSame(cachedAs[limit], cache.Get(a[limit]));
Assert.AreSame(cache.Get("b1"), cache.Get("b1"));
}
}
}

View File

@@ -172,17 +172,22 @@ namespace Markdig.Extensions.AutoIdentifiers
var baseHeadingId = string.IsNullOrEmpty(headingText) ? "section" : headingText;
// Add a trailing -1, -2, -3...etc. in case of collision
int index = 0;
var headingId = baseHeadingId;
var headingBuffer = StringBuilderCache.Local();
while (!identifiers.Add(headingId))
if (!identifiers.Add(headingId))
{
index++;
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
headingBuffer.Append(baseHeadingId);
headingBuffer.Append('-');
headingBuffer.Append(index);
headingId = headingBuffer.ToString();
headingBuffer.Length = 0;
uint index = 0;
do
{
index++;
headingBuffer.Append(index);
headingId = headingBuffer.AsSpan().ToString();
headingBuffer.Length = baseHeadingId.Length + 1;
}
while (!identifiers.Add(headingId));
headingBuffer.Dispose();
}
attributes.Id = headingId;

View File

@@ -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 @@ namespace Markdig.Extensions.AutoLinks
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
{
if (renderer is NormalizeRenderer normalizeRenderer && !normalizeRenderer.ObjectRenderers.Contains<NormalizeAutoLinkRenderer>())
{
normalizeRenderer.ObjectRenderers.InsertBefore<LinkInlineRenderer>(new NormalizeAutoLinkRenderer());
}
}
}
}

View File

@@ -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<LinkInline>
{
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);
}
}
}

View File

@@ -95,13 +95,19 @@ namespace Markdig.Extensions.JiraLinks
jiraLink.Span.End = jiraLink.Span.Start + (endIssue - startKey);
// Builds the Url
var builder = StringBuilderCache.Local();
builder.Append(_baseUrl).Append('/').Append(jiraLink.ProjectKey).Append('-').Append(jiraLink.Issue);
jiraLink.Url = builder.ToString();
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
builder.Append(_baseUrl);
builder.Append('/');
builder.Append(jiraLink.ProjectKey.AsSpan());
builder.Append('-');
builder.Append(jiraLink.Issue.AsSpan());
jiraLink.Url = builder.AsSpan().ToString();
// Builds the Label
builder.Length = 0;
builder.Append(jiraLink.ProjectKey).Append('-').Append(jiraLink.Issue);
builder.Append(jiraLink.ProjectKey.AsSpan());
builder.Append('-');
builder.Append(jiraLink.Issue.AsSpan());
jiraLink.AppendChild(new LiteralInline(builder.ToString()));
if (_options.OpenInNewWindow)

View File

@@ -2,7 +2,8 @@
// This file is licensed under the BSD-Clause 2 license.
// See the license.txt file in the project root for more information.
using System.Text;
using Markdig.Helpers;
using System;
namespace Markdig.Extensions.JiraLinks
{
@@ -38,20 +39,10 @@ namespace Markdig.Extensions.JiraLinks
/// </summary>
public virtual string GetUrl()
{
var url = new StringBuilder();
var baseUrl = BaseUrl;
if (baseUrl != null)
{
url.Append(baseUrl.TrimEnd('/'));
}
url.Append("/");
if (BasePath != null)
{
url.Append(BasePath.Trim('/'));
}
var url = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
url.Append(BaseUrl.AsSpan().TrimEnd('/'));
url.Append('/');
url.Append(BasePath.AsSpan().Trim('/'));
return url.ToString();
}
}

View File

@@ -43,7 +43,7 @@ namespace Markdig.Extensions.Tables
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// Only working on Paragraph block
if (!(processor.Block is ParagraphBlock))
if (!processor.Block!.IsParagraphBlock)
{
return false;
}

View File

@@ -33,7 +33,6 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Markdig.Helpers
{
@@ -81,7 +80,7 @@ namespace Markdig.Helpers
});
}
public static void DecodeEntity(int utf32, StringBuilder sb)
internal static void DecodeEntity(int utf32, ref ValueStringBuilder sb)
{
if (!CharHelper.IsInInclusiveRange(utf32, 1, 1114111) || CharHelper.IsInInclusiveRange(utf32, 55296, 57343))
{
@@ -99,7 +98,7 @@ namespace Markdig.Helpers
}
}
#region [ EntityMap ]
#region [ EntityMap ]
/// <summary>
/// Source: http://www.w3.org/html/wg/drafts/html/master/syntax.html#named-character-references
/// </summary>

View File

@@ -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<char> 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<char> buffer)
{
Write(buffer);
WriteLine();
}
public override Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default)
{
Write(buffer.Span);
return CompletedTask;
}
public override Task WriteLineAsync(ReadOnlyMemory<char> 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();
}
}
}

View File

@@ -4,7 +4,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace Markdig.Helpers
{
@@ -39,22 +38,22 @@ namespace Markdig.Helpers
public static bool TryParseHtmlTag(ref StringSlice text, [NotNullWhen(true)] out string? htmlTag)
{
var builder = StringBuilderCache.Local();
if (TryParseHtmlTag(ref text, builder))
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
if (TryParseHtmlTag(ref text, ref builder))
{
htmlTag = builder.GetStringAndReset();
htmlTag = builder.ToString();
return true;
}
else
{
builder.Dispose();
htmlTag = null;
return false;
}
}
public static bool TryParseHtmlTag(ref StringSlice text, StringBuilder builder)
private static bool TryParseHtmlTag(ref StringSlice text, ref ValueStringBuilder builder)
{
if (builder is null) ThrowHelper.ArgumentNullException(nameof(builder));
var c = text.CurrentChar;
if (c != '<')
{
@@ -67,29 +66,29 @@ namespace Markdig.Helpers
switch (c)
{
case '/':
return TryParseHtmlCloseTag(ref text, builder);
return TryParseHtmlCloseTag(ref text, ref builder);
case '?':
return TryParseHtmlTagProcessingInstruction(ref text, builder);
return TryParseHtmlTagProcessingInstruction(ref text, ref builder);
case '!':
builder.Append(c);
c = text.NextChar();
if (c == '-')
{
return TryParseHtmlTagHtmlComment(ref text, builder);
return TryParseHtmlTagHtmlComment(ref text, ref builder);
}
if (c == '[')
{
return TryParseHtmlTagCData(ref text, builder);
return TryParseHtmlTagCData(ref text, ref builder);
}
return TryParseHtmlTagDeclaration(ref text, builder);
return TryParseHtmlTagDeclaration(ref text, ref builder);
}
return TryParseHtmlTagOpenTag(ref text, builder);
return TryParseHtmlTagOpenTag(ref text, ref builder);
}
internal static bool TryParseHtmlTagOpenTag(ref StringSlice text, StringBuilder builder)
internal static bool TryParseHtmlTagOpenTag(ref StringSlice text, ref ValueStringBuilder builder)
{
var c = text.CurrentChar;
@@ -244,7 +243,7 @@ namespace Markdig.Helpers
}
}
private static bool TryParseHtmlTagDeclaration(ref StringSlice text, StringBuilder builder)
private static bool TryParseHtmlTagDeclaration(ref StringSlice text, ref ValueStringBuilder builder)
{
var c = text.CurrentChar;
bool hasAlpha = false;
@@ -279,7 +278,7 @@ namespace Markdig.Helpers
}
}
private static bool TryParseHtmlTagCData(ref StringSlice text, StringBuilder builder)
private static bool TryParseHtmlTagCData(ref StringSlice text, ref ValueStringBuilder builder)
{
if (text.Match("[CDATA["))
{
@@ -310,7 +309,7 @@ namespace Markdig.Helpers
return false;
}
internal static bool TryParseHtmlCloseTag(ref StringSlice text, StringBuilder builder)
internal static bool TryParseHtmlCloseTag(ref StringSlice text, ref ValueStringBuilder builder)
{
// </[A-Za-z][A-Za-z0-9]+\s*>
builder.Append('/');
@@ -355,7 +354,7 @@ namespace Markdig.Helpers
}
private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, StringBuilder builder)
private static bool TryParseHtmlTagHtmlComment(ref StringSlice text, ref ValueStringBuilder builder)
{
var c = text.NextChar();
if (c != '-')
@@ -393,7 +392,7 @@ namespace Markdig.Helpers
}
}
private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, StringBuilder builder)
private static bool TryParseHtmlTagProcessingInstruction(ref StringSlice text, ref ValueStringBuilder builder)
{
builder.Append('?');
var prevChar = '\0';
@@ -435,13 +434,12 @@ namespace Markdig.Helpers
// remove backslashes before punctuation chars:
int searchPos = 0;
int lastPos = 0;
char c;
char c = '\0';
char[] search = removeBackSlash ? SearchBackAndAmp : SearchAmp;
StringBuilder? sb = null;
var sb = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
while ((searchPos = text!.IndexOfAny(search, searchPos)) != -1)
{
sb ??= StringBuilderCache.Local();
c = text[searchPos];
if (removeBackSlash && c == '\\')
{
@@ -453,7 +451,7 @@ namespace Markdig.Helpers
c = text[searchPos];
if (c.IsEscapableSymbol())
{
sb.Append(text, lastPos, searchPos - lastPos - 1);
sb.Append(text.AsSpan(lastPos, searchPos - lastPos - 1));
lastPos = searchPos;
}
}
@@ -473,26 +471,29 @@ namespace Markdig.Helpers
var decoded = EntityHelper.DecodeEntity(text.AsSpan(entityNameStart, entityNameLength));
if (decoded != null)
{
sb.Append(text, lastPos, searchPos - match - lastPos);
sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos));
sb.Append(decoded);
lastPos = searchPos;
}
}
else if (numericEntity >= 0)
{
sb.Append(text, lastPos, searchPos - match - lastPos);
EntityHelper.DecodeEntity(numericEntity, sb);
sb.Append(text.AsSpan(lastPos, searchPos - match - lastPos));
EntityHelper.DecodeEntity(numericEntity, ref sb);
lastPos = searchPos;
}
}
}
}
if (sb is null || lastPos == 0)
if (c == 0)
{
sb.Dispose();
return text;
}
sb.Append(text, lastPos, text.Length - lastPos);
return sb.GetStringAndReset();
sb.Append(text.AsSpan(lastPos, text.Length - lastPos));
return sb.ToString();
}
/// <summary>

View File

@@ -4,6 +4,8 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Markdig.Helpers
{
@@ -39,41 +41,55 @@ namespace Markdig.Helpers
/// <returns>A new line or null if the end of <see cref="TextReader"/> has been reached</returns>
public StringSlice ReadLine()
{
string text = _text;
string? text = _text;
int end = text.Length;
int sourcePosition = SourcePosition;
int newSourcePosition = int.MaxValue;
NewLine newLine = NewLine.None;
for (int i = sourcePosition; i < text.Length; i++)
if ((uint)sourcePosition >= (uint)end)
{
char c = text[i];
if (c == '\r')
text = null;
}
else
{
#if NETCOREAPP3_1_OR_GREATER
ReadOnlySpan<char> span = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), sourcePosition), end - sourcePosition);
#else
ReadOnlySpan<char> span = text.AsSpan(sourcePosition);
#endif
int crlf = span.IndexOfAny('\r', '\n');
if (crlf >= 0)
{
int length = 1;
var newLine = NewLine.CarriageReturn;
if (c == '\r' && (uint)(i + 1) < (uint)text.Length && text[i + 1] == '\n')
end = sourcePosition + crlf;
newSourcePosition = end + 1;
#if NETCOREAPP3_1_OR_GREATER
if (Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), end) == '\r')
#else
if ((uint)end < (uint)text.Length && text[end] == '\r')
#endif
{
i++;
length = 2;
newLine = NewLine.CarriageReturnLineFeed;
if ((uint)(newSourcePosition) < (uint)text.Length && text[newSourcePosition] == '\n')
{
newLine = NewLine.CarriageReturnLineFeed;
newSourcePosition++;
}
else
{
newLine = NewLine.CarriageReturn;
}
}
else
{
newLine = NewLine.LineFeed;
}
var slice = new StringSlice(text, sourcePosition, i - length, newLine);
SourcePosition = i + 1;
return slice;
}
if (c == '\n')
{
var slice = new StringSlice(text, sourcePosition, i - 1, NewLine.LineFeed);
SourcePosition = i + 1;
return slice;
}
}
if (sourcePosition >= text.Length)
return default;
SourcePosition = int.MaxValue;
return new StringSlice(text, sourcePosition, text.Length - 1);
SourcePosition = newSourcePosition;
return new StringSlice(text, sourcePosition, end - 1, newLine, dummy: false);
}
}
}

View File

@@ -4,7 +4,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text;
using Markdig.Syntax;
namespace Markdig.Helpers
@@ -21,7 +20,7 @@ namespace Markdig.Helpers
public static string Urilize(string headingText, bool allowOnlyAscii, bool keepOpeningDigits = false)
{
var headingBuffer = StringBuilderCache.Local();
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
bool hasLetter = keepOpeningDigits && headingText.Length > 0 && char.IsLetterOrDigit(headingText[0]);
bool previousIsSpace = false;
for (int i = 0; i < headingText.Length; i++)
@@ -92,15 +91,13 @@ namespace Markdig.Helpers
}
}
var text = headingBuffer.ToString();
headingBuffer.Length = 0;
return text;
return headingBuffer.ToString();
}
public static string UrilizeAsGfm(string headingText)
{
// Following https://github.com/jch/html-pipeline/blob/master/lib/html/pipeline/toc_filter.rb
var headingBuffer = StringBuilderCache.Local();
var headingBuffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
for (int i = 0; i < headingText.Length; i++)
{
var c = headingText[i];
@@ -109,7 +106,7 @@ namespace Markdig.Helpers
headingBuffer.Append(c == ' ' ? '-' : char.ToLowerInvariant(c));
}
}
return headingBuffer.GetStringAndReset();
return headingBuffer.ToString();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -165,7 +162,7 @@ namespace Markdig.Helpers
}
}
var builder = StringBuilderCache.Local();
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
// ****************************
// 1. Scan scheme or user email
@@ -193,8 +190,7 @@ namespace Markdig.Helpers
// a scheme is any sequence of 232 characters
if (state > 0 && builder.Length >= 32)
{
builder.Length = 0;
return false;
goto ReturnFalse;
}
builder.Append(c);
}
@@ -202,8 +198,7 @@ namespace Markdig.Helpers
{
if (state < 0 || builder.Length <= 2)
{
builder.Length = 0;
return false;
goto ReturnFalse;
}
state = 1;
break;
@@ -211,16 +206,14 @@ namespace Markdig.Helpers
{
if (state > 0)
{
builder.Length = 0;
return false;
goto ReturnFalse;
}
state = -1;
break;
}
else
{
builder.Length = 0;
return false;
goto ReturnFalse;
}
}
@@ -249,7 +242,6 @@ namespace Markdig.Helpers
text.SkipChar();
link = builder.ToString();
builder.Length = 0;
return true;
}
@@ -297,7 +289,6 @@ namespace Markdig.Helpers
{
text.SkipChar();
link = builder.ToString();
builder.Length = 0;
return true;
}
@@ -318,7 +309,8 @@ namespace Markdig.Helpers
}
}
builder.Length = 0;
ReturnFalse:
builder.Dispose();
return false;
}
@@ -528,8 +520,7 @@ namespace Markdig.Helpers
public static bool TryParseTitle<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
{
bool isValid = false;
var buffer = StringBuilderCache.Local();
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
enclosingCharacter = '\0';
// a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
@@ -582,8 +573,7 @@ namespace Markdig.Helpers
// Skip last quote
text.SkipChar();
isValid = true;
break;
goto ReturnValid;
}
if (hasEscape && !c.IsAsciiPunctuation())
@@ -615,15 +605,18 @@ namespace Markdig.Helpers
}
}
title = isValid ? buffer.ToString() : null;
buffer.Length = 0;
return isValid;
buffer.Dispose();
title = null;
return false;
ReturnValid:
title = buffer.ToString();
return true;
}
public static bool TryParseTitleTrivia<T>(ref T text, out string? title, out char enclosingCharacter) where T : ICharIterator
{
bool isValid = false;
var buffer = StringBuilderCache.Local();
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
enclosingCharacter = '\0';
// a sequence of zero or more characters between straight double-quote characters ("), including a " character only if it is backslash-escaped, or
@@ -676,8 +669,7 @@ namespace Markdig.Helpers
// Skip last quote
text.SkipChar();
isValid = true;
break;
goto ReturnValid;
}
if (hasEscape && !c.IsAsciiPunctuation())
@@ -709,9 +701,13 @@ namespace Markdig.Helpers
}
}
title = isValid ? buffer.ToString() : null;
buffer.Length = 0;
return isValid;
buffer.Dispose();
title = null;
return false;
ReturnValid:
title = buffer.ToString();
return true;
}
public static bool TryParseUrl<T>(T text, [NotNullWhen(true)] out string? link) where T : ICharIterator
@@ -723,7 +719,7 @@ namespace Markdig.Helpers
{
bool isValid = false;
hasPointyBrackets = false;
var buffer = StringBuilderCache.Local();
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
var c = text.CurrentChar;
@@ -854,8 +850,15 @@ namespace Markdig.Helpers
}
}
link = isValid ? buffer.ToString() : null;
buffer.Length = 0;
if (isValid)
{
link = buffer.ToString();
}
else
{
buffer.Dispose();
link = null;
}
return isValid;
}
@@ -863,8 +866,7 @@ namespace Markdig.Helpers
{
bool isValid = false;
hasPointyBrackets = false;
var buffer = StringBuilderCache.Local();
var unescaped = new StringBuilder();
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
var c = text.CurrentChar;
@@ -892,13 +894,11 @@ namespace Markdig.Helpers
if (hasEscape && !c.IsAsciiPunctuation())
{
buffer.Append('\\');
unescaped.Append('\\');
}
if (c == '\\')
{
hasEscape = true;
unescaped.Append('\\');
continue;
}
@@ -910,7 +910,6 @@ namespace Markdig.Helpers
hasEscape = false;
buffer.Append(c);
unescaped.Append(c);
} while (c != '\0');
}
@@ -958,7 +957,6 @@ namespace Markdig.Helpers
{
hasEscape = true;
c = text.NextChar();
unescaped.Append('\\');
continue;
}
@@ -989,7 +987,6 @@ namespace Markdig.Helpers
}
buffer.Append(c);
unescaped.Append(c);
c = text.NextChar();
}
@@ -1000,8 +997,15 @@ namespace Markdig.Helpers
}
}
link = isValid ? buffer.ToString() : null;
buffer.Length = 0;
if (isValid)
{
link = buffer.ToString();
}
else
{
buffer.Dispose();
link = null;
}
return isValid;
}
@@ -1357,7 +1361,7 @@ namespace Markdig.Helpers
{
return false;
}
var buffer = StringBuilderCache.Local();
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
var startLabel = -1;
var endLabel = -1;
@@ -1365,7 +1369,6 @@ namespace Markdig.Helpers
bool hasEscape = false;
bool previousWhitespace = true;
bool hasNonWhiteSpace = false;
bool isValid = false;
while (true)
{
c = lines.NextChar();
@@ -1413,9 +1416,7 @@ namespace Markdig.Helpers
{
labelSpan = SourceSpan.Empty;
}
label = buffer.ToString();
isValid = true;
goto ReturnValid;
}
}
break;
@@ -1458,9 +1459,12 @@ namespace Markdig.Helpers
previousWhitespace = isWhitespace;
}
buffer.Length = 0;
buffer.Dispose();
return false;
return isValid;
ReturnValid:
label = buffer.ToString();
return true;
}
public static bool TryParseLabelTrivia<T>(ref T lines, bool allowEmpty, out string? label, out SourceSpan labelSpan) where T : ICharIterator
@@ -1472,7 +1476,7 @@ namespace Markdig.Helpers
{
return false;
}
var buffer = StringBuilderCache.Local();
var buffer = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
var startLabel = -1;
var endLabel = -1;
@@ -1480,7 +1484,6 @@ namespace Markdig.Helpers
bool hasEscape = false;
bool previousWhitespace = true;
bool hasNonWhiteSpace = false;
bool isValid = false;
while (true)
{
c = lines.NextChar();
@@ -1528,9 +1531,7 @@ namespace Markdig.Helpers
{
labelSpan = SourceSpan.Empty;
}
label = buffer.ToString();
isValid = true;
goto ReturnValid;
}
}
break;
@@ -1577,10 +1578,12 @@ namespace Markdig.Helpers
previousWhitespace = isWhitespace;
}
buffer.Length = 0;
buffer.Dispose();
return false;
return isValid;
ReturnValid:
label = buffer.ToString();
return true;
}
}
}

View File

@@ -20,12 +20,5 @@ namespace Markdig.Helpers
{
return builder.Append(slice.Text, slice.Start, slice.Length);
}
internal static string GetStringAndReset(this StringBuilder builder)
{
string text = builder.ToString();
builder.Length = 0;
return text;
}
}
}

View File

@@ -85,6 +85,14 @@ namespace Markdig.Helpers
Count--;
}
internal void RemoveStartRange(int toRemove)
{
int remaining = Count - toRemove;
Count = remaining;
Array.Copy(Lines, toRemove, Lines, 0, remaining);
Array.Clear(Lines, remaining, toRemove);
}
/// <summary>
/// Adds the specified line to this instance.
/// </summary>
@@ -139,7 +147,7 @@ namespace Markdig.Helpers
}
// Else use a builder
var builder = StringBuilderCache.Local();
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
int previousStartOfLine = 0;
var newLine = NewLine.None;
for (int i = 0; i < Count; i++)
@@ -152,13 +160,13 @@ namespace Markdig.Helpers
ref StringLine line = ref Lines[i];
if (!line.Slice.IsEmpty)
{
builder.Append(line.Slice.Text, line.Slice.Start, line.Slice.Length);
builder.Append(line.Slice.AsSpan());
}
newLine = line.NewLine;
lineOffsets?.Add(new LineOffset(line.Position, line.Column, line.Slice.Start - line.Position, previousStartOfLine, builder.Length));
}
return new StringSlice(builder.GetStringAndReset());
return new StringSlice(builder.ToString());
}
/// <summary>
@@ -213,21 +221,24 @@ namespace Markdig.Helpers
public struct Iterator : ICharIterator
{
private readonly StringLineGroup _lines;
private StringSlice _currentSlice;
private int _offset;
public Iterator(StringLineGroup lines)
public Iterator(StringLineGroup stringLineGroup)
{
this._lines = lines;
_lines = stringLineGroup;
Start = -1;
_offset = -1;
SliceIndex = 0;
CurrentChar = '\0';
End = -1;
for (int i = 0; i < lines.Count; i++)
StringLine[] lines = stringLineGroup.Lines;
for (int i = 0; i < stringLineGroup.Count && i < lines.Length; i++)
{
ref StringLine line = ref lines.Lines[i];
End += line.Slice.Length + line.NewLine.Length(); // Add chars
ref StringSlice slice = ref lines[i].Slice;
End += slice.Length + slice.NewLine.Length(); // Add chars
}
_currentSlice = _lines.Lines[0].Slice;
SkipChar();
}
@@ -243,17 +254,14 @@ namespace Markdig.Helpers
public StringLineGroup Remaining()
{
var lines = _lines;
StringLineGroup lines = _lines;
if (IsEmpty)
{
lines.Clear();
}
else
{
for (int i = SliceIndex - 1; i >= 0; i--)
{
lines.RemoveAt(i);
}
lines.RemoveStartRange(SliceIndex);
if (lines.Count > 0 && _offset > 0)
{
@@ -266,59 +274,85 @@ namespace Markdig.Helpers
return lines;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public char NextChar()
{
Start++;
if (Start <= End)
{
var slice = _lines.Lines[SliceIndex].Slice;
ref StringSlice slice = ref _currentSlice;
_offset++;
if (_offset < slice.Length)
int index = slice.Start + _offset;
string text = slice.Text;
if (index <= slice.End && (uint)index < (uint)text.Length)
{
CurrentChar = slice[slice.Start + _offset];
char c = text[index];
CurrentChar = c;
return c;
}
else
{
var newLine = slice.NewLine;
if (_offset == slice.Length)
{
if (newLine == NewLine.LineFeed)
{
CurrentChar = '\n';
SliceIndex++;
_offset = -1;
}
else if (newLine == NewLine.CarriageReturn)
{
CurrentChar = '\r';
SliceIndex++;
_offset = -1;
}
else if (newLine == NewLine.CarriageReturnLineFeed)
{
CurrentChar = '\r';
}
}
else if (_offset - 1 == slice.Length)
{
if (newLine == NewLine.CarriageReturnLineFeed)
{
CurrentChar = '\n';
SliceIndex++;
_offset = -1;
}
}
return NextCharNewLine();
}
}
else
{
CurrentChar = '\0';
Start = End + 1;
SliceIndex = _lines.Count;
return NextCharEndOfEnumerator();
}
}
private char NextCharNewLine()
{
int sliceLength = _currentSlice.Length;
NewLine newLine = _currentSlice.NewLine;
if (_offset == sliceLength)
{
if (newLine == NewLine.LineFeed)
{
CurrentChar = '\n';
goto MoveToNewLine;
}
else if (newLine == NewLine.CarriageReturn)
{
CurrentChar = '\r';
goto MoveToNewLine;
}
else if (newLine == NewLine.CarriageReturnLineFeed)
{
CurrentChar = '\r';
}
}
else if (_offset - 1 == sliceLength)
{
if (newLine == NewLine.CarriageReturnLineFeed)
{
CurrentChar = '\n';
goto MoveToNewLine;
}
}
goto Return;
MoveToNewLine:
SliceIndex++;
_offset = -1;
_currentSlice = _lines.Lines[SliceIndex];
Return:
return CurrentChar;
}
private char NextCharEndOfEnumerator()
{
CurrentChar = '\0';
Start = End + 1;
SliceIndex = _lines.Count;
return '\0';
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SkipChar() => NextChar();
public readonly char PeekChar() => PeekChar(1);
@@ -335,16 +369,17 @@ namespace Markdig.Helpers
offset += _offset;
int sliceIndex = SliceIndex;
ref StringLine line = ref _lines.Lines[sliceIndex];
ref StringSlice slice = ref line.Slice;
if (!(line.NewLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1))
ref StringSlice slice = ref _lines.Lines[sliceIndex].Slice;
NewLine newLine = slice.NewLine;
if (!(newLine == NewLine.CarriageReturnLineFeed && offset == slice.Length + 1))
{
while (offset > slice.Length)
{
// We are not peeking at the same line
offset -= slice.Length + 1; // + 1 for new line
Debug.Assert(sliceIndex + 1 < _lines.Lines.Length, "'Start + offset > End' check above should prevent us from indexing out of range");
Debug.Assert(sliceIndex + 1 < _lines.Count, "'Start + offset > End' check above should prevent us from indexing out of range");
slice = ref _lines.Lines[++sliceIndex].Slice;
}
}
@@ -358,15 +393,15 @@ namespace Markdig.Helpers
if (offset == slice.Length)
{
if (line.NewLine == NewLine.LineFeed)
if (newLine == NewLine.LineFeed)
{
return '\n';
}
if (line.NewLine == NewLine.CarriageReturn)
if (newLine == NewLine.CarriageReturn)
{
return '\r';
}
if (line.NewLine == NewLine.CarriageReturnLineFeed)
if (newLine == NewLine.CarriageReturnLineFeed)
{
return '\r'; // /r of /r/n (first character)
}

View File

@@ -6,6 +6,7 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Markdig.Helpers
{
@@ -82,6 +83,15 @@ namespace Markdig.Helpers
NewLine = newLine;
}
// Internal ctor to skip the null check
internal StringSlice(string text, int start, int end, NewLine newLine, bool dummy)
{
Text = text;
Start = start;
End = end;
NewLine = newLine;
}
/// <summary>
/// The text of this slice.
/// </summary>
@@ -453,6 +463,25 @@ namespace Markdig.Helpers
return text.Substring(start, length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly ReadOnlySpan<char> AsSpan()
{
string text = Text;
int start = Start;
int length = End - start + 1;
if (text is null || (ulong)(uint)start + (ulong)(uint)length > (ulong)(uint)text.Length)
{
return default;
}
#if NETCOREAPP3_1_OR_GREATER
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref Unsafe.AsRef(text.GetPinnableReference()), start), length);
#else
return text.AsSpan(start, length);
#endif
}
/// <summary>
/// Determines whether this slice is empty or made only of whitespaces.
/// </summary>

View File

@@ -0,0 +1,130 @@
// 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.Linq;
using System.Threading;
namespace Markdig.Helpers
{
internal sealed class TransformedStringCache
{
internal const int InputLengthLimit = 20; // Avoid caching unreasonably long strings
internal const int MaxEntriesPerCharacter = 8; // Avoid growing too much
private readonly EntryGroup[] _groups; // One per ASCII character
private readonly Func<string, string> _transformation;
public TransformedStringCache(Func<string, string> transformation)
{
_transformation = transformation ?? throw new ArgumentNullException(nameof(transformation));
_groups = new EntryGroup[128];
}
public string Get(ReadOnlySpan<char> inputSpan)
{
if ((uint)(inputSpan.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit]
{
int firstCharacter = inputSpan[0];
EntryGroup[] groups = _groups;
if ((uint)firstCharacter < (uint)groups.Length)
{
ref EntryGroup group = ref groups[firstCharacter];
string? transformed = group.TryGet(inputSpan);
if (transformed is null)
{
string input = inputSpan.ToString();
transformed = _transformation(input);
group.TryAdd(input, transformed);
}
return transformed;
}
}
return _transformation(inputSpan.ToString());
}
public string Get(string input)
{
if ((uint)(input.Length - 1) < InputLengthLimit) // Length: [1, LengthLimit]
{
int firstCharacter = input[0];
EntryGroup[] groups = _groups;
if ((uint)firstCharacter < (uint)groups.Length)
{
ref EntryGroup group = ref groups[firstCharacter];
string? transformed = group.TryGet(input.AsSpan());
if (transformed is null)
{
transformed = _transformation(input);
group.TryAdd(input, transformed);
}
return transformed;
}
}
return _transformation(input);
}
private struct EntryGroup
{
private struct Entry
{
public string Input;
public string Transformed;
}
private Entry[]? _entries;
public string? TryGet(ReadOnlySpan<char> inputSpan)
{
Entry[]? entries = _entries;
if (entries is not null)
{
for (int i = 0; i < entries.Length; i++)
{
if (inputSpan.SequenceEqual(entries[i].Input.AsSpan()))
{
return entries[i].Transformed;
}
}
}
return null;
}
public void TryAdd(string input, string transformed)
{
if (_entries is null)
{
Interlocked.CompareExchange(ref _entries, new Entry[MaxEntriesPerCharacter], null);
}
if (_entries[MaxEntriesPerCharacter - 1].Input is null) // There is still space
{
lock (_entries)
{
for (int i = 0; i < _entries.Length; i++)
{
string? existingInput = _entries[i].Input;
if (existingInput is null)
{
ref Entry entry = ref _entries[i];
Volatile.Write(ref entry.Transformed, transformed);
Volatile.Write(ref entry.Input, input);
break;
}
if (input == existingInput)
{
// We lost a race and a different thread already added the same value
break;
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,197 @@
// 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.
// Inspired by https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/ValueStringBuilder.cs
using System;
using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace Markdig.Helpers
{
internal ref partial struct ValueStringBuilder
{
#if DEBUG
public const int StackallocThreshold = 7;
#else
#if NET5_0_OR_GREATER
// NET5+ has SkipLocalsInit, so allocating more is "free"
public const int StackallocThreshold = 256;
#else
public const int StackallocThreshold = 64;
#endif
#endif
private char[]? _arrayToReturnToPool;
private Span<char> _chars;
private int _pos;
public ValueStringBuilder(Span<char> initialBuffer)
{
_arrayToReturnToPool = null;
_chars = initialBuffer;
_pos = 0;
}
public int Length
{
get => _pos;
set
{
Debug.Assert(value >= 0);
Debug.Assert(value <= _chars.Length);
_pos = value;
}
}
public ref char this[int index]
{
get
{
Debug.Assert(index < _pos);
return ref _chars[index];
}
}
public override string ToString()
{
string s = _chars.Slice(0, _pos).ToString();
Dispose();
return s;
}
public ReadOnlySpan<char> AsSpan() => _chars.Slice(0, _pos);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(char c)
{
int pos = _pos;
Span<char> chars = _chars;
if ((uint)pos < (uint)chars.Length)
{
chars[pos] = c;
_pos = pos + 1;
}
else
{
GrowAndAppend(c);
}
}
public void Append(char c, int count)
{
if (_pos > _chars.Length - count)
{
Grow(count);
}
Span<char> dst = _chars.Slice(_pos, count);
for (int i = 0; i < dst.Length; i++)
{
dst[i] = c;
}
_pos += count;
}
public void Append(uint i)
{
if (i < 10)
{
Append((char)('0' + i));
}
else
{
Append(i.ToString());
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string s)
{
int pos = _pos;
if (pos > _chars.Length - s.Length)
{
Grow(s.Length);
}
s
#if !NET5_0_OR_GREATER
.AsSpan()
#endif
.CopyTo(_chars.Slice(pos));
_pos += s.Length;
}
public void Append(ReadOnlySpan<char> value)
{
if (_pos > _chars.Length - value.Length)
{
Grow(value.Length);
}
value.CopyTo(_chars.Slice(_pos));
_pos += value.Length;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<char> AppendSpan(int length)
{
int origPos = _pos;
if (origPos > _chars.Length - length)
{
Grow(length);
}
_pos = origPos + length;
return _chars.Slice(origPos, length);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowAndAppend(char c)
{
Grow(1);
Append(c);
}
/// <summary>
/// Resize the internal buffer either by doubling current buffer size or
/// by adding <paramref name="additionalCapacityBeyondPos"/> to
/// <see cref="_pos"/> whichever is greater.
/// </summary>
/// <param name="additionalCapacityBeyondPos">
/// Number of chars requested beyond current position.
/// </param>
[MethodImpl(MethodImplOptions.NoInlining)]
private void Grow(int additionalCapacityBeyondPos)
{
Debug.Assert(additionalCapacityBeyondPos > 0);
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed.");
// Make sure to let Rent throw an exception if the caller has a bug and the desired capacity is negative
char[] poolArray = ArrayPool<char>.Shared.Rent((int)Math.Max((uint)(_pos + additionalCapacityBeyondPos), (uint)_chars.Length * 2));
_chars.Slice(0, _pos).CopyTo(poolArray);
char[]? toReturn = _arrayToReturnToPool;
_chars = _arrayToReturnToPool = poolArray;
if (toReturn != null)
{
ArrayPool<char>.Shared.Return(toReturn);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
char[]? toReturn = _arrayToReturnToPool;
this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again
if (toReturn != null)
{
ArrayPool<char>.Shared.Return(toReturn);
}
}
}
}

View File

@@ -7,6 +7,11 @@
<None Remove="readme.md" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="readme.md" LunetApiDotNet="true"/>
<AdditionalFiles Include="readme.md" LunetApiDotNet="true" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Markdig.Tests" />
</ItemGroup>
</Project>

View File

@@ -37,11 +37,19 @@
<ItemGroup>
<None Include="../../img/markdig.png" Pack="true" PackagePath="" />
<None Include="../../readme.md" Pack="true" PackagePath="/"/>
<PackageReference Include="MinVer" Version="2.5.0">
<PackageReference Include="MinVer" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.*" PrivateAssets="All"/>
</ItemGroup>
<Target Name="PatchVersion" AfterTargets="MinVer">
<PropertyGroup>
<!--In Markdig, the minor version is like a major version because Major is 0
Need to remove this when Markdig will be >= 1.0-->
<AssemblyVersion>$(MinVerMajor).$(MinVerMinor).0.0</AssemblyVersion>
</PropertyGroup>
</Target>
</Project>

View File

@@ -94,9 +94,7 @@ namespace Markdig
internal sealed class HtmlRendererCache : ObjectCache<HtmlRenderer>
{
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 @@ namespace Markdig
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 @@ namespace Markdig
if (_customWriter)
{
instance.Writer = _dummyWriter;
instance.Writer = s_dummyWriter;
}
else
{
((StringWriter)instance.Writer).GetStringBuilder().Length = 0;
((FastStringWriter)instance.Writer).Reset();
}
}
}

View File

@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
using Markdig.Syntax;
@@ -592,24 +593,27 @@ namespace Markdig.Parsers
/// <param name="stackIndex">Index of a block in a stack considered as the last block to update from.</param>
private void UpdateLastBlockAndContainer(int stackIndex = -1)
{
currentStackIndex = stackIndex < 0 ? OpenedBlocks.Count - 1 : stackIndex;
CurrentBlock = null;
LastBlock = null;
for (int i = OpenedBlocks.Count - 1; i >= 0; i--)
{
var block = OpenedBlocks[i];
if (CurrentBlock is null)
{
CurrentBlock = block;
}
List<Block> openedBlocks = OpenedBlocks;
currentStackIndex = stackIndex < 0 ? openedBlocks.Count - 1 : stackIndex;
if (block is ContainerBlock container)
Block? currentBlock = null;
for (int i = openedBlocks.Count - 1; i >= 0; i--)
{
var block = openedBlocks[i];
currentBlock ??= block;
if (block.IsContainerBlock)
{
CurrentContainer = container;
LastBlock = CurrentContainer.LastChild;
break;
var currentContainer = Unsafe.As<ContainerBlock>(block);
CurrentContainer = currentContainer;
LastBlock = currentContainer.LastChild;
CurrentBlock = currentBlock;
return;
}
}
CurrentBlock = currentBlock;
LastBlock = null;
}
/// <summary>
@@ -639,7 +643,7 @@ namespace Markdig.Parsers
ParseIndent();
// If we have a paragraph block, we want to try to match other blocks before trying the Paragraph
if (block is ParagraphBlock)
if (block.IsParagraphBlock)
{
break;
}
@@ -675,7 +679,7 @@ namespace Markdig.Parsers
}
// If we have a leaf block
if (block is LeafBlock leaf && NewBlocks.Count == 0)
if (block.IsLeafBlock && NewBlocks.Count == 0)
{
ContinueProcessingLine = false;
if (!result.IsDiscard())
@@ -689,7 +693,8 @@ namespace Markdig.Parsers
UnwindAllIndents();
}
}
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
Unsafe.As<LeafBlock>(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
}
}
@@ -804,7 +809,7 @@ namespace Markdig.Parsers
continue;
}
IsLazy = blockParser is ParagraphBlockParser && lastBlock is ParagraphBlock;
IsLazy = lastBlock.IsParagraphBlock && blockParser is ParagraphBlockParser;
var result = IsLazy
? blockParser.TryContinue(this, lastBlock)
@@ -825,7 +830,7 @@ namespace Markdig.Parsers
// Special case for paragraph
UpdateLastBlockAndContainer();
if (IsLazy && CurrentBlock is ParagraphBlock paragraph)
if (IsLazy && CurrentBlock is { } currentBlock && currentBlock.IsParagraphBlock)
{
Debug.Assert(NewBlocks.Count == 0);
@@ -835,12 +840,13 @@ namespace Markdig.Parsers
{
UnwindAllIndents();
}
paragraph.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
Unsafe.As<ParagraphBlock>(currentBlock).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
}
if (TrackTrivia)
{
// special case: take care when refactoring this
if (paragraph.Parent is QuoteBlock qb)
if (currentBlock.Parent is QuoteBlock qb)
{
var triviaAfter = UseTrivia(Start - 1);
qb.QuoteLines.Last().TriviaAfter = triviaAfter;
@@ -893,20 +899,19 @@ namespace Markdig.Parsers
block.Line = LineIndex;
// If we have a leaf block
var leaf = block as LeafBlock;
if (leaf != null)
if (block.IsLeafBlock)
{
if (!result.IsDiscard())
{
if (TrackTrivia)
{
if (block is ParagraphBlock ||
block is HtmlBlock)
if (block.IsParagraphBlock || block is HtmlBlock)
{
UnwindAllIndents();
}
}
leaf.AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
Unsafe.As<LeafBlock>(block).AppendLine(ref Line, Column, LineIndex, CurrentLineStartPosition, TrackTrivia);
}
if (newBlocks.Count > 0)
@@ -934,7 +939,7 @@ namespace Markdig.Parsers
// Add a block BlockProcessor to the stack (and leave it opened)
OpenedBlocks.Add(block);
if (leaf != null)
if (block.IsLeafBlock)
{
ContinueProcessingLine = false;
return;

View File

@@ -3,7 +3,7 @@
// See the license.txt file in the project root for more information.
using System;
using System.Diagnostics;
using Markdig.Helpers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
@@ -40,6 +40,9 @@ namespace Markdig.Parsers
/// <seealso cref="BlockParser" />
public abstract class FencedBlockParserBase<T> : FencedBlockParserBase where T : Block, IFencedBlock
{
private static readonly TransformedStringCache _infoStringCache = new(static infoString => HtmlHelper.Unescape(infoString));
private TransformedStringCache? _infoPrefixCache;
/// <summary>
/// Initializes a new instance of the <see cref="FencedBlockParserBase{T}"/> class.
/// </summary>
@@ -50,10 +53,22 @@ namespace Markdig.Parsers
MaximumMatchCount = int.MaxValue;
}
private string? _infoPrefix;
/// <summary>
/// Gets or sets the language prefix (default is "language-")
/// </summary>
public string? InfoPrefix { get; set; }
public string? InfoPrefix
{
get => _infoPrefix;
set
{
if (_infoPrefix != value)
{
_infoPrefixCache = new TransformedStringCache(infoString => value + infoString);
_infoPrefix = value;
}
}
}
public int MinimumMatchCount { get; set; }
@@ -161,7 +176,7 @@ namespace Markdig.Parsers
end:
fenced.TriviaAfterFencedChar = afterFence;
fenced.Info = HtmlHelper.Unescape(info.ToString());
fenced.Info = _infoStringCache.Get(info.AsSpan());
fenced.UnescapedInfo = info;
fenced.TriviaAfterInfo = afterInfo;
fenced.Arguments = HtmlHelper.Unescape(arg.ToString());
@@ -182,9 +197,6 @@ namespace Markdig.Parsers
/// <returns><c>true</c> if parsing of the line is successfull; <c>false</c> otherwise</returns>
public static bool DefaultInfoParser(BlockProcessor state, ref StringSlice line, IFencedBlock fenced, char openingCharacter)
{
string infoString;
string? argString = null;
// An info string cannot contain any backticks (unless it is a tilde block)
int firstSpace = -1;
if (openingCharacter == '`')
@@ -215,9 +227,12 @@ namespace Markdig.Parsers
}
}
StringSlice infoStringSlice;
string? argString = null;
if (firstSpace > 0)
{
infoString = line.Text.AsSpan(line.Start, firstSpace - line.Start).Trim().ToString();
infoStringSlice = new StringSlice(line.Text, line.Start, firstSpace - 1);
// Skip any spaces after info string
firstSpace++;
@@ -234,16 +249,18 @@ namespace Markdig.Parsers
}
}
argString = line.Text.Substring(firstSpace, line.End - firstSpace + 1).Trim();
var argStringSlice = new StringSlice(line.Text, firstSpace, line.End);
argStringSlice.Trim();
argString = argStringSlice.ToString();
}
else
{
var lineCopy = line;
lineCopy.Trim();
infoString = lineCopy.ToString();
infoStringSlice = line;
}
fenced.Info = HtmlHelper.Unescape(infoString);
infoStringSlice.Trim();
fenced.Info = _infoStringCache.Get(infoStringSlice.AsSpan());
fenced.Arguments = HtmlHelper.Unescape(argString);
return true;
@@ -295,7 +312,9 @@ namespace Markdig.Parsers
// Add the language as an attribute by default
if (!string.IsNullOrEmpty(fenced.Info))
{
fenced.GetAttributes().AddClass(InfoPrefix + fenced.Info);
Debug.Assert(_infoPrefixCache is not null || InfoPrefix is null);
string infoWithPrefix = _infoPrefixCache?.Get(fenced.Info!) ?? fenced.Info!;
fenced.GetAttributes().AddClass(infoWithPrefix);
}
// Store the number of matched string into the context
@@ -332,9 +351,13 @@ namespace Markdig.Parsers
var fencedBlock = (IFencedBlock)block;
fencedBlock.ClosingFencedCharCount = closingCount;
fencedBlock.NewLine = processor.Line.NewLine;
fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim);
if (processor.TrackTrivia)
{
fencedBlock.NewLine = processor.Line.NewLine;
fencedBlock.TriviaBeforeClosingFence = processor.UseTrivia(sourcePosition - 1);
fencedBlock.TriviaAfter = new StringSlice(processor.Line.Text, lastFenceCharPosition, endBeforeTrim);
}
// Don't keep the last line
return BlockState.BreakDiscard;

View File

@@ -26,13 +26,19 @@ namespace Markdig.Parsers
protected override FencedCodeBlock CreateFencedBlock(BlockProcessor processor)
{
return new FencedCodeBlock(this)
var codeBlock = new FencedCodeBlock(this)
{
IndentCount = processor.Indent,
LinesBefore = processor.UseLinesBefore(),
TriviaBefore = processor.UseTrivia(processor.Start - 1),
NewLine = processor.Line.NewLine,
};
if (processor.TrackTrivia)
{
codeBlock.LinesBefore = processor.UseLinesBefore();
codeBlock.TriviaBefore = processor.UseTrivia(processor.Start - 1);
codeBlock.NewLine = processor.Line.NewLine;
}
return codeBlock;
}
public override BlockState TryContinue(BlockProcessor processor, Block block)

View File

@@ -82,20 +82,25 @@ namespace Markdig.Parsers
var headingBlock = new HeadingBlock(this)
{
HeaderChar = matchingChar,
TriviaAfterAtxHeaderChar = trivia,
Level = leadingCount,
Column = column,
Span = { Start = sourcePosition },
TriviaBefore = processor.UseTrivia(sourcePosition - 1),
LinesBefore = processor.UseLinesBefore(),
NewLine = processor.Line.NewLine,
};
processor.NewBlocks.Push(headingBlock);
if (!processor.TrackTrivia)
if (processor.TrackTrivia)
{
headingBlock.TriviaAfterAtxHeaderChar = trivia;
headingBlock.TriviaBefore = processor.UseTrivia(sourcePosition - 1);
headingBlock.LinesBefore = processor.UseLinesBefore();
headingBlock.NewLine = processor.Line.NewLine;
}
else
{
processor.GoToColumn(column + leadingCount + 1);
}
processor.NewBlocks.Push(headingBlock);
// Gives a chance to parse attributes
TryParseAttributes?.Invoke(processor, ref processor.Line, headingBlock);

View File

@@ -62,10 +62,10 @@ namespace Markdig.Parsers
private BlockState TryParseTagType7(BlockProcessor state, StringSlice line, int startColumn, int startPosition)
{
var builder = StringBuilderCache.Local();
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
var c = line.CurrentChar;
var result = BlockState.None;
if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(ref line, builder)) || HtmlHelper.TryParseHtmlTagOpenTag(ref line, builder))
if ((c == '/' && HtmlHelper.TryParseHtmlCloseTag(ref line, ref builder)) || HtmlHelper.TryParseHtmlTagOpenTag(ref line, ref builder))
{
// Must be followed by whitespace only
bool hasOnlySpaces = true;
@@ -90,7 +90,7 @@ namespace Markdig.Parsers
}
}
builder.Length = 0;
builder.Dispose();
return result;
}
@@ -270,16 +270,22 @@ namespace Markdig.Parsers
private BlockState CreateHtmlBlock(BlockProcessor state, HtmlBlockType type, int startColumn, int startPosition)
{
state.NewBlocks.Push(new HtmlBlock(this)
var htmlBlock = new HtmlBlock(this)
{
Column = startColumn,
Type = type,
// By default, setup to the end of line
Span = new SourceSpan(startPosition, startPosition + state.Line.End),
//BeforeWhitespace = state.PopBeforeWhitespace(startPosition - 1),
LinesBefore = state.UseLinesBefore(),
NewLine = state.Line.NewLine,
});
};
if (state.TrackTrivia)
{
htmlBlock.LinesBefore = state.UseLinesBefore();
htmlBlock.NewLine = state.Line.NewLine;
}
state.NewBlocks.Push(htmlBlock);
return BlockState.Continue;
}

View File

@@ -17,7 +17,7 @@ namespace Markdig.Parsers
{
public override bool CanInterrupt(BlockProcessor processor, Block block)
{
return !(block is ParagraphBlock);
return !block.IsParagraphBlock;
}
public override BlockState TryOpen(BlockProcessor processor)
@@ -36,9 +36,14 @@ namespace Markdig.Parsers
{
Column = processor.Column,
Span = new SourceSpan(processor.Start, processor.Line.End),
LinesBefore = processor.UseLinesBefore(),
NewLine = processor.Line.NewLine,
};
if (processor.TrackTrivia)
{
codeBlock.LinesBefore = processor.UseLinesBefore();
codeBlock.NewLine = processor.Line.NewLine;
}
var codeBlockLine = new CodeBlockLine
{
TriviaBefore = processor.UseTrivia(sourceStartPosition - 1)
@@ -68,8 +73,12 @@ namespace Markdig.Parsers
if (line.Slice.IsEmpty)
{
codeBlock.Lines.RemoveAt(i);
processor.LinesBefore ??= new List<StringSlice>();
processor.LinesBefore.Add(line.Slice);
if (processor.TrackTrivia)
{
processor.LinesBefore ??= new List<StringSlice>();
processor.LinesBefore.Add(line.Slice);
}
}
else
{
@@ -92,12 +101,15 @@ namespace Markdig.Parsers
// lines
var cb = (CodeBlock)block;
var codeBlockLine = new CodeBlockLine
{
TriviaBefore = processor.UseTrivia(processor.Start - 1)
};
var codeBlockLine = new CodeBlockLine();
cb.CodeBlockLines.Add(codeBlockLine);
cb.NewLine = processor.Line.NewLine; // ensure block newline is last newline
if (processor.TrackTrivia)
{
codeBlockLine.TriviaBefore = processor.UseTrivia(processor.Start - 1);
cb.NewLine = processor.Line.NewLine; // ensure block newline is last newline
}
}
return BlockState.Continue;

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
using Markdig.Parsers.Inlines;
using Markdig.Syntax;
@@ -321,9 +322,10 @@ namespace Markdig.Parsers
var container = Block!.Inline!;
for (int depth = 0; ; depth++)
{
if (container.LastChild is ContainerInline nextContainer && !nextContainer.IsClosed)
Inline? lastChild = container.LastChild;
if (lastChild is not null && lastChild.IsContainerInline && !lastChild.IsClosed)
{
container = nextContainer;
container = Unsafe.As<ContainerInline>(lastChild);
}
else
{

View File

@@ -40,7 +40,7 @@ namespace Markdig.Parsers.Inlines
char c = slice.CurrentChar;
var builder = StringBuilderCache.Local();
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.
@@ -98,33 +98,38 @@ namespace Markdig.Parsers.Inlines
bool isMatching = false;
if (closeSticks == openSticks)
{
string content;
ReadOnlySpan<char> contentSpan = builder.AsSpan();
// Remove one space from front and back if the string is not all spaces
if (!allSpace && builder.Length > 2 && builder[0] == ' ' && builder[builder.Length - 1] == ' ')
if (!allSpace && contentSpan.Length > 2 && contentSpan[0] == ' ' && contentSpan[contentSpan.Length - 1] == ' ')
{
content = builder.ToString(1, builder.Length - 2);
}
else
{
content = builder.ToString();
contentSpan = contentSpan.Slice(1, contentSpan.Length - 2);
}
string content = contentSpan.ToString();
int delimiterCount = Math.Min(openSticks, closeSticks);
var spanStart = processor.GetSourcePosition(startPosition, out int line, out int column);
var spanEnd = processor.GetSourcePosition(slice.Start - 1);
processor.Inline = new CodeInline(content)
var codeInline = new CodeInline(content)
{
Delimiter = match,
ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1),
Span = new SourceSpan(spanStart, spanEnd),
Line = line,
Column = column,
DelimiterCount = delimiterCount,
};
if (processor.TrackTrivia)
{
codeInline.ContentWithTrivia = new StringSlice(slice.Text, contentStart, contentEnd - 1);
}
processor.Inline = codeInline;
isMatching = true;
}
builder.Dispose();
return isMatching;
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
using Markdig.Renderers.Html;
using Markdig.Syntax;
@@ -91,11 +92,13 @@ namespace Markdig.Parsers.Inlines
public bool PostProcess(InlineProcessor state, Inline? root, Inline? lastChild, int postInlineProcessorIndex, bool isFinalProcessing)
{
if (!(root is ContainerInline container))
if (root is null || !root.IsContainerInline)
{
return true;
}
ContainerInline container = Unsafe.As<ContainerInline>(root);
List<EmphasisDelimiterInline>? delimiters = null;
if (container is EmphasisDelimiterInline emphasisDelimiter)
{
@@ -122,7 +125,8 @@ namespace Markdig.Parsers.Inlines
continue;
}
child = child.NextSibling;
// Follow DelimiterInline (EmphasisDelimiter, TableDelimiter...)
child = child is DelimiterInline delimiterInline ? delimiterInline.FirstChild : child.NextSibling;
}
if (delimiters != null)

View File

@@ -30,7 +30,7 @@ namespace Markdig.Parsers.Inlines
public override bool Match(InlineProcessor processor, ref StringSlice slice)
{
// Hard line breaks are for separating inline content within a block. Neither syntax for hard line breaks works at the end of a paragraph or other block element:
if (!(processor.Block is ParagraphBlock))
if (!processor.Block!.IsParagraphBlock)
{
return false;
}

View File

@@ -5,6 +5,7 @@
using Markdig.Helpers;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using System.Diagnostics.CodeAnalysis;
namespace Markdig.Parsers.Inlines
{
@@ -78,18 +79,23 @@ namespace Markdig.Parsers.Inlines
// Else we insert a LinkDelimiter
slice.SkipChar();
var labelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End);
processor.Inline = new LinkDelimiterInline(this)
var linkDelimiter = new LinkDelimiterInline(this)
{
Type = DelimiterType.Open,
Label = label,
LabelWithTrivia = labelWithTrivia,
LabelSpan = processor.GetSourcePositionFromLocalSpan(labelSpan),
IsImage = isImage,
Span = new SourceSpan(startPosition, processor.GetSourcePosition(slice.Start - 1)),
Line = line,
Column = column
};
if (processor.TrackTrivia)
{
linkDelimiter.LabelWithTrivia = new StringSlice(slice.Text, labelWithTriviaSpan.Start, labelWithTriviaSpan.End);
}
processor.Inline = linkDelimiter;
return true;
case ']':
@@ -137,18 +143,13 @@ namespace Markdig.Parsers.Inlines
// Create a default link if the callback was not found
if (link is null)
{
var labelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End);
// Inline Link
link = new LinkInline()
var linkInline = new LinkInline()
{
Url = HtmlHelper.Unescape(linkRef.Url),
Title = HtmlHelper.Unescape(linkRef.Title),
Label = label,
LabelSpan = labelSpan,
LabelWithTrivia = labelWithTrivia,
LinkRefDefLabel = linkRef.Label,
LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia,
LocalLabel = localLabel,
UrlSpan = linkRef.UrlSpan,
IsImage = parent.IsImage,
IsShortcut = isShortcut,
@@ -157,6 +158,16 @@ namespace Markdig.Parsers.Inlines
Line = parent.Line,
Column = parent.Column,
};
if (state.TrackTrivia)
{
linkInline.LabelWithTrivia = new StringSlice(text.Text, labelWithriviaSpan.Start, labelWithriviaSpan.End);
linkInline.LinkRefDefLabel = linkRef.Label;
linkInline.LinkRefDefLabelWithTrivia = linkRef.LabelWithTrivia;
linkInline.LocalLabel = localLabel;
}
link = linkInline;
}
if (link is ContainerInline containerLink)
@@ -233,74 +244,18 @@ namespace Markdig.Parsers.Inlines
if (text.CurrentChar == '(')
{
LinkInline? link = null;
if (inlineState.TrackTrivia)
{
if (LinkHelper.TryParseInlineLinkTrivia(
ref text,
out string? url,
out SourceSpan unescapedUrlSpan,
out string? title,
out SourceSpan unescapedTitleSpan,
out char titleEnclosingCharacter,
out SourceSpan linkSpan,
out SourceSpan titleSpan,
out SourceSpan triviaBeforeLink,
out SourceSpan triviaAfterLink,
out SourceSpan triviaAfterTitle,
out bool urlHasPointyBrackets))
{
var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End);
var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End);
var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End);
var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End);
var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End);
// Inline Link
var link = new LinkInline()
{
TriviaBeforeUrl = wsBeforeLink,
Url = HtmlHelper.Unescape(url),
UnescapedUrl = unescapedUrl,
UrlHasPointyBrackets = urlHasPointyBrackets,
TriviaAfterUrl = wsAfterLink,
Title = HtmlHelper.Unescape(title),
UnescapedTitle = unescapedTitle,
TitleEnclosingCharacter = titleEnclosingCharacter,
TriviaAfterTitle = wsAfterTitle,
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(link);
// Notifies processor as we are creating an inline locally
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.
// (This will prevent us from getting links within links.)
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
link.IsClosed = true;
return true;
}
link = TryParseInlineLinkTrivia(ref text, inlineState, openParent);
}
else
{
if (LinkHelper.TryParseInlineLink(ref text, out string? url, out string? title, out SourceSpan linkSpan, out SourceSpan titleSpan))
{
// Inline Link
var link = new LinkInline()
link = new LinkInline()
{
Url = HtmlHelper.Unescape(url),
Title = HtmlHelper.Unescape(title),
@@ -312,34 +267,36 @@ namespace Markdig.Parsers.Inlines
Line = openParent.Line,
Column = openParent.Column,
};
openParent.ReplaceBy(link);
// Notifies processor as we are creating an inline locally
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.
// (This will prevent us from getting links within links.)
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
link.IsClosed = true;
return true;
}
}
if (link is not null)
{
openParent.ReplaceBy(link);
// Notifies processor as we are creating an inline locally
inlineState.Inline = link;
// Process emphasis delimiters
inlineState.PostProcessInlines(0, link, null, false);
// If we have a link (and not an image),
// we also set all [ delimiters before the opening delimiter to inactive.
// (This will prevent us from getting links within links.)
if (!openParent.IsImage)
{
MarkParentAsInactive(parentDelimiter);
}
link.IsClosed = true;
return true;
}
text = savedText;
}
var labelSpan = SourceSpan.Empty;
string? label = null;
SourceSpan labelWithTrivia = SourceSpan.Empty;
bool isLabelSpanLocal = true;
bool isShortcut = false;
@@ -363,9 +320,10 @@ namespace Markdig.Parsers.Inlines
label = openParent.Label;
isShortcut = true;
}
if (label != null || LinkHelper.TryParseLabelTrivia(ref text, true, out label, out labelSpan))
{
labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
SourceSpan labelWithTrivia = new SourceSpan(labelSpan.Start, labelSpan.End);
if (isLabelSpanLocal)
{
labelSpan = inlineState.GetSourcePositionFromLocalSpan(labelSpan);
@@ -399,9 +357,55 @@ namespace Markdig.Parsers.Inlines
inlineState.Inline = openParent.ReplaceBy(literal);
return false;
static LinkInline? TryParseInlineLinkTrivia(ref StringSlice text, InlineProcessor inlineState, LinkDelimiterInline openParent)
{
if (LinkHelper.TryParseInlineLinkTrivia(
ref text,
out string? url,
out SourceSpan unescapedUrlSpan,
out string? title,
out SourceSpan unescapedTitleSpan,
out char titleEnclosingCharacter,
out SourceSpan linkSpan,
out SourceSpan titleSpan,
out SourceSpan triviaBeforeLink,
out SourceSpan triviaAfterLink,
out SourceSpan triviaAfterTitle,
out bool urlHasPointyBrackets))
{
var wsBeforeLink = new StringSlice(text.Text, triviaBeforeLink.Start, triviaBeforeLink.End);
var wsAfterLink = new StringSlice(text.Text, triviaAfterLink.Start, triviaAfterLink.End);
var wsAfterTitle = new StringSlice(text.Text, triviaAfterTitle.Start, triviaAfterTitle.End);
var unescapedUrl = new StringSlice(text.Text, unescapedUrlSpan.Start, unescapedUrlSpan.End);
var unescapedTitle = new StringSlice(text.Text, unescapedTitleSpan.Start, unescapedTitleSpan.End);
return new LinkInline()
{
TriviaBeforeUrl = wsBeforeLink,
Url = HtmlHelper.Unescape(url),
UnescapedUrl = unescapedUrl,
UrlHasPointyBrackets = urlHasPointyBrackets,
TriviaAfterUrl = wsAfterLink,
Title = HtmlHelper.Unescape(title),
UnescapedTitle = unescapedTitle,
TitleEnclosingCharacter = titleEnclosingCharacter,
TriviaAfterTitle = wsAfterTitle,
IsImage = openParent.IsImage,
LabelSpan = openParent.LabelSpan,
UrlSpan = inlineState.GetSourcePositionFromLocalSpan(linkSpan),
TitleSpan = inlineState.GetSourcePositionFromLocalSpan(titleSpan),
Span = new SourceSpan(openParent.Span.Start, inlineState.GetSourcePosition(text.Start - 1)),
Line = openParent.Line,
Column = openParent.Column,
};
}
return null;
}
}
private void MarkParentAsInactive(Inline? inline)
private static void MarkParentAsInactive(Inline? inline)
{
while (inline != null)
{

View File

@@ -93,8 +93,12 @@ namespace Markdig.Parsers
{
// TODO: We remove the thematic break, as it will be created later, but this is inefficient, try to find another way
var thematicBreak = processor.NewBlocks.Pop();
var linesBefore = thematicBreak.LinesBefore;
processor.LinesBefore = linesBefore;
if (processor.TrackTrivia)
{
processor.LinesBefore = thematicBreak.LinesBefore;
}
return BlockState.None;
}
}
@@ -259,10 +263,11 @@ namespace Markdig.Parsers
// Starts/continue the list unless:
// - an empty list item follows a paragraph
// - an ordered list is not starting by '1'
if ((block ?? state.LastBlock) is ParagraphBlock previousParagraph)
block ??= state.LastBlock;
if (block is not null && block.IsParagraphBlock)
{
if (state.IsBlankLine ||
state.IsOpen(previousParagraph) && listInfo.BulletType == '1' && listInfo.OrderedStart is not "1")
state.IsOpen(block) && listInfo.BulletType == '1' && listInfo.OrderedStart is not "1")
{
state.GoToColumn(initColumn);
state.TriviaStart = savedTriviaStart; // restore changed TriviaStart state
@@ -276,12 +281,17 @@ namespace Markdig.Parsers
Column = initColumn,
ColumnWidth = columnWidth,
Order = order,
SourceBullet = listInfo.SourceBullet,
TriviaBefore = triviaBefore,
Span = new SourceSpan(sourcePosition, sourceEndPosition),
LinesBefore = state.UseLinesBefore(),
NewLine = state.Line.NewLine,
};
if (state.TrackTrivia)
{
newListItem.TriviaBefore = triviaBefore;
newListItem.LinesBefore = state.UseLinesBefore();
newListItem.NewLine = state.Line.NewLine;
newListItem.SourceBullet = listInfo.SourceBullet;
}
state.NewBlocks.Push(newListItem);
if (currentParent != null)
@@ -313,8 +323,13 @@ namespace Markdig.Parsers
OrderedDelimiter = listInfo.OrderedDelimiter,
DefaultOrderedStart = listInfo.DefaultOrderedStart,
OrderedStart = listInfo.OrderedStart,
LinesBefore = state.UseLinesBefore(),
};
if (state.TrackTrivia)
{
newList.LinesBefore = state.UseLinesBefore();
}
state.NewBlocks.Push(newList);
}
return BlockState.Continue;

View File

@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Markdig.Helpers;
using Markdig.Syntax;
@@ -43,8 +44,8 @@ namespace Markdig.Parsers
if (pipeline.PreciseSourceLocation)
{
int roughLineCountEstimate = text.Length / 40;
roughLineCountEstimate = Math.Min(4, Math.Max(512, roughLineCountEstimate));
int roughLineCountEstimate = text.Length / 32;
roughLineCountEstimate = Math.Max(4, Math.Min(512, roughLineCountEstimate));
document.LineStartIndexes = new List<int>(roughLineCountEstimate);
}
@@ -149,8 +150,9 @@ namespace Markdig.Parsers
for (; item.Index < container.Count; item.Index++)
{
var block = container[item.Index];
if (block is LeafBlock leafBlock)
if (block.IsLeafBlock)
{
LeafBlock leafBlock = Unsafe.As<LeafBlock>(block);
leafBlock.OnProcessInlinesBegin(inlineProcessor);
if (leafBlock.ProcessInlines)
{
@@ -167,10 +169,10 @@ namespace Markdig.Parsers
}
leafBlock.OnProcessInlinesEnd(inlineProcessor);
}
else if (block is ContainerBlock newContainer)
else if (block.IsContainerBlock)
{
// If we need to remove it
if (newContainer.RemoveAfterProcessInlines)
if (block.RemoveAfterProcessInlines)
{
container.RemoveAt(item.Index);
}
@@ -185,8 +187,8 @@ namespace Markdig.Parsers
Array.Resize(ref blocks, blockCount * 2);
ThrowHelper.CheckDepthLimit(blocks.Length);
}
blocks[blockCount++] = new ContainerItem(newContainer);
newContainer.OnProcessInlinesBegin(inlineProcessor);
blocks[blockCount++] = new ContainerItem(Unsafe.As<ContainerBlock>(block));
block.OnProcessInlinesBegin(inlineProcessor);
goto process_new_block;
}
}

View File

@@ -23,13 +23,19 @@ namespace Markdig.Parsers
}
// We continue trying to match by default
processor.NewBlocks.Push(new ParagraphBlock(this)
var paragraph = new ParagraphBlock(this)
{
Column = processor.Column,
Span = new SourceSpan(processor.Line.Start, processor.Line.End),
LinesBefore = processor.UseLinesBefore(),
NewLine = processor.Line.NewLine,
});
};
if (processor.TrackTrivia)
{
paragraph.LinesBefore = processor.UseLinesBefore();
paragraph.NewLine = processor.Line.NewLine;
}
processor.NewBlocks.Push(paragraph);
return BlockState.Continue;
}
@@ -128,15 +134,19 @@ namespace Markdig.Parsers
Span = new SourceSpan(paragraph.Span.Start, line.Start),
Level = level,
Lines = paragraph.Lines,
TriviaBefore = state.UseTrivia(sourcePosition - 1), // remove dashes
TriviaAfter = new StringSlice(state.Line.Text, state.Start, line.End),
LinesBefore = paragraph.LinesBefore,
NewLine = state.Line.NewLine,
IsSetext = true,
HeaderCharCount = count,
SetextNewline = paragraph.NewLine,
};
if (!state.TrackTrivia)
if (state.TrackTrivia)
{
heading.LinesBefore = paragraph.LinesBefore;
heading.TriviaBefore = state.UseTrivia(sourcePosition - 1); // remove dashes
heading.TriviaAfter = new StringSlice(state.Line.Text, state.Start, line.End);
heading.NewLine = state.Line.NewLine;
heading.SetextNewline = paragraph.NewLine;
}
else
{
heading.Lines.Trim();
}

View File

@@ -41,9 +41,13 @@ namespace Markdig.Parsers
QuoteChar = quoteChar,
Column = column,
Span = new SourceSpan(sourcePosition, processor.Line.End),
LinesBefore = processor.UseLinesBefore()
};
if (processor.TrackTrivia)
{
quoteBlock.LinesBefore = processor.UseLinesBefore();
}
bool hasSpaceAfterQuoteChar = false;
if (c == ' ')
{
@@ -56,28 +60,34 @@ namespace Markdig.Parsers
processor.NextColumn();
}
var triviaBefore = processor.UseTrivia(sourcePosition - 1);
StringSlice triviaAfter = StringSlice.Empty;
bool wasEmptyLine = false;
if (processor.Line.IsEmptyOrWhitespace())
if (processor.TrackTrivia)
{
processor.TriviaStart = processor.Start;
triviaAfter = processor.UseTrivia(processor.Line.End);
wasEmptyLine = true;
var triviaBefore = processor.UseTrivia(sourcePosition - 1);
StringSlice triviaAfter = StringSlice.Empty;
bool wasEmptyLine = false;
if (processor.Line.IsEmptyOrWhitespace())
{
processor.TriviaStart = processor.Start;
triviaAfter = processor.UseTrivia(processor.Line.End);
wasEmptyLine = true;
}
if (!wasEmptyLine)
{
processor.TriviaStart = processor.Start;
}
quoteBlock.QuoteLines.Add(new QuoteBlockLine
{
TriviaBefore = triviaBefore,
TriviaAfter = triviaAfter,
QuoteChar = true,
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
NewLine = processor.Line.NewLine,
});
}
quoteBlock.QuoteLines.Add(new QuoteBlockLine
{
TriviaBefore = triviaBefore,
TriviaAfter = triviaAfter,
QuoteChar = true,
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
NewLine = processor.Line.NewLine,
});
processor.NewBlocks.Push(quoteBlock);
if (!wasEmptyLine)
{
processor.TriviaStart = processor.Start;
}
return BlockState.Continue;
}
@@ -94,7 +104,6 @@ namespace Markdig.Parsers
// 5.1 Block quotes
// A block quote marker consists of 0-3 spaces of initial indent, plus (a) the character > together with a following space, or (b) a single character > not followed by a space.
var c = processor.CurrentChar;
bool hasSpaceAfterQuoteChar = false;
if (c != quote.QuoteChar)
{
if (processor.IsBlankLine)
@@ -103,14 +112,19 @@ namespace Markdig.Parsers
}
else
{
quote.QuoteLines.Add(new QuoteBlockLine
if (processor.TrackTrivia)
{
QuoteChar = false,
NewLine = processor.Line.NewLine,
});
quote.QuoteLines.Add(new QuoteBlockLine
{
QuoteChar = false,
NewLine = processor.Line.NewLine,
});
}
return BlockState.None;
}
}
bool hasSpaceAfterQuoteChar = false;
c = processor.NextChar(); // Skip quote marker char
if (c == ' ')
{
@@ -122,28 +136,33 @@ namespace Markdig.Parsers
{
processor.NextColumn();
}
var TriviaSpaceBefore = processor.UseTrivia(sourcePosition - 1);
StringSlice triviaAfter = StringSlice.Empty;
bool wasEmptyLine = false;
if (processor.Line.IsEmptyOrWhitespace())
{
processor.TriviaStart = processor.Start;
triviaAfter = processor.UseTrivia(processor.Line.End);
wasEmptyLine = true;
}
quote.QuoteLines.Add(new QuoteBlockLine
{
QuoteChar = true,
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
TriviaBefore = TriviaSpaceBefore,
TriviaAfter = triviaAfter,
NewLine = processor.Line.NewLine,
});
if (!wasEmptyLine)
if (processor.TrackTrivia)
{
processor.TriviaStart = processor.Start;
var triviaSpaceBefore = processor.UseTrivia(sourcePosition - 1);
StringSlice triviaAfter = StringSlice.Empty;
bool wasEmptyLine = false;
if (processor.Line.IsEmptyOrWhitespace())
{
processor.TriviaStart = processor.Start;
triviaAfter = processor.UseTrivia(processor.Line.End);
wasEmptyLine = true;
}
quote.QuoteLines.Add(new QuoteBlockLine
{
QuoteChar = true,
HasSpaceAfterQuoteChar = hasSpaceAfterQuoteChar,
TriviaBefore = triviaSpaceBefore,
TriviaAfter = triviaAfter,
NewLine = processor.Line.NewLine,
});
if (!wasEmptyLine)
{
processor.TriviaStart = processor.Start;
}
}
block.UpdateSpanEnd(processor.Line.End);
return BlockState.Continue;
}

View File

@@ -85,7 +85,7 @@ namespace Markdig.Parsers
}
// Push a new block
processor.NewBlocks.Push(new ThematicBreakBlock(this)
var thematicBreak = new ThematicBreakBlock(this)
{
Column = processor.Column,
Span = new SourceSpan(startPosition, line.End),
@@ -94,10 +94,16 @@ namespace Markdig.Parsers
// TODO: should we separate whitespace before/after?
//BeforeWhitespace = beforeWhitespace,
//AfterWhitespace = processor.PopBeforeWhitespace(processor.CurrentLineStartPosition),
LinesBefore = processor.UseLinesBefore(),
Content = new StringSlice(line.Text, processor.TriviaStart, line.End, line.NewLine), //include whitespace for now
NewLine = processor.Line.NewLine,
});
};
if (processor.TrackTrivia)
{
thematicBreak.LinesBefore = processor.UseLinesBefore();
thematicBreak.NewLine = processor.Line.NewLine;
}
processor.NewBlocks.Push(thematicBreak);
return BlockState.BreakDiscard;
}
}

View File

@@ -15,6 +15,9 @@ namespace System.Diagnostics.CodeAnalysis
public bool ReturnValue { get; }
}
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = false)]
public sealed class AllowNullAttribute : Attribute { }
#endif
#if !NET5_0_OR_GREATER

View File

@@ -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<T>(object o) where T : class
{
return (T)o;
}
}
#endif
}

View File

@@ -15,27 +15,25 @@ namespace Markdig.Renderers.Html
/// <seealso cref="HtmlObjectRenderer{CodeBlock}" />
public class CodeBlockRenderer : HtmlObjectRenderer<CodeBlock>
{
private HashSet<string>? _blocksAsDiv;
/// <summary>
/// Initializes a new instance of the <see cref="CodeBlockRenderer"/> class.
/// </summary>
public CodeBlockRenderer()
{
BlocksAsDiv = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
public CodeBlockRenderer() { }
public bool OutputAttributesOnPre { get; set; }
/// <summary>
/// Gets a map of fenced code block infos that should be rendered as div blocks instead of pre/code blocks.
/// </summary>
public HashSet<string> BlocksAsDiv { get; }
public HashSet<string> BlocksAsDiv => _blocksAsDiv ??= new HashSet<string>(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 @@ namespace Markdig.Renderers.Html
renderer.Write("<div")
.WriteAttributes(obj.TryGetAttributes(),
cls => cls.StartsWith(infoPrefix, StringComparison.Ordinal) ? cls.Substring(infoPrefix.Length) : cls)
.Write('>');
.WriteRaw('>');
}
renderer.WriteLeafRawLines(obj, true, true, true);
@@ -57,7 +55,6 @@ namespace Markdig.Renderers.Html
{
renderer.WriteLine("</div>");
}
}
else
{
@@ -70,14 +67,14 @@ namespace Markdig.Renderers.Html
renderer.WriteAttributes(obj);
}
renderer.Write("><code");
renderer.WriteRaw("><code");
if (!OutputAttributesOnPre)
{
renderer.WriteAttributes(obj);
}
renderer.Write('>');
renderer.WriteRaw('>');
}
renderer.WriteLeafRawLines(obj, true, true);

View File

@@ -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 @@ namespace Markdig.Renderers.Html
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("</").Write(headingText).WriteLine(">");
renderer.Write("</");
renderer.WriteRaw(headingText);
renderer.WriteLine('>');
}
renderer.EnsureLine();

View File

@@ -54,28 +54,26 @@ namespace Markdig.Renderers.Html.Inlines
{
if (renderer.EnableHtmlForInline)
{
renderer.Write("<a href=\"");
if (obj.IsEmail)
{
renderer.Write("mailto:");
}
renderer.Write(obj.IsEmail ? "<a href=\"mailto:" : "<a href=\"");
renderer.WriteEscapeUrl(obj.Url);
renderer.Write('"');
renderer.WriteRaw('"');
renderer.WriteAttributes(obj);
if (!obj.IsEmail && !string.IsNullOrWhiteSpace(Rel))
{
renderer.Write($" rel=\"{Rel}\"");
renderer.WriteRaw(" rel=\"");
renderer.WriteRaw(Rel);
renderer.WriteRaw('"');
}
renderer.Write('>');
renderer.WriteRaw('>');
}
renderer.WriteEscape(obj.Url);
if (renderer.EnableHtmlForInline)
{
renderer.Write("</a>");
renderer.WriteRaw("</a>");
}
}
}

View File

@@ -16,7 +16,9 @@ namespace Markdig.Renderers.Html.Inlines
{
if (renderer.EnableHtmlForInline)
{
renderer.Write("<code").WriteAttributes(obj).Write('>');
renderer.Write("<code");
renderer.WriteAttributes(obj);
renderer.WriteRaw('>');
}
if (renderer.EnableHtmlEscape)
{
@@ -28,7 +30,7 @@ namespace Markdig.Renderers.Html.Inlines
}
if (renderer.EnableHtmlForInline)
{
renderer.Write("</code>");
renderer.WriteRaw("</code>");
}
}
}

View File

@@ -39,12 +39,17 @@ namespace Markdig.Renderers.Html.Inlines
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("</").Write(tag).Write('>');
renderer.Write("</");
renderer.WriteRaw(tag);
renderer.WriteRaw('>');
}
}
@@ -53,7 +58,7 @@ namespace Markdig.Renderers.Html.Inlines
/// </summary>
/// <param name="obj">The object.</param>
/// <returns></returns>
public string? GetDefaultTag(EmphasisInline obj)
public static string? GetDefaultTag(EmphasisInline obj)
{
if (obj.DelimiterChar is '*' or '_')
{

View File

@@ -51,14 +51,14 @@ namespace Markdig.Renderers.Html.Inlines
{
renderer.Write(link.IsImage ? "<img src=\"" : "<a href=\"");
renderer.WriteEscapeUrl(link.GetDynamicUrl != null ? link.GetDynamicUrl() ?? link.Url : link.Url);
renderer.Write('"');
renderer.WriteRaw('"');
renderer.WriteAttributes(link);
}
if (link.IsImage)
{
if (renderer.EnableHtmlForInline)
{
renderer.Write(" alt=\"");
renderer.WriteRaw(" alt=\"");
}
var wasEnableHtmlForInline = renderer.EnableHtmlForInline;
renderer.EnableHtmlForInline = false;
@@ -66,22 +66,22 @@ namespace Markdig.Renderers.Html.Inlines
renderer.EnableHtmlForInline = wasEnableHtmlForInline;
if (renderer.EnableHtmlForInline)
{
renderer.Write('"');
renderer.WriteRaw('"');
}
}
if (renderer.EnableHtmlForInline && !string.IsNullOrEmpty(link.Title))
{
renderer.Write(" title=\"");
renderer.WriteRaw(" title=\"");
renderer.WriteEscape(link.Title);
renderer.Write('"');
renderer.WriteRaw('"');
}
if (link.IsImage)
{
if (renderer.EnableHtmlForInline)
{
renderer.Write(" />");
renderer.WriteRaw(" />");
}
}
else
@@ -90,9 +90,11 @@ namespace Markdig.Renderers.Html.Inlines
{
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)

View File

@@ -22,12 +22,16 @@ namespace Markdig.Renderers.Html
renderer.Write("<ol");
if (listBlock.BulletType != '1')
{
renderer.Write(" type=\"").Write(listBlock.BulletType).Write('"');
renderer.WriteRaw(" type=\"");
renderer.WriteRaw(listBlock.BulletType);
renderer.WriteRaw('"');
}
if (listBlock.OrderedStart != null && (listBlock.OrderedStart is not "1"))
if (listBlock.OrderedStart is not null && listBlock.OrderedStart != "1")
{
renderer.Write(" start=\"").Write(listBlock.OrderedStart).Write('"');
renderer.Write(" start=\"");
renderer.WriteRaw(listBlock.OrderedStart);
renderer.WriteRaw('"');
}
renderer.WriteAttributes(listBlock);
renderer.WriteLine('>');
@@ -49,7 +53,9 @@ namespace Markdig.Renderers.Html
renderer.EnsureLine();
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<li").WriteAttributes(listItem).Write('>');
renderer.Write("<li");
renderer.WriteAttributes(listItem);
renderer.WriteRaw('>');
}
renderer.WriteChildren(listItem);

View File

@@ -21,12 +21,14 @@ namespace Markdig.Renderers.Html
renderer.EnsureLine();
}
renderer.Write("<p").WriteAttributes(obj).Write(">");
renderer.Write("<p");
renderer.WriteAttributes(obj);
renderer.WriteRaw('>');
}
renderer.WriteLeafInline(obj);
if (!renderer.ImplicitParagraph)
{
if(renderer.EnableHtmlForBlock)
if (renderer.EnableHtmlForBlock)
{
renderer.WriteLine("</p>");
}

View File

@@ -17,7 +17,9 @@ namespace Markdig.Renderers.Html
renderer.EnsureLine();
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<blockquote").WriteAttributes(obj).WriteLine(">");
renderer.Write("<blockquote");
renderer.WriteAttributes(obj);
renderer.WriteLine('>');
}
var savedImplicitParagraph = renderer.ImplicitParagraph;
renderer.ImplicitParagraph = false;

View File

@@ -16,7 +16,9 @@ namespace Markdig.Renderers.Html
{
if (renderer.EnableHtmlForBlock)
{
renderer.Write("<hr").WriteAttributes(obj).WriteLine(" />");
renderer.Write("<hr");
renderer.WriteAttributes(obj);
renderer.WriteLine(" />");
}
}
}

View File

@@ -20,6 +20,8 @@ namespace Markdig.Renderers
/// <seealso cref="TextRendererBase{HtmlRenderer}" />
public class HtmlRenderer : TextRendererBase<HtmlRenderer>
{
private static readonly char[] s_writeEscapeIndexOfAnyChars = new[] { '<', '>', '&', '"' };
/// <summary>
/// Initializes a new instance of the <see cref="HtmlRenderer"/> class.
/// </summary>
@@ -94,10 +96,7 @@ namespace Markdig.Renderers
[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 @@ namespace Markdig.Renderers
[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;
}
/// <summary>
@@ -126,7 +122,8 @@ namespace Markdig.Renderers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public HtmlRenderer WriteEscape(StringSlice slice, bool softEscape = false)
{
return WriteEscape(ref slice, softEscape);
WriteEscape(slice.AsSpan(), softEscape);
return this;
}
/// <summary>
@@ -139,58 +136,82 @@ namespace Markdig.Renderers
/// <returns>This instance</returns>
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;
}
var end = offset + length;
int previousOffset = offset;
for (;offset < end; offset++)
/// <summary>
/// Writes the content escaped for HTML.
/// </summary>
/// <param name="content">The content.</param>
/// <param name="softEscape">Only escape &lt; and &amp;</param>
public void WriteEscape(ReadOnlySpan<char> content, bool softEscape = false)
{
if (!content.IsEmpty)
{
switch (content[offset])
int nextIndex = content.IndexOfAny(s_writeEscapeIndexOfAnyChars);
if (nextIndex == -1)
{
Write(content);
}
else
{
WriteEscapeSlow(content, softEscape);
}
}
}
private void WriteEscapeSlow(ReadOnlySpan<char> content, bool softEscape = false)
{
WriteIndent();
int previousOffset = 0;
for (int i = 0; i < content.Length; i++)
{
switch (content[i])
{
case '<':
Write(content, previousOffset, offset - previousOffset);
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
Write("&lt;");
WriteRaw("&lt;");
}
previousOffset = offset + 1;
previousOffset = i + 1;
break;
case '>':
if (!softEscape)
{
Write(content, previousOffset, offset - previousOffset);
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
Write("&gt;");
WriteRaw("&gt;");
}
previousOffset = offset + 1;
previousOffset = i + 1;
}
break;
case '&':
Write(content, previousOffset, offset - previousOffset);
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
Write("&amp;");
WriteRaw("&amp;");
}
previousOffset = offset + 1;
previousOffset = i + 1;
break;
case '"':
if (!softEscape)
{
Write(content, previousOffset, offset - previousOffset);
WriteRaw(content.Slice(previousOffset, i - previousOffset));
if (EnableHtmlEscape)
{
Write("&quot;");
WriteRaw("&quot;");
}
previousOffset = offset + 1;
previousOffset = i + 1;
}
break;
}
}
Write(content, previousOffset, end - previousOffset);
return this;
WriteRaw(content.Slice(previousOffset));
}
private static readonly IdnMapping IdnMapping = new IdnMapping();
@@ -218,8 +239,8 @@ namespace Markdig.Renderers
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 +381,9 @@ namespace Markdig.Renderers
if (attributes.Id != null)
{
Write(" id=\"").WriteEscape(attributes.Id).Write('"');
Write(" id=\"");
WriteEscape(attributes.Id);
WriteRaw('"');
}
if (attributes.Classes is { Count: > 0 })
@@ -371,21 +394,22 @@ namespace Markdig.Renderers
var cssClass = attributes.Classes[i];
if (i > 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 +427,40 @@ namespace Markdig.Renderers
public HtmlRenderer WriteLeafRawLines(LeafBlock leafBlock, bool writeEndOfLines, bool escape, bool softEscape = false)
{
if (leafBlock is null) ThrowHelper.ArgumentNullException_leafBlock();
if (leafBlock.Lines.Lines != null)
var slices = leafBlock.Lines.Lines;
if (slices is not null)
{
var lines = leafBlock.Lines;
var slices = lines.Lines;
for (int i = 0; i < lines.Count; i++)
for (int i = 0; i < slices.Length; i++)
{
ref StringSlice slice = ref slices[i].Slice;
if (slice.Text is null)
{
break;
}
if (!writeEndOfLines && i > 0)
{
WriteLine();
}
ReadOnlySpan<char> 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;
}
}

View File

@@ -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 @@ namespace Markdig.Renderers
/// Accepts the specified <see cref="MarkdownObject"/>.
/// </summary>
/// <param name="renderer">The renderer.</param>
/// <param name="obj">The Markdown object.</param>
/// <param name="objectType">The <see cref="Type"/> of the Markdown object.</param>
/// <returns><c>true</c> If this renderer is accepting to render the specified Markdown object</returns>
bool Accept(RendererBase renderer, MarkdownObject obj);
bool Accept(RendererBase renderer, Type objectType);
/// <summary>
/// Writes the specified <see cref="MarkdownObject"/> to the <paramref name="renderer"/>.

View File

@@ -4,6 +4,7 @@
using Markdig.Helpers;
using Markdig.Syntax;
using System;
namespace Markdig.Renderers
{
@@ -15,16 +16,15 @@ namespace Markdig.Renderers
/// <seealso cref="IMarkdownObjectRenderer" />
public abstract class MarkdownObjectRenderer<TRenderer, TObject> : IMarkdownObjectRenderer where TRenderer : RendererBase where TObject : MarkdownObject
{
protected MarkdownObjectRenderer()
{
TryWriters = new OrderedList<TryWriteDelegate>();
}
private OrderedList<TryWriteDelegate>? _tryWriters;
protected MarkdownObjectRenderer() { }
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)
@@ -32,23 +32,31 @@ namespace Markdig.Renderers
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;
}
/// <summary>
/// Gets the optional writers attached to this instance.
/// </summary>
public OrderedList<TryWriteDelegate> TryWriters { get; }
public OrderedList<TryWriteDelegate> TryWriters => _tryWriters ??= new();
/// <summary>
/// Writes the specified Markdown object to the renderer.

View File

@@ -14,6 +14,12 @@ namespace Markdig.Renderers.Normalize.Inlines
{
protected override void Write(NormalizeRenderer renderer, LinkInline link)
{
if (link.IsAutoLink && !renderer.Options.ExpandAutoLinks)
{
renderer.Write(link.Url);
return;
}
if (link.IsImage)
{
renderer.Write('!');

View File

@@ -16,21 +16,34 @@ namespace Markdig.Renderers
/// <seealso cref="IMarkdownRenderer" />
public abstract class RendererBase : IMarkdownRenderer
{
private readonly Dictionary<Type, IMarkdownObjectRenderer> renderersPerType;
private IMarkdownObjectRenderer? previousRenderer;
private Type? previousObjectType;
internal int childrenDepth = 0;
private readonly Dictionary<RuntimeTypeHandle, IMarkdownObjectRenderer?> _renderersPerType = new();
internal int _childrenDepth = 0;
/// <summary>
/// Initializes a new instance of the <see cref="RendererBase"/> class.
/// </summary>
protected RendererBase()
protected RendererBase() { }
private IMarkdownObjectRenderer? GetRendererInstance(MarkdownObject obj)
{
ObjectRenderers = new ObjectRendererCollection();
renderersPerType = new Dictionary<Type, IMarkdownObjectRenderer>();
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[typeHandle] = renderer;
return renderer;
}
}
_renderersPerType[typeHandle] = null;
return null;
}
public ObjectRendererCollection ObjectRenderers { get; }
public ObjectRendererCollection ObjectRenderers { get; } = new();
public abstract object Render(MarkdownObject markdownObject);
@@ -59,7 +72,7 @@ namespace Markdig.Renderers
return;
}
ThrowHelper.CheckDepthLimit(childrenDepth++);
ThrowHelper.CheckDepthLimit(_childrenDepth++);
bool saveIsFirstInContainer = IsFirstInContainer;
bool saveIsLastInContainer = IsLastInContainer;
@@ -75,7 +88,7 @@ namespace Markdig.Renderers
IsFirstInContainer = saveIsFirstInContainer;
IsLastInContainer = saveIsLastInContainer;
childrenDepth--;
_childrenDepth--;
}
/// <summary>
@@ -89,7 +102,7 @@ namespace Markdig.Renderers
return;
}
ThrowHelper.CheckDepthLimit(childrenDepth++);
ThrowHelper.CheckDepthLimit(_childrenDepth++);
bool saveIsFirstInContainer = IsFirstInContainer;
bool saveIsLastInContainer = IsLastInContainer;
@@ -110,7 +123,7 @@ namespace Markdig.Renderers
IsFirstInContainer = saveIsFirstInContainer;
IsLastInContainer = saveIsLastInContainer;
childrenDepth--;
_childrenDepth--;
}
/// <summary>
@@ -127,43 +140,23 @@ namespace Markdig.Renderers
// Calls before writing an object
ObjectWriteBefore?.Invoke(this, obj);
var objectType = obj.GetType();
IMarkdownObjectRenderer? renderer;
// Handle regular renderers
if (objectType == previousObjectType)
if (!_renderersPerType.TryGetValue(Type.GetTypeHandle(obj), out IMarkdownObjectRenderer? renderer))
{
renderer = previousRenderer;
}
else if (!renderersPerType.TryGetValue(objectType, out renderer))
{
for (int i = 0; i < ObjectRenderers.Count; i++)
{
var testRenderer = ObjectRenderers[i];
if (testRenderer.Accept(this, obj))
{
renderersPerType[objectType] = renderer = testRenderer;
break;
}
}
renderer = GetRendererInstance(obj);
}
if (renderer != null)
if (renderer is not null)
{
renderer.Write(this, obj);
previousObjectType = objectType;
previousRenderer = renderer;
}
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);

View File

@@ -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
/// <seealso cref="RendererBase" />
public abstract class TextRendererBase : RendererBase
{
private TextWriter writer;
private TextWriter _writer;
/// <summary>
/// Initializes a new instance of the <see cref="TextRendererBase"/> class.
@@ -27,9 +28,7 @@ namespace Markdig.Renderers
/// <exception cref="ArgumentNullException"></exception>
protected TextRendererBase(TextWriter writer)
{
if (writer is null) ThrowHelper.ArgumentNullException_writer();
this.writer = writer;
this.writer.NewLine = "\n";
Writer = writer;
}
/// <summary>
@@ -38,7 +37,8 @@ namespace Markdig.Renderers
/// <exception cref="ArgumentNullException">if the value is null</exception>
public TextWriter Writer
{
get { return writer; }
get => _writer;
[MemberNotNull(nameof(_writer))]
set
{
if (value is null)
@@ -48,7 +48,7 @@ namespace Markdig.Renderers
// By default we output a newline with '\n' only even on Windows platforms
value.NewLine = "\n";
writer = value;
_writer = value;
}
}
@@ -137,7 +137,7 @@ namespace Markdig.Renderers
internal void ResetInternal()
{
childrenDepth = 0;
_childrenDepth = 0;
previousWasLine = true;
indents.Clear();
}
@@ -146,11 +146,13 @@ namespace Markdig.Renderers
/// Ensures a newline.
/// </summary>
/// <returns>This instance</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T EnsureLine()
{
if (!previousWasLine)
{
WriteLine();
previousWasLine = true;
Writer.WriteLine();
}
return (T)this;
}
@@ -177,20 +179,25 @@ namespace Markdig.Renderers
indents.RemoveAt(indents.Count - 1);
}
private void WriteIndent()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private protected 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);
}
}
/// <summary>
/// Writes the specified content.
@@ -201,9 +208,8 @@ namespace Markdig.Renderers
public T Write(string? content)
{
WriteIndent();
previousWasLine = false;
Writer.Write(content);
return (T) this;
return (T)this;
}
/// <summary>
@@ -214,11 +220,8 @@ namespace Markdig.Renderers
[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;
}
/// <summary>
@@ -229,7 +232,8 @@ namespace Markdig.Renderers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Write(StringSlice slice)
{
return Write(ref slice);
Write(slice.AsSpan());
return (T)this;
}
/// <summary>
@@ -241,9 +245,12 @@ namespace Markdig.Renderers
public T Write(char content)
{
WriteIndent();
previousWasLine = content == '\n';
if (content == '\n')
{
previousWasLine = true;
}
Writer.Write(content);
return (T) this;
return (T)this;
}
/// <summary>
@@ -255,36 +262,49 @@ namespace Markdig.Renderers
/// <returns>This instance</returns>
public T Write(string content, int offset, int length)
{
if (content is null)
if (content is not null)
{
return (T) this;
Write(content.AsSpan(offset, length));
}
return (T)this;
}
WriteIndent();
previousWasLine = false;
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
Writer.Write(content.AsSpan(offset, length));
#else
if (offset == 0 && content.Length == length)
/// <summary>
/// Writes the specified content.
/// </summary>
/// <param name="content">The content.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Write(ReadOnlySpan<char> content)
{
if (!content.IsEmpty)
{
Writer.Write(content);
WriteIndent();
WriteRaw(content);
}
}
[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<char> content)
{
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
Writer.Write(content);
#else
if (content.Length > buffer.Length)
{
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;
}
/// <summary>
@@ -297,7 +317,7 @@ namespace Markdig.Renderers
WriteIndent();
Writer.WriteLine();
previousWasLine = true;
return (T) this;
return (T)this;
}
/// <summary>
@@ -324,7 +344,7 @@ namespace Markdig.Renderers
WriteIndent();
previousWasLine = true;
Writer.WriteLine(content);
return (T) this;
return (T)this;
}
/// <summary>
@@ -350,15 +370,15 @@ namespace Markdig.Renderers
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;
}
}
}

View File

@@ -111,14 +111,14 @@ All trivia in a document should be attached to a node. The `Block` class defines
/// <summary>
/// Gets or sets the trivia right before this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBefore { get; set; }
/// <summary>
/// Gets or sets trivia occurring after this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaAfter { get; set; }
```

View File

@@ -0,0 +1,7 @@
// 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 NET5_0_OR_GREATER
[module: System.Runtime.CompilerServices.SkipLocalsInit]
#endif

View File

@@ -14,6 +14,9 @@ namespace Markdig.Syntax
/// <seealso cref="MarkdownObject" />
public abstract class Block : MarkdownObject, IBlock
{
private BlockTriviaProperties? _trivia;
private BlockTriviaProperties Trivia => _trivia ??= new();
/// <summary>
/// Initializes a new instance of the <see cref="Block"/> class.
/// </summary>
@@ -28,13 +31,17 @@ namespace Markdig.Syntax
/// <summary>
/// Gets the parent of this container. May be null.
/// </summary>
public ContainerBlock? Parent { get; internal set; }
public ContainerBlock? Parent { get; internal set; }
/// <summary>
/// Gets the parser associated to this instance.
/// </summary>
public BlockParser? Parser { get; }
internal bool IsContainerBlock { get; private protected set; }
internal bool IsLeafBlock { get; private protected set; }
internal bool IsParagraphBlock { get; private protected set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is still open.
/// </summary>
@@ -46,7 +53,8 @@ namespace Markdig.Syntax
public bool IsBreakable { get; set; }
/// <summary>
/// The last newline of this block
/// The last newline of this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled
/// </summary>
public NewLine NewLine { get; set; }
@@ -58,28 +66,28 @@ namespace Markdig.Syntax
/// <summary>
/// Gets or sets the trivia right before this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBefore { get; set; }
public StringSlice TriviaBefore { get => _trivia?.TriviaBefore ?? StringSlice.Empty; set => Trivia.TriviaBefore = value; }
/// <summary>
/// Gets or sets trivia occurring after this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaAfter { get; set; }
public StringSlice TriviaAfter { get => _trivia?.TriviaAfter ?? StringSlice.Empty; set => Trivia.TriviaAfter = value; }
/// <summary>
/// Gets or sets the empty lines occurring before this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise null.
/// </summary>
public List<StringSlice>? LinesBefore { get; set; }
public List<StringSlice>? LinesBefore { get => _trivia?.LinesBefore; set => Trivia.LinesBefore = value; }
/// <summary>
/// Gets or sets the empty lines occurring after this block.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise null.
/// </summary>
public List<StringSlice>? LinesAfter { get; set; }
public List<StringSlice>? LinesAfter { get => _trivia?.LinesAfter; set => Trivia.LinesAfter = value; }
/// <summary>
/// Occurs when the process of inlines begin.
@@ -128,11 +136,30 @@ namespace Markdig.Syntax
internal static Block FindRootMostContainerParent(Block block)
{
while (block.Parent is ContainerBlock && block.Parent is not MarkdownDocument)
while (true)
{
block = block.Parent;
Block? parent = block.Parent;
if (parent is null || !parent.IsContainerBlock || parent is MarkdownDocument)
{
break;
}
block = parent;
}
return block;
}
private protected T? TryGetDerivedTrivia<T>() where T : class => _trivia?.DerivedTriviaSlot as T;
private protected T GetOrSetDerivedTrivia<T>() where T : new() => (T)(Trivia.DerivedTriviaSlot ??= new T());
private sealed class BlockTriviaProperties
{
// Used by derived types to store their own TriviaProperties
public object? DerivedTriviaSlot;
public StringSlice TriviaBefore;
public StringSlice TriviaAfter;
public List<StringSlice>? LinesBefore;
public List<StringSlice>? LinesAfter;
}
}
}

View File

@@ -21,7 +21,8 @@ namespace Markdig.Syntax
public StringSlice TriviaBefore { get; set; }
}
public List<CodeBlockLine> CodeBlockLines { get; } = new ();
private List<CodeBlockLine>? _codeBlockLines;
public List<CodeBlockLine> CodeBlockLines => _codeBlockLines ??= new();
/// <summary>
/// Initializes a new instance of the <see cref="CodeBlock"/> class.

View File

@@ -28,6 +28,7 @@ namespace Markdig.Syntax
protected ContainerBlock(BlockParser? parser) : base(parser)
{
children = ArrayHelper.Empty<Block>();
IsContainerBlock = true;
}
/// <summary>

View File

@@ -15,6 +15,9 @@ namespace Markdig.Syntax
/// </remarks>
public class FencedCodeBlock : CodeBlock, IFencedBlock
{
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
/// <summary>
/// Initializes a new instance of the <see cref="FencedCodeBlock"/> class.
/// </summary>
@@ -41,33 +44,44 @@ namespace Markdig.Syntax
public int OpeningFencedCharCount { get; set; }
/// <inheritdoc />
public StringSlice TriviaAfterFencedChar { get; set; }
public StringSlice TriviaAfterFencedChar { get => _trivia?.TriviaAfterFencedChar ?? StringSlice.Empty; set => Trivia.TriviaAfterFencedChar = value; }
/// <inheritdoc />
public string? Info { get; set; }
/// <inheritdoc />
public StringSlice UnescapedInfo { get; set; }
public StringSlice UnescapedInfo { get => _trivia?.UnescapedInfo ?? StringSlice.Empty; set => Trivia.UnescapedInfo = value; }
/// <inheritdoc />
public StringSlice TriviaAfterInfo { get; set; }
public StringSlice TriviaAfterInfo { get => _trivia?.TriviaAfterInfo ?? StringSlice.Empty; set => Trivia.TriviaAfterInfo = value; }
/// <inheritdoc />
public string? Arguments { get; set; }
/// <inheritdoc />
public StringSlice UnescapedArguments { get; set; }
public StringSlice UnescapedArguments { get => _trivia?.UnescapedArguments ?? StringSlice.Empty; set => Trivia.UnescapedArguments = value; }
/// <inheritdoc />
public StringSlice TriviaAfterArguments { get; set; }
public StringSlice TriviaAfterArguments { get => _trivia?.TriviaAfterArguments ?? StringSlice.Empty; set => Trivia.TriviaAfterArguments = value; }
/// <inheritdoc />
public NewLine InfoNewLine { get; set; }
public NewLine InfoNewLine { get => _trivia?.InfoNewLine ?? NewLine.None; set => Trivia.InfoNewLine = value; }
/// <inheritdoc />
public StringSlice TriviaBeforeClosingFence { get; set; }
public StringSlice TriviaBeforeClosingFence { get => _trivia?.TriviaBeforeClosingFence ?? StringSlice.Empty; set => Trivia.TriviaBeforeClosingFence = value; }
/// <inheritdoc />
public int ClosingFencedCharCount { get; set; }
private sealed class TriviaProperties
{
public StringSlice TriviaAfterFencedChar;
public StringSlice UnescapedInfo;
public StringSlice TriviaAfterInfo;
public StringSlice UnescapedArguments;
public StringSlice TriviaAfterArguments;
public NewLine InfoNewLine;
public StringSlice TriviaBeforeClosingFence;
}
}
}

View File

@@ -14,6 +14,9 @@ namespace Markdig.Syntax
[DebuggerDisplay("{GetType().Name} Line: {Line}, {Lines} Level: {Level}")]
public class HeadingBlock : LeafBlock
{
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
/// <summary>
/// Initializes a new instance of the <see cref="HeadingBlock"/> class.
/// </summary>
@@ -45,14 +48,21 @@ namespace Markdig.Syntax
/// <summary>
/// Gets or sets the newline of the first line when <see cref="IsSetext"/> is true.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
/// </summary>
public NewLine SetextNewline { get; set; }
public NewLine SetextNewline { get => _trivia?.SetextNewline ?? NewLine.None; set => Trivia.SetextNewline = value; }
/// <summary>
/// Gets or sets the whitespace after the # character when <see cref="IsSetext"/> is false.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaAfterAtxHeaderChar { get; set; }
public StringSlice TriviaAfterAtxHeaderChar { get => _trivia?.TriviaAfterAtxHeaderChar ?? StringSlice.Empty; set => Trivia.TriviaAfterAtxHeaderChar = value; }
private sealed class TriviaProperties
{
public NewLine SetextNewline;
public StringSlice TriviaAfterAtxHeaderChar;
}
}
}

View File

@@ -61,13 +61,13 @@ namespace Markdig.Syntax
/// <summary>
/// Trivia occurring before this block
/// </summary>
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.IsEmpty"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.Empty"/>.
StringSlice TriviaBefore { get; set; }
/// <summary>
/// Trivia occurring after this block
/// </summary>
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.IsEmpty"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise <see cref="StringSlice.Empty"/>.
StringSlice TriviaAfter { get; set; }
}
}

View File

@@ -3,7 +3,6 @@
// See the license.txt file in the project root for more information.
using Markdig.Helpers;
using Markdig.Parsers;
namespace Markdig.Syntax
{
@@ -25,7 +24,7 @@ namespace Markdig.Syntax
/// <summary>
/// Gets or sets the trivia after the <see cref="FencedChar"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
StringSlice TriviaAfterFencedChar { get; set; }
@@ -38,14 +37,14 @@ namespace Markdig.Syntax
/// <summary>
/// Non-escaped <see cref="Info"/> exactly as in source markdown.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
StringSlice UnescapedInfo { get; set; }
/// <summary>
/// Gets or sets the trivia after the <see cref="Info"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
StringSlice TriviaAfterInfo { get; set; }
@@ -58,28 +57,28 @@ namespace Markdig.Syntax
/// <summary>
/// Non-escaped <see cref="Arguments"/> exactly as in source markdown.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
StringSlice UnescapedArguments { get; set; }
/// <summary>
/// Gets or sets the trivia after the <see cref="Arguments"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
StringSlice TriviaAfterArguments { get; set; }
/// <summary>
/// Newline of the line with the opening fenced chars.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="NewLine.None"/>.
/// </summary>
NewLine InfoNewLine { get; set; }
/// <summary>
/// Trivia before the closing fenced chars
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
StringSlice TriviaBeforeClosingFence { get; set; }
@@ -92,7 +91,7 @@ namespace Markdig.Syntax
/// Newline after the last line, which is always the line containing the closing fence chars.
/// "Inherited" from <see cref="Block.NewLine"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="NewLine.None"/>.
/// </summary>
NewLine NewLine { get; set; }
}

View File

@@ -14,6 +14,9 @@ namespace Markdig.Syntax.Inlines
[DebuggerDisplay("`{Content}`")]
public class CodeInline : LeafInline
{
private TriviaProperties? _trivia;
private TriviaProperties Trivia => _trivia ??= new();
public CodeInline(string content)
{
Content = content;
@@ -37,16 +40,13 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// Gets or sets the content with trivia and whitespace.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice ContentWithTrivia { get; set; }
public StringSlice ContentWithTrivia { get => _trivia?.ContentWithTrivia ?? StringSlice.Empty; set => Trivia.ContentWithTrivia = value; }
/// <summary>
/// True if the first and last character of the content enclosed in a backtick `
/// is a space.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// false.
/// </summary>
public bool FirstAndLastWasSpace { get; set; }
private sealed class TriviaProperties
{
public StringSlice ContentWithTrivia;
}
}
}

View File

@@ -17,6 +17,11 @@ namespace Markdig.Syntax.Inlines
/// <seealso cref="Inline" />
public class ContainerInline : Inline, IEnumerable<Inline>
{
public ContainerInline()
{
IsContainerInline = true;
}
/// <summary>
/// Gets the parent block of this inline.
/// </summary>

View File

@@ -30,6 +30,8 @@ namespace Markdig.Syntax.Inlines
/// </summary>
public Inline? NextSibling { get; internal set; }
internal bool IsContainerInline { get; private protected set; }
/// <summary>
/// Gets or sets a value indicating whether this instance is closed.
/// </summary>

View File

@@ -13,6 +13,9 @@ namespace Markdig.Syntax.Inlines
/// <seealso cref="DelimiterInline" />
public class LinkDelimiterInline : DelimiterInline
{
private TriviaProperties? _trivia;
private TriviaProperties Trivia => _trivia ??= new();
public LinkDelimiterInline(InlineParser parser) : base(parser)
{
}
@@ -35,13 +38,18 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// Gets or sets the <see cref="Label"/> with trivia.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice LabelWithTrivia { get; set; }
public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; }
public override string ToLiteral()
{
return IsImage ? "![" : "[";
}
private sealed class TriviaProperties
{
public StringSlice LabelWithTrivia;
}
}
}

View File

@@ -7,12 +7,13 @@ using System.Diagnostics;
namespace Markdig.Syntax.Inlines
{
public enum LocalLabel
public enum LocalLabel : byte
{
Local, // [foo][bar]
Empty, // [foo][]
None, // [foo]
}
/// <summary>
/// A Link inline (Section 6.5 CommonMark specs)
/// </summary>
@@ -20,6 +21,9 @@ namespace Markdig.Syntax.Inlines
[DebuggerDisplay("Url: {Url} Title: {Title} Image: {IsImage}")]
public class LinkInline : ContainerInline
{
private TriviaProperties? _trivia;
private TriviaProperties Trivia => _trivia ??= new();
/// <summary>
/// A delegate to use if it is setup on this instance to allow late binding
/// of a Url.
@@ -63,16 +67,16 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// Gets or sets the <see cref="Label"/> with trivia.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice LabelWithTrivia { get; set; }
public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; }
/// <summary>
/// Gets or sets the type of label parsed
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="LocalLabel.None"/>.
/// </summary>
public LocalLabel LocalLabel { get; set; }
public LocalLabel LocalLabel { get => _trivia?.LocalLabel ?? LocalLabel.None; set => Trivia.LocalLabel = value; }
/// <summary>
/// Gets or sets the reference this link is attached to. May be null.
@@ -81,21 +85,22 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// Gets or sets the label as matched against the <see cref="LinkReferenceDefinition"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
/// </summary>
public string? LinkRefDefLabel { get; set; }
public string? LinkRefDefLabel { get => _trivia?.LinkRefDefLabel; set => Trivia.LinkRefDefLabel = value; }
/// <summary>
/// Gets or sets the <see cref="LinkRefDefLabel"/> with trivia as matched against
/// the <see cref="LinkReferenceDefinition"/>
/// </summary>
public StringSlice LinkRefDefLabelWithTrivia { get; set; }
public StringSlice LinkRefDefLabelWithTrivia { get => _trivia?.LinkRefDefLabelWithTrivia ?? StringSlice.Empty; set => Trivia.LinkRefDefLabelWithTrivia = value; }
/// <summary>
/// Gets or sets the trivia before the <see cref="Url"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBeforeUrl { get; set; }
public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; }
/// <summary>
/// True if the <see cref="Url"/> in the source document is enclosed
@@ -103,7 +108,7 @@ namespace Markdig.Syntax.Inlines
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// false.
/// </summary>
public bool UrlHasPointyBrackets { get; set; }
public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; }
/// <summary>
/// Gets or sets the URL.
@@ -118,16 +123,16 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// The <see cref="Url"/> but with trivia and unescaped characters
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice UnescapedUrl { get; set; }
public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; }
/// <summary>
/// Any trivia after the <see cref="Url"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaAfterUrl { get; set; }
public StringSlice TriviaAfterUrl { get => _trivia?.TriviaAfterUrl ?? StringSlice.Empty; set => Trivia.TriviaAfterUrl = value; }
/// <summary>
/// Gets or sets the GetDynamicUrl delegate. If this property is set,
@@ -137,10 +142,9 @@ namespace Markdig.Syntax.Inlines
/// <summary>
/// Gets or sets the character used to enclose the <see cref="Title"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
/// </summary>
public char TitleEnclosingCharacter { get; set; }
public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; }
/// <summary>
/// Gets or sets the title.
@@ -156,16 +160,16 @@ namespace Markdig.Syntax.Inlines
/// Gets or sets the <see cref="Title"/> exactly as parsed from the
/// source document including unescaped characters
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice UnescapedTitle { get; set; }
public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; }
/// <summary>
/// Gets or sets the trivia after the <see cref="Title"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaAfterTitle { get; set; }
public StringSlice TriviaAfterTitle { get => _trivia?.TriviaAfterTitle ?? StringSlice.Empty; set => Trivia.TriviaAfterTitle = value; }
/// <summary>
/// Gets or sets a boolean indicating if this link is a shortcut link to a <see cref="LinkReferenceDefinition"/>
@@ -176,5 +180,20 @@ namespace Markdig.Syntax.Inlines
/// Gets or sets a boolean indicating whether the inline link was parsed using markdown syntax or was automatic recognized.
/// </summary>
public bool IsAutoLink { get; set; }
private sealed class TriviaProperties
{
public StringSlice LabelWithTrivia;
public LocalLabel LocalLabel;
public string? LinkRefDefLabel;
public StringSlice LinkRefDefLabelWithTrivia;
public StringSlice TriviaBeforeUrl;
public bool UrlHasPointyBrackets;
public StringSlice UnescapedUrl;
public StringSlice TriviaAfterUrl;
public char TitleEnclosingCharacter;
public StringSlice UnescapedTitle;
public StringSlice TriviaAfterTitle;
}
}
}

View File

@@ -24,6 +24,7 @@ namespace Markdig.Syntax
/// <param name="parser">The parser used to create this block.</param>
protected LeafBlock(BlockParser? parser) : base(parser)
{
IsLeafBlock = true;
}
/// <summary>
@@ -80,28 +81,20 @@ namespace Markdig.Syntax
{
Lines = new StringLineGroup(4, ProcessInlines);
}
var stringLine = new StringLine(ref slice, line, column, sourceLinePosition, slice.NewLine);
// Regular case, we are not in the middle of a tab
if (slice.CurrentChar != '\t' || !CharHelper.IsAcrossTab(column))
// Regular case: we are not in the middle of a tab
if (slice.CurrentChar == '\t' && CharHelper.IsAcrossTab(column) && !trackTrivia)
{
Lines.Add(ref stringLine);
}
else
{
var builder = StringBuilderCache.Local();
if (trackTrivia)
{
builder.Append(slice.Text, slice.Start, slice.Length);
}
else
{
// We need to expand tabs to spaces
builder.Append(' ', CharHelper.AddTab(column) - column);
builder.Append(slice.Text, slice.Start + 1, slice.Length - 1);
}
stringLine.Slice = new StringSlice(builder.GetStringAndReset());
Lines.Add(ref stringLine);
// We need to expand tabs to spaces
var builder = new ValueStringBuilder(stackalloc char[ValueStringBuilder.StackallocThreshold]);
builder.Append(' ', CharHelper.AddTab(column) - column);
builder.Append(slice.AsSpan().Slice(1));
stringLine.Slice = new StringSlice(builder.ToString());
}
Lines.Add(ref stringLine);
NewLine = slice.NewLine; // update newline, as it should be the last newline of the block
}
}

View File

@@ -16,6 +16,9 @@ namespace Markdig.Syntax
/// <seealso cref="LeafBlock" />
public class LinkReferenceDefinition : LeafBlock
{
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
/// <summary>
/// Creates an inline link for the specified <see cref="LinkReferenceDefinition"/>.
/// </summary>
@@ -60,16 +63,16 @@ namespace Markdig.Syntax
/// <summary>
/// Non-normalized Label (includes trivia)
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice LabelWithTrivia { get; set; }
public StringSlice LabelWithTrivia { get => _trivia?.LabelWithTrivia ?? StringSlice.Empty; set => Trivia.LabelWithTrivia = value; }
/// <summary>
/// Whitespace before the <see cref="Url"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBeforeUrl { get; set; }
public StringSlice TriviaBeforeUrl { get => _trivia?.TriviaBeforeUrl ?? StringSlice.Empty; set => Trivia.TriviaBeforeUrl = value; }
/// <summary>
/// Gets or sets the URL.
@@ -84,23 +87,23 @@ namespace Markdig.Syntax
/// <summary>
/// Non-normalized <see cref="Url"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice UnescapedUrl { get; set; }
public StringSlice UnescapedUrl { get => _trivia?.UnescapedUrl ?? StringSlice.Empty; set => Trivia.UnescapedUrl = value; }
/// <summary>
/// True when the <see cref="Url"/> is enclosed in point brackets in the source document.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// false.
/// </summary>
public bool UrlHasPointyBrackets { get; set; }
public bool UrlHasPointyBrackets { get => _trivia?.UrlHasPointyBrackets ?? false; set => Trivia.UrlHasPointyBrackets = value; }
/// <summary>
/// gets or sets the whitespace before a <see cref="Title"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice TriviaBeforeTitle { get; set; }
public StringSlice TriviaBeforeTitle { get => _trivia?.TriviaBeforeTitle ?? StringSlice.Empty; set => Trivia.TriviaBeforeTitle = value; }
/// <summary>
/// Gets or sets the title.
@@ -115,15 +118,15 @@ namespace Markdig.Syntax
/// <summary>
/// Non-normalized <see cref="Title"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice UnescapedTitle { get; set; }
public StringSlice UnescapedTitle { get => _trivia?.UnescapedTitle ?? StringSlice.Empty; set => Trivia.UnescapedTitle = value; }
/// <summary>
/// Gets or sets the character the <see cref="Title"/> is enclosed in.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise \0.
/// </summary>
public char TitleEnclosingCharacter { get; set; }
public char TitleEnclosingCharacter { get => _trivia?.TitleEnclosingCharacter ?? default; set => Trivia.TitleEnclosingCharacter = value; }
/// <summary>
/// Gets or sets the create link inline callback for this instance.
@@ -227,5 +230,16 @@ namespace Markdig.Syntax
};
return true;
}
private sealed class TriviaProperties
{
public StringSlice LabelWithTrivia;
public StringSlice TriviaBeforeUrl;
public StringSlice UnescapedUrl;
public bool UrlHasPointyBrackets;
public StringSlice TriviaBeforeTitle;
public StringSlice UnescapedTitle;
public char TitleEnclosingCharacter;
}
}
}

View File

@@ -13,6 +13,9 @@ namespace Markdig.Syntax
/// <seealso cref="ContainerBlock" />
public class ListItemBlock : ContainerBlock
{
private TriviaProperties? _trivia => TryGetDerivedTrivia<TriviaProperties>();
private TriviaProperties Trivia => GetOrSetDerivedTrivia<TriviaProperties>();
/// <summary>
/// Initializes a new instance of the <see cref="ListItemBlock"/> class.
/// </summary>
@@ -31,8 +34,13 @@ namespace Markdig.Syntax
/// <summary>
/// Gets or sets the bullet as parsed in the source document.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// <see cref="StringSlice.Empty"/>.
/// </summary>
public StringSlice SourceBullet { get; set; }
public StringSlice SourceBullet { get => _trivia?.SourceBullet ?? StringSlice.Empty; set => Trivia.SourceBullet = value; }
private sealed class TriviaProperties
{
public StringSlice SourceBullet;
}
}
}

View File

@@ -29,6 +29,7 @@ namespace Markdig.Syntax
{
// Inlines are processed for a paragraph
ProcessInlines = true;
IsParagraphBlock = true;
}
public int LastLine => Line + Lines.Count - 1;

View File

@@ -14,6 +14,8 @@ namespace Markdig.Syntax
/// <seealso cref="ContainerBlock" />
public class QuoteBlock : ContainerBlock
{
private List<QuoteBlockLine> Trivia => GetOrSetDerivedTrivia<List<QuoteBlockLine>>();
/// <summary>
/// Initializes a new instance of the <see cref="QuoteBlock"/> class.
/// </summary>
@@ -24,10 +26,9 @@ namespace Markdig.Syntax
/// <summary>
/// Gets or sets the trivia per line of this QuoteBlock.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="StringSlice.IsEmpty"/>.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise null.
/// </summary>
public List<QuoteBlockLine> QuoteLines { get; } = new ();
public List<QuoteBlockLine> QuoteLines => Trivia;
/// <summary>
/// Gets or sets the quote character (usually `&gt;`)
@@ -37,30 +38,23 @@ namespace Markdig.Syntax
/// <summary>
/// Represents trivia per line part of a QuoteBlock.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="QuoteBlock.QuoteLines"/> is empty.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled.
/// </summary>
public class QuoteBlockLine
{
/// <summary>
/// Gets or sets trivia occuring before the first quote character.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="QuoteBlock.QuoteLines"/> is empty.
/// </summary>
public StringSlice TriviaBefore { get; set; }
/// <summary>
/// True when this QuoteBlock line has a quote character. False when
/// this line is a "lazy line".
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="QuoteBlock.QuoteLines"/> is empty.
/// </summary>
public bool QuoteChar { get; set; }
/// <summary>
/// True if a space is parsed right after the quote character.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="QuoteBlock.QuoteLines"/> is empty.
/// </summary>
public bool HasSpaceAfterQuoteChar { get; set; }
@@ -68,15 +62,11 @@ namespace Markdig.Syntax
/// Gets or sets the trivia after the the space after the quote character.
/// The first space is assigned to <see cref="HasSpaceAfterQuoteChar"/>, subsequent
/// trivia is assigned to this property.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="QuoteBlock.QuoteLines"/> is empty.
/// </summary>
public StringSlice TriviaAfter { get; set; }
/// <summary>
/// Gets or sets the newline of this QuoeBlockLine.
/// Trivia: only parsed when <see cref="MarkdownPipeline.TrackTrivia"/> is enabled, otherwise
/// <see cref="QuoteBlock.QuoteLines"/> is empty.
/// </summary>
public NewLine NewLine { get; set; }
}