mirror of
https://github.com/xoofx/markdig.git
synced 2026-02-08 13:54:54 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccf455d316 | ||
|
|
8beb096814 | ||
|
|
6a35ec45b9 | ||
|
|
ed83943ba5 | ||
|
|
9adf60116b | ||
|
|
31904f6c53 | ||
|
|
3f3b3c46b6 | ||
|
|
f3d6c2775b | ||
|
|
bb6ace15b7 | ||
|
|
202ac1e4f9 | ||
|
|
2604239764 | ||
|
|
cc04208b95 | ||
|
|
14ab45cf8f | ||
|
|
e36d4564f1 | ||
|
|
358a5f09ef | ||
|
|
315ffd42ab | ||
|
|
2675b4dd1e | ||
|
|
58d7fae12d | ||
|
|
e16ed79dcd | ||
|
|
9ef5171369 | ||
|
|
0cfe6d7da4 | ||
|
|
fe65c1b187 | ||
|
|
92385ee19a | ||
|
|
9f651feac0 | ||
|
|
b7d02cadbb | ||
|
|
6f75b5156c | ||
|
|
61452c91e9 | ||
|
|
b697a03c2b | ||
|
|
9f734ba3c9 | ||
|
|
88cdbf3a17 | ||
|
|
fb9561cf89 | ||
|
|
9145f47f89 | ||
|
|
1862b37bbd |
189
src/Markdig.Tests/TestFastStringWriter.cs
Normal file
189
src/Markdig.Tests/TestFastStringWriter.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/Markdig.Tests/TestFencedCodeBlocks.cs
Normal file
46
src/Markdig.Tests/TestFencedCodeBlocks.cs
Normal 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	 arg´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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
102
src/Markdig.Tests/TestTransformedStringCache.cs
Normal file
102
src/Markdig.Tests/TestTransformedStringCache.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
293
src/Markdig/Helpers/FastStringWriter.cs
Normal file
293
src/Markdig/Helpers/FastStringWriter.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 2–32 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
130
src/Markdig/Helpers/TransformedStringCache.cs
Normal file
130
src/Markdig/Helpers/TransformedStringCache.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
197
src/Markdig/Helpers/ValueStringBuilder.cs
Normal file
197
src/Markdig/Helpers/ValueStringBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
17
src/Markdig/Polyfills/Unsafe.cs
Normal file
17
src/Markdig/Polyfills/Unsafe.cs
Normal 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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 '_')
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(" />");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 < and &</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("<");
|
||||
WriteRaw("<");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
break;
|
||||
case '>':
|
||||
if (!softEscape)
|
||||
{
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write(">");
|
||||
WriteRaw(">");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
}
|
||||
break;
|
||||
case '&':
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write("&");
|
||||
WriteRaw("&");
|
||||
}
|
||||
previousOffset = offset + 1;
|
||||
previousOffset = i + 1;
|
||||
break;
|
||||
case '"':
|
||||
if (!softEscape)
|
||||
{
|
||||
Write(content, previousOffset, offset - previousOffset);
|
||||
WriteRaw(content.Slice(previousOffset, i - previousOffset));
|
||||
if (EnableHtmlEscape)
|
||||
{
|
||||
Write(""");
|
||||
WriteRaw(""");
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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('!');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
```
|
||||
|
||||
7
src/Markdig/SkipLocalsInit.cs
Normal file
7
src/Markdig/SkipLocalsInit.cs
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Markdig.Syntax
|
||||
protected ContainerBlock(BlockParser? parser) : base(parser)
|
||||
{
|
||||
children = ArrayHelper.Empty<Block>();
|
||||
IsContainerBlock = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ namespace Markdig.Syntax
|
||||
{
|
||||
// Inlines are processed for a paragraph
|
||||
ProcessInlines = true;
|
||||
IsParagraphBlock = true;
|
||||
}
|
||||
|
||||
public int LastLine => Line + Lines.Count - 1;
|
||||
|
||||
@@ -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 `>`)
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user